1pub struct ParsedCombinations {
35 pub segments: Vec<Segment>,
37 pub modulus: u64,
39}
40
41impl polydat::derive_support::PolydatSetup for ParsedCombinations {}
42
43pub enum Segment {
45 Charset(Vec<char>),
47 Literal(String),
49}
50
51impl ParsedCombinations {
52 pub fn from_pattern(pattern: &str) -> Self {
56 let mut segments = Vec::new();
57 let mut modulus: u64 = 1;
58 for spec in pattern.split(';') {
59 let chars = parse_charset(spec);
60 if chars.len() == 1 && !spec.contains('-') {
61 segments.push(Segment::Literal(chars[0].to_string()));
62 } else if chars.is_empty() {
63 segments.push(Segment::Literal(spec.to_string()));
64 } else {
65 modulus = modulus.saturating_mul(chars.len() as u64);
66 segments.push(Segment::Charset(chars));
67 }
68 }
69 Self { segments, modulus }
70 }
71}
72
73#[polydat::polydat_node(category = String)]
76fn combinations(
77 input: u64,
78 pattern: polydat::derive_support::Const<&str>,
79 #[poly_const(ParsedCombinations::from_pattern, from = pattern)] parsed: &ParsedCombinations,
80) -> String {
81 let mut remainder = if parsed.modulus > 0 {
82 input % parsed.modulus
83 } else {
84 input
85 };
86 let mut result = String::with_capacity(parsed.segments.len() * 2);
87 for seg in &parsed.segments {
88 match seg {
89 Segment::Literal(s) => result.push_str(s),
90 Segment::Charset(chars) => {
91 let radix = chars.len() as u64;
92 if radix > 0 {
93 let idx = (remainder % radix) as usize;
94 result.push(chars[idx]);
95 remainder /= radix;
96 }
97 }
98 }
99 }
100 result
101}
102
103impl Combinations {
104 pub fn cardinality(&self) -> u64 {
106 self.parsed.modulus
107 }
108}
109
110fn parse_charset(spec: &str) -> Vec<char> {
112 let mut chars = Vec::new();
113 let spec_chars: Vec<char> = spec.chars().collect();
114 let mut i = 0;
115 while i < spec_chars.len() {
116 if i + 2 < spec_chars.len() && spec_chars[i + 1] == '-' {
117 let start = spec_chars[i];
119 let end = spec_chars[i + 2];
120 for c in start..=end {
121 chars.push(c);
122 }
123 i += 3;
124 } else {
125 chars.push(spec_chars[i]);
126 i += 1;
127 }
128 }
129 chars
130}
131
132#[polydat::polydat_node(category = String)]
147fn number_to_words(input: u64) -> String {
148 u64_to_words(input)
149}
150
151const ONES: [&str; 20] = [
152 "zero",
153 "one",
154 "two",
155 "three",
156 "four",
157 "five",
158 "six",
159 "seven",
160 "eight",
161 "nine",
162 "ten",
163 "eleven",
164 "twelve",
165 "thirteen",
166 "fourteen",
167 "fifteen",
168 "sixteen",
169 "seventeen",
170 "eighteen",
171 "nineteen",
172];
173
174const TENS: [&str; 10] = [
175 "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
176];
177
178const SCALES: [&str; 7] = [
179 "",
180 "thousand",
181 "million",
182 "billion",
183 "trillion",
184 "quadrillion",
185 "quintillion",
186];
187
188fn u64_to_words(n: u64) -> String {
189 if n < 20 {
190 return ONES[n as usize].to_string();
191 }
192
193 let mut buf = String::with_capacity(64);
194 let mut chunks = [0u32; 7];
195 let mut num_chunks = 0;
196 let mut remaining = n;
197
198 while remaining > 0 {
199 chunks[num_chunks] = (remaining % 1000) as u32;
200 num_chunks += 1;
201 remaining /= 1000;
202 }
203
204 let mut first = true;
205 for i in (0..num_chunks).rev() {
206 let chunk = chunks[i];
207 if chunk > 0 {
208 if !first {
209 buf.push(' ');
210 }
211 first = false;
212 append_chunk_to_words(&mut buf, chunk);
213 if i > 0 && i < SCALES.len() {
214 buf.push(' ');
215 buf.push_str(SCALES[i]);
216 }
217 }
218 }
219
220 buf
221}
222
223fn append_chunk_to_words(buf: &mut String, n: u32) {
224 let hundreds = n / 100;
225 let remainder = n % 100;
226
227 let mut has_hundreds = false;
228 if hundreds > 0 {
229 buf.push_str(ONES[hundreds as usize]);
230 buf.push_str(" hundred");
231 has_hundreds = true;
232 }
233
234 if remainder >= 20 {
235 if has_hundreds {
236 buf.push(' ');
237 }
238 let tens = remainder / 10;
239 let ones = remainder % 10;
240 buf.push_str(TENS[tens as usize]);
241 if ones > 0 {
242 buf.push('-');
243 buf.push_str(ONES[ones as usize]);
244 }
245 } else if remainder > 0 {
246 if has_hundreds {
247 buf.push(' ');
248 }
249 buf.push_str(ONES[remainder as usize]);
250 }
251}
252
253#[polydat::polydat_node(category = String)]
264fn hashed_uuid(input: u64) -> String {
265 let h1 = xxhash_rust::xxh3::xxh3_64(&input.to_le_bytes());
267 let h2 = xxhash_rust::xxh3::xxh3_64(&h1.to_le_bytes());
268 let mut bytes = [0u8; 16];
269 bytes[..8].copy_from_slice(&h1.to_le_bytes());
270 bytes[8..].copy_from_slice(&h2.to_le_bytes());
271 bytes[6] = (bytes[6] & 0x0F) | 0x40;
273 bytes[8] = (bytes[8] & 0x3F) | 0x80;
275 format!(
276 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
277 bytes[0],
278 bytes[1],
279 bytes[2],
280 bytes[3],
281 bytes[4],
282 bytes[5],
283 bytes[6],
284 bytes[7],
285 bytes[8],
286 bytes[9],
287 bytes[10],
288 bytes[11],
289 bytes[12],
290 bytes[13],
291 bytes[14],
292 bytes[15],
293 )
294}
295
296fn expand_charset(charset: &str) -> Vec<char> {
310 if charset.is_empty() {
311 return ('a'..='z').collect();
312 }
313 let mut result = Vec::new();
314 let chars_vec: Vec<char> = charset.chars().collect();
315 let mut i = 0;
316 while i < chars_vec.len() {
317 if i + 2 < chars_vec.len() && chars_vec[i + 1] == '-' {
318 for c in chars_vec[i]..=chars_vec[i + 2] {
319 result.push(c);
320 }
321 i += 3;
322 } else {
323 result.push(chars_vec[i]);
324 i += 1;
325 }
326 }
327 if result.is_empty() {
328 ('a'..='z').collect()
329 } else {
330 result
331 }
332}
333
334#[polydat::polydat_node(category = String)]
337fn char_buf(
338 seed: u64,
339 charset: polydat::derive_support::Const<&str>,
340 length: u64,
341 #[poly_const(expand_charset, from = charset)] chars: &Vec<char>,
342) -> String {
343 let n = chars.len();
344 let len = length as usize;
345 if n == 0 || len == 0 {
346 return String::new();
347 }
348 let mut result = String::with_capacity(len);
349 let mut h = seed;
350 for _ in 0..len {
351 h = xxhash_rust::xxh3::xxh3_64(&h.to_le_bytes());
352 result.push(chars[(h as usize) % n]);
353 }
354 result
355}
356
357fn read_file_lines(filename: &str) -> Vec<String> {
365 let content = std::fs::read_to_string(filename)
366 .unwrap_or_else(|e| panic!("failed to read file '{filename}': {e}"));
367 let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
368 if lines.is_empty() {
369 panic!("file '{filename}' has no lines");
370 }
371 lines
372}
373
374#[polydat::polydat_node(category = String)]
378fn file_line_at(
379 index: u64,
380 filename: polydat::derive_support::Const<&str>,
381 #[poly_const(read_file_lines, from = filename)] lines: &Vec<String>,
382) -> String {
383 let _ = filename;
384 let idx = index as usize;
385 lines[idx % lines.len()].clone()
386}
387
388#[polydat::polydat_node(category = String)]
410fn str_concat(parts: &[polydat::ast::Value]) -> String {
411 use polydat::ast::Value;
412 let mut out = String::new();
413 for v in parts {
414 match v {
415 Value::Str(s) => out.push_str(s),
416 Value::U64(n) => out.push_str(&n.to_string()),
417 Value::F64(n) => out.push_str(&n.to_string()),
418 Value::Bool(b) => out.push_str(&b.to_string()),
419 Value::Json(j) => out.push_str(&j.to_string()),
420 Value::Bytes(b) => out.push_str(&String::from_utf8_lossy(b)),
421 other => out.push_str(&other.to_display_string()),
424 }
425 }
426 out
427}
428
429#[polydat::polydat_node(category = String)]
441fn str_lower(input: String) -> String {
442 input.to_lowercase()
443}
444
445#[polydat::polydat_node(category = String)]
449fn str_upper(input: String) -> String {
450 input.to_uppercase()
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use polydat::ast::{PolydatNode, Value};
457
458 #[test]
461 fn combinations_digits() {
462 let node = Combinations::new("0-9;0-9;0-9".to_string());
463 let mut out = [Value::None];
464 node.eval(&[Value::U64(123)], &mut out);
465 let s = out[0].as_str();
466 assert_eq!(s.len(), 3);
467 assert!(s.chars().all(|c| c.is_ascii_digit()));
468 }
469
470 #[test]
471 fn combinations_with_separator() {
472 let node = Combinations::new("0-9;0-9;0-9;-;0-9;0-9;0-9".to_string());
473 let mut out = [Value::None];
474 node.eval(&[Value::U64(0)], &mut out);
475 let s = out[0].as_str();
476 assert_eq!(s.len(), 7); assert_eq!(&s[3..4], "-");
478 }
479
480 #[test]
481 fn combinations_alpha() {
482 let node = Combinations::new("A-Z;A-Z;A-Z".to_string());
483 let mut out = [Value::None];
484 node.eval(&[Value::U64(0)], &mut out);
485 assert_eq!(out[0].as_str(), "AAA");
486 node.eval(&[Value::U64(1)], &mut out);
487 assert_eq!(out[0].as_str(), "BAA");
488 }
489
490 #[test]
491 fn combinations_cardinality() {
492 let node = Combinations::new("0-9;0-9;-;A-Z".to_string());
493 assert_eq!(node.cardinality(), 2600);
495 }
496
497 #[test]
498 fn combinations_deterministic() {
499 let node = Combinations::new("A-Z;0-9".to_string());
500 let mut out1 = [Value::None];
501 let mut out2 = [Value::None];
502 node.eval(&[Value::U64(42)], &mut out1);
503 node.eval(&[Value::U64(42)], &mut out2);
504 assert_eq!(out1[0].as_str(), out2[0].as_str());
505 }
506
507 #[test]
508 fn combinations_wraps() {
509 let node = Combinations::new("0-9".to_string());
510 let mut out = [Value::None];
511 node.eval(&[Value::U64(0)], &mut out);
512 let a = out[0].as_str().to_string();
513 node.eval(&[Value::U64(10)], &mut out);
514 assert_eq!(out[0].as_str(), &a, "should wrap at cardinality");
515 }
516
517 #[test]
520 fn number_to_words_zero() {
521 assert_eq!(u64_to_words(0), "zero");
522 }
523
524 #[test]
525 fn number_to_words_teens() {
526 assert_eq!(u64_to_words(1), "one");
527 assert_eq!(u64_to_words(11), "eleven");
528 assert_eq!(u64_to_words(19), "nineteen");
529 }
530
531 #[test]
532 fn number_to_words_tens() {
533 assert_eq!(u64_to_words(20), "twenty");
534 assert_eq!(u64_to_words(42), "forty-two");
535 assert_eq!(u64_to_words(99), "ninety-nine");
536 }
537
538 #[test]
539 fn number_to_words_hundreds() {
540 assert_eq!(u64_to_words(100), "one hundred");
541 assert_eq!(u64_to_words(123), "one hundred twenty-three");
542 assert_eq!(u64_to_words(500), "five hundred");
543 }
544
545 #[test]
546 fn number_to_words_thousands() {
547 assert_eq!(u64_to_words(1000), "one thousand");
548 assert_eq!(u64_to_words(1001), "one thousand one");
549 assert_eq!(
550 u64_to_words(12345),
551 "twelve thousand three hundred forty-five"
552 );
553 }
554
555 #[test]
556 fn number_to_words_millions() {
557 assert_eq!(u64_to_words(1_000_000), "one million");
558 assert_eq!(
559 u64_to_words(1_234_567),
560 "one million two hundred thirty-four thousand five hundred sixty-seven"
561 );
562 }
563
564 #[test]
565 fn number_to_words_large() {
566 let s = u64_to_words(1_000_000_000_000);
567 assert!(s.starts_with("one trillion"), "got: {s}");
568 }
569
570 #[test]
571 fn number_to_words_node() {
572 let node = NumberToWords::new();
573 let mut out = [Value::None];
574 node.eval(&[Value::U64(42)], &mut out);
575 assert_eq!(out[0].as_str(), "forty-two");
576 }
577
578 #[test]
581 fn str_concat_basic() {
582 let node = StrConcat::new(2);
583 let mut out = [Value::None];
584 node.eval(
585 &[Value::Str("hello ".into()), Value::Str("world".into())],
586 &mut out,
587 );
588 assert_eq!(out[0].as_str(), "hello world");
589 }
590
591 #[test]
592 fn str_concat_renders_extension_values_by_display() {
593 #[derive(Debug, Clone)]
594 struct Tag(u64);
595 impl polydat::ast::ReflectedValue for Tag {
596 fn type_name(&self) -> &str {
597 "Tag"
598 }
599 fn display(&self) -> String {
600 format!("tag#{}", self.0)
601 }
602 fn clone_reflected(&self) -> Box<dyn polydat::ast::ReflectedValue> {
603 Box::new(self.clone())
604 }
605 fn as_any(&self) -> &dyn std::any::Any {
606 self
607 }
608 }
609 let node = StrConcat::new(2);
610 let mut out = [Value::None];
611 node.eval(
612 &[Value::Str("x".into()), Value::Ext(Box::new(Tag(7)))],
613 &mut out,
614 );
615 assert_eq!(out[0].as_str(), "xtag#7");
616 }
617
618 #[test]
619 fn str_concat_mixed_types() {
620 let node = StrConcat::new(4);
621 let mut out = [Value::None];
622 node.eval(
623 &[
624 Value::Str("id=".into()),
625 Value::U64(42),
626 Value::Str(" v=".into()),
627 Value::F64(3.14),
628 ],
629 &mut out,
630 );
631 assert_eq!(out[0].as_str(), "id=42 v=3.14");
632 }
633
634 #[test]
635 fn str_concat_empty() {
636 let node = StrConcat::new(0);
637 let mut out = [Value::None];
638 node.eval(&[], &mut out);
639 assert_eq!(out[0].as_str(), "");
640 }
641
642 #[test]
643 fn str_lower_ascii_and_unicode() {
644 let node = StrLower::new();
645 let mut out = [Value::None];
646 node.eval(&[Value::Str("OTHER_M8".into())], &mut out);
647 assert_eq!(out[0].as_str(), "other_m8");
648 node.eval(&[Value::Str("ÄPFEL".into())], &mut out);
650 assert_eq!(out[0].as_str(), "äpfel");
651 }
652
653 #[test]
654 fn str_lower_idempotent_on_already_lowercase() {
655 let node = StrLower::new();
656 let mut out = [Value::None];
657 node.eval(&[Value::Str("fknn_oat_other".into())], &mut out);
658 assert_eq!(out[0].as_str(), "fknn_oat_other");
659 }
660
661 #[test]
662 fn str_upper_ascii_and_unicode() {
663 let node = StrUpper::new();
664 let mut out = [Value::None];
665 node.eval(&[Value::Str("other_m8".into())], &mut out);
666 assert_eq!(out[0].as_str(), "OTHER_M8");
667 node.eval(&[Value::Str("äpfel".into())], &mut out);
668 assert_eq!(out[0].as_str(), "ÄPFEL");
669 }
670
671 }