Phone Generator 2.1.1
Phone numbers valid in each country's numbering plan for C++23
Loading...
Searching...
No Matches
phonegen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Diego Dasso Migotto
3
4#ifndef DASMIG_PHONEGEN_HPP
5#define DASMIG_PHONEGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <array>
10#include <bit>
11#include <cstddef>
12#include <cstdint>
13#include <filesystem>
14#include <format>
15#include <optional>
16#include <ostream>
17#include <span>
18#include <stdexcept>
19#include <string>
20#include <string_view>
21#include <system_error>
22#include <unordered_map>
23#include <utility>
24#include <vector>
25
26#ifdef DASMIG_PHONE_EMBED
27#include "phonegen_data.hpp"
28#endif
29
30/// @file phonegen.hpp
31/// @brief Phone Generator library — phone numbers valid in each country's numbering plan for C++23.
32/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
33/// @see See doc/usage.md for the narrative tutorial.
34
35DASMIG_CORE_REQUIRE(1, 1);
36
37namespace dasmig
38{
39
40/// @brief The kinds of phone number generated.
41enum class phone_type : std::uint8_t
42{
43 mobile, ///< A mobile number.
44 fixed ///< A fixed-line number.
45};
46
47/// @brief What a phone call asks for.
48///
49/// Every generation call takes one; `phone_request{}` asks for the
50/// defaults. Choices are never stored on the generator, so one call cannot
51/// change the next.
53{
54 /// @brief ISO-3166 two-letter country code (any case), or a country
55 /// drawn when absent.
56 std::optional<std::string> country{std::nullopt};
57
58 /// @brief Mobile or fixed line; drawn when absent (mobile seven times in
59 /// ten, or the only type the country has).
60 std::optional<phone_type> type{std::nullopt};
61
62 /// @brief Use numbers the regulators set aside for fiction.
63 ///
64 /// Where the country has such a range the number comes from it and
65 /// phone::reserved is true. Elsewhere the number is drawn outside the
66 /// country's allocated ranges and phone::reserved is false; no error, so
67 /// batches over random countries do not fail.
68 bool safe{false};
69
70 /// @brief Weighted selection (default): countries by population and
71 /// ranges by their weight: within a length every valid number is
72 /// equally likely, and a plan with several lengths is drawn mostly
73 /// at the length of its example number. False selects countries
74 /// and ranges uniformly.
75 bool weighted{true};
76};
77
78/// @brief Return type for phone number generation.
79///
80/// Supports implicit conversion to std::string (the international format),
81/// streaming via operator<< and JSON output via to_json().
82class phone
83{
84 public:
85 std::string country_code; ///< ISO 3166-1 alpha-2 of the region the number belongs to.
86 std::string calling_code; ///< Calling code without the plus sign ("55").
87 std::string national_number; ///< The digits after the calling code, without a national prefix.
88 phone_type type{phone_type::mobile}; ///< Mobile or fixed line.
89 bool reserved{false}; ///< True when the number comes from a range set aside for fiction.
90
91 /// @brief The number in E.164.
92 /// @return For example `+5511987654321`.
93 [[nodiscard]] std::string e164() const { return "+" + calling_code + national_number; }
94
95 /// @brief The number as written for international dialling.
96 /// @return For example `+55 11 98765-4321`.
97 [[nodiscard]] std::string international() const
98 {
99 if (_international == "-") return e164();
100 return "+" + calling_code + " " + apply_(_international);
101 }
102
103 /// @brief The number as written inside its country.
104 /// @return For example `(11) 98765-4321`; with the national prefix where
105 /// the country dials one.
106 [[nodiscard]] std::string national() const { return apply_(_national); }
107
108 /// @brief The number as an RFC 3966 URI.
109 /// @return For example `tel:+55-11-98765-4321`.
110 [[nodiscard]] std::string rfc3966() const
111 {
112 std::string out = "tel:+" + calling_code;
113 const std::string body = _international == "-" ? national_number : apply_(_international);
114 bool gap = true; // a separator is pending
115 for (const char c : body)
116 {
117 if (c >= '0' && c <= '9')
118 {
119 if (gap) out += '-';
120 gap = false;
121 out += c;
122 }
123 else
124 {
125 gap = true;
126 }
127 }
128 return out;
129 }
130
131 /// @brief Retrieve the random seed used to generate this phone number.
132 /// @return The call seed; pass it to get_phone(request, seed) with the
133 /// same request and data to replay it.
134 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
135
136 /// @brief The seed and the data this number was generated from.
137 /// @return The replay signature; `data` is phg::data_hash() at the time
138 /// of generation.
139 [[nodiscard]] const core::seed_signature& signature() const { return _signature; }
140
141 /// @brief Human-readable summary string.
142 /// @return The international format.
143 [[nodiscard]] std::string to_string() const { return international(); }
144
145 /// @brief Write this number as a JSON object.
146 /// @param w Writer positioned where a value is allowed.
147 void to_json(core::json_writer& w) const
148 {
149 w.begin_object()
150 .field("country_code", country_code)
151 .field("type", type == phone_type::mobile ? "mobile" : "fixed")
152 .field("e164", e164())
153 .field("international", international())
154 .field("national", national())
155 .field("reserved", reserved)
156 .key("signature");
157 _signature.to_json(w);
158 w.end_object();
159 }
160
161 /// @brief This number as a JSON document.
162 /// @return Compact JSON, for example
163 /// `{"country_code":"BR","type":"mobile","e164":"+55...",...}`.
164 [[nodiscard]] std::string to_json() const
165 {
166 core::json_writer w;
167 to_json(w);
168 return w.str();
169 }
170
171 /// @brief Implicit conversion to std::string.
172 /// @return Summary string.
173 operator std::string() const // NOLINT(hicpp-explicit-conversions)
174 {
175 return to_string();
176 }
177
178 /// @brief Stream the summary to an output stream.
179 /// @param os Output stream.
180 /// @param v The number to stream.
181 /// @return The stream.
182 friend std::ostream& operator<<(std::ostream& os, const phone& v)
183 {
184 os << v.to_string();
185 return os;
186 }
187
188 private:
189 // Fills a template such as "($1) $2-$3" with the digit groups.
190 [[nodiscard]] std::string apply_(std::string_view tmpl) const
191 {
192 std::string out;
193 for (std::size_t i = 0; i < tmpl.size(); ++i)
194 {
195 if (tmpl[i] == '$' && i + 1 < tmpl.size() && tmpl[i + 1] >= '1' && tmpl[i + 1] <= '9')
196 {
197 const auto g = static_cast<std::size_t>(tmpl[++i] - '1');
198 std::size_t from = 0;
199 for (std::size_t k = 0; k < g && k < _groups.size(); ++k) from += _groups[k];
200 if (g < _groups.size()) out.append(national_number, from, _groups[g]);
201 }
202 else
203 {
204 out += tmpl[i];
205 }
206 }
207 return out;
208 }
209
210 core::seed_signature _signature;
211 std::vector<std::uint8_t> _groups; // digit group lengths
212 std::string _national; // template, "$1" the first group
213 std::string _international; // template, or "-" for none
214 friend class phg;
215};
216
217/// @brief The tables of the phone data, for extending it.
218enum class phone_table : std::uint8_t
219{
220 countries, ///< `countries.tsv`: calling codes, national prefixes and populations.
221 ranges, ///< `ranges.tsv`: the allocated number ranges, by country and type.
222 formats, ///< `formats.tsv`: the digit grouping and templates the ranges refer to.
223 reserved ///< `reserved.tsv`: the ranges set aside for fiction (the `safe` option).
224};
225
226/// @brief Phone Generator: phone numbers valid in each country's numbering plan.
227///
228/// A call draws a country (by population), a type, a range of that country
229/// (by weight: even over the numbers of a length) and the remaining digits, each
230/// from a stream split from the call seed (`"country"`, `"phone-type"`,
231/// `"phone-range"`, `"phone-digits"`). Output is identical on every compiler
232/// and platform (dasmig-core).
233///
234/// Can be used as a singleton via instance() or constructed
235/// independently.
236///
237/// @par Thread safety
238/// Each instance is independent. Concurrent calls to get_phone(request, seed) on the
239/// **same** instance are safe; calls that use the instance engine, loading and
240/// extending require external synchronisation.
241class phg
242{
243 public:
244 /// @brief Default constructor — creates an empty generator with no
245 /// data.
246 phg() : _tables{make_tables_()} {}
247
248 phg(const phg&) = delete; ///< Not copyable.
249 phg& operator=(const phg&) = delete; ///< Not copyable.
250 phg(phg&&) noexcept = default; ///< Movable.
251 phg& operator=(phg&&) noexcept = default; ///< Movable.
252 ~phg() = default; ///< Destructor.
253
254 /// @brief Access the global singleton instance.
255 ///
256 /// Loads the `lite` tier (or `full` if `lite` is missing) on first
257 /// access, from compiled-in data or an auto-probed `resources/`.
258 /// @return The singleton.
259 static phg& instance()
260 {
261 static phg inst{auto_probe_tag{}};
262 return inst;
263 }
264
265 // -- Generation -------------------------------------------------------
266
267 /// @brief Generate a random phone number.
268 /// Use phone_request{} for the defaults.
269 /// @param request What to generate.
270 /// @return The phone number.
271 /// @throws std::runtime_error If no data has been loaded.
272 /// @throws std::invalid_argument If the country is unknown or has no
273 /// number of the requested type.
274 [[nodiscard]] phone get_phone(const phone_request& request)
275 {
276 return get_phone(request, draw_seed_());
277 }
278
279 /// @brief Generate a deterministic phone number.
280 /// @param request What to generate.
281 /// @param call_seed Seed; the same request, seed and data give the same
282 /// result. Does not touch the instance engine.
283 /// @return The phone number.
284 /// @throws std::runtime_error If no data has been loaded.
285 /// @throws std::invalid_argument If the country is unknown or has no
286 /// number of the requested type.
287 [[nodiscard]] phone get_phone(const phone_request& request, std::uint64_t call_seed) const
288 {
289 require_data_();
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);
295
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())];
303
304 phone v;
305 v._signature = {.seed = call_seed, .data = data_hash()};
306 v.country_code = country.region;
307 v.calling_code = country.code;
308 v.type = ti == 0 ? phone_type::mobile : phone_type::fixed;
309 v.reserved = reserved;
310 v.national_number = draw_number_(request.safe && !reserved, country, range, dr);
311 apply_format_(v, reserved ? nearest_format_(by_type, v.national_number) : range.format);
312 return v;
313 }
314
315 // -- Batch generation -------------------------------------------------
316
317 /// @brief Generate a batch of phone results from a seed drawn like
318 /// get_phone() draws its own.
319 ///
320 /// Item `i` is `get_phone(request, core::item_seed(batch_seed, options.first + i))`
321 /// for the drawn batch seed; see the seeded overload.
322 ///
323 /// @param request What every item is generated from.
324 /// @param options Count, first index, threads and uniqueness.
325 /// @return The results, in index order.
326 [[nodiscard]] std::vector<phone> get_phone_batch(const phone_request& request,
327 const core::batch_options& options)
328 {
329 return get_phone_batch(request, options, draw_seed_());
330 }
331
332 /// @brief Generate a batch of phone results, deterministically.
333 ///
334 /// Item `i` is exactly `get_phone(request, core::item_seed(batch_seed,
335 /// options.first + i))`, so an item replays on its own, a range can be
336 /// generated directly, two batches that follow each other equal one, and
337 /// the output does not depend on `options.threads`.
338 ///
339 /// @param request What every item is generated from.
340 /// @param options Count, first index, threads and uniqueness (see
341 /// core::unique_rule; the rule must be made for `phone`).
342 /// @param batch_seed Seed of the whole batch.
343 /// @return The results, in index order.
344 /// @throws std::invalid_argument If the request or options are invalid.
345 /// @throws std::runtime_error If no data has been loaded, or a unique
346 /// batch runs out of attempts.
347 [[nodiscard]] std::vector<phone> get_phone_batch(const phone_request& request,
348 const core::batch_options& options,
349 std::uint64_t batch_seed) const
350 {
351 return core::run_batch(options, batch_seed,
352 [&](std::uint64_t item_seed) { return get_phone(request, item_seed); });
353 }
354
355 /// @brief Stream a batch of phone results to a sink without holding the
356 /// whole batch.
357 ///
358 /// Generates the same items as get_phone_batch() and calls
359 /// `sink(index, item)` in index order on the calling thread; `index`
360 /// includes `options.first`.
361 ///
362 /// @tparam Sink Callable taking `(std::uint64_t, phone&)`; for example
363 /// core::jsonl_writer or core::csv_writer.
364 /// @param request What every item is generated from.
365 /// @param options Count, first index, threads and uniqueness.
366 /// @param batch_seed Seed of the whole batch.
367 /// @param sink Receives the items.
368 template <typename Sink>
369 void for_each_phone(const phone_request& request, const core::batch_options& options,
370 std::uint64_t batch_seed, Sink&& sink) const
371 {
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));
375 }
376
377 /// @brief Stream a batch of phone results with a batch seed drawn like
378 /// get_phone() draws its own.
379 /// @tparam Sink Callable taking `(std::uint64_t, phone&)`.
380 /// @param request What every item is generated from.
381 /// @param options Count, first index, threads and uniqueness.
382 /// @param sink Receives the items.
383 template <typename Sink>
384 void for_each_phone(const phone_request& request, const core::batch_options& options, Sink&& sink)
385 {
386 for_each_phone(request, options, draw_seed_(), std::forward<Sink>(sink));
387 }
388
389 // -- Data management --------------------------------------------------
390
391 /// @brief Check whether any data has been loaded.
392 /// @return True when at least one country is loaded.
393 [[nodiscard]] bool has_data() const { return !_model.countries.empty(); }
394
395 /// @brief Return the number of countries that have numbers.
396 /// @return The count; it is `countries().size()`.
397 [[nodiscard]] std::size_t country_count() const { return _model.codes.size(); }
398
399 /// @brief The countries a number can be generated for.
400 /// @return Their ISO-3166 two-letter codes (upper case), in the order of countries.tsv; the
401 /// list a request's `country` is chosen from.
402 [[nodiscard]] const std::vector<std::string>& countries() const { return _model.codes; }
403
404 /// @brief Whether numbers can be generated for a country.
405 /// @param country ISO-3166 two-letter code (any case).
406 /// @return True when the country is in countries() (it is loaded and has at least one range).
407 [[nodiscard]] bool has_country(std::string_view country) const
408 {
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();
412 });
413 }
414
415 /// @brief Whether a country has a range set aside for fiction.
416 /// @param country ISO-3166 two-letter code (any case).
417 /// @param type Mobile or fixed line.
418 /// @return True when `safe` numbers of that type are reserved ones.
419 [[nodiscard]] bool has_reserved(std::string_view country, phone_type type) const
420 {
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();
424 }
425
426 /// @brief Load data from a resource directory, replacing all data and
427 /// layers loaded before.
428 ///
429 /// The directory must contain `countries.tsv`; the other tables are read
430 /// when present. A path without `countries.tsv` is ignored.
431 ///
432 /// @param dir Directory such as `resources/lite`.
433 /// @throws core::tsv_error If a file is malformed; the data is then
434 /// unchanged.
435 void load(const std::filesystem::path& dir)
436 {
437 std::error_code ec;
438 if (!std::filesystem::is_directory(dir, ec) ||
439 !std::filesystem::is_regular_file(dir / file_name_(phone_table::countries), ec))
440 {
441 return;
442 }
443 apply_(read_directory_(dir), core::layer_mode::replace, true);
444 }
445
446 /// @brief Load a dataset tier, replacing all data and layers loaded
447 /// before.
448 ///
449 /// Uses the compiled-in copy when the library is built with
450 /// `DASMIG_PHONE_EMBED`, otherwise searches `resources/`, `../resources`,
451 /// `phone-generator/resources` and `../phone-generator/resources`.
452 ///
453 /// @param tier The tier.
454 /// @return False when the tier was not found.
455 /// @throws core::tsv_error If a file is malformed.
456 [[nodiscard]] bool load(dataset tier)
457 {
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);
461 documents docs;
462 for (const auto t : all_tables_)
463 {
464 docs.at(index_(t)) = core::load_resource(tier_dir + "/" + std::string{file_name_(t)},
465 embedded_files_(), root);
466 }
467 if (!docs.at(index_(phone_table::countries))) return false;
468 apply_(std::move(docs), core::layer_mode::replace, true);
469 return true;
470 }
471
472 /// @brief Add data layers from a directory or a file.
473 ///
474 /// A directory may hold any of the table files (see phone_table); all
475 /// are applied together or, if any fails, none. A file must have one of
476 /// those names.
477 ///
478 /// @param path Directory or file.
479 /// @param mode How the layers combine with the data loaded before.
480 /// @throws core::tsv_error If a file cannot be read or a row is
481 /// invalid; the data is then unchanged.
482 /// @throws std::invalid_argument If @p path is a file with another name.
483 void extend(const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
484 {
485 std::error_code ec;
486 if (std::filesystem::is_directory(path, ec))
487 {
488 apply_(read_directory_(path), mode, false);
489 return;
490 }
491 for (const auto t : all_tables_)
492 {
493 if (path.filename() == file_name_(t))
494 {
495 (void)extend(t, core::tsv_document::load(path), mode);
496 return;
497 }
498 }
499 throw std::invalid_argument("extend: expected countries.tsv, ranges.tsv, formats.tsv or "
500 "reserved.tsv, got " + path.filename().string());
501 }
502
503 /// @brief Add a data layer to one table.
504 ///
505 /// Rows are keyed by the table's key columns (see doc/data.md). In the
506 /// default override mode a row replaces the loaded row with the same key
507 /// and new keys are added; a row with `disabled` set to 1 removes its key.
508 ///
509 /// @param which The table.
510 /// @param doc TSV with that table's columns.
511 /// @param mode How the layer combines with the data loaded before.
512 /// @return What the layer did (rows added, replaced, removed, warnings).
513 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
514 const core::layer_info& extend(phone_table which, core::tsv_document doc,
515 core::layer_mode mode = core::layer_mode::override)
516 {
517 documents docs;
518 docs.at(index_(which)).emplace(std::move(doc));
519 apply_(std::move(docs), mode, false);
520 return _tables.at(index_(which)).layers().back();
521 }
522
523 /// @brief Add a data layer to one table from TSV text (for example
524 /// from a WebAssembly host).
525 /// @param which The table.
526 /// @param text TSV with that table's columns.
527 /// @param source Name used in errors.
528 /// @param mode How the layer combines with the data loaded before.
529 /// @return What the layer did.
530 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
531 const core::layer_info& extend_from_string(phone_table which, std::string_view text, std::string source,
532 core::layer_mode mode = core::layer_mode::override)
533 {
534 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
535 }
536
537 /// @brief The layers in effect for one table, in order.
538 /// @param which The table.
539 /// @return The layer records.
540 [[nodiscard]] const std::vector<core::layer_info>& layers(phone_table which) const
541 {
542 return _tables.at(index_(which)).layers();
543 }
544
545 /// @brief Hash of the loaded data (all tables), recorded in each
546 /// result's signature().
547 /// @return The hash; 0 when nothing is loaded.
548 [[nodiscard]] std::uint64_t data_hash() const
549 {
550 std::uint64_t h = 0;
551 bool any = false;
552 for (const auto& t : _tables)
553 {
554 any = any || !t.layers().empty();
555 h = core::hash_combine(h, t.data_hash());
556 }
557 return any ? h : 0;
558 }
559
560 private:
561 using table = phone_table;
562
563 // -- Tables -------------------------------------------------------------
564
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};
569
570 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
571 using tables = std::array<core::layered_table, table_count_>;
572
573 static constexpr std::size_t index_(table t) { return static_cast<std::size_t>(t); }
574
575 static constexpr std::string_view file_name_(table t)
576 {
577 switch (t)
578 {
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";
583 }
584 return "";
585 }
586
587 static core::table_schema schema_(table t)
588 {
589 switch (t)
590 {
591 case table::countries:
592 return {.key = {"region"},
593 .required = {"code"},
594 .optional = {"intl_prefix", "national_prefix", "main", "population"}};
595 case table::ranges:
596 return {.key = {"region", "type", "prefix", "length"},
597 .required = {"mask", "weight", "format"},
598 .optional = {}};
599 case table::formats:
600 return {.key = {"id"}, .required = {"groups", "national"}, .optional = {"international"}};
601 case table::reserved:
602 return {.key = {"region", "type", "prefix", "length"}, .required = {"mask", "weight"}, .optional = {}};
603 }
604 return {};
605 }
606
607 static tables make_tables_()
608 {
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)}};
611 }
612
613 // -- Parsed rows --------------------------------------------------------
614
615 static constexpr std::size_t any_ = 2; // index of "either type" in the per-type arrays
616 static constexpr double mobile_share_ = 0.7;
617 static constexpr int outside_attempts_ = 64;
618
619 // `count` digits, each one of the set bits of `digits` (bit d = digit d).
620 struct mask_token
621 {
622 std::uint16_t digits{0x3ff};
623 std::uint16_t count{1};
624 };
625
626 struct range_data
627 {
628 std::string prefix;
629 std::vector<mask_token> tail;
630 std::uint32_t length{0};
631 double weight{0.0};
632 std::uint32_t format{0};
633 };
634
635 struct format_data
636 {
637 std::vector<std::uint8_t> groups;
638 std::size_t total{0};
639 std::string national;
640 std::string international;
641 };
642
643 struct type_data
644 {
645 std::vector<range_data> ranges;
646 core::weighted_table<std::size_t> weights; // range index, by weight
647 std::vector<range_data> reserved;
648 core::weighted_table<std::size_t> reserved_weights;
649 };
650
651 struct country_data
652 {
653 std::string region;
654 std::string code;
655 double population{1.0};
656 std::array<type_data, 2> types;
657 };
658
659 struct model
660 {
661 std::vector<country_data> countries;
662 std::vector<std::string> codes; // the countries with numbers, in table order
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; // mobile, fixed, either
666 std::array<core::weighted_table<std::size_t>, 3> by_uniform;
667 };
668
669 model _model;
670 tables _tables;
671 core::rng _engine{core::rng::from_entropy()};
672
673 struct auto_probe_tag {};
674
675 explicit phg(auto_probe_tag /*tag*/) : _tables{make_tables_()}
676 {
677 if (!load(dataset::lite)) (void)load(dataset::full);
678 }
679
680 // -- Steps of get_phone -------------------------------------------------
681
682 // The country: the requested one, or a draw among those with numbers of the type.
683 [[nodiscard]] std::size_t pick_country_(const phone_request& request, std::size_t want, const core::rng& call) const
684 {
685 if (!request.country)
686 {
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);
691 }
692 const auto it = _model.by_region.find(upper_(*request.country));
693 if (it == _model.by_region.end())
694 {
695 throw std::invalid_argument("get_phone: unknown country '" + *request.country + "'");
696 }
697 if (want != any_ && _model.countries[it->second].types.at(want).ranges.empty())
698 {
699 throw std::invalid_argument("get_phone: " + _model.countries[it->second].region + " has no " +
700 (want == 0 ? "mobile" : "fixed-line") + " numbers");
701 }
702 return it->second;
703 }
704
705 // The type: the requested one, or mobile seven times in ten when the country has both.
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)
708 {
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)
713 {
714 // Prefer the type the regulator has a range for.
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)
718 {
719 has_mobile = reserved_mobile;
720 has_fixed = reserved_fixed;
721 }
722 }
723 if (!has_mobile && !has_fixed)
724 {
725 throw std::invalid_argument("get_phone: " + country.region + " has no numbers");
726 }
727 auto tr = call.split("phone-type");
728 return has_mobile && (!has_fixed || core::bernoulli(tr, mobile_share_)) ? 0 : 1;
729 }
730
731 // The digits of a range; with @p outside (safe, no reserved range) a number no allocated
732 // range of the country holds, when one turns up in a few tries.
733 [[nodiscard]] static std::string draw_number_(bool outside, const country_data& country, const range_data& range,
734 core::rng& dr)
735 {
736 std::string digits = draw_digits_(range, dr);
737 for (int attempt = 0; outside && attempt < outside_attempts_; ++attempt)
738 {
739 std::string candidate = random_digits_(range.length, dr);
740 if (!country_holds_(country, candidate))
741 {
742 return candidate;
743 }
744 }
745 return digits;
746 }
747
748 // A reserved range has no format of its own and need not be allocated: the format of the
749 // allocated range of the same length that shares the longest prefix with the number.
750 static std::uint32_t nearest_format_(const type_data& by_type, const std::string& digits)
751 {
752 std::uint32_t format = 0;
753 std::size_t best = 0;
754 bool found = false;
755 for (const auto& r : by_type.ranges)
756 {
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)
761 {
762 best = common;
763 format = r.format;
764 found = true;
765 }
766 }
767 return format;
768 }
769
770 // Writes the format's groups and templates into the number, or one group of every digit.
771 void apply_format_(phone& v, std::uint32_t format) const
772 {
773 const auto it = _model.formats.find(format);
774 if (it != _model.formats.end() && it->second.total == v.national_number.size())
775 {
776 v._groups = it->second.groups;
777 v._national = it->second.national;
778 v._international = it->second.international;
779 return;
780 }
781 v._groups = {static_cast<std::uint8_t>(v.national_number.size())};
782 v._national = "$1";
783 v._international = "$1";
784 }
785
786 // -- Helpers ----------------------------------------------------------
787
788 static std::span<const core::embedded_file> embedded_files_()
789 {
790#ifdef DASMIG_PHONE_EMBED
791 return embedded::phonegen::files;
792#else
793 return {};
794#endif
795 }
796
797 std::uint64_t draw_seed_() { return _engine(); }
798
799 static std::string upper_(std::string_view s)
800 {
801 std::string out{s};
802 for (auto& c : out)
803 {
804 if (c >= 'a' && c <= 'z') c = static_cast<char>(c - 'a' + 'A');
805 }
806 return out;
807 }
808
809 static std::size_t pick_uniform_(core::rng& r, std::size_t n)
810 {
811 return core::uniform_int<std::size_t>(r, 0, n - 1);
812 }
813
814 static constexpr std::uint16_t digit_bit_(int d) { return static_cast<std::uint16_t>(1U << static_cast<unsigned>(d)); }
815
816 static constexpr bool has_digit_(std::uint16_t set, int d) { return (set & digit_bit_(d)) != 0U; }
817
818 static char draw_digit_(const mask_token& t, core::rng& r)
819 {
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)
823 {
824 if (has_digit_(t.digits, d) && k-- == 0) return static_cast<char>('0' + d);
825 }
826 return '0';
827 }
828
829 static std::string draw_digits_(const range_data& range, core::rng& r)
830 {
831 std::string out = range.prefix;
832 for (const auto& t : range.tail)
833 {
834 for (std::uint16_t i = 0; i < t.count; ++i) out += draw_digit_(t, r);
835 }
836 return out;
837 }
838
839 static std::string random_digits_(std::size_t length, core::rng& r)
840 {
841 std::string out;
842 out.reserve(length);
843 for (std::size_t i = 0; i < length; ++i) out += static_cast<char>('0' + core::uniform_int(r, 0, 9));
844 return out;
845 }
846
847 static bool range_holds_(const range_data& range, std::string_view digits)
848 {
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)
852 {
853 for (std::uint16_t i = 0; i < t.count; ++i, ++pos)
854 {
855 if (!has_digit_(t.digits, digits[pos] - '0')) return false;
856 }
857 }
858 return true;
859 }
860
861 static bool country_holds_(const country_data& country, std::string_view digits)
862 {
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); });
865 });
866 }
867
868 static documents read_directory_(const std::filesystem::path& dir)
869 {
870 documents docs;
871 for (const auto t : all_tables_)
872 {
873 const auto file = dir / file_name_(t);
874 std::error_code ec;
875 if (std::filesystem::is_regular_file(file, ec))
876 {
877 docs.at(index_(t)) = core::tsv_document::load(file);
878 }
879 }
880 return docs;
881 }
882
883 // Apply layers to copies of the tables, then commit: all or nothing.
884 // With @p reset (load), tables without a document are emptied.
885 void apply_(documents docs, core::layer_mode mode, bool reset)
886 {
887 tables next = _tables;
888 for (const auto t : all_tables_)
889 {
890 auto& doc = docs.at(index_(t));
891 auto& tbl = next.at(index_(t));
892 if (!doc)
893 {
894 if (reset) tbl.clear();
895 continue;
896 }
897 (void)tbl.extend(std::move(*doc), mode, [t](const core::tsv_row& row) { validate_(t, row); });
898 }
899 auto built = build_model_(next);
900 _tables = std::move(next);
901 _model = std::move(built);
902 }
903
904 // -- Row checks and the model -------------------------------------------
905
906 // The digits of a `[...]` class (its @p body, between the brackets), as a set.
907 static std::uint16_t parse_class_(const core::tsv_row& row, std::string_view body)
908 {
909 std::uint16_t bits = 0;
910 for (std::size_t k = 0; k < body.size(); ++k)
911 {
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');
917 if (span) k += 2;
918 }
919 if (bits == 0) throw row.error("mask", "empty class");
920 return bits;
921 }
922
923 // The count of a `{n}` repeat (its @p body, between the braces).
924 static std::uint16_t parse_repeat_(const core::tsv_row& row, std::string_view body)
925 {
926 unsigned n = 0;
927 for (const char c : body)
928 {
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");
932 }
933 if (n == 0) throw row.error("mask", "repeat must be positive");
934 return static_cast<std::uint16_t>(n);
935 }
936
937 static std::vector<mask_token> parse_mask_(const core::tsv_row& row, std::string_view text)
938 {
939 std::vector<mask_token> out;
940 std::size_t i = 0;
941 while (i < text.size())
942 {
943 mask_token t;
944 const char c = text[i];
945 if (c == '[')
946 {
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));
950 i = close + 1;
951 }
952 else if (c == '#' || (c >= '0' && c <= '9'))
953 {
954 t.digits = c == '#' ? std::uint16_t{0x3ff} : digit_bit_(c - '0');
955 ++i;
956 }
957 else
958 {
959 throw row.error("mask", std::string{"unexpected '"} + c + "' in '" + std::string{text} + "'");
960 }
961 if (i < text.size() && text[i] == '{')
962 {
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));
966 i = close + 1;
967 }
968 out.push_back(t);
969 }
970 return out;
971 }
972
973 static range_data parse_range_(const core::tsv_row& row, bool with_format)
974 {
975 range_data r;
976 r.prefix = std::string{row["prefix"]};
977 for (const char c : r.prefix)
978 {
979 if (c < '0' || c > '9') throw row.error("prefix", "must be digits, got '" + r.prefix + "'");
980 }
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");
990 if (with_format)
991 {
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);
995 }
996 return r;
997 }
998
999 static std::size_t type_index_(const core::tsv_row& row)
1000 {
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} + "'");
1005 }
1006
1007 static void validate_(table t, const core::tsv_row& row)
1008 {
1009 switch (t)
1010 {
1011 case table::countries:
1012 {
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)
1017 {
1018 if (c < '0' || c > '9') throw row.error("code", "must be digits");
1019 }
1020 if (row.get_or<double>("population", 1.0) < 0.0) throw row.error("population", "must not be negative");
1021 break;
1022 }
1023 case table::ranges:
1024 (void)type_index_(row);
1025 (void)parse_range_(row, true);
1026 break;
1027 case table::reserved:
1028 (void)type_index_(row);
1029 (void)parse_range_(row, false);
1030 break;
1031 case table::formats:
1032 {
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);
1036 break;
1037 }
1038 }
1039 }
1040
1041 static std::vector<std::uint8_t> parse_groups_(const core::tsv_row& row)
1042 {
1043 std::vector<std::uint8_t> out;
1044 unsigned n = 0;
1045 bool digit = false;
1046 for (const char c : std::string{row["groups"]} + "-")
1047 {
1048 if (c >= '0' && c <= '9')
1049 {
1050 n = n * 10 + static_cast<unsigned>(c - '0');
1051 digit = true;
1052 if (n > 30) throw row.error("groups", "group too long");
1053 }
1054 else if (c == '-' && digit)
1055 {
1056 if (n == 0) throw row.error("groups", "empty group");
1057 out.push_back(static_cast<std::uint8_t>(n));
1058 n = 0;
1059 digit = false;
1060 }
1061 else
1062 {
1063 throw row.error("groups", "expected lengths joined with '-', got '" + std::string{row["groups"]} + "'");
1064 }
1065 }
1066 return out;
1067 }
1068
1069 static void read_formats_(model& m, const tables& tbls)
1070 {
1071 for (const auto& row : tbls.at(index_(table::formats)).rows())
1072 {
1073 format_data f;
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"]}
1079 : f.national;
1080 m.formats[static_cast<std::uint32_t>(row.get<std::int64_t>("id"))] = std::move(f);
1081 }
1082 }
1083
1084 static void read_countries_(model& m, const tables& tbls)
1085 {
1086 for (const auto& row : tbls.at(index_(table::countries)).rows())
1087 {
1088 country_data c;
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));
1094 }
1095 }
1096
1097 static country_data& locate_(model& m, const core::tsv_row& row)
1098 {
1099 const auto it = m.by_region.find(upper_(row["region"]));
1100 if (it == m.by_region.end())
1101 {
1102 throw row.error("region", "no country '" + std::string{row["region"]} + "' in countries.tsv");
1103 }
1104 return m.countries[it->second];
1105 }
1106
1107 static void read_ranges_(model& m, const tables& tbls)
1108 {
1109 for (const auto& row : tbls.at(index_(table::ranges)).rows())
1110 {
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)
1116 {
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));
1119 }
1120 c.types.at(type_index_(row)).ranges.push_back(std::move(r));
1121 }
1122 for (const auto& row : tbls.at(index_(table::reserved)).rows())
1123 {
1124 locate_(m, row).types.at(type_index_(row)).reserved.push_back(parse_range_(row, false));
1125 }
1126 }
1127
1128 // The weighted tables of the draws; only countries with numbers of the type enter them.
1129 static void build_draw_tables_(model& m)
1130 {
1131 for (std::size_t i = 0; i < m.countries.size(); ++i)
1132 {
1133 auto& c = m.countries[i];
1134 std::array<bool, 2> has{};
1135 for (std::size_t t = 0; t < 2; ++t)
1136 {
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))
1142 {
1143 throw std::invalid_argument("country " + c.region + ": every range has weight 0");
1144 }
1145 }
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)
1149 {
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);
1153 }
1154 }
1155 }
1156
1157 static model build_model_(const tables& tbls)
1158 {
1159 model m;
1160 read_formats_(m, tbls);
1161 read_countries_(m, tbls);
1162 read_ranges_(m, tbls);
1163 build_draw_tables_(m);
1164 return m;
1165 }
1166
1167 void require_data_() const
1168 {
1169 if (_model.countries.empty())
1170 {
1171 throw std::runtime_error("No phone data loaded. Call load() first.");
1172 }
1173 }
1174};
1175
1176} // namespace dasmig
1177
1178#endif // DASMIG_PHONEGEN_HPP
Phone Generator: phone numbers valid in each country's numbering plan.
Definition phonegen.hpp:242
void load(const std::filesystem::path &dir)
Load data from a resource directory, replacing all data and layers loaded before.
Definition phonegen.hpp:435
phone get_phone(const phone_request &request)
Generate a random phone number. Use phone_request{} for the defaults.
Definition phonegen.hpp:274
void for_each_phone(const phone_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of phone results with a batch seed drawn like get_phone() draws its own.
Definition phonegen.hpp:384
phg(const phg &)=delete
Not copyable.
const std::vector< std::string > & countries() const
The countries a number can be generated for.
Definition phonegen.hpp:402
void for_each_phone(const phone_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of phone results to a sink without holding the whole batch.
Definition phonegen.hpp:369
bool load(dataset tier)
Load a dataset tier, replacing all data and layers loaded before.
Definition phonegen.hpp:456
const core::layer_info & extend_from_string(phone_table which, std::string_view text, std::string source, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table from TSV text (for example from a WebAssembly host).
Definition phonegen.hpp:531
bool has_country(std::string_view country) const
Whether numbers can be generated for a country.
Definition phonegen.hpp:407
phone get_phone(const phone_request &request, std::uint64_t call_seed) const
Generate a deterministic phone number.
Definition phonegen.hpp:287
const core::layer_info & extend(phone_table which, core::tsv_document doc, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table.
Definition phonegen.hpp:514
phg(phg &&) noexcept=default
Movable.
const std::vector< core::layer_info > & layers(phone_table which) const
The layers in effect for one table, in order.
Definition phonegen.hpp:540
static phg & instance()
Access the global singleton instance.
Definition phonegen.hpp:259
phg()
Default constructor — creates an empty generator with no data.
Definition phonegen.hpp:246
bool has_reserved(std::string_view country, phone_type type) const
Whether a country has a range set aside for fiction.
Definition phonegen.hpp:419
phg & operator=(const phg &)=delete
Not copyable.
std::size_t country_count() const
Return the number of countries that have numbers.
Definition phonegen.hpp:397
std::vector< phone > get_phone_batch(const phone_request &request, const core::batch_options &options)
Generate a batch of phone results from a seed drawn like get_phone() draws its own.
Definition phonegen.hpp:326
std::uint64_t data_hash() const
Hash of the loaded data (all tables), recorded in each result's signature().
Definition phonegen.hpp:548
bool has_data() const
Check whether any data has been loaded.
Definition phonegen.hpp:393
std::vector< phone > get_phone_batch(const phone_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of phone results, deterministically.
Definition phonegen.hpp:347
void extend(const std::filesystem::path &path, core::layer_mode mode=core::layer_mode::override)
Add data layers from a directory or a file.
Definition phonegen.hpp:483
Return type for phone number generation.
Definition phonegen.hpp:83
std::string calling_code
Calling code without the plus sign ("55").
Definition phonegen.hpp:86
std::string to_json() const
This number as a JSON document.
Definition phonegen.hpp:164
bool reserved
True when the number comes from a range set aside for fiction.
Definition phonegen.hpp:89
friend std::ostream & operator<<(std::ostream &os, const phone &v)
Stream the summary to an output stream.
Definition phonegen.hpp:182
phone_type type
Mobile or fixed line.
Definition phonegen.hpp:88
const core::seed_signature & signature() const
The seed and the data this number was generated from.
Definition phonegen.hpp:139
std::string international() const
The number as written for international dialling.
Definition phonegen.hpp:97
std::string national() const
The number as written inside its country.
Definition phonegen.hpp:106
std::string rfc3966() const
The number as an RFC 3966 URI.
Definition phonegen.hpp:110
std::string national_number
The digits after the calling code, without a national prefix.
Definition phonegen.hpp:87
std::string country_code
ISO 3166-1 alpha-2 of the region the number belongs to.
Definition phonegen.hpp:85
std::uint64_t seed() const
Retrieve the random seed used to generate this phone number.
Definition phonegen.hpp:134
std::string e164() const
The number in E.164.
Definition phonegen.hpp:93
std::string to_string() const
Human-readable summary string.
Definition phonegen.hpp:143
void to_json(core::json_writer &w) const
Write this number as a JSON object.
Definition phonegen.hpp:147
phone_type
The kinds of phone number generated.
Definition phonegen.hpp:42
@ mobile
A mobile number.
@ fixed
A fixed-line number.
phone_table
The tables of the phone data, for extending it.
Definition phonegen.hpp:219
@ formats
formats.tsv: the digit grouping and templates the ranges refer to.
@ countries
countries.tsv: calling codes, national prefixes and populations.
@ reserved
reserved.tsv: the ranges set aside for fiction (the safe option).
@ ranges
ranges.tsv: the allocated number ranges, by country and type.
What a phone call asks for.
Definition phonegen.hpp:53
std::optional< std::string > country
ISO-3166 two-letter country code (any case), or a country drawn when absent.
Definition phonegen.hpp:56
bool weighted
Weighted selection (default): countries by population and ranges by their weight: within a length eve...
Definition phonegen.hpp:75
bool safe
Use numbers the regulators set aside for fiction.
Definition phonegen.hpp:68
std::optional< phone_type > type
Mobile or fixed line; drawn when absent (mobile seven times in ten, or the only type the country has)...
Definition phonegen.hpp:60