1pub fn generate_malo_id(seed: u64) -> String {
8 let base = seed % 10_000_000_000;
10 let base_str = format!("{base:010}");
11 let check = malo_check_digit(&base_str);
12 format!("{base_str}{check}")
13}
14
15pub fn malo_check_digit(digits: &str) -> u8 {
20 let sum: u32 = digits
21 .chars()
22 .enumerate()
23 .map(|(i, c)| {
24 let d = c.to_digit(10).expect("input must be all digits");
25 if (i + 1) % 2 == 0 {
26 2 * d
27 } else {
28 d
29 }
30 })
31 .sum();
32 ((10 - (sum % 10)) % 10) as u8
33}
34
35pub fn generate_melo_id(seed: u64) -> String {
37 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}")
42}
43
44pub fn generate_nelo_id(seed: u64) -> String {
46 let digits = seed % 10_000_000_000;
47 format!("E{digits:010}")
48}
49
50pub fn generate_steuress_id(seed: u64) -> String {
52 let digits = seed % 10_000_000_000;
53 format!("C{digits:010}")
54}
55
56pub fn generate_techress_id(seed: u64) -> String {
58 let digits = seed % 10_000_000_000;
59 format!("D{digits:010}")
60}
61
62pub fn generate_gln(seed: u64) -> String {
67 let base = seed % 1_000_000_000_000;
68 let base_str = format!("{base:012}");
69 let check = gs1_check_digit(&base_str);
70 format!("{base_str}{check}")
71}
72
73pub fn generate_reference_id(seed: u64) -> String {
78 const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
79 let len = 12;
80 let mut result = Vec::with_capacity(len);
81 let mut state = seed;
82 for _ in 0..len {
83 state = state
85 .wrapping_mul(6_364_136_223_846_793_005)
86 .wrapping_add(1);
87 let idx = (state >> 33) as usize % CHARSET.len();
88 result.push(CHARSET[idx]);
89 }
90 String::from_utf8(result).expect("charset is ASCII")
91}
92
93fn gs1_check_digit(digits: &str) -> u8 {
98 let mut sum: u32 = 0;
99 for (i, ch) in digits.chars().rev().enumerate() {
100 let d = ch.to_digit(10).expect("input must be all digits");
101 let weight = if i % 2 == 0 { 3 } else { 1 };
102 sum += d * weight;
103 }
104 ((10 - (sum % 10)) % 10) as u8
105}
106
107pub fn generate_datum(seed: u64) -> String {
109 let (year, month, day) = compute_ymd(seed, seed % 730);
110 let year2 = year % 100;
111 format!("{year2:02}{month:02}{day:02}")
112}
113
114pub fn generate_zeit(seed: u64) -> String {
116 let hour = seed % 24;
117 let s2 = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
118 let minute = (s2 >> 33) % 60;
119 format!("{hour:02}{minute:02}")
120}
121
122pub fn generate_date_ymd(seed: u64) -> String {
124 let (year, month, day) = compute_ymd(seed, seed % 730);
125 format!("{year:04}-{month:02}-{day:02}")
126}
127
128pub fn generate_date_edifact303(seed: u64) -> String {
130 let (year, month, day) = compute_ymd(seed, seed % 730);
131 format!("{year:04}{month:02}{day:02}120000+00")
132}
133
134pub fn generate_date_edifact102(seed: u64) -> String {
136 let (year, month, day) = compute_ymd(seed, seed % 730);
137 format!("{year:04}{month:02}{day:02}")
138}
139
140pub fn compute_ymd(seed: u64, day_offset: u64) -> (u64, u64, u64) {
142 let _ = seed;
143 let base_year = 2024u64;
144 let mut year = base_year;
145 let mut remaining = day_offset;
146 loop {
147 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
148 if remaining < days_in_year {
149 break;
150 }
151 remaining -= days_in_year;
152 year += 1;
153 }
154 let month_days = days_per_month(year);
155 let mut month = 1u64;
156 for &md in &month_days {
157 if remaining < md {
158 break;
159 }
160 remaining -= md;
161 month += 1;
162 }
163 let day = remaining + 1;
164 (year, month, day)
165}
166
167pub fn is_leap_year(year: u64) -> bool {
169 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
170}
171
172pub fn days_per_month(year: u64) -> [u64; 12] {
174 let feb = if is_leap_year(year) { 29 } else { 28 };
175 [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_malo_id_format() {
184 let id = generate_malo_id(12345);
185 assert_eq!(id.len(), 11, "MaLo ID must be 11 chars, got: {id}");
186 assert!(
187 id.chars().all(|c| c.is_ascii_digit()),
188 "MaLo ID must be all digits, got: {id}"
189 );
190 }
191
192 #[test]
193 fn test_malo_id_deterministic() {
194 let a = generate_malo_id(42);
195 let b = generate_malo_id(42);
196 assert_eq!(a, b, "Same seed must produce same MaLo ID");
197 }
198
199 #[test]
204 fn malo_check_digit_follows_the_bdew_rule() {
205 for real in ["51238696781", "10023447486"] {
206 assert_eq!(
207 malo_check_digit(&real[..10]),
208 real[10..].parse::<u8>().unwrap(),
209 "{real}"
210 );
211 }
212 for seed in [1, 42, 7_919, 12_345, 99_999, 134_623] {
213 let id = generate_malo_id(seed);
214 assert_eq!(malo_check_digit(&id[..10]).to_string(), &id[10..], "{id}");
215 }
216 }
217
218 #[test]
219 fn test_melo_id_format() {
220 let id = generate_melo_id(67890);
221 assert!(
222 id.starts_with("DE"),
223 "MeLo ID must start with DE, got: {id}"
224 );
225 assert_eq!(id.len(), 33, "MeLo ID must be 33 chars, got: {id}");
226 assert!(
227 id[2..].chars().all(|c| c.is_ascii_digit()),
228 "MeLo ID digits portion must be all digits, got: {id}"
229 );
230 }
231
232 #[test]
233 fn test_melo_id_deterministic() {
234 let a = generate_melo_id(42);
235 let b = generate_melo_id(42);
236 assert_eq!(a, b, "Same seed must produce same MeLo ID");
237 }
238
239 #[test]
240 fn test_nelo_id_format() {
241 let id = generate_nelo_id(11111);
242 assert_eq!(
243 id.chars().next().unwrap(),
244 'E',
245 "NeLo ID must start with E, got: {id}"
246 );
247 assert_eq!(id.len(), 11, "NeLo ID must be 11 chars, got: {id}");
248 assert!(
249 id[1..].chars().all(|c| c.is_ascii_digit()),
250 "NeLo ID digits portion must be all digits, got: {id}"
251 );
252 }
253
254 #[test]
255 fn test_steuress_id_format() {
256 let id = generate_steuress_id(22222);
257 assert_eq!(
258 id.chars().next().unwrap(),
259 'C',
260 "SteuRess ID must start with C, got: {id}"
261 );
262 assert_eq!(id.len(), 11, "SteuRess ID must be 11 chars, got: {id}");
263 assert!(
264 id[1..].chars().all(|c| c.is_ascii_digit()),
265 "SteuRess ID digits portion must be all digits, got: {id}"
266 );
267 }
268
269 #[test]
270 fn test_techress_id_format() {
271 let id = generate_techress_id(33333);
272 assert_eq!(
273 id.chars().next().unwrap(),
274 'D',
275 "TechRess ID must start with D, got: {id}"
276 );
277 assert_eq!(id.len(), 11, "TechRess ID must be 11 chars, got: {id}");
278 assert!(
279 id[1..].chars().all(|c| c.is_ascii_digit()),
280 "TechRess ID digits portion must be all digits, got: {id}"
281 );
282 }
283
284 #[test]
285 fn test_gln_format() {
286 let id = generate_gln(44444);
287 assert_eq!(id.len(), 13, "GLN must be 13 chars, got: {id}");
288 assert!(
289 id.chars().all(|c| c.is_ascii_digit()),
290 "GLN must be all digits, got: {id}"
291 );
292 }
293
294 #[test]
295 fn test_gln_valid_check_digit() {
296 for seed in [0, 1, 42, 999, 123456789, u64::MAX] {
297 let id = generate_gln(seed);
298 let mut sum: u32 = 0;
300 for (i, ch) in id.chars().rev().enumerate() {
301 let d = ch.to_digit(10).unwrap();
302 let weight = if i % 2 == 0 { 1 } else { 3 };
303 sum += d * weight;
304 }
305 assert_eq!(
306 sum % 10,
307 0,
308 "GLN {id} (seed={seed}) fails GS1 check digit validation: sum={sum}"
309 );
310 }
311 }
312
313 #[test]
314 fn test_gln_deterministic() {
315 let a = generate_gln(42);
316 let b = generate_gln(42);
317 assert_eq!(a, b, "Same seed must produce same GLN");
318 }
319
320 #[test]
321 fn test_reference_id_format() {
322 let id = generate_reference_id(55555);
323 assert!(!id.is_empty(), "Reference ID must not be empty");
324 assert!(
325 !id.contains('+'),
326 "Reference ID must not contain +, got: {id}"
327 );
328 assert!(
329 !id.contains(':'),
330 "Reference ID must not contain :, got: {id}"
331 );
332 assert!(
333 !id.contains('\''),
334 "Reference ID must not contain ', got: {id}"
335 );
336 assert!(
337 !id.contains('?'),
338 "Reference ID must not contain ?, got: {id}"
339 );
340 }
341
342 #[test]
343 fn test_reference_id_deterministic() {
344 let a = generate_reference_id(42);
345 let b = generate_reference_id(42);
346 assert_eq!(a, b, "Same seed must produce same reference ID");
347 }
348
349 #[test]
350 fn test_different_seeds_produce_different_ids() {
351 let a = generate_malo_id(1);
353 let b = generate_malo_id(2);
354 assert_ne!(a, b, "Different seeds should produce different MaLo IDs");
355
356 let a = generate_gln(1);
357 let b = generate_gln(2);
358 assert_ne!(a, b, "Different seeds should produce different GLNs");
359
360 let a = generate_reference_id(1);
361 let b = generate_reference_id(2);
362 assert_ne!(
363 a, b,
364 "Different seeds should produce different reference IDs"
365 );
366 }
367
368 #[test]
369 fn test_generate_datum_format() {
370 let d = generate_datum(42);
371 assert_eq!(d.len(), 6, "datum must be 6 chars YYMMDD, got: {d}");
372 assert!(
373 d.chars().all(|c| c.is_ascii_digit()),
374 "datum must be digits"
375 );
376 }
377
378 #[test]
379 fn test_generate_zeit_format() {
380 let z = generate_zeit(42);
381 assert_eq!(z.len(), 4, "zeit must be 4 chars HHMM, got: {z}");
382 let hour: u64 = z[..2].parse().unwrap();
383 let min: u64 = z[2..].parse().unwrap();
384 assert!(hour < 24, "hour out of range: {hour}");
385 assert!(min < 60, "minute out of range: {min}");
386 }
387
388 #[test]
389 fn test_generate_date_ymd_format() {
390 let d = generate_date_ymd(42);
391 assert_eq!(d.len(), 10, "date_ymd must be YYYY-MM-DD, got: {d}");
392 assert!(d.contains('-'), "date_ymd must contain dashes");
393 }
394
395 #[test]
396 fn test_generate_date_edifact303_format() {
397 let d = generate_date_edifact303(42);
398 assert!(d.ends_with("+00"), "edifact303 must end with +00, got: {d}");
399 assert!(
400 d.len() >= 15,
401 "edifact303 must be at least 15 chars, got: {d}"
402 );
403 }
404
405 #[test]
406 fn test_generate_date_edifact102_format() {
407 let d = generate_date_edifact102(42);
408 assert_eq!(d.len(), 8, "edifact102 must be 8 chars CCYYMMDD, got: {d}");
409 assert!(
410 d.chars().all(|c| c.is_ascii_digit()),
411 "edifact102 must be digits"
412 );
413 }
414}