1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
//! 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.
///
/// # Format specifiers are deliberately ignored
///
/// Unlike this family's short-token crates — where `Display` routes through
/// [`core::fmt::Formatter::pad`] so width, fill, alignment and precision
/// behave as they do for [`str`] — a width here would have to be measured
/// against the *finished* string, and a FEEN string runs to
/// [`MAX_STRING_LENGTH`](crate::MAX_STRING_LENGTH) (4 KiB). Buffering that
/// on the stack to satisfy a `{:>80}` would cost 4 KiB of stack in a crate
/// whose whole point is to run on bare metal without an allocator, so the
/// output is written straight to the sink and the spec is ignored. Callers
/// needing a padded FEEN string can `to_string()` it first (with `alloc`)
/// and pad the `String`.
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::error::EncodeError;
use crate::hands::token_key;
use crate::limits::MAX_STRING_LENGTH;
/// 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())` is
/// `Ok(s)`. Available with the `alloc` feature.
///
/// # Errors
///
/// `Qi` is a more permissive model than FEEN — it admits geometries FEEN
/// cannot spell and sizes this crate will not read back — so the conversion
/// is fallible; see [`EncodeError`]. **Whatever this returns as `Ok`,
/// [`Feen::parse`](crate::Feen::parse) accepts**, and parsing it back yields
/// the position it started from. A position that came from
/// [`Feen::to_qi`](crate::Feen::to_qi) never fails.
pub fn encode(position: &Qi<Piece, Style>) -> Result<String, EncodeError> {
let mut out = String::new();
write_feen(&mut out, position)?;
Ok(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
///
/// Returns [`EncodeError::ShapeNotRepresentable`] or
/// [`EncodeError::TooLong`] *before writing anything*, so a rejected
/// position leaves the sink untouched, and [`EncodeError::Sink`] if the sink
/// itself fails part-way through.
pub fn write_feen<W: Write>(w: &mut W, position: &Qi<Piece, Style>) -> Result<(), EncodeError> {
check_representable(position)?;
// The length is measured by running the very same writer into a counting
// sink, so the figure checked is by construction the figure produced —
// no second, drift-prone size formula. It also means the sink sees
// nothing at all when the position is over the cap.
let mut counter = Counter(0);
// `Counter` never fails, so this cannot be `Err`.
let _ = write_body(&mut counter, position);
if counter.0 > MAX_STRING_LENGTH {
return Err(EncodeError::TooLong);
}
write_body(w, position).map_err(|_: fmt::Error| EncodeError::Sink)
}
/// Rejects a geometry FEEN has no spelling for.
///
/// FEEN encodes dimensionality through separator *depth*, so a dimension is
/// only visible if it is actually split: every dimension but the innermost
/// must hold at least two elements. A single-element outer dimension would
/// either vanish from the output (silently changing the shape) or force a
/// `//` between structures containing no `/`, which the specification's
/// dimensional-coherence rule forbids — and which this crate's own parser
/// rejects.
fn check_representable(position: &Qi<Piece, Style>) -> Result<(), EncodeError> {
// `split_last` rather than `[..len - 1]`: no indexing, no subtraction,
// and an empty shape (which `Qi::new` refuses to build) is handled.
let Some((_innermost, outer)) = position.shape().split_last() else {
return Err(EncodeError::ShapeNotRepresentable);
};
if outer.iter().any(|&size| size < 2) {
return Err(EncodeError::ShapeNotRepresentable);
}
Ok(())
}
/// A [`fmt::Write`] sink that counts the bytes written and discards them.
struct Counter(usize);
impl Write for Counter {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.0 += s.len();
Ok(())
}
}
/// Writes the three fields, with no representability or length checking.
fn write_body<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).
///
/// Only ever called after [`check_representable`], so every dimension but the
/// innermost holds at least two elements. That is what makes the two strides
/// distinct: with a single-rank layer, `layer_stride` would equal `files` and
/// **every** rank boundary would be written as `//`.
fn write_placement<W: Write>(w: &mut W, position: &Qi<Piece, Style>) -> fmt::Result {
let dims = position.shape();
let ndim = dims.len();
let Some((&files, _)) = dims.split_last() else {
return Ok(()); // unreachable: `Qi` has at least one dimension
};
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};