Skip to main content

sashite_feen/
encode.rs

1//! Canonical serialization back to a FEEN string.
2//!
3//! Two entry points produce the same canonical bytes:
4//!
5//! - [`Display`](core::fmt::Display) for [`Feen`] echoes the already-validated
6//!   placement and hands fields and rebuilds the style–turn field. It never
7//!   allocates and needs no `alloc`.
8//! - With the `alloc` feature, [`encode`] (and the sink-based [`write_feen`])
9//!   serialize an owned [`sashite_qi::Qi`] position, re-deriving the placement
10//!   (run-length empties, dimensional separators) and re-sorting each hand into
11//!   canonical order — `Qi` stores hands keyed by piece, not by the FEEN order.
12
13use core::fmt;
14
15use crate::feen::Feen;
16
17impl fmt::Display for Feen<'_> {
18    /// Writes the canonical FEEN string for this position.
19    ///
20    /// The placement and hands fields were validated as canonical when the view
21    /// was parsed, so they are echoed verbatim; the style–turn field is rebuilt
22    /// from the two style tokens. The output is written straight to the
23    /// formatter, so this never allocates and works under `no_std` without the
24    /// `alloc` feature (use `write!`/a sink); with `alloc`, `to_string` follows
25    /// from this impl.
26    ///
27    /// Parsing then displaying a canonical FEEN string reproduces it exactly.
28    ///
29    /// # Format specifiers are deliberately ignored
30    ///
31    /// Unlike this family's short-token crates — where `Display` routes through
32    /// [`core::fmt::Formatter::pad`] so width, fill, alignment and precision
33    /// behave as they do for [`str`] — a width here would have to be measured
34    /// against the *finished* string, and a FEEN string runs to
35    /// [`MAX_STRING_LENGTH`](crate::MAX_STRING_LENGTH) (4 KiB). Buffering that
36    /// on the stack to satisfy a `{:>80}` would cost 4 KiB of stack in a crate
37    /// whose whole point is to run on bare metal without an allocator, so the
38    /// output is written straight to the sink and the spec is ignored. Callers
39    /// needing a padded FEEN string can `to_string()` it first (with `alloc`)
40    /// and pad the `String`.
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        // The bytes were validated as ASCII, so `from_utf8` cannot fail here; the
43        // mapping to `fmt::Error` keeps the impl total without `unsafe`.
44        let placement = core::str::from_utf8(self.placement_field()).map_err(|_| fmt::Error)?;
45        let hands = core::str::from_utf8(self.hands_field()).map_err(|_| fmt::Error)?;
46
47        write!(
48            f,
49            "{placement} {hands} {}/{}",
50            self.active_style().to_char(),
51            self.inactive_style().to_char(),
52        )
53    }
54}
55
56#[cfg(feature = "alloc")]
57mod owned {
58    use alloc::string::String;
59    use alloc::vec::Vec;
60    use core::cmp::Reverse;
61    use core::fmt::{self, Write};
62
63    use sashite_epin::Identifier as Piece;
64    use sashite_qi::{Player, Qi};
65    use sashite_sin::Identifier as Style;
66
67    use crate::error::EncodeError;
68    use crate::hands::token_key;
69    use crate::limits::MAX_STRING_LENGTH;
70
71    /// Encodes an owned [`Qi`] position as its canonical FEEN string.
72    ///
73    /// This is the inverse of [`Feen::to_qi`](crate::Feen::to_qi): for any
74    /// canonical FEEN string `s`, `encode(&Feen::parse(s)?.to_qi())` is
75    /// `Ok(s)`. Available with the `alloc` feature.
76    ///
77    /// # Errors
78    ///
79    /// `Qi` is a more permissive model than FEEN — it admits geometries FEEN
80    /// cannot spell and sizes this crate will not read back — so the conversion
81    /// is fallible; see [`EncodeError`]. **Whatever this returns as `Ok`,
82    /// [`Feen::parse`](crate::Feen::parse) accepts**, and parsing it back yields
83    /// the position it started from. A position that came from
84    /// [`Feen::to_qi`](crate::Feen::to_qi) never fails.
85    pub fn encode(position: &Qi<Piece, Style>) -> Result<String, EncodeError> {
86        let mut out = String::new();
87        write_feen(&mut out, position)?;
88        Ok(out)
89    }
90
91    /// Writes the canonical FEEN string for `position` to a [`fmt::Write`] sink.
92    ///
93    /// Like [`encode`] but streams to an existing sink instead of returning a
94    /// new string. Available with the `alloc` feature.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`EncodeError::ShapeNotRepresentable`] or
99    /// [`EncodeError::TooLong`] *before writing anything*, so a rejected
100    /// position leaves the sink untouched, and [`EncodeError::Sink`] if the sink
101    /// itself fails part-way through.
102    pub fn write_feen<W: Write>(w: &mut W, position: &Qi<Piece, Style>) -> Result<(), EncodeError> {
103        check_representable(position)?;
104        // The length is measured by running the very same writer into a counting
105        // sink, so the figure checked is by construction the figure produced —
106        // no second, drift-prone size formula. It also means the sink sees
107        // nothing at all when the position is over the cap.
108        let mut counter = Counter(0);
109        // `Counter` never fails, so this cannot be `Err`.
110        let _ = write_body(&mut counter, position);
111        if counter.0 > MAX_STRING_LENGTH {
112            return Err(EncodeError::TooLong);
113        }
114
115        write_body(w, position).map_err(|_: fmt::Error| EncodeError::Sink)
116    }
117
118    /// Rejects a geometry FEEN has no spelling for.
119    ///
120    /// FEEN encodes dimensionality through separator *depth*, so a dimension is
121    /// only visible if it is actually split: every dimension but the innermost
122    /// must hold at least two elements. A single-element outer dimension would
123    /// either vanish from the output (silently changing the shape) or force a
124    /// `//` between structures containing no `/`, which the specification's
125    /// dimensional-coherence rule forbids — and which this crate's own parser
126    /// rejects.
127    fn check_representable(position: &Qi<Piece, Style>) -> Result<(), EncodeError> {
128        // `split_last` rather than `[..len - 1]`: no indexing, no subtraction,
129        // and an empty shape (which `Qi::new` refuses to build) is handled.
130        let Some((_innermost, outer)) = position.shape().split_last() else {
131            return Err(EncodeError::ShapeNotRepresentable);
132        };
133        if outer.iter().any(|&size| size < 2) {
134            return Err(EncodeError::ShapeNotRepresentable);
135        }
136        Ok(())
137    }
138
139    /// A [`fmt::Write`] sink that counts the bytes written and discards them.
140    struct Counter(usize);
141
142    impl Write for Counter {
143        fn write_str(&mut self, s: &str) -> fmt::Result {
144            self.0 += s.len();
145            Ok(())
146        }
147    }
148
149    /// Writes the three fields, with no representability or length checking.
150    fn write_body<W: Write>(w: &mut W, position: &Qi<Piece, Style>) -> fmt::Result {
151        write_placement(w, position)?;
152        w.write_str(" ")?;
153        write_hand(w, position.first_hand())?;
154        w.write_str("/")?;
155        write_hand(w, position.second_hand())?;
156
157        // The active style is written first; its case already encodes its side.
158        let (active, inactive) = match position.turn() {
159            Player::First => (position.first_style(), position.second_style()),
160            Player::Second => (position.second_style(), position.first_style()),
161        };
162        write!(w, " {}/{}", active.to_char(), inactive.to_char())
163    }
164
165    /// Re-encodes the board: run-length empties, with a separator after each rank
166    /// whose depth follows the dimensional carry (`//` at a 3-D layer boundary).
167    ///
168    /// Only ever called after [`check_representable`], so every dimension but the
169    /// innermost holds at least two elements. That is what makes the two strides
170    /// distinct: with a single-rank layer, `layer_stride` would equal `files` and
171    /// **every** rank boundary would be written as `//`.
172    fn write_placement<W: Write>(w: &mut W, position: &Qi<Piece, Style>) -> fmt::Result {
173        let dims = position.shape();
174        let ndim = dims.len();
175        let Some((&files, _)) = dims.split_last() else {
176            return Ok(()); // unreachable: `Qi` has at least one dimension
177        };
178        let total = position.square_count();
179        // Stride at which a rank boundary is also a layer boundary (3-D only).
180        let layer_stride = if ndim >= 3 { files * dims[ndim - 2] } else { 0 };
181
182        let mut empty_run: usize = 0;
183        let mut emitted: usize = 0;
184        for cell in position.board() {
185            match cell {
186                Some(piece) => {
187                    flush_empties(w, &mut empty_run)?;
188                    w.write_str(piece.encode().as_str())?;
189                }
190                None => empty_run += 1,
191            }
192            emitted += 1;
193
194            if emitted % files == 0 {
195                flush_empties(w, &mut empty_run)?;
196                if emitted < total {
197                    if ndim >= 3 && emitted % layer_stride == 0 {
198                        w.write_str("//")?;
199                    } else {
200                        w.write_str("/")?;
201                    }
202                }
203            }
204        }
205        Ok(())
206    }
207
208    /// Emits a pending empty-count token, if any, and resets the counter.
209    fn flush_empties<W: Write>(w: &mut W, empty_run: &mut usize) -> fmt::Result {
210        if *empty_run > 0 {
211            write!(w, "{empty_run}")?;
212            *empty_run = 0;
213        }
214        Ok(())
215    }
216
217    /// Re-sorts one hand into FEEN canonical order and serializes its items.
218    ///
219    /// `Qi` yields hand items keyed by piece; FEEN orders them by multiplicity
220    /// (descending) first, then by the token comparator, so the items are
221    /// collected and sorted before being written.
222    fn write_hand<'a, W: Write>(
223        w: &mut W,
224        hand: impl Iterator<Item = (&'a Piece, usize)>,
225    ) -> fmt::Result {
226        let mut items: Vec<(Piece, usize)> = hand.map(|(piece, count)| (*piece, count)).collect();
227        items.sort_unstable_by_key(|&(piece, count)| {
228            let (letter, case, state, terminal, derived) = token_key(piece);
229            (Reverse(count), letter, case, state, terminal, derived)
230        });
231
232        for (piece, count) in items {
233            if count > 1 {
234                write!(w, "{count}")?;
235            }
236            w.write_str(piece.encode().as_str())?;
237        }
238        Ok(())
239    }
240}
241
242#[cfg(feature = "alloc")]
243pub use owned::{encode, write_feen};