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};