246 phg() : _tables{make_tables_()} {}
251 phg& operator=(
phg&&) noexcept = default;
261 static phg inst{auto_probe_tag{}};
290 const core::rng call{call_seed};
291 const std::size_t want = request.
type ?
static_cast<std::size_t
>(*request.
type) : any_;
292 const auto& country = _model.countries[pick_country_(request, want, call)];
293 const std::size_t ti = pick_type_(request, country, want, call);
294 const auto& by_type = country.types.at(ti);
296 auto rr = call.split(
"phone-range");
297 auto dr = call.split(
"phone-digits");
298 const bool reserved = request.
safe && !by_type.reserved.empty();
299 const range_data& range =
reserved ? by_type.reserved[request.
weighted ? by_type.reserved_weights.pick(rr)
300 : pick_uniform_(rr, by_type.reserved.size())]
301 : by_type.ranges[request.
weighted ? by_type.weights.pick(rr)
302 : pick_uniform_(rr, by_type.ranges.size())];
305 v._signature = {.seed = call_seed, .data =
data_hash()};
308 v.
type = ti == 0 ? phone_type::mobile : phone_type::fixed;
327 const core::batch_options& options)
348 const core::batch_options& options,
349 std::uint64_t batch_seed)
const
351 return core::run_batch(options, batch_seed,
352 [&](std::uint64_t item_seed) {
return get_phone(request, item_seed); });
368 template <
typename Sink>
370 std::uint64_t batch_seed, Sink&& sink)
const
372 core::for_each_batch(options, batch_seed,
373 [&](std::uint64_t item_seed) {
return get_phone(request, item_seed); },
374 std::forward<Sink>(sink));
383 template <
typename Sink>
386 for_each_phone(request, options, draw_seed_(), std::forward<Sink>(sink));
393 [[nodiscard]]
bool has_data()
const {
return !_model.countries.empty(); }
397 [[nodiscard]] std::size_t
country_count()
const {
return _model.codes.size(); }
402 [[nodiscard]]
const std::vector<std::string>&
countries()
const {
return _model.codes; }
409 const auto it = _model.by_region.find(upper_(country));
410 return it != _model.by_region.end() && std::ranges::any_of(_model.countries[it->second].types, [](
const type_data& t) {
411 return !t.ranges.empty();
421 const auto it = _model.by_region.find(upper_(country));
422 return it != _model.by_region.end() &&
423 !_model.countries[it->second].types.at(
static_cast<std::size_t
>(type)).reserved.empty();
435 void load(
const std::filesystem::path& dir)
438 if (!std::filesystem::is_directory(dir, ec) ||
439 !std::filesystem::is_regular_file(dir / file_name_(phone_table::countries), ec))
443 apply_(read_directory_(dir), core::layer_mode::replace,
true);
456 [[nodiscard]]
bool load(dataset tier)
458 const std::string tier_dir{core::tier_name(tier)};
459 const auto marker = tier_dir +
"/" + std::string{file_name_(phone_table::countries)};
460 const auto root = core::find_resources(library_name_, marker);
462 for (
const auto t : all_tables_)
464 docs.at(index_(t)) = core::load_resource(tier_dir +
"/" + std::string{file_name_(t)},
465 embedded_files_(), root);
467 if (!docs.at(index_(phone_table::countries)))
return false;
468 apply_(std::move(docs), core::layer_mode::replace,
true);
483 void extend(
const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
486 if (std::filesystem::is_directory(path, ec))
488 apply_(read_directory_(path), mode,
false);
491 for (
const auto t : all_tables_)
493 if (path.filename() == file_name_(t))
495 (void)
extend(t, core::tsv_document::load(path), mode);
499 throw std::invalid_argument(
"extend: expected countries.tsv, ranges.tsv, formats.tsv or "
500 "reserved.tsv, got " + path.filename().string());
515 core::layer_mode mode = core::layer_mode::override)
518 docs.at(index_(which)).emplace(std::move(doc));
519 apply_(std::move(docs), mode,
false);
520 return _tables.at(index_(which)).layers().back();
532 core::layer_mode mode = core::layer_mode::override)
534 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
542 return _tables.at(index_(which)).layers();
552 for (
const auto& t : _tables)
554 any = any || !t.layers().empty();
555 h = core::hash_combine(h, t.data_hash());
565 static constexpr std::string_view library_name_ =
"phone-generator";
566 static constexpr std::size_t table_count_ = 4;
567 static constexpr std::array<table, table_count_> all_tables_{table::countries, table::ranges,
568 table::formats, table::reserved};
570 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
571 using tables = std::array<core::layered_table, table_count_>;
573 static constexpr std::size_t index_(table t) {
return static_cast<std::size_t
>(t); }
575 static constexpr std::string_view file_name_(table t)
579 case table::countries:
return "countries.tsv";
580 case table::ranges:
return "ranges.tsv";
581 case table::formats:
return "formats.tsv";
582 case table::reserved:
return "reserved.tsv";
587 static core::table_schema schema_(table t)
591 case table::countries:
592 return {.key = {
"region"},
593 .required = {
"code"},
594 .optional = {
"intl_prefix",
"national_prefix",
"main",
"population"}};
596 return {.key = {
"region",
"type",
"prefix",
"length"},
597 .required = {
"mask",
"weight",
"format"},
600 return {.key = {
"id"}, .required = {
"groups",
"national"}, .optional = {
"international"}};
601 case table::reserved:
602 return {.key = {
"region",
"type",
"prefix",
"length"}, .required = {
"mask",
"weight"}, .optional = {}};
607 static tables make_tables_()
609 return {core::layered_table{schema_(table::countries)}, core::layered_table{schema_(table::ranges)},
610 core::layered_table{schema_(table::formats)}, core::layered_table{schema_(table::reserved)}};
615 static constexpr std::size_t any_ = 2;
616 static constexpr double mobile_share_ = 0.7;
617 static constexpr int outside_attempts_ = 64;
622 std::uint16_t digits{0x3ff};
623 std::uint16_t count{1};
629 std::vector<mask_token> tail;
630 std::uint32_t length{0};
632 std::uint32_t format{0};
637 std::vector<std::uint8_t> groups;
638 std::size_t total{0};
639 std::string national;
640 std::string international;
645 std::vector<range_data>
ranges;
646 core::weighted_table<std::size_t> weights;
648 core::weighted_table<std::size_t> reserved_weights;
655 double population{1.0};
656 std::array<type_data, 2> types;
662 std::vector<std::string> codes;
663 std::unordered_map<std::string, std::size_t> by_region;
664 std::unordered_map<std::uint32_t, format_data>
formats;
665 std::array<core::weighted_table<std::size_t>, 3> by_population;
666 std::array<core::weighted_table<std::size_t>, 3> by_uniform;
671 core::rng _engine{core::rng::from_entropy()};
673 struct auto_probe_tag {};
675 explicit phg(auto_probe_tag ) : _tables{make_tables_()}
677 if (!
load(dataset::lite)) (void)
load(dataset::full);
683 [[nodiscard]] std::size_t pick_country_(
const phone_request& request, std::size_t want,
const core::rng& call)
const
685 if (!request.country)
687 auto cr = call.split(
"country");
688 const auto& table = request.weighted ? _model.by_population.at(want) : _model.by_uniform.at(want);
689 if (table.empty())
throw std::runtime_error(
"get_phone: no country has numbers of that type");
690 return table.pick(cr);
692 const auto it = _model.by_region.find(upper_(*request.country));
693 if (it == _model.by_region.end())
695 throw std::invalid_argument(
"get_phone: unknown country '" + *request.country +
"'");
697 if (want != any_ && _model.countries[it->second].types.at(want).ranges.empty())
699 throw std::invalid_argument(
"get_phone: " + _model.countries[it->second].region +
" has no " +
700 (want == 0 ?
"mobile" :
"fixed-line") +
" numbers");
706 [[nodiscard]]
static std::size_t pick_type_(
const phone_request& request,
const country_data& country,
707 std::size_t want,
const core::rng& call)
709 if (want != any_)
return want;
710 bool has_mobile = !country.types[0].ranges.empty();
711 bool has_fixed = !country.types[1].ranges.empty();
712 if (request.safe && has_mobile && has_fixed)
715 const bool reserved_mobile = !country.types[0].reserved.empty();
716 const bool reserved_fixed = !country.types[1].reserved.empty();
717 if (reserved_mobile != reserved_fixed)
719 has_mobile = reserved_mobile;
720 has_fixed = reserved_fixed;
723 if (!has_mobile && !has_fixed)
725 throw std::invalid_argument(
"get_phone: " + country.region +
" has no numbers");
727 auto tr = call.split(
"phone-type");
728 return has_mobile && (!has_fixed || core::bernoulli(tr, mobile_share_)) ? 0 : 1;
733 [[nodiscard]]
static std::string draw_number_(
bool outside,
const country_data& country,
const range_data& range,
736 std::string digits = draw_digits_(range, dr);
737 for (
int attempt = 0; outside && attempt < outside_attempts_; ++attempt)
739 std::string candidate = random_digits_(range.length, dr);
740 if (!country_holds_(country, candidate))
750 static std::uint32_t nearest_format_(
const type_data& by_type,
const std::string& digits)
752 std::uint32_t format = 0;
753 std::size_t best = 0;
755 for (
const auto& r : by_type.
ranges)
757 if (r.length != digits.size())
continue;
758 std::size_t common = 0;
759 while (common < r.prefix.size() && r.prefix[common] == digits[common]) ++common;
760 if (!found || common > best)
771 void apply_format_(phone& v, std::uint32_t format)
const
773 const auto it = _model.formats.find(format);
774 if (it != _model.formats.end() && it->second.total == v.national_number.size())
776 v._groups = it->second.groups;
777 v._national = it->second.national;
778 v._international = it->second.international;
781 v._groups = {
static_cast<std::uint8_t
>(v.national_number.size())};
783 v._international =
"$1";
788 static std::span<const core::embedded_file> embedded_files_()
790#ifdef DASMIG_PHONE_EMBED
791 return embedded::phonegen::files;
797 std::uint64_t draw_seed_() {
return _engine(); }
799 static std::string upper_(std::string_view s)
804 if (c >=
'a' && c <=
'z') c =
static_cast<char>(c -
'a' +
'A');
809 static std::size_t pick_uniform_(core::rng& r, std::size_t n)
811 return core::uniform_int<std::size_t>(r, 0, n - 1);
814 static constexpr std::uint16_t digit_bit_(
int d) {
return static_cast<std::uint16_t
>(1U <<
static_cast<unsigned>(d)); }
816 static constexpr bool has_digit_(std::uint16_t set,
int d) {
return (set & digit_bit_(d)) != 0U; }
818 static char draw_digit_(
const mask_token& t, core::rng& r)
820 const int n = std::popcount(
static_cast<unsigned>(t.digits));
821 int k = core::uniform_int(r, 0, n - 1);
822 for (
int d = 0; d < 10; ++d)
824 if (has_digit_(t.digits, d) && k-- == 0)
return static_cast<char>(
'0' + d);
829 static std::string draw_digits_(
const range_data& range, core::rng& r)
831 std::string out = range.prefix;
832 for (
const auto& t : range.tail)
834 for (std::uint16_t i = 0; i < t.count; ++i) out += draw_digit_(t, r);
839 static std::string random_digits_(std::size_t length, core::rng& r)
843 for (std::size_t i = 0; i < length; ++i) out += static_cast<char>(
'0' + core::uniform_int(r, 0, 9));
847 static bool range_holds_(
const range_data& range, std::string_view digits)
849 if (digits.size() != range.length || !digits.starts_with(range.prefix))
return false;
850 std::size_t pos = range.prefix.size();
851 for (
const auto& t : range.tail)
853 for (std::uint16_t i = 0; i < t.count; ++i, ++pos)
855 if (!has_digit_(t.digits, digits[pos] -
'0'))
return false;
861 static bool country_holds_(
const country_data& country, std::string_view digits)
863 return std::ranges::any_of(country.types, [&](
const type_data& t) {
864 return std::ranges::any_of(t.ranges, [&](const range_data& r) { return range_holds_(r, digits); });
868 static documents read_directory_(
const std::filesystem::path& dir)
871 for (
const auto t : all_tables_)
873 const auto file = dir / file_name_(t);
875 if (std::filesystem::is_regular_file(file, ec))
877 docs.at(index_(t)) = core::tsv_document::load(file);
885 void apply_(documents docs, core::layer_mode mode,
bool reset)
887 tables next = _tables;
888 for (
const auto t : all_tables_)
890 auto& doc = docs.at(index_(t));
891 auto& tbl = next.at(index_(t));
894 if (reset) tbl.clear();
897 (void)tbl.extend(std::move(*doc), mode, [t](
const core::tsv_row& row) { validate_(t, row); });
899 auto built = build_model_(next);
900 _tables = std::move(next);
901 _model = std::move(built);
907 static std::uint16_t parse_class_(
const core::tsv_row& row, std::string_view body)
909 std::uint16_t bits = 0;
910 for (std::size_t k = 0; k < body.size(); ++k)
912 if (body[k] <
'0' || body[k] >
'9')
throw row.error(
"mask",
"bad class '[" + std::string{body} +
"]'");
913 const bool span = k + 2 < body.size() && body[k + 1] ==
'-';
914 const char last = span ? body[k + 2] : body[k];
915 if (last < body[k])
throw row.error(
"mask",
"bad range in '[" + std::string{body} +
"]'");
916 for (
char d = body[k]; d <= last; ++d) bits |= digit_bit_(d -
'0');
919 if (bits == 0)
throw row.error(
"mask",
"empty class");
924 static std::uint16_t parse_repeat_(
const core::tsv_row& row, std::string_view body)
927 for (
const char c : body)
929 if (c <
'0' || c >
'9')
throw row.error(
"mask",
"bad repeat '{" + std::string{body} +
"}'");
930 n = n * 10 +
static_cast<unsigned>(c -
'0');
931 if (n > 40)
throw row.error(
"mask",
"repeat too large");
933 if (n == 0)
throw row.error(
"mask",
"repeat must be positive");
934 return static_cast<std::uint16_t
>(n);
937 static std::vector<mask_token> parse_mask_(
const core::tsv_row& row, std::string_view text)
939 std::vector<mask_token> out;
941 while (i < text.size())
944 const char c = text[i];
947 const auto close = text.find(
']', i);
948 if (close == std::string_view::npos)
throw row.error(
"mask",
"unclosed '['");
949 t.digits = parse_class_(row, text.substr(i + 1, close - i - 1));
952 else if (c ==
'#' || (c >=
'0' && c <=
'9'))
954 t.digits = c ==
'#' ? std::uint16_t{0x3ff} : digit_bit_(c -
'0');
959 throw row.error(
"mask", std::string{
"unexpected '"} + c +
"' in '" + std::string{text} +
"'");
961 if (i < text.size() && text[i] ==
'{')
963 const auto close = text.find(
'}', i);
964 if (close == std::string_view::npos)
throw row.error(
"mask",
"unclosed '{'");
965 t.count = parse_repeat_(row, text.substr(i + 1, close - i - 1));
973 static range_data parse_range_(
const core::tsv_row& row,
bool with_format)
976 r.prefix = std::string{row[
"prefix"]};
977 for (
const char c : r.prefix)
979 if (c <
'0' || c >
'9')
throw row.error(
"prefix",
"must be digits, got '" + r.prefix +
"'");
981 r.tail = parse_mask_(row, row[
"mask"]);
982 const auto length = row.get<std::int64_t>(
"length");
983 if (length < 1 || length > 20)
throw row.error(
"length",
"must be 1 to 20");
984 r.length =
static_cast<std::uint32_t
>(length);
985 std::size_t digits = r.prefix.size();
986 for (
const auto& t : r.tail) digits += t.count;
987 if (digits != r.length)
throw row.error(
"mask",
"prefix and mask hold " + std::to_string(digits) +
" digits, not " + std::to_string(r.length));
988 r.weight = row.get<
double>(
"weight");
989 if (!(r.weight >= 0.0))
throw row.error(
"weight",
"must not be negative");
992 const auto f = row.get<std::int64_t>(
"format");
993 if (f < 0)
throw row.error(
"format",
"must not be negative");
994 r.format =
static_cast<std::uint32_t
>(f);
999 static std::size_t type_index_(
const core::tsv_row& row)
1001 const auto t = row[
"type"];
1002 if (t ==
"mobile")
return 0;
1003 if (t ==
"fixed")
return 1;
1004 throw row.error(
"type",
"must be 'mobile' or 'fixed', got '" + std::string{t} +
"'");
1007 static void validate_(table t,
const core::tsv_row& row)
1011 case table::countries:
1013 if (row[
"region"].size() != 2)
throw row.error(
"region",
"must be an ISO 3166-1 alpha-2 code");
1014 const auto code = row[
"code"];
1015 if (code.empty() || code.size() > 4)
throw row.error(
"code",
"must be a calling code of 1 to 4 digits");
1016 for (
const char c : code)
1018 if (c <
'0' || c >
'9')
throw row.error(
"code",
"must be digits");
1020 if (row.get_or<
double>(
"population", 1.0) < 0.0)
throw row.error(
"population",
"must not be negative");
1024 (void)type_index_(row);
1025 (void)parse_range_(row,
true);
1027 case table::reserved:
1028 (void)type_index_(row);
1029 (void)parse_range_(row,
false);
1031 case table::formats:
1033 const auto id = row.get<std::int64_t>(
"id");
1034 if (
id < 0)
throw row.error(
"id",
"must not be negative");
1035 (void)parse_groups_(row);
1041 static std::vector<std::uint8_t> parse_groups_(
const core::tsv_row& row)
1043 std::vector<std::uint8_t> out;
1046 for (
const char c : std::string{row[
"groups"]} +
"-")
1048 if (c >=
'0' && c <=
'9')
1050 n = n * 10 +
static_cast<unsigned>(c -
'0');
1052 if (n > 30)
throw row.error(
"groups",
"group too long");
1054 else if (c ==
'-' && digit)
1056 if (n == 0)
throw row.error(
"groups",
"empty group");
1057 out.push_back(
static_cast<std::uint8_t
>(n));
1063 throw row.error(
"groups",
"expected lengths joined with '-', got '" + std::string{row[
"groups"]} +
"'");
1069 static void read_formats_(model& m,
const tables& tbls)
1071 for (
const auto& row : tbls.at(index_(table::
formats)).rows())
1074 f.groups = parse_groups_(row);
1075 for (
const auto g : f.groups) f.total += g;
1076 f.national = std::string{row[
"national"]};
1077 f.international = row.has(
"international") && !row[
"international"].empty()
1078 ? std::string{row[
"international"]}
1080 m.formats[
static_cast<std::uint32_t
>(row.get<std::int64_t>(
"id"))] = std::move(f);
1084 static void read_countries_(model& m,
const tables& tbls)
1086 for (
const auto& row : tbls.at(index_(table::
countries)).rows())
1089 c.region = upper_(row[
"region"]);
1090 c.code = std::string{row[
"code"]};
1091 c.population = std::max(row.get_or<
double>(
"population", 1.0), 1.0);
1092 m.by_region.emplace(c.region, m.countries.size());
1093 m.countries.push_back(std::move(c));
1097 static country_data& locate_(model& m,
const core::tsv_row& row)
1099 const auto it = m.by_region.find(upper_(row[
"region"]));
1100 if (it == m.by_region.end())
1102 throw row.error(
"region",
"no country '" + std::string{row[
"region"]} +
"' in countries.tsv");
1104 return m.countries[it->second];
1107 static void read_ranges_(model& m,
const tables& tbls)
1109 for (
const auto& row : tbls.at(index_(table::
ranges)).rows())
1111 auto& c = locate_(m, row);
1112 auto r = parse_range_(row,
true);
1113 const auto f = m.formats.find(r.format);
1114 if (f == m.formats.end())
throw row.error(
"format",
"no format " + std::to_string(r.format) +
" in formats.tsv");
1115 if (f->second.total != r.length)
1117 throw row.error(
"format",
"format " + std::to_string(r.format) +
" groups " + std::to_string(f->second.total) +
1118 " digits, the range has " + std::to_string(r.length));
1120 c.types.at(type_index_(row)).ranges.push_back(std::move(r));
1122 for (
const auto& row : tbls.at(index_(table::
reserved)).rows())
1124 locate_(m, row).types.at(type_index_(row)).reserved.push_back(parse_range_(row,
false));
1129 static void build_draw_tables_(model& m)
1131 for (std::size_t i = 0; i < m.countries.size(); ++i)
1133 auto& c = m.countries[i];
1134 std::array<bool, 2> has{};
1135 for (std::size_t t = 0; t < 2; ++t)
1137 auto& td = c.types.at(t);
1138 has.at(t) = !td.ranges.empty();
1139 for (std::size_t k = 0; k < td.ranges.size(); ++k) td.weights.add(k, td.ranges[k].weight);
1140 for (std::size_t k = 0; k < td.reserved.size(); ++k) td.reserved_weights.add(k, td.reserved[k].weight);
1141 if (has.at(t) && !(td.weights.total_weight() > 0.0))
1143 throw std::invalid_argument(
"country " + c.region +
": every range has weight 0");
1146 const std::array<bool, 3> offered{has.at(0), has.at(1), has.at(0) || has.at(1)};
1147 if (offered.at(any_)) m.codes.push_back(c.region);
1148 for (std::size_t t = 0; t < 3; ++t)
1150 if (!offered.at(t))
continue;
1151 m.by_population.at(t).add(i, c.population);
1152 m.by_uniform.at(t).add(i, 1.0);
1157 static model build_model_(
const tables& tbls)
1160 read_formats_(m, tbls);
1161 read_countries_(m, tbls);
1162 read_ranges_(m, tbls);
1163 build_draw_tables_(m);
1167 void require_data_()
const
1169 if (_model.countries.empty())
1171 throw std::runtime_error(
"No phone data loaded. Call load() first.");