#![no_std]
#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/tckimlik/0.1.0")]
extern crate alloc;
use alloc::string::{String, ToString};
use core::fmt;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
InvalidLength,
InvalidFormat,
InvalidChecksum,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Error::InvalidLength => "number must be 11 digits long",
Error::InvalidFormat => "number must be all digits and not start with 0",
Error::InvalidChecksum => "check digits do not match",
};
f.write_str(message)
}
}
impl core::error::Error for Error {}
#[must_use]
pub fn compact(number: &str) -> String {
number.trim().to_string()
}
#[allow(clippy::cast_possible_wrap)] pub fn calc_check_digits(number: &str) -> Result<String, Error> {
let mut weighted: i32 = 0;
let mut plain: i32 = 0;
for (i, character) in number.chars().take(9).enumerate() {
let digit = character.to_digit(10).ok_or(Error::InvalidFormat)? as i32;
weighted += if i % 2 == 0 { 3 } else { 1 } * digit;
plain += digit;
}
let check1 = (10 - weighted).rem_euclid(10);
let check2 = (check1 + plain).rem_euclid(10);
Ok(alloc::format!("{check1}{check2}"))
}
pub fn validate(number: &str) -> Result<String, Error> {
let number = compact(number);
let bytes = number.as_bytes();
if bytes.len() != 11 {
return Err(Error::InvalidLength);
}
if !bytes.iter().all(u8::is_ascii_digit) || bytes[0] == b'0' {
return Err(Error::InvalidFormat);
}
if number[9..11] != calc_check_digits(&number[..9])? {
return Err(Error::InvalidChecksum);
}
Ok(number)
}
#[must_use]
pub fn is_valid(number: &str) -> bool {
validate(number).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_numbers() {
assert!(is_valid("17291716060"));
assert!(is_valid("10000000146"));
assert!(is_valid("12345678950"));
assert_eq!(validate(" 17291716060 ").unwrap(), "17291716060"); }
#[test]
fn invalid_numbers() {
assert_eq!(validate("17291716050"), Err(Error::InvalidChecksum));
assert_eq!(validate("1729171606"), Err(Error::InvalidLength)); assert_eq!(validate("07291716092"), Err(Error::InvalidFormat)); assert_eq!(validate("1729171606a"), Err(Error::InvalidFormat)); assert!(!is_valid(""));
}
#[test]
fn check_digits() {
assert_eq!(calc_check_digits("172917160").unwrap(), "60");
assert_eq!(calc_check_digits("100000001").unwrap(), "46");
assert_eq!(
calc_check_digits("1234567x9").unwrap_err(),
Error::InvalidFormat
);
}
#[test]
fn generate_then_validate() {
for first_nine in ["123456789", "987654321", "100000001", "555555555"] {
let check = calc_check_digits(first_nine).unwrap();
let full = alloc::format!("{first_nine}{check}");
assert!(is_valid(&full), "{full} should be valid");
}
}
}