#![no_std]
#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/iso7064/0.1.0")]
extern crate alloc;
use alloc::format;
use alloc::string::String;
use core::fmt;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
InvalidFormat,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid data format; expected a non-empty string of [0-9A-Z]")
}
}
impl core::error::Error for Error {}
fn is_valid_format(value: &str) -> bool {
!value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || byte.is_ascii_uppercase())
}
fn mod97(value: &str) -> i64 {
let mut buffer: i64 = 0;
for code in value.encode_utf16() {
let code = i64::from(code);
let (multiplier, offset) = if code >= i64::from(b'A') {
(100, i64::from(b'A') - 10)
} else {
(10, i64::from(b'0'))
};
buffer = (buffer * multiplier - offset + code) % 97;
}
buffer
}
pub fn compute(value: &str) -> Result<i64, Error> {
if is_valid_format(value) {
Ok(mod97(value))
} else {
Err(Error::InvalidFormat)
}
}
#[must_use]
pub fn compute_without_check(value: &str) -> i64 {
mod97(value)
}
#[must_use]
pub fn is_valid(value: &str) -> bool {
is_valid_format(value) && mod97(value) == 1
}
pub fn generate_check_digits(base: &str) -> Result<String, Error> {
if !is_valid_format(base) {
return Err(Error::InvalidFormat);
}
let remainder = (mod97(base) * 100) % 97;
let check = (98 - remainder) % 97;
Ok(format!("{check:02}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_matches_reference() {
assert_eq!(compute("969500KSV493XWY0PS").unwrap(), 54);
assert_eq!(compute("7245005WBNJAFHBD0S").unwrap(), 55);
assert_eq!(compute("0").unwrap(), 0);
assert_eq!(compute("A").unwrap(), 10);
assert_eq!(compute("Z").unwrap(), 35);
}
#[test]
fn compute_without_check_is_lenient() {
assert_eq!(compute_without_check("abc"), 66);
assert_eq!(compute_without_check("a!b"), 94);
assert_eq!(compute_without_check("969500KSV493XWY0PS"), 54);
}
#[test]
fn invalid_format() {
assert_eq!(compute(""), Err(Error::InvalidFormat));
assert_eq!(compute("abc"), Err(Error::InvalidFormat)); assert_eq!(compute("A B"), Err(Error::InvalidFormat));
assert_eq!(generate_check_digits(""), Err(Error::InvalidFormat));
}
#[test]
fn validate_and_generate() {
assert_eq!(generate_check_digits("969500KSV493XWY0PS").unwrap(), "33");
assert!(is_valid("969500KSV493XWY0PS33"));
assert!(!is_valid("969500KSV493XWY0PS00"));
assert!(!is_valid("lowercase")); }
#[test]
fn generate_round_trips() {
for base in [
"5493001KJTIIGC8Y1R",
"529900T8BM49AURSDO",
"ABC123",
"0",
"ZZZZ",
] {
let check = generate_check_digits(base).unwrap();
let full = alloc::format!("{base}{check}");
assert!(is_valid(&full), "{full} should be valid");
}
}
}