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;
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    /// 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)]
1230#[allow(clippy::unwrap_used)]
1231mod tests {
1232    use super::*;
1233    use rand::SeedableRng;
1234    use rand_chacha::ChaCha8Rng;
1235
1236    #[test]
1237    fn test_name_pool_generation() {
1238        let mut rng = ChaCha8Rng::seed_from_u64(42);
1239        let pool = NamePool::western_us();
1240        let name = pool.generate_name(&mut rng);
1241
1242        assert!(!name.first_name.is_empty());
1243        assert!(!name.last_name.is_empty());
1244        assert_eq!(name.culture, NameCulture::WesternUs);
1245    }
1246
1247    #[test]
1248    fn test_multi_culture_generator() {
1249        let mut rng = ChaCha8Rng::seed_from_u64(42);
1250        let generator = MultiCultureNameGenerator::new();
1251
1252        // Generate multiple names and check distribution
1253        let mut culture_counts: HashMap<NameCulture, usize> = HashMap::new();
1254        for _ in 0..100 {
1255            let name = generator.generate_name(&mut rng);
1256            *culture_counts.entry(name.culture).or_insert(0) += 1;
1257        }
1258
1259        // Should have some diversity
1260        assert!(culture_counts.len() > 1);
1261    }
1262
1263    #[test]
1264    fn test_user_id_generation() {
1265        let name = PersonName {
1266            first_name: "John".to_string(),
1267            last_name: "Smith".to_string(),
1268            display_name: "John Smith".to_string(),
1269            culture: NameCulture::WesternUs,
1270            is_male: true,
1271        };
1272
1273        let user_id = name.to_user_id(42);
1274        assert_eq!(user_id, "JSMITH042");
1275    }
1276
1277    #[test]
1278    fn test_email_generation() {
1279        let name = PersonName {
1280            first_name: "María".to_string(),
1281            last_name: "García".to_string(),
1282            display_name: "María García".to_string(),
1283            culture: NameCulture::Hispanic,
1284            is_male: false,
1285        };
1286
1287        let email = name.to_email("acme.com");
1288        assert_eq!(email, "mara.garca@acme.com"); // Note: non-ASCII stripped
1289    }
1290
1291    #[test]
1292    fn test_all_culture_pools() {
1293        let mut rng = ChaCha8Rng::seed_from_u64(42);
1294
1295        for culture in NameCulture::all() {
1296            let pool = NamePool::for_culture(*culture);
1297            let name = pool.generate_name(&mut rng);
1298            assert!(!name.first_name.is_empty());
1299            assert!(!name.last_name.is_empty());
1300            assert_eq!(name.culture, *culture);
1301        }
1302    }
1303}