pub fn generate_malo_id(seed: u64) -> String {
let base = seed % 10_000_000_000;
let base_str = format!("{base:010}");
let check = malo_check_digit(&base_str);
format!("{base_str}{check}")
}
pub fn malo_check_digit(digits: &str) -> u8 {
let sum: u32 = digits
.chars()
.enumerate()
.map(|(i, c)| {
let d = c.to_digit(10).expect("input must be all digits");
if (i + 1) % 2 == 0 {
2 * d
} else {
d
}
})
.sum();
((10 - (sum % 10)) % 10) as u8
}
pub fn generate_melo_id(seed: u64) -> String {
let part1 = seed % 10_000_000_000; let part2 = (seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1)) % 10_000_000_000; let part3 = (seed.wrapping_mul(2_862_933_555_777_941_757).wrapping_add(3)) % 100_000_000_000; format!("DE{part1:010}{part2:010}{part3:011}")
}
pub fn generate_nelo_id(seed: u64) -> String {
let digits = seed % 10_000_000_000;
format!("E{digits:010}")
}
pub fn generate_steuress_id(seed: u64) -> String {
let digits = seed % 10_000_000_000;
format!("C{digits:010}")
}
pub fn generate_techress_id(seed: u64) -> String {
let digits = seed % 10_000_000_000;
format!("D{digits:010}")
}
pub fn generate_gln(seed: u64) -> String {
let base = seed % 1_000_000_000_000;
let base_str = format!("{base:012}");
let check = gs1_check_digit(&base_str);
format!("{base_str}{check}")
}
pub fn generate_reference_id(seed: u64) -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let len = 12;
let mut result = Vec::with_capacity(len);
let mut state = seed;
for _ in 0..len {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let idx = (state >> 33) as usize % CHARSET.len();
result.push(CHARSET[idx]);
}
String::from_utf8(result).expect("charset is ASCII")
}
fn gs1_check_digit(digits: &str) -> u8 {
let mut sum: u32 = 0;
for (i, ch) in digits.chars().rev().enumerate() {
let d = ch.to_digit(10).expect("input must be all digits");
let weight = if i % 2 == 0 { 3 } else { 1 };
sum += d * weight;
}
((10 - (sum % 10)) % 10) as u8
}
pub fn generate_datum(seed: u64) -> String {
let (year, month, day) = compute_ymd(seed, seed % 730);
let year2 = year % 100;
format!("{year2:02}{month:02}{day:02}")
}
pub fn generate_zeit(seed: u64) -> String {
let hour = seed % 24;
let s2 = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
let minute = (s2 >> 33) % 60;
format!("{hour:02}{minute:02}")
}
pub fn generate_date_ymd(seed: u64) -> String {
let (year, month, day) = compute_ymd(seed, seed % 730);
format!("{year:04}-{month:02}-{day:02}")
}
pub fn generate_date_edifact303(seed: u64) -> String {
let (year, month, day) = compute_ymd(seed, seed % 730);
format!("{year:04}{month:02}{day:02}120000+00")
}
pub fn generate_date_edifact102(seed: u64) -> String {
let (year, month, day) = compute_ymd(seed, seed % 730);
format!("{year:04}{month:02}{day:02}")
}
pub fn compute_ymd(seed: u64, day_offset: u64) -> (u64, u64, u64) {
let _ = seed;
let base_year = 2024u64;
let mut year = base_year;
let mut remaining = day_offset;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if remaining < days_in_year {
break;
}
remaining -= days_in_year;
year += 1;
}
let month_days = days_per_month(year);
let mut month = 1u64;
for &md in &month_days {
if remaining < md {
break;
}
remaining -= md;
month += 1;
}
let day = remaining + 1;
(year, month, day)
}
pub fn is_leap_year(year: u64) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
pub fn days_per_month(year: u64) -> [u64; 12] {
let feb = if is_leap_year(year) { 29 } else { 28 };
[31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_malo_id_format() {
let id = generate_malo_id(12345);
assert_eq!(id.len(), 11, "MaLo ID must be 11 chars, got: {id}");
assert!(
id.chars().all(|c| c.is_ascii_digit()),
"MaLo ID must be all digits, got: {id}"
);
}
#[test]
fn test_malo_id_deterministic() {
let a = generate_malo_id(42);
let b = generate_malo_id(42);
assert_eq!(a, b, "Same seed must produce same MaLo ID");
}
#[test]
fn malo_check_digit_follows_the_bdew_rule() {
for real in ["51238696781", "10023447486"] {
assert_eq!(
malo_check_digit(&real[..10]),
real[10..].parse::<u8>().unwrap(),
"{real}"
);
}
for seed in [1, 42, 7_919, 12_345, 99_999, 134_623] {
let id = generate_malo_id(seed);
assert_eq!(malo_check_digit(&id[..10]).to_string(), &id[10..], "{id}");
}
}
#[test]
fn test_melo_id_format() {
let id = generate_melo_id(67890);
assert!(
id.starts_with("DE"),
"MeLo ID must start with DE, got: {id}"
);
assert_eq!(id.len(), 33, "MeLo ID must be 33 chars, got: {id}");
assert!(
id[2..].chars().all(|c| c.is_ascii_digit()),
"MeLo ID digits portion must be all digits, got: {id}"
);
}
#[test]
fn test_melo_id_deterministic() {
let a = generate_melo_id(42);
let b = generate_melo_id(42);
assert_eq!(a, b, "Same seed must produce same MeLo ID");
}
#[test]
fn test_nelo_id_format() {
let id = generate_nelo_id(11111);
assert_eq!(
id.chars().next().unwrap(),
'E',
"NeLo ID must start with E, got: {id}"
);
assert_eq!(id.len(), 11, "NeLo ID must be 11 chars, got: {id}");
assert!(
id[1..].chars().all(|c| c.is_ascii_digit()),
"NeLo ID digits portion must be all digits, got: {id}"
);
}
#[test]
fn test_steuress_id_format() {
let id = generate_steuress_id(22222);
assert_eq!(
id.chars().next().unwrap(),
'C',
"SteuRess ID must start with C, got: {id}"
);
assert_eq!(id.len(), 11, "SteuRess ID must be 11 chars, got: {id}");
assert!(
id[1..].chars().all(|c| c.is_ascii_digit()),
"SteuRess ID digits portion must be all digits, got: {id}"
);
}
#[test]
fn test_techress_id_format() {
let id = generate_techress_id(33333);
assert_eq!(
id.chars().next().unwrap(),
'D',
"TechRess ID must start with D, got: {id}"
);
assert_eq!(id.len(), 11, "TechRess ID must be 11 chars, got: {id}");
assert!(
id[1..].chars().all(|c| c.is_ascii_digit()),
"TechRess ID digits portion must be all digits, got: {id}"
);
}
#[test]
fn test_gln_format() {
let id = generate_gln(44444);
assert_eq!(id.len(), 13, "GLN must be 13 chars, got: {id}");
assert!(
id.chars().all(|c| c.is_ascii_digit()),
"GLN must be all digits, got: {id}"
);
}
#[test]
fn test_gln_valid_check_digit() {
for seed in [0, 1, 42, 999, 123456789, u64::MAX] {
let id = generate_gln(seed);
let mut sum: u32 = 0;
for (i, ch) in id.chars().rev().enumerate() {
let d = ch.to_digit(10).unwrap();
let weight = if i % 2 == 0 { 1 } else { 3 };
sum += d * weight;
}
assert_eq!(
sum % 10,
0,
"GLN {id} (seed={seed}) fails GS1 check digit validation: sum={sum}"
);
}
}
#[test]
fn test_gln_deterministic() {
let a = generate_gln(42);
let b = generate_gln(42);
assert_eq!(a, b, "Same seed must produce same GLN");
}
#[test]
fn test_reference_id_format() {
let id = generate_reference_id(55555);
assert!(!id.is_empty(), "Reference ID must not be empty");
assert!(
!id.contains('+'),
"Reference ID must not contain +, got: {id}"
);
assert!(
!id.contains(':'),
"Reference ID must not contain :, got: {id}"
);
assert!(
!id.contains('\''),
"Reference ID must not contain ', got: {id}"
);
assert!(
!id.contains('?'),
"Reference ID must not contain ?, got: {id}"
);
}
#[test]
fn test_reference_id_deterministic() {
let a = generate_reference_id(42);
let b = generate_reference_id(42);
assert_eq!(a, b, "Same seed must produce same reference ID");
}
#[test]
fn test_different_seeds_produce_different_ids() {
let a = generate_malo_id(1);
let b = generate_malo_id(2);
assert_ne!(a, b, "Different seeds should produce different MaLo IDs");
let a = generate_gln(1);
let b = generate_gln(2);
assert_ne!(a, b, "Different seeds should produce different GLNs");
let a = generate_reference_id(1);
let b = generate_reference_id(2);
assert_ne!(
a, b,
"Different seeds should produce different reference IDs"
);
}
#[test]
fn test_generate_datum_format() {
let d = generate_datum(42);
assert_eq!(d.len(), 6, "datum must be 6 chars YYMMDD, got: {d}");
assert!(
d.chars().all(|c| c.is_ascii_digit()),
"datum must be digits"
);
}
#[test]
fn test_generate_zeit_format() {
let z = generate_zeit(42);
assert_eq!(z.len(), 4, "zeit must be 4 chars HHMM, got: {z}");
let hour: u64 = z[..2].parse().unwrap();
let min: u64 = z[2..].parse().unwrap();
assert!(hour < 24, "hour out of range: {hour}");
assert!(min < 60, "minute out of range: {min}");
}
#[test]
fn test_generate_date_ymd_format() {
let d = generate_date_ymd(42);
assert_eq!(d.len(), 10, "date_ymd must be YYYY-MM-DD, got: {d}");
assert!(d.contains('-'), "date_ymd must contain dashes");
}
#[test]
fn test_generate_date_edifact303_format() {
let d = generate_date_edifact303(42);
assert!(d.ends_with("+00"), "edifact303 must end with +00, got: {d}");
assert!(
d.len() >= 15,
"edifact303 must be at least 15 chars, got: {d}"
);
}
#[test]
fn test_generate_date_edifact102_format() {
let d = generate_date_edifact102(42);
assert_eq!(d.len(), 8, "edifact102 must be 8 chars CCYYMMDD, got: {d}");
assert!(
d.chars().all(|c| c.is_ascii_digit()),
"edifact102 must be digits"
);
}
}