nyandere 0.1.2

i help with keeping track of purchases. meow
Documentation
//! Offering, selling and buying need a common denominator.

use std::{num::ParseIntError, ops::RangeInclusive, str::FromStr};

use thiserror::Error;

use crate::aux::Stack;

/// Global trade item number. The number behind the barcode you find in stores.
///
/// Internationally standardized.
/// This encompasses typical products one would buy off-the-shelf
/// as well ase more specialized cases like books and smaller products.
///
/// # Note on validation
///
/// While there are only limited possibilities for the lengths of GTINs
/// (namely, 8, 10, 13, 14), this is not validated.
/// Any positive number with at most 14 digits in base 10 is accepted.
/// Shorter ones are just padded with zeroes at the start.
///
/// # Resources
///
/// - <https://en.wikipedia.org/wiki/Global_Trade_Item_Number>
// largest number representable by 14 digits is `10^14 - 1`,
// which requires `ceil(log2(10^14 - 1)) = 47` bits
// next largest int is u64
// which has the nice side effect of "automatically" padding shorter GTINs with zeroes
#[derive(Stack!)]
pub struct Gtin(u64);

impl Gtin {
    /// The largest possible GTIN has 14 digits. For now, that is.
    pub const DIGITS_MIN: u8 = 8;
    pub const DIGITS_MAX: u8 = 14;
    pub const DIGITS_RANGE: RangeInclusive<u8> = Self::DIGITS_MIN..=Self::DIGITS_MAX;
    pub const MAX: Self = Self(10u64.pow(Self::DIGITS_MAX as u32) - 1);

    /// Interpret the integer as-is as GTIN.
    ///
    /// # Errors
    ///
    /// Returns an error if the integer is longer than 14 digits.
    pub fn new(source: u64) -> Result<Self, OutOfRangeError> {
        let gtin = Self(source);

        if gtin.digits() > Self::DIGITS_MAX {
            return Err(OutOfRangeError {
                orig: source,
                n: gtin.digits(),
            });
        }

        Ok(gtin)
    }

    #[must_use]
    pub fn get(&self) -> u64 {
        self.0
    }

    /// How many digits are in this GTIN
    /// when represented in base 10?
    #[must_use]
    pub fn digits(&self) -> u8 {
        let n = self.0;
        if n == 0 { 1 } else { n.ilog10() as u8 + 1 }
    }
}

impl FromStr for Gtin {
    type Err = GtinParseError;
    fn from_str(source: &str) -> Result<Self, Self::Err> {
        let parsed = source.parse()?;

        // note: the u8 cast is fine,
        // ceil(log10(2^64 - 1)) = 20,
        // so 20 digits can be at max in a u64
        let digits = source.len() as u8;
        if !Self::DIGITS_RANGE.contains(&digits) {
            return Err(OutOfRangeError {
                orig: parsed,
                n: digits,
            }
            .into());
        }

        let gtin = Self::new(parsed)?;
        Ok(gtin)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum GtinParseError {
    #[error("couldn't parse as an integer: {0}")]
    ExpectedInteger(#[from] ParseIntError),
    #[error("valid int, but out of range: {0}")]
    OutOfRange(#[from] OutOfRangeError),
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error(
    "`{orig}` contains {n} digits, which is not in the allowed range [{}, {}]",
    Gtin::DIGITS_MIN,
    Gtin::DIGITS_MAX
)]
pub struct OutOfRangeError {
    pub orig: u64,
    pub n: u8,
}