il_tz 0.1.3

A Rust library for working with Israeli ID numbers (TZ)
Documentation
use std::fmt::Display;

use num::Integer;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum TZError {
    #[error("Invalid TZ length: {0}")]
    InvalidTZLength(String),
    #[error("Invalid digit in TZ: {0}")]
    InvalidTZDigit(String),
    #[error("Invalid TZ: {0}")]
    InvalidTZ(String),
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TZDigit {
    Zero = 0,
    One = 1,
    Two = 2,
    Three = 3,
    Four = 4,
    Five = 5,
    Six = 6,
    Seven = 7,
    Eight = 8,
    Nine = 9,
}

impl From<TZDigit> for u8 {
    fn from(val: TZDigit) -> Self {
        val as u8
    }
}

impl TryFrom<u8> for TZDigit {
    type Error = TZError;

    fn try_from(d: u8) -> Result<Self, Self::Error> {
        match d {
            0 => Ok(TZDigit::Zero),
            1 => Ok(TZDigit::One),
            2 => Ok(TZDigit::Two),
            3 => Ok(TZDigit::Three),
            4 => Ok(TZDigit::Four),
            5 => Ok(TZDigit::Five),
            6 => Ok(TZDigit::Six),
            7 => Ok(TZDigit::Seven),
            8 => Ok(TZDigit::Eight),
            9 => Ok(TZDigit::Nine),
            _ => Err(TZError::InvalidTZDigit(d.to_string())),
        }
    }
}

impl TryFrom<char> for TZDigit {
    type Error = TZError;

    fn try_from(c: char) -> Result<Self, Self::Error> {
        match c {
            '0' => Ok(TZDigit::Zero),
            '1' => Ok(TZDigit::One),
            '2' => Ok(TZDigit::Two),
            '3' => Ok(TZDigit::Three),
            '4' => Ok(TZDigit::Four),
            '5' => Ok(TZDigit::Five),
            '6' => Ok(TZDigit::Six),
            '7' => Ok(TZDigit::Seven),
            '8' => Ok(TZDigit::Eight),
            '9' => Ok(TZDigit::Nine),
            _ => Err(TZError::InvalidTZDigit(c.to_string())),
        }
    }
}

impl Display for TZDigit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", *self as u8)
    }
}

impl TZDigit {
    pub fn to_sum(&self) -> Self {
        match self {
            Self::Zero => Self::Zero,
            Self::One => Self::Two,
            Self::Two => Self::Four,
            Self::Three => Self::Six,
            Self::Four => Self::Eight,
            Self::Five => Self::One,
            Self::Six => Self::Three,
            Self::Seven => Self::Five,
            Self::Eight => Self::Seven,
            Self::Nine => Self::Nine,
        }
    }
}

pub type TZ = [TZDigit; 9];
pub const MAX_TZ_RANGE: usize = 999_999_999;

pub fn str2tz(s: &str) -> Result<TZ, TZError> {
    let s9 = format!("{:0>9}", s);
    if s9.len() != 9 {
        return Err(TZError::InvalidTZLength(s.to_string()));
    }
    let mut result = [TZDigit::Zero; 9];
    for (i, c) in s9.chars().enumerate() {
        let digit = TZDigit::try_from(c)?;
        result[i] = digit;
    }
    Ok(result)
}

pub fn int2tz<T>(n: T) -> Result<TZ, TZError>
where
    T: Integer + std::fmt::Display + std::convert::From<usize>,
{
    if n > MAX_TZ_RANGE.into() {
        return Err(TZError::InvalidTZLength(n.to_string()));
    }
    if n < 0.into() {
        return Err(TZError::InvalidTZ(n.to_string()));
    }
    str2tz(&n.to_string())
}

fn tz_last_digit(tz: &TZ) -> Result<TZDigit, TZError> {
    let sum = tz[0] as u8
        + tz[1].to_sum() as u8
        + tz[2] as u8
        + tz[3].to_sum() as u8
        + tz[4] as u8
        + tz[5].to_sum() as u8
        + tz[6] as u8
        + tz[7].to_sum() as u8;
    let remainder = sum % 10;
    if remainder == 0 {
        Ok(TZDigit::Zero)
    } else {
        TZDigit::try_from(10 - remainder)
    }
}

pub fn validate(tz: &TZ) -> bool {
    tz[8] == tz_last_digit(tz).unwrap_or(TZDigit::Zero)
}

pub fn tz2str(tz: &TZ) -> String {
    tz.iter().map(|d| d.to_string()).collect()
}

