Country Generator 3.3.2
Procedural country generation for C++23
Loading...
Searching...
No Matches
countrygen.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_COUNTRYGEN_HPP
5#define DASMIG_COUNTRYGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <array>
10#include <charconv>
11#include <chrono>
12#include <cmath>
13#include <cstddef>
14#include <cstdint>
15#include <cstdlib>
16#include <filesystem>
17#include <map>
18#include <optional>
19#include <ostream>
20#include <set>
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_COUNTRY_EMBED
31#include "countrygen_data.hpp"
32#endif
33#ifdef DASMIG_COUNTRY_EMBED_LOCALE
34#include "countrygen_locale_data.hpp"
35#endif
36
37/// @file countrygen.hpp
38/// @brief Country generator library — procedural country generation for C++23.
39/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
40/// @see See doc/usage.md for the narrative tutorial.
41
42DASMIG_CORE_REQUIRE(1, 4);
43
44namespace dasmig
45{
46
47/// @brief One language of a country and the share of its population that speaks it.
49{
50 std::string name; ///< English name of the language (for example "Portuguese").
51 double percent{0.0}; ///< Percent of the population that speaks it, 0 to 100.
52};
53
54/// @brief Return type for country generation, holding all data fields.
55///
56/// Supports implicit conversion to std::string (returns the common name),
57/// streaming via operator<< and JSON output via to_json().
59{
60 public:
61 // -- Identification ---------------------------------------------------
62
63 /// @brief ISO 3166-1 alpha-2 code (e.g. "BR").
64 std::string cca2;
65
66 /// @brief ISO 3166-1 alpha-3 code (e.g. "BRA").
67 std::string cca3;
68
69 /// @brief ISO 3166-1 numeric code (e.g. "076").
70 std::string ccn3;
71
72 /// @brief Common English name (e.g. "Brazil").
73 std::string name_common;
74
75 /// @brief Official English name (e.g. "Federative Republic of Brazil").
76 std::string name_official;
77
78 /// @brief Capital city (semicolon-separated if multiple).
79 std::string capital;
80
81 // -- Geography --------------------------------------------------------
82
83 /// @brief UN geoscheme region (e.g. "Americas").
84 std::string region;
85
86 /// @brief UN geoscheme subregion (e.g. "South America").
87 std::string subregion;
88
89 /// @brief Continent (semicolon-separated if multiple).
90 std::string continent;
91
92 /// @brief WGS84 latitude in decimal degrees.
93 double latitude{0.0};
94
95 /// @brief WGS84 longitude in decimal degrees.
96 double longitude{0.0};
97
98 /// @brief Total land area in km².
99 std::uint64_t area{0};
100
101 /// @brief Total population.
102 std::uint64_t population{0};
103
104 /// @brief Whether the country is landlocked.
105 bool landlocked{false};
106
107 // -- Political --------------------------------------------------------
108
109 /// @brief Whether the country is an independent sovereign state.
110 bool independent{false};
111
112 /// @brief Whether the country is a UN member.
113 bool un_member{false};
114
115 // -- Culture ----------------------------------------------------------
116
117 /// @brief Semicolon-separated language names (e.g. "Portuguese").
118 std::string languages;
119
120 /// @brief Semicolon-separated `Name:percent` pairs, largest first, for the
121 /// languages at least 1% of the population speaks (for example
122 /// "Portuguese:91;English:8"); empty when the data has none.
123 ///
124 /// Each percent is the share of people who speak that language, so they
125 /// overlap and can sum above 100. Use language_distribution() to read them.
126 std::string language_shares;
127
128 /// @brief Primary ISO 4217 currency code (e.g. "BRL").
129 std::string currency_code;
130
131 /// @brief Primary currency name (e.g. "Brazilian real").
132 std::string currency_name;
133
134 /// @brief Primary currency symbol (e.g. "R$").
135 std::string currency_symbol;
136
137 // -- Borders & time ---------------------------------------------------
138
139 /// @brief Semicolon-separated ISO alpha-3 border codes.
140 std::string borders;
141
142 /// @brief Semicolon-separated UTC offset strings.
143 std::string timezones;
144
145 /// @brief Driving side: "right" or "left".
146 std::string driving_side;
147
148 // -- Telecom & web ----------------------------------------------------
149
150 /// @brief Country-code top-level domain (e.g. ".br").
151 std::string tld;
152
153 /// @brief IDD root (e.g. "+5").
154 std::string idd_root;
155
156 /// @brief IDD primary suffix (e.g. "5").
157 std::string idd_suffix;
158
159 // -- Demonyms & misc --------------------------------------------------
160
161 /// @brief English male demonym (e.g. "Brazilian").
162 std::string demonym_m;
163
164 /// @brief English female demonym (e.g. "Brazilian").
165 std::string demonym_f;
166
167 /// @brief Unicode flag emoji.
168 std::string flag_emoji;
169
170 /// @brief World Bank income level (e.g. "Upper middle income").
171 std::string income_level;
172
173 /// @brief Start of the week: "monday", "sunday", or "saturday".
174 std::string start_of_week;
175
176 // -- Methods ----------------------------------------------------------
177
178 /// @brief The language names of `languages` as a list.
179 /// @return The official languages in the data's order; empty when none.
180 [[nodiscard]] std::vector<std::string> language_list() const
181 {
182 std::vector<std::string> out;
183 std::string_view text{languages};
184 while (!text.empty())
185 {
186 const auto pos = text.find(';');
187 out.emplace_back(text.substr(0, pos));
188 text = pos == std::string_view::npos ? std::string_view{} : text.substr(pos + 1);
189 }
190 return out;
191 }
192
193 /// @brief The languages and the share of the population that speaks each.
194 /// @return `language_shares` parsed, largest first; empty when the data has none.
195 [[nodiscard]] std::vector<language_share> language_distribution() const
196 {
197 return parse_shares_(language_shares).value_or(std::vector<language_share>{});
198 }
199
200 /// @brief The language most of the population speaks.
201 /// @return The largest share's name, else the first of `languages`, else empty.
202 [[nodiscard]] std::string primary_language() const
203 {
204 const auto shares = language_distribution();
205 if (!shares.empty()) return shares.front().name;
206 auto list = language_list();
207 return list.empty() ? std::string{} : std::move(list.front());
208 }
209
210 /// @brief The English demonym for a person of this country.
211 /// @param bio_sex The person's sex.
212 /// @return `demonym_m` or `demonym_f`, falling back to the other when empty.
213 [[nodiscard]] const std::string& demonym(sex bio_sex) const
214 {
215 const bool male = bio_sex == sex::male;
216 const auto& wanted = male ? demonym_m : demonym_f;
217 const auto& other = male ? demonym_f : demonym_m;
218 return wanted.empty() ? other : wanted;
219 }
220
221 /// @brief Retrieve the random seed used to generate this country.
222 /// @return The per-call seed for replay (0 for find_country()).
223 /// @see cntg::get_country(const country_request&, std::uint64_t) const
224 [[nodiscard]] std::uint64_t seed() const
225 {
226 return _signature.seed;
227 }
228
229 /// @brief The seed and the data this country was drawn from.
230 /// @return The replay signature; `data` is cntg::data_hash() at the
231 /// time of generation.
232 [[nodiscard]] const core::seed_signature& signature() const
233 {
234 return _signature;
235 }
236
237 /// @brief Write this country as a JSON object.
238 ///
239 /// Semicolon-separated fields (capital, continent, languages, borders,
240 /// timezones) are written as arrays.
241 ///
242 /// @param w Writer positioned where a value is allowed.
243 void to_json(core::json_writer& w) const
244 {
245 auto list = [&w](std::string_view name, std::string_view text) {
246 w.key(name).begin_array();
247 while (!text.empty())
248 {
249 const auto pos = text.find(';');
250 w.value(text.substr(0, pos));
251 text = pos == std::string_view::npos ? std::string_view{} : text.substr(pos + 1);
252 }
253 w.end_array();
254 };
255 w.begin_object()
256 .field("cca2", cca2)
257 .field("cca3", cca3)
258 .field("ccn3", ccn3)
259 .field("name_common", name_common)
260 .field("name_official", name_official);
261 list("capital", capital);
262 w.field("region", region).field("subregion", subregion);
263 list("continent", continent);
264 w.field("latitude", latitude)
265 .field("longitude", longitude)
266 .field("area", area)
267 .field("population", population)
268 .field("landlocked", landlocked)
269 .field("independent", independent)
270 .field("un_member", un_member);
271 list("languages", languages);
272 w.key("language_shares").begin_array();
273 (void)for_each_share_(language_shares, [&w](std::string_view name, double percent) {
274 w.begin_object().field("name", name).field("percent", percent).end_object();
275 });
276 w.end_array();
277 w.field("currency_code", currency_code)
278 .field("currency_name", currency_name)
279 .field("currency_symbol", currency_symbol);
280 list("borders", borders);
281 list("timezones", timezones);
282 w.field("driving_side", driving_side)
283 .field("tld", tld)
284 .field("idd_root", idd_root)
285 .field("idd_suffix", idd_suffix)
286 .field("demonym_m", demonym_m)
287 .field("demonym_f", demonym_f)
288 .field("flag_emoji", flag_emoji)
289 .field("income_level", income_level)
290 .field("start_of_week", start_of_week)
291 .key("signature");
292 _signature.to_json(w);
293 w.end_object();
294 }
295
296 /// @brief This country as a JSON document.
297 /// @return Compact JSON.
298 [[nodiscard]] std::string to_json() const
299 {
300 core::json_writer w;
301 to_json(w);
302 return w.str();
303 }
304
305 /// @brief Implicit conversion to std::string.
306 /// @return The common English name.
307 operator std::string() const // NOLINT(hicpp-explicit-conversions)
308 {
309 return name_common;
310 }
311
312 /// @brief Stream the common name to an output stream.
313 /// @param os Output stream.
314 /// @param c Country to stream.
315 /// @return The stream.
316 friend std::ostream& operator<<(std::ostream& os, const country& c)
317 {
318 os << c.name_common;
319 return os;
320 }
321
322 private:
323 core::seed_signature _signature;
324
325 // Calls visit(name, percent) for each "Name:percent" pair of a shares text and returns true;
326 // returns false at the first malformed pair (no colon, empty name, or a percent not in (0, 100]).
327 template <typename Visit>
328 [[nodiscard]] static bool for_each_share_(std::string_view text, const Visit& visit)
329 {
330 while (!text.empty())
331 {
332 const auto end = text.find(';');
333 const auto pair = text.substr(0, end);
334 const auto colon = pair.rfind(':');
335 if (colon == std::string_view::npos || colon == 0) return false;
336 const auto percent = core::detail::parse_number<double>(pair.substr(colon + 1));
337 if (!percent || !(*percent > 0.0) || *percent > 100.0) return false;
338 visit(pair.substr(0, colon), *percent);
339 text = end == std::string_view::npos ? std::string_view{} : text.substr(end + 1);
340 }
341 return true;
342 }
343
344 // "Name:percent;..." -> shares; nullopt when a pair is malformed.
345 [[nodiscard]] static std::optional<std::vector<language_share>> parse_shares_(std::string_view text)
346 {
347 std::vector<language_share> out;
348 if (!for_each_share_(text, [&out](std::string_view name, double percent) {
349 out.push_back({.name = std::string{name}, .percent = percent});
350 }))
351 {
352 return std::nullopt;
353 }
354 return out;
355 }
356
357 friend class cntg;
358};
359
360/// @brief Per-call country selection options.
362{
363 /// @brief UN geoscheme region; unset selects from all loaded countries.
364 /// An empty or unknown region throws std::invalid_argument.
365 std::optional<std::string> region{std::nullopt};
366
367 /// @brief Population-weighted selection (default); false selects uniformly.
368 bool weighted{true};
369};
370
371// -- Locales, time zones, currencies and holidays ------------------------------
372
373/// @brief The length of a date format (CLDR `dateFormats`).
374enum class date_style : std::uint8_t
375{
376 short_, ///< `dd/MM/y` in `pt-BR`.
377 medium, ///< `d 'de' MMM 'de' y`.
378 long_, ///< `d 'de' MMMM 'de' y`.
379 full ///< `EEEE, d 'de' MMMM 'de' y`.
380};
381
382/// @brief The length of a time format (CLDR `timeFormats`; the long and full
383/// ones name a zone and are not stored).
384enum class time_style : std::uint8_t
385{
386 short_, ///< `HH:mm`.
387 medium ///< `HH:mm:ss`.
388};
389
390/// @brief A country's measurement system (CLDR `measurementData`).
391enum class measurement : std::uint8_t
392{
393 metric, ///< Everywhere else.
394 us, ///< The United States and Liberia.
395 uk ///< The United Kingdom and, in CLDR 47, Myanmar.
396};
397
398/// @brief What kind of day a holiday is (python-holidays' categories).
399enum class holiday_kind : std::uint8_t
400{
401 public_, ///< A public holiday; always returned.
402 bank, ///< A bank holiday; always returned.
403 optional, ///< A day off for some (Brazil's Carnival, government-only days); on request.
404 observance ///< A named day that is not a day off (Mother's Day); on request.
405};
406
407/// @brief The tables of the country data, for loading and extending it.
408enum class country_table : std::uint8_t
409{
410 countries, ///< `countries.tsv`: the countries.
411 locales, ///< `locales.tsv`: each country's locale tags and their CLDR formats.
412 zones, ///< `zones.tsv`: each country's IANA zones and their rules.
413 zone_links, ///< `zone_links.tsv`: old zone names and the zones they now name.
414 currencies, ///< `currencies.tsv`: the ISO 4217 minor unit of each currency.
415 holidays, ///< `holidays.tsv`: each country's holiday rules.
416 holiday_events, ///< `holiday_events.tsv`: the dates of lunar and other calendar events.
417 holiday_years ///< `holiday_years.tsv`: the years each country's holidays are served for.
418};
419
420/// @brief The years a country's holidays are served for (holiday_years()).
422{
423 int from{}; ///< First year.
424 int to{}; ///< Last year.
425
426 /// @brief Equal when both years are.
427 friend bool operator==(const year_span&, const year_span&) = default;
428};
429
430/// @cond INTERNAL
431namespace country_detail
432{
433
434[[nodiscard]] inline std::string upper(std::string_view s)
435{
436 std::string out{s};
437 for (auto& c : out)
438 {
439 if (c >= 'a' && c <= 'z') c = static_cast<char>(c - 'a' + 'A');
440 }
441 return out;
442}
443
444[[nodiscard]] inline std::string lower(std::string_view s)
445{
446 std::string out{s};
447 for (auto& c : out)
448 {
449 if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
450 }
451 return out;
452}
453
454[[nodiscard]] constexpr bool is_letter(char c) noexcept
455{
456 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
457}
458
459// Weekday codes and names in std::chrono::weekday's c_encoding (Sunday 0).
460inline constexpr std::array<std::string_view, 7> weekday_codes{"sun", "mon", "tue", "wed", "thu", "fri", "sat"};
461inline constexpr std::array<std::string_view, 7> weekday_names{"sunday", "monday", "tuesday", "wednesday",
462 "thursday", "friday", "saturday"};
463
464// Measurement system names, by dasmig::measurement.
465inline constexpr std::array<std::string_view, 3> measurement_names{"metric", "us", "uk"};
466
467[[nodiscard]] inline std::optional<std::chrono::weekday> parse_weekday(std::string_view code)
468{
469 for (unsigned i = 0; i < weekday_codes.size(); ++i)
470 {
471 if (weekday_codes.at(i) == code) return std::chrono::weekday{i};
472 }
473 return std::nullopt;
474}
475
476// Appends v (not negative) in base 10, padded with zeros to width digits.
477// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
478inline void append_padded(std::string& out, std::int64_t v, std::size_t width)
479{
480 std::array<char, 24> buf{};
481 const auto res = std::to_chars(buf.data(), std::to_address(buf.end()), v);
482 const auto n = static_cast<std::size_t>(res.ptr - buf.data());
483 if (n < width) out.append(width - n, '0');
484 out.append(buf.data(), n);
485}
486
487// "2024-04-10".
488[[nodiscard]] inline std::string iso_date(std::chrono::year_month_day d)
489{
490 std::string out;
491 append_padded(out, static_cast<int>(d.year()), 4);
492 out += '-';
493 append_padded(out, static_cast<unsigned>(d.month()), 2);
494 out += '-';
495 append_padded(out, static_cast<unsigned>(d.day()), 2);
496 return out;
497}
498
499// "2024-04-10" -> the date; nullopt unless the text is exactly that form and a valid date.
500[[nodiscard]] inline std::optional<std::chrono::year_month_day> parse_iso_date(std::string_view t)
501{
502 if (t.size() != 10 || t[4] != '-' || t[7] != '-') return std::nullopt;
503 const auto y = core::detail::parse_number<int>(t.substr(0, 4));
504 const auto m = core::detail::parse_number<unsigned>(t.substr(5, 2));
505 const auto d = core::detail::parse_number<unsigned>(t.substr(8, 2));
506 if (!y || !m || !d) return std::nullopt;
507 const std::chrono::year_month_day ymd{std::chrono::year{*y}, std::chrono::month{*m}, std::chrono::day{*d}};
508 if (!ymd.ok()) return std::nullopt;
509 return ymd;
510}
511
512// -- LDML date and time patterns (design 5.1) ----------------------------------
513
514// Walks a pattern: literal(text) for literal text (quotes resolved), field(letter, count) for each run
515// of one letter. Returns false for an unterminated quote.
516template <typename Literal, typename Field>
517// NOLINTNEXTLINE(readability-function-cognitive-complexity): one pass over the pattern
518bool walk_pattern(std::string_view p, const Literal& literal, const Field& field)
519{
520 std::size_t i = 0;
521 while (i < p.size())
522 {
523 if (p[i] == '\'')
524 {
525 if (i + 1 < p.size() && p[i + 1] == '\'')
526 {
527 literal(std::string_view{"'"});
528 i += 2;
529 continue;
530 }
531 std::string text;
532 std::size_t j = i + 1;
533 for (;; ++j)
534 {
535 if (j == p.size()) return false;
536 if (p[j] != '\'')
537 {
538 text += p[j];
539 continue;
540 }
541 if (j + 1 < p.size() && p[j + 1] == '\'')
542 {
543 text += '\'';
544 ++j;
545 continue;
546 }
547 break;
548 }
549 literal(std::string_view{text});
550 i = j + 1;
551 continue;
552 }
553 if (is_letter(p[i]))
554 {
555 std::size_t j = i;
556 while (j < p.size() && p[j] == p[i]) ++j;
557 field(p[i], static_cast<unsigned>(j - i));
558 i = j;
559 continue;
560 }
561 std::size_t j = i;
562 while (j < p.size() && p[j] != '\'' && !is_letter(p[j])) ++j;
563 literal(p.substr(i, j - i));
564 i = j;
565 }
566 return true;
567}
568
569// Whether a field is one the formatter writes, for a date or a time pattern.
570// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
571[[nodiscard]] constexpr bool supported_field(char c, unsigned n, bool time) noexcept
572{
573 if (time)
574 {
575 switch (c)
576 {
577 case 'a': return n <= 3;
578 case 'h':
579 case 'H':
580 case 'm':
581 case 's': return n <= 2;
582 default: return false;
583 }
584 }
585 switch (c)
586 {
587 case 'G': return n <= 3;
588 case 'y':
589 case 'M':
590 case 'L':
591 case 'E':
592 case 'c': return n <= 4;
593 case 'd': return n <= 2;
594 default: return false;
595 }
596}
597
598// Empty when the pattern is one the formatter writes; else what is wrong.
599[[nodiscard]] inline std::string check_pattern(std::string_view p, bool time)
600{
601 std::string error;
602 const bool closed = walk_pattern(
603 p, [](std::string_view) {},
604 [&](char c, unsigned n) {
605 if (error.empty() && !supported_field(c, n, time))
606 {
607 error = "unsupported field '" + std::string(n, c) + "'";
608 }
609 });
610 if (!closed) return "unterminated quote";
611 return error;
612}
613
614// -- CLDR number patterns (design 5.2, 5.3) ------------------------------------
615
616// Affix text keeps these marks where the pattern has ¤ and -.
617inline constexpr char currency_mark = '\x01';
618inline constexpr char minus_mark = '\x02';
619
620struct number_format
621{
622 std::string positive_prefix, positive_suffix, negative_prefix, negative_suffix;
623 std::size_t primary{0}, secondary{0}; // grouping sizes; 0 = no grouping
624 std::size_t min_integer{1}, min_fraction{0}, max_fraction{0};
625};
626
627[[nodiscard]] constexpr bool number_char(char c) noexcept
628{
629 return c == '#' || c == '0' || c == ',' || c == '.';
630}
631
632// One subpattern into prefix, number body and suffix; false on an unsupported character.
633// NOLINTNEXTLINE(bugprone-easily-swappable-parameters,readability-function-cognitive-complexity): one pass
634inline bool split_subpattern(std::string_view p, std::string& prefix, std::string& body, std::string& suffix)
635{
636 constexpr std::string_view currency_sign = "\xC2\xA4"; // ¤
637 int part = 0; // 0 prefix, 1 body, 2 suffix
638 for (std::size_t i = 0; i < p.size(); ++i)
639 {
640 const char c = p[i];
641 if (part == 1 && !number_char(c)) part = 2;
642 if (part == 0 && number_char(c)) part = 1;
643 if (part == 1)
644 {
645 body += c;
646 continue;
647 }
648 auto& affix = part == 0 ? prefix : suffix;
649 if (c == '\'')
650 {
651 if (i + 1 < p.size() && p[i + 1] == '\'')
652 {
653 affix += '\'';
654 ++i;
655 continue;
656 }
657 const auto end = p.find('\'', i + 1);
658 if (end == std::string_view::npos) return false;
659 affix += p.substr(i + 1, end - i - 1);
660 i = end;
661 continue;
662 }
663 if (p.substr(i, currency_sign.size()) == currency_sign)
664 {
665 affix += currency_mark;
666 i += currency_sign.size() - 1;
667 continue;
668 }
669 if (c == '-')
670 {
671 affix += minus_mark;
672 continue;
673 }
674 if (c == '%' || c == '+' || c == '*' || c == '@' || c == ';' || c == currency_mark || c == minus_mark)
675 {
676 return false;
677 }
678 affix += c;
679 }
680 return !body.empty();
681}
682
683// Parses a pattern; empty error when it is one the formatter writes.
684// NOLINTNEXTLINE(readability-function-cognitive-complexity): one check per pattern part
685[[nodiscard]] inline number_format parse_number_pattern(std::string_view text, bool currency, std::string& error)
686{
687 number_format f;
688 error.clear();
689 // The subpatterns split at a ';' outside quotes.
690 std::size_t semi = std::string_view::npos;
691 bool quoted = false;
692 for (std::size_t i = 0; i < text.size(); ++i)
693 {
694 if (text[i] == '\'') quoted = !quoted;
695 if (text[i] == ';' && !quoted)
696 {
697 semi = i;
698 break;
699 }
700 }
701 std::string body;
702 if (!split_subpattern(text.substr(0, semi), f.positive_prefix, body, f.positive_suffix))
703 {
704 error = "unsupported number pattern";
705 return f;
706 }
707 const auto dot = body.find('.');
708 const std::string_view integer = std::string_view{body}.substr(0, dot);
709 const std::string_view fraction =
710 dot == std::string::npos ? std::string_view{} : std::string_view{body}.substr(dot + 1);
711 // The fraction is zeros then hashes ("00", "###", "0##"); the integer does not end in a comma.
712 const auto hash = fraction.find('#');
713 if (integer.empty() || integer.back() == ',' || fraction.find_first_not_of("0#") != std::string_view::npos ||
714 (hash != std::string_view::npos && fraction.find('0', hash) != std::string_view::npos))
715 {
716 error = "unsupported number pattern";
717 return f;
718 }
719 f.min_integer = static_cast<std::size_t>(std::ranges::count(integer, '0'));
720 f.min_fraction = static_cast<std::size_t>(std::ranges::count(fraction, '0'));
721 f.max_fraction = fraction.size();
722 const auto last_comma = integer.rfind(',');
723 if (last_comma != std::string_view::npos)
724 {
725 f.primary = integer.size() - last_comma - 1;
726 const auto before = last_comma == 0 ? std::string_view::npos : integer.rfind(',', last_comma - 1);
727 f.secondary = before == std::string_view::npos ? f.primary : last_comma - before - 1;
728 if (f.primary == 0 || f.secondary == 0)
729 {
730 error = "unsupported number pattern";
731 return f;
732 }
733 }
734 if (semi != std::string_view::npos)
735 {
736 std::string negative_body;
737 if (!split_subpattern(text.substr(semi + 1), f.negative_prefix, negative_body, f.negative_suffix))
738 {
739 error = "unsupported negative subpattern";
740 return f;
741 }
742 }
743 else
744 {
745 f.negative_prefix = minus_mark + f.positive_prefix;
746 f.negative_suffix = f.positive_suffix;
747 }
748 const auto has_currency = [](const std::string& a, const std::string& b) {
749 return a.contains(currency_mark) || b.contains(currency_mark);
750 };
751 if (currency != has_currency(f.positive_prefix, f.positive_suffix) ||
752 currency != has_currency(f.negative_prefix, f.negative_suffix))
753 {
754 error = currency ? "a currency pattern needs ¤" : "a number pattern cannot have ¤";
755 }
756 return f;
757}
758
759// The first and last code points of UTF-8 text (0 for empty text).
760[[nodiscard]] inline char32_t first_code_point(std::string_view s) noexcept
761{
762 if (s.empty()) return 0;
763 const auto b = static_cast<unsigned char>(s[0]);
764 std::size_t n = 4;
765 if (b < 0x80) n = 1;
766 else if (b < 0xE0) n = 2;
767 else if (b < 0xF0) n = 3;
768 if (s.size() < n) return 0;
769 constexpr std::array<unsigned, 5> lead_bits{0, 0x7FU, 0x1FU, 0x0FU, 0x07U}; // by sequence length
770 char32_t cp = b & lead_bits.at(n);
771 for (std::size_t i = 1; i < n; ++i) cp = (cp << 6U) | (static_cast<char32_t>(static_cast<unsigned char>(s[i])) & 0x3FU);
772 return cp;
773}
774
775[[nodiscard]] inline char32_t last_code_point(std::string_view s) noexcept
776{
777 std::size_t i = s.size();
778 while (i > 0 && (static_cast<unsigned char>(s[i - 1]) & 0xC0U) == 0x80U) --i;
779 return i == 0 ? 0 : first_code_point(s.substr(i - 1));
780}
781
782// Whether a code point is a symbol or a separator ([:S:] or [:Z:]) for CLDR's currencySpacing,
783// for the characters currency symbols are made of: the currency symbols (Sc), the ASCII and
784// Latin-1 symbols and the spaces. Letters, marks, digits and punctuation (the '.' of "лв.")
785// are not, so a symbol that ends in one is spaced from the number.
786[[nodiscard]] constexpr bool symbol_or_space(char32_t c) noexcept
787{
788 if (c < 0x80)
789 {
790 return c == U' ' || c == U'$' || c == U'+' || c == U'<' || c == U'=' || c == U'>' || c == U'^' ||
791 c == U'`' || c == U'|' || c == U'~';
792 }
793 if (c <= 0xFF)
794 {
795 return c == 0xA0 || (c >= 0xA2 && c <= 0xA6) || c == 0xA8 || c == 0xA9 || c == 0xAC ||
796 (c >= 0xAE && c <= 0xB1) || c == 0xB4 || c == 0xB8 || c == 0xD7 || c == 0xF7;
797 }
798 constexpr std::array<char32_t, 25> currency{0x058F, 0x060B, 0x07FE, 0x07FF, 0x09F2, 0x09F3, 0x09FB,
799 0x0AF1, 0x0BF9, 0x0E3F, 0x17DB, 0xA838, 0xFDFC, 0xFE69,
800 0xFF04, 0xFFE0, 0xFFE1, 0xFFE5, 0xFFE6, 0x11FDD, 0x11FDE,
801 0x11FDF, 0x11FE0, 0x1E2FF, 0x1ECB0};
802 if (std::ranges::find(currency, c) != currency.end()) return true;
803 return (c >= 0x20A0 && c <= 0x20C0) || c == 0x1680 || (c >= 0x2000 && c <= 0x200A) || c == 0x2028 ||
804 c == 0x2029 || c == 0x202F || c == 0x205F || c == 0x3000;
805}
806
807// The integer and fraction digits of |v|: the shortest decimal that reads back to v, rounded
808// half-even to at most max_fraction digits, on the text (design 5.2).
809// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
810[[nodiscard]] inline std::pair<std::string, std::string> decimal_digits(double v, std::size_t max_fraction)
811{
812 std::array<char, 400> buf{};
813 const auto res = std::to_chars(buf.data(), std::to_address(buf.end()), std::fabs(v), std::chars_format::fixed);
814 const std::string_view text{buf.data(), static_cast<std::size_t>(res.ptr - buf.data())};
815 const auto dot = text.find('.');
816 std::string integer{text.substr(0, dot)};
817 std::string fraction{dot == std::string_view::npos ? std::string_view{} : text.substr(dot + 1)};
818 if (fraction.size() <= max_fraction) return {integer, fraction};
819 const char first = fraction[max_fraction];
820 const bool rest = fraction.find_first_not_of('0', max_fraction + 1) != std::string::npos;
821 fraction.resize(max_fraction);
822 const char kept = max_fraction > 0 ? fraction.back() : integer.back();
823 const bool up = first > '5' || (first == '5' && (rest || (kept - '0') % 2 == 1));
824 if (!up) return {integer, fraction};
825 for (auto* digits : {&fraction, &integer})
826 {
827 for (char& c : std::views::reverse(*digits))
828 {
829 if (c != '9')
830 {
831 ++c;
832 return {integer, fraction};
833 }
834 c = '0';
835 }
836 }
837 integer.insert(integer.begin(), '1');
838 return {integer, fraction};
839}
840
841// -- Holidays (design 8) ---------------------------------------------------------
842
843enum class rule_form : std::uint8_t
844{
845 fixed,
846 nth,
847 after,
848 before,
849 easter,
850 orthodox,
851 event
852};
853
854struct holiday_rule
855{
856 rule_form form{rule_form::fixed};
857 unsigned month{1}, day{1};
858 int n{0}; // the N-th weekday (-1 the last), or the days added to Easter or the event
859 std::chrono::weekday weekday{};
860 std::string event;
861};
862
863[[nodiscard]] constexpr bool key_char(char c) noexcept
864{
865 return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
866}
867
868[[nodiscard]] constexpr bool valid_key(std::string_view k) noexcept
869{
870 return !k.empty() && std::ranges::all_of(k, key_char);
871}
872
873// "+3", "-48" or empty -> the offset; nullopt when malformed or beyond a year.
874[[nodiscard]] inline std::optional<int> parse_offset(std::string_view t)
875{
876 if (t.empty()) return 0;
877 if (t.size() < 2 || (t[0] != '+' && t[0] != '-')) return std::nullopt;
878 const auto v = core::detail::parse_number<int>(t.substr(1));
879 if (!v || *v > 366 || t.substr(1).find_first_not_of("0123456789") != std::string_view::npos)
880 {
881 return std::nullopt;
882 }
883 return t[0] == '-' ? -*v : *v;
884}
885
886// "MM-DD" -> month and day, valid in a leap year.
887[[nodiscard]] inline bool parse_month_day(std::string_view t, unsigned& month, unsigned& day)
888{
889 if (t.size() != 5 || t[2] != '-') return false;
890 const auto m = core::detail::parse_number<unsigned>(t.substr(0, 2));
891 const auto d = core::detail::parse_number<unsigned>(t.substr(3, 2));
892 if (!m || !d) return false;
893 month = *m;
894 day = *d;
895 return std::chrono::year_month_day{std::chrono::year{2024}, std::chrono::month{month}, std::chrono::day{day}}.ok();
896}
897
898// NOLINTNEXTLINE(readability-function-cognitive-complexity): one branch per rule form
899[[nodiscard]] inline std::optional<holiday_rule> parse_rule(std::string_view t)
900{
901 holiday_rule r;
902 if (t.starts_with("event:"))
903 {
904 const auto rest = t.substr(6);
905 const auto sign = rest.find_first_of("+-");
906 r.form = rule_form::event;
907 r.event = std::string{rest.substr(0, sign)};
908 const auto n = parse_offset(sign == std::string_view::npos ? std::string_view{} : rest.substr(sign));
909 if (!valid_key(r.event) || !n) return std::nullopt;
910 r.n = *n;
911 return r;
912 }
913 for (const auto& [name, form] : {std::pair{std::string_view{"easter"}, rule_form::easter},
914 std::pair{std::string_view{"orthodox"}, rule_form::orthodox}})
915 {
916 if (t.starts_with(name))
917 {
918 const auto n = parse_offset(t.substr(name.size()));
919 if (!n) return std::nullopt;
920 r.form = form;
921 r.n = *n;
922 return r;
923 }
924 }
925 if (t.size() > 3 && t[2] == ':')
926 {
927 // MM:N:dow
928 const auto second = t.find(':', 3);
929 if (second == std::string_view::npos) return std::nullopt;
930 const auto m = core::detail::parse_number<unsigned>(t.substr(0, 2));
931 const auto n = core::detail::parse_number<int>(t.substr(3, second - 3));
932 const auto dow = parse_weekday(t.substr(second + 1));
933 if (!m || *m < 1 || *m > 12 || !n || (*n != -1 && (*n < 1 || *n > 5)) || !dow) return std::nullopt;
934 r.form = rule_form::nth;
935 r.month = *m;
936 r.n = *n;
937 r.weekday = *dow;
938 return r;
939 }
940 if (t.size() == 9 && (t[5] == '>' || t[5] == '<'))
941 {
942 const auto dow = parse_weekday(t.substr(6));
943 if (!parse_month_day(t.substr(0, 5), r.month, r.day) || !dow) return std::nullopt;
944 r.form = t[5] == '>' ? rule_form::after : rule_form::before;
945 r.weekday = *dow;
946 return r;
947 }
948 if (!parse_month_day(t, r.month, r.day)) return std::nullopt;
949 r.form = rule_form::fixed;
950 return r;
951}
952
953// Western Easter Sunday (the Anonymous Gregorian algorithm, Meeus).
954[[nodiscard]] constexpr std::chrono::sys_days easter(int y) noexcept
955{
956 const int a = y % 19;
957 const int b = y / 100;
958 const int c = y % 100;
959 const int d = b / 4;
960 const int e = b % 4;
961 const int f = (b + 8) / 25;
962 const int g = (b - f + 1) / 3;
963 const int h = (19 * a + b - d - g + 15) % 30;
964 const int i = c / 4;
965 const int k = c % 4;
966 const int l = (32 + 2 * e + 2 * i - h - k) % 7;
967 const int m = (a + 11 * h + 22 * l) / 451;
968 const int n = h + l - 7 * m + 114;
969 return std::chrono::sys_days{std::chrono::year{y} / std::chrono::month{static_cast<unsigned>(n / 31)} /
970 std::chrono::day{static_cast<unsigned>(n % 31 + 1)}};
971}
972
973// Orthodox Easter Sunday (the Julian computus, Meeus), as a Gregorian date.
974[[nodiscard]] constexpr std::chrono::sys_days orthodox_easter(int y) noexcept
975{
976 const int a = y % 4;
977 const int b = y % 7;
978 const int c = y % 19;
979 const int d = (19 * c + 15) % 30;
980 const int e = (2 * a + 4 * b - d + 34) % 7;
981 const int n = d + e + 114;
982 return std::chrono::sys_days{std::chrono::year{y} / std::chrono::month{static_cast<unsigned>(n / 31)} /
983 std::chrono::day{static_cast<unsigned>(n % 31 + 1)}} +
984 std::chrono::days{y / 100 - y / 400 - 2};
985}
986
987[[nodiscard]] inline int year_of(std::chrono::sys_days d)
988{
989 return static_cast<int>(std::chrono::year_month_day{d}.year());
990}
991
992// The start dates a rule gives in year y; events: the event's dates for the country, sorted.
993inline void rule_dates(const holiday_rule& r, int y, const std::vector<std::chrono::sys_days>* events,
994 std::vector<std::chrono::sys_days>& out)
995{
996 using namespace std::chrono;
997 out.clear();
998 switch (r.form)
999 {
1000 case rule_form::fixed:
1001 {
1002 const year_month_day d{year{y}, month{r.month}, day{r.day}};
1003 if (d.ok()) out.push_back(sys_days{d});
1004 return;
1005 }
1006 case rule_form::nth:
1007 {
1008 if (r.n > 0)
1009 {
1010 const sys_days first{year{y} / month{r.month} / 1};
1011 out.push_back(first + (r.weekday - weekday{first}) + days{7 * (r.n - 1)});
1012 }
1013 else
1014 {
1015 const sys_days final_day{year{y} / month{r.month} / std::chrono::last};
1016 out.push_back(final_day - (weekday{final_day} - r.weekday));
1017 }
1018 return;
1019 }
1020 case rule_form::after:
1021 case rule_form::before:
1022 {
1023 const sys_days base{year{y} / month{r.month} / day{r.day}};
1024 out.push_back(r.form == rule_form::after ? base + (r.weekday - weekday{base})
1025 : base - (weekday{base} - r.weekday));
1026 return;
1027 }
1028 case rule_form::easter: out.push_back(easter(y) + days{r.n}); return;
1029 case rule_form::orthodox: out.push_back(orthodox_easter(y) + days{r.n}); return;
1030 case rule_form::event:
1031 {
1032 if (events == nullptr) return;
1033 const sys_days lo = sys_days{year{y} / 1 / 1} - days{r.n};
1034 const sys_days hi = sys_days{year{y + 1} / 1 / 1} - days{r.n};
1035 for (auto it = std::ranges::lower_bound(*events, lo); it != events->end() && *it < hi; ++it)
1036 {
1037 out.push_back(*it + days{r.n});
1038 }
1039 return;
1040 }
1041 }
1042}
1043
1044// An observed policy (design 8.4): none, a weekday map, next_weekday or weekend>next, with overlap.
1045struct observed_policy
1046{
1047 enum class type : std::uint8_t
1048 {
1049 none,
1050 map,
1051 next_weekday,
1052 weekend_next
1053 };
1054 type kind{type::none};
1055 // By c_encoding of the day that moves: the weekday it moves to, and +1 (after) or -1 (before).
1056 std::array<std::optional<std::pair<std::chrono::weekday, int>>, 7> moves{};
1057 bool overlap{false};
1058 std::string text;
1059};
1060
1061[[nodiscard]] inline std::optional<observed_policy> parse_policy(std::string_view t)
1062{
1063 observed_policy p;
1064 p.text = std::string{t};
1065 if (t == "none") return p;
1066 if (t.ends_with(",overlap"))
1067 {
1068 p.overlap = true;
1069 t.remove_suffix(8);
1070 }
1071 if (t == "next_weekday")
1072 {
1073 p.kind = observed_policy::type::next_weekday;
1074 return p;
1075 }
1076 if (t == "weekend>next")
1077 {
1078 p.kind = observed_policy::type::weekend_next;
1079 return p;
1080 }
1081 p.kind = observed_policy::type::map;
1082 while (!t.empty())
1083 {
1084 const auto comma = t.find(',');
1085 const auto item = t.substr(0, comma);
1086 t = comma == std::string_view::npos ? std::string_view{} : t.substr(comma + 1);
1087 if (item.size() != 7 || (item[3] != '<' && item[3] != '>')) return std::nullopt;
1088 const auto from = parse_weekday(item.substr(0, 3));
1089 const auto to = parse_weekday(item.substr(4));
1090 if (!from || !to || *from == *to || p.moves.at(from->c_encoding())) return std::nullopt;
1091 p.moves.at(from->c_encoding()) = std::pair{*to, item[3] == '>' ? 1 : -1};
1092 }
1093 return p;
1094}
1095
1096// What the library needs of one holiday row.
1097struct holiday_row
1098{
1099 std::string key, name, name_en;
1100 holiday_kind kind{holiday_kind::public_};
1101 holiday_rule rule;
1102 unsigned days{1};
1103 observed_policy observed;
1104 int from{}, to{};
1105 const std::vector<std::chrono::sys_days>* events{nullptr}; // the country's dates of rule.event
1106};
1107
1108[[nodiscard]] inline std::optional<holiday_kind> parse_kind(std::string_view t)
1109{
1110 if (t == "public") return holiday_kind::public_;
1111 if (t == "bank") return holiday_kind::bank;
1112 if (t == "optional") return holiday_kind::optional;
1113 if (t == "observance") return holiday_kind::observance;
1114 return std::nullopt;
1115}
1116
1117[[nodiscard]] constexpr std::string_view kind_name(holiday_kind k) noexcept
1118{
1119 switch (k)
1120 {
1121 case holiday_kind::public_: return "public";
1122 case holiday_kind::bank: return "bank";
1123 case holiday_kind::optional: return "optional";
1124 case holiday_kind::observance: return "observance";
1125 }
1126 return "public";
1127}
1128
1129// Moves one occurrence's days by its policy (design 8.4). taken: the days of the kind's holidays
1130// and the moves so far; shared: the days moved under each policy, so holidays on one date move
1131// together. moved[i] is the observed day of the occurrence's day i.
1132// NOLINTNEXTLINE(readability-function-cognitive-complexity): the observed-day algorithm of design 8.4
1133inline void simulate(const observed_policy& p, std::chrono::sys_days start, unsigned days, std::set<int>& taken,
1134 unsigned weekend, std::map<std::pair<int, std::string_view>, int>& shared,
1135 std::vector<std::optional<std::chrono::sys_days>>& moved)
1136{
1137 using std::chrono::sys_days;
1138 moved.assign(days, std::nullopt);
1139 if (p.kind == observed_policy::type::none) return;
1140 const auto day_of = [](sys_days d) { return static_cast<int>(d.time_since_epoch().count()); };
1141 const auto is_weekend = [weekend](sys_days d) {
1142 return ((weekend >> std::chrono::weekday{d}.c_encoding()) & 1U) != 0;
1143 };
1144 const auto free = [&](sys_days t, int step) {
1145 while (!p.overlap && (taken.contains(day_of(t)) || is_weekend(t))) t += std::chrono::days{step};
1146 return t;
1147 };
1148 if (p.kind == observed_policy::type::weekend_next)
1149 {
1150 sys_days t = start + std::chrono::days{days};
1151 for (unsigned i = 0; i < days; ++i)
1152 {
1153 if (!is_weekend(start + std::chrono::days{i})) continue;
1154 t = free(t, 1);
1155 moved[i] = t;
1156 taken.insert(day_of(t));
1157 t += std::chrono::days{1};
1158 }
1159 return;
1160 }
1161 for (unsigned i = 0; i < days; ++i)
1162 {
1163 const sys_days d = start + std::chrono::days{i};
1164 sys_days t = d;
1165 int step = 1;
1166 if (p.kind == observed_policy::type::next_weekday)
1167 {
1168 if (!is_weekend(d)) continue;
1169 t = d + std::chrono::days{1};
1170 }
1171 else
1172 {
1173 const auto& move = p.moves.at(std::chrono::weekday{d}.c_encoding());
1174 if (!move) continue;
1175 step = move->second;
1176 t = d + std::chrono::days{step};
1177 while (std::chrono::weekday{t} != move->first) t += std::chrono::days{step};
1178 }
1179 const std::pair<int, std::string_view> id{day_of(d), p.text};
1180 if (const auto it = shared.find(id); it != shared.end())
1181 {
1182 moved[i] = sys_days{std::chrono::days{it->second}};
1183 continue;
1184 }
1185 t = free(t, step);
1186 moved[i] = t;
1187 shared.emplace(id, day_of(t));
1188 taken.insert(day_of(t));
1189 }
1190}
1191
1192} // namespace country_detail
1193/// @endcond
1194
1195/// @brief Per-call locale selection options (cntg::get_locale()).
1197{
1198 /// @brief ISO 3166-1 alpha-2 code, any case; unset draws the country as
1199 /// get_country() does (or by speakers of `language`, when set).
1200 std::optional<std::string> country{std::nullopt};
1201
1202 /// @brief ISO 639 code (`fr`), any case; narrows the draw to that language.
1203 std::optional<std::string> language{std::nullopt};
1204
1205 /// @brief Weighted by speakers (default); false makes every eligible tag,
1206 /// and every eligible country, equally likely.
1207 bool weighted{true};
1208};
1209
1210/// @brief A locale of a country: its tag, CLDR formats and calendar data.
1211///
1212/// Every row is complete: the build resolved CLDR's inheritance, so the
1213/// formatting functions read only these fields.
1215{
1216 public:
1217 std::string tag; ///< BCP 47, minimised: "pt-BR", "zh-TW", "sr-Latn-RS".
1218 std::string language; ///< ISO 639 code: "pt".
1219 std::string script; ///< ISO 15924 code when the tag needs one ("Latn"); else empty.
1220 std::string language_name; ///< English, as country::language_shares writes it: "Portuguese".
1221 std::string country_code; ///< ISO 3166-1 alpha-2: "BR".
1222 double share{0.0}; ///< Percent of the country that speaks the language (CLDR).
1223 std::string basis; ///< "cldr", "inherited" or "fallback" (doc/data.md).
1224
1225 std::string date_short; ///< LDML short date pattern: "dd/MM/y".
1226 std::string date_medium; ///< LDML medium date pattern.
1227 std::string date_long; ///< LDML long date pattern.
1228 std::string date_full; ///< LDML full date pattern.
1229 std::string time_short; ///< LDML short time pattern: "HH:mm".
1230 std::string time_medium; ///< LDML medium time pattern: "HH:mm:ss".
1231 std::array<std::string, 12> months{}; ///< Wide month names, format context, January first.
1232 std::array<std::string, 12> months_abbr{}; ///< Abbreviated month names.
1233 std::array<std::string, 7> weekdays{}; ///< Wide weekday names, format context, Sunday first.
1234 std::array<std::string, 7> weekdays_abbr{}; ///< Abbreviated weekday names, Sunday first.
1235 std::string am; ///< Day period before noon.
1236 std::string pm; ///< Day period after noon.
1237 std::array<std::string, 2> eras{}; ///< Abbreviated eras: BC, AD.
1238
1239 std::string decimal; ///< Decimal symbol: ",".
1240 std::string group; ///< Grouping symbol: ".".
1241 std::string minus; ///< Minus sign: "-".
1242 std::string number_pattern; ///< CLDR decimal pattern: "#,##0.###".
1243 std::string currency_pattern; ///< CLDR currency pattern: "¤ #,##0.00".
1244 std::string currency_spacing; ///< Put between a letter symbol and the number; usually U+00A0.
1245 std::string currency_code; ///< The country's ISO 4217 code: "BRL".
1246 std::string currency_symbol; ///< This locale's symbol for it: "R$".
1247 std::uint8_t currency_digits{2}; ///< Its ISO 4217 minor unit.
1248
1249 std::chrono::weekday first_day{std::chrono::Monday}; ///< First day of the week.
1250 std::chrono::weekday weekend_start{std::chrono::Saturday}; ///< First day of the weekend.
1251 std::chrono::weekday weekend_end{std::chrono::Sunday}; ///< Last day of the weekend.
1252 dasmig::measurement measurement{dasmig::measurement::metric}; ///< Measurement system.
1253
1254 /// @brief Write a date with one of the locale's date patterns.
1255 /// @param d The date.
1256 /// @param style Which pattern.
1257 /// @return The text, in UTF-8.
1258 /// @throws std::invalid_argument If @p d is not a valid date, or the
1259 /// pattern holds a field the formatter does not write.
1260 // NOLINTNEXTLINE(readability-function-cognitive-complexity): one case per field
1261 [[nodiscard]] std::string format_date(std::chrono::year_month_day d, date_style style = date_style::short_) const
1262 {
1263 if (!d.ok()) throw std::invalid_argument("format_date: not a valid date");
1264 const std::array<const std::string*, 4> patterns{&date_short, &date_medium, &date_long, &date_full};
1265 const std::string& pattern = *patterns.at(static_cast<std::size_t>(style));
1266 const int y = static_cast<int>(d.year());
1267 const auto month = static_cast<unsigned>(d.month());
1268 const unsigned wd = std::chrono::weekday{std::chrono::sys_days{d}}.c_encoding();
1269 std::string out;
1270 write_pattern_(pattern, false, "format_date", out, [&](char c, unsigned n) {
1271 const int era_year = y > 0 ? y : 1 - y;
1272 switch (c)
1273 {
1274 case 'G': out += eras.at(y > 0 ? 1 : 0); break;
1275 case 'y':
1276 if (n == 2) country_detail::append_padded(out, era_year % 100, 2);
1277 else country_detail::append_padded(out, era_year, n);
1278 break;
1279 case 'M':
1280 case 'L':
1281 if (n >= 3) out += (n == 3 ? months_abbr : months).at(month - 1);
1282 else country_detail::append_padded(out, month, n);
1283 break;
1284 case 'd': country_detail::append_padded(out, static_cast<unsigned>(d.day()), n); break;
1285 default: out += (n == 4 ? weekdays : weekdays_abbr).at(wd); break; // E and c
1286 }
1287 });
1288 return out;
1289 }
1290
1291 /// @brief Write a time of day with one of the locale's time patterns.
1292 /// @param t The time (0 to 24 hours).
1293 /// @param style Which pattern.
1294 /// @return The text, in UTF-8.
1295 /// @throws std::invalid_argument If @p t is negative or 24 hours or more,
1296 /// or the pattern holds a field the formatter does not write.
1297 [[nodiscard]] std::string format_time(std::chrono::hh_mm_ss<std::chrono::seconds> t,
1298 time_style style = time_style::short_) const
1299 {
1300 if (t.is_negative() || t.hours().count() > 23)
1301 {
1302 throw std::invalid_argument("format_time: the time must be from 00:00:00 to 23:59:59");
1303 }
1304 const auto h = t.hours().count();
1305 std::string out;
1306 write_pattern_(style == time_style::short_ ? time_short : time_medium, true, "format_time", out,
1307 [&](char c, unsigned n) {
1308 switch (c)
1309 {
1310 case 'a': out += h < 12 ? am : pm; break;
1311 case 'h': country_detail::append_padded(out, h % 12 == 0 ? 12 : h % 12, n); break;
1312 case 'H': country_detail::append_padded(out, h, n); break;
1313 case 'm': country_detail::append_padded(out, t.minutes().count(), n); break;
1314 default: country_detail::append_padded(out, t.seconds().count(), n); break;
1315 }
1316 });
1317 return out;
1318 }
1319
1320 /// @brief Write a number with the locale's decimal pattern.
1321 ///
1322 /// The value's shortest round-trip digits are rounded half-even, as
1323 /// text, to at most @p max_fraction fraction digits (and the pattern's
1324 /// maximum), so 2.675 at two digits is `2.68` on every platform.
1325 ///
1326 /// @param value The number.
1327 /// @param max_fraction Most fraction digits.
1328 /// @return The text, in UTF-8.
1329 /// @throws std::invalid_argument If @p value is not finite or the pattern
1330 /// is not one the formatter writes.
1331 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
1332 [[nodiscard]] std::string format_number(double value, unsigned max_fraction = 3) const
1333 {
1334 if (!std::isfinite(value)) throw std::invalid_argument("format_number: the value is not finite");
1335 const auto f = number_format_(number_pattern, false, "format_number");
1336 const std::size_t digits = std::min<std::size_t>(max_fraction, f.max_fraction);
1337 auto [integer, fraction] = country_detail::decimal_digits(value, digits);
1338 const std::size_t min_fraction = std::min(f.min_fraction, digits);
1339 while (fraction.size() > min_fraction && fraction.back() == '0') fraction.pop_back();
1340 if (fraction.size() < min_fraction) fraction.append(min_fraction - fraction.size(), '0');
1341 const bool zero = integer.find_first_not_of('0') == std::string::npos &&
1342 fraction.find_first_not_of('0') == std::string::npos;
1343 return render_(f, value < 0 && !zero, integer, fraction, {});
1344 }
1345
1346 /// @brief Write an amount with the locale's currency pattern.
1347 ///
1348 /// Exactly `m.digits` fraction digits, from the integer minor units, so
1349 /// nothing is rounded. The symbol is the locale's own for its currency
1350 /// and the ISO code for any other; CLDR's currency spacing puts
1351 /// currency_spacing between a symbol that ends (or starts) with a letter
1352 /// and the number: `CHF 1,234.50` but `$1,234.50` in `en-US`.
1353 ///
1354 /// @param m The amount.
1355 /// @return The text, in UTF-8.
1356 /// @throws std::invalid_argument If the pattern is not one the formatter writes.
1357 [[nodiscard]] std::string format_money(const core::money& m) const
1358 {
1359 const auto f = number_format_(currency_pattern, true, "format_money");
1360 const std::uint64_t magnitude =
1361 m.minor < 0 ? static_cast<std::uint64_t>(-(m.minor + 1)) + 1 : static_cast<std::uint64_t>(m.minor);
1362 std::string digits = std::to_string(magnitude);
1363 if (digits.size() <= m.digits) digits.insert(0, m.digits + 1 - digits.size(), '0');
1364 const auto split = digits.size() - m.digits;
1365 return render_(f, m.minor < 0, digits.substr(0, split), digits.substr(split),
1366 m.currency == currency_code ? std::string_view{currency_symbol} : std::string_view{m.currency});
1367 }
1368
1369 /// @brief The seed this locale was drawn with.
1370 /// @return The per-call seed (0 for find_locale() and locales()).
1371 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
1372
1373 /// @brief The seed and the data this locale was drawn from.
1374 /// @return The replay signature.
1375 [[nodiscard]] const core::seed_signature& signature() const { return _signature; }
1376
1377 /// @brief Write this locale as a JSON object (doc/usage.md, "Locales").
1378 ///
1379 /// The month, weekday, era and day-period names are left out: they are
1380 /// data, reachable through cntg::find_locale().
1381 ///
1382 /// @param w Writer positioned where a value is allowed.
1383 void to_json(core::json_writer& w) const
1384 {
1385 w.begin_object()
1386 .field("tag", tag)
1387 .field("language", language)
1388 .field("script", script)
1389 .field("language_name", language_name)
1390 .field("country", country_code)
1391 .field("share", share)
1392 .field("basis", basis)
1393 .key("formats")
1394 .begin_object()
1395 .key("date")
1396 .begin_object()
1397 .field("short", date_short)
1398 .field("medium", date_medium)
1399 .field("long", date_long)
1400 .field("full", date_full)
1401 .end_object()
1402 .key("time")
1403 .begin_object()
1404 .field("short", time_short)
1405 .field("medium", time_medium)
1406 .end_object()
1407 .field("number", number_pattern)
1408 .field("currency", currency_pattern)
1409 .end_object()
1410 .key("symbols")
1411 .begin_object()
1412 .field("decimal", decimal)
1413 .field("group", group)
1414 .field("minus", minus)
1415 .end_object()
1416 .key("currency")
1417 .begin_object()
1418 .field("code", currency_code)
1419 .field("symbol", currency_symbol)
1420 .field("digits", static_cast<unsigned>(currency_digits))
1421 .end_object()
1422 .field("first_day", country_detail::weekday_names.at(first_day.c_encoding()))
1423 .key("weekend")
1424 .begin_array()
1425 .value(country_detail::weekday_names.at(weekend_start.c_encoding()))
1426 .value(country_detail::weekday_names.at(weekend_end.c_encoding()))
1427 .end_array()
1428 .field("measurement", country_detail::measurement_names.at(static_cast<std::size_t>(measurement)))
1429 .key("signature");
1430 _signature.to_json(w);
1431 w.end_object();
1432 }
1433
1434 /// @brief This locale as a JSON document.
1435 /// @return Compact JSON.
1436 [[nodiscard]] std::string to_json() const
1437 {
1438 core::json_writer w;
1439 to_json(w);
1440 return w.str();
1441 }
1442
1443 /// @brief Implicit conversion to std::string.
1444 /// @return The tag.
1445 operator std::string() const // NOLINT(hicpp-explicit-conversions)
1446 {
1447 return tag;
1448 }
1449
1450 /// @brief Stream the tag.
1451 /// @param os Output stream.
1452 /// @param l Locale to stream.
1453 /// @return The stream.
1454 friend std::ostream& operator<<(std::ostream& os, const locale& l)
1455 {
1456 os << l.tag;
1457 return os;
1458 }
1459
1460 private:
1461 core::seed_signature _signature;
1462
1463 template <typename Field>
1464 static void write_pattern_(std::string_view pattern, bool time, std::string_view caller, std::string& out,
1465 Field&& field)
1466 {
1467 if (const auto error = country_detail::check_pattern(pattern, time); !error.empty())
1468 {
1469 throw std::invalid_argument(std::string{caller} + ": " + error + " in '" + std::string{pattern} + "'");
1470 }
1471 (void)country_detail::walk_pattern(
1472 pattern, [&out](std::string_view text) { out += text; }, std::forward<Field>(field));
1473 }
1474
1475 static country_detail::number_format number_format_(std::string_view pattern, bool currency,
1476 std::string_view caller)
1477 {
1478 std::string error;
1479 auto f = country_detail::parse_number_pattern(pattern, currency, error);
1480 if (!error.empty())
1481 {
1482 throw std::invalid_argument(std::string{caller} + ": " + error + " in '" + std::string{pattern} + "'");
1483 }
1484 return f;
1485 }
1486
1487 // The number with its affixes, the symbols in place of the marks and the currency spacing.
1488 // NOLINTNEXTLINE(readability-function-cognitive-complexity): one pass over the affixes
1489 [[nodiscard]] std::string render_(const country_detail::number_format& f, bool negative, std::string integer,
1490 std::string_view fraction, std::string_view symbol) const
1491 {
1492 const auto start = integer.find_first_not_of('0');
1493 integer.erase(0, start == std::string::npos ? integer.size() : start);
1494 if (integer.size() < f.min_integer) integer.insert(0, f.min_integer - integer.size(), '0');
1495 std::string number;
1496 if (f.primary == 0 || integer.size() <= f.primary)
1497 {
1498 number = integer;
1499 }
1500 else
1501 {
1502 std::vector<std::string_view> groups;
1503 std::string_view rest{integer};
1504 groups.push_back(rest.substr(rest.size() - f.primary));
1505 rest.remove_suffix(f.primary);
1506 while (rest.size() > f.secondary)
1507 {
1508 groups.push_back(rest.substr(rest.size() - f.secondary));
1509 rest.remove_suffix(f.secondary);
1510 }
1511 groups.push_back(rest);
1512 for (auto it = groups.rbegin(); it != groups.rend(); ++it)
1513 {
1514 if (it != groups.rbegin()) number += group;
1515 number += *it;
1516 }
1517 }
1518 if (!fraction.empty())
1519 {
1520 number += decimal;
1521 number += fraction;
1522 }
1523 const auto& prefix = negative ? f.negative_prefix : f.positive_prefix;
1524 const auto& suffix = negative ? f.negative_suffix : f.positive_suffix;
1525 const auto expand = [&](const std::string& affix, std::string& out) {
1526 for (const char c : affix)
1527 {
1528 if (c == country_detail::currency_mark) out += symbol;
1529 else if (c == country_detail::minus_mark) out += minus;
1530 else out += c;
1531 }
1532 };
1533 std::string out;
1534 expand(prefix, out);
1535 if (!prefix.empty() && prefix.back() == country_detail::currency_mark &&
1536 !country_detail::symbol_or_space(country_detail::last_code_point(symbol)))
1537 {
1538 out += currency_spacing;
1539 }
1540 out += number;
1541 if (!suffix.empty() && suffix.front() == country_detail::currency_mark &&
1542 !country_detail::symbol_or_space(country_detail::first_code_point(symbol)))
1543 {
1544 out += currency_spacing;
1545 }
1546 expand(suffix, out);
1547 return out;
1548 }
1549
1550 friend class cntg;
1551};
1552
1553/// @brief Per-call time-zone selection options (cntg::get_time_zone()).
1555{
1556 /// @brief The date whose offset a result reports, at 12:00 UTC, unless set.
1557 static constexpr std::chrono::year_month_day default_reference_date{std::chrono::year{2023} /
1558 std::chrono::July / 1};
1559
1560 /// @brief ISO 3166-1 alpha-2 code, any case; unset draws the country as
1561 /// get_country() does.
1562 std::optional<std::string> country{std::nullopt};
1563
1564 /// @brief The date whose offset, abbreviation and daylight flag are reported.
1565 std::chrono::year_month_day reference_date{default_reference_date};
1566
1567 /// @brief Weighted by population (default); false makes every zone of the
1568 /// country, and every country, equally likely.
1569 bool weighted{true};
1570};
1571
1572/// @brief An IANA time zone of a country, with its rules.
1574{
1575 public:
1576 std::string name; ///< IANA name: "America/Sao_Paulo".
1577 std::string country_code; ///< ISO 3166-1 alpha-2: "BR".
1578 double share{0.0}; ///< Percent of the country's population in the zone (GeoNames).
1579 std::string basis; ///< "tzdata", "geonames" or "fallback" (doc/data.md).
1580 core::tz_rules rules; ///< The zone's rules, from the tier's first year on.
1581
1582 std::chrono::seconds utc_offset{}; ///< At the reference date, 12:00 UTC: east of UTC.
1583 std::string abbreviation; ///< At the reference date: "-03", "CEST".
1584 bool dst{false}; ///< At the reference date: daylight time.
1585 bool observes_dst{false}; ///< Any daylight time from the reference date on.
1586
1587 /// @brief Local time at a UTC instant.
1588 /// @param t The instant.
1589 /// @return The local time.
1590 /// @throws std::out_of_range Before the zone's data (core::tz_rules).
1591 [[nodiscard]] std::chrono::local_seconds to_local(std::chrono::sys_seconds t) const { return rules.to_local(t); }
1592
1593 /// @brief The UTC instant of a local time.
1594 /// @param t The local time.
1595 /// @param choose Which instant, for a local time that occurs twice.
1596 /// @return The instant (a local time in a gap is moved forward by the gap).
1597 /// @throws std::out_of_range Before the zone's data (core::tz_rules).
1598 [[nodiscard]] std::chrono::sys_seconds to_utc(std::chrono::local_seconds t,
1599 core::tz_choose choose = core::tz_choose::earliest) const
1600 {
1601 return rules.to_utc(t, choose);
1602 }
1603
1604 /// @brief The seed this zone was drawn with.
1605 /// @return The per-call seed (0 for find_time_zone() and time_zones()).
1606 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
1607
1608 /// @brief The seed and the data this zone was drawn from.
1609 /// @return The replay signature.
1610 [[nodiscard]] const core::seed_signature& signature() const { return _signature; }
1611
1612 /// @brief Write this zone as a JSON object; the offset as `±HH:MM`.
1613 /// @param w Writer positioned where a value is allowed.
1614 void to_json(core::json_writer& w) const
1615 {
1616 std::string offset{utc_offset.count() < 0 ? "-" : "+"};
1617 const auto total = utc_offset.count() < 0 ? -utc_offset.count() : utc_offset.count();
1618 country_detail::append_padded(offset, total / 3600, 2);
1619 offset += ':';
1620 country_detail::append_padded(offset, total / 60 % 60, 2);
1621 if (total % 60 != 0)
1622 {
1623 offset += ':';
1624 country_detail::append_padded(offset, total % 60, 2);
1625 }
1626 w.begin_object()
1627 .field("name", name)
1628 .field("country", country_code)
1629 .field("share", share)
1630 .field("basis", basis)
1631 .field("utc_offset", offset)
1632 .field("abbreviation", abbreviation)
1633 .field("dst", dst)
1634 .field("observes_dst", observes_dst)
1635 .key("signature");
1636 _signature.to_json(w);
1637 w.end_object();
1638 }
1639
1640 /// @brief This zone as a JSON document.
1641 /// @return Compact JSON.
1642 [[nodiscard]] std::string to_json() const
1643 {
1644 core::json_writer w;
1645 to_json(w);
1646 return w.str();
1647 }
1648
1649 /// @brief Implicit conversion to std::string.
1650 /// @return The IANA name.
1651 operator std::string() const // NOLINT(hicpp-explicit-conversions)
1652 {
1653 return name;
1654 }
1655
1656 /// @brief Stream the IANA name.
1657 /// @param os Output stream.
1658 /// @param z Zone to stream.
1659 /// @return The stream.
1660 friend std::ostream& operator<<(std::ostream& os, const time_zone& z)
1661 {
1662 os << z.name;
1663 return os;
1664 }
1665
1666 private:
1667 core::seed_signature _signature;
1668 friend class cntg;
1669};
1670
1671/// @brief What cntg::holidays() returns.
1673{
1674 std::string country; ///< ISO 3166-1 alpha-2 code, any case; required.
1675 int year{}; ///< The year; required.
1676 bool optional{false}; ///< Include the days off for some (holiday_kind::optional).
1677 bool observances{false}; ///< Include the days that are not days off (holiday_kind::observance).
1678};
1679
1680/// @brief One day of a holiday in a year.
1682{
1683 std::chrono::year_month_day date{}; ///< The day itself.
1684 std::optional<std::chrono::year_month_day> observed; ///< Only when a weekend rule moves the day off.
1685 std::string key; ///< Stable: "christmas_day", "eid_al_fitr_holiday".
1686 std::string name; ///< In the country's language, as python-holidays writes it: "Natal".
1687 std::string name_en; ///< In English: "Christmas Day".
1688 holiday_kind kind{holiday_kind::public_}; ///< The kind of day.
1689 unsigned day{1}; ///< 1-based day of a multi-day holiday (Eid's second day is 2).
1690
1691 /// @brief Write this holiday as a JSON object, dates as `YYYY-MM-DD`.
1692 /// @param w Writer positioned where a value is allowed.
1693 void to_json(core::json_writer& w) const
1694 {
1695 w.begin_object().field("date", country_detail::iso_date(date)).key("observed");
1696 if (observed) w.value(country_detail::iso_date(*observed));
1697 else w.value(nullptr);
1698 w.field("key", key)
1699 .field("name", name)
1700 .field("name_en", name_en)
1701 .field("kind", country_detail::kind_name(kind))
1702 .field("day", day)
1703 .end_object();
1704 }
1705
1706 /// @brief This holiday as a JSON document.
1707 /// @return Compact JSON.
1708 [[nodiscard]] std::string to_json() const
1709 {
1710 core::json_writer w;
1711 to_json(w);
1712 return w.str();
1713 }
1714};
1715
1716/// @brief Country generator that produces random countries from
1717/// aggregated open-data sources.
1718///
1719/// Generates countries using population-weighted random selection by
1720/// default. Larger countries are proportionally more likely to be
1721/// selected, mirroring real-world demographic distributions.
1722///
1723/// Next to the countries it holds each country's locales (with their CLDR
1724/// formats), IANA time zones, currency minor units and national holidays,
1725/// in their own tables (country_table), which load with the tier when they
1726/// are there.
1727///
1728/// Output is identical on every compiler and platform (dasmig-core).
1729///
1730/// Can be used as a singleton via instance() or constructed independently.
1731///
1732/// @par Thread safety
1733/// Each instance is independent. Concurrent calls to the unseeded getters
1734/// on the **same** instance require external synchronization. Calls with
1735/// an explicit seed and the lookups (find_*, locales(), time_zones(),
1736/// holidays(), is_holiday()) only read data and may run concurrently.
1737/// load() and extend() must not run concurrently with any of them.
1738class cntg
1739{
1740 public:
1741 /// @brief Default constructor — creates an empty generator with no data.
1742 cntg() = default;
1743
1744 cntg(const cntg&) = delete; ///< Not copyable.
1745 cntg& operator=(const cntg&) = delete; ///< Not copyable.
1746 cntg(cntg&&) noexcept = default; ///< Move constructor.
1747 cntg& operator=(cntg&&) noexcept = default; ///< Move assignment.
1748 ~cntg() = default; ///< Default destructor.
1749
1750 /// @brief Access the global singleton instance.
1751 ///
1752 /// Loads the `lite` tier (or `full` if `lite` is missing) on first
1753 /// access, from compiled-in data or an auto-probed `resources/`.
1754 /// @return Reference to the global cntg instance.
1755 static cntg& instance()
1756 {
1757 static cntg inst{auto_probe_tag{}};
1758 return inst;
1759 }
1760
1761 // -- Generation -------------------------------------------------------
1762
1763 /// @brief Generate a random country with per-call selection options.
1764 ///
1765 /// Draws a call seed from the internal random engine. Replay with the
1766 /// same request, the returned country's seed and the same data.
1767 /// @param request Region filter and selection mode for this call.
1768 /// @return A country object with all fields.
1769 /// @throws std::runtime_error If no data has been loaded.
1770 /// @throws std::invalid_argument If no countries match the requested region.
1771 [[nodiscard]] country get_country(const country_request& request)
1772 {
1773 return get_country(request, _engine());
1774 }
1775
1776 /// @brief Generate a deterministic country with per-call selection options.
1777 ///
1778 /// Does not advance the internal random engine. Replay with the same
1779 /// request, seed and data.
1780 /// @param request Region filter and selection mode for this call.
1781 /// @param call_seed Seed for reproducible results.
1782 /// @return A country object with all fields.
1783 /// @throws std::runtime_error If no data has been loaded.
1784 /// @throws std::invalid_argument If no countries match the requested region.
1785 [[nodiscard]] country get_country(const country_request& request,
1786 std::uint64_t call_seed) const
1787 {
1788 require_data_();
1789 if (!request.region)
1790 {
1791 auto r = core::rng{call_seed}.split("country");
1792 const auto idx = request.weighted
1793 ? _distribution(r)
1794 : static_cast<std::size_t>(core::bounded(r, _countries.size()));
1795 return result_(_countries[idx], call_seed);
1796 }
1797
1798 const auto it = _region_index.find(*request.region);
1799 if (it == _region_index.end())
1800 {
1801 throw std::invalid_argument("No countries found for region: " + *request.region);
1802 }
1803 const auto& entry = it->second;
1804 auto r = core::rng{call_seed}.split("country");
1805 const auto selected = request.weighted
1806 ? entry.distribution(r)
1807 : static_cast<std::size_t>(core::bounded(r, entry.indices.size()));
1808 return result_(_countries[entry.indices[selected]], call_seed);
1809 }
1810
1811 // -- Batch generation -------------------------------------------------
1812
1813 /// @brief Generate a batch of country results from a seed drawn like
1814 /// get_country() draws its own.
1815 ///
1816 /// Item `i` is `get_country(request, core::item_seed(batch_seed, options.first + i))`
1817 /// for the drawn batch seed; see the seeded overload.
1818 ///
1819 /// @param request What every item is generated from.
1820 /// @param options Count, first index, threads and uniqueness.
1821 /// @return The results, in index order.
1822 [[nodiscard]] std::vector<country> get_country_batch(const country_request& request,
1823 const core::batch_options& options)
1824 {
1825 return get_country_batch(request, options, _engine());
1826 }
1827
1828 /// @brief Generate a batch of country results, deterministically.
1829 ///
1830 /// Item `i` is exactly `get_country(request, core::item_seed(batch_seed,
1831 /// options.first + i))`, so an item replays on its own, a range can be
1832 /// generated directly, two batches that follow each other equal one, and
1833 /// the output does not depend on `options.threads`.
1834 ///
1835 /// @param request What every item is generated from.
1836 /// @param options Count, first index, threads and uniqueness (see
1837 /// core::unique_rule; the rule must be made for `country`).
1838 /// @param batch_seed Seed of the whole batch.
1839 /// @return The results, in index order.
1840 /// @throws std::invalid_argument If the request or options are invalid.
1841 /// @throws std::runtime_error If no data has been loaded, or a unique
1842 /// batch runs out of attempts.
1843 [[nodiscard]] std::vector<country> get_country_batch(const country_request& request,
1844 const core::batch_options& options,
1845 std::uint64_t batch_seed) const
1846 {
1847 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
1848 { return get_country(request, item_seed); });
1849 }
1850
1851 /// @brief Stream a batch of country results to a sink without holding the
1852 /// whole batch.
1853 ///
1854 /// Generates the same items as get_country_batch() and calls
1855 /// `sink(index, item)` in index order on the calling thread; `index`
1856 /// includes `options.first`.
1857 ///
1858 /// @tparam Sink Callable taking `(std::uint64_t, country&)`; for example
1859 /// core::jsonl_writer or core::csv_writer.
1860 /// @param request What every item is generated from.
1861 /// @param options Count, first index, threads and uniqueness.
1862 /// @param batch_seed Seed of the whole batch.
1863 /// @param sink Receives the items.
1864 template <typename Sink>
1865 void for_each_country(const country_request& request, const core::batch_options& options,
1866 std::uint64_t batch_seed, Sink&& sink) const
1867 {
1868 core::for_each_batch(options, batch_seed,
1869 [&](std::uint64_t item_seed) { return get_country(request, item_seed); },
1870 std::forward<Sink>(sink));
1871 }
1872
1873 /// @brief Stream a batch of country results with a batch seed drawn like
1874 /// get_country() draws its own.
1875 /// @tparam Sink Callable taking `(std::uint64_t, country&)`.
1876 /// @param request What every item is generated from.
1877 /// @param options Count, first index, threads and uniqueness.
1878 /// @param sink Receives the items.
1879 template <typename Sink>
1880 void for_each_country(const country_request& request, const core::batch_options& options,
1881 Sink&& sink)
1882 {
1883 for_each_country(request, options, _engine(), std::forward<Sink>(sink));
1884 }
1885
1886
1887 /// @brief Look up a country by its ISO 3166-1 alpha-2 code.
1888 /// @param cca2 Two-letter country code (e.g. "US", "BR").
1889 /// @return The matching country object (seed 0).
1890 /// @throws std::runtime_error If no data has been loaded.
1891 /// @throws std::invalid_argument If no country matches the code.
1892 [[nodiscard]] country find_country(const std::string& cca2) const
1893 {
1894 require_data_();
1895 const auto it = _cca2_index.find(cca2);
1896 if (it == _cca2_index.end())
1897 {
1898 throw std::invalid_argument("No country found for CCA2 code: " + cca2);
1899 }
1900 return result_(_countries[it->second], 0);
1901 }
1902
1903 /// @brief Draw a language a person of a country speaks, in proportion to
1904 /// the share of the population that speaks it.
1905 ///
1906 /// Falls back to the first of the country's `languages` when the data has
1907 /// no shares for it. A pure function of the data, the country and the seed.
1908 /// @param cca2 ISO 3166-1 alpha-2 code.
1909 /// @param call_seed The call seed.
1910 /// @return The language's English name.
1911 /// @throws std::runtime_error If no data has been loaded.
1912 /// @throws std::invalid_argument If no country matches the code, or the
1913 /// country has no language data.
1914 [[nodiscard]] std::string get_language(const std::string& cca2, std::uint64_t call_seed) const
1915 {
1916 const auto c = find_country(cca2);
1917 const auto shares = c.language_distribution();
1918 if (shares.empty())
1919 {
1920 auto list = c.language_list();
1921 if (list.empty()) throw std::invalid_argument("No language data for CCA2 code: " + cca2);
1922 return std::move(list.front());
1923 }
1924 std::vector<double> weights;
1925 weights.reserve(shares.size());
1926 for (const auto& share : shares) weights.push_back(share.percent);
1927 auto r = core::rng{call_seed}.split("language");
1928 return shares[core::discrete{weights}(r)].name;
1929 }
1930
1931 // -- Locales ------------------------------------------------------------
1932
1933 /// @brief Draw a locale of a country, by speakers.
1934 ///
1935 /// Draws a call seed from the internal random engine. Replay with the
1936 /// same request, the returned locale's seed and the same data.
1937 /// @param request The country, the language and the selection mode.
1938 /// @return The locale.
1939 /// @throws std::runtime_error If the locale tables are not loaded.
1940 /// @throws std::invalid_argument As the seeded overload.
1941 [[nodiscard]] locale get_locale(const locale_request& request) { return get_locale(request, _engine()); }
1942
1943 /// @brief Draw a locale of a country, by speakers, deterministically.
1944 ///
1945 /// The country is the request's, or else drawn from the `"country"`
1946 /// stream exactly as get_country() draws it, so
1947 /// `get_locale({}, s).country_code == get_country({}, s).cca2` for every
1948 /// country with a locale (a drawn country with none, Antarctica in
1949 /// `full`, is replaced by a draw among the countries that have one). With
1950 /// only `language` set, the country is drawn among those with a locale of
1951 /// that language, by speakers. The tag is drawn from the `"locale"`
1952 /// stream, weighted by its share.
1953 ///
1954 /// @param request The country, the language and the selection mode.
1955 /// @param call_seed Seed for reproducible results.
1956 /// @return The locale.
1957 /// @throws std::runtime_error If no countries or no locale tables are loaded.
1958 /// @throws std::invalid_argument For an unknown country, a language no
1959 /// country has a locale of, or a country with no locale (of the
1960 /// language, when set).
1961 [[nodiscard]] locale get_locale(const locale_request& request, std::uint64_t call_seed) const
1962 {
1963 require_locale_table_(country_table::locales);
1964 const auto& m = _locale;
1965 std::optional<std::string> language;
1966 if (request.language) language = country_detail::lower(*request.language);
1967 bool drawn = false;
1968 std::string cc;
1969 if (request.country)
1970 {
1971 cc = known_country_(*request.country);
1972 }
1973 else if (!language)
1974 {
1975 cc = draw_country_(call_seed, request.weighted, m.locale_countries, m.locales_by_country, "locale_country");
1976 drawn = true;
1977 }
1978 else
1979 {
1980 const auto it = m.locale_languages.find(*language);
1981 if (it == m.locale_languages.end()) throw std::invalid_argument("No locale for language: " + *language);
1982 require_data_();
1983 auto r = core::rng{call_seed}.split("country");
1984 const auto j = request.weighted ? it->second.weighted(r)
1985 : static_cast<std::size_t>(core::bounded(r, it->second.items.size()));
1986 cc = _countries[it->second.items[j]].cca2;
1987 drawn = true;
1988 }
1989 const auto it = m.locales_by_country.find(cc);
1990 if (it == m.locales_by_country.end()) throw std::invalid_argument("No locale data for country: " + cc);
1991 auto r = core::rng{call_seed}.split("locale");
1992 std::size_t index = 0;
1993 if (!language)
1994 {
1995 const auto& items = it->second.items;
1996 index = items[request.weighted ? it->second.weighted(r)
1997 : static_cast<std::size_t>(core::bounded(r, items.size()))];
1998 }
1999 else
2000 {
2001 std::vector<std::size_t> items;
2002 std::vector<double> weights;
2003 for (const auto i : it->second.items)
2004 {
2005 if (m.locales[i].language != *language) continue;
2006 items.push_back(i);
2007 weights.push_back(m.locales[i].share);
2008 }
2009 if (items.empty())
2010 {
2011 throw std::invalid_argument("No locale of language " + *language + " for country: " + cc);
2012 }
2013 index = items[request.weighted ? core::discrete{weights}(r)
2014 : static_cast<std::size_t>(core::bounded(r, items.size()))];
2015 }
2016 locale out = m.locales[index];
2017 out._signature = {.seed = call_seed,
2018 .data = drawn ? core::hash_combine(locale_hash_(), data_hash()) : locale_hash_()};
2019 return out;
2020 }
2021
2022 /// @brief Generate a batch of locales from a seed drawn like get_locale()
2023 /// draws its own.
2024 /// @param request What every item is generated from.
2025 /// @param options Count, first index, threads and uniqueness.
2026 /// @return The results, in index order.
2027 [[nodiscard]] std::vector<locale> get_locale_batch(const locale_request& request,
2028 const core::batch_options& options)
2029 {
2030 return get_locale_batch(request, options, _engine());
2031 }
2032
2033 /// @brief Generate a batch of locales, deterministically: item `i` is
2034 /// `get_locale(request, core::item_seed(batch_seed, options.first + i))`.
2035 /// @param request What every item is generated from.
2036 /// @param options Count, first index, threads and uniqueness.
2037 /// @param batch_seed Seed of the whole batch.
2038 /// @return The results, in index order.
2039 /// @throws std::invalid_argument If the request or options are invalid.
2040 /// @throws std::runtime_error If no data is loaded, or a unique batch runs
2041 /// out of attempts.
2042 [[nodiscard]] std::vector<locale> get_locale_batch(const locale_request& request,
2043 const core::batch_options& options,
2044 std::uint64_t batch_seed) const
2045 {
2046 return core::run_batch(options, batch_seed,
2047 [&](std::uint64_t item_seed) { return get_locale(request, item_seed); });
2048 }
2049
2050 /// @brief Stream a batch of locales to a sink without holding the batch.
2051 /// @tparam Sink Callable taking `(std::uint64_t, locale&)`.
2052 /// @param request What every item is generated from.
2053 /// @param options Count, first index, threads and uniqueness.
2054 /// @param batch_seed Seed of the whole batch.
2055 /// @param sink Receives the items, in index order.
2056 template <typename Sink>
2057 void for_each_locale(const locale_request& request, const core::batch_options& options,
2058 std::uint64_t batch_seed, Sink&& sink) const
2059 {
2060 core::for_each_batch(options, batch_seed,
2061 [&](std::uint64_t item_seed) { return get_locale(request, item_seed); },
2062 std::forward<Sink>(sink));
2063 }
2064
2065 /// @brief Stream a batch of locales with a batch seed drawn like
2066 /// get_locale() draws its own.
2067 /// @tparam Sink Callable taking `(std::uint64_t, locale&)`.
2068 /// @param request What every item is generated from.
2069 /// @param options Count, first index, threads and uniqueness.
2070 /// @param sink Receives the items, in index order.
2071 template <typename Sink>
2072 void for_each_locale(const locale_request& request, const core::batch_options& options, Sink&& sink)
2073 {
2074 for_each_locale(request, options, _engine(), std::forward<Sink>(sink));
2075 }
2076
2077 /// @brief Look up a locale by its tag.
2078 ///
2079 /// Accepts any case, `_` for `-`, and a script where the tag has none
2080 /// (`pt-Latn-BR` is `pt-BR`): a script the table has no row of for that
2081 /// language and country is taken as the language's likely one.
2082 ///
2083 /// @param tag BCP 47 tag.
2084 /// @return The locale (seed 0).
2085 /// @throws std::runtime_error If the locale tables are not loaded.
2086 /// @throws std::invalid_argument If no locale has the tag.
2087 [[nodiscard]] locale find_locale(std::string_view tag) const
2088 {
2089 require_locale_table_(country_table::locales);
2090 std::string key = country_detail::lower(tag);
2091 std::ranges::replace(key, '_', '-');
2092 auto it = _locale.locale_by_tag.find(key);
2093 if (it == _locale.locale_by_tag.end())
2094 {
2095 // language-Script-REGION -> language-REGION, when that row has no script.
2096 const auto first = key.find('-');
2097 const auto second = first == std::string::npos ? std::string::npos : key.find('-', first + 1);
2098 if (second == first + 5 && key.find('-', second + 1) == std::string::npos)
2099 {
2100 const auto shorter = _locale.locale_by_tag.find(key.substr(0, first) + key.substr(second));
2101 if (shorter != _locale.locale_by_tag.end() && _locale.locales[shorter->second].script.empty())
2102 {
2103 it = shorter;
2104 }
2105 }
2106 }
2107 if (it == _locale.locale_by_tag.end()) throw std::invalid_argument("Unknown locale tag: " + std::string{tag});
2108 locale out = _locale.locales[it->second];
2109 out._signature = {.seed = 0, .data = locale_hash_()};
2110 return out;
2111 }
2112
2113 /// @brief Every locale of a country, largest share first, then by tag.
2114 /// @param country ISO 3166-1 alpha-2 code, any case.
2115 /// @return The locales (seed 0); empty for a country with none.
2116 /// @throws std::runtime_error If no countries or no locale tables are loaded.
2117 /// @throws std::invalid_argument For an unknown country.
2118 [[nodiscard]] std::vector<locale> locales(std::string_view country) const
2119 {
2120 require_locale_table_(country_table::locales);
2121 const auto cc = known_country_(country);
2122 std::vector<locale> out;
2123 if (const auto it = _locale.locales_by_country.find(cc); it != _locale.locales_by_country.end())
2124 {
2125 for (const auto i : it->second.items)
2126 {
2127 out.push_back(_locale.locales[i]);
2128 out.back()._signature = {.seed = 0, .data = locale_hash_()};
2129 }
2130 }
2131 return out;
2132 }
2133
2134 // -- Time zones -----------------------------------------------------------
2135
2136 /// @brief Draw a time zone of a country, by population.
2137 /// @param request The country, the reference date and the selection mode.
2138 /// @return The zone.
2139 /// @throws std::runtime_error If the zone tables are not loaded.
2140 /// @throws std::invalid_argument As the seeded overload.
2141 [[nodiscard]] time_zone get_time_zone(const time_zone_request& request)
2142 {
2143 return get_time_zone(request, _engine());
2144 }
2145
2146 /// @brief Draw a time zone of a country, by population, deterministically.
2147 ///
2148 /// The country is the request's, or else drawn from the `"country"`
2149 /// stream as get_country() draws it; the zone is drawn from the
2150 /// `"time_zone"` stream, weighted by its share of the population. The
2151 /// offset, abbreviation and daylight flag are those at the reference
2152 /// date, 12:00 UTC.
2153 ///
2154 /// @param request The country, the reference date and the selection mode.
2155 /// @param call_seed Seed for reproducible results.
2156 /// @return The zone.
2157 /// @throws std::runtime_error If no countries or no zone tables are loaded.
2158 /// @throws std::invalid_argument For an unknown country, one with no zone,
2159 /// or an invalid reference date.
2160 /// @throws std::out_of_range For a reference date before the zone's data.
2161 [[nodiscard]] time_zone get_time_zone(const time_zone_request& request, std::uint64_t call_seed) const
2162 {
2163 require_locale_table_(country_table::zones);
2164 const auto& m = _locale;
2165 bool drawn = false;
2166 std::string cc;
2167 if (request.country)
2168 {
2169 cc = known_country_(*request.country);
2170 }
2171 else
2172 {
2173 cc = draw_country_(call_seed, request.weighted, m.zone_countries, m.zones_by_country, "time_zone_country");
2174 drawn = true;
2175 }
2176 const auto it = m.zones_by_country.find(cc);
2177 if (it == m.zones_by_country.end()) throw std::invalid_argument("No time zone data for country: " + cc);
2178 auto r = core::rng{call_seed}.split("time_zone");
2179 const auto& items = it->second.items;
2180 const auto index = items[request.weighted ? it->second.weighted(r)
2181 : static_cast<std::size_t>(core::bounded(r, items.size()))];
2182 return zone_at_(m.zones[index], request.reference_date,
2183 {.seed = call_seed,
2184 .data = drawn ? core::hash_combine(locale_hash_(), data_hash()) : locale_hash_()});
2185 }
2186
2187 /// @brief Generate a batch of time zones from a seed drawn like
2188 /// get_time_zone() draws its own.
2189 /// @param request What every item is generated from.
2190 /// @param options Count, first index, threads and uniqueness.
2191 /// @return The results, in index order.
2192 [[nodiscard]] std::vector<time_zone> get_time_zone_batch(const time_zone_request& request,
2193 const core::batch_options& options)
2194 {
2195 return get_time_zone_batch(request, options, _engine());
2196 }
2197
2198 /// @brief Generate a batch of time zones, deterministically: item `i` is
2199 /// `get_time_zone(request, core::item_seed(batch_seed, options.first + i))`.
2200 /// @param request What every item is generated from.
2201 /// @param options Count, first index, threads and uniqueness.
2202 /// @param batch_seed Seed of the whole batch.
2203 /// @return The results, in index order.
2204 /// @throws std::invalid_argument If the request or options are invalid.
2205 /// @throws std::runtime_error If no data is loaded, or a unique batch runs
2206 /// out of attempts.
2207 [[nodiscard]] std::vector<time_zone> get_time_zone_batch(const time_zone_request& request,
2208 const core::batch_options& options,
2209 std::uint64_t batch_seed) const
2210 {
2211 return core::run_batch(options, batch_seed,
2212 [&](std::uint64_t item_seed) { return get_time_zone(request, item_seed); });
2213 }
2214
2215 /// @brief Stream a batch of time zones to a sink without holding the batch.
2216 /// @tparam Sink Callable taking `(std::uint64_t, time_zone&)`.
2217 /// @param request What every item is generated from.
2218 /// @param options Count, first index, threads and uniqueness.
2219 /// @param batch_seed Seed of the whole batch.
2220 /// @param sink Receives the items, in index order.
2221 template <typename Sink>
2222 void for_each_time_zone(const time_zone_request& request, const core::batch_options& options,
2223 std::uint64_t batch_seed, Sink&& sink) const
2224 {
2225 core::for_each_batch(options, batch_seed,
2226 [&](std::uint64_t item_seed) { return get_time_zone(request, item_seed); },
2227 std::forward<Sink>(sink));
2228 }
2229
2230 /// @brief Stream a batch of time zones with a batch seed drawn like
2231 /// get_time_zone() draws its own.
2232 /// @tparam Sink Callable taking `(std::uint64_t, time_zone&)`.
2233 /// @param request What every item is generated from.
2234 /// @param options Count, first index, threads and uniqueness.
2235 /// @param sink Receives the items, in index order.
2236 template <typename Sink>
2237 void for_each_time_zone(const time_zone_request& request, const core::batch_options& options, Sink&& sink)
2238 {
2239 for_each_time_zone(request, options, _engine(), std::forward<Sink>(sink));
2240 }
2241
2242 /// @brief Look up a time zone by its IANA name.
2243 ///
2244 /// Accepts IANA's `backward` links (`Asia/Calcutta` is `Asia/Kolkata`),
2245 /// which GeoNames and older systems write; the result's name is then the
2246 /// current one. A zone two countries share is returned with the country
2247 /// IANA lists it for.
2248 ///
2249 /// @param name IANA name.
2250 /// @param reference_date The date whose offset is reported, at 12:00 UTC.
2251 /// @return The zone (seed 0).
2252 /// @throws std::runtime_error If the zone tables are not loaded.
2253 /// @throws std::invalid_argument For an unknown name or an invalid date.
2254 /// @throws std::out_of_range For a reference date before the zone's data.
2255 [[nodiscard]] time_zone find_time_zone(std::string_view name,
2256 std::chrono::year_month_day reference_date =
2258 {
2259 require_locale_table_(country_table::zones);
2260 auto it = _locale.zone_by_name.find(name);
2261 if (it == _locale.zone_by_name.end())
2262 {
2263 if (const auto link = _locale.zone_links.find(name); link != _locale.zone_links.end())
2264 {
2265 it = _locale.zone_by_name.find(link->second);
2266 }
2267 }
2268 if (it == _locale.zone_by_name.end()) throw std::invalid_argument("Unknown time zone: " + std::string{name});
2269 return zone_at_(_locale.zones[it->second], reference_date, {.seed = 0, .data = locale_hash_()});
2270 }
2271
2272 /// @brief Every time zone of a country, largest share first, then by name,
2273 /// at the default reference date.
2274 /// @param country ISO 3166-1 alpha-2 code, any case.
2275 /// @return The zones (seed 0); empty for a country with none.
2276 /// @throws std::runtime_error If no countries or no zone tables are loaded.
2277 /// @throws std::invalid_argument For an unknown country.
2278 [[nodiscard]] std::vector<time_zone> time_zones(std::string_view country) const
2279 {
2280 require_locale_table_(country_table::zones);
2281 const auto cc = known_country_(country);
2282 std::vector<time_zone> out;
2283 if (const auto it = _locale.zones_by_country.find(cc); it != _locale.zones_by_country.end())
2284 {
2285 for (const auto i : it->second.items)
2286 {
2287 out.push_back(zone_at_(_locale.zones[i], time_zone_request::default_reference_date,
2288 {.seed = 0, .data = locale_hash_()}));
2289 }
2290 }
2291 return out;
2292 }
2293
2294 // -- Holidays and currencies ----------------------------------------------
2295
2296 /// @brief A country's holidays in a year.
2297 ///
2298 /// A lookup, not a draw: no seed. Sorted by date, then key, then day.
2299 /// Public and bank holidays always; optional days and observances when
2300 /// asked. A day off a weekend rule moves has `observed` set (design 8.4);
2301 /// a move python-holidays records but no rule gives is its own entry,
2302 /// named as python-holidays names it ("Christmas Day (observed)").
2303 ///
2304 /// @param request The country, the year and the kinds.
2305 /// @return The holidays.
2306 /// @throws std::runtime_error If no countries or no holiday tables are loaded.
2307 /// @throws std::invalid_argument For an unknown country, or one with no
2308 /// holiday data.
2309 /// @throws std::out_of_range For a year outside holiday_years().
2310 [[nodiscard]] std::vector<holiday> holidays(const holiday_request& request) const
2311 {
2312 const auto& hc = holiday_country_(request.country);
2313 check_holiday_year_(hc, request.year);
2314 return evaluate_(hc, request.year, request.optional, request.observances);
2315 }
2316
2317 /// @brief Whether a date is a holiday's date or its observed date.
2318 ///
2319 /// Looks at the holidays of the year before and after too, since an
2320 /// observed day can cross New Year (the US's New Year's Day 2022 was
2321 /// observed on 2021-12-31).
2322 ///
2323 /// @param country ISO 3166-1 alpha-2 code, any case.
2324 /// @param date The date.
2325 /// @param optional Count holiday_kind::optional days too.
2326 /// @return True for a public or bank holiday (or optional, if asked).
2327 /// @throws std::runtime_error If no countries or no holiday tables are loaded.
2328 /// @throws std::invalid_argument For an unknown country, one with no
2329 /// holiday data, or an invalid date.
2330 /// @throws std::out_of_range For a date whose year is outside holiday_years().
2331 [[nodiscard]] bool is_holiday(std::string_view country, std::chrono::year_month_day date,
2332 bool optional = false) const
2333 {
2334 if (!date.ok()) throw std::invalid_argument("is_holiday: not a valid date");
2335 const auto& hc = holiday_country_(country);
2336 const int y = static_cast<int>(date.year());
2337 check_holiday_year_(hc, y);
2338 for (int year = std::max(y - 1, hc.years.from); year <= std::min(y + 1, hc.years.to); ++year)
2339 {
2340 for (const auto& h : evaluate_(hc, year, optional, false))
2341 {
2342 if (h.date == date || h.observed == date) return true;
2343 }
2344 }
2345 return false;
2346 }
2347
2348 /// @brief The years holidays() serves for a country.
2349 /// @param country ISO 3166-1 alpha-2 code, any case.
2350 /// @return The span (python-holidays' years for the country, within 1970 to 2050).
2351 /// @throws std::runtime_error If no countries or no holiday tables are loaded.
2352 /// @throws std::invalid_argument For an unknown country, or one with no
2353 /// holiday data.
2354 [[nodiscard]] year_span holiday_years(std::string_view country) const
2355 {
2356 return holiday_country_(country).years;
2357 }
2358
2359 /// @brief The ISO 4217 minor unit of a currency (0 for JPY, 3 for KWD).
2360 /// @param code ISO 4217 alphabetic code, any case.
2361 /// @return The digits, 0 to 4.
2362 /// @throws std::runtime_error If the currency table is not loaded.
2363 /// @throws std::invalid_argument For an unknown code.
2364 [[nodiscard]] std::uint8_t currency_digits(std::string_view code) const
2365 {
2366 require_locale_table_(country_table::currencies);
2367 const auto it = _locale.currencies.find(country_detail::upper(code));
2368 if (it == _locale.currencies.end()) throw std::invalid_argument("Unknown currency code: " + std::string{code});
2369 return it->second.digits;
2370 }
2371
2372 /// @brief Whether any locale table (locales, zones, currencies, holidays)
2373 /// is loaded.
2374 /// @return False when only `countries.tsv` is.
2375 [[nodiscard]] bool has_locale_data() const
2376 {
2377 return std::ranges::any_of(_locale_tables, [](const core::layered_table& t) { return !t.empty(); });
2378 }
2379
2380 // -- Data management --------------------------------------------------
2381
2382 /// @brief Check whether any data has been loaded.
2383 /// @return True when at least one country is loaded.
2384 [[nodiscard]] bool has_data() const
2385 {
2386 return !_countries.empty();
2387 }
2388
2389 /// @brief Return the number of loaded countries.
2390 /// @return The count.
2391 [[nodiscard]] std::size_t country_count() const
2392 {
2393 return _countries.size();
2394 }
2395
2396 /// @brief Load country data from a TSV file or a tier directory,
2397 /// replacing all data and layers loaded before.
2398 ///
2399 /// A file is `countries.tsv` (see doc/data.md); the locale tables are
2400 /// then left as they are. A directory such as `resources/lite` must hold
2401 /// `countries.tsv` and loads every other table it holds; the ones it does
2402 /// not hold are cleared. Any other path is ignored.
2403 ///
2404 /// @param path Path to the TSV file or the directory.
2405 /// @throws core::tsv_error If a file is malformed; the data is then
2406 /// unchanged.
2407 void load(const std::filesystem::path& path)
2408 {
2409 std::error_code ec;
2410 if (std::filesystem::is_directory(path, ec))
2411 {
2412 const auto countries = path / data_file_;
2413 if (!std::filesystem::is_regular_file(countries, ec)) return;
2414 replace_all_(core::tsv_document::load(countries), read_locale_directory_(path));
2415 return;
2416 }
2417 if (!std::filesystem::is_regular_file(path, ec)) return;
2418 (void)extend(core::tsv_document::load(path), core::layer_mode::replace);
2419 }
2420
2421 /// @brief Load a dataset tier, replacing all data and layers loaded
2422 /// before.
2423 ///
2424 /// Loads `countries.tsv`, then every locale table of the tier it finds
2425 /// (a missing one is cleared). Each uses the compiled-in copy when the
2426 /// library is built with `DASMIG_COUNTRY_EMBED` (the countries) or
2427 /// `DASMIG_COUNTRY_EMBED_LOCALE` (the locale tables), otherwise searches
2428 /// `resources/`, `../resources`, `country-generator/resources` and
2429 /// `../country-generator/resources`. The locale tables are read from the
2430 /// first of those that has both the tier's `countries.tsv` and its
2431 /// `locales.tsv`, so another library's `locales.tsv` is never read.
2432 ///
2433 /// @param tier The dataset size to load.
2434 /// @return `true` if the tier's `countries.tsv` was found and loaded.
2435 /// @throws core::tsv_error If a file is malformed; the data is then
2436 /// unchanged.
2437 [[nodiscard]] bool load(dataset tier)
2438 {
2439 const std::string dir{core::tier_name(tier)};
2440 const std::string path = dir + "/" + std::string{data_file_};
2441 auto doc = core::load_resource(path, embedded_files_(),
2442 core::find_resources(library_name_, path));
2443 if (!doc) return false;
2444 const auto root = find_locale_resources_(dir);
2445 locale_documents docs;
2446 for (const auto t : locale_tables_)
2447 {
2448 docs.at(index_(t)) = core::load_resource(dir + "/" + std::string{file_name_(t)}, embedded_locale_files_(), root);
2449 }
2450 replace_all_(std::move(*doc), std::move(docs));
2451 return true;
2452 }
2453
2454 /// @brief Add a data layer from a TSV file with the `countries.tsv`
2455 /// columns.
2456 ///
2457 /// Rows are keyed by `cca2`. In the default override mode a row
2458 /// replaces the loaded country with the same code and new codes are
2459 /// added; a row with `disabled` set to 1 removes its country.
2460 ///
2461 /// @param file The layer file.
2462 /// @param mode How the layer combines with the data loaded before.
2463 /// @return What the layer did.
2464 /// @throws core::tsv_error If the file cannot be read or a row is
2465 /// invalid; the data is then unchanged.
2466 const core::layer_info& extend(const std::filesystem::path& file,
2467 core::layer_mode mode = core::layer_mode::override)
2468 {
2469 return extend(core::tsv_document::load(file), mode);
2470 }
2471
2472 /// @brief Add a data layer from TSV text.
2473 /// @param text TSV with the `countries.tsv` columns.
2474 /// @param source Name used in errors.
2475 /// @param mode How the layer combines with the data loaded before.
2476 /// @return What the layer did.
2477 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
2478 const core::layer_info& extend_from_string(std::string_view text, std::string source,
2479 core::layer_mode mode = core::layer_mode::override)
2480 {
2481 return extend(core::tsv_document::parse(text, std::move(source)), mode);
2482 }
2483
2484 /// @brief Add a data layer.
2485 /// @param doc TSV with the `countries.tsv` columns.
2486 /// @param mode How the layer combines with the data loaded before.
2487 /// @return What the layer did.
2488 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
2489 const core::layer_info& extend(core::tsv_document doc,
2490 core::layer_mode mode = core::layer_mode::override)
2491 {
2492 const auto& info = _table.extend(std::move(doc), mode, [](const core::tsv_row& row) {
2493 (void)parse_(row);
2494 });
2495 rebuild_();
2496 return info;
2497 }
2498
2499 /// @brief Add a data layer to one table.
2500 ///
2501 /// Rows are keyed by the table's key columns (doc/data.md). In the
2502 /// default override mode a row replaces the loaded row with the same key
2503 /// and new keys are added; a row with `disabled` set to 1 removes its key.
2504 /// A locale table's rows must name loaded countries, and refer to loaded
2505 /// rows of the other tables (a locale's currency, a link's zone, a
2506 /// holiday's event dates and span).
2507 ///
2508 /// @param which The table.
2509 /// @param doc TSV with that table's columns.
2510 /// @param mode How the layer combines with the data loaded before.
2511 /// @return What the layer did.
2512 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
2513 const core::layer_info& extend(country_table which, core::tsv_document doc,
2514 core::layer_mode mode = core::layer_mode::override)
2515 {
2516 if (which == country_table::countries) return extend(std::move(doc), mode);
2517 locale_documents docs;
2518 docs.at(index_(which)).emplace(std::move(doc));
2519 apply_locale_(std::move(docs), mode, false);
2520 return _locale_tables.at(index_(which)).layers().back();
2521 }
2522
2523 /// @brief Add a data layer to one table from a TSV file.
2524 /// @param which The table.
2525 /// @param file The layer file.
2526 /// @param mode How the layer combines with the data loaded before.
2527 /// @return What the layer did.
2528 /// @throws core::tsv_error If the file cannot be read or a row is
2529 /// invalid; the data is then unchanged.
2530 const core::layer_info& extend(country_table which, const std::filesystem::path& file,
2531 core::layer_mode mode = core::layer_mode::override)
2532 {
2533 return extend(which, core::tsv_document::load(file), mode);
2534 }
2535
2536 /// @brief Add a data layer to one table from TSV text.
2537 /// @param which The table.
2538 /// @param text TSV with that table's columns.
2539 /// @param source Name used in errors.
2540 /// @param mode How the layer combines with the data loaded before.
2541 /// @return What the layer did.
2542 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
2543 const core::layer_info& extend_from_string(country_table which, std::string_view text, std::string source,
2544 core::layer_mode mode = core::layer_mode::override)
2545 {
2546 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
2547 }
2548
2549 /// @brief The data layers of `countries.tsv` in effect, in order.
2550 /// @return The layer records.
2551 [[nodiscard]] const std::vector<core::layer_info>& layers() const
2552 {
2553 return _table.layers();
2554 }
2555
2556 /// @brief The data layers of one table in effect, in order.
2557 /// @param which The table.
2558 /// @return The layer records.
2559 [[nodiscard]] const std::vector<core::layer_info>& layers(country_table which) const
2560 {
2561 if (which == country_table::countries) return _table.layers();
2562 return _locale_tables.at(index_(which)).layers();
2563 }
2564
2565 /// @brief Hash of the loaded `countries.tsv`, recorded in each country's
2566 /// signature() (the locale tables have their own, which locales,
2567 /// zones and holidays record).
2568 /// @return The hash; 0 when nothing is loaded.
2569 [[nodiscard]] std::uint64_t data_hash() const { return _table.data_hash(); }
2570
2571 private:
2572 static constexpr std::string_view library_name_ = "country-generator";
2573 static constexpr std::string_view data_file_ = "countries.tsv";
2574
2575 static core::table_schema schema_()
2576 {
2577 return {.key = {"cca2"},
2578 .required = {"cca3", "name_common", "region", "latitude", "longitude",
2579 "area", "population", "landlocked", "independent", "un_member"},
2580 .optional = {"ccn3", "name_official", "capital", "subregion", "continent",
2581 "languages", "language_shares", "currency_code", "currency_name",
2582 "currency_symbol", "borders", "timezones", "driving_side", "tld", "idd_root",
2583 "idd_suffix", "demonym_m", "demonym_f", "flag_emoji",
2584 "income_level", "start_of_week"}};
2585 }
2586
2587 // Parse and check a row; throws core::tsv_error located at the row.
2588 static country parse_(const core::tsv_row& row)
2589 {
2590 country c;
2591 c.cca2 = row.get<std::string>("cca2");
2592 c.cca3 = row.get<std::string>("cca3");
2593 c.ccn3 = row.get_or<std::string>("ccn3", "");
2594 c.name_common = row.get<std::string>("name_common");
2595 c.name_official = row.get_or<std::string>("name_official", "");
2596 c.capital = row.get_or<std::string>("capital", "");
2597 c.region = row.get<std::string>("region");
2598 c.subregion = row.get_or<std::string>("subregion", "");
2599 c.continent = row.get_or<std::string>("continent", "");
2600 c.latitude = row.get<double>("latitude");
2601 c.longitude = row.get<double>("longitude");
2602 if (c.latitude < -90.0 || c.latitude > 90.0)
2603 {
2604 throw row.error("latitude", "must be between -90 and 90");
2605 }
2606 if (c.longitude < -180.0 || c.longitude > 180.0)
2607 {
2608 throw row.error("longitude", "must be between -180 and 180");
2609 }
2610 // The source marks an unknown area as -1 (Svalbard and Jan Mayen);
2611 // it reads as 0, as in v1.
2612 c.area = static_cast<std::uint64_t>(std::max<std::int64_t>(row.get<std::int64_t>("area"), 0));
2613 c.population = row.get<std::uint64_t>("population");
2614 c.landlocked = row.get<bool>("landlocked");
2615 c.independent = row.get<bool>("independent");
2616 c.un_member = row.get<bool>("un_member");
2617 c.languages = row.get_or<std::string>("languages", "");
2618 c.language_shares = row.get_or<std::string>("language_shares", "");
2619 if (!country::for_each_share_(c.language_shares, [](std::string_view, double) {}))
2620 {
2621 throw row.error("language_shares", "must be Name:percent pairs separated by ';', each percent above 0 and at most 100");
2622 }
2623 c.currency_code = row.get_or<std::string>("currency_code", "");
2624 c.currency_name = row.get_or<std::string>("currency_name", "");
2625 c.currency_symbol = row.get_or<std::string>("currency_symbol", "");
2626 c.borders = row.get_or<std::string>("borders", "");
2627 c.timezones = row.get_or<std::string>("timezones", "");
2628 c.driving_side = row.get_or<std::string>("driving_side", "");
2629 c.tld = row.get_or<std::string>("tld", "");
2630 c.idd_root = row.get_or<std::string>("idd_root", "");
2631 c.idd_suffix = row.get_or<std::string>("idd_suffix", "");
2632 c.demonym_m = row.get_or<std::string>("demonym_m", "");
2633 c.demonym_f = row.get_or<std::string>("demonym_f", "");
2634 c.flag_emoji = row.get_or<std::string>("flag_emoji", "");
2635 c.income_level = row.get_or<std::string>("income_level", "");
2636 c.start_of_week = row.get_or<std::string>("start_of_week", "");
2637 return c;
2638 }
2639
2640 // Population weight; every country keeps a chance even with population 0.
2641 static double weight_(const country& c)
2642 {
2643 return static_cast<double>(std::max<std::uint64_t>(c.population, 1));
2644 }
2645
2646 struct region_entry
2647 {
2648 std::vector<std::size_t> indices;
2649 core::discrete distribution{1.0};
2650 };
2651
2652 core::layered_table _table{schema_()};
2653 std::vector<country> _countries; // in table row order
2654 core::discrete _distribution{1.0}; // replaced in rebuild_() when data is loaded
2655 std::map<std::string, region_entry, std::less<>> _region_index;
2656 std::map<std::string, std::size_t, std::less<>> _cca2_index;
2657 core::rng _engine{core::rng::from_entropy()};
2658
2659 struct auto_probe_tag {};
2660
2661 explicit cntg(auto_probe_tag /*tag*/)
2662 {
2663 if (!load(dataset::lite)) (void)load(dataset::full);
2664 }
2665
2666 static std::span<const core::embedded_file> embedded_files_()
2667 {
2668#ifdef DASMIG_COUNTRY_EMBED
2669 return embedded::countrygen::files;
2670#else
2671 return {};
2672#endif
2673 }
2674
2675 void require_data_() const
2676 {
2677 if (_countries.empty())
2678 {
2679 throw std::runtime_error("No country data loaded. Call load() first.");
2680 }
2681 }
2682
2683 [[nodiscard]] country result_(const country& c, std::uint64_t call_seed) const
2684 {
2685 country result = c;
2686 result._signature = {.seed = call_seed, .data = data_hash()};
2687 return result;
2688 }
2689
2690 // Rebuilds the countries' indexes and, unless with_locale is false, the locale model on them.
2691 void rebuild_(bool with_locale = true)
2692 {
2693 std::vector<country> countries;
2694 countries.reserve(_table.size());
2695 for (const auto& row : _table.rows()) countries.push_back(parse_(row));
2696
2697 core::discrete distribution{1.0};
2698 std::map<std::string, region_entry, std::less<>> regions;
2699 std::map<std::string, std::size_t, std::less<>> cca2_index;
2700 if (!countries.empty())
2701 {
2702 std::vector<double> weights;
2703 weights.reserve(countries.size());
2704 std::map<std::string, std::vector<double>, std::less<>> region_weights;
2705 for (std::size_t i = 0; i < countries.size(); ++i)
2706 {
2707 const auto& c = countries[i];
2708 weights.push_back(weight_(c));
2709 regions[c.region].indices.push_back(i);
2710 region_weights[c.region].push_back(weight_(c));
2711 cca2_index.emplace(c.cca2, i);
2712 }
2713 distribution = core::discrete{weights};
2714 for (auto& [name, entry] : regions)
2715 {
2716 entry.distribution = core::discrete{region_weights[name]};
2717 }
2718 }
2719 _countries = std::move(countries);
2720 _distribution = std::move(distribution);
2721 _region_index = std::move(regions);
2722 _cca2_index = std::move(cca2_index);
2723 if (with_locale) _locale = build_locale_model_(_locale_tables, false);
2724 }
2725
2726 // -- Locale tables ----------------------------------------------------------
2727
2728 static constexpr std::size_t locale_table_count_ = 7;
2729 static constexpr std::array<country_table, locale_table_count_> locale_tables_{
2730 country_table::locales, country_table::zones, country_table::zone_links, country_table::currencies,
2731 country_table::holidays, country_table::holiday_events, country_table::holiday_years};
2732
2733 using locale_documents = std::array<std::optional<core::tsv_document>, locale_table_count_>;
2734 using locale_table_set = std::array<core::layered_table, locale_table_count_>;
2735
2736 static constexpr std::size_t index_(country_table t) { return static_cast<std::size_t>(t) - 1; }
2737
2738 static constexpr std::string_view file_name_(country_table t)
2739 {
2740 switch (t)
2741 {
2742 case country_table::countries: return "countries.tsv";
2743 case country_table::locales: return "locales.tsv";
2744 case country_table::zones: return "zones.tsv";
2745 case country_table::zone_links: return "zone_links.tsv";
2746 case country_table::currencies: return "currencies.tsv";
2747 case country_table::holidays: return "holidays.tsv";
2748 case country_table::holiday_events: return "holiday_events.tsv";
2749 case country_table::holiday_years: return "holiday_years.tsv";
2750 }
2751 return "";
2752 }
2753
2754 static core::table_schema locale_schema_(country_table t)
2755 {
2756 switch (t)
2757 {
2758 case country_table::locales:
2759 return {.key = {"country", "tag"},
2760 .required = {"language", "language_name", "share", "basis", "date_short", "date_medium",
2761 "date_long", "date_full", "time_short", "time_medium", "months", "months_abbr",
2762 "weekdays", "weekdays_abbr", "am", "pm", "eras", "decimal", "group", "minus",
2763 "number_pattern", "currency_pattern", "currency_code", "currency_symbol",
2764 "first_day", "weekend_start", "weekend_end", "measurement"},
2765 .optional = {"script", "currency_spacing"}};
2766 case country_table::zones:
2767 return {.key = {"country", "zone"}, .required = {"share", "basis", "periods"}, .optional = {"posix"}};
2768 case country_table::zone_links: return {.key = {"link"}, .required = {"zone"}};
2769 case country_table::currencies:
2770 return {.key = {"code"}, .required = {"numeric", "digits"}, .optional = {"cash_digits"}};
2771 case country_table::holidays:
2772 return {.key = {"country", "key", "from_year"},
2773 .required = {"kind", "rule", "days", "observed", "to_year"},
2774 .optional = {"name", "name_en"}};
2775 case country_table::holiday_events: return {.key = {"event", "country", "date"}};
2776 case country_table::holiday_years: return {.key = {"country"}, .required = {"from_year", "to_year"}};
2777 case country_table::countries: break;
2778 }
2779 return schema_();
2780 }
2781
2782 static locale_table_set make_locale_tables_()
2783 {
2784 return {core::layered_table{locale_schema_(country_table::locales)},
2785 core::layered_table{locale_schema_(country_table::zones)},
2786 core::layered_table{locale_schema_(country_table::zone_links)},
2787 core::layered_table{locale_schema_(country_table::currencies)},
2788 core::layered_table{locale_schema_(country_table::holidays)},
2789 core::layered_table{locale_schema_(country_table::holiday_events)},
2790 core::layered_table{locale_schema_(country_table::holiday_years)}};
2791 }
2792
2793 // -- Row parsing (throws core::tsv_error located at the row) ----------------
2794
2795 static std::chrono::weekday weekday_cell_(const core::tsv_row& row, std::string_view column)
2796 {
2797 const auto d = country_detail::parse_weekday(row.get<std::string_view>(column));
2798 if (!d) throw row.error(column, "expected sun, mon, tue, wed, thu, fri or sat");
2799 return *d;
2800 }
2801
2802 template <std::size_t N>
2803 static void list_cell_(const core::tsv_row& row, std::string_view column, std::array<std::string, N>& out)
2804 {
2805 const auto items = row.list(column);
2806 if (items.size() != N || std::ranges::any_of(items, [](std::string_view s) { return s.empty(); }))
2807 {
2808 throw row.error(column, "expected " + std::to_string(N) + " names separated by ';'");
2809 }
2810 for (std::size_t i = 0; i < N; ++i) out.at(i) = std::string{items[i]};
2811 }
2812
2813 static locale parse_locale_(const core::tsv_row& row)
2814 {
2815 locale l;
2816 l.country_code = row.get<std::string>("country");
2817 l.tag = row.get<std::string>("tag");
2818 l.language = row.get<std::string>("language");
2819 l.script = row.get_or<std::string>("script", "");
2820 l.language_name = row.get<std::string>("language_name");
2821 const auto lowercase = [](std::string_view s) {
2822 return std::ranges::all_of(s, [](char c) { return c >= 'a' && c <= 'z'; });
2823 };
2824 const bool script_ok = l.script.empty() || (l.script.size() == 4 && l.script[0] >= 'A' &&
2825 l.script[0] <= 'Z' && lowercase(l.script.substr(1)));
2826 if (l.language.size() < 2 || l.language.size() > 3 || !lowercase(l.language))
2827 {
2828 throw row.error("language", "expected an ISO 639 code of 2 or 3 lowercase letters");
2829 }
2830 if (!script_ok) throw row.error("script", "expected an ISO 15924 code such as Latn");
2831 if (!core::is_alpha2(l.country_code)) throw row.error("country", "expected an ISO 3166-1 alpha-2 code");
2832 if (l.tag != l.language + (l.script.empty() ? "" : "-" + l.script) + "-" + l.country_code)
2833 {
2834 throw row.error("tag", "expected language[-Script]-REGION from the language, script and country columns");
2835 }
2836 l.share = row.get<double>("share");
2837 if (!(l.share > 0.0) || l.share > 100.0) throw row.error("share", "must be above 0 and at most 100");
2838 l.basis = row.get<std::string>("basis");
2839 if (l.basis != "cldr" && l.basis != "inherited" && l.basis != "fallback")
2840 {
2841 throw row.error("basis", "expected cldr, inherited or fallback");
2842 }
2843 for (const auto& [column, field, time] :
2844 {std::tuple{"date_short", &locale::date_short, false}, std::tuple{"date_medium", &locale::date_medium, false},
2845 std::tuple{"date_long", &locale::date_long, false}, std::tuple{"date_full", &locale::date_full, false},
2846 std::tuple{"time_short", &locale::time_short, true}, std::tuple{"time_medium", &locale::time_medium, true}})
2847 {
2848 l.*field = row.get<std::string>(column);
2849 if (const auto error = country_detail::check_pattern(l.*field, time); !error.empty())
2850 {
2851 throw row.error(column, error);
2852 }
2853 }
2854 list_cell_(row, "months", l.months);
2855 list_cell_(row, "months_abbr", l.months_abbr);
2856 list_cell_(row, "weekdays", l.weekdays);
2857 list_cell_(row, "weekdays_abbr", l.weekdays_abbr);
2858 list_cell_(row, "eras", l.eras);
2859 l.am = row.get<std::string>("am");
2860 l.pm = row.get<std::string>("pm");
2861 l.decimal = row.get<std::string>("decimal");
2862 l.group = row.get<std::string>("group");
2863 l.minus = row.get<std::string>("minus");
2864 for (const auto& [column, field, currency] : {std::tuple{"number_pattern", &locale::number_pattern, false},
2865 std::tuple{"currency_pattern", &locale::currency_pattern, true}})
2866 {
2867 l.*field = row.get<std::string>(column);
2868 std::string error;
2869 (void)country_detail::parse_number_pattern(l.*field, currency, error);
2870 if (!error.empty()) throw row.error(column, error);
2871 }
2872 l.currency_spacing = row.get_or<std::string>("currency_spacing", "");
2873 l.currency_code = row.get<std::string>("currency_code");
2874 if (!is_currency_code_(l.currency_code)) throw row.error("currency_code", "expected three capital letters");
2875 l.currency_symbol = row.get<std::string>("currency_symbol");
2876 l.first_day = weekday_cell_(row, "first_day");
2877 l.weekend_start = weekday_cell_(row, "weekend_start");
2878 l.weekend_end = weekday_cell_(row, "weekend_end");
2879 const auto m = row.get<std::string_view>("measurement");
2880 if (m == "metric") l.measurement = measurement::metric;
2881 else if (m == "us") l.measurement = measurement::us;
2882 else if (m == "uk") l.measurement = measurement::uk;
2883 else throw row.error("measurement", "expected metric, us or uk");
2884 return l;
2885 }
2886
2887 static bool is_currency_code_(std::string_view c)
2888 {
2889 return c.size() == 3 && std::ranges::all_of(c, [](char ch) { return ch >= 'A' && ch <= 'Z'; });
2890 }
2891
2892 static time_zone parse_zone_(const core::tsv_row& row)
2893 {
2894 time_zone z;
2895 z.country_code = row.get<std::string>("country");
2896 if (!core::is_alpha2(z.country_code)) throw row.error("country", "expected an ISO 3166-1 alpha-2 code");
2897 z.name = row.get<std::string>("zone");
2898 z.share = row.get<double>("share");
2899 if (!(z.share > 0.0) || z.share > 100.0) throw row.error("share", "must be above 0 and at most 100");
2900 z.basis = row.get<std::string>("basis");
2901 if (z.basis != "tzdata" && z.basis != "geonames" && z.basis != "fallback")
2902 {
2903 throw row.error("basis", "expected tzdata, geonames or fallback");
2904 }
2905 try
2906 {
2907 z.rules = core::tz_rules::parse(row.get<std::string_view>("periods"), row.get_or<std::string_view>("posix", ""));
2908 }
2909 catch (const std::invalid_argument& e)
2910 {
2911 throw row.error("periods", e.what());
2912 }
2913 return z;
2914 }
2915
2916 struct currency_entry
2917 {
2918 std::string numeric;
2919 std::uint8_t digits{2};
2920 std::optional<std::uint8_t> cash_digits;
2921 };
2922
2923 static currency_entry parse_currency_(const core::tsv_row& row)
2924 {
2925 if (!is_currency_code_(row.get<std::string_view>("code"))) throw row.error("code", "expected three capital letters");
2926 currency_entry c;
2927 c.numeric = row.get<std::string>("numeric");
2928 if (c.numeric.size() != 3 || c.numeric.find_first_not_of("0123456789") != std::string::npos)
2929 {
2930 throw row.error("numeric", "expected three digits");
2931 }
2932 const auto digits = row.get<unsigned>("digits");
2933 if (digits > 4) throw row.error("digits", "must be from 0 to 4");
2934 c.digits = static_cast<std::uint8_t>(digits);
2935 if (row.has("cash_digits"))
2936 {
2937 const auto cash = row.get<unsigned>("cash_digits");
2938 if (cash > 4) throw row.error("cash_digits", "must be from 0 to 4");
2939 c.cash_digits = static_cast<std::uint8_t>(cash);
2940 }
2941 return c;
2942 }
2943
2944 static country_detail::holiday_row parse_holiday_(const core::tsv_row& row)
2945 {
2946 if (!core::is_alpha2(row.get<std::string_view>("country")))
2947 {
2948 throw row.error("country", "expected an ISO 3166-1 alpha-2 code");
2949 }
2950 country_detail::holiday_row h;
2951 h.key = row.get<std::string>("key");
2952 if (!country_detail::valid_key(h.key)) throw row.error("key", "expected [a-z0-9_]+");
2953 h.name = row.get_or<std::string>("name", "");
2954 h.name_en = row.get_or<std::string>("name_en", "");
2955 const auto kind = country_detail::parse_kind(row.get<std::string_view>("kind"));
2956 if (!kind) throw row.error("kind", "expected public, bank, optional or observance");
2957 h.kind = *kind;
2958 auto rule = country_detail::parse_rule(row.get<std::string_view>("rule"));
2959 if (!rule) throw row.error("rule", "expected MM-DD, MM:N:dow, MM-DD>dow, MM-DD<dow, easter+N, orthodox+N or event:key+N");
2960 h.rule = std::move(*rule);
2961 h.days = row.get<unsigned>("days");
2962 if (h.days < 1 || h.days > 10) throw row.error("days", "must be from 1 to 10");
2963 auto observed = country_detail::parse_policy(row.get<std::string_view>("observed"));
2964 if (!observed) throw row.error("observed", "expected none, next_weekday, weekend>next or a weekday map such as sat<fri,sun>mon");
2965 h.observed = std::move(*observed);
2966 h.from = row.get<int>("from_year");
2967 h.to = row.get<int>("to_year");
2968 if (h.from < 1 || h.to > 9998 || h.from > h.to) throw row.error("to_year", "must be from from_year to 9998");
2969 return h;
2970 }
2971
2972 struct event_row
2973 {
2974 std::string_view event; // views into the row
2975 std::string_view country; // "*" for the shared date
2976 std::chrono::sys_days date;
2977 };
2978
2979 static event_row parse_event_(const core::tsv_row& row)
2980 {
2981 event_row e;
2982 e.event = row.get<std::string_view>("event");
2983 if (!country_detail::valid_key(e.event)) throw row.error("event", "expected [a-z0-9_]+");
2984 e.country = row.get<std::string_view>("country");
2985 if (e.country != "*" && !core::is_alpha2(e.country))
2986 {
2987 throw row.error("country", "expected * (every country) or an ISO 3166-1 alpha-2 code");
2988 }
2989 const auto d = country_detail::parse_iso_date(row.get<std::string_view>("date"));
2990 if (!d) throw row.error("date", "expected a valid YYYY-MM-DD date");
2991 e.date = std::chrono::sys_days{*d};
2992 return e;
2993 }
2994
2995 static year_span parse_years_(const core::tsv_row& row)
2996 {
2997 if (!core::is_alpha2(row.get<std::string_view>("country")))
2998 {
2999 throw row.error("country", "expected an ISO 3166-1 alpha-2 code");
3000 }
3001 const year_span s{.from = row.get<int>("from_year"), .to = row.get<int>("to_year")};
3002 if (s.from < 1 || s.to > 9998 || s.from > s.to) throw row.error("to_year", "must be from from_year to 9998");
3003 return s;
3004 }
3005
3006 // -- The model the getters read ---------------------------------------------
3007
3008 struct pick_list
3009 {
3010 std::vector<std::size_t> items; // in draw order
3011 core::discrete weighted{1.0};
3012 };
3013
3014 struct holiday_country
3015 {
3016 year_span years;
3017 unsigned weekend{0b1000001U}; // bits by c_encoding: Saturday and Sunday
3018 std::vector<country_detail::holiday_row> rows;
3019 std::map<std::string, std::vector<std::chrono::sys_days>, std::less<>> events;
3020 };
3021
3022 struct locale_model
3023 {
3024 std::vector<locale> locales;
3025 std::map<std::string, std::size_t, std::less<>> locale_by_tag; // lowercase tag
3026 std::map<std::string, pick_list, std::less<>> locales_by_country; // share desc, then tag
3027 std::map<std::string, pick_list, std::less<>> locale_languages; // countries, by speakers
3028 pick_list locale_countries; // countries with a locale
3029 std::vector<time_zone> zones;
3030 std::map<std::string, std::size_t, std::less<>> zone_by_name;
3031 std::map<std::string, std::string, std::less<>> zone_links;
3032 std::map<std::string, pick_list, std::less<>> zones_by_country; // share desc, then name
3033 pick_list zone_countries; // countries with a zone
3034 std::map<std::string, currency_entry, std::less<>> currencies;
3035 std::map<std::string, holiday_country, std::less<>> holidays;
3036 };
3037
3038 // Builds the model from locale tables and the loaded countries. strict: every row is parsed, and
3039 // a bad row, a row of a country that is not loaded, or a reference to a missing row of another
3040 // table, is an error located at the row; so the strict build that applies a layer is its
3041 // validation. Otherwise (after the countries change) a row of a missing country is left out.
3042 [[nodiscard]] locale_model build_locale_model_(const locale_table_set& t, bool strict) const
3043 {
3044 locale_model m;
3045 build_locales_(m, t, strict);
3046 build_zones_(m, t, strict);
3047 build_holidays_(m, t, strict);
3048 return m;
3049 }
3050
3051 // Whether a row's country is loaded; when strict, a row of one that is not is an error.
3052 [[nodiscard]] bool row_loaded_(const core::tsv_row& row, bool strict) const
3053 {
3054 const auto cc = row["country"];
3055 if (_cca2_index.contains(cc)) return true;
3056 if (strict) throw row.error("country", "country " + std::string{cc} + " is not loaded");
3057 return false;
3058 }
3059
3060 static void weigh_(pick_list& p, const std::vector<double>& weights)
3061 {
3062 if (!weights.empty()) p.weighted = core::discrete{weights};
3063 }
3064
3065 // Sorts a country's rows by share, largest first, then by name, and weights the draw by share.
3066 template <typename Row, typename Name>
3067 static void order_by_share_(pick_list& p, const std::vector<Row>& rows, Name name)
3068 {
3069 std::ranges::sort(p.items, [&](std::size_t a, std::size_t b) {
3070 if (rows[a].share != rows[b].share) return rows[a].share > rows[b].share;
3071 return name(rows[a]) < name(rows[b]);
3072 });
3073 std::vector<double> weights;
3074 weights.reserve(p.items.size());
3075 for (const auto i : p.items) weights.push_back(rows[i].share);
3076 weigh_(p, weights);
3077 }
3078
3079 // The loaded countries that by_country has, weighted as get_country() weighs them.
3080 template <typename ByCountry>
3081 [[nodiscard]] pick_list countries_with_(const ByCountry& by_country) const
3082 {
3083 pick_list p;
3084 std::vector<double> weights;
3085 for (std::size_t i = 0; i < _countries.size(); ++i)
3086 {
3087 if (!by_country.contains(_countries[i].cca2)) continue;
3088 p.items.push_back(i);
3089 weights.push_back(weight_(_countries[i]));
3090 }
3091 weigh_(p, weights);
3092 return p;
3093 }
3094
3095 // Currencies, then locales, their indexes and their draws.
3096 void build_locales_(locale_model& m, const locale_table_set& t, bool strict) const
3097 {
3098 for (const auto& row : t.at(index_(country_table::currencies)).rows())
3099 {
3100 m.currencies.emplace(std::string{row["code"]}, parse_currency_(row));
3101 }
3102 for (const auto& row : t.at(index_(country_table::locales)).rows())
3103 {
3104 if (!row_loaded_(row, strict)) continue;
3105 auto l = parse_locale_(row);
3106 const auto currency = m.currencies.find(l.currency_code);
3107 if (currency == m.currencies.end())
3108 {
3109 if (strict) throw row.error("currency_code", "no currencies.tsv row for " + l.currency_code);
3110 continue;
3111 }
3112 l.currency_digits = currency->second.digits;
3113 m.locales.push_back(std::move(l));
3114 }
3115 std::map<std::string, std::map<std::string, double>, std::less<>> language_shares; // language -> cc -> share
3116 for (std::size_t i = 0; i < m.locales.size(); ++i)
3117 {
3118 const auto& l = m.locales[i];
3119 m.locale_by_tag.emplace(country_detail::lower(l.tag), i);
3120 m.locales_by_country[l.country_code].items.push_back(i);
3121 auto& share = language_shares[l.language][l.country_code];
3122 share = std::max(share, l.share);
3123 }
3124 for (auto& [cc, p] : m.locales_by_country)
3125 {
3126 order_by_share_(p, m.locales, [](const locale& l) -> const std::string& { return l.tag; });
3127 }
3128 for (const auto& [language, shares] : language_shares)
3129 {
3130 pick_list p;
3131 std::vector<double> weights;
3132 for (std::size_t i = 0; i < _countries.size(); ++i)
3133 {
3134 const auto it = shares.find(_countries[i].cca2);
3135 if (it == shares.end()) continue;
3136 p.items.push_back(i);
3137 weights.push_back(weight_(_countries[i]) * it->second / 100.0);
3138 }
3139 if (p.items.empty()) continue;
3140 weigh_(p, weights);
3141 m.locale_languages.emplace(language, std::move(p));
3142 }
3143 m.locale_countries = countries_with_(m.locales_by_country);
3144 }
3145
3146 // Zones, their indexes and draws, then the links to them.
3147 void build_zones_(locale_model& m, const locale_table_set& t, bool strict) const
3148 {
3149 for (const auto& row : t.at(index_(country_table::zones)).rows())
3150 {
3151 if (row_loaded_(row, strict)) m.zones.push_back(parse_zone_(row));
3152 }
3153 for (std::size_t i = 0; i < m.zones.size(); ++i)
3154 {
3155 const auto& z = m.zones[i];
3156 m.zones_by_country[z.country_code].items.push_back(i);
3157 const auto [it, added] = m.zone_by_name.emplace(z.name, i);
3158 // A zone two countries share answers for the one IANA lists it for.
3159 if (!added && m.zones[it->second].basis == "geonames" && z.basis != "geonames") it->second = i;
3160 }
3161 for (auto& [cc, p] : m.zones_by_country)
3162 {
3163 order_by_share_(p, m.zones, [](const time_zone& z) -> const std::string& { return z.name; });
3164 }
3165 m.zone_countries = countries_with_(m.zones_by_country);
3166 for (const auto& row : t.at(index_(country_table::zone_links)).rows())
3167 {
3168 const auto target = row.get<std::string_view>("zone");
3169 if (!m.zone_by_name.contains(target))
3170 {
3171 if (strict) throw row.error("zone", "zone " + std::string{target} + " is not loaded");
3172 continue;
3173 }
3174 m.zone_links.emplace(std::string{row["link"]}, std::string{target});
3175 }
3176 }
3177
3178 // Event dates, keyed by views into the tables, which outlive the build: the shared dates of
3179 // each event, and each country's own and overriding dates with their rows.
3180 using shared_events_ = std::map<std::string_view, std::vector<std::chrono::sys_days>, std::less<>>;
3181 using own_events_ = std::map<std::pair<std::string_view, std::string_view>,
3182 std::vector<std::pair<std::chrono::sys_days, const core::tsv_row*>>>;
3183
3184 // Replaces the shared date nearest each of a country's own dates, which must be within three days.
3185 static void override_dates_(std::vector<std::chrono::sys_days>& dates,
3186 const std::vector<std::pair<std::chrono::sys_days, const core::tsv_row*>>& mine,
3187 const std::string& ev)
3188 {
3189 for (const auto& [date, row] : mine)
3190 {
3191 auto near = std::ranges::min_element(dates, {}, [date](std::chrono::sys_days d) {
3192 return d > date ? (d - date).count() : (date - d).count();
3193 });
3194 const auto gap = near == dates.end() ? 4 : std::abs((*near - date).count());
3195 if (gap > 3)
3196 {
3197 throw row->error("date", "no shared date of " + ev + " within three days");
3198 }
3199 *near = date;
3200 }
3201 }
3202
3203 // An event's dates for a country: the shared dates with the country's own replacing the one
3204 // within three days of each; or, for an event with no shared dates, the country's own. Null
3205 // when there are none (an error at the rule's row when strict).
3206 [[nodiscard]] static const std::vector<std::chrono::sys_days>*
3207 event_dates_(holiday_country& hc, const std::string& cc, const std::string& ev, const core::tsv_row& at,
3208 const shared_events_& shared, const own_events_& own, bool strict)
3209 {
3210 if (const auto it = hc.events.find(ev); it != hc.events.end()) return &it->second;
3211 const auto mine = own.find({cc, ev});
3212 const auto base = shared.find(ev);
3213 std::vector<std::chrono::sys_days> dates;
3214 if (base != shared.end())
3215 {
3216 dates = base->second;
3217 if (mine != own.end()) override_dates_(dates, mine->second, ev);
3218 }
3219 else if (mine != own.end())
3220 {
3221 for (const auto& [date, row] : mine->second) dates.push_back(date);
3222 }
3223 else
3224 {
3225 if (strict) throw at.error("rule", "no holiday_events.tsv dates for event " + ev);
3226 return nullptr;
3227 }
3228 std::ranges::sort(dates);
3229 return &hc.events.emplace(ev, std::move(dates)).first->second;
3230 }
3231
3232 // The weekend of a locale (CLDR weekData) as bits by c_encoding.
3233 [[nodiscard]] static unsigned weekend_mask_(const locale& l)
3234 {
3235 unsigned mask = 0;
3236 for (unsigned d = l.weekend_start.c_encoding();; d = (d + 1) % 7)
3237 {
3238 mask |= 1U << d;
3239 if (d == l.weekend_end.c_encoding()) break;
3240 }
3241 return mask;
3242 }
3243
3244 // A holiday row's names. A continuation row (both empty) takes those of the nearest row above
3245 // with its country and key; names holds the last row's, as views into the table.
3246 static void resolve_names_(country_detail::holiday_row& h, const core::tsv_row& row,
3247 std::pair<std::string_view, std::string_view>& names)
3248 {
3249 if (h.name.empty() && h.name_en.empty())
3250 {
3251 if (names.second.empty())
3252 {
3253 throw row.error("name_en", "empty names need a row above with the same country and key");
3254 }
3255 h.name = names.first;
3256 h.name_en = names.second;
3257 return;
3258 }
3259 if (h.name_en.empty()) throw row.error("name_en", "missing value");
3260 names = {row[h.name.empty() ? "name_en" : "name"], row["name_en"]};
3261 if (h.name.empty()) h.name = h.name_en;
3262 }
3263
3264 // A country's holiday entry, made on first use with its span and the weekend of its first
3265 // locale (else Saturday and Sunday).
3266 static holiday_country& holiday_entry_(locale_model& m, const std::map<std::string, year_span, std::less<>>& spans,
3267 const std::string& cc)
3268 {
3269 auto [it, added] = m.holidays.try_emplace(cc);
3270 if (added)
3271 {
3272 it->second.years = spans.at(cc);
3273 const auto l = std::ranges::find(m.locales, cc, &locale::country_code);
3274 if (l != m.locales.end()) it->second.weekend = weekend_mask_(*l);
3275 }
3276 return it->second;
3277 }
3278
3279 // Holidays: the event dates, the spans, then each country's rows, with its weekend.
3280 void build_holidays_(locale_model& m, const locale_table_set& t, bool strict) const
3281 {
3282 shared_events_ shared;
3283 own_events_ own;
3284 for (const auto& row : t.at(index_(country_table::holiday_events)).rows())
3285 {
3286 const auto e = parse_event_(row);
3287 if (e.country == "*")
3288 {
3289 shared[e.event].push_back(e.date);
3290 }
3291 else if (row_loaded_(row, strict))
3292 {
3293 own[{e.country, e.event}].emplace_back(e.date, &row);
3294 }
3295 }
3296 for (auto& [k, dates] : shared) std::ranges::sort(dates);
3297 std::map<std::string, year_span, std::less<>> spans;
3298 for (const auto& row : t.at(index_(country_table::holiday_years)).rows())
3299 {
3300 if (row_loaded_(row, strict)) spans.emplace(std::string{row["country"]}, parse_years_(row));
3301 }
3302 // (country, key) -> the names of the last row with them.
3303 std::map<std::pair<std::string_view, std::string_view>, std::pair<std::string_view, std::string_view>>
3304 last_names;
3305 for (const auto& row : t.at(index_(country_table::holidays)).rows())
3306 {
3307 if (!row_loaded_(row, strict)) continue;
3308 const std::string cc{row["country"]};
3309 if (!spans.contains(cc))
3310 {
3311 if (strict) throw row.error("country", "no holiday_years.tsv row for " + cc);
3312 continue;
3313 }
3314 auto h = parse_holiday_(row);
3315 resolve_names_(h, row, last_names[{row["country"], row["key"]}]);
3316 auto& hc = holiday_entry_(m, spans, cc);
3317 if (h.rule.form == country_detail::rule_form::event)
3318 {
3319 h.events = event_dates_(hc, cc, h.rule.event, row, shared, own, strict);
3320 if (h.events == nullptr) continue;
3321 }
3322 hc.rows.push_back(std::move(h));
3323 }
3324 for (const auto& [cc, span] : spans) (void)holiday_entry_(m, spans, cc);
3325 }
3326
3327 locale_table_set _locale_tables{make_locale_tables_()};
3328 locale_model _locale;
3329
3330 // Applies locale-table documents together: all or, if any fails, none.
3331 void apply_locale_(locale_documents docs, core::layer_mode mode, bool reset)
3332 {
3333 auto next = _locale_tables;
3334 for (const auto t : locale_tables_)
3335 {
3336 auto& doc = docs.at(index_(t));
3337 auto& tbl = next.at(index_(t));
3338 if (!doc)
3339 {
3340 if (reset) tbl.clear();
3341 continue;
3342 }
3343 (void)tbl.extend(std::move(*doc), mode); // the strict build below validates every row
3344 }
3345 auto built = build_locale_model_(next, true);
3346 _locale_tables = std::move(next);
3347 _locale = std::move(built);
3348 }
3349
3350 // Replaces every table: the countries, then the locale tables (a missing one is cleared).
3351 void replace_all_(core::tsv_document countries, locale_documents docs)
3352 {
3353 auto saved = _table;
3354 (void)_table.extend(std::move(countries), core::layer_mode::replace,
3355 [](const core::tsv_row& row) { (void)parse_(row); });
3356 try
3357 {
3358 rebuild_(false);
3359 apply_locale_(std::move(docs), core::layer_mode::replace, true);
3360 }
3361 catch (...)
3362 {
3363 _table = std::move(saved);
3364 rebuild_(false);
3365 throw;
3366 }
3367 }
3368
3369 // The first resources/ directory that holds this tier's countries.tsv and
3370 // locales.tsv. Both are asked for because other libraries ship a file of
3371 // either name (user-generator has its own locales.tsv, phone-generator its
3372 // own countries.tsv), and a program that composes them runs from their
3373 // repository, where `resources/` is theirs.
3374 static std::optional<std::filesystem::path> find_locale_resources_(const std::string& dir)
3375 {
3376 for (const auto& root : core::resource_candidates(library_name_))
3377 {
3378 std::error_code ec;
3379 if (std::filesystem::is_regular_file(root / dir / data_file_, ec) &&
3380 std::filesystem::is_regular_file(root / dir / file_name_(country_table::locales), ec))
3381 {
3382 return root;
3383 }
3384 }
3385 return std::nullopt;
3386 }
3387
3388 static locale_documents read_locale_directory_(const std::filesystem::path& dir)
3389 {
3390 locale_documents docs;
3391 for (const auto t : locale_tables_)
3392 {
3393 const auto file = dir / file_name_(t);
3394 std::error_code ec;
3395 if (std::filesystem::is_regular_file(file, ec)) docs.at(index_(t)) = core::tsv_document::load(file);
3396 }
3397 return docs;
3398 }
3399
3400 static std::span<const core::embedded_file> embedded_locale_files_()
3401 {
3402#ifdef DASMIG_COUNTRY_EMBED_LOCALE
3403 return embedded::countrygen_locale::files;
3404#else
3405 return {};
3406#endif
3407 }
3408
3409 [[nodiscard]] std::uint64_t locale_hash_() const
3410 {
3411 std::uint64_t h = 0;
3412 for (const auto& t : _locale_tables) h = core::hash_combine(h, t.data_hash());
3413 return h;
3414 }
3415
3416 void require_locale_table_(country_table t) const
3417 {
3418 if (_locale_tables.at(index_(t)).empty())
3419 {
3420 throw std::runtime_error(
3421 "No locale data loaded. Load a tier with the locale tables, or define DASMIG_COUNTRY_EMBED_LOCALE.");
3422 }
3423 }
3424
3425 // The code in capitals, when that country is loaded.
3426 [[nodiscard]] std::string known_country_(std::string_view code) const
3427 {
3428 require_data_();
3429 auto cc = country_detail::upper(code);
3430 if (!_cca2_index.contains(cc)) throw core::unknown_country_error(code);
3431 return cc;
3432 }
3433
3434 // Draws a country exactly as get_country() does; one with no rows in by_country is replaced by a
3435 // draw, from the fallback stream, among those that have some.
3436 template <typename ByCountry>
3437 [[nodiscard]] std::string draw_country_(std::uint64_t call_seed, bool weighted, const pick_list& with,
3438 const ByCountry& by_country, std::string_view fallback) const
3439 {
3440 require_data_();
3441 auto r = core::rng{call_seed}.split("country");
3442 const auto idx = weighted ? _distribution(r) : static_cast<std::size_t>(core::bounded(r, _countries.size()));
3443 if (by_country.contains(_countries[idx].cca2)) return _countries[idx].cca2;
3444 if (with.items.empty()) throw std::invalid_argument("No loaded country has this data");
3445 auto again = core::rng{call_seed}.split(fallback);
3446 const auto j = weighted ? with.weighted(again) : static_cast<std::size_t>(core::bounded(again, with.items.size()));
3447 return _countries[with.items[j]].cca2;
3448 }
3449
3450 [[nodiscard]] static time_zone zone_at_(const time_zone& base, std::chrono::year_month_day reference,
3451 core::seed_signature signature)
3452 {
3453 if (!reference.ok()) throw std::invalid_argument("time zone: the reference date is not valid");
3454 time_zone z = base;
3455 const std::chrono::sys_seconds t{std::chrono::sys_days{reference} + std::chrono::hours{12}};
3456 const auto info = z.rules.at(t);
3457 z.utc_offset = info.offset;
3458 z.abbreviation = std::string{info.abbreviation};
3459 z.dst = info.dst;
3460 z.observes_dst = z.rules.observes_dst_after(t);
3461 z._signature = signature;
3462 return z;
3463 }
3464
3465 [[nodiscard]] const holiday_country& holiday_country_(std::string_view country) const
3466 {
3467 require_locale_table_(country_table::holiday_years);
3468 const auto cc = known_country_(country);
3469 const auto it = _locale.holidays.find(cc);
3470 if (it == _locale.holidays.end()) throw std::invalid_argument("No holiday data for country " + cc);
3471 return it->second;
3472 }
3473
3474 static void check_holiday_year_(const holiday_country& hc, int year)
3475 {
3476 if (year < hc.years.from || year > hc.years.to)
3477 {
3478 throw std::out_of_range("Year " + std::to_string(year) + " is outside the holiday years, " +
3479 std::to_string(hc.years.from) + " to " + std::to_string(hc.years.to));
3480 }
3481 }
3482
3483 // A year's holidays as design 8.3 and 8.4 compute them: every rule's days in the year and the years
3484 // either side, sorted by date, key and day; then, kind by kind, each occurrence's days moved by
3485 // its policy; the occurrences that start in the year, sorted by date, key and day.
3486 // NOLINTNEXTLINE(readability-function-cognitive-complexity): the year's evaluation of design 8.3 and 8.4
3487 [[nodiscard]] static std::vector<holiday> evaluate_(const holiday_country& hc, int year, bool optional,
3488 bool observances)
3489 {
3490 using std::chrono::sys_days;
3491 struct entry
3492 {
3493 sys_days date;
3494 const country_detail::holiday_row* row;
3495 unsigned day;
3496 sys_days start;
3497 };
3498 const auto wanted = [&](holiday_kind k) {
3499 return (k != holiday_kind::optional || optional) && (k != holiday_kind::observance || observances);
3500 };
3501 std::vector<entry> entries;
3502 std::vector<sys_days> starts;
3503 for (int y = year - 1; y <= year + 1; ++y)
3504 {
3505 for (const auto& row : hc.rows)
3506 {
3507 if (y < row.from || y > row.to || !wanted(row.kind)) continue;
3508 country_detail::rule_dates(row.rule, y, row.events, starts);
3509 for (const auto s : starts)
3510 {
3511 for (unsigned i = 0; i < row.days; ++i)
3512 {
3513 entries.push_back({.date = s + std::chrono::days{i}, .row = &row, .day = i + 1, .start = s});
3514 }
3515 }
3516 }
3517 }
3518 std::ranges::stable_sort(entries, [](const entry& a, const entry& b) {
3519 if (a.date != b.date) return a.date < b.date;
3520 if (a.row->key != b.row->key) return a.row->key < b.row->key;
3521 return a.day < b.day;
3522 });
3523 const auto day_of = [](sys_days d) { return static_cast<int>(d.time_since_epoch().count()); };
3524 std::vector<holiday> out;
3525 std::vector<std::optional<sys_days>> moved;
3526 for (const auto kind : {holiday_kind::public_, holiday_kind::bank, holiday_kind::optional, holiday_kind::observance})
3527 {
3528 std::set<int> taken;
3529 for (const auto& e : entries)
3530 {
3531 if (e.row->kind == kind) taken.insert(day_of(e.date));
3532 }
3533 if (taken.empty()) continue;
3534 std::set<std::pair<const country_detail::holiday_row*, int>> done;
3535 std::map<std::pair<int, std::string_view>, int> shared;
3536 for (const auto& e : entries)
3537 {
3538 if (e.row->kind != kind || !done.emplace(e.row, day_of(e.start)).second) continue;
3539 country_detail::simulate(e.row->observed, e.start, e.row->days, taken, hc.weekend, shared, moved);
3540 if (country_detail::year_of(e.start) != year) continue;
3541 for (unsigned i = 0; i < e.row->days; ++i)
3542 {
3543 holiday h;
3544 h.date = std::chrono::year_month_day{e.start + std::chrono::days{i}};
3545 if (moved[i]) h.observed = std::chrono::year_month_day{*moved[i]};
3546 h.key = e.row->key;
3547 h.name = e.row->name;
3548 h.name_en = e.row->name_en;
3549 h.kind = kind;
3550 h.day = i + 1;
3551 out.push_back(std::move(h));
3552 }
3553 }
3554 }
3555 std::ranges::sort(out, [](const holiday& a, const holiday& b) {
3556 if (a.date != b.date) return a.date < b.date;
3557 if (a.key != b.key) return a.key < b.key;
3558 return a.day < b.day;
3559 });
3560 return out;
3561 }
3562};
3563
3564} // namespace dasmig
3565
3566#endif // DASMIG_COUNTRYGEN_HPP
Country generator that produces random countries from aggregated open-data sources.
void for_each_locale(const locale_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of locales to a sink without holding the batch.
std::vector< country > get_country_batch(const country_request &request, const core::batch_options &options)
Generate a batch of country results from a seed drawn like get_country() draws its own.
const core::layer_info & extend(country_table which, const std::filesystem::path &file, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table from a TSV file.
bool load(dataset tier)
Load a dataset tier, replacing all data and layers loaded before.
cntg(cntg &&) noexcept=default
Move constructor.
void for_each_country(const country_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of country results to a sink without holding the whole batch.
const core::layer_info & extend(core::tsv_document doc, core::layer_mode mode=core::layer_mode::override)
Add a data layer.
time_zone get_time_zone(const time_zone_request &request, std::uint64_t call_seed) const
Draw a time zone of a country, by population, deterministically.
locale get_locale(const locale_request &request)
Draw a locale of a country, by speakers.
const std::vector< core::layer_info > & layers() const
The data layers of countries.tsv in effect, in order.
const std::vector< core::layer_info > & layers(country_table which) const
The data layers of one table in effect, in order.
cntg(const cntg &)=delete
Not copyable.
const core::layer_info & extend_from_string(country_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.
void for_each_country(const country_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of country results with a batch seed drawn like get_country() draws its own.
const core::layer_info & extend(const std::filesystem::path &file, core::layer_mode mode=core::layer_mode::override)
Add a data layer from a TSV file with the countries.tsv columns.
std::vector< holiday > holidays(const holiday_request &request) const
A country's holidays in a year.
bool has_locale_data() const
Whether any locale table (locales, zones, currencies, holidays) is loaded.
std::vector< time_zone > get_time_zone_batch(const time_zone_request &request, const core::batch_options &options)
Generate a batch of time zones from a seed drawn like get_time_zone() draws its own.
cntg & operator=(const cntg &)=delete
Not copyable.
bool is_holiday(std::string_view country, std::chrono::year_month_day date, bool optional=false) const
Whether a date is a holiday's date or its observed date.
time_zone get_time_zone(const time_zone_request &request)
Draw a time zone of a country, by population.
std::vector< time_zone > get_time_zone_batch(const time_zone_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of time zones, deterministically: item i is get_time_zone(request,...
const core::layer_info & extend_from_string(std::string_view text, std::string source, core::layer_mode mode=core::layer_mode::override)
Add a data layer from TSV text.
std::vector< locale > get_locale_batch(const locale_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of locales, deterministically: item i is get_locale(request, core::item_seed(batch_s...
std::uint8_t currency_digits(std::string_view code) const
The ISO 4217 minor unit of a currency (0 for JPY, 3 for KWD).
void for_each_time_zone(const time_zone_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of time zones with a batch seed drawn like get_time_zone() draws its own.
std::uint64_t data_hash() const
Hash of the loaded countries.tsv, recorded in each country's signature() (the locale tables have thei...
static cntg & instance()
Access the global singleton instance.
const core::layer_info & extend(country_table which, core::tsv_document doc, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table.
std::string get_language(const std::string &cca2, std::uint64_t call_seed) const
Draw a language a person of a country speaks, in proportion to the share of the population that speak...
void load(const std::filesystem::path &path)
Load country data from a TSV file or a tier directory, replacing all data and layers loaded before.
country get_country(const country_request &request, std::uint64_t call_seed) const
Generate a deterministic country with per-call selection options.
std::vector< country > get_country_batch(const country_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of country results, deterministically.
void for_each_locale(const locale_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of locales with a batch seed drawn like get_locale() draws its own.
country find_country(const std::string &cca2) const
Look up a country by its ISO 3166-1 alpha-2 code.
country get_country(const country_request &request)
Generate a random country with per-call selection options.
std::vector< time_zone > time_zones(std::string_view country) const
Every time zone of a country, largest share first, then by name, at the default reference date.
std::vector< locale > locales(std::string_view country) const
Every locale of a country, largest share first, then by tag.
void for_each_time_zone(const time_zone_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of time zones to a sink without holding the batch.
cntg()=default
Default constructor — creates an empty generator with no data.
time_zone find_time_zone(std::string_view name, std::chrono::year_month_day reference_date=time_zone_request::default_reference_date) const
Look up a time zone by its IANA name.
year_span holiday_years(std::string_view country) const
The years holidays() serves for a country.
std::size_t country_count() const
Return the number of loaded countries.
bool has_data() const
Check whether any data has been loaded.
locale get_locale(const locale_request &request, std::uint64_t call_seed) const
Draw a locale of a country, by speakers, deterministically.
locale find_locale(std::string_view tag) const
Look up a locale by its tag.
std::vector< locale > get_locale_batch(const locale_request &request, const core::batch_options &options)
Generate a batch of locales from a seed drawn like get_locale() draws its own.
Return type for country generation, holding all data fields.
std::vector< std::string > language_list() const
The language names of languages as a list.
friend std::ostream & operator<<(std::ostream &os, const country &c)
Stream the common name to an output stream.
std::uint64_t seed() const
Retrieve the random seed used to generate this country.
std::string cca3
ISO 3166-1 alpha-3 code (e.g. "BRA").
std::string ccn3
ISO 3166-1 numeric code (e.g. "076").
double longitude
WGS84 longitude in decimal degrees.
std::string idd_suffix
IDD primary suffix (e.g. "5").
std::string timezones
Semicolon-separated UTC offset strings.
std::string demonym_f
English female demonym (e.g. "Brazilian").
std::uint64_t area
Total land area in km².
std::string flag_emoji
Unicode flag emoji.
std::string name_official
Official English name (e.g. "Federative Republic of Brazil").
std::string language_shares
Semicolon-separated Name:percent pairs, largest first, for the languages at least 1% of the populatio...
std::string region
UN geoscheme region (e.g. "Americas").
std::string continent
Continent (semicolon-separated if multiple).
const std::string & demonym(sex bio_sex) const
The English demonym for a person of this country.
const core::seed_signature & signature() const
The seed and the data this country was drawn from.
std::string capital
Capital city (semicolon-separated if multiple).
std::string cca2
ISO 3166-1 alpha-2 code (e.g. "BR").
std::string start_of_week
Start of the week: "monday", "sunday", or "saturday".
std::string to_json() const
This country as a JSON document.
bool un_member
Whether the country is a UN member.
std::string borders
Semicolon-separated ISO alpha-3 border codes.
std::string name_common
Common English name (e.g. "Brazil").
bool independent
Whether the country is an independent sovereign state.
std::string income_level
World Bank income level (e.g. "Upper middle income").
double latitude
WGS84 latitude in decimal degrees.
std::string driving_side
Driving side: "right" or "left".
std::string currency_symbol
Primary currency symbol (e.g. "R$").
std::string primary_language() const
The language most of the population speaks.
std::string demonym_m
English male demonym (e.g. "Brazilian").
std::string tld
Country-code top-level domain (e.g. ".br").
std::string languages
Semicolon-separated language names (e.g. "Portuguese").
void to_json(core::json_writer &w) const
Write this country as a JSON object.
std::string idd_root
IDD root (e.g. "+5").
std::uint64_t population
Total population.
bool landlocked
Whether the country is landlocked.
std::string subregion
UN geoscheme subregion (e.g. "South America").
std::string currency_code
Primary ISO 4217 currency code (e.g. "BRL").
std::vector< language_share > language_distribution() const
The languages and the share of the population that speaks each.
std::string currency_name
Primary currency name (e.g. "Brazilian real").
A locale of a country: its tag, CLDR formats and calendar data.
std::string date_long
LDML long date pattern.
std::string pm
Day period after noon.
std::string language_name
English, as country::language_shares writes it: "Portuguese".
std::string tag
BCP 47, minimised: "pt-BR", "zh-TW", "sr-Latn-RS".
std::string currency_symbol
This locale's symbol for it: "R$".
std::string to_json() const
This locale as a JSON document.
std::string currency_code
The country's ISO 4217 code: "BRL".
std::string decimal
Decimal symbol: ",".
std::string format_number(double value, unsigned max_fraction=3) const
Write a number with the locale's decimal pattern.
std::string language
ISO 639 code: "pt".
std::array< std::string, 2 > eras
Abbreviated eras: BC, AD.
std::string time_short
LDML short time pattern: "HH:mm".
std::string date_full
LDML full date pattern.
std::array< std::string, 12 > months
Wide month names, format context, January first.
std::uint8_t currency_digits
Its ISO 4217 minor unit.
friend std::ostream & operator<<(std::ostream &os, const locale &l)
Stream the tag.
std::string basis
"cldr", "inherited" or "fallback" (doc/data.md).
std::string time_medium
LDML medium time pattern: "HH:mm:ss".
std::string group
Grouping symbol: ".".
std::string date_medium
LDML medium date pattern.
double share
Percent of the country that speaks the language (CLDR).
std::string number_pattern
CLDR decimal pattern: "#,##0.###".
std::string format_date(std::chrono::year_month_day d, date_style style=date_style::short_) const
Write a date with one of the locale's date patterns.
std::string format_money(const core::money &m) const
Write an amount with the locale's currency pattern.
std::string currency_spacing
Put between a letter symbol and the number; usually U+00A0.
std::string am
Day period before noon.
std::string script
ISO 15924 code when the tag needs one ("Latn"); else empty.
std::string format_time(std::chrono::hh_mm_ss< std::chrono::seconds > t, time_style style=time_style::short_) const
Write a time of day with one of the locale's time patterns.
std::chrono::weekday weekend_end
Last day of the weekend.
std::array< std::string, 7 > weekdays_abbr
Abbreviated weekday names, Sunday first.
std::array< std::string, 12 > months_abbr
Abbreviated month names.
std::string minus
Minus sign: "-".
void to_json(core::json_writer &w) const
Write this locale as a JSON object (doc/usage.md, "Locales").
std::uint64_t seed() const
The seed this locale was drawn with.
const core::seed_signature & signature() const
The seed and the data this locale was drawn from.
std::string currency_pattern
CLDR currency pattern: "¤ #,##0.00".
std::chrono::weekday weekend_start
First day of the weekend.
std::string country_code
ISO 3166-1 alpha-2: "BR".
std::array< std::string, 7 > weekdays
Wide weekday names, format context, Sunday first.
std::chrono::weekday first_day
First day of the week.
std::string date_short
LDML short date pattern: "dd/MM/y".
An IANA time zone of a country, with its rules.
std::string name
IANA name: "America/Sao_Paulo".
bool observes_dst
Any daylight time from the reference date on.
std::string basis
"tzdata", "geonames" or "fallback" (doc/data.md).
std::string country_code
ISO 3166-1 alpha-2: "BR".
friend std::ostream & operator<<(std::ostream &os, const time_zone &z)
Stream the IANA name.
std::chrono::seconds utc_offset
At the reference date, 12:00 UTC: east of UTC.
std::string to_json() const
This zone as a JSON document.
core::tz_rules rules
The zone's rules, from the tier's first year on.
void to_json(core::json_writer &w) const
Write this zone as a JSON object; the offset as ±HH:MM.
std::chrono::local_seconds to_local(std::chrono::sys_seconds t) const
Local time at a UTC instant.
std::string abbreviation
At the reference date: "-03", "CEST".
bool dst
At the reference date: daylight time.
std::uint64_t seed() const
The seed this zone was drawn with.
std::chrono::sys_seconds to_utc(std::chrono::local_seconds t, core::tz_choose choose=core::tz_choose::earliest) const
The UTC instant of a local time.
double share
Percent of the country's population in the zone (GeoNames).
const core::seed_signature & signature() const
The seed and the data this zone was drawn from.
date_style
The length of a date format (CLDR dateFormats).
@ medium
‘d 'de’ MMM 'de' y`.
@ short_
dd/MM/y in pt-BR.
@ full
‘EEEE, d 'de’ MMMM 'de' y`.
@ long_
‘d 'de’ MMMM 'de' y`.
measurement
A country's measurement system (CLDR measurementData).
@ us
The United States and Liberia.
@ metric
Everywhere else.
@ uk
The United Kingdom and, in CLDR 47, Myanmar.
country_table
The tables of the country data, for loading and extending it.
@ holiday_years
holiday_years.tsv: the years each country's holidays are served for.
@ zones
zones.tsv: each country's IANA zones and their rules.
@ locales
locales.tsv: each country's locale tags and their CLDR formats.
@ countries
countries.tsv: the countries.
@ currencies
currencies.tsv: the ISO 4217 minor unit of each currency.
@ holiday_events
holiday_events.tsv: the dates of lunar and other calendar events.
@ zone_links
zone_links.tsv: old zone names and the zones they now name.
@ holidays
holidays.tsv: each country's holiday rules.
time_style
The length of a time format (CLDR timeFormats; the long and full ones name a zone and are not stored)...
holiday_kind
What kind of day a holiday is (python-holidays' categories).
@ public_
A public holiday; always returned.
@ observance
A named day that is not a day off (Mother's Day); on request.
@ bank
A bank holiday; always returned.
@ optional
A day off for some (Brazil's Carnival, government-only days); on request.
Per-call country selection options.
bool weighted
Population-weighted selection (default); false selects uniformly.
std::optional< std::string > region
UN geoscheme region; unset selects from all loaded countries. An empty or unknown region throws std::...
What cntg::holidays() returns.
bool optional
Include the days off for some (holiday_kind::optional).
bool observances
Include the days that are not days off (holiday_kind::observance).
std::string country
ISO 3166-1 alpha-2 code, any case; required.
int year
The year; required.
One day of a holiday in a year.
std::string to_json() const
This holiday as a JSON document.
holiday_kind kind
The kind of day.
std::chrono::year_month_day date
The day itself.
std::string key
Stable: "christmas_day", "eid_al_fitr_holiday".
void to_json(core::json_writer &w) const
Write this holiday as a JSON object, dates as YYYY-MM-DD.
std::string name
In the country's language, as python-holidays writes it: "Natal".
std::optional< std::chrono::year_month_day > observed
Only when a weekend rule moves the day off.
std::string name_en
In English: "Christmas Day".
unsigned day
1-based day of a multi-day holiday (Eid's second day is 2).
One language of a country and the share of its population that speaks it.
double percent
Percent of the population that speaks it, 0 to 100.
std::string name
English name of the language (for example "Portuguese").
Per-call locale selection options (cntg::get_locale()).
std::optional< std::string > language
ISO 639 code (fr), any case; narrows the draw to that language.
bool weighted
Weighted by speakers (default); false makes every eligible tag, and every eligible country,...
std::optional< std::string > country
ISO 3166-1 alpha-2 code, any case; unset draws the country as get_country() does (or by speakers of l...
Per-call time-zone selection options (cntg::get_time_zone()).
static constexpr std::chrono::year_month_day default_reference_date
The date whose offset a result reports, at 12:00 UTC, unless set.
std::optional< std::string > country
ISO 3166-1 alpha-2 code, any case; unset draws the country as get_country() does.
bool weighted
Weighted by population (default); false makes every zone of the country, and every country,...
std::chrono::year_month_day reference_date
The date whose offset, abbreviation and daylight flag are reported.
The years a country's holidays are served for (holiday_years()).
friend bool operator==(const year_span &, const year_span &)=default
Equal when both years are.
int from
First year.
int to
Last year.