![]() |
Birth Generator 3.2.2
Procedural birth date generation for C++23
|
This guide covers every feature of the birth-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/birthgen.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 with their three TSV files) so it is accessible at runtime.-std=c++23.The library ships two dataset tiers:
| Tier | Enum | Countries | Description |
|---|---|---|---|
| lite | dasmig::dataset::lite | 193 | Sovereign states with UN age data |
| full | dasmig::dataset::full | 235 | All countries and territories with UN age data |
Each tier stores three TSV files:
| File | Content |
|---|---|
countries.tsv | ISO codes, name, region, latitude, life expectancy, C-section rate |
age_pyramid.tsv | Population by single year of age (0–100) for male and female |
monthly_births.tsv | Seasonal birth weights by month (latitude-derived) |
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_BIRTH_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 | birth-generator/resources/ |
| 4 | ../birth-generator/resources/ |
If the country code is not found (e.g., "XX"), get_birth() throws std::invalid_argument.
Every generation call takes a birth_request: use get_birth(request) for a random birth or get_birth(request, seed) for a deterministic birth. The request owns its country code and can be saved and reused:
| Field | Type | Default / meaning |
|---|---|---|
country_code | std::optional<std::string> | Omitted: random country; otherwise a loaded ISO alpha-2 code |
bio_sex | std::optional<dasmig::sex> | Omitted: draw from the country's M:F ratio |
year | std::optional<dasmig::year_t> | Omitted: derive birth year from the drawn age |
ages | std::optional<dasmig::age_range> | Omitted: full age pyramid; otherwise inclusive range, clamped to 100 |
weighted | bool | true: population-weighted; false: uniform (ignored when country is fixed) |
reference_date | std::chrono::year_month_day | birth_request::default_reference_date, 2023-07-01 |
year and ages are mutually exclusive; specifying both throws std::invalid_argument. The existing range validation (min > max), age clamping, unknown-country errors, and invalid-reference-date errors also apply to requests. A fixed year's derived age is still clamped to [0, 100].
All generation choices live on the request; there are no generator-wide weighting or reference-date settings. birth_request{} always uses population weighting and the default reference date. The random overload draws a seed from the internal engine; the explicit-seed overload does not advance it.
The result shape, seeded output, seed signature and stream splits are unchanged. Replay needs the same request and loaded data as well as the seed: the signature records only seed and data hash, not request fields.
Fix the biological sex instead of drawing it from the M:F ratio:
Fix the birth year instead of drawing from the age pyramid. The age is the completed years at the reference date (see Reference Date), clamped to [0, 100]:
Fix both sex and year — only month, day, weekday, LE, and cohort are randomised:
Constrain the age to an inclusive range. The age is drawn from the country's age pyramid restricted to the range, so ages inside it keep their relative frequencies:
Throws std::invalid_argument if min > max.
Every dasmig::birth object exposes these fields:
| Field | Type | Description |
|---|---|---|
country_code | std::string | ISO 3166-1 alpha-2 code |
year | std::uint16_t | Birth year |
month | std::uint8_t | Birth month (1–12) |
day | std::uint8_t | Birth day (1–31) |
age | std::uint8_t | Age in completed years |
bio_sex | dasmig::sex | sex::male or sex::female |
weekday | std::uint8_t | Day of week (0=Sun, 1=Mon, …, 6=Sat) |
le_remaining | double | Estimated years of life remaining |
cohort | std::string | Generational cohort label |
| Cohort | Birth years |
|---|---|
| Greatest Generation | ≤ 1927 |
| Silent Generation | 1928–1945 |
| Baby Boomer | 1946–1964 |
| Generation X | 1965–1980 |
| Millennial | 1981–1996 |
| Generation Z | 1997–2012 |
| Generation Alpha | ≥ 2013 |
The generator exposes what it draws from, for code that builds on births (a family, a cohort, a chart):
age_weights returns the rows of age_pyramid.tsv as relative weights (population by single age, not normalised, so compare ages within a country, not between countries). life_expectancy is the figure le_remaining is computed from, and cohort_of gives the label birth::cohort carries. An unknown country throws std::invalid_argument; with no data loaded they throw std::runtime_error.
Set birth_request::country_code to an ISO 3166-1 alpha-2 code to generate a birth from a specific country:
The age pyramid, life expectancy, seasonal model, and C-section rate are all country-specific.
By default, countries are selected proportional to their total population. This means births from China, India, and the United States are far more common than births from small nations.
Switch to equal-probability country selection:
The choice affects only that request; it is ignored when country_code is fixed. Add a seed as the second argument for deterministic selection.
Every birth stores the random seed used to generate it:
Deterministic generation takes a seed explicitly on every call. There are no generator-level seed() / unseed() controls. get_birth(request) draws from an entropy-initialized internal random engine; its result still records the call seed for replay. get_birth(request, seed) neither uses nor advances that engine.
Construct independent instances for isolation:
Useful when embedding inside other generators or when different threads need independent generators.
Each get_birth() call runs the following pipeline:
birth_request::bio_sex).age_range), or derived from a fixed year_t.csection_rate × 0.5 (up to 3 retries).std::chrono::weekday from the final date.max(0, LE_at_birth − age) using sex-specific period life expectancy from UN WPP 2024.The three files are built by scripts/prepare_births.py from UN World Population Prospects 2024, REST Countries and WHO caesarean rates. Data and Method describes the sources, every column, the generation model and its limitations.
Ages are completed years at a fixed reference date, not at today's date, so the same request, seed and data give the same birth whenever it runs. The default is birth_request::default_reference_date, 1 July 2023, the date of the UN population estimates in the data.
Changing the request's reference date shifts age-drawn birth years (a fixed year_t stays fixed and its age changes). Other requests are unaffected. Replaying a seed requires the same request, including its reference date and weighting.
The shipped data is a base layer in three tables (see Loading Resources). Files with the same columns can be loaded on top to add countries, change them or remove them, without editing the shipped files. Rows are keyed by cca2.
countries.tsv row with le_male and le_female, and an age_pyramid.tsv row. Without a monthly_births.tsv row its months are uniform.dasmig::core::tsv_error with the file, line and column, and the data stays as it was.layers(table) lists what is loaded in each table; data_hash() covers all three.load(...) replaces the data and drops every layer.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:
Seeds are strings because JavaScript numbers cannot hold every 64-bit value. to_json(writer) writes into an existing dasmig::core::json_writer.
Define DASMIG_BIRTH_EMBED before including the header to compile the lite tier into the program (about 350 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 files, so seeds replay the same either way.
dasmig/birthgen_data.hpp is generated from resources/lite by make embed; CI checks it is up to date.
The public generation API now consists only of get_birth(request) and get_birth(request, seed). The no-argument, seed-only and positional country/sex/year/age overloads have been removed, as have the generator-wide weighted() and reference_date() getter/setters and seed() / unseed() controls.
| Previous call or setting | Request-first replacement |
|---|---|
get_birth() | get_birth(dasmig::birth_request{}) |
get_birth(seed) | get_birth(dasmig::birth_request{}, seed) |
get_birth("US", seed) | get_birth(dasmig::birth_request{.country_code = "US"}, seed) |
| Country, sex, year or age arguments | Set country_code, bio_sex, year or ages on the request |
weighted(false) | Set request.weighted = false |
reference_date(date) | Set request.reference_date = date |
Generator seed(seed) / unseed() | Pass a seed to each get_birth(request, seed), or omit it for ordinary random generation |
For random generation, omit only the second (seed) argument, never the request. The request defaults, demographic distributions, result fields and seeded output are unchanged. Retain and reuse the full request when replaying a saved seed. Data loading, extension and inspection APIs are unchanged.
Version 2 moves the library onto dasmig-core.
load(path)** still takes a directory and ignores a path that is not one; malformed files now throw dasmig::core::tsv_error with the file, line and column instead of skipping rows.random.hpp with core.hpp and the core/ directory (see Installation).reference_date, extend(), extend_from_string(), layers(), data_hash(), birth::signature(), birth::to_json(), DASMIG_BIRTH_EMBED.The random engine inside each bthg instance is not thread-safe. If you call get_birth() from multiple threads, either:
bthg instance per thread, orThe singleton bthg::instance() returns a single shared instance, so concurrent access must be synchronized.
Every seeded getter has batch forms: get_birth_batch return a std::vector, and for_each_birth streams the items to a sink in index order without holding the batch.
i is exactly get_birth(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<birth>(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, birth&) works.| Error | Cause |
|---|---|
std::runtime_error("No birth data loaded. Call load() first.") | Called get_birth() before any load() and auto-probe found nothing. |
std::invalid_argument("Unknown country code: XX") | birth_request::country_code is not in the loaded dataset. |
std::invalid_argument("age_range: min must be <= max") | Passed an age_range where min > max. |
std::invalid_argument("reference_date: not a valid date") | Invalid date in birth_request::reference_date. |
std::invalid_argument("birth_request: year and ages are mutually exclusive") | Specified both a fixed year and an age range in one request. |
std::invalid_argument | extend(file) with a file not named after a table. |
dasmig::core::tsv_error | A malformed data file or layer (message gives file, line and column); the data is unchanged. |