331 ng& operator=(
ng&&) noexcept = default;
342 static ng inst{auto_probe_tag{}};
351 const auto code = core::to_utf8(std::wstring{country_code});
352 for (std::size_t i = 0; i < culture_codes_.size(); ++i)
354 if (culture_codes_.at(i) == code)
return static_cast<culture>(i);
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;
374 const auto idx =
static_cast<std::size_t
>(c);
375 return idx < culture_codes_.size() ? culture_codes_.at(idx) : std::string_view{};
383 const auto idx =
static_cast<std::size_t
>(c);
384 return idx < culture_names_.size() ? culture_names_.at(idx) : std::string_view{
"any"};
393 return get_name(request, _engine());
403 std::uint64_t call_seed)
const
405 return solver(
true, request.
gender, request.
culture, call_seed);
420 const core::batch_options& options)
441 const core::batch_options& options,
442 std::uint64_t batch_seed)
const
444 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
445 {
return get_name(request, item_seed); });
461 template <
typename Sink>
463 std::uint64_t batch_seed, Sink&& sink)
const
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));
476 template <
typename Sink>
480 for_each_name(request, options, _engine(), std::forward<Sink>(sink));
500 std::uint64_t call_seed)
const
502 return solver(
false, request.
gender, request.
culture, call_seed);
517 const core::batch_options& options)
538 const core::batch_options& options,
539 std::uint64_t batch_seed)
const
541 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
558 template <
typename Sink>
560 std::uint64_t batch_seed, Sink&& sink)
const
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));
573 template <
typename Sink>
585 return !_m_pool.empty() || !_f_pool.empty() || !_sur_pool.empty();
599 void load(
const std::filesystem::path& resource_path)
602 if (!std::filesystem::is_directory(resource_path, ec))
return;
603 apply_(read_directory_(resource_path), core::layer_mode::replace,
true);
617 [[nodiscard]]
bool load(dataset tier)
619 const std::string tier_dir{core::tier_name(tier)};
620 std::vector<names_file> files;
621 for (
const auto& file : embedded_files_())
623 if (file.path.starts_with(tier_dir +
"/") && file.path.ends_with(
".names"))
625 files.push_back(parse_names_(file.text(),
"embedded:" + std::string{file.path}));
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);
634 if (files.empty())
return false;
635 apply_(std::move(files), core::layer_mode::replace,
true);
651 void extend(
const std::filesystem::path& path,
652 core::layer_mode mode = core::layer_mode::override)
655 if (std::filesystem::is_directory(path, ec))
657 apply_(read_directory_(path), mode,
false);
660 if (path.extension() !=
".names")
662 throw std::invalid_argument(
"extend: expected a .names file, got " +
663 path.filename().string());
665 std::vector<names_file> files;
666 files.push_back(read_file_(path));
667 apply_(std::move(files), mode,
false);
681 core::layer_mode mode = core::layer_mode::override)
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();
699 return _tables.at(list_of_(g)).at(c).layers();
711 const auto list = g == gender::m ? male_surname_list
712 : g == gender::f ? female_surname_list
714 return _tables.at(list).at(c).layers();
720 [[nodiscard]] std::uint64_t
data_hash()
const {
return _data_hash; }
723 static constexpr std::string_view library_name_ =
"name-generator";
724 static constexpr std::string_view marker_file_ =
"american/male.names";
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"};
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"};
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));
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;
779 std::vector<std::uint32_t> ends;
780 core::discrete dist{1.0};
782 [[nodiscard]]
bool empty() const noexcept {
return ends.empty(); }
785 [[nodiscard]] std::vector<std::wstring> names()
const
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));
793 [[nodiscard]]
bool contains(std::wstring_view name)
const
795 std::size_t first = 0;
796 for (
const std::uint32_t end : ends)
798 if (std::wstring_view{text}.substr(first, end - first) == name)
return true;
805 [[nodiscard]] std::wstring at(std::size_t i)
const
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);
814 [[nodiscard]]
static std::uint64_t hash_of_(
815 const std::array<std::map<culture, core::layered_table>, list_count>& tables)
818 for (std::size_t list = 0; list < tables.size(); ++list)
820 for (
const auto& [c, table] : tables.at(list))
822 h = core::hash_combine(h, (list << 8U) |
static_cast<std::uint64_t
>(c));
823 h = core::hash_combine(h, table.data_hash());
834 core::tsv_document doc;
838 std::array<std::map<culture, core::layered_table>, list_count> _tables;
839 std::uint64_t _data_hash{0};
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;
847 core::rng _engine{core::rng::from_entropy()};
850 struct auto_probe_tag {};
853 explicit ng(auto_probe_tag )
855 if (!
load(dataset::lite)) (void)
load(dataset::full);
858 static std::span<const core::embedded_file> embedded_files_()
860#ifdef DASMIG_NAME_EMBED
861 return embedded::namegen::files;
867 [[nodiscard]]
static constexpr list_kind list_of_(gender g)
871 case gender::m:
return male_list;
872 case gender::f:
return female_list;
873 case gender::any:
return surname_list;
878 [[nodiscard]]
const std::map<culture, name_pool>& pools_(list_kind list)
const
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;
889 static core::table_schema schema_()
891 return {.key = {
"name"}, .required = {}, .optional = {
"weight"}};
895 static double weight_of_(
const core::tsv_row& row)
897 const auto w = row.get_or<
double>(
"weight", 1.0);
898 if (!(w > 0.0))
throw row.error(
"weight",
"must be positive");
905 static names_file parse_names_(std::string text, std::string source)
907 if (text.starts_with(
"\xEF\xBB\xBF")) text.erase(0, 3);
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)
914 if (line == 1 || from >= text.size())
916 throw core::tsv_error{source, line,
"",
"expected a country code and a gender line"};
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())};
924 const auto [code, after_code] = header_line(0, 1);
925 const auto [gender_text, after_gender] = header_line(after_code, 2);
928 const auto c =
to_culture(core::from_utf8(code));
929 if (c == culture::any)
931 throw core::tsv_error{source, 1,
"",
"unknown country code '" + code +
"'"};
933 list_kind list = surname_list;
934 if (gender_text ==
"male" || gender_text ==
"m")
938 else if (gender_text ==
"female" || gender_text ==
"f")
942 else if (gender_text ==
"male-surname")
944 list = male_surname_list;
946 else if (gender_text ==
"female-surname")
948 list = female_surname_list;
950 else if (gender_text !=
"any")
952 throw core::tsv_error{source, 2,
"",
953 "expected male, female, any, male-surname or female-surname, got '" +
957 text.insert(after_code,
"#");
959 return {.c = c, .list = list,
960 .doc = core::tsv_document::parse(text, std::move(source), {
"name",
"weight",
"disabled"})};
963 static names_file read_file_(
const std::filesystem::path& file)
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());
976 static std::vector<names_file> read_directory_(
const std::filesystem::path& dir)
978 std::vector<std::filesystem::path> paths;
979 for (
const auto& entry : std::filesystem::recursive_directory_iterator(dir))
981 if (entry.is_regular_file() && entry.path().extension() ==
".names")
983 paths.push_back(entry.path());
986 std::ranges::sort(paths);
987 std::vector<names_file> files;
988 files.reserve(paths.size());
989 for (
const auto& path : paths) files.push_back(read_file_(path));
995 void apply_(std::vector<names_file> files, core::layer_mode mode,
bool reset)
997 auto next = reset ?
decltype(_tables){} : _tables;
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)
1004 auto& tables = next.at(file.list);
1005 auto it = tables.try_emplace(file.c, schema_()).first;
1006 auto list_mode = mode;
1009 list_mode = started.at(file.list)[file.c] ? core::layer_mode::override
1010 : core::layer_mode::replace;
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); });
1019 std::array<std::map<culture, name_pool>, list_count> pools;
1020 for (std::size_t list = 0; list < pools.size(); ++list)
1022 if (!reset) pools.at(list) = pools_(list);
1023 for (
const auto& [c, changed] : touched.at(list))
1025 const auto& table = next.at(list).at(c);
1026 pools.at(list).erase(c);
1027 if (table.empty())
continue;
1029 std::vector<double> weights;
1030 weights.reserve(table.size());
1031 pool.ends.reserve(table.size());
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())
1037 throw std::length_error(
"name-generator: too many names in one list");
1039 pool.text.reserve(bytes);
1040 for (
const auto& row : table.rows())
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));
1046 if (pool.text.capacity() - pool.text.size() > pool.text.size() / 8)
1048 pool.text.shrink_to_fit();
1050 pool.dist = core::discrete{weights};
1051 pools.at(list).emplace(c, std::move(pool));
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));
1067 static culture resolve_culture(culture c,
const std::map<culture, name_pool>& db,
1070 if (c != culture::any)
return c;
1073 throw std::invalid_argument(
"No names loaded for any culture");
1075 auto it = db.begin();
1076 std::advance(it,
static_cast<std::ptrdiff_t
>(core::bounded(engine, db.size())));
1081 static gender resolve_gender(gender g, core::rng& engine)
1083 if (g != gender::any)
return g;
1084 return core::bernoulli(engine, 0.5) ? gender::f : gender::m;
1088 [[nodiscard]]
static const char* gender_label(gender g)
1092 case gender::m:
return "male";
1093 case gender::f:
return "female";
1094 case gender::any:
return "any";
1100 [[nodiscard]]
static std::wstring pick(
const std::map<culture, name_pool>& db,
1101 culture c, gender g, core::rng& engine)
1103 auto it = db.find(c);
1104 if (it == db.end() || it->second.empty())
1106 throw std::invalid_argument(
1107 std::string(
"No ") + gender_label(g) +
" names loaded for culture '" +
1110 const auto& pool = it->second;
1111 return pool.at(pool.dist(engine));
1115 [[nodiscard]] std::tuple<std::wstring, gender, culture> draw_forename_(
1116 gender g, culture c,
const core::rng& r)
const
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};
1130 [[nodiscard]] std::pair<std::wstring, culture> draw_surname_(gender g, culture c,
1131 const core::rng& r)
const
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};
1142 [[nodiscard]] name solver(
bool is_name, gender requested_gender,
1143 culture requested_culture, std::uint64_t call_seed)
const
1145 const core::rng r{call_seed};
1146 name result = [&]() -> name {
1149 auto [text, g, c] = draw_forename_(requested_gender, requested_culture, r);
1150 return {std::move(text), g, c,
this};
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};
1158 result._signature = {.seed = call_seed, .data =
data_hash()};
1164 void append_name_impl(name& n, gender g, culture c)
const
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)));
1170 void append_surname_impl(name& n, culture c)
const
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);
1177 friend struct ::ng_test_access;