use crate::generated::enums::WrittenUnit;
use crate::normalize::{is_joiner, slot_position};
use crate::tables::Position;
fn expansion(unit: WrittenUnit, position: Position) -> Option<&'static [WrittenUnit]> {
match (unit, position) {
(WrittenUnit::Dd, _) => Some(&[WrittenUnit::O, WrittenUnit::A]),
(WrittenUnit::H, Position::Medi) => Some(&[WrittenUnit::A, WrittenUnit::A]),
(WrittenUnit::Hx, Position::Medi) => Some(&[WrittenUnit::N, WrittenUnit::N]),
(WrittenUnit::Cr, Position::Init) => Some(&[WrittenUnit::O, WrittenUnit::O]),
_ => None,
}
}
fn contraction(
first: WrittenUnit,
second: WrittenUnit,
result: Position,
preceding: Option<WrittenUnit>,
) -> Option<WrittenUnit> {
if second != WrittenUnit::Aa {
return None;
}
match (first, result) {
(WrittenUnit::A, Position::Isol) => Some(WrittenUnit::A),
(WrittenUnit::A, Position::Fina)
if matches!(
preceding,
Some(
WrittenUnit::B
| WrittenUnit::P
| WrittenUnit::F
| WrittenUnit::G
| WrittenUnit::Gx
| WrittenUnit::K
| WrittenUnit::K2
)
) =>
{
Some(WrittenUnit::Aa)
}
(WrittenUnit::O, Position::Fina) => Some(WrittenUnit::B2),
(WrittenUnit::I, Position::Fina) => Some(WrittenUnit::G),
_ => None,
}
}
pub(crate) fn collapse(shape: &[WrittenUnit]) -> Vec<WrittenUnit> {
let mut out = Vec::with_capacity(shape.len() + 2);
let mut start = 0;
while start < shape.len() {
if shape[start].is_structural() {
out.push(shape[start]);
start += 1;
continue;
}
let mut end = start;
while end < shape.len() && !shape[end].is_structural() {
end += 1;
}
let pad_left = usize::from(start > 0 && is_joiner(shape[start - 1]));
let pad_right = usize::from(end < shape.len() && is_joiner(shape[end]));
out.extend(collapse_chain(&shape[start..end], pad_left, pad_right));
start = end;
}
out
}
fn collapse_chain(chain: &[WrittenUnit], pad_left: usize, pad_right: usize) -> Vec<WrittenUnit> {
let position =
|index: usize, len: usize| slot_position(index + pad_left, 1, len + pad_left + pad_right);
let mut units = Vec::with_capacity(chain.len() + 2);
for (index, &unit) in chain.iter().enumerate() {
match expansion(unit, position(index, chain.len())) {
Some(expanded) => units.extend_from_slice(expanded),
None => units.push(unit),
}
}
if let Some(index) = units.len().checked_sub(2) {
if let Some(merged) = contraction(
units[index],
units[index + 1],
position(index, units.len() - 1),
index.checked_sub(1).map(|previous| units[previous]),
) {
units[index] = merged;
units.pop();
}
}
units
}
#[cfg(test)]
mod tests {
use super::*;
use WrittenUnit::{
Aa, Cr, Dd, Hx, Mvs, Nirugu, Zwj, A, B, B2, D, G, H, I, L, M, N, O, R, S, T,
};
#[test]
fn the_five_expanding_duplicates_collapse_by_position() {
assert_eq!(collapse(&[A, A, R, A, Dd]), [A, A, R, A, O, A]); assert_eq!(collapse(&[A, O, Dd, B, O]), [A, O, O, A, B, O]); assert_eq!(collapse(&[B, A, H, S, A]), [B, A, A, A, S, A]); assert_eq!(collapse(&[A, A, R, Hx, A, L]), [A, A, R, N, N, A, L]); assert_eq!(collapse(&[Cr, Nirugu]), [O, O, Nirugu]); }
#[test]
fn the_four_contracting_duplicates_collapse_by_position() {
assert_eq!(collapse(&[A, Aa]), [A]); assert_eq!(collapse(&[B, A, Aa]), [B, Aa]); assert_eq!(collapse(&[Nirugu, O, Aa]), [Nirugu, B2]); assert_eq!(collapse(&[Nirugu, I, Aa]), [Nirugu, G]); }
#[test]
fn contraction_needs_the_position_the_pair_was_verified_in() {
assert_eq!(collapse(&[O, Aa]), [O, Aa]);
assert_eq!(collapse(&[I, Aa]), [I, Aa]);
assert_eq!(collapse(&[A, Aa, Zwj]), [A, Aa, Zwj]);
assert_eq!(collapse(&[Aa]), [Aa]);
assert_eq!(collapse(&[Mvs, Aa]), [Mvs, Aa]);
assert_eq!(collapse(&[Cr]), [Cr]);
}
#[test]
fn initial_and_final_h_and_hx_are_distinct_ink_and_stay() {
assert_eq!(collapse(&[H, O, D, A]), [H, O, D, A]); assert_eq!(collapse(&[T, A, H]), [T, A, H]); assert_eq!(collapse(&[Hx, A]), [Hx, A]); assert_eq!(collapse(&[A, N, A, Hx]), [A, N, A, Hx]); }
#[test]
fn structural_units_split_chains_and_joiners_pad_them() {
assert_eq!(collapse(&[T, A, H, Mvs, Aa]), [T, A, H, Mvs, Aa]);
assert_eq!(collapse(&[T, A, H, Zwj]), [T, A, A, A, Zwj]);
assert_eq!(collapse(&[Nirugu, H, A]), [Nirugu, A, A, A]);
assert_eq!(collapse(&[M, O, Dd, Mvs, Aa]), [M, O, O, A, Mvs, Aa]);
assert_eq!(collapse(&[B, A, Aa, Mvs, A, Aa]), [B, Aa, Mvs, A]);
}
#[test]
fn final_a_aa_requires_an_immediately_preceding_bowed_unit() {
use WrittenUnit::{Gx, F, K, K2, P};
for bowed_unit in [B, P, F, G, Gx, K, K2] {
assert_eq!(collapse(&[bowed_unit, A, Aa]), [bowed_unit, Aa]);
assert_eq!(collapse(&[N, bowed_unit, A, Aa]), [N, bowed_unit, Aa]);
assert_eq!(collapse(&[bowed_unit, A, A, Aa]), [bowed_unit, A, A, Aa]);
}
for non_bowed_unit in [A, N, O, I, B2, D, R] {
assert_eq!(collapse(&[non_bowed_unit, A, Aa]), [non_bowed_unit, A, Aa]);
}
assert_eq!(collapse(&[B, Zwj, A, Aa]), [B, Zwj, A, Aa]);
assert_eq!(collapse(&[Nirugu, A, Aa]), [Nirugu, A, Aa]);
assert_eq!(collapse(&[B, A, Aa, Zwj]), [B, A, Aa, Zwj]);
assert_eq!(collapse(&[B, H, Aa]), [B, A, A, Aa]);
assert_eq!(collapse(&[B, O, Dd, Aa]), [B, O, O, A, Aa]);
}
#[test]
fn collapse_is_idempotent_over_every_short_sequence() {
use WrittenUnit::{Gx, F, K, K2, P};
const ALPHABET: [WrittenUnit; 20] = [
A, Aa, O, I, N, B, P, F, Gx, K, K2, B2, G, Dd, H, Hx, Cr, Nirugu, Zwj, Mvs,
];
for length in 0..=4u32 {
for index in 0..ALPHABET.len().pow(length) {
let mut rest = index;
let shape: Vec<WrittenUnit> = (0..length)
.map(|_| {
let unit = ALPHABET[rest % ALPHABET.len()];
rest /= ALPHABET.len();
unit
})
.collect();
let once = collapse(&shape);
assert_eq!(
collapse(&once),
once,
"not idempotent: {shape:?} -> {once:?}"
);
assert!(once.len() <= 2 * shape.len(), "runaway growth: {shape:?}");
assert!(
!once.contains(&Dd),
"Dd survived the collapse: {shape:?} -> {once:?}"
);
}
}
}
#[test]
fn idempotent_and_identity_elsewhere() {
for shape in [
vec![A, A, R, Hx, A, L],
vec![A, A, R, A, Dd],
vec![Cr, Nirugu],
vec![A, Aa],
vec![B, A, A, Aa],
vec![Nirugu, O, Aa],
vec![Nirugu, I, Aa],
vec![B, O, Dd, Aa],
vec![T, A, H, Zwj],
] {
let once = collapse(&shape);
assert_eq!(collapse(&once), once, "not idempotent: {shape:?}");
}
assert_eq!(collapse(&[S, A, G, A]), [S, A, G, A]);
assert_eq!(collapse(&[]), []);
assert_eq!(collapse(&[Mvs]), [Mvs]);
}
}