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::IndexedRandom;
7use rand::{Rng, RngExt};
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(char::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(char::is_ascii_alphabetic)
81            .collect::<String>()
82            .to_lowercase();
83        let last = self
84            .last_name
85            .chars()
86            .filter(char::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    /// Validate that the name pool has at least one entry in each list.
1013    ///
1014    /// Returns `Err` if any name list is empty, preventing panics during generation.
1015    pub fn validate(&self) -> Result<(), String> {
1016        if self.first_names_male.is_empty() {
1017            return Err(format!(
1018                "NamePool for {:?} has empty male first names list",
1019                self.culture
1020            ));
1021        }
1022        if self.first_names_female.is_empty() {
1023            return Err(format!(
1024                "NamePool for {:?} has empty female first names list",
1025                self.culture
1026            ));
1027        }
1028        if self.last_names.is_empty() {
1029            return Err(format!(
1030                "NamePool for {:?} has empty last names list",
1031                self.culture
1032            ));
1033        }
1034        Ok(())
1035    }
1036
1037    /// Generate a random name from this pool.
1038    pub fn generate_name(&self, rng: &mut impl Rng) -> PersonName {
1039        let is_male = rng.random_bool(0.5);
1040
1041        let first_name = if is_male {
1042            self.first_names_male.choose(rng).expect(
1043                "NamePool male first names must not be empty; call validate() at construction",
1044            )
1045        } else {
1046            self.first_names_female.choose(rng).expect(
1047                "NamePool female first names must not be empty; call validate() at construction",
1048            )
1049        };
1050
1051        let last_name = self
1052            .last_names
1053            .choose(rng)
1054            .expect("NamePool last names must not be empty; call validate() at construction");
1055
1056        PersonName {
1057            first_name: first_name.clone(),
1058            last_name: last_name.clone(),
1059            display_name: format!("{first_name} {last_name}"),
1060            culture: self.culture,
1061            is_male,
1062        }
1063    }
1064
1065    /// Create a `NamePool` from a `CultureConfig` loaded from a country pack.
1066    pub fn from_culture_config(
1067        config: &crate::country::schema::CultureConfig,
1068        culture: NameCulture,
1069    ) -> Self {
1070        Self {
1071            culture,
1072            first_names_male: config.male_first_names.clone(),
1073            first_names_female: config.female_first_names.clone(),
1074            last_names: config.last_names.clone(),
1075        }
1076    }
1077}
1078
1079/// Multi-culture name generator with weighted distribution.
1080#[derive(Debug, Clone)]
1081pub struct MultiCultureNameGenerator {
1082    pools: HashMap<NameCulture, NamePool>,
1083    distribution: Vec<(NameCulture, f64)>,
1084    email_domain: String,
1085}
1086
1087impl Default for MultiCultureNameGenerator {
1088    fn default() -> Self {
1089        Self::new()
1090    }
1091}
1092
1093impl MultiCultureNameGenerator {
1094    /// Create a new generator with default distribution.
1095    pub fn new() -> Self {
1096        let mut pools = HashMap::new();
1097        for culture in NameCulture::all() {
1098            pools.insert(*culture, NamePool::for_culture(*culture));
1099        }
1100
1101        Self {
1102            pools,
1103            distribution: vec![
1104                (NameCulture::WesternUs, 0.40),
1105                (NameCulture::Hispanic, 0.20),
1106                (NameCulture::German, 0.10),
1107                (NameCulture::French, 0.05),
1108                (NameCulture::Chinese, 0.10),
1109                (NameCulture::Japanese, 0.05),
1110                (NameCulture::Indian, 0.10),
1111            ],
1112            email_domain: "company.com".to_string(),
1113        }
1114    }
1115
1116    /// Create a generator with custom distribution.
1117    pub fn with_distribution(distribution: Vec<(NameCulture, f64)>) -> Self {
1118        let mut gen = Self::new();
1119        gen.distribution = distribution;
1120        gen
1121    }
1122
1123    /// Set the email domain.
1124    pub fn with_email_domain(mut self, domain: &str) -> Self {
1125        self.email_domain = domain.to_string();
1126        self
1127    }
1128
1129    /// Set the email domain (mutable reference).
1130    pub fn set_email_domain(&mut self, domain: &str) {
1131        self.email_domain = domain.to_string();
1132    }
1133
1134    /// Get the email domain.
1135    pub fn email_domain(&self) -> &str {
1136        &self.email_domain
1137    }
1138
1139    /// Select a culture based on the distribution.
1140    fn select_culture(&self, rng: &mut impl Rng) -> NameCulture {
1141        let roll: f64 = rng.random();
1142        let mut cumulative = 0.0;
1143
1144        for (culture, weight) in &self.distribution {
1145            cumulative += weight;
1146            if roll < cumulative {
1147                return *culture;
1148            }
1149        }
1150
1151        // Fallback to default
1152        NameCulture::WesternUs
1153    }
1154
1155    /// Generate a random name from the weighted distribution.
1156    pub fn generate_name(&self, rng: &mut impl Rng) -> PersonName {
1157        let culture = self.select_culture(rng);
1158        self.pools
1159            .get(&culture)
1160            .map(|pool| pool.generate_name(rng))
1161            .unwrap_or_else(|| NamePool::western_us().generate_name(rng))
1162    }
1163
1164    /// Generate a name with a specific culture.
1165    pub fn generate_name_for_culture(
1166        &self,
1167        culture: NameCulture,
1168        rng: &mut impl Rng,
1169    ) -> PersonName {
1170        self.pools
1171            .get(&culture)
1172            .map(|pool| pool.generate_name(rng))
1173            .unwrap_or_else(|| NamePool::western_us().generate_name(rng))
1174    }
1175
1176    /// Generate a user ID from a name.
1177    pub fn generate_user_id(&self, name: &PersonName, index: usize) -> String {
1178        name.to_user_id(index)
1179    }
1180
1181    /// Generate an email from a name.
1182    pub fn generate_email(&self, name: &PersonName) -> String {
1183        name.to_email(&self.email_domain)
1184    }
1185
1186    /// Create a name generator from a country pack's names configuration.
1187    pub fn from_country_pack(pack: &crate::country::schema::CountryPack) -> Self {
1188        let mut pools = HashMap::new();
1189        let mut distribution = Vec::new();
1190
1191        for culture_config in &pack.names.cultures {
1192            // Map culture_id to NameCulture enum; default to WesternUs for unknown.
1193            let culture = match culture_config.culture_id.as_str() {
1194                "western_us" | "western" => NameCulture::WesternUs,
1195                "german" | "deutsch" => NameCulture::German,
1196                "french" | "français" => NameCulture::French,
1197                "chinese" | "中文" => NameCulture::Chinese,
1198                "japanese" | "日本語" => NameCulture::Japanese,
1199                "indian" | "hindi" => NameCulture::Indian,
1200                "hispanic" | "latino" | "español" => NameCulture::Hispanic,
1201                _ => NameCulture::WesternUs,
1202            };
1203
1204            let pool = NamePool::from_culture_config(culture_config, culture);
1205            pools.insert(culture, pool);
1206            distribution.push((culture, culture_config.weight));
1207        }
1208
1209        // Fallback: if no cultures were configured, use default pools.
1210        if pools.is_empty() {
1211            return Self::new();
1212        }
1213
1214        let email_domain = pack
1215            .names
1216            .email_domains
1217            .first()
1218            .cloned()
1219            .unwrap_or_else(|| "company.com".to_string());
1220
1221        Self {
1222            pools,
1223            distribution,
1224            email_domain,
1225        }
1226    }
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231    use super::*;
1232    use rand::SeedableRng;
1233    use rand_chacha::ChaCha8Rng;
1234
1235    #[test]
1236    fn test_name_pool_generation() {
1237        let mut rng = ChaCha8Rng::seed_from_u64(42);
1238        let pool = NamePool::western_us();
1239        let name = pool.generate_name(&mut rng);
1240
1241        assert!(!name.first_name.is_empty());
1242        assert!(!name.last_name.is_empty());
1243        assert_eq!(name.culture, NameCulture::WesternUs);
1244    }
1245
1246    #[test]
1247    fn test_multi_culture_generator() {
1248        let mut rng = ChaCha8Rng::seed_from_u64(42);
1249        let generator = MultiCultureNameGenerator::new();
1250
1251        // Generate multiple names and check distribution
1252        let mut culture_counts: HashMap<NameCulture, usize> = HashMap::new();
1253        for _ in 0..100 {
1254            let name = generator.generate_name(&mut rng);
1255            *culture_counts.entry(name.culture).or_insert(0) += 1;
1256        }
1257
1258        // Should have some diversity
1259        assert!(culture_counts.len() > 1);
1260    }
1261
1262    #[test]
1263    fn test_user_id_generation() {
1264        let name = PersonName {
1265            first_name: "John".to_string(),
1266            last_name: "Smith".to_string(),
1267            display_name: "John Smith".to_string(),
1268            culture: NameCulture::WesternUs,
1269            is_male: true,
1270        };
1271
1272        let user_id = name.to_user_id(42);
1273        assert_eq!(user_id, "JSMITH042");
1274    }
1275
1276    #[test]
1277    fn test_email_generation() {
1278        let name = PersonName {
1279            first_name: "María".to_string(),
1280            last_name: "García".to_string(),
1281            display_name: "María García".to_string(),
1282            culture: NameCulture::Hispanic,
1283            is_male: false,
1284        };
1285
1286        let email = name.to_email("acme.com");
1287        assert_eq!(email, "mara.garca@acme.com"); // Note: non-ASCII stripped
1288    }
1289
1290    #[test]
1291    fn test_all_culture_pools() {
1292        let mut rng = ChaCha8Rng::seed_from_u64(42);
1293
1294        for culture in NameCulture::all() {
1295            let pool = NamePool::for_culture(*culture);
1296            let name = pool.generate_name(&mut rng);
1297            assert!(!name.first_name.is_empty());
1298            assert!(!name.last_name.is_empty());
1299            assert_eq!(name.culture, *culture);
1300        }
1301    }
1302}