sashite-feen 0.1.0

Field Expression Encoding Notation (FEEN): a compact, ASCII-only, no_std, zero-allocation validator and encoder for board-game positions in abstract strategy games, built on EPIN and SIN.
Documentation
//! Canonical serialization back to a FEEN string.
//!
//! Two entry points produce the same canonical bytes:
//!
//! - [`Display`](core::fmt::Display) for [`Feen`] echoes the already-validated
//!   placement and hands fields and rebuilds the style–turn field. It never
//!   allocates and needs no `alloc`.
//! - With the `alloc` feature, [`encode`] (and the sink-based [`write_feen`])
//!   serialize an owned [`sashite_qi::Qi`] position, re-deriving the placement
//!   (run-length empties, dimensional separators) and re-sorting each hand into
//!   canonical order — `Qi` stores hands keyed by piece, not by the FEEN order.

use core::fmt;

use crate::feen::Feen;

impl fmt::Display for Feen<'_> {
    /// Writes the canonical FEEN string for this position.
    ///
    /// The placement and hands fields were validated as canonical when the view
    /// was parsed, so they are echoed verbatim; the style–turn field is rebuilt
    /// from the two style tokens. The output is written straight to the
    /// formatter, so this never allocates and works under `no_std` without the
    /// `alloc` feature (use `write!`/a sink); with `alloc`, `to_string` follows
    /// from this impl.
    ///
    /// Parsing then displaying a canonical FEEN string reproduces it exactly.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // The bytes were validated as ASCII, so `from_utf8` cannot fail here; the
        // mapping to `fmt::Error` keeps the impl total without `unsafe`.
        let placement = core::str::from_utf8(self.placement_field()).map_err(|_| fmt::Error)?;
        let hands = core::str::from_utf8(self.hands_field()).map_err(|_| fmt::Error)?;

        write!(
            f,
            "{placement} {hands} {}/{}",
            self.active_style().to_char(),
            self.inactive_style().to_char(),
        )
    }
}

#[cfg(feature = "alloc")]
mod owned {
    use alloc::string::String;
    use alloc::vec::Vec;
    use core::cmp::Reverse;
    use core::fmt::{self, Write};

    use sashite_epin::Identifier as Piece;
    use sashite_qi::{Player, Qi};
    use sashite_sin::Identifier as Style;

    use crate::hands::token_key;

    /// Encodes an owned [`Qi`] position as its canonical FEEN string.
    ///
    /// This is the inverse of [`Feen::to_qi`](crate::Feen::to_qi): for any
    /// canonical FEEN string `s`, `encode(&Feen::parse(s)?.to_qi())` equals `s`.
    /// Available with the `alloc` feature.
    #[must_use]
    pub fn encode(position: &Qi<Piece, Style>) -> String {
        let mut out = String::new();
        // Writing to a `String` is infallible; the `fmt::Result` cannot be `Err`.
        let _ = write_feen(&mut out, position);
        out
    }

    /// Writes the canonical FEEN string for `position` to a [`fmt::Write`] sink.
    ///
    /// Like [`encode`] but streams to an existing sink instead of returning a
    /// new string. Available with the `alloc` feature.
    ///
    /// # Errors
    ///
    /// Propagates any error returned by the sink `w`.
    pub fn write_feen<W: Write>(w: &mut W, position: &Qi<Piece, Style>) -> fmt::Result {
        write_placement(w, position)?;
        w.write_str(" ")?;
        write_hand(w, position.first_hand())?;
        w.write_str("/")?;
        write_hand(w, position.second_hand())?;

        // The active style is written first; its case already encodes its side.
        let (active, inactive) = match position.turn() {
            Player::First => (position.first_style(), position.second_style()),
            Player::Second => (position.second_style(), position.first_style()),
        };
        write!(w, " {}/{}", active.to_char(), inactive.to_char())
    }

    /// Re-encodes the board: run-length empties, with a separator after each rank
    /// whose depth follows the dimensional carry (`//` at a 3-D layer boundary).
    fn write_placement<W: Write>(w: &mut W, position: &Qi<Piece, Style>) -> fmt::Result {
        let dims = position.shape();
        let ndim = dims.len();
        let files = dims[ndim - 1]; // innermost dimension; always >= 1
        let total = position.square_count();
        // Stride at which a rank boundary is also a layer boundary (3-D only).
        let layer_stride = if ndim >= 3 { files * dims[ndim - 2] } else { 0 };

        let mut empty_run: usize = 0;
        let mut emitted: usize = 0;
        for cell in position.board() {
            match cell {
                Some(piece) => {
                    flush_empties(w, &mut empty_run)?;
                    w.write_str(piece.encode().as_str())?;
                }
                None => empty_run += 1,
            }
            emitted += 1;

            if emitted % files == 0 {
                flush_empties(w, &mut empty_run)?;
                if emitted < total {
                    if ndim >= 3 && emitted % layer_stride == 0 {
                        w.write_str("//")?;
                    } else {
                        w.write_str("/")?;
                    }
                }
            }
        }
        Ok(())
    }

    /// Emits a pending empty-count token, if any, and resets the counter.
    fn flush_empties<W: Write>(w: &mut W, empty_run: &mut usize) -> fmt::Result {
        if *empty_run > 0 {
            write!(w, "{empty_run}")?;
            *empty_run = 0;
        }
        Ok(())
    }

    /// Re-sorts one hand into FEEN canonical order and serializes its items.
    ///
    /// `Qi` yields hand items keyed by piece; FEEN orders them by multiplicity
    /// (descending) first, then by the token comparator, so the items are
    /// collected and sorted before being written.
    fn write_hand<'a, W: Write>(
        w: &mut W,
        hand: impl Iterator<Item = (&'a Piece, usize)>,
    ) -> fmt::Result {
        let mut items: Vec<(Piece, usize)> = hand.map(|(piece, count)| (*piece, count)).collect();
        items.sort_unstable_by_key(|&(piece, count)| {
            let (letter, case, state, terminal, derived) = token_key(piece);
            (Reverse(count), letter, case, state, terminal, derived)
        });

        for (piece, count) in items {
            if count > 1 {
                write!(w, "{count}")?;
            }
            w.write_str(piece.encode().as_str())?;
        }
        Ok(())
    }
}

#[cfg(feature = "alloc")]
pub use owned::{encode, write_feen};