1pub struct ParsedCombinations {
34 pub segments: Vec<Segment>,
36 pub modulus: u64,
38}
39
40impl polydat::derive_support::PolydatSetup for ParsedCombinations {}
41
42pub enum Segment {
44 Charset(Vec<char>),
46 Literal(String),
48}
49
50impl ParsedCombinations {
51 pub fn from_pattern(pattern: &str) -> Self {
55 let mut segments = Vec::new();
56 let mut modulus: u64 = 1;
57 for spec in pattern.split(';') {
58 let chars = parse_charset(spec);
59 if chars.len() == 1 && !spec.contains('-') {
60 segments.push(Segment::Literal(chars[0].to_string()));
61 } else if chars.is_empty() {
62 segments.push(Segment::Literal(spec.to_string()));
63 } else {
64 modulus = modulus.saturating_mul(chars.len() as u64);
65 segments.push(Segment::Charset(chars));
66 }
67 }
68 Self { segments, modulus }
69 }
70}
71
72#[polydat::polydat_node(category = String)]
75fn combinations(
76 input: u64,
77 pattern: polydat::derive_support::Const<&str>,
78 #[poly_const(ParsedCombinations::from_pattern, from = pattern)] parsed: &ParsedCombinations,
79) -> String {
80 let mut remainder = if parsed.modulus > 0 {
81 input % parsed.modulus
82 } else {
83 input
84 };
85 let mut result = String::with_capacity(parsed.segments.len() * 2);
86 for seg in &parsed.segments {
87 match seg {
88 Segment::Literal(s) => result.push_str(s),
89 Segment::Charset(chars) => {
90 let radix = chars.len() as u64;
91 if radix > 0 {
92 let idx = (remainder % radix) as usize;
93 result.push(chars[idx]);
94 remainder /= radix;
95 }
96 }
97 }
98 }
99 result
100}
101
102impl Combinations {
103 pub fn cardinality(&self) -> u64 {
105 self.parsed.modulus
106 }
107}
108
109fn parse_charset(spec: &str) -> Vec<char> {
111 let mut chars = Vec::new();
112 let spec_chars: Vec<char> = spec.chars().collect();
113 let mut i = 0;
114 while i < spec_chars.len() {
115 if i + 2 < spec_chars.len() && spec_chars[i + 1] == '-' {
116 let start = spec_chars[i];
118 let end = spec_chars[i + 2];
119 for c in start..=end {
120 chars.push(c);
121 }
122 i += 3;
123 } else {
124 chars.push(spec_chars[i]);
125 i += 1;
126 }
127 }
128 chars
129}
130
131#[polydat::polydat_node(category = String)]
145fn number_to_words(input: u64) -> String {
146 u64_to_words(input)
147}
148
149const ONES: [&str; 20] = [
150 "zero",
151 "one",
152 "two",
153 "three",
154 "four",
155 "five",
156 "six",
157 "seven",
158 "eight",
159 "nine",
160 "ten",
161 "eleven",
162 "twelve",
163 "thirteen",
164 "fourteen",
165 "fifteen",
166 "sixteen",
167 "seventeen",
168 "eighteen",
169 "nineteen",
170];
171
172const TENS: [&str; 10] = [
173 "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
174];
175
176const SCALES: [&str; 7] = [
177 "",
178 "thousand",
179 "million",
180 "billion",
181 "trillion",
182 "quadrillion",
183 "quintillion",
184];
185
186fn u64_to_words(n: u64) -> String {
187 if n < 20 {
188 return ONES[n as usize].to_string();
189 }
190
191 let mut buf = String::with_capacity(64);
192 let mut chunks = [0u32; 7];
193 let mut num_chunks = 0;
194 let mut remaining = n;
195
196 while remaining > 0 {
197 chunks[num_chunks] = (remaining % 1000) as u32;
198 num_chunks += 1;
199 remaining /= 1000;
200 }
201
202 let mut first = true;
203 for i in (0..num_chunks).rev() {
204 let chunk = chunks[i];
205 if chunk > 0 {
206 if !first {
207 buf.push(' ');
208 }
209 first = false;
210 append_chunk_to_words(&mut buf, chunk);
211 if i > 0 && i < SCALES.len() {
212 buf.push(' ');
213 buf.push_str(SCALES[i]);
214 }
215 }
216 }
217
218 buf
219}
220
221fn append_chunk_to_words(buf: &mut String, n: u32) {
222 let hundreds = n / 100;
223 let remainder = n % 100;
224
225 let mut has_hundreds = false;
226 if hundreds > 0 {
227 buf.push_str(ONES[hundreds as usize]);
228 buf.push_str(" hundred");
229 has_hundreds = true;
230 }
231
232 if remainder >= 20 {
233 if has_hundreds {
234 buf.push(' ');
235 }
236 let tens = remainder / 10;
237 let ones = remainder % 10;
238 buf.push_str(TENS[tens as usize]);
239 if ones > 0 {
240 buf.push('-');
241 buf.push_str(ONES[ones as usize]);
242 }
243 } else if remainder > 0 {
244 if has_hundreds {
245 buf.push(' ');
246 }
247 buf.push_str(ONES[remainder as usize]);
248 }
249}
250
251#[polydat::polydat_node(category = String)]
262fn hashed_uuid(input: u64) -> String {
263 let h1 = xxhash_rust::xxh3::xxh3_64(&input.to_le_bytes());
265 let h2 = xxhash_rust::xxh3::xxh3_64(&h1.to_le_bytes());
266 let mut bytes = [0u8; 16];
267 bytes[..8].copy_from_slice(&h1.to_le_bytes());
268 bytes[8..].copy_from_slice(&h2.to_le_bytes());
269 bytes[6] = (bytes[6] & 0x0F) | 0x40;
271 bytes[8] = (bytes[8] & 0x3F) | 0x80;
273 format!(
274 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
275 bytes[0],
276 bytes[1],
277 bytes[2],
278 bytes[3],
279 bytes[4],
280 bytes[5],
281 bytes[6],
282 bytes[7],
283 bytes[8],
284 bytes[9],
285 bytes[10],
286 bytes[11],
287 bytes[12],
288 bytes[13],
289 bytes[14],
290 bytes[15],
291 )
292}
293
294fn expand_charset(charset: &str) -> Vec<char> {
308 if charset.is_empty() {
309 return ('a'..='z').collect();
310 }
311 let mut result = Vec::new();
312 let chars_vec: Vec<char> = charset.chars().collect();
313 let mut i = 0;
314 while i < chars_vec.len() {
315 if i + 2 < chars_vec.len() && chars_vec[i + 1] == '-' {
316 for c in chars_vec[i]..=chars_vec[i + 2] {
317 result.push(c);
318 }
319 i += 3;
320 } else {
321 result.push(chars_vec[i]);
322 i += 1;
323 }
324 }
325 if result.is_empty() {
326 ('a'..='z').collect()
327 } else {
328 result
329 }
330}
331
332#[polydat::polydat_node(category = String)]
335fn char_buf(
336 seed: u64,
337 charset: polydat::derive_support::Const<&str>,
338 length: u64,
339 #[poly_const(expand_charset, from = charset)] chars: &Vec<char>,
340) -> String {
341 let n = chars.len();
342 let len = length as usize;
343 if n == 0 || len == 0 {
344 return String::new();
345 }
346 let mut result = String::with_capacity(len);
347 let mut h = seed;
348 for _ in 0..len {
349 h = xxhash_rust::xxh3::xxh3_64(&h.to_le_bytes());
350 result.push(chars[(h as usize) % n]);
351 }
352 result
353}
354
355fn read_file_lines(filename: &str) -> Vec<String> {
363 let content = std::fs::read_to_string(filename)
364 .unwrap_or_else(|e| panic!("failed to read file '{filename}': {e}"));
365 let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
366 if lines.is_empty() {
367 panic!("file '{filename}' has no lines");
368 }
369 lines
370}
371
372#[polydat::polydat_node(category = String)]
376fn file_line_at(
377 index: u64,
378 filename: polydat::derive_support::Const<&str>,
379 #[poly_const(read_file_lines, from = filename)] lines: &Vec<String>,
380) -> String {
381 let _ = filename;
382 let idx = index as usize;
383 lines[idx % lines.len()].clone()
384}
385
386#[polydat::polydat_node(category = String)]
408fn str_concat(parts: &[polydat::ast::Value]) -> String {
409 use polydat::ast::Value;
410 let mut out = String::new();
411 for v in parts {
412 match v {
413 Value::Str(s) => out.push_str(s),
414 Value::U64(n) => out.push_str(&n.to_string()),
415 Value::F64(n) => out.push_str(&n.to_string()),
416 Value::Bool(b) => out.push_str(&b.to_string()),
417 Value::Json(j) => out.push_str(&j.to_string()),
418 Value::Bytes(b) => out.push_str(&String::from_utf8_lossy(b)),
419 other => out.push_str(&other.to_display_string()),
422 }
423 }
424 out
425}
426
427#[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 }