use crate::error::ParseError;
use crate::limits::{MAX_DIMENSIONS, MAX_DIMENSION_SIZE};
use crate::shape::Shape;
use crate::token::epin_token;
pub(crate) fn validate(field: &[u8]) -> Result<(Shape, u32), ParseError> {
if field.is_empty() {
return Err(ParseError::PlacementEmpty);
}
if field[0] == b'/' {
return Err(ParseError::PlacementStartsWithSeparator);
}
if field[field.len() - 1] == b'/' {
return Err(ParseError::PlacementEndsWithSeparator);
}
let mut i = 0usize;
let mut piece_count: u32 = 0;
let mut s1: Option<u32> = None;
let mut ranks_in_layer: u32 = 0;
let mut s2: Option<u32> = None;
let mut completed_layers: u32 = 0;
let mut max_sep: usize = 0;
loop {
let cells = read_segment(field, &mut i, &mut piece_count)?;
match s1 {
None => s1 = Some(cells),
Some(v) if v != cells => return Err(ParseError::BoardNotRegular),
Some(_) => {}
}
ranks_in_layer += 1;
if i == field.len() {
break;
}
let group_start = i;
while i < field.len() && field[i] == b'/' {
i += 1;
}
let group_len = i - group_start;
if group_len > MAX_DIMENSIONS - 1 {
return Err(ParseError::TooManyDimensions);
}
if group_len > max_sep {
max_sep = group_len;
}
if group_len == 2 {
if ranks_in_layer < 2 {
return Err(ParseError::DimensionalCoherence);
}
match s2 {
None => s2 = Some(ranks_in_layer),
Some(v) if v != ranks_in_layer => return Err(ParseError::BoardNotRegular),
Some(_) => {}
}
completed_layers += 1;
ranks_in_layer = 0;
}
}
let Some(s1) = s1 else {
return Err(ParseError::PlacementEmpty);
};
let (sizes, ndim) = finalize(max_sep, s1, ranks_in_layer, s2, completed_layers)?;
Ok((Shape::from_sizes(sizes, ndim), piece_count))
}
fn finalize(
max_sep: usize,
s1: u32,
ranks_in_layer: u32,
s2: Option<u32>,
completed_layers: u32,
) -> Result<([u8; MAX_DIMENSIONS], u8), ParseError> {
let files = to_dim(s1)?;
match max_sep {
0 => Ok(([files, 0, 0], 1)),
1 => {
let ranks = to_dim(ranks_in_layer)?;
Ok(([ranks, files, 0], 2))
}
2 => {
let Some(ranks_per_layer) = s2 else {
return Err(ParseError::DimensionalCoherence);
};
if ranks_in_layer < 2 {
return Err(ParseError::DimensionalCoherence);
}
if ranks_in_layer != ranks_per_layer {
return Err(ParseError::BoardNotRegular);
}
let ranks = to_dim(ranks_per_layer)?;
let layers = to_dim(completed_layers + 1)?;
Ok(([layers, ranks, files], 3))
}
_ => Err(ParseError::TooManyDimensions),
}
}
fn to_dim(size: u32) -> Result<u8, ParseError> {
u8::try_from(size).map_err(|_| ParseError::DimensionTooLarge)
}
fn read_segment(field: &[u8], i: &mut usize, piece_count: &mut u32) -> Result<u32, ParseError> {
let cap = u32::from(MAX_DIMENSION_SIZE);
let mut cells: u32 = 0;
while *i < field.len() && field[*i] != b'/' {
if field[*i].is_ascii_digit() {
if field[*i] == b'0' {
return Err(ParseError::InvalidEmptyCount);
}
let mut value: u32 = 0;
while *i < field.len() && field[*i].is_ascii_digit() {
value = value * 10 + u32::from(field[*i] - b'0');
if value > cap {
return Err(ParseError::DimensionTooLarge);
}
*i += 1;
}
cells += value;
} else {
match epin_token(&field[*i..]) {
Some((len, _)) => {
cells += 1;
*piece_count += 1;
*i += len;
}
None => return Err(ParseError::InvalidPieceToken),
}
}
if cells > cap {
return Err(ParseError::DimensionTooLarge);
}
}
if cells == 0 {
return Err(ParseError::EmptySegment);
}
Ok(cells)
}
#[cfg(test)]
mod tests {
use super::*;
fn shape_of(field: &str) -> (Vec<u8>, u32) {
let (s, pieces) = validate(field.as_bytes()).expect("valid field");
(s.dimensions().to_vec(), pieces)
}
fn err_of(field: &str) -> ParseError {
validate(field.as_bytes()).unwrap_err()
}
#[test]
fn chess_start() {
let f = "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R";
assert_eq!(shape_of(f), (vec![8, 8], 32));
}
#[test]
fn shogi_start() {
let f = "lnsgk^gsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGK^GSNL";
assert_eq!(shape_of(f), (vec![9, 9], 40));
}
#[test]
fn xiangqi_start() {
let f = "rheag^aehr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RHEAG^AEHR";
assert_eq!(shape_of(f), (vec![10, 9], 32));
}
#[test]
fn empty_board() {
assert_eq!(shape_of("8/8/8/8/8/8/8/8"), (vec![8, 8], 0));
}
#[test]
fn one_dimensional() {
assert_eq!(shape_of("k^+p4+PK^"), (vec![8], 4));
}
#[test]
fn three_dimensional() {
assert_eq!(shape_of("ab/cd//AB/CD"), (vec![2, 2, 2], 8));
}
#[test]
fn three_dimensional_three_layers() {
assert_eq!(shape_of("ab/cd//AB/CD//ef/gh"), (vec![3, 2, 2], 12));
}
#[test]
fn thin_two_dimensional() {
assert_eq!(shape_of("a/b/c"), (vec![3, 1], 3));
}
#[test]
fn full_modifier_token() {
assert_eq!(shape_of("+K^'"), (vec![1], 1));
}
#[test]
fn adjacent_derived_then_plain() {
assert_eq!(shape_of("K'K"), (vec![2], 2));
}
#[test]
fn promoted_derived_token() {
assert_eq!(shape_of("K^'"), (vec![1], 1));
}
#[test]
fn max_dimension_size_exact() {
assert_eq!(shape_of("255"), (vec![255], 0));
}
#[test]
fn empty_input() {
assert_eq!(err_of(""), ParseError::PlacementEmpty);
}
#[test]
fn starts_with_separator() {
assert_eq!(err_of("/8/8"), ParseError::PlacementStartsWithSeparator);
}
#[test]
fn ends_with_separator() {
assert_eq!(err_of("8/8/"), ParseError::PlacementEndsWithSeparator);
}
#[test]
fn interior_short_layer() {
assert_eq!(err_of("rkr//PPPP"), ParseError::DimensionalCoherence);
}
#[test]
fn final_short_layer() {
assert_eq!(err_of("ab/cd//AB"), ParseError::DimensionalCoherence);
}
#[test]
fn too_many_dimensions() {
assert_eq!(err_of("a/b///c/d"), ParseError::TooManyDimensions);
}
#[test]
fn irregular_ranks() {
assert_eq!(err_of("rkr/pp/PPPP"), ParseError::BoardNotRegular);
}
#[test]
fn irregular_layers() {
assert_eq!(err_of("a/b//A/B/C"), ParseError::BoardNotRegular);
}
#[test]
fn leading_zero_empty_count() {
assert_eq!(err_of("08"), ParseError::InvalidEmptyCount);
}
#[test]
fn bare_zero_empty_count() {
assert_eq!(err_of("0"), ParseError::InvalidEmptyCount);
}
#[test]
fn invalid_piece_token() {
assert_eq!(err_of("k$k"), ParseError::InvalidPieceToken);
}
#[test]
fn single_run_too_large() {
assert_eq!(err_of("256"), ParseError::DimensionTooLarge);
}
#[test]
fn segment_total_too_large() {
assert_eq!(err_of("200A100"), ParseError::DimensionTooLarge);
}
}