use core::cmp::Ordering;
use sashite_sin::{Identifier, Letter, ParseError, Side};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn all_ids() -> Vec<Identifier> {
let mut ids = Vec::with_capacity(52);
for letter in Letter::ALL {
for side in [Side::First, Side::Second] {
ids.push(Identifier::new(letter, side));
}
}
ids
}
#[test]
fn flip_is_an_involution_and_changes_only_side() {
for id in all_ids() {
let flipped = id.flipped();
assert_ne!(flipped.side(), id.side());
assert_eq!(flipped.flipped(), id, "double flip must restore {id:?}");
assert_eq!(flipped.letter(), id.letter());
let expected = if id.is_first() {
id.to_char().to_ascii_lowercase()
} else {
id.to_char().to_ascii_uppercase()
};
assert_eq!(flipped.to_char(), expected);
}
assert_eq!(Side::First.flip().flip(), Side::First);
}
#[test]
fn with_setters_change_only_their_target() {
let chinese = Letter::try_from_char('C').unwrap();
for id in all_ids() {
for side in [Side::First, Side::Second] {
let changed = id.with_side(side);
assert_eq!(changed.side(), side);
assert_eq!(changed.letter(), id.letter());
}
let changed = id.with_letter(chinese);
assert_eq!(changed.letter(), chinese);
assert_eq!(changed.side(), id.side());
}
}
#[test]
fn queries_agree_with_accessors() {
for id in all_ids() {
assert_eq!(id.is_first(), id.side() == Side::First);
assert_eq!(id.is_second(), id.side() == Side::Second);
assert!(id.is_first() ^ id.is_second());
}
}
const WESTERN: Identifier = Identifier::new(Letter::ALL[22], Side::First); const WESTERN_SECOND: Identifier = WESTERN.flipped();
const CHINESE_FIRST: Identifier = WESTERN.with_letter(Letter::ALL[2]);
#[test]
fn transforms_work_in_const_context() {
assert_eq!(WESTERN.letter().as_char(), 'W');
assert_eq!(WESTERN.encode().as_str(), "W");
assert_eq!(WESTERN_SECOND.encode().as_str(), "w");
assert_eq!(CHINESE_FIRST.encode().as_str(), "C");
}
const PARSED: Result<Identifier, ParseError> = Identifier::parse("W");
const WESTERN_IS_VALID: bool = Identifier::is_valid("W");
#[test]
fn parsing_works_in_const_context() {
assert_eq!(WESTERN_IS_VALID, Identifier::is_valid("W"));
assert_eq!(PARSED, Identifier::parse("W"));
}
#[test]
fn letter_helpers() {
let upper = Letter::try_from_char('C').unwrap();
let lower = Letter::try_from_char('c').unwrap();
assert_eq!(upper, lower);
assert_eq!(upper.as_char(), 'C');
assert_eq!(upper.as_ascii(), b'C');
assert_eq!(Letter::try_from('Z').unwrap().as_char(), 'Z');
assert!(Letter::try_from('!').is_err());
let letter_a = Letter::try_from_char('A').unwrap();
assert_eq!(Letter::from_ascii(b'A'), Some((letter_a, Side::First)));
assert_eq!(
Letter::from_ascii(b'a').map(|(letter, side)| (letter.as_char(), side)),
Some(('A', Side::Second)),
);
assert_eq!(Letter::from_ascii(b'0'), None);
let spelled: String = Letter::ALL.iter().map(|letter| letter.as_char()).collect();
assert_eq!(spelled, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
#[test]
fn derived_orderings_are_canonical() {
assert!(Side::First < Side::Second);
let less = |left: &str, right: &str| {
Identifier::parse(left).unwrap() < Identifier::parse(right).unwrap()
};
assert!(less("A", "B")); assert!(less("A", "a")); assert!(!less("Z", "a")); assert!(less("a", "B"));
let a_second = Identifier::parse("a").unwrap();
let z_first = Identifier::parse("Z").unwrap();
assert_eq!(a_second.cmp(&z_first), Ordering::Less);
assert_eq!(
a_second.to_char().cmp(&z_first.to_char()),
Ordering::Greater
);
}
fn assert_letter_invariant(letter: Letter, origin: &str) {
let byte = letter.as_ascii();
assert!(
byte.is_ascii_uppercase(),
"Letter invariant broken from {origin}: byte {byte:#04X}",
);
assert_eq!(letter.as_char(), char::from(byte));
let lowered = Identifier::new(letter, Side::Second).to_char();
assert_eq!(u32::from(byte) + 32, u32::from(lowered));
assert!(lowered.is_ascii_lowercase());
assert_eq!(
Identifier::new(letter, Side::First).to_char(),
letter.as_char()
);
}
#[test]
fn every_public_constructor_upholds_the_letter_invariant() {
for letter in Letter::ALL {
assert_letter_invariant(letter, "ALL");
}
for byte in 0u8..=255 {
match Letter::from_ascii(byte) {
Some((letter, side)) => {
assert!(byte.is_ascii_alphabetic(), "byte {byte:#04X} decoded");
assert_letter_invariant(letter, "from_ascii");
let expected = if byte.is_ascii_uppercase() {
Side::First
} else {
Side::Second
};
assert_eq!(side, expected, "byte {byte:#04X}");
assert_eq!(Identifier::new(letter, side).to_char(), char::from(byte));
}
None => assert!(!byte.is_ascii_alphabetic(), "byte {byte:#04X} rejected"),
}
}
let mut accepted = 0u32;
for scalar in 0u32..=0x0010_FFFF {
let Some(c) = char::from_u32(scalar) else {
continue;
};
assert_eq!(
Letter::try_from(c),
Letter::try_from_char(c),
"U+{scalar:04X}"
);
match Letter::try_from_char(c) {
Ok(letter) => {
assert!(c.is_ascii_alphabetic(), "char U+{scalar:04X} accepted");
assert_letter_invariant(letter, "try_from_char");
assert_eq!(letter.as_char(), c.to_ascii_uppercase());
accepted += 1;
}
Err(e) => {
assert!(!c.is_ascii_alphabetic(), "char U+{scalar:04X} rejected");
assert_eq!(e, ParseError::InvalidLetter, "U+{scalar:04X}");
}
}
}
assert_eq!(
accepted, 52,
"exactly the 52 ASCII letters are abbreviations"
);
for byte in 0u8..=255 {
let buf = [byte];
if let Ok(id) = Identifier::try_from(&buf[..]) {
assert_letter_invariant(id.letter(), "Identifier::try_from(&[u8])");
}
}
}
#[test]
fn encoding_round_trips_over_the_whole_product() {
for id in all_ids() {
let encoded = id.encode();
assert_eq!(Identifier::parse(&encoded).unwrap(), id);
assert_eq!(
Identifier::try_from(encoded.as_str().as_bytes()).unwrap(),
id
);
let text = encoded.as_str().to_owned();
assert_eq!(Identifier::parse(&text).unwrap().encode().as_str(), text);
assert_eq!(id.to_char().to_string(), text);
assert_eq!(id.to_string(), text);
assert_eq!(format!("{id}"), text);
assert_eq!(encoded.as_str(), text);
assert_eq!(id.to_char().is_ascii_uppercase(), id.is_first());
assert_eq!(id.to_char().to_ascii_uppercase(), id.letter().as_char());
}
}
#[test]
fn setters_compose_over_the_whole_product() {
for id in all_ids() {
assert_eq!(id.with_letter(id.letter()), id);
assert_eq!(id.with_side(id.side()), id);
for letter in Letter::ALL {
for side in [Side::First, Side::Second] {
let target = Identifier::new(letter, side);
assert_eq!(id.with_letter(letter).with_side(side), target);
assert_eq!(id.with_side(side).with_letter(letter), target);
assert_eq!(
id.with_letter(letter).with_letter(letter),
id.with_letter(letter)
);
assert_eq!(id.with_side(side).with_side(side), id.with_side(side));
}
}
assert_eq!(id.flipped(), id.with_side(id.side().flip()));
}
}
fn hash_of(id: Identifier) -> u64 {
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
}
#[test]
fn ord_partial_ord_eq_and_hash_are_mutually_consistent() {
let ids = all_ids();
for &left in &ids {
for &right in &ids {
let by_fields = (left.letter(), left.side()).cmp(&(right.letter(), right.side()));
assert_eq!(left.cmp(&right), by_fields, "{left:?} vs {right:?}");
assert_eq!(left.partial_cmp(&right), Some(left.cmp(&right)));
assert_eq!(left == right, left.cmp(&right) == Ordering::Equal);
assert_eq!(left < right, left.cmp(&right) == Ordering::Less);
assert_eq!(left.cmp(&right), right.cmp(&left).reverse());
if left == right {
assert_eq!(hash_of(left), hash_of(right));
}
}
}
for &a in &ids {
for &b in &ids {
for &c in &ids {
if a <= b && b <= c {
assert!(a <= c, "{a:?} <= {b:?} <= {c:?}");
}
}
}
}
for (i, left) in Letter::ALL.iter().enumerate() {
for (j, right) in Letter::ALL.iter().enumerate() {
assert_eq!(left.cmp(right), i.cmp(&j));
}
}
}