pub struct ParsedCombinations {
pub segments: Vec<Segment>,
pub modulus: u64,
}
impl polydat::derive_support::PolydatSetup for ParsedCombinations {}
pub enum Segment {
Charset(Vec<char>),
Literal(String),
}
impl ParsedCombinations {
pub fn from_pattern(pattern: &str) -> Self {
let mut segments = Vec::new();
let mut modulus: u64 = 1;
for spec in pattern.split(';') {
let chars = parse_charset(spec);
if chars.len() == 1 && !spec.contains('-') {
segments.push(Segment::Literal(chars[0].to_string()));
} else if chars.is_empty() {
segments.push(Segment::Literal(spec.to_string()));
} else {
modulus = modulus.saturating_mul(chars.len() as u64);
segments.push(Segment::Charset(chars));
}
}
Self { segments, modulus }
}
}
#[polydat::polydat_node(category = String)]
fn combinations(
input: u64,
pattern: polydat::derive_support::Const<&str>,
#[poly_const(ParsedCombinations::from_pattern, from = pattern)] parsed: &ParsedCombinations,
) -> String {
let mut remainder = if parsed.modulus > 0 {
input % parsed.modulus
} else {
input
};
let mut result = String::with_capacity(parsed.segments.len() * 2);
for seg in &parsed.segments {
match seg {
Segment::Literal(s) => result.push_str(s),
Segment::Charset(chars) => {
let radix = chars.len() as u64;
if radix > 0 {
let idx = (remainder % radix) as usize;
result.push(chars[idx]);
remainder /= radix;
}
}
}
}
result
}
impl Combinations {
pub fn cardinality(&self) -> u64 {
self.parsed.modulus
}
}
fn parse_charset(spec: &str) -> Vec<char> {
let mut chars = Vec::new();
let spec_chars: Vec<char> = spec.chars().collect();
let mut i = 0;
while i < spec_chars.len() {
if i + 2 < spec_chars.len() && spec_chars[i + 1] == '-' {
let start = spec_chars[i];
let end = spec_chars[i + 2];
for c in start..=end {
chars.push(c);
}
i += 3;
} else {
chars.push(spec_chars[i]);
i += 1;
}
}
chars
}
#[polydat::polydat_node(category = String)]
fn number_to_words(input: u64) -> String {
u64_to_words(input)
}
const ONES: [&str; 20] = [
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
"eleven",
"twelve",
"thirteen",
"fourteen",
"fifteen",
"sixteen",
"seventeen",
"eighteen",
"nineteen",
];
const TENS: [&str; 10] = [
"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
];
const SCALES: [&str; 7] = [
"",
"thousand",
"million",
"billion",
"trillion",
"quadrillion",
"quintillion",
];
fn u64_to_words(n: u64) -> String {
if n < 20 {
return ONES[n as usize].to_string();
}
let mut buf = String::with_capacity(64);
let mut chunks = [0u32; 7];
let mut num_chunks = 0;
let mut remaining = n;
while remaining > 0 {
chunks[num_chunks] = (remaining % 1000) as u32;
num_chunks += 1;
remaining /= 1000;
}
let mut first = true;
for i in (0..num_chunks).rev() {
let chunk = chunks[i];
if chunk > 0 {
if !first {
buf.push(' ');
}
first = false;
append_chunk_to_words(&mut buf, chunk);
if i > 0 && i < SCALES.len() {
buf.push(' ');
buf.push_str(SCALES[i]);
}
}
}
buf
}
fn append_chunk_to_words(buf: &mut String, n: u32) {
let hundreds = n / 100;
let remainder = n % 100;
let mut has_hundreds = false;
if hundreds > 0 {
buf.push_str(ONES[hundreds as usize]);
buf.push_str(" hundred");
has_hundreds = true;
}
if remainder >= 20 {
if has_hundreds {
buf.push(' ');
}
let tens = remainder / 10;
let ones = remainder % 10;
buf.push_str(TENS[tens as usize]);
if ones > 0 {
buf.push('-');
buf.push_str(ONES[ones as usize]);
}
} else if remainder > 0 {
if has_hundreds {
buf.push(' ');
}
buf.push_str(ONES[remainder as usize]);
}
}
#[polydat::polydat_node(category = String)]
fn hashed_uuid(input: u64) -> String {
let h1 = xxhash_rust::xxh3::xxh3_64(&input.to_le_bytes());
let h2 = xxhash_rust::xxh3::xxh3_64(&h1.to_le_bytes());
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&h1.to_le_bytes());
bytes[8..].copy_from_slice(&h2.to_le_bytes());
bytes[6] = (bytes[6] & 0x0F) | 0x40;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0],
bytes[1],
bytes[2],
bytes[3],
bytes[4],
bytes[5],
bytes[6],
bytes[7],
bytes[8],
bytes[9],
bytes[10],
bytes[11],
bytes[12],
bytes[13],
bytes[14],
bytes[15],
)
}
fn expand_charset(charset: &str) -> Vec<char> {
if charset.is_empty() {
return ('a'..='z').collect();
}
let mut result = Vec::new();
let chars_vec: Vec<char> = charset.chars().collect();
let mut i = 0;
while i < chars_vec.len() {
if i + 2 < chars_vec.len() && chars_vec[i + 1] == '-' {
for c in chars_vec[i]..=chars_vec[i + 2] {
result.push(c);
}
i += 3;
} else {
result.push(chars_vec[i]);
i += 1;
}
}
if result.is_empty() {
('a'..='z').collect()
} else {
result
}
}
#[polydat::polydat_node(category = String)]
fn char_buf(
seed: u64,
charset: polydat::derive_support::Const<&str>,
length: u64,
#[poly_const(expand_charset, from = charset)] chars: &Vec<char>,
) -> String {
let n = chars.len();
let len = length as usize;
if n == 0 || len == 0 {
return String::new();
}
let mut result = String::with_capacity(len);
let mut h = seed;
for _ in 0..len {
h = xxhash_rust::xxh3::xxh3_64(&h.to_le_bytes());
result.push(chars[(h as usize) % n]);
}
result
}
fn read_file_lines(filename: &str) -> Vec<String> {
let content = std::fs::read_to_string(filename)
.unwrap_or_else(|e| panic!("failed to read file '{filename}': {e}"));
let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
if lines.is_empty() {
panic!("file '{filename}' has no lines");
}
lines
}
#[polydat::polydat_node(category = String)]
fn file_line_at(
index: u64,
filename: polydat::derive_support::Const<&str>,
#[poly_const(read_file_lines, from = filename)] lines: &Vec<String>,
) -> String {
let _ = filename;
let idx = index as usize;
lines[idx % lines.len()].clone()
}
#[polydat::polydat_node(category = String)]
fn str_concat(parts: &[polydat::ast::Value]) -> String {
use polydat::ast::Value;
let mut out = String::new();
for v in parts {
match v {
Value::Str(s) => out.push_str(s),
Value::U64(n) => out.push_str(&n.to_string()),
Value::F64(n) => out.push_str(&n.to_string()),
Value::Bool(b) => out.push_str(&b.to_string()),
Value::Json(j) => out.push_str(&j.to_string()),
Value::Bytes(b) => out.push_str(&String::from_utf8_lossy(b)),
other => out.push_str(&other.to_display_string()),
}
}
out
}
#[polydat::polydat_node(category = String)]
fn str_lower(input: String) -> String {
input.to_lowercase()
}
#[polydat::polydat_node(category = String)]
fn str_upper(input: String) -> String {
input.to_uppercase()
}
#[cfg(test)]
mod tests {
use super::*;
use polydat::ast::{PolydatNode, Value};
#[test]
fn combinations_digits() {
let node = Combinations::new("0-9;0-9;0-9".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(123)], &mut out);
let s = out[0].as_str();
assert_eq!(s.len(), 3);
assert!(s.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn combinations_with_separator() {
let node = Combinations::new("0-9;0-9;0-9;-;0-9;0-9;0-9".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
let s = out[0].as_str();
assert_eq!(s.len(), 7); assert_eq!(&s[3..4], "-");
}
#[test]
fn combinations_alpha() {
let node = Combinations::new("A-Z;A-Z;A-Z".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
assert_eq!(out[0].as_str(), "AAA");
node.eval(&[Value::U64(1)], &mut out);
assert_eq!(out[0].as_str(), "BAA");
}
#[test]
fn combinations_cardinality() {
let node = Combinations::new("0-9;0-9;-;A-Z".to_string());
assert_eq!(node.cardinality(), 2600);
}
#[test]
fn combinations_deterministic() {
let node = Combinations::new("A-Z;0-9".to_string());
let mut out1 = [Value::None];
let mut out2 = [Value::None];
node.eval(&[Value::U64(42)], &mut out1);
node.eval(&[Value::U64(42)], &mut out2);
assert_eq!(out1[0].as_str(), out2[0].as_str());
}
#[test]
fn combinations_wraps() {
let node = Combinations::new("0-9".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
let a = out[0].as_str().to_string();
node.eval(&[Value::U64(10)], &mut out);
assert_eq!(out[0].as_str(), &a, "should wrap at cardinality");
}
#[test]
fn number_to_words_zero() {
assert_eq!(u64_to_words(0), "zero");
}
#[test]
fn number_to_words_teens() {
assert_eq!(u64_to_words(1), "one");
assert_eq!(u64_to_words(11), "eleven");
assert_eq!(u64_to_words(19), "nineteen");
}
#[test]
fn number_to_words_tens() {
assert_eq!(u64_to_words(20), "twenty");
assert_eq!(u64_to_words(42), "forty-two");
assert_eq!(u64_to_words(99), "ninety-nine");
}
#[test]
fn number_to_words_hundreds() {
assert_eq!(u64_to_words(100), "one hundred");
assert_eq!(u64_to_words(123), "one hundred twenty-three");
assert_eq!(u64_to_words(500), "five hundred");
}
#[test]
fn number_to_words_thousands() {
assert_eq!(u64_to_words(1000), "one thousand");
assert_eq!(u64_to_words(1001), "one thousand one");
assert_eq!(
u64_to_words(12345),
"twelve thousand three hundred forty-five"
);
}
#[test]
fn number_to_words_millions() {
assert_eq!(u64_to_words(1_000_000), "one million");
assert_eq!(
u64_to_words(1_234_567),
"one million two hundred thirty-four thousand five hundred sixty-seven"
);
}
#[test]
fn number_to_words_large() {
let s = u64_to_words(1_000_000_000_000);
assert!(s.starts_with("one trillion"), "got: {s}");
}
#[test]
fn number_to_words_node() {
let node = NumberToWords::new();
let mut out = [Value::None];
node.eval(&[Value::U64(42)], &mut out);
assert_eq!(out[0].as_str(), "forty-two");
}
#[test]
fn str_concat_basic() {
let node = StrConcat::new(2);
let mut out = [Value::None];
node.eval(
&[Value::Str("hello ".into()), Value::Str("world".into())],
&mut out,
);
assert_eq!(out[0].as_str(), "hello world");
}
#[test]
fn str_concat_renders_extension_values_by_display() {
#[derive(Debug, Clone)]
struct Tag(u64);
impl polydat::ast::ReflectedValue for Tag {
fn type_name(&self) -> &str {
"Tag"
}
fn display(&self) -> String {
format!("tag#{}", self.0)
}
fn clone_reflected(&self) -> Box<dyn polydat::ast::ReflectedValue> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let node = StrConcat::new(2);
let mut out = [Value::None];
node.eval(
&[Value::Str("x".into()), Value::Ext(Box::new(Tag(7)))],
&mut out,
);
assert_eq!(out[0].as_str(), "xtag#7");
}
#[test]
fn str_concat_mixed_types() {
let node = StrConcat::new(4);
let mut out = [Value::None];
node.eval(
&[
Value::Str("id=".into()),
Value::U64(42),
Value::Str(" v=".into()),
Value::F64(3.14),
],
&mut out,
);
assert_eq!(out[0].as_str(), "id=42 v=3.14");
}
#[test]
fn str_concat_empty() {
let node = StrConcat::new(0);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_str(), "");
}
#[test]
fn str_lower_ascii_and_unicode() {
let node = StrLower::new();
let mut out = [Value::None];
node.eval(&[Value::Str("OTHER_M8".into())], &mut out);
assert_eq!(out[0].as_str(), "other_m8");
node.eval(&[Value::Str("ÄPFEL".into())], &mut out);
assert_eq!(out[0].as_str(), "äpfel");
}
#[test]
fn str_lower_idempotent_on_already_lowercase() {
let node = StrLower::new();
let mut out = [Value::None];
node.eval(&[Value::Str("fknn_oat_other".into())], &mut out);
assert_eq!(out[0].as_str(), "fknn_oat_other");
}
#[test]
fn str_upper_ascii_and_unicode() {
let node = StrUpper::new();
let mut out = [Value::None];
node.eval(&[Value::Str("other_m8".into())], &mut out);
assert_eq!(out[0].as_str(), "OTHER_M8");
node.eval(&[Value::Str("äpfel".into())], &mut out);
assert_eq!(out[0].as_str(), "ÄPFEL");
}
}