![]() |
Country Generator 3.3.2
Procedural country generation for C++23
|
This guide covers every feature of the country-generator library in detail. For a quick overview, see the README. For the full API reference, run doxygen Doxyfile from the repository root and open doc/api/html/index.html.
dasmig/countrygen.hpp, dasmig/core.hpp and the dasmig/core/ directory into your include path, keeping them side by side.resources/ folder (containing full/ and/or lite/ subdirectories) so it is accessible at runtime. Each tier holds countries.tsv and the locale tables; leave the locale tables out if you need only countries.-std=c++23.The library ships two dataset tiers:
| Tier | Enum | Countries | Description |
|---|---|---|---|
| lite | dasmig::dataset::lite | ~195 | Sovereign states only |
| full | dasmig::dataset::full | ~250 | All countries and territories |
Resources are stored as resources/lite/countries.tsv and resources/full/countries.tsv. Next to each are the locale tables: locales.tsv, zones.tsv, zone_links.tsv, currencies.tsv, holidays.tsv, holiday_events.tsv and holiday_years.tsv (see Data and Method). Loading a tier loads every one of them it finds; a missing table is not an error, and only the getters that need it throw.
On first access the singleton loads the lite tier (or full if lite is missing). It uses the compiled-in copy when built with DASMIG_COUNTRY_EMBED (see Compiled-in Data), and otherwise looks for lite/countries.tsv in these directories, relative to the working directory:
| Priority | Base path |
|---|---|
| 1 | resources/ |
| 2 | ../resources/ |
| 3 | country-generator/resources/ |
| 4 | ../country-generator/resources/ |
The locale tables (locales.tsv, zones.tsv and the rest) are compiled in with DASMIG_COUNTRY_EMBED_LOCALE. Otherwise they are read from the first of these directories that has both the tier's countries.tsv and its locales.tsv. That way, a program that runs from another library's repository never reads that library's own locales.tsv.
If your resources are elsewhere, call load() with a direct path:
Each load() replaces the data loaded before (use extend() to add on top; see Extending Data). load(dataset) returns bool (true if the tier's countries.tsv was found and loaded). load(path) returns void and does nothing if the path is neither a file nor a directory holding countries.tsv. A directory's missing locale tables are cleared; a file leaves the locale tables as they are. A malformed file throws dasmig::core::tsv_error with its file, line and column, and nothing changes. has_locale_data() says whether any locale table is loaded.
Every generation call takes a dasmig::country_request first. Use get_country(request) for a random call, or get_country(request, seed) for a deterministic call:
| Field | Type | Default | Meaning |
|---|---|---|---|
region | std::optional<std::string> | Unset | All loaded countries; set to a case-sensitive UN geoscheme region to filter |
weighted | bool | true | Population weighting; false gives every matching country the same probability |
An explicitly empty or unknown region throws std::invalid_argument. No loaded data throws std::runtime_error before region validation.
All selection choices come from the request, with no mutable generator defaults. Random calls draw a call seed from the internal engine; explicit seed calls do not advance it and can be made on a const generator. The only generation overloads are get_country(request) and get_country(request, seed).
The returned country and its JSON format are unchanged. To replay a request-based result, retain the request as well as the seed and use the same data. The result signature records the seed and data hash, not the request.
Every generated country includes all 32 data fields:
| Field | Type | Description |
|---|---|---|
cca2 | string | ISO 3166-1 alpha-2 code |
cca3 | string | ISO 3166-1 alpha-3 code |
ccn3 | string | ISO 3166-1 numeric code |
name_common | string | Common English name |
name_official | string | Official English name |
capital | string | Capital city (;-separated if multiple) |
region | string | UN geoscheme region |
subregion | string | UN geoscheme subregion |
continent | string | Continent (;-separated if multiple) |
latitude | double | WGS84 decimal degrees |
longitude | double | WGS84 decimal degrees |
area | uint64_t | Total land area in km² |
population | uint64_t | Total population |
landlocked | bool | Whether landlocked |
independent | bool | Sovereign state flag |
un_member | bool | UN membership |
languages | string | ;-separated language names |
language_shares | string | ;-separated Name:percent pairs, largest first (see Languages and demonyms) |
currency_code | string | Primary ISO 4217 code |
currency_name | string | Primary currency name |
currency_symbol | string | Primary currency symbol |
borders | string | ;-separated alpha-3 border codes |
timezones | string | ;-separated UTC offsets |
driving_side | string | "right" or "left" |
tld | string | Country-code TLD |
idd_root | string | IDD root (e.g. "+5") |
idd_suffix | string | IDD primary suffix |
demonym_m | string | English male demonym |
demonym_f | string | English female demonym |
flag_emoji | string | Unicode flag emoji |
income_level | string | World Bank income level |
start_of_week | string | "monday", "sunday", or "saturday" |
languages lists a country's official languages. language_shares adds how many people speak each, from Unicode CLDR: the languages at least 1% of the population speaks, as Name:percent pairs. The shares overlap (a country where many people speak two languages sums above 100), because each is the share of people who speak that language.
get_language(cca2, seed) is a pure function of the data, the country and the seed. A country with no shares falls back to the first of its languages; one with no language data at all throws std::invalid_argument. primary_language() does the same fallback. demonym(sex) returns demonym_m or demonym_f, and the other one when the wanted form is empty.
Layers can set language_shares like any column; a malformed value (a pair without a percent, or a percent that is not above 0 and at most 100) is rejected with its row and column. to_json() writes the shares as an array of {"name", "percent"}.
A country's locales are the languages its people speak, as BCP 47 tags (en-CA, fr-CA), each with the date, time, number and currency formats of Unicode CLDR. get_locale draws one by speakers:
locale_request field | Default | Meaning |
|---|---|---|
country | Unset | ISO alpha-2 code, any case. Unset draws the country exactly as get_country does, so get_locale({}, s).country_code == get_country({}, s).cca2 |
language | Unset | ISO 639 code ("fr"), any case. Only that language's tags; with no country, the country is drawn by that language's speakers |
weighted | true | Weighted by share, the percent of the country that speaks the language; false makes every eligible tag, and country, equally likely |
A drawn country with no locale (Antarctica in full) is replaced by a draw among the countries that have one. locales(country) lists a country's locales by share, and find_locale(tag) looks one up; both give seed 0.
Formatting. A locale formats with its own patterns, names and symbols:
date_style is short_, medium, long_ or full; time_style is short_ or medium. Digits are always Latin.format_money uses the locale's symbol for the locale's own currency (R$ in pt-BR) and the ISO code for any other, so an amount is never ambiguous.date_short, number_pattern, currency_pattern), months, weekdays (Sunday first), am, pm, eras, the symbols (decimal, group, minus), currency_code, currency_symbol and currency_digits, first_day, weekend_start and weekend_end, and measurement (metric, us or uk).basis says where the row came from: cldr (CLDR has the locale for the country), inherited (CLDR has only the language's formats, so en-AR writes as en does, with Argentina's week and currency) or fallback (no language qualifies, so the country's first language is used: en-HM in full).to_json() writes the tag, the formats, the symbols, the currency and the week; the month and weekday names are left out to keep records short:
A country's time zones are its IANA zones, each with the share of the country's population that lives in it (from GeoNames) and its rules as a core::tz_rules:
time_zone_request field | Default | Meaning |
|---|---|---|
country | Unset | ISO alpha-2 code, any case; unset draws the country exactly as get_country does |
reference_date | 2023-07-01 | The date whose offset, abbreviation and daylight flag the result reports, at 12:00 UTC |
weighted | true | Weighted by population share; false makes every zone equally likely |
utc_offset, abbreviation and dst are the rules at the reference date; observes_dst is whether the zone has daylight time from then on. find_time_zone(name, date) takes the reference date too: Berlin is CEST on the default date and CET on 2024-01-15.to_local(sys_seconds) and to_utc(local_seconds, choose) convert at any instant. A local time that happens twice when clocks go back is resolved by core::tz_choose::earliest (the default) or latest; one that never happens when clocks go forward is read with the offset before the change (02:30 on Berlin's spring-forward night is 01:30 UTC).find_time_zone follows IANA's backward links, the old names GeoNames and older systems still write; city-generator's cities carry such names, so pass a city's zone to find_time_zone rather than drawing one.basis is tzdata (IANA lists the zone for the country), geonames (IANA lists none, so the zones of the country's places are used: Kosovo gets Europe/Belgrade), or fallback (a fixed Etc zone from countries.tsv's first UTC offset, for the two uninhabited territories of full with no place).lite and 1970 in full; an earlier instant throws std::out_of_range.The rules come from the data (IANA 2026f), not from std::chrono::tzdb. That database is the operating system's, it differs by platform and update level, and it is missing from Emscripten and older libc++, so the same seed would give different local times on different machines.
holidays(request) lists a country's national holidays in a year. It is a lookup, not a draw, so it takes no seed:
holiday_request field | Default | Meaning |
|---|---|---|
country | Required | ISO alpha-2 code, any case |
year | Required | Within holiday_years(country) |
optional | false | Include days off for some only (Brazil's Carnival, government-only days) |
observances | false | Include days that are not days off (Mother's Day) |
holiday has a date, a stable key (christmas_day), a name in the country's language and name_en in English, a kind (holiday_kind::public_, bank, optional or observance) and, for a multi-day holiday, its 1-based day (Saudi Arabia's Eid al-Fitr holiday of 2024 is four days, 9 to 12 April).observed is set when a weekend rule moves the day off: the US's Christmas Day 2022, a Sunday, is observed on Monday 26 December. is_holiday counts both dates, and looks at the years either side, since an observed day can cross New Year.holiday_years throws std::out_of_range. The span is python-holidays' for that country, within 1970 to 2050.The holidays are stored as rules (fixed dates, n-th weekdays, Easter offsets, dated events such as Eid) inferred from python-holidays 0.107 and checked against it for every country and year; see Data and Method. Where the law changes, a layer corrects a country at once (see Extending Data). to_json() writes a holiday with its dates as YYYY-MM-DD:
currency_digits(code) is the ISO 4217 minor unit of a currency, any case, which is what core::money needs:
A locale's currency_code is its country's currency_code when that is a current ISO 4217 code; otherwise it is CLDR's current tender for the country, or, where CLDR is behind ISO, a code the build sets.
Set a UN geoscheme region on the request to restrict generation:
Available regions: Africa, Americas, Antarctic, Asia, Europe, Oceania.
Region-filtered selection is also population-weighted within the region. Use country_request{.region = "Europe", .weighted = false} for uniform selection within a region. Throws std::invalid_argument if no countries match the region; region names are case-sensitive.
Look up a specific country by its ISO 3166-1 alpha-2 code:
Throws std::invalid_argument if no country matches the code, or std::runtime_error if no data has been loaded. Codes are case-sensitive (uppercase only).
By default, countries are selected with probability proportional to their population. China and India are far more likely to appear than Liechtenstein. Countries with zero population are assigned a minimum weight of 1.
Switch to equal-probability selection where every loaded country has the same chance of being picked, regardless of population:
Selection mode applies only to the current request. A subsequent gen.get_country({}) call is population-weighted, regardless of earlier calls.
Pass a request followed by an explicit seed to produce the same country every time:
Every country records its seed. Retrieve it with country::seed() and pass it back with the original request to reproduce the exact same result:
Supply an explicit seed for each call. For a repeatable sequence, keep the same seed sequence, requests and data:
The generator has no public seed() or unseed() controls.
Note: Replay with
gen.get_country(request, c.seed())using the original request. Omitting the region or changing the weighting mode may produce a different country. The result signature records the seed and data, not these request options.
Construct independent generators with their own data and engine:
The shipped datasets are generated from open data sources by scripts/prepare_countries.py. Data and Method describes the sources, every column and how selection works.
The shipped data is a base layer. Files with the same columns as countries.tsv can be loaded on top of it to add countries, change them or remove them, without editing the shipped file. Rows are keyed by cca2.
cca2, cca3, name_common, region, latitude, longitude, area, population, landlocked, independent and un_member; the other columns are optional and default to empty. Numbers and flags (0/1) are checked, and latitude and longitude must be in range, before anything changes. A bad layer throws dasmig::core::tsv_error with the file, line and column, and the data stays as it was.get_country({.region = region}) at once.layers() lists what is loaded: source, mode, rows added, replaced and removed, and warnings such as unknown columns.load(...) replaces the base and drops every layer.Locale tables. Every table takes layers the same way, named by a dasmig::country_table (countries, locales, zones, zone_links, currencies, holidays, holiday_events, holiday_years). The overloads without one mean countries. A layer can add a country to countries.tsv and its locales, zones and holidays in the other tables, or correct a country's holidays without waiting for a release:
| Table | Key | Checked before a layer is applied |
|---|---|---|
locales | country, tag | The country is loaded; the tag is well formed; the share is above 0 and at most 100; patterns hold only supported fields; 12 months, 7 weekdays and 2 eras; the week and measurement are known values |
zones | country, zone | The country is loaded; the share is above 0; periods and posix parse as core::tz_rules |
zone_links | link | The target zone is loaded |
currencies | code | Three capitals, a three-digit numeric, digits and cash_digits from 0 to 4 |
holidays | country, key, from_year | The country is loaded; the key is [a-z0-9_]+; the kind, rule and observed policy parse; days is 1 to 10; an event: rule names an event in holiday_events |
holiday_events | event, country, date | A valid date; the country is * (every country) or a loaded one |
holiday_years | country | The country is loaded; from_year is not after to_year |
The columns and rule forms are in Data and Method. A bad row throws dasmig::core::tsv_error with its file, line and column, and the data stays as it was.
Every result records the seed and the data it came from:
to_json() gives the result as JSON, the form the WebAssembly layer and the game consume. Semicolon-separated fields (capital, continent, languages, borders, timezones) become arrays:
Seeds are strings because JavaScript numbers cannot hold every 64-bit value. to_json(writer) writes into an existing dasmig::core::json_writer. find_country() results have seed 0.
Define DASMIG_COUNTRY_EMBED before including the header to compile the lite tier's countries into the program (about 70 KB of source). load(dataset::lite) and instance() then use it and need no files, which is what a WebAssembly build wants. The compiled-in data has the same data_hash() as the file, so seeds replay the same either way.
Define DASMIG_COUNTRY_EMBED_LOCALE as well to compile in the lite locale tables (about 1.9 MB of source). They have their own macro because most programs that embed the countries use none of them; with DASMIG_COUNTRY_EMBED alone, the locale tables are read from disk if they are there, and the program is the same as before they existed.
dasmig/countrygen_data.hpp and dasmig/countrygen_locale_data.hpp are generated from resources/lite by make embed; CI checks they are up to date.
Generation now uses exactly get_country(request) and get_country(request, seed), where request is a country_request. The former no-argument, seed-only and region-taking overloads and generator-level weighted(), seed() and unseed() controls have been removed.
| Former call | Request-first replacement |
|---|---|
get_country() | get_country({}) |
get_country(seed) | get_country({}, seed) |
get_country("Europe") | get_country({.region = "Europe"}) |
get_country("Europe", seed) | get_country({.region = "Europe"}, seed) |
weighted(false) followed by generation | Set .weighted = false on each applicable request |
seed() / unseed() on a generator | Pass explicit per-call seeds for repeatable output; omit the seed for normal random generation |
The request defaults remain all loaded countries with population weighting. Matching requests, seeds and data preserve the established deterministic results, including zero-population minimum weights, region validation, JSON and signatures. Data loading, extensions and find_country() are unchanged.
Version 2 moves the library onto dasmig-core.
std:: distributions could not promise. Seeds saved with v1 do not reproduce their v1 results. The selection probabilities are unchanged (checked against v1 over 20,000 draws per scenario).load(path) replaces the data.** In v1 each call added to what was loaded. Use extend() to add data on top.dasmig::core::tsv_error with the file, line and column and loads nothing. A path that does not exist is still ignored by load(path).random.hpp with core.hpp and the core/ directory (see Installation).extend(), extend_from_string(), layers(), data_hash(), country::signature(), country::to_json(), DASMIG_COUNTRY_EMBED.Each cntg instance is independent. The static instance() singleton uses a local static for safe initialization.
| Operation | Thread-safe? |
|---|---|
instance() | Yes (static local) |
get_country(request) on different instances | Yes |
get_country(request) on the same instance | No — requires external synchronization |
get_country(request, seed) | Yes for concurrent reads, provided no data mutation occurs |
get_locale(request, seed), get_time_zone(request, seed) | Yes, as get_country(request, seed) |
Lookups: find_*, locales, time_zones, holidays, is_holiday, currency_digits | Yes, as get_country(request, seed) |
load(), extend() | No — must not be called concurrently with generation on the same instance |
Call load() once during initialization before spawning threads. For concurrent generation, give each thread its own cntg instance.
Every seeded getter has batch forms: get_country_batch, get_locale_batch and get_time_zone_batch return a std::vector, and for_each_country, for_each_locale and for_each_time_zone stream the items to a sink in index order without holding the batch.
i is exactly get_country(request, core::item_seed(batch_seed, first + i)). A range can be generated directly (first = 1000 starts at item 1000), two batches that follow each other equal one, and the output is the same for any threads (0 means the hardware count). Without a batch_seed one is drawn like the single getter draws its own.core::batch_options has count, first, threads and unique. The request describes one item; the options describe the batch..unique = core::unique_rule::by<country>(key) regenerates an item whose key already appeared in the batch, with a deterministic retry seed, and throws std::runtime_error naming the item when the attempts run out.core::jsonl_writer writes one to_json() per line, core::csv_writer flattens each result's JSON into dotted columns. Any callable (std::uint64_t index, country&) works.| Exception | Condition |
|---|---|
std::runtime_error | Either get_country overload or find_country() called with no data loaded |
std::invalid_argument | Either get_country overload with an empty or unknown request region |
std::invalid_argument | find_country(cca2) with unknown CCA2 code |
std::invalid_argument | get_language(cca2, seed) with an unknown code, or a country with no language data |
std::runtime_error | A locale, zone, holiday or currency getter when its table is not loaded ("No locale data loaded. Load a tier with the locale tables, or define DASMIG_COUNTRY_EMBED_LOCALE.") |
std::invalid_argument | An unknown country, locale tag, zone name or currency code; a language no country (or not the requested one) has a locale of; a country with no holiday data; an invalid date |
std::out_of_range | holidays or is_holiday for a year outside holiday_years(country); a zone's reference date or instant before its data (2000 in lite, 1970 in full) |
dasmig::core::tsv_error | A malformed table or layer (message gives file, line and column); the data is unchanged |