use crate::errors::ValidationError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Valor {
bytes: [u8; Self::MAX_LENGTH],
len: u8,
}
impl Valor {
pub const MIN_LENGTH: usize = 1;
pub const MAX_LENGTH: usize = 9;
pub fn parse(s: &str) -> Result<Self, ValidationError> {
let found = s.chars().count();
if !(Self::MIN_LENGTH..=Self::MAX_LENGTH).contains(&found) {
return Err(ValidationError::Structure {
rule: "VALOR must be 1 to 9 digits",
});
}
for (i, ch) in s.chars().enumerate() {
if !ch.is_ascii_digit() {
return Err(ValidationError::InvalidCharacter {
position: i + 1,
found: ch,
});
}
}
let mut bytes = [0u8; Self::MAX_LENGTH];
if let Some(slot) = bytes.get_mut(..found) {
slot.copy_from_slice(s.as_bytes());
}
Ok(Self {
bytes,
len: u8::try_from(found).unwrap_or(0),
})
}
pub fn validate(s: &str) -> Result<(), ValidationError> {
Self::parse(s).map(|_| ())
}
#[must_use]
pub const fn from_bytes_unchecked(bytes: [u8; Self::MAX_LENGTH], len: u8) -> Self {
Self { bytes, len }
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
core::str::from_utf8(self.as_bytes()).unwrap_or("")
}
#[must_use]
#[inline]
pub fn as_bytes(&self) -> &[u8] {
self.bytes.get(..self.len as usize).unwrap_or(&[])
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.len as usize
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
#[inline]
pub fn as_u64(&self) -> u64 {
let mut value: u64 = 0;
for &b in self.as_bytes() {
value = value * 10 + u64::from(b.wrapping_sub(b'0'));
}
value
}
#[must_use]
#[inline]
pub fn as_u32(&self) -> u32 {
u32::try_from(self.as_u64()).unwrap_or(0)
}
}
impl core::fmt::Display for Valor {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
impl core::str::FromStr for Valor {
type Err = ValidationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for Valor {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::display;
use core::str::FromStr;
const GOLDEN: &[&str] = &["1213853", "908440", "24476758"];
#[test]
fn parses_golden_valors() {
for &s in GOLDEN {
let valor = Valor::parse(s).unwrap_or_else(|e| panic!("{s} should parse: {e}"));
assert_eq!(valor.as_str(), s);
}
}
#[test]
fn accepts_minimum_and_maximum_length() {
let one = Valor::parse("7").unwrap();
assert_eq!(one.len(), 1);
assert_eq!(one.as_str(), "7");
let nine = Valor::parse("123456789").unwrap();
assert_eq!(nine.len(), 9);
assert_eq!(nine.as_str(), "123456789");
assert_eq!(Valor::MIN_LENGTH, 1);
assert_eq!(Valor::MAX_LENGTH, 9);
}
#[test]
fn accessors() {
let valor = Valor::parse("24476758").unwrap();
assert_eq!(valor.as_str(), "24476758");
assert_eq!(valor.as_bytes(), b"24476758");
assert_eq!(valor.len(), 8);
assert_eq!(valor.as_u64(), 24_476_758);
}
#[test]
fn as_u64_computes_numeric_value() {
assert_eq!(Valor::parse("1213853").unwrap().as_u64(), 1_213_853);
assert_eq!(Valor::parse("908440").unwrap().as_u64(), 908_440);
assert_eq!(Valor::parse("0").unwrap().as_u64(), 0);
assert_eq!(Valor::parse("000123").unwrap().as_u64(), 123);
assert_eq!(Valor::parse("999999999").unwrap().as_u64(), 999_999_999);
}
#[test]
fn as_u32_agrees_with_as_u64() {
for s in ["0", "1213853", "908440", "999999999", "000123"] {
let v = Valor::parse(s).unwrap();
assert_eq!(u64::from(v.as_u32()), v.as_u64(), "value {s}");
}
assert_eq!(Valor::parse("999999999").unwrap().as_u32(), 999_999_999);
}
#[test]
fn rejects_empty_input() {
assert_eq!(
Valor::parse(""),
Err(ValidationError::Structure {
rule: "VALOR must be 1 to 9 digits",
})
);
}
#[test]
fn rejects_too_long() {
assert_eq!(
Valor::parse("1234567890"),
Err(ValidationError::Structure {
rule: "VALOR must be 1 to 9 digits",
})
);
}
#[test]
fn rejects_non_digit_character() {
assert_eq!(
Valor::parse("1213853A"),
Err(ValidationError::InvalidCharacter {
position: 8,
found: 'A',
})
);
}
#[test]
fn rejects_non_digit_at_first_position() {
assert!(matches!(
Valor::parse("X12345"),
Err(ValidationError::InvalidCharacter { position: 1, .. })
));
}
#[test]
fn rejects_non_ascii_without_panic() {
assert!(Valor::parse("12345é").is_err());
assert!(Valor::parse("é").is_err());
}
#[test]
fn validate_matches_parse() {
assert!(Valor::validate("908440").is_ok());
assert!(Valor::validate("").is_err());
assert!(Valor::validate("1234567890").is_err());
}
#[test]
fn round_trips_through_str() {
for &s in GOLDEN {
assert_eq!(Valor::parse(s).unwrap().as_str(), s);
}
}
#[test]
fn from_str_matches_parse() {
assert_eq!(Valor::from_str("1213853"), Valor::parse("1213853"));
assert!(Valor::from_str("nonsense").is_err());
}
#[test]
fn display_renders_identifier() {
let valor = Valor::parse("1213853").unwrap();
assert_eq!(display(valor).as_str(), "1213853");
}
#[test]
fn as_ref_str() {
let valor = Valor::parse("908440").unwrap();
let s: &str = valor.as_ref();
assert_eq!(s, "908440");
}
#[test]
fn from_bytes_unchecked_round_trip() {
let valor = Valor::from_bytes_unchecked(*b"908440\0\0\0", 6);
assert_eq!(valor, Valor::parse("908440").unwrap());
}
#[test]
fn unused_tail_is_zeroed_for_eq_and_hash() {
let a = Valor::parse("12345").unwrap();
let b = Valor::parse("12345").unwrap();
assert_eq!(a, b);
assert_ne!(a, Valor::parse("123456").unwrap());
assert_ne!(a, Valor::parse("1234").unwrap());
}
#[test]
fn is_copy_and_eq_and_hashable() {
let a = Valor::parse("1213853").unwrap();
let b = a; assert_eq!(a, b);
assert_ne!(a, Valor::parse("908440").unwrap());
let keys = [a, b];
assert_eq!(keys[0], keys[1]);
}
}