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