Skip to main content

datasynth_core/templates/
names.rs

1//! Multi-cultural name generation for realistic user personas.
2//!
3//! Provides name pools for various cultures to generate realistic
4//! user names, IDs, and email addresses.
5
6use rand::seq::SliceRandom;
7use rand::Rng;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Cultural background for name generation.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum NameCulture {
15    /// Western US names (default)
16    #[default]
17    WesternUs,
18    /// German names
19    German,
20    /// French names
21    French,
22    /// Chinese names
23    Chinese,
24    /// Japanese names
25    Japanese,
26    /// Indian names
27    Indian,
28    /// Hispanic/Latino names
29    Hispanic,
30}
31
32impl NameCulture {
33    /// Get all available cultures.
34    pub fn all() -> &'static [NameCulture] {
35        &[
36            NameCulture::WesternUs,
37            NameCulture::German,
38            NameCulture::French,
39            NameCulture::Chinese,
40            NameCulture::Japanese,
41            NameCulture::Indian,
42            NameCulture::Hispanic,
43        ]
44    }
45}
46
47/// A generated person name with components.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct PersonName {
50    /// First name
51    pub first_name: String,
52    /// Last name
53    pub last_name: String,
54    /// Full display name
55    pub display_name: String,
56    /// Culture of origin
57    pub culture: NameCulture,
58    /// Whether the name is typically male
59    pub is_male: bool,
60}
61
62impl PersonName {
63    /// Generate a user ID from the name (e.g., "JSMITH001").
64    pub fn to_user_id(&self, index: usize) -> String {
65        let first_initial = self.first_name.chars().next().unwrap_or('X');
66        let last_part: String = self
67            .last_name
68            .chars()
69            .filter(|c| c.is_ascii_alphabetic())
70            .take(5)
71            .collect();
72        format!("{}{}{:03}", first_initial, last_part.to_uppercase(), index)
73    }
74
75    /// Generate an email address from the name.
76    pub fn to_email(&self, domain: &str) -> String {
77        let first = self
78            .first_name
79            .chars()
80            .filter(|c| c.is_ascii_alphabetic())
81            .collect::<String>()
82            .to_lowercase();
83        let last = self
84            .last_name
85            .chars()
86            .filter(|c| c.is_ascii_alphabetic())
87            .collect::<String>()
88            .to_lowercase();
89        format!("{}.{}@{}", first, last, domain)
90    }
91}
92
93/// Pool of names for a specific culture.
94#[derive(Debug, Clone)]
95pub struct NamePool {
96    /// Male first names
97    pub first_names_male: Vec<String>,
98    /// Female first names
99    pub first_names_female: Vec<String>,
100    /// Last names / family names
101    pub last_names: Vec<String>,
102    /// The culture this pool represents
103    pub culture: NameCulture,
104}
105
106/// Helper to convert `&[&str]` to `Vec<String>` concisely in factory methods.
107fn sv(items: &[&str]) -> Vec<String> {
108    items.iter().map(|s| (*s).to_string()).collect()
109}
110
111impl NamePool {
112    /// Create a Western US name pool.
113    pub fn western_us() -> Self {
114        Self {
115            culture: NameCulture::WesternUs,
116            first_names_male: sv(&[
117                "James",
118                "John",
119                "Robert",
120                "Michael",
121                "William",
122                "David",
123                "Richard",
124                "Joseph",
125                "Thomas",
126                "Christopher",
127                "Charles",
128                "Daniel",
129                "Matthew",
130                "Anthony",
131                "Mark",
132                "Donald",
133                "Steven",
134                "Paul",
135                "Andrew",
136                "Joshua",
137                "Kenneth",
138                "Kevin",
139                "Brian",
140                "George",
141                "Timothy",
142                "Ronald",
143                "Edward",
144                "Jason",
145                "Jeffrey",
146                "Ryan",
147                "Jacob",
148                "Gary",
149                "Nicholas",
150                "Eric",
151                "Jonathan",
152                "Stephen",
153                "Larry",
154                "Justin",
155                "Scott",
156                "Brandon",
157                "Benjamin",
158                "Samuel",
159                "Raymond",
160                "Gregory",
161                "Frank",
162                "Alexander",
163                "Patrick",
164                "Jack",
165                "Dennis",
166                "Jerry",
167                "Tyler",
168                "Aaron",
169                "Jose",
170                "Adam",
171                "Nathan",
172            ]),
173            first_names_female: sv(&[
174                "Mary",
175                "Patricia",
176                "Jennifer",
177                "Linda",
178                "Barbara",
179                "Elizabeth",
180                "Susan",
181                "Jessica",
182                "Sarah",
183                "Karen",
184                "Lisa",
185                "Nancy",
186                "Betty",
187                "Margaret",
188                "Sandra",
189                "Ashley",
190                "Kimberly",
191                "Emily",
192                "Donna",
193                "Michelle",
194                "Dorothy",
195                "Carol",
196                "Amanda",
197                "Melissa",
198                "Deborah",
199                "Stephanie",
200                "Rebecca",
201                "Sharon",
202                "Laura",
203                "Cynthia",
204                "Kathleen",
205                "Amy",
206                "Angela",
207                "Shirley",
208                "Anna",
209                "Brenda",
210                "Pamela",
211                "Emma",
212                "Nicole",
213                "Helen",
214                "Samantha",
215                "Katherine",
216                "Christine",
217                "Debra",
218                "Rachel",
219                "Carolyn",
220                "Janet",
221                "Catherine",
222                "Maria",
223                "Heather",
224                "Diane",
225                "Ruth",
226                "Julie",
227            ]),
228            last_names: sv(&[
229                "Smith",
230                "Johnson",
231                "Williams",
232                "Brown",
233                "Jones",
234                "Garcia",
235                "Miller",
236                "Davis",
237                "Rodriguez",
238                "Martinez",
239                "Hernandez",
240                "Lopez",
241                "Gonzalez",
242                "Wilson",
243                "Anderson",
244                "Thomas",
245                "Taylor",
246                "Moore",
247                "Jackson",
248                "Martin",
249                "Lee",
250                "Perez",
251                "Thompson",
252                "White",
253                "Harris",
254                "Sanchez",
255                "Clark",
256                "Ramirez",
257                "Lewis",
258                "Robinson",
259                "Walker",
260                "Young",
261                "Allen",
262                "King",
263                "Wright",
264                "Scott",
265                "Torres",
266                "Nguyen",
267                "Hill",
268                "Flores",
269                "Green",
270                "Adams",
271                "Nelson",
272                "Baker",
273                "Hall",
274                "Rivera",
275                "Campbell",
276                "Mitchell",
277                "Carter",
278                "Roberts",
279                "Turner",
280                "Phillips",
281                "Evans",
282                "Parker",
283                "Edwards",
284                "Collins",
285            ]),
286        }
287    }
288
289    /// Create a German name pool.
290    pub fn german() -> Self {
291        Self {
292            culture: NameCulture::German,
293            first_names_male: sv(&[
294                "Hans",
295                "Klaus",
296                "Wolfgang",
297                "Dieter",
298                "Jürgen",
299                "Peter",
300                "Michael",
301                "Thomas",
302                "Andreas",
303                "Stefan",
304                "Markus",
305                "Christian",
306                "Martin",
307                "Frank",
308                "Bernd",
309                "Uwe",
310                "Ralf",
311                "Werner",
312                "Heinz",
313                "Helmut",
314                "Gerhard",
315                "Manfred",
316                "Horst",
317                "Karl",
318                "Heinrich",
319                "Friedrich",
320                "Wilhelm",
321                "Otto",
322                "Matthias",
323                "Tobias",
324                "Sebastian",
325                "Florian",
326                "Alexander",
327                "Maximilian",
328                "Felix",
329                "Lukas",
330                "Jonas",
331                "Leon",
332                "Paul",
333                "Philipp",
334                "Tim",
335                "Jan",
336                "Nico",
337                "Erik",
338                "Lars",
339                "Sven",
340                "Kai",
341                "Olaf",
342                "Rainer",
343            ]),
344            first_names_female: sv(&[
345                "Anna",
346                "Maria",
347                "Elisabeth",
348                "Ursula",
349                "Helga",
350                "Monika",
351                "Petra",
352                "Sabine",
353                "Karin",
354                "Renate",
355                "Ingrid",
356                "Brigitte",
357                "Gisela",
358                "Erika",
359                "Christa",
360                "Andrea",
361                "Claudia",
362                "Susanne",
363                "Birgit",
364                "Heike",
365                "Martina",
366                "Nicole",
367                "Stefanie",
368                "Julia",
369                "Katharina",
370                "Christina",
371                "Sandra",
372                "Melanie",
373                "Daniela",
374                "Anja",
375                "Tanja",
376                "Simone",
377                "Silke",
378                "Nadine",
379                "Yvonne",
380                "Manuela",
381                "Sonja",
382                "Michaela",
383                "Angelika",
384                "Barbara",
385                "Gabriele",
386                "Beate",
387                "Doris",
388                "Eva",
389                "Franziska",
390                "Lena",
391                "Hannah",
392                "Sophie",
393                "Leonie",
394                "Laura",
395            ]),
396            last_names: sv(&[
397                "Müller",
398                "Schmidt",
399                "Schneider",
400                "Fischer",
401                "Weber",
402                "Meyer",
403                "Wagner",
404                "Becker",
405                "Schulz",
406                "Hoffmann",
407                "Schäfer",
408                "Koch",
409                "Bauer",
410                "Richter",
411                "Klein",
412                "Wolf",
413                "Schröder",
414                "Neumann",
415                "Schwarz",
416                "Zimmermann",
417                "Braun",
418                "Krüger",
419                "Hofmann",
420                "Hartmann",
421                "Lange",
422                "Schmitt",
423                "Werner",
424                "Schmitz",
425                "Krause",
426                "Meier",
427                "Lehmann",
428                "Schmid",
429                "Schulze",
430                "Maier",
431                "Köhler",
432                "Herrmann",
433                "König",
434                "Walter",
435                "Mayer",
436                "Huber",
437                "Kaiser",
438                "Fuchs",
439                "Peters",
440                "Lang",
441                "Scholz",
442                "Möller",
443                "Weiß",
444                "Jung",
445                "Hahn",
446                "Schubert",
447            ]),
448        }
449    }
450
451    /// Create a French name pool.
452    pub fn french() -> Self {
453        Self {
454            culture: NameCulture::French,
455            first_names_male: sv(&[
456                "Jean",
457                "Pierre",
458                "Michel",
459                "André",
460                "Philippe",
461                "Jacques",
462                "Bernard",
463                "Alain",
464                "François",
465                "Robert",
466                "Marcel",
467                "René",
468                "Louis",
469                "Claude",
470                "Daniel",
471                "Yves",
472                "Christian",
473                "Patrick",
474                "Nicolas",
475                "Julien",
476                "Thomas",
477                "Antoine",
478                "Alexandre",
479                "Maxime",
480                "Lucas",
481                "Hugo",
482                "Théo",
483                "Mathieu",
484                "Guillaume",
485                "Laurent",
486                "Olivier",
487                "Christophe",
488                "Sébastien",
489                "Frédéric",
490                "Vincent",
491                "David",
492                "Eric",
493                "Pascal",
494                "Gilles",
495                "Thierry",
496                "Stéphane",
497                "Bruno",
498                "Dominique",
499                "Serge",
500                "Maurice",
501                "Henri",
502                "Paul",
503                "Charles",
504                "Emmanuel",
505                "Raphaël",
506            ]),
507            first_names_female: sv(&[
508                "Marie",
509                "Jeanne",
510                "Françoise",
511                "Monique",
512                "Catherine",
513                "Nathalie",
514                "Isabelle",
515                "Sylvie",
516                "Anne",
517                "Martine",
518                "Nicole",
519                "Christine",
520                "Sophie",
521                "Valérie",
522                "Julie",
523                "Camille",
524                "Léa",
525                "Manon",
526                "Emma",
527                "Chloé",
528                "Inès",
529                "Sarah",
530                "Laura",
531                "Louise",
532                "Jade",
533                "Alice",
534                "Lola",
535                "Margot",
536                "Charlotte",
537                "Clara",
538                "Pauline",
539                "Marine",
540                "Aurélie",
541                "Céline",
542                "Sandrine",
543                "Virginie",
544                "Stéphanie",
545                "Élodie",
546                "Delphine",
547                "Laurence",
548                "Brigitte",
549                "Jacqueline",
550                "Simone",
551                "Denise",
552                "Madeleine",
553                "Thérèse",
554                "Hélène",
555                "Élise",
556                "Juliette",
557                "Marguerite",
558            ]),
559            last_names: sv(&[
560                "Martin",
561                "Bernard",
562                "Dubois",
563                "Thomas",
564                "Robert",
565                "Richard",
566                "Petit",
567                "Durand",
568                "Leroy",
569                "Moreau",
570                "Simon",
571                "Laurent",
572                "Lefebvre",
573                "Michel",
574                "Garcia",
575                "David",
576                "Bertrand",
577                "Roux",
578                "Vincent",
579                "Fournier",
580                "Morel",
581                "Girard",
582                "André",
583                "Lefèvre",
584                "Mercier",
585                "Dupont",
586                "Lambert",
587                "Bonnet",
588                "François",
589                "Martinez",
590                "Legrand",
591                "Garnier",
592                "Faure",
593                "Rousseau",
594                "Blanc",
595                "Guérin",
596                "Muller",
597                "Henry",
598                "Roussel",
599                "Nicolas",
600                "Perrin",
601                "Morin",
602                "Mathieu",
603                "Clément",
604                "Gauthier",
605                "Dumont",
606                "Lopez",
607                "Fontaine",
608                "Chevalier",
609                "Robin",
610            ]),
611        }
612    }
613
614    /// Create a Chinese name pool.
615    pub fn chinese() -> Self {
616        Self {
617            culture: NameCulture::Chinese,
618            first_names_male: sv(&[
619                "Wei", "Fang", "Lei", "Jun", "Jian", "Hao", "Chen", "Yang", "Ming", "Tao", "Long",
620                "Feng", "Bin", "Qiang", "Gang", "Hui", "Peng", "Xiang", "Bo", "Chao", "Dong",
621                "Liang", "Ning", "Kai", "Jie", "Yong", "Hai", "Lin", "Wen", "Zheng", "Hong", "Xin",
622                "Da", "Zhi", "Guang", "Cheng", "Yi", "Sheng", "Biao", "Ping", "Yun", "Song",
623                "Chang", "Kang", "Rui", "Nan", "Jia", "Xiao", "Yu", "Hua",
624            ]),
625            first_names_female: sv(&[
626                "Fang", "Min", "Jing", "Li", "Yan", "Hong", "Juan", "Mei", "Ying", "Xia", "Hui",
627                "Lin", "Ling", "Ping", "Dan", "Yun", "Na", "Qian", "Xin", "Ya", "Wei", "Wen",
628                "Jie", "Qing", "Yu", "Hua", "Yue", "Xue", "Lan", "Zhen", "Rong", "Shu", "Fei",
629                "Lei", "Shan", "Ting", "Ni", "Ying", "Chen", "Huan", "Lu", "Ai", "Xiao", "Xiang",
630                "Yao", "Meng", "Qi", "Jun", "Bei", "Zhi",
631            ]),
632            last_names: sv(&[
633                "Wang", "Li", "Zhang", "Liu", "Chen", "Yang", "Huang", "Zhao", "Wu", "Zhou", "Xu",
634                "Sun", "Ma", "Zhu", "Hu", "Guo", "He", "Lin", "Gao", "Luo", "Zheng", "Liang",
635                "Xie", "Tang", "Song", "Xu", "Han", "Deng", "Feng", "Cao", "Peng", "Xiao", "Cheng",
636                "Yuan", "Tian", "Dong", "Pan", "Cai", "Jiang", "Wei", "Yu", "Du", "Ye", "Shi",
637                "Lu", "Shen", "Su", "Jia", "Fan", "Jin",
638            ]),
639        }
640    }
641
642    /// Create a Japanese name pool.
643    pub fn japanese() -> Self {
644        Self {
645            culture: NameCulture::Japanese,
646            first_names_male: sv(&[
647                "Hiroshi", "Takeshi", "Kenji", "Yuki", "Kazuki", "Ryota", "Daiki", "Shota", "Yuto",
648                "Kenta", "Haruto", "Sota", "Riku", "Yuma", "Kaito", "Ren", "Hayato", "Takumi",
649                "Kouki", "Ryuu", "Naoki", "Tsubasa", "Yuuki", "Akira", "Satoshi", "Makoto",
650                "Tetsuya", "Masaki", "Shin", "Kei", "Daisuke", "Shunsuke", "Tomoya", "Yusuke",
651                "Tatsuya", "Katsuki", "Shun", "Yamato", "Koji", "Hideo", "Takahiro", "Noboru",
652                "Shinji", "Osamu", "Minoru", "Hideki", "Jun", "Masaru", "Ken", "Ryo",
653            ]),
654            first_names_female: sv(&[
655                "Yuki", "Sakura", "Hana", "Yui", "Mio", "Rin", "Aoi", "Mei", "Saki", "Miku",
656                "Nanami", "Ayaka", "Misaki", "Haruka", "Momoka", "Rina", "Yuna", "Hinata",
657                "Koharu", "Miyu", "Akari", "Hikari", "Kaede", "Natsuki", "Mai", "Ami", "Aya",
658                "Emi", "Kana", "Megumi", "Tomoko", "Yoko", "Keiko", "Naomi", "Mayumi", "Chika",
659                "Nana", "Risa", "Asuka", "Fumiko", "Kyoko", "Reiko", "Noriko", "Sachiko", "Mariko",
660                "Shiori", "Midori", "Kanako", "Minami", "Eriko",
661            ]),
662            last_names: sv(&[
663                "Sato",
664                "Suzuki",
665                "Takahashi",
666                "Tanaka",
667                "Watanabe",
668                "Ito",
669                "Yamamoto",
670                "Nakamura",
671                "Kobayashi",
672                "Kato",
673                "Yoshida",
674                "Yamada",
675                "Sasaki",
676                "Yamaguchi",
677                "Matsumoto",
678                "Inoue",
679                "Kimura",
680                "Hayashi",
681                "Shimizu",
682                "Yamazaki",
683                "Mori",
684                "Abe",
685                "Ikeda",
686                "Hashimoto",
687                "Yamashita",
688                "Ishikawa",
689                "Nakajima",
690                "Maeda",
691                "Fujita",
692                "Ogawa",
693                "Goto",
694                "Okada",
695                "Hasegawa",
696                "Murakami",
697                "Kondo",
698                "Ishii",
699                "Saito",
700                "Sakamoto",
701                "Endo",
702                "Aoki",
703                "Fujii",
704                "Nishimura",
705                "Fukuda",
706                "Ota",
707                "Miura",
708                "Okamoto",
709                "Matsuda",
710                "Nakagawa",
711                "Fujiwara",
712                "Kawamura",
713            ]),
714        }
715    }
716
717    /// Create an Indian name pool.
718    pub fn indian() -> Self {
719        Self {
720            culture: NameCulture::Indian,
721            first_names_male: sv(&[
722                "Raj", "Amit", "Vikram", "Rahul", "Sanjay", "Arun", "Suresh", "Rajesh", "Deepak",
723                "Vijay", "Prakash", "Manoj", "Sunil", "Anil", "Ravi", "Ashok", "Ramesh", "Mukesh",
724                "Sandeep", "Ajay", "Naveen", "Pradeep", "Sachin", "Nitin", "Vinod", "Rakesh",
725                "Srinivas", "Ganesh", "Krishna", "Mohan", "Kiran", "Venkat", "Hari", "Shankar",
726                "Dinesh", "Mahesh", "Satish", "Girish", "Naresh", "Harish", "Pavan", "Arjun",
727                "Anand", "Vivek", "Rohit", "Gaurav", "Kunal", "Vishal", "Akhil", "Dev",
728            ]),
729            first_names_female: sv(&[
730                "Priya",
731                "Anita",
732                "Sunita",
733                "Kavita",
734                "Neha",
735                "Pooja",
736                "Anjali",
737                "Deepa",
738                "Meena",
739                "Rekha",
740                "Lakshmi",
741                "Padma",
742                "Gita",
743                "Sita",
744                "Rani",
745                "Devi",
746                "Shalini",
747                "Nisha",
748                "Swati",
749                "Preeti",
750                "Divya",
751                "Shreya",
752                "Aishwarya",
753                "Ritu",
754                "Seema",
755                "Jyoti",
756                "Shweta",
757                "Pallavi",
758                "Rashmi",
759                "Smita",
760                "Varsha",
761                "Archana",
762                "Asha",
763                "Manisha",
764                "Usha",
765                "Vandana",
766                "Geeta",
767                "Rita",
768                "Maya",
769                "Radha",
770                "Sapna",
771                "Megha",
772                "Nikita",
773                "Tanvi",
774                "Aditi",
775                "Bhavna",
776                "Chitra",
777                "Komal",
778                "Madhuri",
779                "Parul",
780            ]),
781            last_names: sv(&[
782                "Sharma",
783                "Patel",
784                "Singh",
785                "Kumar",
786                "Gupta",
787                "Joshi",
788                "Verma",
789                "Shah",
790                "Mehta",
791                "Reddy",
792                "Rao",
793                "Iyer",
794                "Nair",
795                "Menon",
796                "Pillai",
797                "Das",
798                "Bose",
799                "Sen",
800                "Chatterjee",
801                "Banerjee",
802                "Mukherjee",
803                "Ghosh",
804                "Roy",
805                "Dutta",
806                "Kapoor",
807                "Malhotra",
808                "Agarwal",
809                "Sinha",
810                "Thakur",
811                "Saxena",
812                "Mishra",
813                "Pandey",
814                "Trivedi",
815                "Desai",
816                "Modi",
817                "Kulkarni",
818                "Patil",
819                "Kaur",
820                "Chopra",
821                "Khanna",
822                "Bhatia",
823                "Choudhury",
824                "Rajan",
825                "Subramaniam",
826                "Venkatesh",
827                "Naidu",
828                "Hegde",
829                "Shukla",
830                "Prasad",
831                "Murthy",
832            ]),
833        }
834    }
835
836    /// Create a Hispanic name pool.
837    pub fn hispanic() -> Self {
838        Self {
839            culture: NameCulture::Hispanic,
840            first_names_male: sv(&[
841                "José",
842                "Juan",
843                "Carlos",
844                "Luis",
845                "Miguel",
846                "Francisco",
847                "Antonio",
848                "Manuel",
849                "Pedro",
850                "Javier",
851                "Fernando",
852                "Rafael",
853                "Alejandro",
854                "Ricardo",
855                "Eduardo",
856                "Roberto",
857                "Daniel",
858                "Pablo",
859                "Sergio",
860                "Jorge",
861                "Andrés",
862                "Raúl",
863                "Diego",
864                "Enrique",
865                "Óscar",
866                "Adrián",
867                "Víctor",
868                "Martín",
869                "Gabriel",
870                "Álvaro",
871                "Iván",
872                "Mario",
873                "César",
874                "Héctor",
875                "Alberto",
876                "Gustavo",
877                "Arturo",
878                "Ramón",
879                "Hugo",
880                "Salvador",
881                "Ernesto",
882                "Guillermo",
883                "Ignacio",
884                "Jaime",
885                "Felipe",
886                "Tomás",
887                "Santiago",
888                "Mateo",
889                "Sebastián",
890                "Nicolás",
891            ]),
892            first_names_female: sv(&[
893                "María",
894                "Carmen",
895                "Ana",
896                "Laura",
897                "Rosa",
898                "Isabel",
899                "Elena",
900                "Patricia",
901                "Teresa",
902                "Lucia",
903                "Pilar",
904                "Dolores",
905                "Paula",
906                "Sara",
907                "Marta",
908                "Julia",
909                "Cristina",
910                "Claudia",
911                "Andrea",
912                "Mónica",
913                "Gloria",
914                "Beatriz",
915                "Alicia",
916                "Rocío",
917                "Victoria",
918                "Silvia",
919                "Eva",
920                "Raquel",
921                "Adriana",
922                "Lorena",
923                "Gabriela",
924                "Marina",
925                "Sandra",
926                "Verónica",
927                "Natalia",
928                "Carolina",
929                "Diana",
930                "Alejandra",
931                "Cecilia",
932                "Daniela",
933                "Sofía",
934                "Valentina",
935                "Camila",
936                "Isabella",
937                "Mariana",
938                "Fernanda",
939                "Paola",
940                "Liliana",
941                "Angela",
942                "Inés",
943            ]),
944            last_names: sv(&[
945                "García",
946                "Rodríguez",
947                "Martínez",
948                "López",
949                "González",
950                "Hernández",
951                "Pérez",
952                "Sánchez",
953                "Ramírez",
954                "Torres",
955                "Flores",
956                "Rivera",
957                "Gómez",
958                "Díaz",
959                "Reyes",
960                "Cruz",
961                "Morales",
962                "Ortiz",
963                "Gutiérrez",
964                "Chávez",
965                "Ramos",
966                "Vargas",
967                "Castillo",
968                "Jiménez",
969                "Moreno",
970                "Romero",
971                "Herrera",
972                "Medina",
973                "Aguilar",
974                "Vega",
975                "Castro",
976                "Mendoza",
977                "Ruiz",
978                "Fernández",
979                "Álvarez",
980                "Muñoz",
981                "Rojas",
982                "Silva",
983                "Suárez",
984                "Delgado",
985                "Navarro",
986                "Santos",
987                "Molina",
988                "Espinoza",
989                "Guerrero",
990                "Cabrera",
991                "Campos",
992                "Cortés",
993                "Salazar",
994                "Luna",
995            ]),
996        }
997    }
998
999    /// Get a name pool for a specific culture.
1000    pub fn for_culture(culture: NameCulture) -> Self {
1001        match culture {
1002            NameCulture::WesternUs => Self::western_us(),
1003            NameCulture::German => Self::german(),
1004            NameCulture::French => Self::french(),
1005            NameCulture::Chinese => Self::chinese(),
1006            NameCulture::Japanese => Self::japanese(),
1007            NameCulture::Indian => Self::indian(),
1008            NameCulture::Hispanic => Self::hispanic(),
1009        }
1010    }
1011
1012    /// Generate a random name from this pool.
1013    pub fn generate_name(&self, rng: &mut impl Rng) -> PersonName {
1014        let is_male = rng.gen_bool(0.5);
1015
1016        let first_name = if is_male {
1017            self.first_names_male
1018                .choose(rng)
1019                .expect("non-empty name list")
1020        } else {
1021            self.first_names_female
1022                .choose(rng)
1023                .expect("non-empty name list")
1024        };
1025
1026        let last_name = self.last_names.choose(rng).expect("non-empty name list");
1027
1028        PersonName {
1029            first_name: first_name.clone(),
1030            last_name: last_name.clone(),
1031            display_name: format!("{} {}", first_name, last_name),
1032            culture: self.culture,
1033            is_male,
1034        }
1035    }
1036
1037    /// Create a `NamePool` from a `CultureConfig` loaded from a country pack.
1038    pub fn from_culture_config(
1039        config: &crate::country::schema::CultureConfig,
1040        culture: NameCulture,
1041    ) -> Self {
1042        Self {
1043            culture,
1044            first_names_male: config.male_first_names.clone(),
1045            first_names_female: config.female_first_names.clone(),
1046            last_names: config.last_names.clone(),
1047        }
1048    }
1049}
1050
1051/// Multi-culture name generator with weighted distribution.
1052#[derive(Debug, Clone)]
1053pub struct MultiCultureNameGenerator {
1054    pools: HashMap<NameCulture, NamePool>,
1055    distribution: Vec<(NameCulture, f64)>,
1056    email_domain: String,
1057}
1058
1059impl Default for MultiCultureNameGenerator {
1060    fn default() -> Self {
1061        Self::new()
1062    }
1063}
1064
1065impl MultiCultureNameGenerator {
1066    /// Create a new generator with default distribution.
1067    pub fn new() -> Self {
1068        let mut pools = HashMap::new();
1069        for culture in NameCulture::all() {
1070            pools.insert(*culture, NamePool::for_culture(*culture));
1071        }
1072
1073        Self {
1074            pools,
1075            distribution: vec![
1076                (NameCulture::WesternUs, 0.40),
1077                (NameCulture::Hispanic, 0.20),
1078                (NameCulture::German, 0.10),
1079                (NameCulture::French, 0.05),
1080                (NameCulture::Chinese, 0.10),
1081                (NameCulture::Japanese, 0.05),
1082                (NameCulture::Indian, 0.10),
1083            ],
1084            email_domain: "company.com".to_string(),
1085        }
1086    }
1087
1088    /// Create a generator with custom distribution.
1089    pub fn with_distribution(distribution: Vec<(NameCulture, f64)>) -> Self {
1090        let mut gen = Self::new();
1091        gen.distribution = distribution;
1092        gen
1093    }
1094
1095    /// Set the email domain.
1096    pub fn with_email_domain(mut self, domain: &str) -> Self {
1097        self.email_domain = domain.to_string();
1098        self
1099    }
1100
1101    /// Set the email domain (mutable reference).
1102    pub fn set_email_domain(&mut self, domain: &str) {
1103        self.email_domain = domain.to_string();
1104    }
1105
1106    /// Get the email domain.
1107    pub fn email_domain(&self) -> &str {
1108        &self.email_domain
1109    }
1110
1111    /// Select a culture based on the distribution.
1112    fn select_culture(&self, rng: &mut impl Rng) -> NameCulture {
1113        let roll: f64 = rng.gen();
1114        let mut cumulative = 0.0;
1115
1116        for (culture, weight) in &self.distribution {
1117            cumulative += weight;
1118            if roll < cumulative {
1119                return *culture;
1120            }
1121        }
1122
1123        // Fallback to default
1124        NameCulture::WesternUs
1125    }
1126
1127    /// Generate a random name from the weighted distribution.
1128    pub fn generate_name(&self, rng: &mut impl Rng) -> PersonName {
1129        let culture = self.select_culture(rng);
1130        self.pools
1131            .get(&culture)
1132            .map(|pool| pool.generate_name(rng))
1133            .unwrap_or_else(|| NamePool::western_us().generate_name(rng))
1134    }
1135
1136    /// Generate a name with a specific culture.
1137    pub fn generate_name_for_culture(
1138        &self,
1139        culture: NameCulture,
1140        rng: &mut impl Rng,
1141    ) -> PersonName {
1142        self.pools
1143            .get(&culture)
1144            .map(|pool| pool.generate_name(rng))
1145            .unwrap_or_else(|| NamePool::western_us().generate_name(rng))
1146    }
1147
1148    /// Generate a user ID from a name.
1149    pub fn generate_user_id(&self, name: &PersonName, index: usize) -> String {
1150        name.to_user_id(index)
1151    }
1152
1153    /// Generate an email from a name.
1154    pub fn generate_email(&self, name: &PersonName) -> String {
1155        name.to_email(&self.email_domain)
1156    }
1157
1158    /// Create a name generator from a country pack's names configuration.
1159    pub fn from_country_pack(pack: &crate::country::schema::CountryPack) -> Self {
1160        let mut pools = HashMap::new();
1161        let mut distribution = Vec::new();
1162
1163        for culture_config in &pack.names.cultures {
1164            // Map culture_id to NameCulture enum; default to WesternUs for unknown.
1165            let culture = match culture_config.culture_id.as_str() {
1166                "western_us" | "western" => NameCulture::WesternUs,
1167                "german" | "deutsch" => NameCulture::German,
1168                "french" | "français" => NameCulture::French,
1169                "chinese" | "中文" => NameCulture::Chinese,
1170                "japanese" | "日本語" => NameCulture::Japanese,
1171                "indian" | "hindi" => NameCulture::Indian,
1172                "hispanic" | "latino" | "español" => NameCulture::Hispanic,
1173                _ => NameCulture::WesternUs,
1174            };
1175
1176            let pool = NamePool::from_culture_config(culture_config, culture);
1177            pools.insert(culture, pool);
1178            distribution.push((culture, culture_config.weight));
1179        }
1180
1181        // Fallback: if no cultures were configured, use default pools.
1182        if pools.is_empty() {
1183            return Self::new();
1184        }
1185
1186        let email_domain = pack
1187            .names
1188            .email_domains
1189            .first()
1190            .cloned()
1191            .unwrap_or_else(|| "company.com".to_string());
1192
1193        Self {
1194            pools,
1195            distribution,
1196            email_domain,
1197        }
1198    }
1199}
1200
1201#[cfg(test)]
1202#[allow(clippy::unwrap_used)]
1203mod tests {
1204    use super::*;
1205    use rand::SeedableRng;
1206    use rand_chacha::ChaCha8Rng;
1207
1208    #[test]
1209    fn test_name_pool_generation() {
1210        let mut rng = ChaCha8Rng::seed_from_u64(42);
1211        let pool = NamePool::western_us();
1212        let name = pool.generate_name(&mut rng);
1213
1214        assert!(!name.first_name.is_empty());
1215        assert!(!name.last_name.is_empty());
1216        assert_eq!(name.culture, NameCulture::WesternUs);
1217    }
1218
1219    #[test]
1220    fn test_multi_culture_generator() {
1221        let mut rng = ChaCha8Rng::seed_from_u64(42);
1222        let generator = MultiCultureNameGenerator::new();
1223
1224        // Generate multiple names and check distribution
1225        let mut culture_counts: HashMap<NameCulture, usize> = HashMap::new();
1226        for _ in 0..100 {
1227            let name = generator.generate_name(&mut rng);
1228            *culture_counts.entry(name.culture).or_insert(0) += 1;
1229        }
1230
1231        // Should have some diversity
1232        assert!(culture_counts.len() > 1);
1233    }
1234
1235    #[test]
1236    fn test_user_id_generation() {
1237        let name = PersonName {
1238            first_name: "John".to_string(),
1239            last_name: "Smith".to_string(),
1240            display_name: "John Smith".to_string(),
1241            culture: NameCulture::WesternUs,
1242            is_male: true,
1243        };
1244
1245        let user_id = name.to_user_id(42);
1246        assert_eq!(user_id, "JSMITH042");
1247    }
1248
1249    #[test]
1250    fn test_email_generation() {
1251        let name = PersonName {
1252            first_name: "María".to_string(),
1253            last_name: "García".to_string(),
1254            display_name: "María García".to_string(),
1255            culture: NameCulture::Hispanic,
1256            is_male: false,
1257        };
1258
1259        let email = name.to_email("acme.com");
1260        assert_eq!(email, "mara.garca@acme.com"); // Note: non-ASCII stripped
1261    }
1262
1263    #[test]
1264    fn test_all_culture_pools() {
1265        let mut rng = ChaCha8Rng::seed_from_u64(42);
1266
1267        for culture in NameCulture::all() {
1268            let pool = NamePool::for_culture(*culture);
1269            let name = pool.generate_name(&mut rng);
1270            assert!(!name.first_name.is_empty());
1271            assert!(!name.last_name.is_empty());
1272            assert_eq!(name.culture, *culture);
1273        }
1274    }
1275}