Skip to main content

seedfaker_core/
ctx.rs

1use crate::gen::helpers::handle::{pick_archetype, unique_tag, HandleArchetype};
2use crate::gen::helpers::nickname::{build_nickname, Nickname};
3use crate::gen::{ascii_lower, pick_locale};
4use crate::locale::shared;
5use crate::locale::{Locale, NameOrder};
6use crate::rng::Rng;
7
8/// Shared identity for context-aware generation (--ctx strict/loose).
9pub struct Identity {
10    pub locale_code: &'static str,
11    pub name_order: NameOrder,
12    pub first_name: String,
13    pub last_name: String,
14    pub last_name2: String,
15    pub first_ascii: String,
16    pub last_ascii: String,
17    pub archetype: HandleArchetype,
18    /// Birth year for tag correlation (nicknames, birthdate field).
19    pub birth_year: i64,
20    /// Birth month (1-12) — used for realistic numeric suffixes (DDMM/MMDD).
21    pub birth_month: u8,
22    /// Birth day (1-28) — used for realistic numeric suffixes (DDMM/MMDD).
23    pub birth_day: u8,
24    /// Pre-computed nickname. None for `NameOnly` archetype.
25    pub nickname: Option<Nickname>,
26    pub city: String,
27    pub region: String,
28    pub postal: String,
29    pub lat: f64,
30    pub lon: f64,
31    pub tz: &'static str,
32}
33
34/// Weighted birth year distribution — realistic age pyramid.
35/// `ref_year` = `until` (the "present" year for generated data).
36pub fn weighted_birth_year(rng: &mut Rng, since: i64, until: i64) -> i64 {
37    let ref_year = until;
38    // (age_lo, age_hi, weight out of 1000)
39    let buckets: [(i64, i64, i64); 8] = [
40        (18, 25, 250),
41        (26, 35, 250),
42        (36, 45, 200),
43        (46, 55, 130),
44        (56, 65, 80),
45        (66, 75, 50),
46        (76, 85, 25),
47        (86, 100, 15),
48    ];
49    let roll = rng.range(0, 999);
50    let mut acc = 0;
51    for (age_lo, age_hi, weight) in buckets {
52        acc += weight;
53        if roll < acc {
54            let birth_hi = (ref_year - age_lo).min(until);
55            let birth_lo = (ref_year - age_hi).max(since);
56            if birth_lo < birth_hi {
57                return rng.range(birth_lo, birth_hi);
58            }
59        }
60    }
61    let lo = (ref_year - 40).max(since);
62    let hi = (ref_year - 18).min(until);
63    if lo < hi {
64        rng.range(lo, hi)
65    } else {
66        since
67    }
68}
69
70impl Identity {
71    pub fn new(
72        rng: &mut Rng,
73        locales: &[&Locale],
74        birth_year_range: Option<(i64, i64)>,
75        since: i64,
76        until: i64,
77    ) -> Self {
78        let loc = pick_locale(rng, locales);
79        // 5% chance of international first name (Leon in Russia, Sofia in Japan).
80        let first_name = if rng.urange(0, 99) < 5 {
81            shared::INTL_FIRST_NAMES[rng.urange(0, shared::INTL_FIRST_NAMES.len() - 1)].to_string()
82        } else {
83            shared::weighted_choice(rng, loc.first_names, loc.first_names_common).to_string()
84        };
85        let last_name =
86            shared::weighted_choice(rng, loc.last_names, loc.last_names_common).to_string();
87        let last_name2 = match loc.name_order {
88            NameOrder::DoubleSurname => (*rng.choice(loc.last_names)).to_string(),
89            NameOrder::Patronymic { .. } | NameOrder::PatronymicMiddle => {
90                (*rng.choice(loc.first_names)).to_string()
91            }
92            _ => String::new(),
93        };
94        let first_ascii = ascii_lower(rng, &first_name);
95        let last_ascii = ascii_lower(rng, &last_name);
96        let archetype = pick_archetype(rng);
97        let birth_year = if let Some((from, to)) = birth_year_range {
98            let yf = crate::temporal::epoch_to_year(from);
99            let yt = crate::temporal::epoch_to_year(to.saturating_sub(1)).max(yf);
100            rng.range(yf, yt)
101        } else {
102            let yf = crate::temporal::epoch_to_year(since);
103            let yt = crate::temporal::epoch_to_year(until.saturating_sub(1)).max(yf);
104            weighted_birth_year(rng, yf, yt)
105        };
106        let birth_month = rng.range(1, 12) as u8;
107        let birth_day = rng.range(1, 28) as u8;
108        let nickname = if archetype == HandleArchetype::NameOnly {
109            None
110        } else {
111            let nick_tag = unique_tag(rng.record(), 0xDEAD);
112            Some(build_nickname(nick_tag, rng))
113        };
114        let city = rng.choice(loc.cities);
115        Self {
116            locale_code: loc.code,
117            name_order: loc.name_order,
118            first_name,
119            last_name,
120            last_name2,
121            first_ascii,
122            last_ascii,
123            archetype,
124            birth_year,
125            birth_month,
126            birth_day,
127            nickname,
128            city: city.name.to_string(),
129            region: city.region.to_string(),
130            postal: city.postal.to_string(),
131            lat: city.lat,
132            lon: city.lon,
133            tz: city.tz,
134        }
135    }
136}
137
138/// Generation context — replaces globals and rigid `GenFn` params.
139pub struct GenContext<'a> {
140    pub rng: Rng,
141    pub locales: &'a [&'a Locale],
142    pub modifier: &'a str,
143    pub identity: Option<&'a Identity>,
144    pub tz_offset_minutes: i32,
145    pub since: i64,
146    pub until: i64,
147    /// Per-field range override, resolved (both bounds filled).
148    pub range: Option<(i64, i64)>,
149    /// Monotonic ordering: Asc/Desc use record number for position.
150    pub ordering: crate::field::Ordering,
151    /// Zipf distribution over the range. None = uniform.
152    pub zipf: Option<crate::field::ZipfSpec>,
153    /// Raw numeric value — set by numeric generators, read by aggregators.
154    pub numeric: Option<f64>,
155}
156
157impl<'a> GenContext<'a> {
158    /// Locale locked to Identity when ctx active, otherwise random.
159    pub fn locale(&mut self) -> &'a Locale {
160        assert!(!self.locales.is_empty(), "GenContext requires non-empty locales");
161        if let Some(id) = self.identity {
162            crate::locale::get(id.locale_code).unwrap_or(self.locales[0])
163        } else {
164            self.locales[self.rng.urange(0, self.locales.len() - 1)]
165        }
166    }
167
168    /// Always random locale pick (ignores identity).
169    pub fn pick_locale(&mut self) -> &'a Locale {
170        assert!(!self.locales.is_empty(), "GenContext requires non-empty locales");
171        self.locales[self.rng.urange(0, self.locales.len() - 1)]
172    }
173
174    /// TZ offset for Apache log: "+HHMM" / "-HHMM"
175    pub fn tz_log(&self, buf: &mut String) {
176        let m = self.tz_offset_minutes;
177        buf.push(if m < 0 { '-' } else { '+' });
178        let abs = m.unsigned_abs();
179        super::gen::date::push_pad2(buf, i64::from(abs / 60));
180        super::gen::date::push_pad2(buf, i64::from(abs % 60));
181    }
182
183    /// TZ offset for ISO 8601: "Z" or "+HH:MM" / "-HH:MM"
184    pub fn tz_iso(&self, buf: &mut String) {
185        let m = self.tz_offset_minutes;
186        if m == 0 {
187            buf.push('Z');
188            return;
189        }
190        buf.push(if m < 0 { '-' } else { '+' });
191        let abs = m.unsigned_abs();
192        super::gen::date::push_pad2(buf, i64::from(abs / 60));
193        buf.push(':');
194        super::gen::date::push_pad2(buf, i64::from(abs % 60));
195    }
196}