#![no_std]
#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/dni/0.1.0")]
extern crate alloc;
use alloc::string::{String, ToString};
use core::fmt;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
const CHECK_LETTERS: &[u8; 23] = b"TRWAGMYFPDXBNJZSQVHLCKE";
#[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 has an invalid length",
Error::InvalidFormat => "number has an invalid format",
Error::InvalidChecksum => "check letter does not match",
};
f.write_str(message)
}
}
impl core::error::Error for Error {}
#[must_use]
pub fn compact(number: &str) -> String {
let filtered: String = number.chars().filter(|&c| c != ' ' && c != '-').collect();
filtered.to_uppercase().trim().to_string()
}
fn is_digits(value: &str) -> bool {
!value.is_empty() && value.bytes().all(|b| b.is_ascii_digit())
}
fn modulo_23(digits: &str) -> Option<u32> {
if digits.is_empty() {
return None;
}
let mut remainder = 0u32;
for character in digits.chars() {
remainder = (remainder * 10 + character.to_digit(10)?) % 23;
}
Some(remainder)
}
pub fn calc_check_digit(number: &str) -> Result<char, Error> {
let remainder = modulo_23(number).ok_or(Error::InvalidFormat)?;
Ok(char::from(CHECK_LETTERS[remainder as usize]))
}
pub fn validate(number: &str) -> Result<String, Error> {
let number = compact(number);
let chars: alloc::vec::Vec<char> = number.chars().collect();
let body: String = body_before_last(&chars);
if !is_digits(&body) {
return Err(Error::InvalidFormat);
}
if chars.len() != 9 {
return Err(Error::InvalidLength);
}
if chars[chars.len() - 1] != calc_check_digit(&body)? {
return Err(Error::InvalidChecksum);
}
Ok(number)
}
#[must_use]
pub fn is_valid(number: &str) -> bool {
validate(number).is_ok()
}
fn body_before_last(chars: &[char]) -> String {
if chars.is_empty() {
String::new()
} else {
chars[..chars.len() - 1].iter().collect()
}
}
pub mod nie {
use super::{
body_before_last, calc_check_digit as dni_calc, compact as dni_compact, is_digits, Error,
};
use alloc::string::String;
use alloc::{format, vec::Vec};
#[must_use]
pub fn compact(number: &str) -> String {
dni_compact(number)
}
pub fn calc_check_digit(number: &str) -> Result<char, Error> {
let mut chars = number.chars();
let prefix = match chars.next() {
Some('X') => '0',
Some('Y') => '1',
Some('Z') => '2',
_ => return Err(Error::InvalidFormat),
};
let rest: String = chars.collect();
dni_calc(&format!("{prefix}{rest}"))
}
pub fn validate(number: &str) -> Result<String, Error> {
let number = dni_compact(number);
let chars: Vec<char> = number.chars().collect();
let middle: String = if chars.len() >= 2 {
chars[1..chars.len() - 1].iter().collect()
} else {
String::new()
};
let first = chars.first().copied();
if !is_digits(&middle) || !matches!(first, Some('X' | 'Y' | 'Z')) {
return Err(Error::InvalidFormat);
}
if chars.len() != 9 {
return Err(Error::InvalidLength);
}
if chars[chars.len() - 1] != calc_check_digit(&body_before_last(&chars))? {
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 dni_valid() {
assert!(is_valid("12345678Z"));
assert!(is_valid("00000000T"));
assert!(is_valid("54362315K"));
assert_eq!(validate("12345678-z").unwrap(), "12345678Z"); assert_eq!(calc_check_digit("12345678").unwrap(), 'Z');
}
#[test]
fn dni_invalid() {
assert_eq!(validate("12345678A"), Err(Error::InvalidChecksum));
assert_eq!(validate("1234567Z"), Err(Error::InvalidLength));
assert_eq!(validate("1234567XZ"), Err(Error::InvalidFormat)); assert!(!is_valid("X1234567L")); assert!(!is_valid(""));
}
#[test]
fn nie_valid() {
assert!(nie::is_valid("X1234567L"));
assert!(nie::is_valid("Y1234567X"));
assert!(nie::is_valid("Z1234567R"));
assert_eq!(nie::validate("x-2482300-w").unwrap(), "X2482300W");
assert_eq!(nie::calc_check_digit("X1234567").unwrap(), 'L');
}
#[test]
fn nie_invalid() {
assert_eq!(nie::validate("X2482300A"), Err(Error::InvalidChecksum));
assert_eq!(nie::validate("X2482300"), Err(Error::InvalidLength));
assert_eq!(nie::validate("A1234567L"), Err(Error::InvalidFormat)); assert!(!nie::is_valid("12345678Z")); }
}