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 if n == 0 || length == 0 {
345 return String::new();
346 }
347 let mut result = polydat::derive_support::string_for(length, "char_buf");
350 let mut h = seed;
351 for _ in 0..length {
352 h = xxhash_rust::xxh3::xxh3_64(&h.to_le_bytes());
353 result.push(chars[(h as usize) % n]);
354 }
355 result
356}
357
358fn read_file_lines(filename: &str) -> Vec<String> {
366 let content = std::fs::read_to_string(filename)
367 .unwrap_or_else(|e| panic!("failed to read file '{filename}': {e}"));
368 let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
369 if lines.is_empty() {
370 panic!("file '{filename}' has no lines");
371 }
372 lines
373}
374
375#[polydat::polydat_node(category = String)]
379fn file_line_at(
380 index: u64,
381 filename: polydat::derive_support::Const<&str>,
382 #[poly_const(read_file_lines, from = filename)] lines: &Vec<String>,
383) -> String {
384 let _ = filename;
385 let idx = index as usize;
386 lines[idx % lines.len()].clone()
387}
388
389#[polydat::polydat_node(category = String)]
411fn str_concat(parts: &[polydat::ast::Value]) -> String {
412 use polydat::ast::Value;
413 let mut out = String::new();
414 for v in parts {
415 match v {
416 Value::Str(s) => out.push_str(s),
417 Value::U64(n) => out.push_str(&n.to_string()),
418 Value::F64(n) => out.push_str(&n.to_string()),
419 Value::Bool(b) => out.push_str(&b.to_string()),
420 Value::Json(j) => out.push_str(&j.to_string()),
421 Value::Bytes(b) => out.push_str(&String::from_utf8_lossy(b)),
422 other => out.push_str(&other.to_display_string()),
425 }
426 }
427 out
428}
429
430#[polydat::polydat_node(category = String)]
442fn str_lower(input: String) -> String {
443 input.to_lowercase()
444}
445
446#[polydat::polydat_node(category = String)]
450fn str_upper(input: String) -> String {
451 input.to_uppercase()
452}
453#[polydat::polydat_node(category = Conversions)]
463fn str_to_u64(s: &str) -> u64 {
464 s.trim().parse::<u64>().unwrap_or(0)
465}
466
467#[polydat::polydat_node(category = Conversions)]
479fn str_digit_suffix(s: &str) -> u64 {
480 let s = s.trim_end();
481 let start = s
482 .char_indices()
483 .rev()
484 .take_while(|(_, c)| c.is_ascii_digit())
485 .last()
486 .map(|(i, _)| i);
487 match start {
488 Some(i) => s[i..].parse::<u64>().unwrap_or(0),
489 None => 0,
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use polydat::ast::{PolydatNode, Value};
497
498 #[test]
501 fn combinations_digits() {
502 let node = Combinations::new("0-9;0-9;0-9".to_string());
503 let mut out = [Value::None];
504 node.eval(&[Value::U64(123)], &mut out);
505 let s = out[0].as_str();
506 assert_eq!(s.len(), 3);
507 assert!(s.chars().all(|c| c.is_ascii_digit()));
508 }
509
510 #[test]
511 fn combinations_with_separator() {
512 let node = Combinations::new("0-9;0-9;0-9;-;0-9;0-9;0-9".to_string());
513 let mut out = [Value::None];
514 node.eval(&[Value::U64(0)], &mut out);
515 let s = out[0].as_str();
516 assert_eq!(s.len(), 7); assert_eq!(&s[3..4], "-");
518 }
519
520 #[test]
521 fn combinations_alpha() {
522 let node = Combinations::new("A-Z;A-Z;A-Z".to_string());
523 let mut out = [Value::None];
524 node.eval(&[Value::U64(0)], &mut out);
525 assert_eq!(out[0].as_str(), "AAA");
526 node.eval(&[Value::U64(1)], &mut out);
527 assert_eq!(out[0].as_str(), "BAA");
528 }
529
530 #[test]
531 fn combinations_cardinality() {
532 let node = Combinations::new("0-9;0-9;-;A-Z".to_string());
533 assert_eq!(node.cardinality(), 2600);
535 }
536
537 #[test]
538 fn combinations_deterministic() {
539 let node = Combinations::new("A-Z;0-9".to_string());
540 let mut out1 = [Value::None];
541 let mut out2 = [Value::None];
542 node.eval(&[Value::U64(42)], &mut out1);
543 node.eval(&[Value::U64(42)], &mut out2);
544 assert_eq!(out1[0].as_str(), out2[0].as_str());
545 }
546
547 #[test]
548 fn combinations_wraps() {
549 let node = Combinations::new("0-9".to_string());
550 let mut out = [Value::None];
551 node.eval(&[Value::U64(0)], &mut out);
552 let a = out[0].as_str().to_string();
553 node.eval(&[Value::U64(10)], &mut out);
554 assert_eq!(out[0].as_str(), &a, "should wrap at cardinality");
555 }
556
557 #[test]
560 fn number_to_words_zero() {
561 assert_eq!(u64_to_words(0), "zero");
562 }
563
564 #[test]
565 fn number_to_words_teens() {
566 assert_eq!(u64_to_words(1), "one");
567 assert_eq!(u64_to_words(11), "eleven");
568 assert_eq!(u64_to_words(19), "nineteen");
569 }
570
571 #[test]
572 fn number_to_words_tens() {
573 assert_eq!(u64_to_words(20), "twenty");
574 assert_eq!(u64_to_words(42), "forty-two");
575 assert_eq!(u64_to_words(99), "ninety-nine");
576 }
577
578 #[test]
579 fn number_to_words_hundreds() {
580 assert_eq!(u64_to_words(100), "one hundred");
581 assert_eq!(u64_to_words(123), "one hundred twenty-three");
582 assert_eq!(u64_to_words(500), "five hundred");
583 }
584
585 #[test]
586 fn number_to_words_thousands() {
587 assert_eq!(u64_to_words(1000), "one thousand");
588 assert_eq!(u64_to_words(1001), "one thousand one");
589 assert_eq!(
590 u64_to_words(12345),
591 "twelve thousand three hundred forty-five"
592 );
593 }
594
595 #[test]
596 fn number_to_words_millions() {
597 assert_eq!(u64_to_words(1_000_000), "one million");
598 assert_eq!(
599 u64_to_words(1_234_567),
600 "one million two hundred thirty-four thousand five hundred sixty-seven"
601 );
602 }
603
604 #[test]
605 fn number_to_words_large() {
606 let s = u64_to_words(1_000_000_000_000);
607 assert!(s.starts_with("one trillion"), "got: {s}");
608 }
609
610 #[test]
611 fn number_to_words_node() {
612 let node = NumberToWords::new();
613 let mut out = [Value::None];
614 node.eval(&[Value::U64(42)], &mut out);
615 assert_eq!(out[0].as_str(), "forty-two");
616 }
617
618 #[test]
621 fn str_concat_basic() {
622 let node = StrConcat::new(2);
623 let mut out = [Value::None];
624 node.eval(
625 &[Value::Str("hello ".into()), Value::Str("world".into())],
626 &mut out,
627 );
628 assert_eq!(out[0].as_str(), "hello world");
629 }
630
631 #[test]
632 fn str_concat_renders_extension_values_by_display() {
633 #[derive(Debug, Clone)]
634 struct Tag(u64);
635 impl polydat::ast::ReflectedValue for Tag {
636 fn type_name(&self) -> &str {
637 "Tag"
638 }
639 fn display(&self) -> String {
640 format!("tag#{}", self.0)
641 }
642 fn clone_reflected(&self) -> Box<dyn polydat::ast::ReflectedValue> {
643 Box::new(self.clone())
644 }
645 fn as_any(&self) -> &dyn std::any::Any {
646 self
647 }
648 }
649 let node = StrConcat::new(2);
650 let mut out = [Value::None];
651 node.eval(
652 &[Value::Str("x".into()), Value::Ext(Box::new(Tag(7)))],
653 &mut out,
654 );
655 assert_eq!(out[0].as_str(), "xtag#7");
656 }
657
658 #[test]
659 fn str_concat_mixed_types() {
660 let node = StrConcat::new(4);
661 let mut out = [Value::None];
662 node.eval(
663 &[
664 Value::Str("id=".into()),
665 Value::U64(42),
666 Value::Str(" v=".into()),
667 Value::F64(3.14),
668 ],
669 &mut out,
670 );
671 assert_eq!(out[0].as_str(), "id=42 v=3.14");
672 }
673
674 #[test]
675 fn str_concat_empty() {
676 let node = StrConcat::new(0);
677 let mut out = [Value::None];
678 node.eval(&[], &mut out);
679 assert_eq!(out[0].as_str(), "");
680 }
681
682 #[test]
683 fn str_lower_ascii_and_unicode() {
684 let node = StrLower::new();
685 let mut out = [Value::None];
686 node.eval(&[Value::Str("OTHER_M8".into())], &mut out);
687 assert_eq!(out[0].as_str(), "other_m8");
688 node.eval(&[Value::Str("ÄPFEL".into())], &mut out);
690 assert_eq!(out[0].as_str(), "äpfel");
691 }
692
693 #[test]
694 fn str_lower_idempotent_on_already_lowercase() {
695 let node = StrLower::new();
696 let mut out = [Value::None];
697 node.eval(&[Value::Str("fknn_oat_other".into())], &mut out);
698 assert_eq!(out[0].as_str(), "fknn_oat_other");
699 }
700
701 #[test]
702 fn str_upper_ascii_and_unicode() {
703 let node = StrUpper::new();
704 let mut out = [Value::None];
705 node.eval(&[Value::Str("other_m8".into())], &mut out);
706 assert_eq!(out[0].as_str(), "OTHER_M8");
707 node.eval(&[Value::Str("äpfel".into())], &mut out);
708 assert_eq!(out[0].as_str(), "ÄPFEL");
709 }
710
711 }