pub fn generate(start: usize, end: usize) -> Vec<TZ> {
    let mut end = if end > MAX_TZ_RANGE {
        MAX_TZ_RANGE
    } else {
        end
    };
    let mut start = if start > end { end } else { start };
    start -= start % 10;
    end -= end % 10;
    let capacity = (end - start + 1) / 10;
    let mut result = Vec::with_capacity(capacity);
    for i in (start..=end).step_by(10) {
        let mut tz = int2tz(i).unwrap();
        tz[8] = tz_last_digit(&tz).unwrap();
        result.push(tz);
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_str_9() {
        let tz = str2tz("123456789").unwrap();
        assert_eq!(
            tz,
            [
                TZDigit::One,
                TZDigit::Two,
                TZDigit::Three,
                TZDigit::Four,
                TZDigit::Five,
                TZDigit::Six,
                TZDigit::Seven,
                TZDigit::Eight,
                TZDigit::Nine
            ]
        );
        let tz = str2tz("002345678").unwrap();
        assert_eq!(
            tz,
            [
                TZDigit::Zero,
                TZDigit::Zero,
                TZDigit::Two,
                TZDigit::Three,
                TZDigit::Four,
                TZDigit::Five,
                TZDigit::Six,
                TZDigit::Seven,
                TZDigit::Eight
            ]
        );
        let tz = str2tz("000000000").unwrap();
        assert_eq!(tz, [TZDigit::Zero; 9]);
        let tz = str2tz("999999999").unwrap();
        assert_eq!(tz, [TZDigit::Nine; 9]);
    }

    #[test]
    fn test_str_long() {
        let tz = str2tz("1234567890");
        assert!(tz.is_err());
        let tz = str2tz("12345678901");
        assert!(tz.is_err());
        let tz = str2tz("123456789012");
        assert!(tz.is_err());
    }

    #[test]
    fn test_str_short() {
        assert_eq!(str2tz("12345678").unwrap(), str2tz("012345678").unwrap());
        assert_eq!(str2tz("1234567").unwrap(), str2tz("001234567").unwrap());
        assert_eq!(str2tz("123456").unwrap(), str2tz("000123456").unwrap());
        assert_eq!(str2tz("12345").unwrap(), str2tz("000012345").unwrap());
        assert_eq!(str2tz("1234").unwrap(), str2tz("000001234").unwrap());
        assert_eq!(str2tz("123").unwrap(), str2tz("000000123").unwrap());
        assert_eq!(str2tz("12").unwrap(), str2tz("000000012").unwrap());
        assert_eq!(str2tz("1").unwrap(), str2tz("000000001").unwrap());
        assert_eq!(str2tz("").unwrap(), str2tz("000000000").unwrap());
    }

    #[test]
    fn test_int_good() {
        assert_eq!(int2tz(123456789).unwrap(), str2tz("123456789").unwrap());
        assert_eq!(int2tz(11).unwrap(), str2tz("000000011").unwrap());
        assert_eq!(int2tz(MAX_TZ_RANGE).unwrap(), str2tz("999999999").unwrap());
    }

    #[test]
    fn test_bad_input() {
        let tz = int2tz(1_000_000_000);
        assert!(tz.is_err());
        let tz = str2tz("-1");
        assert!(tz.is_err());
        let tz = str2tz("12345a789");
        assert!(tz.is_err());
    }

    #[test]
    fn test_valid_tz() {
        let input = [37015971, 037015906, 037015955, 037015963];
        for i in input {
            let tz = int2tz(i).unwrap();
            assert!(validate(&tz));
        }
    }

    #[test]
    fn test_invalid_tz() {
        let input = [3701591, 037015936, 037515955, 0000963];
        for i in input {
            let tz = int2tz(i).unwrap();
            assert!(!validate(&tz));
        }
    }

    #[test]
    fn test_generate() {
        let list = generate(37015900, 37016000);
        assert_eq!(list.len(), 11);
        let valid = str2tz("037015971").unwrap();
        assert!(list.contains(&valid));
        let list = generate(100, 200);
        assert_eq!(list.len(), 11);
        let valid = int2tz(141).unwrap();
        assert!(list.contains(&valid));
    }

    #[test]
    fn test_generate_invalid() {
        let list = generate(300, 3);
        assert_eq!(list.len(), 1);
        assert_eq!(list[0], [TZDigit::Zero; 9]);
        let list = generate(0, 1);
        assert_eq!(list.len(), 1);
        assert_eq!(list[0], [TZDigit::Zero; 9]);
        let list = generate(0, 0);
        assert_eq!(list.len(), 1);
        assert_eq!(list[0], [TZDigit::Zero; 9]);
        let list = generate(MAX_TZ_RANGE + 10, MAX_TZ_RANGE + 200);
        assert_eq!(list.len(), 1);
        assert_eq!(list[0], int2tz(999_999_998).unwrap());
    }
}