use sashite_epin::{Identifier, Side, State};
use crate::error::ParseError;
use crate::token::epin_token;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HandItem {
piece: Identifier,
count: u32,
}
impl HandItem {
#[must_use]
pub const fn piece(self) -> Identifier {
self.piece
}
#[must_use]
pub const fn count(self) -> u32 {
self.count
}
}
#[derive(Debug)]
pub struct HandIter<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> HandIter<'a> {
pub(crate) const fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
}
impl Iterator for HandIter<'_> {
type Item = HandItem;
fn next(&mut self) -> Option<HandItem> {
if self.pos >= self.bytes.len() {
return None;
}
let Ok((next, count, piece)) = read_item(self.bytes, self.pos) else {
self.pos = self.bytes.len();
return None;
};
self.pos = next;
Some(HandItem { piece, count })
}
}
pub(crate) fn validate(field: &[u8]) -> Result<u32, ParseError> {
let mut delimiter = None;
let mut slashes = 0usize;
for (idx, &b) in field.iter().enumerate() {
if b == b'/' {
slashes += 1;
delimiter = Some(idx);
}
}
let (1, Some(at)) = (slashes, delimiter) else {
return Err(ParseError::InvalidHandsDelimiter);
};
let first = validate_one_hand(&field[..at])?;
let second = validate_one_hand(&field[at + 1..])?;
Ok(first.saturating_add(second))
}
fn validate_one_hand(bytes: &[u8]) -> Result<u32, ParseError> {
let mut seen = [0u64; 10];
let mut prev: Option<(u32, u8, u8, u8, u8, u8)> = None;
let mut total: u32 = 0;
let len = bytes.len();
let mut i = 0;
while i < len {
let (next, count, id) = read_item(bytes, i)?;
i = next;
let (letter, case, state, terminal, derived) = token_key(id);
let index = usize::from(letter) * 24
+ usize::from(case) * 12
+ usize::from(state) * 4
+ usize::from(terminal) * 2
+ usize::from(derived);
let bit = 1u64 << (index % 64);
let word = index / 64;
if seen[word] & bit != 0 {
return Err(ParseError::HandNotAggregated);
}
seen[word] |= bit;
let cur = (u32::MAX - count, letter, case, state, terminal, derived);
if let Some(p) = prev {
if cur < p {
return Err(ParseError::HandNotCanonical);
}
}
prev = Some(cur);
total = total.saturating_add(count);
}
Ok(total)
}
fn read_item(bytes: &[u8], start: usize) -> Result<(usize, u32, Identifier), ParseError> {
let len = bytes.len();
let mut i = start;
let count = if i < len && bytes[i].is_ascii_digit() {
if bytes[i] == b'0' {
return Err(ParseError::InvalidHandCount); }
let mut value: u32 = 0;
while i < len && bytes[i].is_ascii_digit() {
value = value
.saturating_mul(10)
.saturating_add(u32::from(bytes[i] - b'0'));
i += 1;
}
if value < 2 {
return Err(ParseError::InvalidHandCount); }
value
} else {
1
};
match epin_token(&bytes[i..]) {
Some((tok_len, id)) => Ok((i + tok_len, count, id)),
None => Err(ParseError::InvalidPieceToken),
}
}
pub(crate) fn token_key(id: Identifier) -> (u8, u8, u8, u8, u8) {
let letter = id.letter().as_ascii() - b'A';
let case = match id.side() {
Side::First => 0,
Side::Second => 1,
};
let state = match id.state() {
State::Diminished => 0,
State::Enhanced => 1,
State::Normal => 2,
};
(
letter,
case,
state,
u8::from(id.is_terminal()),
u8::from(id.is_derived()),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ParseError;
fn ok(field: &str) -> u32 {
validate(field.as_bytes()).expect("valid hands")
}
fn err(field: &str) -> ParseError {
validate(field.as_bytes()).unwrap_err()
}
#[test]
fn both_empty() {
assert_eq!(ok("/"), 0);
}
#[test]
fn first_only() {
assert_eq!(ok("P/"), 1);
}
#[test]
fn second_only() {
assert_eq!(ok("/p"), 1);
}
#[test]
fn multiplicity() {
assert_eq!(ok("3P2B/"), 5); }
#[test]
fn case_order() {
assert_eq!(ok("BbPp/"), 4); }
#[test]
fn cross_hands() {
assert_eq!(ok("3P2B/3p2b"), 10);
}
#[test]
fn shogi_like_mixed() {
assert_eq!(ok("2P/p"), 3);
}
#[test]
fn state_terminal_derivation_keys() {
assert_eq!(ok("-K+KKK^K^'/"), 5);
}
#[test]
fn iterator_round_trip() {
let encoded: Vec<(char, u32)> = HandIter::new(b"3P2Bp")
.map(|it| (it.piece().letter().as_char(), it.count()))
.collect();
assert_eq!(encoded, vec![('P', 3), ('B', 2), ('P', 1)]);
}
#[test]
fn no_delimiter() {
assert_eq!(err("PP"), ParseError::InvalidHandsDelimiter);
}
#[test]
fn two_delimiters() {
assert_eq!(err("P/p/"), ParseError::InvalidHandsDelimiter);
}
#[test]
fn explicit_count_one() {
assert_eq!(err("1P/"), ParseError::InvalidHandCount);
}
#[test]
fn leading_zero_count() {
assert_eq!(err("02P/"), ParseError::InvalidHandCount);
}
#[test]
fn dangling_count() {
assert_eq!(err("2/"), ParseError::InvalidPieceToken);
}
#[test]
fn bad_piece() {
assert_eq!(err("P$/"), ParseError::InvalidPieceToken);
}
#[test]
fn adjacent_duplicate() {
assert_eq!(err("PP/"), ParseError::HandNotAggregated); }
#[test]
fn nonadjacent_duplicate_different_counts() {
assert_eq!(err("3P3Q2P/"), ParseError::HandNotAggregated);
}
#[test]
fn wrong_letter_order() {
assert_eq!(err("QP/"), ParseError::HandNotCanonical); }
#[test]
fn wrong_multiplicity_order() {
assert_eq!(err("2P3Q/"), ParseError::HandNotCanonical); }
#[test]
fn wrong_case_order() {
assert_eq!(err("pP/"), ParseError::HandNotCanonical); }
}