sashite-sin 1.0.0

Style Identifier Notation (SIN): a compact, ASCII-only, no_std token encoding a player's side and style in abstract strategy board games.
Documentation
//! Transformation, query, and ordering tests.
//!
//! These exercise the algebra of the builder-style methods on [`Identifier`]
//! (each returns a new value; the type is `Copy`) over the whole closed domain,
//! plus the [`Letter`] helpers and the documented total orderings.

use sashite_sin::{Identifier, Letter, Side};

/// Builds all 52 identifiers directly from their typed components.
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:?}");
        // The letter (the only other attribute) is preserved.
        assert_eq!(flipped.letter(), id.letter());

        // Flipping toggles the case of the rendered character.
        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);
    }
    // Side's own involution.
    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);

        // Exactly one side-query holds.
        assert!(id.is_first() ^ id.is_second());
    }
}

// The const fns are usable in const context: these are evaluated at compile time.
const WESTERN: Identifier = Identifier::new(Letter::ALL[22], Side::First); // 'W'
const WESTERN_SECOND: Identifier = WESTERN.flipped();
const CHINESE_FIRST: Identifier = WESTERN.with_letter(Letter::ALL[2]); // 'C'

#[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");
}

// Parsing and validation are const fns too: usable at compile time.
const PARSED: Result<Identifier, sashite_sin::ParseError> = Identifier::parse("W");
const WESTERN_IS_VALID: bool = Identifier::is_valid("W");

#[test]
fn parsing_works_in_const_context() {
    // Each const is evaluated at compile time; comparing against a runtime call
    // both consumes it and confirms const and runtime parsing agree.
    assert_eq!(WESTERN_IS_VALID, Identifier::is_valid("W"));
    assert_eq!(PARSED, Identifier::parse("W"));
}

#[test]
fn letter_helpers() {
    // Case folding: both cases yield the same uppercase letter.
    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');

    // TryFrom<char>.
    assert_eq!(Letter::try_from('Z').unwrap().as_char(), 'Z');
    assert!(Letter::try_from('!').is_err());

    // from_ascii reports the side implied by case.
    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);

    // ALL spans A..=Z in order.
    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);

    // Identifier compares by letter, then side.
    let less = |left: &str, right: &str| {
        Identifier::parse(left).unwrap() < Identifier::parse(right).unwrap()
    };
    assert!(less("A", "B")); // letter
    assert!(less("A", "a")); // same letter: First < Second
    assert!(!less("Z", "a")); // 'Z' > 'A': the letter dominates over side
    assert!(less("a", "B")); // letter 'A' < 'B' regardless of side
}