Skip to main content

polydat_nodes/
realer.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Bundled real-world data for realistic data generation.
5//!
6//! Provides grab-and-go nodes for generating person names, country
7//! names, US state codes, and nationalities from embedded Census and
8//! geographic datasets. All data is compiled into the binary via
9//! `include_str!` — no runtime file I/O.
10//!
11//! Each node takes a u64 input (should be hashed for uniform
12//! distribution) and returns a String. Weighted variants select
13//! proportionally to Census frequency data.
14//!
15//! The bundled datasets are parsed once into process-global
16//! `OnceLock`s (the samplers are stateless after construction and
17//! the data is `include_str!`-baked, so there's nothing
18//! per-instance to configure).
19//!
20//! `first_names` (struct `FirstNames`) samples female names and
21//! `first_names_male` (struct `FirstNamesMale`) samples male names;
22//! both are first-class DSL nodes.
23
24use crate::sampling::alias::AliasTableU64;
25use std::sync::OnceLock;
26
27// =================================================================
28// Bundled CSV data
29// =================================================================
30
31static FEMALE_FIRSTNAMES_CSV: &str = include_str!("../data/census/female_firstnames.csv");
32static MALE_FIRSTNAMES_CSV: &str = include_str!("../data/census/male_firstnames.csv");
33static STATES_CSV: &str = include_str!("../data/census/census_state_abbrev.csv");
34static COUNTRIES_CSV: &str = include_str!("../data/census/countries.csv");
35static NATIONALITIES_CSV: &str = include_str!("../data/census/nationalities.csv");
36
37// =================================================================
38// CSV parsing helpers
39// =================================================================
40
41/// Parse a name+weight CSV (skipping header). Returns (names, weights).
42fn parse_name_weight_csv(csv: &str) -> (Vec<String>, Vec<f64>) {
43    let mut names = Vec::new();
44    let mut weights = Vec::new();
45    for line in csv.lines().skip(1) {
46        let parts: Vec<&str> = line.split(',').collect();
47        if parts.len() >= 2 {
48            let name = parts[0].trim().to_string();
49            if let Ok(w) = parts[1].trim().parse::<f64>()
50                && !name.is_empty()
51                && w > 0.0
52            {
53                names.push(name);
54                weights.push(w);
55            }
56        }
57    }
58    (names, weights)
59}
60
61/// Parse a single-column CSV (skipping header). Returns list of values.
62fn parse_single_column_csv(csv: &str) -> Vec<String> {
63    csv.lines()
64        .skip(1)
65        .map(|l| l.trim().to_string())
66        .filter(|l| !l.is_empty())
67        .collect()
68}
69
70/// Parse a two-column CSV with code,name (skipping header).
71fn parse_code_name_csv(csv: &str) -> Vec<(String, String)> {
72    csv.lines()
73        .skip(1)
74        .filter_map(|l| {
75            let parts: Vec<&str> = l.split(',').collect();
76            if parts.len() >= 2 {
77                Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
78            } else {
79                None
80            }
81        })
82        .collect()
83}
84
85// =================================================================
86// Generic weighted name sampler
87// =================================================================
88
89/// A weighted name sampler backed by an alias table.
90pub struct WeightedNameSampler {
91    names: Vec<String>,
92    table: AliasTableU64,
93}
94
95impl WeightedNameSampler {
96    fn new(names: Vec<String>, weights: Vec<f64>) -> Self {
97        let table = AliasTableU64::from_weights(&weights);
98        Self { names, table }
99    }
100
101    fn sample(&self, input: u64) -> &str {
102        let idx = self.table.sample(input) as usize;
103        &self.names[idx]
104    }
105}
106
107/// A uniform name sampler (no weights, just mod index).
108pub struct UniformNameSampler {
109    names: Vec<String>,
110}
111
112impl UniformNameSampler {
113    fn new(names: Vec<String>) -> Self {
114        Self { names }
115    }
116
117    fn sample(&self, input: u64) -> &str {
118        let idx = (input as usize) % self.names.len();
119        &self.names[idx]
120    }
121}
122
123// =================================================================
124// Process-global sampler caches (`include_str!` data is static —
125// the samplers are stateless after parse — so one global instance
126// per dataset is the right cache granularity).
127// =================================================================
128
129fn female_first_names() -> &'static WeightedNameSampler {
130    static CELL: OnceLock<WeightedNameSampler> = OnceLock::new();
131    CELL.get_or_init(|| {
132        let (names, weights) = parse_name_weight_csv(FEMALE_FIRSTNAMES_CSV);
133        WeightedNameSampler::new(names, weights)
134    })
135}
136
137fn male_first_names() -> &'static WeightedNameSampler {
138    static CELL: OnceLock<WeightedNameSampler> = OnceLock::new();
139    CELL.get_or_init(|| {
140        let (names, weights) = parse_name_weight_csv(MALE_FIRSTNAMES_CSV);
141        WeightedNameSampler::new(names, weights)
142    })
143}
144
145fn state_codes_data() -> &'static UniformNameSampler {
146    static CELL: OnceLock<UniformNameSampler> = OnceLock::new();
147    CELL.get_or_init(|| UniformNameSampler::new(parse_single_column_csv(STATES_CSV)))
148}
149
150fn country_names_data() -> &'static UniformNameSampler {
151    static CELL: OnceLock<UniformNameSampler> = OnceLock::new();
152    CELL.get_or_init(|| {
153        let pairs = parse_code_name_csv(COUNTRIES_CSV);
154        UniformNameSampler::new(pairs.into_iter().map(|(_, name)| name).collect())
155    })
156}
157
158fn country_codes_data() -> &'static UniformNameSampler {
159    static CELL: OnceLock<UniformNameSampler> = OnceLock::new();
160    CELL.get_or_init(|| {
161        let pairs = parse_code_name_csv(COUNTRIES_CSV);
162        UniformNameSampler::new(pairs.into_iter().map(|(code, _)| code).collect())
163    })
164}
165
166fn nationalities_data() -> &'static UniformNameSampler {
167    static CELL: OnceLock<UniformNameSampler> = OnceLock::new();
168    CELL.get_or_init(|| UniformNameSampler::new(parse_single_column_csv(NATIONALITIES_CSV)))
169}
170
171fn last_names_data() -> &'static UniformNameSampler {
172    static CELL: OnceLock<UniformNameSampler> = OnceLock::new();
173    CELL.get_or_init(|| {
174        UniformNameSampler::new(
175            crate::random::LASTNAMES
176                .lines()
177                .filter(|l| !l.is_empty())
178                .map(|l| l.to_string())
179                .collect(),
180        )
181    })
182}
183
184// =================================================================
185// Polydat Nodes
186// =================================================================
187
188/// `first_names(input) -> String` — Census female first name,
189/// weighted by frequency.
190#[polydat::polydat_node(category = RealData)]
191fn first_names(input: u64) -> String {
192    female_first_names().sample(input).to_string()
193}
194
195/// `first_names_male(input) -> String` — Census male first name,
196/// weighted by frequency. Companion to `first_names` (female).
197#[polydat::polydat_node(category = RealData)]
198fn first_names_male(input: u64) -> String {
199    male_first_names().sample(input).to_string()
200}
201
202/// `state_codes(input) -> String` — US state abbreviation
203/// (uniform selection).
204#[polydat::polydat_node(category = RealData)]
205fn state_codes(input: u64) -> String {
206    state_codes_data().sample(input).to_string()
207}
208
209/// `country_names(input) -> String` — country name (uniform
210/// selection over the full ISO list).
211#[polydat::polydat_node(category = RealData)]
212fn country_names(input: u64) -> String {
213    country_names_data().sample(input).to_string()
214}
215
216/// `country_codes(input) -> String` — country code (uniform
217/// selection over the full ISO list).
218#[polydat::polydat_node(category = RealData)]
219fn country_codes(input: u64) -> String {
220    country_codes_data().sample(input).to_string()
221}
222
223/// `nationalities(input) -> String` — nationality name (uniform
224/// selection).
225#[polydat::polydat_node(category = RealData)]
226fn nationalities(input: u64) -> String {
227    nationalities_data().sample(input).to_string()
228}
229
230/// `full_names(input) -> String` — combined first + last name.
231///
232/// Uses two hash-derived values from the input to independently
233/// select a first name and last name. The first name's gender
234/// is decided by bit 0 of the secondary hash.
235#[polydat::polydat_node(category = RealData)]
236fn full_names(input: u64) -> String {
237    use xxhash_rust::xxh3::xxh3_64;
238    let h2 = xxh3_64(&input.to_le_bytes());
239    let h3 = xxh3_64(&h2.to_le_bytes());
240    let first = if h2 & 1 == 0 {
241        female_first_names().sample(h2)
242    } else {
243        male_first_names().sample(h2)
244    };
245    let last_name = last_names_data().sample(h3);
246    format!("{first} {last_name}")
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use polydat::ast::{PolydatNode, Value};
253    use xxhash_rust::xxh3::xxh3_64;
254
255    #[test]
256    fn first_names_female() {
257        let node = FirstNames::new();
258        let mut out = [Value::None];
259        let h = xxh3_64(&42u64.to_le_bytes());
260        node.eval(&[Value::U64(h)], &mut out);
261        let name = out[0].as_str();
262        assert!(!name.is_empty());
263        assert!(name.chars().all(|c| c.is_alphabetic()));
264    }
265
266    #[test]
267    fn first_names_male() {
268        let node = FirstNamesMale::new();
269        let mut out = [Value::None];
270        let h = xxh3_64(&42u64.to_le_bytes());
271        node.eval(&[Value::U64(h)], &mut out);
272        assert!(!out[0].as_str().is_empty());
273    }
274
275    #[test]
276    fn first_names_weighted() {
277        // "Mary" is the most common female name — should appear often
278        let node = FirstNames::new();
279        let mut mary_count = 0;
280        let mut out = [Value::None];
281        for i in 0..10_000u64 {
282            let h = xxh3_64(&i.to_le_bytes());
283            node.eval(&[Value::U64(h)], &mut out);
284            if out[0].as_str() == "Mary" {
285                mary_count += 1;
286            }
287        }
288        assert!(
289            mary_count > 50,
290            "Mary should appear frequently, got {mary_count}"
291        );
292    }
293
294    #[test]
295    fn state_codes_valid() {
296        let node = StateCodes::new();
297        let mut out = [Value::None];
298        for i in 0..100u64 {
299            node.eval(&[Value::U64(i)], &mut out);
300            let code = out[0].as_str();
301            assert_eq!(code.len(), 2, "state code should be 2 chars: {code}");
302            assert!(code.chars().all(|c| c.is_ascii_uppercase()));
303        }
304    }
305
306    #[test]
307    fn country_names_nonempty() {
308        let node = CountryNames::new();
309        let mut out = [Value::None];
310        for i in 0..100u64 {
311            node.eval(&[Value::U64(i)], &mut out);
312            assert!(!out[0].as_str().is_empty());
313        }
314    }
315
316    #[test]
317    fn country_codes_two_char() {
318        let node = CountryCodes::new();
319        let mut out = [Value::None];
320        for i in 0..100u64 {
321            node.eval(&[Value::U64(i)], &mut out);
322            assert_eq!(out[0].as_str().len(), 2);
323        }
324    }
325
326    #[test]
327    fn nationalities_nonempty() {
328        let node = Nationalities::new();
329        let mut out = [Value::None];
330        for i in 0..100u64 {
331            node.eval(&[Value::U64(i)], &mut out);
332            assert!(!out[0].as_str().is_empty());
333        }
334    }
335
336    #[test]
337    fn full_names_format() {
338        let node = FullNames::new();
339        let mut out = [Value::None];
340        let h = xxh3_64(&42u64.to_le_bytes());
341        node.eval(&[Value::U64(h)], &mut out);
342        let name = out[0].as_str();
343        assert!(name.contains(' '), "full name should have a space: {name}");
344        assert!(name.len() > 3, "full name too short: {name}");
345    }
346
347    #[test]
348    fn full_names_deterministic() {
349        let node = FullNames::new();
350        let mut out1 = [Value::None];
351        let mut out2 = [Value::None];
352        let h = xxh3_64(&99u64.to_le_bytes());
353        node.eval(&[Value::U64(h)], &mut out1);
354        node.eval(&[Value::U64(h)], &mut out2);
355        assert_eq!(out1[0].as_str(), out2[0].as_str());
356    }
357}