4#ifndef DASMIG_COUNTRYGEN_HPP
5#define DASMIG_COUNTRYGEN_HPP
25#include <system_error>
30#ifdef DASMIG_COUNTRY_EMBED
31#include "countrygen_data.hpp"
33#ifdef DASMIG_COUNTRY_EMBED_LOCALE
34#include "countrygen_locale_data.hpp"
42DASMIG_CORE_REQUIRE(1, 4);
182 std::vector<std::string> out;
184 while (!text.empty())
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);
197 return parse_shares_(
language_shares).value_or(std::vector<language_share>{});
205 if (!shares.empty())
return shares.front().name;
207 return list.empty() ? std::string{} : std::move(list.front());
213 [[nodiscard]]
const std::string&
demonym(sex bio_sex)
const
215 const bool male = bio_sex == sex::male;
218 return wanted.empty() ? other : wanted;
224 [[nodiscard]] std::uint64_t
seed()
const
226 return _signature.seed;
232 [[nodiscard]]
const core::seed_signature&
signature()
const
245 auto list = [&w](std::string_view name, std::string_view text) {
246 w.key(name).begin_array();
247 while (!text.empty())
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);
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();
292 _signature.to_json(w);
307 operator std::string() const
323 core::seed_signature _signature;
327 template <
typename Visit>
328 [[nodiscard]]
static bool for_each_share_(std::string_view text,
const Visit& visit)
330 while (!text.empty())
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);
345 [[nodiscard]]
static std::optional<std::vector<language_share>> parse_shares_(std::string_view text)
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});
365 std::optional<std::string>
region{std::nullopt};
431namespace country_detail
434[[nodiscard]]
inline std::string upper(std::string_view s)
439 if (c >=
'a' && c <=
'z') c =
static_cast<char>(c -
'a' +
'A');
444[[nodiscard]]
inline std::string lower(std::string_view s)
449 if (c >=
'A' && c <=
'Z') c =
static_cast<char>(c -
'A' +
'a');
454[[nodiscard]]
constexpr bool is_letter(
char c)
noexcept
456 return (c >=
'A' && c <=
'Z') || (c >=
'a' && c <=
'z');
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"};
465inline constexpr std::array<std::string_view, 3> measurement_names{
"metric",
"us",
"uk"};
467[[nodiscard]]
inline std::optional<std::chrono::weekday> parse_weekday(std::string_view code)
469 for (
unsigned i = 0; i < weekday_codes.size(); ++i)
471 if (weekday_codes.at(i) == code)
return std::chrono::weekday{i};
478inline void append_padded(std::string& out, std::int64_t v, std::size_t width)
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);
488[[nodiscard]]
inline std::string iso_date(std::chrono::year_month_day d)
491 append_padded(out,
static_cast<int>(d.year()), 4);
493 append_padded(out,
static_cast<unsigned>(d.month()), 2);
495 append_padded(out,
static_cast<unsigned>(d.day()), 2);
500[[nodiscard]]
inline std::optional<std::chrono::year_month_day> parse_iso_date(std::string_view t)
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;
516template <
typename Literal,
typename Field>
518bool walk_pattern(std::string_view p,
const Literal& literal,
const Field& field)
525 if (i + 1 < p.size() && p[i + 1] ==
'\'')
527 literal(std::string_view{
"'"});
532 std::size_t j = i + 1;
535 if (j == p.size())
return false;
541 if (j + 1 < p.size() && p[j + 1] ==
'\'')
549 literal(std::string_view{text});
556 while (j < p.size() && p[j] == p[i]) ++j;
557 field(p[i],
static_cast<unsigned>(j - i));
562 while (j < p.size() && p[j] !=
'\'' && !is_letter(p[j])) ++j;
563 literal(p.substr(i, j - i));
571[[nodiscard]]
constexpr bool supported_field(
char c,
unsigned n,
bool time)
noexcept
577 case 'a':
return n <= 3;
581 case 's':
return n <= 2;
582 default:
return false;
587 case 'G':
return n <= 3;
592 case 'c':
return n <= 4;
593 case 'd':
return n <= 2;
594 default:
return false;
599[[nodiscard]]
inline std::string check_pattern(std::string_view p,
bool time)
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))
607 error =
"unsupported field '" + std::string(n, c) +
"'";
610 if (!closed)
return "unterminated quote";
617inline constexpr char currency_mark =
'\x01';
618inline constexpr char minus_mark =
'\x02';
622 std::string positive_prefix, positive_suffix, negative_prefix, negative_suffix;
623 std::size_t primary{0}, secondary{0};
624 std::size_t min_integer{1}, min_fraction{0}, max_fraction{0};
627[[nodiscard]]
constexpr bool number_char(
char c)
noexcept
629 return c ==
'#' || c ==
'0' || c ==
',' || c ==
'.';
634inline bool split_subpattern(std::string_view p, std::string& prefix, std::string& body, std::string& suffix)
636 constexpr std::string_view currency_sign =
"\xC2\xA4";
638 for (std::size_t i = 0; i < p.size(); ++i)
641 if (part == 1 && !number_char(c)) part = 2;
642 if (part == 0 && number_char(c)) part = 1;
648 auto& affix = part == 0 ? prefix : suffix;
651 if (i + 1 < p.size() && p[i + 1] ==
'\'')
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);
663 if (p.substr(i, currency_sign.size()) == currency_sign)
665 affix += currency_mark;
666 i += currency_sign.size() - 1;
674 if (c ==
'%' || c ==
'+' || c ==
'*' || c ==
'@' || c ==
';' || c == currency_mark || c == minus_mark)
680 return !body.empty();
685[[nodiscard]]
inline number_format parse_number_pattern(std::string_view text,
bool currency, std::string& error)
690 std::size_t semi = std::string_view::npos;
692 for (std::size_t i = 0; i < text.size(); ++i)
694 if (text[i] ==
'\'') quoted = !quoted;
695 if (text[i] ==
';' && !quoted)
702 if (!split_subpattern(text.substr(0, semi), f.positive_prefix, body, f.positive_suffix))
704 error =
"unsupported number pattern";
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);
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))
716 error =
"unsupported number pattern";
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)
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)
730 error =
"unsupported number pattern";
734 if (semi != std::string_view::npos)
736 std::string negative_body;
737 if (!split_subpattern(text.substr(semi + 1), f.negative_prefix, negative_body, f.negative_suffix))
739 error =
"unsupported negative subpattern";
745 f.negative_prefix = minus_mark + f.positive_prefix;
746 f.negative_suffix = f.positive_suffix;
748 const auto has_currency = [](
const std::string& a,
const std::string& b) {
749 return a.contains(currency_mark) || b.contains(currency_mark);
751 if (currency != has_currency(f.positive_prefix, f.positive_suffix) ||
752 currency != has_currency(f.negative_prefix, f.negative_suffix))
754 error = currency ?
"a currency pattern needs ¤" :
"a number pattern cannot have ¤";
760[[nodiscard]]
inline char32_t first_code_point(std::string_view s)
noexcept
762 if (s.empty())
return 0;
763 const auto b =
static_cast<unsigned char>(s[0]);
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};
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);
775[[nodiscard]]
inline char32_t last_code_point(std::string_view s)
noexcept
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));
786[[nodiscard]]
constexpr bool symbol_or_space(
char32_t c)
noexcept
790 return c == U
' ' || c == U
'$' || c == U
'+' || c == U
'<' || c == U
'=' || c == U
'>' || c == U
'^' ||
791 c == U
'`' || c == U
'|' || c == U
'~';
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;
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;
810[[nodiscard]]
inline std::pair<std::string, std::string> decimal_digits(
double v, std::size_t max_fraction)
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})
827 for (
char& c : std::views::reverse(*digits))
832 return {integer, fraction};
837 integer.insert(integer.begin(),
'1');
838 return {integer, fraction};
843enum class rule_form : std::uint8_t
856 rule_form form{rule_form::fixed};
857 unsigned month{1}, day{1};
859 std::chrono::weekday weekday{};
863[[nodiscard]]
constexpr bool key_char(
char c)
noexcept
865 return (c >=
'a' && c <=
'z') || (c >=
'0' && c <=
'9') || c ==
'_';
868[[nodiscard]]
constexpr bool valid_key(std::string_view k)
noexcept
870 return !k.empty() && std::ranges::all_of(k, key_char);
874[[nodiscard]]
inline std::optional<int> parse_offset(std::string_view t)
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)
883 return t[0] ==
'-' ? -*v : *v;
887[[nodiscard]]
inline bool parse_month_day(std::string_view t,
unsigned& month,
unsigned& day)
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;
895 return std::chrono::year_month_day{std::chrono::year{2024}, std::chrono::month{month}, std::chrono::day{day}}.ok();
899[[nodiscard]]
inline std::optional<holiday_rule> parse_rule(std::string_view t)
902 if (t.starts_with(
"event:"))
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;
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}})
916 if (t.starts_with(name))
918 const auto n = parse_offset(t.substr(name.size()));
919 if (!n)
return std::nullopt;
925 if (t.size() > 3 && t[2] ==
':')
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;
940 if (t.size() == 9 && (t[5] ==
'>' || t[5] ==
'<'))
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;
948 if (!parse_month_day(t, r.month, r.day))
return std::nullopt;
949 r.form = rule_form::fixed;
954[[nodiscard]]
constexpr std::chrono::sys_days easter(
int y)
noexcept
956 const int a = y % 19;
957 const int b = y / 100;
958 const int c = y % 100;
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;
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)}};
974[[nodiscard]]
constexpr std::chrono::sys_days orthodox_easter(
int y)
noexcept
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};
987[[nodiscard]]
inline int year_of(std::chrono::sys_days d)
989 return static_cast<int>(std::chrono::year_month_day{d}.year());
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)
996 using namespace std::chrono;
1000 case rule_form::fixed:
1002 const year_month_day d{year{y}, month{r.month}, day{r.day}};
1003 if (d.ok()) out.push_back(sys_days{d});
1006 case rule_form::nth:
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)});
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));
1020 case rule_form::after:
1021 case rule_form::before:
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));
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:
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)
1037 out.push_back(*it + days{r.n});
1045struct observed_policy
1047 enum class type : std::uint8_t
1054 type kind{type::none};
1056 std::array<std::optional<std::pair<std::chrono::weekday, int>>, 7> moves{};
1057 bool overlap{
false};
1061[[nodiscard]]
inline std::optional<observed_policy> parse_policy(std::string_view t)
1064 p.text = std::string{t};
1065 if (t ==
"none")
return p;
1066 if (t.ends_with(
",overlap"))
1071 if (t ==
"next_weekday")
1073 p.kind = observed_policy::type::next_weekday;
1076 if (t ==
"weekend>next")
1078 p.kind = observed_policy::type::weekend_next;
1081 p.kind = observed_policy::type::map;
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};
1099 std::string key, name, name_en;
1103 observed_policy observed;
1105 const std::vector<std::chrono::sys_days>* events{
nullptr};
1108[[nodiscard]]
inline std::optional<holiday_kind> parse_kind(std::string_view t)
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;
1117[[nodiscard]]
constexpr std::string_view kind_name(holiday_kind k)
noexcept
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";
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)
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;
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};
1148 if (p.kind == observed_policy::type::weekend_next)
1150 sys_days t = start + std::chrono::days{days};
1151 for (
unsigned i = 0; i < days; ++i)
1153 if (!is_weekend(start + std::chrono::days{i}))
continue;
1156 taken.insert(day_of(t));
1157 t += std::chrono::days{1};
1161 for (
unsigned i = 0; i < days; ++i)
1163 const sys_days d = start + std::chrono::days{i};
1166 if (p.kind == observed_policy::type::next_weekday)
1168 if (!is_weekend(d))
continue;
1169 t = d + std::chrono::days{1};
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};
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())
1182 moved[i] = sys_days{std::chrono::days{it->second}};
1187 shared.emplace(
id, day_of(t));
1188 taken.insert(day_of(t));
1263 if (!d.ok())
throw std::invalid_argument(
"format_date: not a valid date");
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();
1270 write_pattern_(pattern,
false,
"format_date", out, [&](
char c,
unsigned n) {
1271 const int era_year = y > 0 ? y : 1 - y;
1274 case 'G': out +=
eras.at(y > 0 ? 1 : 0);
break;
1276 if (n == 2) country_detail::append_padded(out, era_year % 100, 2);
1277 else country_detail::append_padded(out, era_year, n);
1282 else country_detail::append_padded(out, month, n);
1284 case 'd': country_detail::append_padded(out,
static_cast<unsigned>(d.day()), n);
break;
1297 [[nodiscard]] std::string
format_time(std::chrono::hh_mm_ss<std::chrono::seconds> t,
1300 if (t.is_negative() || t.hours().count() > 23)
1302 throw std::invalid_argument(
"format_time: the time must be from 00:00:00 to 23:59:59");
1304 const auto h = t.hours().count();
1307 [&](
char c,
unsigned n) {
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;
1332 [[nodiscard]] std::string
format_number(
double value,
unsigned max_fraction = 3)
const
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, {});
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});
1371 [[nodiscard]] std::uint64_t
seed()
const {
return _signature.seed; }
1375 [[nodiscard]]
const core::seed_signature&
signature()
const {
return _signature; }
1391 .field(
"share",
share)
1392 .field(
"basis",
basis)
1413 .field(
"group",
group)
1414 .field(
"minus",
minus)
1422 .field(
"first_day", country_detail::weekday_names.at(
first_day.c_encoding()))
1425 .value(country_detail::weekday_names.at(
weekend_start.c_encoding()))
1426 .value(country_detail::weekday_names.at(
weekend_end.c_encoding()))
1428 .field(
"measurement", country_detail::measurement_names.at(
static_cast<std::size_t
>(
measurement)))
1430 _signature.to_json(w);
1438 core::json_writer w;
1445 operator std::string() const
1461 core::seed_signature _signature;
1463 template <
typename Field>
1464 static void write_pattern_(std::string_view pattern,
bool time, std::string_view caller, std::string& out,
1467 if (
const auto error = country_detail::check_pattern(pattern, time); !error.empty())
1469 throw std::invalid_argument(std::string{caller} +
": " + error +
" in '" + std::string{pattern} +
"'");
1471 (void)country_detail::walk_pattern(
1472 pattern, [&out](std::string_view text) { out += text; }, std::forward<Field>(field));
1475 static country_detail::number_format number_format_(std::string_view pattern,
bool currency,
1476 std::string_view caller)
1479 auto f = country_detail::parse_number_pattern(pattern, currency, error);
1482 throw std::invalid_argument(std::string{caller} +
": " + error +
" in '" + std::string{pattern} +
"'");
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
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');
1496 if (f.primary == 0 || integer.size() <= f.primary)
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)
1508 groups.push_back(rest.substr(rest.size() - f.secondary));
1509 rest.remove_suffix(f.secondary);
1511 groups.push_back(rest);
1512 for (
auto it = groups.rbegin(); it != groups.rend(); ++it)
1514 if (it != groups.rbegin()) number +=
group;
1518 if (!fraction.empty())
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)
1528 if (c == country_detail::currency_mark) out += symbol;
1529 else if (c == country_detail::minus_mark) out +=
minus;
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)))
1541 if (!suffix.empty() && suffix.front() == country_detail::currency_mark &&
1542 !country_detail::symbol_or_space(country_detail::first_code_point(symbol)))
1546 expand(suffix, out);
1558 std::chrono::July / 1};
1591 [[nodiscard]] std::chrono::local_seconds
to_local(std::chrono::sys_seconds t)
const {
return rules.to_local(t); }
1598 [[nodiscard]] std::chrono::sys_seconds
to_utc(std::chrono::local_seconds t,
1599 core::tz_choose choose = core::tz_choose::earliest)
const
1601 return rules.to_utc(t, choose);
1606 [[nodiscard]] std::uint64_t
seed()
const {
return _signature.seed; }
1610 [[nodiscard]]
const core::seed_signature&
signature()
const {
return _signature; }
1616 std::string offset{
utc_offset.count() < 0 ?
"-" :
"+"};
1618 country_detail::append_padded(offset, total / 3600, 2);
1620 country_detail::append_padded(offset, total / 60 % 60, 2);
1621 if (total % 60 != 0)
1624 country_detail::append_padded(offset, total % 60, 2);
1627 .field(
"name",
name)
1629 .field(
"share",
share)
1630 .field(
"basis",
basis)
1631 .field(
"utc_offset", offset)
1636 _signature.to_json(w);
1644 core::json_writer w;
1651 operator std::string() const
1667 core::seed_signature _signature;
1683 std::chrono::year_month_day
date{};
1695 w.begin_object().field(
"date", country_detail::iso_date(
date)).key(
"observed");
1697 else w.value(
nullptr);
1699 .field(
"name",
name)
1701 .field(
"kind", country_detail::kind_name(
kind))
1710 core::json_writer w;
1757 static cntg inst{auto_probe_tag{}};
1786 std::uint64_t call_seed)
const
1791 auto r = core::rng{call_seed}.split(
"country");
1794 :
static_cast<std::size_t
>(core::bounded(r, _countries.size()));
1795 return result_(_countries[idx], call_seed);
1798 const auto it = _region_index.find(*request.
region);
1799 if (it == _region_index.end())
1801 throw std::invalid_argument(
"No countries found for region: " + *request.
region);
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);
1823 const core::batch_options& options)
1844 const core::batch_options& options,
1845 std::uint64_t batch_seed)
const
1847 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
1864 template <
typename Sink>
1866 std::uint64_t batch_seed, Sink&& sink)
const
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));
1879 template <
typename Sink>
1895 const auto it = _cca2_index.find(cca2);
1896 if (it == _cca2_index.end())
1898 throw std::invalid_argument(
"No country found for CCA2 code: " + cca2);
1900 return result_(_countries[it->second], 0);
1914 [[nodiscard]] std::string
get_language(
const std::string& cca2, std::uint64_t call_seed)
const
1917 const auto shares = c.language_distribution();
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());
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;
1963 require_locale_table_(country_table::locales);
1964 const auto& m = _locale;
1965 std::optional<std::string> language;
1971 cc = known_country_(*request.
country);
1975 cc = draw_country_(call_seed, request.
weighted, m.locale_countries, m.locales_by_country,
"locale_country");
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);
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;
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;
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()))];
2001 std::vector<std::size_t> items;
2002 std::vector<double> weights;
2003 for (
const auto i : it->second.items)
2005 if (m.locales[i].language != *language)
continue;
2007 weights.push_back(m.locales[i].share);
2011 throw std::invalid_argument(
"No locale of language " + *language +
" for country: " + cc);
2013 index = items[request.
weighted ? core::discrete{weights}(r)
2014 :
static_cast<std::size_t
>(core::bounded(r, items.size()))];
2016 locale out = m.locales[index];
2017 out._signature = {.seed = call_seed,
2018 .data = drawn ? core::hash_combine(locale_hash_(),
data_hash()) : locale_hash_()};
2028 const core::batch_options& options)
2043 const core::batch_options& options,
2044 std::uint64_t batch_seed)
const
2046 return core::run_batch(options, batch_seed,
2047 [&](std::uint64_t item_seed) {
return get_locale(request, item_seed); });
2056 template <
typename Sink>
2058 std::uint64_t batch_seed, Sink&& sink)
const
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));
2071 template <
typename Sink>
2074 for_each_locale(request, options, _engine(), std::forward<Sink>(sink));
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())
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)
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())
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_()};
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())
2125 for (
const auto i : it->second.items)
2127 out.push_back(_locale.locales[i]);
2128 out.back()._signature = {.seed = 0, .data = locale_hash_()};
2163 require_locale_table_(country_table::zones);
2164 const auto& m = _locale;
2169 cc = known_country_(*request.
country);
2173 cc = draw_country_(call_seed, request.
weighted, m.zone_countries, m.zones_by_country,
"time_zone_country");
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()))];
2184 .data = drawn ? core::hash_combine(locale_hash_(), data_hash()) : locale_hash_()});
2193 const core::batch_options& options)
2208 const core::batch_options& options,
2209 std::uint64_t batch_seed)
const
2211 return core::run_batch(options, batch_seed,
2212 [&](std::uint64_t item_seed) {
return get_time_zone(request, item_seed); });
2221 template <
typename Sink>
2223 std::uint64_t batch_seed, Sink&& sink)
const
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));
2236 template <
typename Sink>
2256 std::chrono::year_month_day reference_date =
2259 require_locale_table_(country_table::zones);
2260 auto it = _locale.zone_by_name.find(name);
2261 if (it == _locale.zone_by_name.end())
2263 if (
const auto link = _locale.zone_links.find(name); link != _locale.zone_links.end())
2265 it = _locale.zone_by_name.find(link->second);
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_()});
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())
2285 for (
const auto i : it->second.items)
2288 {.seed = 0, .data = locale_hash_()}));
2312 const auto& hc = holiday_country_(request.
country);
2313 check_holiday_year_(hc, request.
year);
2332 bool optional =
false)
const
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)
2340 for (
const auto& h : evaluate_(hc, year,
optional,
false))
2342 if (h.date == date || h.observed == date)
return true;
2356 return holiday_country_(
country).years;
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;
2377 return std::ranges::any_of(_locale_tables, [](
const core::layered_table& t) {
return !t.empty(); });
2386 return !_countries.empty();
2393 return _countries.size();
2407 void load(
const std::filesystem::path& path)
2410 if (std::filesystem::is_directory(path, ec))
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));
2417 if (!std::filesystem::is_regular_file(path, ec))
return;
2418 (void)
extend(core::tsv_document::load(path), core::layer_mode::replace);
2437 [[nodiscard]]
bool load(dataset tier)
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_)
2448 docs.at(index_(t)) = core::load_resource(dir +
"/" + std::string{file_name_(t)}, embedded_locale_files_(), root);
2450 replace_all_(std::move(*doc), std::move(docs));
2466 const core::layer_info&
extend(
const std::filesystem::path& file,
2467 core::layer_mode mode = core::layer_mode::override)
2469 return extend(core::tsv_document::load(file), mode);
2479 core::layer_mode mode = core::layer_mode::override)
2481 return extend(core::tsv_document::parse(text, std::move(source)), mode);
2489 const core::layer_info&
extend(core::tsv_document doc,
2490 core::layer_mode mode = core::layer_mode::override)
2492 const auto& info = _table.extend(std::move(doc), mode, [](
const core::tsv_row& row) {
2514 core::layer_mode mode = core::layer_mode::override)
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();
2531 core::layer_mode mode = core::layer_mode::override)
2533 return extend(which, core::tsv_document::load(file), mode);
2544 core::layer_mode mode = core::layer_mode::override)
2546 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
2551 [[nodiscard]]
const std::vector<core::layer_info>&
layers()
const
2553 return _table.layers();
2561 if (which == country_table::countries)
return _table.layers();
2562 return _locale_tables.at(index_(which)).layers();
2569 [[nodiscard]] std::uint64_t
data_hash()
const {
return _table.data_hash(); }
2572 static constexpr std::string_view library_name_ =
"country-generator";
2573 static constexpr std::string_view data_file_ =
"countries.tsv";
2575 static core::table_schema schema_()
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"}};
2588 static country parse_(
const core::tsv_row& row)
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)
2604 throw row.error(
"latitude",
"must be between -90 and 90");
2606 if (c.longitude < -180.0 || c.longitude > 180.0)
2608 throw row.error(
"longitude",
"must be between -180 and 180");
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) {}))
2621 throw row.error(
"language_shares",
"must be Name:percent pairs separated by ';', each percent above 0 and at most 100");
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",
"");
2641 static double weight_(
const country& c)
2643 return static_cast<double>(std::max<std::uint64_t>(c.population, 1));
2648 std::vector<std::size_t> indices;
2649 core::discrete distribution{1.0};
2652 core::layered_table _table{schema_()};
2653 std::vector<country> _countries;
2654 core::discrete _distribution{1.0};
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()};
2659 struct auto_probe_tag {};
2661 explicit cntg(auto_probe_tag )
2663 if (!
load(dataset::lite)) (void)
load(dataset::full);
2666 static std::span<const core::embedded_file> embedded_files_()
2668#ifdef DASMIG_COUNTRY_EMBED
2669 return embedded::countrygen::files;
2675 void require_data_()
const
2677 if (_countries.empty())
2679 throw std::runtime_error(
"No country data loaded. Call load() first.");
2683 [[nodiscard]] country result_(
const country& c, std::uint64_t call_seed)
const
2686 result._signature = {.seed = call_seed, .data =
data_hash()};
2691 void rebuild_(
bool with_locale =
true)
2695 for (
const auto& row : _table.rows())
countries.push_back(parse_(row));
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;
2702 std::vector<double> weights;
2704 std::map<std::string, std::vector<double>, std::less<>> region_weights;
2705 for (std::size_t i = 0; i <
countries.size(); ++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);
2713 distribution = core::discrete{weights};
2714 for (
auto& [name, entry] : regions)
2716 entry.distribution = core::discrete{region_weights[name]};
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);
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};
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_>;
2736 static constexpr std::size_t index_(country_table t) {
return static_cast<std::size_t
>(t) - 1; }
2738 static constexpr std::string_view file_name_(country_table t)
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";
2754 static core::table_schema locale_schema_(country_table t)
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;
2782 static locale_table_set make_locale_tables_()
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)}};
2795 static std::chrono::weekday weekday_cell_(
const core::tsv_row& row, std::string_view column)
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");
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)
2805 const auto items = row.list(column);
2806 if (items.size() != N || std::ranges::any_of(items, [](std::string_view s) { return s.empty(); }))
2808 throw row.error(column,
"expected " + std::to_string(N) +
" names separated by ';'");
2810 for (std::size_t i = 0; i < N; ++i) out.at(i) = std::string{items[i]};
2813 static locale parse_locale_(
const core::tsv_row& row)
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'; });
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))
2828 throw row.error(
"language",
"expected an ISO 639 code of 2 or 3 lowercase letters");
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)
2834 throw row.error(
"tag",
"expected language[-Script]-REGION from the language, script and country columns");
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")
2841 throw row.error(
"basis",
"expected cldr, inherited or fallback");
2843 for (
const auto& [column, field, time] :
2848 l.*field = row.get<std::string>(column);
2849 if (
const auto error = country_detail::check_pattern(l.*field, time); !error.empty())
2851 throw row.error(column, error);
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},
2867 l.*field = row.get<std::string>(column);
2869 (void)country_detail::parse_number_pattern(l.*field, currency, error);
2870 if (!error.empty())
throw row.error(column, error);
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");
2887 static bool is_currency_code_(std::string_view c)
2889 return c.size() == 3 && std::ranges::all_of(c, [](
char ch) {
return ch >=
'A' && ch <=
'Z'; });
2892 static time_zone parse_zone_(
const core::tsv_row& row)
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")
2903 throw row.error(
"basis",
"expected tzdata, geonames or fallback");
2907 z.rules = core::tz_rules::parse(row.get<std::string_view>(
"periods"), row.get_or<std::string_view>(
"posix",
""));
2909 catch (
const std::invalid_argument& e)
2911 throw row.error(
"periods", e.what());
2916 struct currency_entry
2918 std::string numeric;
2919 std::uint8_t digits{2};
2920 std::optional<std::uint8_t> cash_digits;
2923 static currency_entry parse_currency_(
const core::tsv_row& row)
2925 if (!is_currency_code_(row.get<std::string_view>(
"code")))
throw row.error(
"code",
"expected three capital letters");
2927 c.numeric = row.get<std::string>(
"numeric");
2928 if (c.numeric.size() != 3 || c.numeric.find_first_not_of(
"0123456789") != std::string::npos)
2930 throw row.error(
"numeric",
"expected three digits");
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"))
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);
2944 static country_detail::holiday_row parse_holiday_(
const core::tsv_row& row)
2946 if (!core::is_alpha2(row.get<std::string_view>(
"country")))
2948 throw row.error(
"country",
"expected an ISO 3166-1 alpha-2 code");
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");
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");
2974 std::string_view event;
2975 std::string_view country;
2976 std::chrono::sys_days date;
2979 static event_row parse_event_(
const core::tsv_row& row)
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))
2987 throw row.error(
"country",
"expected * (every country) or an ISO 3166-1 alpha-2 code");
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};
2995 static year_span parse_years_(
const core::tsv_row& row)
2997 if (!core::is_alpha2(row.get<std::string_view>(
"country")))
2999 throw row.error(
"country",
"expected an ISO 3166-1 alpha-2 code");
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");
3010 std::vector<std::size_t> items;
3011 core::discrete weighted{1.0};
3014 struct holiday_country
3017 unsigned weekend{0b1000001U};
3018 std::vector<country_detail::holiday_row> rows;
3019 std::map<std::string, std::vector<std::chrono::sys_days>, std::less<>> events;
3025 std::map<std::string, std::size_t, std::less<>> locale_by_tag;
3026 std::map<std::string, pick_list, std::less<>> locales_by_country;
3027 std::map<std::string, pick_list, std::less<>> locale_languages;
3028 pick_list locale_countries;
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;
3033 pick_list zone_countries;
3034 std::map<std::string, currency_entry, std::less<>>
currencies;
3035 std::map<std::string, holiday_country, std::less<>>
holidays;
3042 [[nodiscard]] locale_model build_locale_model_(
const locale_table_set& t,
bool strict)
const
3045 build_locales_(m, t, strict);
3046 build_zones_(m, t, strict);
3047 build_holidays_(m, t, strict);
3052 [[nodiscard]]
bool row_loaded_(
const core::tsv_row& row,
bool strict)
const
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");
3060 static void weigh_(pick_list& p,
const std::vector<double>& weights)
3062 if (!weights.empty()) p.weighted = core::discrete{weights};
3066 template <
typename Row,
typename Name>
3067 static void order_by_share_(pick_list& p,
const std::vector<Row>& rows, Name name)
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]);
3073 std::vector<double> weights;
3074 weights.reserve(p.items.size());
3075 for (
const auto i : p.items) weights.push_back(rows[i].share);
3080 template <
typename ByCountry>
3081 [[nodiscard]] pick_list countries_with_(
const ByCountry& by_country)
const
3084 std::vector<double> weights;
3085 for (std::size_t i = 0; i < _countries.size(); ++i)
3087 if (!by_country.contains(_countries[i].cca2))
continue;
3088 p.items.push_back(i);
3089 weights.push_back(weight_(_countries[i]));
3096 void build_locales_(locale_model& m,
const locale_table_set& t,
bool strict)
const
3100 m.currencies.emplace(std::string{row[
"code"]}, parse_currency_(row));
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())
3109 if (strict)
throw row.error(
"currency_code",
"no currencies.tsv row for " + l.currency_code);
3112 l.currency_digits = currency->second.digits;
3113 m.locales.push_back(std::move(l));
3115 std::map<std::string, std::map<std::string, double>, std::less<>> language_shares;
3116 for (std::size_t i = 0; i < m.locales.size(); ++i)
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);
3124 for (
auto& [cc, p] : m.locales_by_country)
3126 order_by_share_(p, m.locales, [](
const locale& l) ->
const std::string& { return l.tag; });
3128 for (
const auto& [language, shares] : language_shares)
3131 std::vector<double> weights;
3132 for (std::size_t i = 0; i < _countries.size(); ++i)
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);
3139 if (p.items.empty())
continue;
3141 m.locale_languages.emplace(language, std::move(p));
3143 m.locale_countries = countries_with_(m.locales_by_country);
3147 void build_zones_(locale_model& m,
const locale_table_set& t,
bool strict)
const
3151 if (row_loaded_(row, strict)) m.zones.push_back(parse_zone_(row));
3153 for (std::size_t i = 0; i < m.zones.size(); ++i)
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);
3159 if (!added && m.zones[it->second].basis ==
"geonames" && z.basis !=
"geonames") it->second = i;
3161 for (
auto& [cc, p] : m.zones_by_country)
3163 order_by_share_(p, m.zones, [](
const time_zone& z) ->
const std::string& { return z.name; });
3165 m.zone_countries = countries_with_(m.zones_by_country);
3168 const auto target = row.get<std::string_view>(
"zone");
3169 if (!m.zone_by_name.contains(target))
3171 if (strict)
throw row.error(
"zone",
"zone " + std::string{target} +
" is not loaded");
3174 m.zone_links.emplace(std::string{row[
"link"]}, std::string{target});
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*>>>;
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)
3189 for (
const auto& [date, row] : mine)
3191 auto near = std::ranges::min_element(dates, {}, [date](std::chrono::sys_days d) {
3192 return d > date ? (d - date).count() : (date - d).count();
3194 const auto gap = near == dates.end() ? 4 : std::abs((*near - date).count());
3197 throw row->error(
"date",
"no shared date of " + ev +
" within three days");
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)
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())
3216 dates = base->second;
3217 if (mine != own.end()) override_dates_(dates, mine->second, ev);
3219 else if (mine != own.end())
3221 for (
const auto& [date, row] : mine->second) dates.push_back(date);
3225 if (strict)
throw at.error(
"rule",
"no holiday_events.tsv dates for event " + ev);
3228 std::ranges::sort(dates);
3229 return &hc.events.emplace(ev, std::move(dates)).first->second;
3233 [[nodiscard]]
static unsigned weekend_mask_(
const locale& l)
3236 for (
unsigned d = l.weekend_start.c_encoding();; d = (d + 1) % 7)
3239 if (d == l.weekend_end.c_encoding())
break;
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)
3249 if (h.name.empty() && h.name_en.empty())
3251 if (names.second.empty())
3253 throw row.error(
"name_en",
"empty names need a row above with the same country and key");
3255 h.name = names.first;
3256 h.name_en = names.second;
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;
3266 static holiday_country& holiday_entry_(locale_model& m,
const std::map<std::string, year_span, std::less<>>& spans,
3267 const std::string& cc)
3269 auto [it, added] = m.holidays.try_emplace(cc);
3272 it->second.years = spans.at(cc);
3274 if (l != m.locales.end()) it->second.weekend = weekend_mask_(*l);
3280 void build_holidays_(locale_model& m,
const locale_table_set& t,
bool strict)
const
3282 shared_events_ shared;
3286 const auto e = parse_event_(row);
3287 if (e.country ==
"*")
3289 shared[e.event].push_back(e.date);
3291 else if (row_loaded_(row, strict))
3293 own[{e.country, e.event}].emplace_back(e.date, &row);
3296 for (
auto& [k, dates] : shared) std::ranges::sort(dates);
3297 std::map<std::string, year_span, std::less<>> spans;
3300 if (row_loaded_(row, strict)) spans.emplace(std::string{row[
"country"]}, parse_years_(row));
3303 std::map<std::pair<std::string_view, std::string_view>, std::pair<std::string_view, std::string_view>>
3307 if (!row_loaded_(row, strict))
continue;
3308 const std::string cc{row[
"country"]};
3309 if (!spans.contains(cc))
3311 if (strict)
throw row.error(
"country",
"no holiday_years.tsv row for " + cc);
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)
3319 h.events = event_dates_(hc, cc, h.rule.event, row, shared, own, strict);
3320 if (h.events ==
nullptr)
continue;
3322 hc.rows.push_back(std::move(h));
3324 for (
const auto& [cc, span] : spans) (void)holiday_entry_(m, spans, cc);
3327 locale_table_set _locale_tables{make_locale_tables_()};
3328 locale_model _locale;
3331 void apply_locale_(locale_documents docs, core::layer_mode mode,
bool reset)
3333 auto next = _locale_tables;
3334 for (
const auto t : locale_tables_)
3336 auto& doc = docs.at(index_(t));
3337 auto& tbl = next.at(index_(t));
3340 if (reset) tbl.clear();
3343 (void)tbl.extend(std::move(*doc), mode);
3345 auto built = build_locale_model_(next,
true);
3346 _locale_tables = std::move(next);
3347 _locale = std::move(built);
3351 void replace_all_(core::tsv_document countries, locale_documents docs)
3353 auto saved = _table;
3354 (void)_table.extend(std::move(countries), core::layer_mode::replace,
3355 [](
const core::tsv_row& row) { (void)parse_(row); });
3359 apply_locale_(std::move(docs), core::layer_mode::replace,
true);
3363 _table = std::move(saved);
3374 static std::optional<std::filesystem::path> find_locale_resources_(
const std::string& dir)
3376 for (
const auto& root : core::resource_candidates(library_name_))
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))
3385 return std::nullopt;
3388 static locale_documents read_locale_directory_(
const std::filesystem::path& dir)
3390 locale_documents docs;
3391 for (
const auto t : locale_tables_)
3393 const auto file = dir / file_name_(t);
3395 if (std::filesystem::is_regular_file(file, ec)) docs.at(index_(t)) = core::tsv_document::load(file);
3400 static std::span<const core::embedded_file> embedded_locale_files_()
3402#ifdef DASMIG_COUNTRY_EMBED_LOCALE
3403 return embedded::countrygen_locale::files;
3409 [[nodiscard]] std::uint64_t locale_hash_()
const
3411 std::uint64_t h = 0;
3412 for (
const auto& t : _locale_tables) h = core::hash_combine(h, t.
data_hash());
3416 void require_locale_table_(country_table t)
const
3418 if (_locale_tables.at(index_(t)).empty())
3420 throw std::runtime_error(
3421 "No locale data loaded. Load a tier with the locale tables, or define DASMIG_COUNTRY_EMBED_LOCALE.");
3426 [[nodiscard]] std::string known_country_(std::string_view code)
const
3429 auto cc = country_detail::upper(code);
3430 if (!_cca2_index.contains(cc))
throw core::unknown_country_error(code);
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
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;
3450 [[nodiscard]]
static time_zone zone_at_(
const time_zone& base, std::chrono::year_month_day reference,
3451 core::seed_signature signature)
3453 if (!reference.ok())
throw std::invalid_argument(
"time zone: the reference date is not valid");
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};
3460 z.observes_dst = z.rules.observes_dst_after(t);
3461 z._signature = signature;
3465 [[nodiscard]]
const holiday_country& holiday_country_(std::string_view country)
const
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);
3474 static void check_holiday_year_(
const holiday_country& hc,
int year)
3476 if (year < hc.years.from || year > hc.years.to)
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));
3487 [[nodiscard]]
static std::vector<holiday> evaluate_(
const holiday_country& hc,
int year,
bool optional,
3490 using std::chrono::sys_days;
3494 const country_detail::holiday_row* row;
3499 return (k != holiday_kind::optional || optional) && (k != holiday_kind::observance || observances);
3501 std::vector<entry> entries;
3502 std::vector<sys_days> starts;
3503 for (
int y = year - 1; y <= year + 1; ++y)
3505 for (
const auto& row : hc.rows)
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)
3511 for (
unsigned i = 0; i < row.days; ++i)
3513 entries.push_back({.date = s + std::chrono::days{i}, .row = &row, .day = i + 1, .start = s});
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;
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})
3528 std::set<int> taken;
3529 for (
const auto& e : entries)
3531 if (e.row->kind == kind) taken.insert(day_of(e.date));
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)
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)
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]};
3547 h.name = e.row->name;
3548 h.name_en = e.row->name_en;
3551 out.push_back(std::move(h));
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;
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.
@ 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.