Name Generator 4.2.1
Culture-aware name generation for C++23
Loading...
Searching...
No Matches
namegen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_NAMEGEN_HPP
5#define DASMIG_NAMEGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <cstddef>
12#include <cstdint>
13#include <filesystem>
14#include <fstream>
15#include <functional>
16#include <iterator>
17#include <limits>
18#include <map>
19#include <optional>
20#include <ostream>
21#include <span>
22#include <stdexcept>
23#include <string>
24#include <string_view>
25#include <system_error>
26#include <tuple>
27#include <utility>
28#include <vector>
29
30#ifdef DASMIG_NAME_EMBED
31#include "namegen_data.hpp"
32#endif
33
34/// @file namegen.hpp
35/// @brief Name generator library — culture-aware name generation for C++23.
36/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
37/// @see See doc/usage.md for the narrative tutorial.
38
39DASMIG_CORE_REQUIRE(1, 1);
40
41struct ng_test_access;
42
43namespace dasmig
44{
45
46/// @brief Culture representing a country or a broader group.
47enum class culture : std::uint8_t
48{
49 afghan,
50 albanian,
51 algerian,
52 american,
53 angolan,
54 argentinian,
55 austrian,
56 azerbaijani,
57 bahraini,
58 bangladeshi,
59 belgian,
60 bolivian,
61 botswanan,
62 brazilian,
63 british,
64 bruneian,
65 bulgarian,
66 burkinabe,
67 burundian,
68 cambodian,
69 cameroonian,
70 canadian,
71 chilean,
72 chinese,
73 colombian,
74 costarican,
75 croatian,
76 cypriot,
77 czech,
78 danish,
79 djiboutian,
80 dutch,
81 ecuadorian,
82 egyptian,
83 emirati,
84 estonian,
85 ethiopian,
86 fijian,
87 filipino,
88 finnish,
89 french,
90 georgian,
91 german,
92 ghanaian,
93 greek,
94 guatemalan,
95 haitian,
96 honduran,
97 hongkonger,
98 hungarian,
99 icelandic,
100 indian,
101 indonesian,
102 iranian,
103 iraqi,
104 irish,
105 israeli,
106 italian,
107 jamaican,
108 japanese,
109 jordanian,
110 kazakh,
111 korean,
112 kuwaiti,
113 lebanese,
114 libyan,
115 lithuanian,
116 luxembourgish,
117 macanese,
118 malaysian,
119 maldivian,
120 maltese,
121 mauritian,
122 mexican,
123 moldovan,
124 moroccan,
125 namibian,
126 nigerian,
127 norwegian,
128 omani,
129 palestinian,
130 panamanian,
131 peruvian,
132 polish,
133 portuguese,
134 puertorican,
135 qatari,
136 russian,
137 salvadoran,
138 saudi,
139 serbian,
140 singaporean,
141 slovenian,
142 southafrican,
143 spanish,
144 sudanese,
145 swedish,
146 swiss,
147 syrian,
148 taiwanese,
149 tunisian,
150 turkish,
151 turkmen,
152 uruguayan,
153 yemeni,
154 any
155};
156
157/// @brief Simple gender enum to distinguish between male and female names.
158enum class gender : std::uint8_t
159{
160 m,
161 f,
162 any
163};
164
165/// @brief The per-call choices of a generation: gender and culture.
166///
167/// Passed to ng::get_name() and ng::get_surname(). Both default to `any`
168/// (random). For a surname the gender selects a gendered surname list where
169/// the culture has one.
170///
171/// @code
172/// gen.get_name({.gender = gender::f, .culture = culture::french});
173/// @endcode
175{
176 dasmig::gender gender = dasmig::gender::any; ///< Requested gender.
177 dasmig::culture culture = dasmig::culture::any; ///< Requested culture.
178};
179
180class ng;
181
182/// @brief Return type for name generation, holding both individual parts and
183/// the full composed string.
184///
185/// Supports implicit conversion to std::wstring, streaming via operator<<,
186/// chained appending of names and surnames, and JSON output via to_json().
187class name
188{
189 public:
190 /// @brief Retrieve the random seed used to generate this name.
191 /// @return The per-call seed for replay.
192 /// @see ng::get_name(const name_request&, std::uint64_t)
193 [[nodiscard]] std::uint64_t seed() const
194 {
195 return _signature.seed;
196 }
197
198 /// @brief The seed and the data this name was generated from.
199 /// @return The replay signature; `data` is ng::data_hash() at the time
200 /// of generation.
201 [[nodiscard]] const core::seed_signature& signature() const
202 {
203 return _signature;
204 }
205
206 /// @brief Return the individual parts (names/surnames) as a vector.
207 /// @return Vector of name parts.
208 [[nodiscard]] const std::vector<std::wstring>& parts() const
209 {
210 return _parts;
211 }
212
213 /// @brief Append a forename to this name, preserving gender and culture.
214 ///
215 /// The part is drawn from this name's seed and its position, so the same
216 /// seed and the same appends give the same full name.
217 /// @return `*this` for chaining.
218 /// @throws std::invalid_argument If no names are loaded for the culture
219 /// and gender.
220 name& append_name();
221
222 /// @brief Append a forename of a specific culture.
223 /// @param c Culture for the appended name.
224 /// @return `*this` for chaining.
225 /// @throws std::invalid_argument If no names are loaded for @p c.
227
228 /// @brief Append a surname to this name, preserving culture.
229 /// @return `*this` for chaining.
230 /// @throws std::invalid_argument If no surnames are loaded for the
231 /// culture.
233
234 /// @brief Append a surname of a specific culture.
235 /// @param c Culture for the appended surname.
236 /// @return `*this` for chaining.
237 /// @throws std::invalid_argument If no surnames are loaded for @p c.
239
240 /// @brief Write this name as a JSON object
241 /// `{"name":…,"parts":[…],"gender":…,"culture":…,"signature":{…}}`.
242 ///
243 /// `gender` is the gender of the forenames (for a surname, the one
244 /// append_name() would use) and `culture` the culture of the first part.
245 /// @param w Writer positioned where a value is allowed.
246 void to_json(core::json_writer& w) const;
247
248 /// @brief This name as a JSON document.
249 /// @return Compact JSON.
250 [[nodiscard]] std::string to_json() const
251 {
252 core::json_writer w;
253 to_json(w);
254 return w.str();
255 }
256
257 /// @brief Implicit conversion to std::wstring.
258 /// @return The full composed name string.
259 operator std::wstring() const // NOLINT(hicpp-explicit-conversions)
260 {
261 return _full_string;
262 }
263
264 /// @brief Implicit conversion to a vector of name parts.
265 operator std::vector<std::wstring>() const // NOLINT(hicpp-explicit-conversions)
266 {
267 return _parts;
268 }
269
270 /// @brief Stream the name to a wide output stream.
271 /// @param wos Output stream.
272 /// @param n Name to stream.
273 /// @return Reference to the output stream.
274 friend std::wostream& operator<<(std::wostream& wos, const name& n)
275 {
276 wos << n._full_string;
277 return wos;
278 }
279
280 private:
281 // Private constructor — names are created only by ng.
282 name(std::wstring name_str, gender g, culture c, const ng* owner)
283 : _full_string(std::move(name_str)), _gender(g), _culture(c), _owner(owner)
284 {
285 _parts.push_back(_full_string);
286 }
287
288 void append_part(std::wstring part)
289 {
290 _full_string.append(L" ").append(part);
291 _parts.push_back(std::move(part));
292 }
293
294 std::wstring _full_string; ///< Full composed name.
295 std::vector<std::wstring> _parts; ///< Individual name parts.
296 gender _gender; ///< Gender of the forenames.
297 culture _culture; ///< Culture of the first part.
298 const ng* _owner; ///< Generator that created this name.
299 core::seed_signature _signature; ///< Seed and data hash.
300
301 friend class ng;
302};
303
304/// @brief Name generator that produces culture-aware names and surnames.
305///
306/// Generates realistic names by picking from popular name databases indexed
307/// by culture and gender, weighted by popularity. Supports 105 cultures.
308///
309/// Output is identical on every compiler and platform (dasmig-core): a seed,
310/// the same request and the same data give the same name everywhere.
311///
312/// Can be used as a singleton via instance() or constructed independently.
313/// Independent instances own their own name databases and random engine.
314///
315/// @par Thread safety
316/// Each instance is independent. Concurrent calls to get_name() on
317/// the **same** instance require external synchronization. load() and
318/// extend() mutate internal state and must not be called concurrently with
319/// get_name() on the same instance.
320class ng
321{
322 public:
323 /// @brief Default constructor — creates an empty generator with no names.
324 ///
325 /// Call load() to populate name databases before generating.
326 ng() = default;
327
328 ng(const ng&) = delete; ///< Not copyable.
329 ng& operator=(const ng&) = delete; ///< Not copyable.
330 ng(ng&&) noexcept = default; ///< Move constructor.
331 ng& operator=(ng&&) noexcept = default; ///< Move assignment.
332 ~ng() = default; ///< Default destructor.
333
334 /// @brief Access the global singleton instance.
335 ///
336 /// Loads the `lite` tier on first access (or `full` when `lite` is not
337 /// found), from compiled-in data or an auto-probed `resources/`.
338 /// For independent generators, prefer constructing a separate ng instance.
339 /// @return Reference to the global ng instance.
340 static ng& instance()
341 {
342 static ng inst{auto_probe_tag{}};
343 return inst;
344 }
345
346 /// @brief Translate an ISO 3166 2-letter country code to a culture enum.
347 /// @param country_code Two-letter country code (e.g., L"us", L"br").
348 /// @return Matching culture, or culture::any if not recognized.
349 [[nodiscard]] static culture to_culture(std::wstring_view country_code)
350 {
351 const auto code = core::to_utf8(std::wstring{country_code});
352 for (std::size_t i = 0; i < culture_codes_.size(); ++i)
353 {
354 if (culture_codes_.at(i) == code) return static_cast<culture>(i);
355 }
356 return culture::any;
357 }
358
359 /// @brief Translate a gender string to a gender enum.
360 /// @param gender_string Gender string (e.g., L"male", L"female", L"m", L"f").
361 /// @return Matching gender, or gender::any if not recognized.
362 [[nodiscard]] static gender to_gender(std::wstring_view gender_string)
363 {
364 if (gender_string == L"m" || gender_string == L"male") return gender::m;
365 if (gender_string == L"f" || gender_string == L"female") return gender::f;
366 return gender::any;
367 }
368
369 /// @brief The ISO 3166 2-letter country code of a culture.
370 /// @param c Culture.
371 /// @return Lower-case code such as `"br"`, or `""` for culture::any.
372 [[nodiscard]] static std::string_view culture_code(culture c)
373 {
374 const auto idx = static_cast<std::size_t>(c);
375 return idx < culture_codes_.size() ? culture_codes_.at(idx) : std::string_view{};
376 }
377
378 /// @brief The name of a culture.
379 /// @param c Culture.
380 /// @return Lower-case name such as `"brazilian"`, or `"any"`.
381 [[nodiscard]] static std::string_view culture_name(culture c)
382 {
383 const auto idx = static_cast<std::size_t>(c);
384 return idx < culture_names_.size() ? culture_names_.at(idx) : std::string_view{"any"};
385 }
386
387 /// @brief Generate a first name.
388 /// @param request Gender and culture (both default to `any`).
389 /// @return A name object supporting chained appending.
390 /// @throws std::invalid_argument If no names loaded for the resolved culture/gender.
391 [[nodiscard]] name get_name(const name_request& request)
392 {
393 return get_name(request, _engine());
394 }
395
396 /// @brief Generate a deterministic first name using a specific seed.
397 /// @param request Gender and culture.
398 /// @param call_seed Seed; the same seed, request and data give the same
399 /// name on every platform.
400 /// @return A name object.
401 /// @throws std::invalid_argument If no names loaded for the resolved culture/gender.
402 [[nodiscard]] name get_name(const name_request& request,
403 std::uint64_t call_seed) const
404 {
405 return solver(true, request.gender, request.culture, call_seed);
406 }
407
408 // -- Batch generation -------------------------------------------------
409
410 /// @brief Generate a batch of name results from a seed drawn like
411 /// get_name() draws its own.
412 ///
413 /// Item `i` is `get_name(request, core::item_seed(batch_seed, options.first + i))`
414 /// for the drawn batch seed; see the seeded overload.
415 ///
416 /// @param request What every item is generated from.
417 /// @param options Count, first index, threads and uniqueness.
418 /// @return The results, in index order.
419 [[nodiscard]] std::vector<name> get_name_batch(const name_request& request,
420 const core::batch_options& options)
421 {
422 return get_name_batch(request, options, _engine());
423 }
424
425 /// @brief Generate a batch of name results, deterministically.
426 ///
427 /// Item `i` is exactly `get_name(request, core::item_seed(batch_seed,
428 /// options.first + i))`, so an item replays on its own, a range can be
429 /// generated directly, two batches that follow each other equal one, and
430 /// the output does not depend on `options.threads`.
431 ///
432 /// @param request What every item is generated from.
433 /// @param options Count, first index, threads and uniqueness (see
434 /// core::unique_rule; the rule must be made for `name`).
435 /// @param batch_seed Seed of the whole batch.
436 /// @return The results, in index order.
437 /// @throws std::invalid_argument If the request or options are invalid.
438 /// @throws std::runtime_error If no data has been loaded, or a unique
439 /// batch runs out of attempts.
440 [[nodiscard]] std::vector<name> get_name_batch(const name_request& request,
441 const core::batch_options& options,
442 std::uint64_t batch_seed) const
443 {
444 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
445 { return get_name(request, item_seed); });
446 }
447
448 /// @brief Stream a batch of name results to a sink without holding the
449 /// whole batch.
450 ///
451 /// Generates the same items as get_name_batch() and calls
452 /// `sink(index, item)` in index order on the calling thread; `index`
453 /// includes `options.first`.
454 ///
455 /// @tparam Sink Callable taking `(std::uint64_t, name&)`; for example
456 /// core::jsonl_writer or core::csv_writer.
457 /// @param request What every item is generated from.
458 /// @param options Count, first index, threads and uniqueness.
459 /// @param batch_seed Seed of the whole batch.
460 /// @param sink Receives the items.
461 template <typename Sink>
462 void for_each_name(const name_request& request, const core::batch_options& options,
463 std::uint64_t batch_seed, Sink&& sink) const
464 {
465 core::for_each_batch(options, batch_seed,
466 [&](std::uint64_t item_seed) { return get_name(request, item_seed); },
467 std::forward<Sink>(sink));
468 }
469
470 /// @brief Stream a batch of name results with a batch seed drawn like
471 /// get_name() draws its own.
472 /// @tparam Sink Callable taking `(std::uint64_t, name&)`.
473 /// @param request What every item is generated from.
474 /// @param options Count, first index, threads and uniqueness.
475 /// @param sink Receives the items.
476 template <typename Sink>
477 void for_each_name(const name_request& request, const core::batch_options& options,
478 Sink&& sink)
479 {
480 for_each_name(request, options, _engine(), std::forward<Sink>(sink));
481 }
482
483
484 /// @brief Generate a surname.
485 /// @param request Culture, and gender for cultures with gendered surnames
486 /// (both default to `any`).
487 /// @return A name object supporting chained appending.
488 /// @throws std::invalid_argument If no surnames loaded for the resolved culture.
489 [[nodiscard]] name get_surname(const name_request& request)
490 {
491 return get_surname(request, _engine());
492 }
493
494 /// @brief Generate a deterministic surname using a specific seed.
495 /// @param request Culture and gender.
496 /// @param call_seed Seed for reproducible results.
497 /// @return A name object.
498 /// @throws std::invalid_argument If no surnames loaded for the resolved culture.
499 [[nodiscard]] name get_surname(const name_request& request,
500 std::uint64_t call_seed) const
501 {
502 return solver(false, request.gender, request.culture, call_seed);
503 }
504
505 // -- Batch generation -------------------------------------------------
506
507 /// @brief Generate a batch of surname results from a seed drawn like
508 /// get_surname() draws its own.
509 ///
510 /// Item `i` is `get_surname(request, core::item_seed(batch_seed, options.first + i))`
511 /// for the drawn batch seed; see the seeded overload.
512 ///
513 /// @param request What every item is generated from.
514 /// @param options Count, first index, threads and uniqueness.
515 /// @return The results, in index order.
516 [[nodiscard]] std::vector<name> get_surname_batch(const name_request& request,
517 const core::batch_options& options)
518 {
519 return get_surname_batch(request, options, _engine());
520 }
521
522 /// @brief Generate a batch of surname results, deterministically.
523 ///
524 /// Item `i` is exactly `get_surname(request, core::item_seed(batch_seed,
525 /// options.first + i))`, so an item replays on its own, a range can be
526 /// generated directly, two batches that follow each other equal one, and
527 /// the output does not depend on `options.threads`.
528 ///
529 /// @param request What every item is generated from.
530 /// @param options Count, first index, threads and uniqueness (see
531 /// core::unique_rule; the rule must be made for `name`).
532 /// @param batch_seed Seed of the whole batch.
533 /// @return The results, in index order.
534 /// @throws std::invalid_argument If the request or options are invalid.
535 /// @throws std::runtime_error If no data has been loaded, or a unique
536 /// batch runs out of attempts.
537 [[nodiscard]] std::vector<name> get_surname_batch(const name_request& request,
538 const core::batch_options& options,
539 std::uint64_t batch_seed) const
540 {
541 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
542 { return get_surname(request, item_seed); });
543 }
544
545 /// @brief Stream a batch of surname results to a sink without holding the
546 /// whole batch.
547 ///
548 /// Generates the same items as get_surname_batch() and calls
549 /// `sink(index, item)` in index order on the calling thread; `index`
550 /// includes `options.first`.
551 ///
552 /// @tparam Sink Callable taking `(std::uint64_t, name&)`; for example
553 /// core::jsonl_writer or core::csv_writer.
554 /// @param request What every item is generated from.
555 /// @param options Count, first index, threads and uniqueness.
556 /// @param batch_seed Seed of the whole batch.
557 /// @param sink Receives the items.
558 template <typename Sink>
559 void for_each_surname(const name_request& request, const core::batch_options& options,
560 std::uint64_t batch_seed, Sink&& sink) const
561 {
562 core::for_each_batch(options, batch_seed,
563 [&](std::uint64_t item_seed) { return get_surname(request, item_seed); },
564 std::forward<Sink>(sink));
565 }
566
567 /// @brief Stream a batch of surname results with a batch seed drawn like
568 /// get_surname() draws its own.
569 /// @tparam Sink Callable taking `(std::uint64_t, name&)`.
570 /// @param request What every item is generated from.
571 /// @param options Count, first index, threads and uniqueness.
572 /// @param sink Receives the items.
573 template <typename Sink>
574 void for_each_surname(const name_request& request, const core::batch_options& options,
575 Sink&& sink)
576 {
577 for_each_surname(request, options, _engine(), std::forward<Sink>(sink));
578 }
579
580
581 /// @brief Check whether any name databases have been loaded.
582 /// @return `true` if at least one name list is loaded.
583 [[nodiscard]] bool has_resources() const
584 {
585 return !_m_pool.empty() || !_f_pool.empty() || !_sur_pool.empty();
586 }
587
588 /// @brief Load name files from a directory, replacing all names and
589 /// layers loaded before.
590 ///
591 /// Reads every `.names` file below @p resource_path, in path order (see
592 /// doc/usage.md for the format). A path that is not a directory is
593 /// ignored.
594 ///
595 /// @param resource_path Directory containing `.names` files, such as
596 /// `resources/lite`.
597 /// @throws core::tsv_error If a file is malformed; the names are then
598 /// unchanged.
599 void load(const std::filesystem::path& resource_path)
600 {
601 std::error_code ec;
602 if (!std::filesystem::is_directory(resource_path, ec)) return;
603 apply_(read_directory_(resource_path), core::layer_mode::replace, true);
604 }
605
606 /// @brief Load a dataset tier, replacing all names and layers loaded
607 /// before.
608 ///
609 /// `lite` uses the compiled-in copy when built with `DASMIG_NAME_EMBED`.
610 /// Otherwise the tier is read from the first of `resources`,
611 /// `../resources`, `name-generator/resources` and
612 /// `../name-generator/resources` that has it.
613 ///
614 /// @param tier The dataset size to load (dataset::lite or dataset::full).
615 /// @return `true` if the tier was found and loaded.
616 /// @throws core::tsv_error If a file is malformed.
617 [[nodiscard]] bool load(dataset tier)
618 {
619 const std::string tier_dir{core::tier_name(tier)};
620 std::vector<names_file> files;
621 for (const auto& file : embedded_files_())
622 {
623 if (file.path.starts_with(tier_dir + "/") && file.path.ends_with(".names"))
624 {
625 files.push_back(parse_names_(file.text(), "embedded:" + std::string{file.path}));
626 }
627 }
628 if (files.empty())
629 {
630 const auto root = core::find_resources(library_name_, tier_dir + "/" + std::string{marker_file_});
631 if (!root) return false;
632 files = read_directory_(*root / tier_dir);
633 }
634 if (files.empty()) return false;
635 apply_(std::move(files), core::layer_mode::replace, true);
636 return true;
637 }
638
639 /// @brief Add name layers from a `.names` file or a directory of them.
640 ///
641 /// Each file names its culture and gender in its first two lines, so it
642 /// adds to (or changes, or removes from) that list. A directory is
643 /// applied all together, or not at all if any file is invalid.
644 ///
645 /// @param path A `.names` file or a directory.
646 /// @param mode How the layers combine with the names loaded before.
647 /// @throws core::tsv_error If a file cannot be read or is invalid; the
648 /// names are then unchanged.
649 /// @throws std::invalid_argument If @p path is a file without the
650 /// `.names` extension.
651 void extend(const std::filesystem::path& path,
652 core::layer_mode mode = core::layer_mode::override)
653 {
654 std::error_code ec;
655 if (std::filesystem::is_directory(path, ec))
656 {
657 apply_(read_directory_(path), mode, false);
658 return;
659 }
660 if (path.extension() != ".names")
661 {
662 throw std::invalid_argument("extend: expected a .names file, got " +
663 path.filename().string());
664 }
665 std::vector<names_file> files;
666 files.push_back(read_file_(path));
667 apply_(std::move(files), mode, false);
668 }
669
670 /// @brief Add a name layer from text in the `.names` format.
671 /// @param text The country code, the list (`male`, `female` or `any`
672 /// for surnames; `male-surname` or `female-surname` for the
673 /// surnames of one gender), then one name per line with an
674 /// optional weight.
675 /// @param source Name used in errors.
676 /// @param mode How the layer combines with the names loaded before.
677 /// @return What the layer did.
678 /// @throws core::tsv_error If the text is invalid; the names are then
679 /// unchanged.
680 const core::layer_info& extend_from_string(std::string_view text, std::string source,
681 core::layer_mode mode = core::layer_mode::override)
682 {
683 std::vector<names_file> files;
684 files.push_back(parse_names_(std::string{text}, std::move(source)));
685 const auto list = files.front().list;
686 const auto c = files.front().c;
687 apply_(std::move(files), mode, false);
688 return _tables.at(list).at(c).layers().back();
689 }
690
691 /// @brief The layers in effect for one name list.
692 /// @param c Culture.
693 /// @param g gender::m or gender::f for forenames, gender::any for
694 /// surnames.
695 /// @return The layer records.
696 /// @throws std::out_of_range If that list was never loaded.
697 [[nodiscard]] const std::vector<core::layer_info>& layers(culture c, gender g) const
698 {
699 return _tables.at(list_of_(g)).at(c).layers();
700 }
701
702 /// @brief The layers in effect for the surnames of one gender.
703 /// @param c Culture.
704 /// @param g gender::m or gender::f; gender::any is the surname list
705 /// shared by both.
706 /// @return The layer records.
707 /// @throws std::out_of_range If that list was never loaded (a culture
708 /// has one only when its surnames change with gender).
709 [[nodiscard]] const std::vector<core::layer_info>& surname_layers(culture c, gender g) const
710 {
711 const auto list = g == gender::m ? male_surname_list
712 : g == gender::f ? female_surname_list
713 : surname_list;
714 return _tables.at(list).at(c).layers();
715 }
716
717 /// @brief Hash of the loaded names, recorded in each result's
718 /// signature().
719 /// @return The hash; 0 when nothing is loaded.
720 [[nodiscard]] std::uint64_t data_hash() const { return _data_hash; }
721
722 private:
723 static constexpr std::string_view library_name_ = "name-generator";
724 static constexpr std::string_view marker_file_ = "american/male.names";
725
726 // Indexed by culture.
727 static constexpr std::array<std::string_view, 105> culture_codes_{
728 "af", "al", "dz", "us", "ao", "ar", "at", "az", "bh", "bd", "be", "bo", "bw", "br", "gb",
729 "bn", "bg", "bf", "bi", "kh", "cm", "ca", "cl", "cn", "co", "cr", "hr", "cy", "cz", "dk",
730 "dj", "nl", "ec", "eg", "ae", "ee", "et", "fj", "ph", "fi", "fr", "ge", "de", "gh", "gr",
731 "gt", "ht", "hn", "hk", "hu", "is", "in", "id", "ir", "iq", "ie", "il", "it", "jm", "jp",
732 "jo", "kz", "kr", "kw", "lb", "ly", "lt", "lu", "mo", "my", "mv", "mt", "mu", "mx", "md",
733 "ma", "na", "ng", "no", "om", "ps", "pa", "pe", "pl", "pt", "pr", "qa", "ru", "sv", "sa",
734 "rs", "sg", "si", "za", "es", "sd", "se", "ch", "sy", "tw", "tn", "tr", "tm", "uy", "ye"};
735
736 static constexpr std::array<std::string_view, 105> culture_names_{
737 "afghan", "albanian", "algerian", "american", "angolan",
738 "argentinian", "austrian", "azerbaijani", "bahraini", "bangladeshi",
739 "belgian", "bolivian", "botswanan", "brazilian", "british",
740 "bruneian", "bulgarian", "burkinabe", "burundian", "cambodian",
741 "cameroonian", "canadian", "chilean", "chinese", "colombian",
742 "costarican", "croatian", "cypriot", "czech", "danish",
743 "djiboutian", "dutch", "ecuadorian", "egyptian", "emirati",
744 "estonian", "ethiopian", "fijian", "filipino", "finnish",
745 "french", "georgian", "german", "ghanaian", "greek",
746 "guatemalan", "haitian", "honduran", "hongkonger", "hungarian",
747 "icelandic", "indian", "indonesian", "iranian", "iraqi",
748 "irish", "israeli", "italian", "jamaican", "japanese",
749 "jordanian", "kazakh", "korean", "kuwaiti", "lebanese",
750 "libyan", "lithuanian", "luxembourgish", "macanese", "malaysian",
751 "maldivian", "maltese", "mauritian", "mexican", "moldovan",
752 "moroccan", "namibian", "nigerian", "norwegian", "omani",
753 "palestinian", "panamanian", "peruvian", "polish", "portuguese",
754 "puertorican", "qatari", "russian", "salvadoran", "saudi",
755 "serbian", "singaporean", "slovenian", "southafrican", "spanish",
756 "sudanese", "swedish", "swiss", "syrian", "taiwanese",
757 "tunisian", "turkish", "turkmen", "uruguayan", "yemeni"};
758
759 static_assert(culture_codes_.size() == static_cast<std::size_t>(culture::any));
760 static_assert(culture_names_.size() == static_cast<std::size_t>(culture::any));
761
762 // The kinds of name list, as indices into _tables. The gendered surname
763 // lists exist only for cultures whose surnames change with gender; the
764 // others fall back to surname_list.
765 using list_kind = std::size_t;
766 static constexpr list_kind male_list = 0;
767 static constexpr list_kind female_list = 1;
768 static constexpr list_kind surname_list = 2;
769 static constexpr list_kind male_surname_list = 3;
770 static constexpr list_kind female_surname_list = 4;
771 static constexpr std::size_t list_count = 5;
772
773 // Names of one list, ready for drawing. The names lie one after the
774 // other in a single buffer: a string each would cost 32 bytes and often
775 // an allocation on top of the name.
776 struct name_pool
777 {
778 std::wstring text; // every name, in order
779 std::vector<std::uint32_t> ends; // where each name ends in text
780 core::discrete dist{1.0};
781
782 [[nodiscard]] bool empty() const noexcept { return ends.empty(); }
783
784 // Every name, as strings (the tests compare against them).
785 [[nodiscard]] std::vector<std::wstring> names() const
786 {
787 std::vector<std::wstring> out;
788 out.reserve(ends.size());
789 for (std::size_t i = 0; i < ends.size(); ++i) out.push_back(at(i));
790 return out;
791 }
792
793 [[nodiscard]] bool contains(std::wstring_view name) const
794 {
795 std::size_t first = 0;
796 for (const std::uint32_t end : ends)
797 {
798 if (std::wstring_view{text}.substr(first, end - first) == name) return true;
799 first = end;
800 }
801 return false;
802 }
803
804 // The name at @p i; throws std::out_of_range past the last.
805 [[nodiscard]] std::wstring at(std::size_t i) const
806 {
807 const std::size_t end = ends.at(i);
808 const std::size_t first = i == 0 ? 0 : ends[i - 1];
809 return text.substr(first, end - first);
810 }
811 };
812
813 // The hash data_hash() returns: every table's layers, by list and culture.
814 [[nodiscard]] static std::uint64_t hash_of_(
815 const std::array<std::map<culture, core::layered_table>, list_count>& tables)
816 {
817 std::uint64_t h = 0;
818 for (std::size_t list = 0; list < tables.size(); ++list)
819 {
820 for (const auto& [c, table] : tables.at(list))
821 {
822 h = core::hash_combine(h, (list << 8U) | static_cast<std::uint64_t>(c));
823 h = core::hash_combine(h, table.data_hash());
824 }
825 }
826 return h;
827 }
828
829 // One parsed .names file.
830 struct names_file
831 {
832 culture c;
833 list_kind list;
834 core::tsv_document doc;
835 };
836
837 // Layered data per list kind and culture, and the pools built from it.
838 std::array<std::map<culture, core::layered_table>, list_count> _tables;
839 std::uint64_t _data_hash{0}; // hash_of_(_tables), kept with it: every result needs it
840 std::map<culture, name_pool> _m_pool;
841 std::map<culture, name_pool> _f_pool;
842 std::map<culture, name_pool> _sur_pool;
843 std::map<culture, name_pool> _m_sur_pool;
844 std::map<culture, name_pool> _f_sur_pool;
845
846 // Per-instance random engine for seed drawing.
847 core::rng _engine{core::rng::from_entropy()};
848
849 // Tag type for the auto-probing singleton constructor.
850 struct auto_probe_tag {};
851
852 // Singleton constructor: loads lite, or full when lite is not found.
853 explicit ng(auto_probe_tag /*tag*/)
854 {
855 if (!load(dataset::lite)) (void)load(dataset::full);
856 }
857
858 static std::span<const core::embedded_file> embedded_files_()
859 {
860#ifdef DASMIG_NAME_EMBED
861 return embedded::namegen::files;
862#else
863 return {};
864#endif
865 }
866
867 [[nodiscard]] static constexpr list_kind list_of_(gender g)
868 {
869 switch (g)
870 {
871 case gender::m: return male_list;
872 case gender::f: return female_list;
873 case gender::any: return surname_list;
874 }
875 return surname_list;
876 }
877
878 [[nodiscard]] const std::map<culture, name_pool>& pools_(list_kind list) const
879 {
880 if (list == male_list) return _m_pool;
881 if (list == female_list) return _f_pool;
882 if (list == male_surname_list) return _m_sur_pool;
883 if (list == female_surname_list) return _f_sur_pool;
884 return _sur_pool;
885 }
886
887 // -- Data ---------------------------------------------------------------
888
889 static core::table_schema schema_()
890 {
891 return {.key = {"name"}, .required = {}, .optional = {"weight"}};
892 }
893
894 // A row's weight: 1 when the cell is empty, otherwise a positive number.
895 static double weight_of_(const core::tsv_row& row)
896 {
897 const auto w = row.get_or<double>("weight", 1.0);
898 if (!(w > 0.0)) throw row.error("weight", "must be positive");
899 return w;
900 }
901
902 // Parse the .names format: the country code and the gender on the first
903 // two lines, then `name<TAB>weight` rows. The two header lines are kept
904 // as comments, so errors give the file's own line numbers.
905 static names_file parse_names_(std::string text, std::string source)
906 {
907 if (text.starts_with("\xEF\xBB\xBF")) text.erase(0, 3);
908 // A header line's text and where the next line starts; the gender
909 // line may end the file.
910 auto header_line = [&](std::size_t from, std::size_t line) {
911 auto end = text.find('\n', from);
912 if (end == std::string::npos)
913 {
914 if (line == 1 || from >= text.size())
915 {
916 throw core::tsv_error{source, line, "", "expected a country code and a gender line"};
917 }
918 end = text.size();
919 }
920 auto value = std::string_view{text}.substr(from, end - from);
921 if (value.ends_with('\r')) value.remove_suffix(1);
922 return std::pair{std::string{value}, std::min(end + 1, text.size())};
923 };
924 const auto [code, after_code] = header_line(0, 1);
925 const auto [gender_text, after_gender] = header_line(after_code, 2);
926 (void)after_gender;
927
928 const auto c = to_culture(core::from_utf8(code));
929 if (c == culture::any)
930 {
931 throw core::tsv_error{source, 1, "", "unknown country code '" + code + "'"};
932 }
933 list_kind list = surname_list;
934 if (gender_text == "male" || gender_text == "m")
935 {
936 list = male_list;
937 }
938 else if (gender_text == "female" || gender_text == "f")
939 {
940 list = female_list;
941 }
942 else if (gender_text == "male-surname")
943 {
944 list = male_surname_list;
945 }
946 else if (gender_text == "female-surname")
947 {
948 list = female_surname_list;
949 }
950 else if (gender_text != "any")
951 {
952 throw core::tsv_error{source, 2, "",
953 "expected male, female, any, male-surname or female-surname, got '" +
954 gender_text + "'"};
955 }
956
957 text.insert(after_code, "#");
958 text.insert(0, "#");
959 return {.c = c, .list = list,
960 .doc = core::tsv_document::parse(text, std::move(source), {"name", "weight", "disabled"})};
961 }
962
963 static names_file read_file_(const std::filesystem::path& file)
964 {
965 std::ifstream in{file, std::ios::binary};
966 if (!in) throw core::tsv_error{file.string(), 0, "", "cannot open file"};
967 in.seekg(0, std::ios::end);
968 const auto size = in.tellg();
969 std::string text(size > 0 ? static_cast<std::size_t>(size) : 0, '\0');
970 in.seekg(0, std::ios::beg);
971 in.read(text.data(), static_cast<std::streamsize>(text.size()));
972 text.resize(static_cast<std::size_t>(in.gcount()));
973 return parse_names_(std::move(text), file.string());
974 }
975
976 static std::vector<names_file> read_directory_(const std::filesystem::path& dir)
977 {
978 std::vector<std::filesystem::path> paths;
979 for (const auto& entry : std::filesystem::recursive_directory_iterator(dir))
980 {
981 if (entry.is_regular_file() && entry.path().extension() == ".names")
982 {
983 paths.push_back(entry.path());
984 }
985 }
986 std::ranges::sort(paths); // the iteration order is unspecified
987 std::vector<names_file> files;
988 files.reserve(paths.size());
989 for (const auto& path : paths) files.push_back(read_file_(path));
990 return files;
991 }
992
993 // Apply layers to copies of the affected tables, then commit: all or
994 // nothing. With @p reset (load), every list is replaced.
995 void apply_(std::vector<names_file> files, core::layer_mode mode, bool reset)
996 {
997 auto next = reset ? decltype(_tables){} : _tables;
998 // On reset each list's first file replaces; later files for the same
999 // list extend it.
1000 std::array<std::map<culture, bool>, list_count> started;
1001 std::array<std::map<culture, bool>, list_count> touched;
1002 for (auto& file : files)
1003 {
1004 auto& tables = next.at(file.list);
1005 auto it = tables.try_emplace(file.c, schema_()).first;
1006 auto list_mode = mode;
1007 if (reset)
1008 {
1009 list_mode = started.at(file.list)[file.c] ? core::layer_mode::override
1010 : core::layer_mode::replace;
1011 }
1012 started.at(file.list)[file.c] = true;
1013 touched.at(file.list)[file.c] = true;
1014 (void)it->second.extend(std::move(file.doc), list_mode,
1015 [](const core::tsv_row& row) { (void)weight_of_(row); });
1016 }
1017
1018 // Rebuild the pools of the lists that changed.
1019 std::array<std::map<culture, name_pool>, list_count> pools;
1020 for (std::size_t list = 0; list < pools.size(); ++list)
1021 {
1022 if (!reset) pools.at(list) = pools_(list);
1023 for (const auto& [c, changed] : touched.at(list))
1024 {
1025 const auto& table = next.at(list).at(c);
1026 pools.at(list).erase(c);
1027 if (table.empty()) continue;
1028 name_pool pool;
1029 std::vector<double> weights;
1030 weights.reserve(table.size());
1031 pool.ends.reserve(table.size());
1032 // A name has at most as many characters as bytes.
1033 std::size_t bytes = 0;
1034 for (const auto& row : table.rows()) bytes += row["name"].size();
1035 if (bytes >= std::numeric_limits<std::uint32_t>::max())
1036 {
1037 throw std::length_error("name-generator: too many names in one list");
1038 }
1039 pool.text.reserve(bytes);
1040 for (const auto& row : table.rows())
1041 {
1042 pool.text += core::from_utf8(row["name"]);
1043 pool.ends.push_back(static_cast<std::uint32_t>(pool.text.size()));
1044 weights.push_back(weight_of_(row));
1045 }
1046 if (pool.text.capacity() - pool.text.size() > pool.text.size() / 8)
1047 {
1048 pool.text.shrink_to_fit();
1049 }
1050 pool.dist = core::discrete{weights};
1051 pools.at(list).emplace(c, std::move(pool));
1052 }
1053 }
1054
1055 _tables = std::move(next);
1056 _data_hash = hash_of_(_tables);
1057 _m_pool = std::move(pools.at(male_list));
1058 _f_pool = std::move(pools.at(female_list));
1059 _sur_pool = std::move(pools.at(surname_list));
1060 _m_sur_pool = std::move(pools.at(male_surname_list));
1061 _f_sur_pool = std::move(pools.at(female_surname_list));
1062 }
1063
1064 // -- Generation ---------------------------------------------------------
1065
1066 // Resolve `any` culture to a random culture from those actually loaded.
1067 static culture resolve_culture(culture c, const std::map<culture, name_pool>& db,
1068 core::rng& engine)
1069 {
1070 if (c != culture::any) return c;
1071 if (db.empty())
1072 {
1073 throw std::invalid_argument("No names loaded for any culture");
1074 }
1075 auto it = db.begin();
1076 std::advance(it, static_cast<std::ptrdiff_t>(core::bounded(engine, db.size())));
1077 return it->first;
1078 }
1079
1080 // Resolve `any` gender to a concrete random gender.
1081 static gender resolve_gender(gender g, core::rng& engine)
1082 {
1083 if (g != gender::any) return g;
1084 return core::bernoulli(engine, 0.5) ? gender::f : gender::m;
1085 }
1086
1087 // Convert a gender enum to its display name for error messages.
1088 [[nodiscard]] static const char* gender_label(gender g)
1089 {
1090 switch (g)
1091 {
1092 case gender::m: return "male";
1093 case gender::f: return "female";
1094 case gender::any: return "any";
1095 }
1096 return "unknown";
1097 }
1098
1099 // Pick a weighted-random name/surname from the appropriate map.
1100 [[nodiscard]] static std::wstring pick(const std::map<culture, name_pool>& db,
1101 culture c, gender g, core::rng& engine)
1102 {
1103 auto it = db.find(c);
1104 if (it == db.end() || it->second.empty())
1105 {
1106 throw std::invalid_argument(
1107 std::string("No ") + gender_label(g) + " names loaded for culture '" +
1108 std::string{culture_name(c)} + "'");
1109 }
1110 const auto& pool = it->second;
1111 return pool.at(pool.dist(engine));
1112 }
1113
1114 // One forename: gender, culture and name from their own streams.
1115 [[nodiscard]] std::tuple<std::wstring, gender, culture> draw_forename_(
1116 gender g, culture c, const core::rng& r) const
1117 {
1118 auto rg = r.split("gender");
1119 const gender resolved_g = resolve_gender(g, rg);
1120 const auto& db = resolved_g == gender::f ? _f_pool : _m_pool;
1121 auto rc = r.split("culture");
1122 const culture resolved_c = resolve_culture(c, db, rc);
1123 auto rn = r.split("name");
1124 return {pick(db, resolved_c, resolved_g, rn), resolved_g, resolved_c};
1125 }
1126
1127 // One surname: culture and name from their own streams. A culture with
1128 // a surname list for the gender draws from it, the others from the list
1129 // shared by both.
1130 [[nodiscard]] std::pair<std::wstring, culture> draw_surname_(gender g, culture c,
1131 const core::rng& r) const
1132 {
1133 auto rc = r.split("culture");
1134 const culture resolved_c = resolve_culture(c, _sur_pool, rc);
1135 auto rn = r.split("name");
1136 const auto& gendered = g == gender::f ? _f_sur_pool : _m_sur_pool;
1137 const auto& db = gendered.contains(resolved_c) ? gendered : _sur_pool;
1138 return {pick(db, resolved_c, gender::any, rn), resolved_c};
1139 }
1140
1141 // Core generation logic.
1142 [[nodiscard]] name solver(bool is_name, gender requested_gender,
1143 culture requested_culture, std::uint64_t call_seed) const
1144 {
1145 const core::rng r{call_seed};
1146 name result = [&]() -> name {
1147 if (is_name)
1148 {
1149 auto [text, g, c] = draw_forename_(requested_gender, requested_culture, r);
1150 return {std::move(text), g, c, this};
1151 }
1152 // A surname still has a gender: the one append_name() uses.
1153 auto rg = r.split("gender");
1154 const gender g = resolve_gender(requested_gender, rg);
1155 auto [text, c] = draw_surname_(g, requested_culture, r);
1156 return {std::move(text), g, c, this};
1157 }();
1158 result._signature = {.seed = call_seed, .data = data_hash()};
1159 return result;
1160 }
1161
1162 // Parts appended to a name draw from the name's seed and the part's
1163 // position, so a seed and the same appends replay the whole name.
1164 void append_name_impl(name& n, gender g, culture c) const
1165 {
1166 const auto r = core::rng{n.seed()}.split("part").split(n._parts.size());
1167 n.append_part(std::get<0>(draw_forename_(g, c, r)));
1168 }
1169
1170 void append_surname_impl(name& n, culture c) const
1171 {
1172 const auto r = core::rng{n.seed()}.split("part").split(n._parts.size());
1173 n.append_part(draw_surname_(n._gender, c, r).first);
1174 }
1175
1176 friend class name;
1177 friend struct ::ng_test_access;
1178};
1179
1180// Out-of-line definitions for name methods that call into ng.
1182{
1183 _owner->append_name_impl(*this, _gender, _culture);
1184 return *this;
1185}
1186
1188{
1189 _owner->append_name_impl(*this, _gender, c);
1190 return *this;
1191}
1192
1194{
1195 _owner->append_surname_impl(*this, _culture);
1196 return *this;
1197}
1198
1200{
1201 _owner->append_surname_impl(*this, c);
1202 return *this;
1203}
1204
1205inline void name::to_json(core::json_writer& w) const
1206{
1207 w.begin_object().field("name", core::to_utf8(_full_string)).key("parts").begin_array();
1208 for (const auto& part : _parts) w.value(core::to_utf8(part));
1209 w.end_array()
1210 .field("gender", _gender == gender::f ? "female" : "male")
1211 .field("culture", ng::culture_name(_culture))
1212 .key("signature");
1213 _signature.to_json(w);
1214 w.end_object();
1215}
1216
1217} // namespace dasmig
1218
1219#endif // DASMIG_NAMEGEN_HPP
Return type for name generation, holding both individual parts and the full composed string.
Definition namegen.hpp:188
friend std::wostream & operator<<(std::wostream &wos, const name &n)
Stream the name to a wide output stream.
Definition namegen.hpp:274
name & append_surname()
Append a surname to this name, preserving culture.
Definition namegen.hpp:1193
name & append_name()
Append a forename to this name, preserving gender and culture.
Definition namegen.hpp:1181
const core::seed_signature & signature() const
The seed and the data this name was generated from.
Definition namegen.hpp:201
std::string to_json() const
This name as a JSON document.
Definition namegen.hpp:250
const std::vector< std::wstring > & parts() const
Return the individual parts (names/surnames) as a vector.
Definition namegen.hpp:208
std::uint64_t seed() const
Retrieve the random seed used to generate this name.
Definition namegen.hpp:193
Name generator that produces culture-aware names and surnames.
Definition namegen.hpp:321
std::vector< name > get_surname_batch(const name_request &request, const core::batch_options &options)
Generate a batch of surname results from a seed drawn like get_surname() draws its own.
Definition namegen.hpp:516
ng & operator=(const ng &)=delete
Not copyable.
ng(const ng &)=delete
Not copyable.
static std::string_view culture_name(culture c)
The name of a culture.
Definition namegen.hpp:381
name get_name(const name_request &request, std::uint64_t call_seed) const
Generate a deterministic first name using a specific seed.
Definition namegen.hpp:402
name get_name(const name_request &request)
Generate a first name.
Definition namegen.hpp:391
const std::vector< core::layer_info > & layers(culture c, gender g) const
The layers in effect for one name list.
Definition namegen.hpp:697
bool has_resources() const
Check whether any name databases have been loaded.
Definition namegen.hpp:583
void for_each_surname(const name_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of surname results to a sink without holding the whole batch.
Definition namegen.hpp:559
std::vector< name > get_name_batch(const name_request &request, const core::batch_options &options)
Generate a batch of name results from a seed drawn like get_name() draws its own.
Definition namegen.hpp:419
const core::layer_info & extend_from_string(std::string_view text, std::string source, core::layer_mode mode=core::layer_mode::override)
Add a name layer from text in the .names format.
Definition namegen.hpp:680
std::vector< name > get_surname_batch(const name_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of surname results, deterministically.
Definition namegen.hpp:537
name get_surname(const name_request &request)
Generate a surname.
Definition namegen.hpp:489
ng()=default
Default constructor — creates an empty generator with no names.
const std::vector< core::layer_info > & surname_layers(culture c, gender g) const
The layers in effect for the surnames of one gender.
Definition namegen.hpp:709
bool load(dataset tier)
Load a dataset tier, replacing all names and layers loaded before.
Definition namegen.hpp:617
static gender to_gender(std::wstring_view gender_string)
Translate a gender string to a gender enum.
Definition namegen.hpp:362
void load(const std::filesystem::path &resource_path)
Load name files from a directory, replacing all names and layers loaded before.
Definition namegen.hpp:599
static ng & instance()
Access the global singleton instance.
Definition namegen.hpp:340
void for_each_surname(const name_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of surname results with a batch seed drawn like get_surname() draws its own.
Definition namegen.hpp:574
std::vector< name > get_name_batch(const name_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of name results, deterministically.
Definition namegen.hpp:440
void extend(const std::filesystem::path &path, core::layer_mode mode=core::layer_mode::override)
Add name layers from a .names file or a directory of them.
Definition namegen.hpp:651
void for_each_name(const name_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of name results to a sink without holding the whole batch.
Definition namegen.hpp:462
void for_each_name(const name_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of name results with a batch seed drawn like get_name() draws its own.
Definition namegen.hpp:477
name get_surname(const name_request &request, std::uint64_t call_seed) const
Generate a deterministic surname using a specific seed.
Definition namegen.hpp:499
ng(ng &&) noexcept=default
Move constructor.
static culture to_culture(std::wstring_view country_code)
Translate an ISO 3166 2-letter country code to a culture enum.
Definition namegen.hpp:349
static std::string_view culture_code(culture c)
The ISO 3166 2-letter country code of a culture.
Definition namegen.hpp:372
std::uint64_t data_hash() const
Hash of the loaded names, recorded in each result's signature().
Definition namegen.hpp:720
culture
Culture representing a country or a broader group.
Definition namegen.hpp:48
gender
Simple gender enum to distinguish between male and female names.
Definition namegen.hpp:159
The per-call choices of a generation: gender and culture.
Definition namegen.hpp:175
dasmig::culture culture
Requested culture.
Definition namegen.hpp:177
dasmig::gender gender
Requested gender.
Definition namegen.hpp:176