Occupation Generator 3.0.1
Employment status and occupation by country, age, sex and education for C++23
Loading...
Searching...
No Matches
occupationgen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Diego Dasso Migotto
3
4#ifndef DASMIG_OCCUPATIONGEN_HPP
5#define DASMIG_OCCUPATIONGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <array>
10#include <cstddef>
11#include <cstdint>
12#include <filesystem>
13#include <format>
14#include <map>
15#include <optional>
16#include <ostream>
17#include <span>
18#include <stdexcept>
19#include <string>
20#include <string_view>
21#include <system_error>
22#include <tuple>
23#include <unordered_map>
24#include <utility>
25#include <vector>
26
27#ifdef DASMIG_OCCUPATION_EMBED
28#include "occupationgen_data.hpp"
29#endif
30
31/// @file occupationgen.hpp
32/// @brief Occupation Generator library — employment status, education and occupation by country, age and sex for C++23.
33/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
34/// @see See doc/usage.md for the narrative tutorial.
35
36DASMIG_CORE_REQUIRE(1, 2);
37
38namespace dasmig
39{
40
41/// @brief What a person does with their working-age days.
42enum class employment_status : std::uint8_t
43{
44 employed, ///< In employment.
45 unemployed, ///< Without work and looking for it.
46 student, ///< Studying (always, from 6 to 14; among the inactive, by age).
47 retired, ///< Retired (among the inactive, by age).
48 inactive ///< Outside the labour force for another reason: care, illness, discouragement; under 6, always.
49};
50
51/// @brief Highest education completed, in the ILO's four aggregate levels.
52enum class education_level : std::uint8_t
53{
54 less_than_basic, ///< Less than basic: not completed primary education, or none.
55 basic, ///< Basic: primary or lower secondary (ISCED 1-2).
56 intermediate, ///< Intermediate: upper secondary and post-secondary non-tertiary (ISCED 3-4).
57 advanced ///< Advanced: tertiary (ISCED 5-8).
58};
59
60/// @brief The name of an employment status, as used in the data and in JSON.
61/// @param s The status.
62/// @return "employed", "unemployed", "student", "retired" or "inactive".
63[[nodiscard]] constexpr std::string_view to_string(employment_status s) noexcept
64{
65 switch (s)
66 {
67 case employment_status::employed: return "employed";
68 case employment_status::unemployed: return "unemployed";
69 case employment_status::student: return "student";
70 case employment_status::retired: return "retired";
71 case employment_status::inactive: return "inactive";
72 }
73 return "";
74}
75
76/// @brief The name of an education level, as used in the data and in JSON.
77/// @param e The level.
78/// @return "less_than_basic", "basic", "intermediate" or "advanced".
79[[nodiscard]] constexpr std::string_view to_string(education_level e) noexcept
80{
81 switch (e)
82 {
83 case education_level::less_than_basic: return "less_than_basic";
84 case education_level::basic: return "basic";
85 case education_level::intermediate: return "intermediate";
86 case education_level::advanced: return "advanced";
87 }
88 return "";
89}
90
91/// @brief A job, as drawn.
93{
94 std::string key; ///< Catalogue key (the `key` column of `occupations.tsv`).
95 std::string label; ///< Display label, for example "Plumber".
96 std::string isco; ///< ISCO-08 level-2 group code, for example "71".
97 std::string isco_label; ///< Title of that group.
98 std::string unit; ///< ISCO-08 unit group (four digits); may be empty.
99 std::string skill; ///< ISCO-08 skill level of the group ("1", "2", "3-4"); may be empty.
100 std::vector<std::string> skills; ///< Skill keywords.
101 std::vector<std::string> gear; ///< Clothing and gear keywords.
102 std::vector<std::string> tags; ///< Character tags such as "manual" or "caring", for other generators.
103};
104
105/// @brief Per-call generation choices. Every field may be omitted.
106// NOLINTBEGIN(readability-redundant-member-init)
108{
109 std::string_view country{}; ///< ISO 3166-1 alpha-2 code; empty draws a country (uniform over those loaded).
110 std::uint8_t age{40}; ///< Age in completed years (default 40, an age at which most people work).
111 std::optional<sex> person_sex{}; ///< Sex; unknown draws one (even odds).
112 std::optional<education_level> education{}; ///< Education; unknown draws one from the country, sex and age.
113 std::optional<employment_status> status{}; ///< Status; unknown draws one from the country, sex and age.
114};
115// NOLINTEND(readability-redundant-member-init)
116
117/// @brief Return type for occupation generation.
118///
119/// Supports implicit conversion to std::string (the job label, or the status
120/// when there is no job), streaming via operator<< and JSON output via
121/// to_json().
123{
124 public:
125 std::string country_code; ///< ISO 3166-1 alpha-2 code of the country drawn from.
126 sex person_sex{sex::male}; ///< Sex (given or drawn).
127 std::uint8_t age{0}; ///< Age in completed years.
128 employment_status status{employment_status::employed}; ///< Employment status.
129 std::optional<education_level> education; ///< Education; unknown under 15 when not given.
130 std::optional<job_info> job; ///< The job: the current one when employed, the last or usual one
131 ///< when unemployed, retired or inactive; none for students and
132 ///< for the inactive under 6.
133
134 /// @brief Retrieve the random seed used to generate this occupation.
135 /// @return The call seed; pass it, with the same request, to the seeded
136 /// get_occupation(request, seed) overload to generate the same occupation again.
137 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
138
139 /// @brief The seed and the data this occupation was generated from.
140 /// @return The replay signature; `data` is ocg::data_hash() at the time
141 /// of generation.
142 [[nodiscard]] const core::seed_signature& signature() const { return _signature; }
143
144 /// @brief Human-readable summary string.
145 /// @return The job label, or the status when there is no job.
146 [[nodiscard]] std::string to_string() const
147 {
148 return job ? job->label : std::string{dasmig::to_string(status)};
149 }
150
151 /// @brief Write this occupation as a JSON object.
152 /// @param w Writer positioned where a value is allowed.
153 void to_json(core::json_writer& w) const
154 {
155 w.begin_object()
156 .field("country_code", country_code)
157 .field("sex", core::sex_name(person_sex))
158 .field("age", static_cast<unsigned>(age))
159 .field("status", dasmig::to_string(status));
160 w.key("education");
161 if (education) w.value(dasmig::to_string(*education));
162 else w.value(nullptr);
163 w.key("job");
164 if (job)
165 {
166 w.begin_object()
167 .field("key", job->key)
168 .field("label", job->label)
169 .field("isco", job->isco)
170 .field("isco_label", job->isco_label)
171 .field("unit", job->unit)
172 .field("skill", job->skill);
173 write_list_(w, "skills", job->skills);
174 write_list_(w, "gear", job->gear);
175 write_list_(w, "tags", job->tags);
176 w.end_object();
177 }
178 else
179 {
180 w.value(nullptr);
181 }
182 w.key("signature");
183 _signature.to_json(w);
184 w.end_object();
185 }
186
187 /// @brief This occupation as a JSON document.
188 /// @return Compact JSON, for example
189 /// `{"country_code":"BR","sex":"male","age":30,"status":"employed","education":"intermediate","job":{...},"signature":{...}}`.
190 [[nodiscard]] std::string to_json() const
191 {
192 core::json_writer w;
193 to_json(w);
194 return w.str();
195 }
196
197 /// @brief Implicit conversion to std::string.
198 /// @return Summary string.
199 operator std::string() const // NOLINT(hicpp-explicit-conversions)
200 {
201 return to_string();
202 }
203
204 /// @brief Stream the summary to an output stream.
205 /// @param os Output stream.
206 /// @param v The occupation to stream.
207 /// @return The stream.
208 friend std::ostream& operator<<(std::ostream& os, const occupation& v)
209 {
210 os << v.to_string();
211 return os;
212 }
213
214 private:
215 static void write_list_(core::json_writer& w, std::string_view name, const std::vector<std::string>& items)
216 {
217 w.key(name).begin_array();
218 for (const auto& i : items) w.value(i);
219 w.end_array();
220 }
221
222 core::seed_signature _signature;
223 friend class ocg;
224};
225
226/// @brief Occupation Generator: draws a person's employment status,
227/// education and job.
228///
229/// The country (when not given) comes from the `"country"` stream of the
230/// call seed, the sex from `"sex"`, the status from `"status"`, the education
231/// from `"education"`, the ISCO-08 group from `"isco"` and the job within it
232/// from `"job"`, so fields added later leave these unchanged. Output is
233/// identical on every compiler and platform (dasmig-core).
234///
235/// The status comes from the country's employment and unemployment rates for
236/// the person's sex and age band; the inactive are split into students,
237/// retired and others by age. From 6 to 14 a person is a student, and under 6
238/// inactive with no job. The job is an
239/// ISCO-08 level-2 group drawn by the country's employment share for the
240/// person's sex, multiplied by factors for the person's age class and
241/// education (how much more or less common the group is at that age or
242/// education than in employment as a whole), then a job of the catalogue
243/// within the group. Groups with no job open to the person's age are skipped.
244///
245/// The education comes from the country's shares for the sex and age band. A
246/// level the person is too young to have completed (basic under 14,
247/// intermediate under 17, advanced under 21) counts as the level below.
248///
249/// A country chosen at random is uniform over the countries loaded; the data
250/// has no population column. Pass a country chosen by country-generator for a
251/// population-weighted world.
252///
253/// Every call takes an occupation_request; generation choices are never
254/// stored as instance defaults. Data loading and layers belong to the instance.
255/// Can be used as a singleton via instance() or constructed independently.
256///
257/// @par Thread safety
258/// Each instance is independent. Concurrent calls to get_occupation(request) on the
259/// **same** instance require external synchronisation.
260class ocg
261{
262 public:
263 /// @brief The data tables.
264 enum class table : std::uint8_t
265 {
266 status, ///< `status.tsv`: key `country_code`, `sex`, `band`; employed, unemployed, inactive (percent), basis, year.
267 inactivity, ///< `inactivity.tsv`: key `band`; student, retired, other (shares of the inactive).
268 education, ///< `education.tsv`: key `country_code`, `sex`, `band`; less_than_basic, basic, intermediate, advanced, basis, year.
269 isco, ///< `isco.tsv`: key `code` (ISCO-08 level 2); label, major, skill.
270 shares, ///< `shares.tsv`: key `country_code`, `sex`, `code`; share (percent of employment), basis, year.
271 factors, ///< `factors.tsv`: key `country_code`, `major`, `dimension`, `value`; factor, basis, year.
272 occupations ///< `occupations.tsv`: key `key`; label, isco, weight, unit, min_age, max_age, tags, skills, gear.
273 };
274
275 /// @brief Default constructor — creates an empty generator with no
276 /// data.
277 ocg() : _tables{make_tables_()} {}
278
279 ocg(const ocg&) = delete; ///< Not copyable.
280 ocg& operator=(const ocg&) = delete; ///< Not copyable.
281 ocg(ocg&&) noexcept = default; ///< Movable.
282 ocg& operator=(ocg&&) noexcept = default; ///< Movable.
283 ~ocg() = default; ///< Destructor.
284
285 /// @brief Access the global singleton instance.
286 ///
287 /// Loads the `lite` tier (or `full` if `lite` is missing) on first
288 /// access, from compiled-in data or an auto-probed `resources/`.
289 /// @return The singleton.
290 static ocg& instance()
291 {
292 static ocg inst{auto_probe_tag{}};
293 return inst;
294 }
295
296 // -- Generation -------------------------------------------------------
297
298 /// @brief Generate a random occupation for a request.
299 /// @param request Country, age, sex, education and status; any may be omitted.
300 /// Pass `{}` to use the request defaults.
301 /// @details Draws a call seed from the instance's entropy-seeded engine.
302 /// @return The occupation.
303 /// @throws std::runtime_error If no data has been loaded.
304 /// @throws std::invalid_argument If the country has no data for the
305 /// person (or, with no country, no country has any), or no
306 /// occupation is open to the person.
307 [[nodiscard]] occupation get_occupation(const occupation_request& request)
308 {
309 return generate_(request, draw_seed_());
310 }
311
312 /// @brief Generate a deterministic occupation for a request.
313 /// @param request Country, age, sex, education and status; any may be omitted.
314 /// @param call_seed Seed; the same seed, request and data give the same
315 /// occupation.
316 /// @return The occupation.
317 /// @throws std::runtime_error If no data has been loaded.
318 /// @throws std::invalid_argument As for the unseeded overload.
319 [[nodiscard]] occupation get_occupation(const occupation_request& request, std::uint64_t call_seed) const
320 {
321 return generate_(request, call_seed);
322 }
323
324 // -- Batch generation -------------------------------------------------
325
326 /// @brief Generate a batch of occupation results from a seed drawn like
327 /// get_occupation() draws its own.
328 ///
329 /// Item `i` is `get_occupation(request, core::item_seed(batch_seed, options.first + i))`
330 /// for the drawn batch seed; see the seeded overload.
331 ///
332 /// @param request What every item is generated from.
333 /// @param options Count, first index, threads and uniqueness.
334 /// @return The results, in index order.
335 [[nodiscard]] std::vector<occupation> get_occupation_batch(const occupation_request& request,
336 const core::batch_options& options)
337 {
338 return get_occupation_batch(request, options, draw_seed_());
339 }
340
341 /// @brief Generate a batch of occupation results, deterministically.
342 ///
343 /// Item `i` is exactly `get_occupation(request, core::item_seed(batch_seed,
344 /// options.first + i))`, so an item replays on its own, a range can be
345 /// generated directly, two batches that follow each other equal one, and
346 /// the output does not depend on `options.threads`.
347 ///
348 /// @param request What every item is generated from.
349 /// @param options Count, first index, threads and uniqueness (see
350 /// core::unique_rule; the rule must be made for `occupation`).
351 /// @param batch_seed Seed of the whole batch.
352 /// @return The results, in index order.
353 /// @throws std::invalid_argument If the request or options are invalid.
354 /// @throws std::runtime_error If no data has been loaded, or a unique
355 /// batch runs out of attempts.
356 [[nodiscard]] std::vector<occupation> get_occupation_batch(const occupation_request& request,
357 const core::batch_options& options,
358 std::uint64_t batch_seed) const
359 {
360 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
361 { return get_occupation(request, item_seed); });
362 }
363
364 /// @brief Stream a batch of occupation results to a sink without holding the
365 /// whole batch.
366 ///
367 /// Generates the same items as get_occupation_batch() and calls
368 /// `sink(index, item)` in index order on the calling thread; `index`
369 /// includes `options.first`.
370 ///
371 /// @tparam Sink Callable taking `(std::uint64_t, occupation&)`; for example
372 /// core::jsonl_writer or core::csv_writer.
373 /// @param request What every item is generated from.
374 /// @param options Count, first index, threads and uniqueness.
375 /// @param batch_seed Seed of the whole batch.
376 /// @param sink Receives the items.
377 template <typename Sink>
378 void for_each_occupation(const occupation_request& request, const core::batch_options& options,
379 std::uint64_t batch_seed, Sink&& sink) const
380 {
381 core::for_each_batch(options, batch_seed,
382 [&](std::uint64_t item_seed) { return get_occupation(request, item_seed); },
383 std::forward<Sink>(sink));
384 }
385
386 /// @brief Stream a batch of occupation results with a batch seed drawn like
387 /// get_occupation() draws its own.
388 /// @tparam Sink Callable taking `(std::uint64_t, occupation&)`.
389 /// @param request What every item is generated from.
390 /// @param options Count, first index, threads and uniqueness.
391 /// @param sink Receives the items.
392 template <typename Sink>
393 void for_each_occupation(const occupation_request& request, const core::batch_options& options,
394 Sink&& sink)
395 {
396 for_each_occupation(request, options, draw_seed_(), std::forward<Sink>(sink));
397 }
398
399
400 // -- Queries ----------------------------------------------------------
401
402 /// @brief Check whether status data has been loaded.
403 /// @return True when at least one status row is loaded.
404 [[nodiscard]] bool has_data() const { return !_tables.at(index_(table::status)).empty(); }
405
406 /// @brief Return the number of countries that can be drawn from.
407 /// @return The count: countries with status rows for both sexes.
408 [[nodiscard]] std::size_t country_count() const { return _model.codes.size(); }
409
410 /// @brief Return the number of jobs in the catalogue.
411 /// @return The count.
412 [[nodiscard]] std::size_t job_count() const { return _model.jobs.size(); }
413
414 /// @brief Rows that name something that is not loaded: a share for an
415 /// unknown ISCO group, a job for an unknown ISCO group. They are
416 /// ignored, so removing a group needs no other change; this lists
417 /// them to catch misspelt codes.
418 /// @return One description per ignored row, such as
419 /// `occupations.tsv:12: unknown ISCO group '7x'`.
420 [[nodiscard]] const std::vector<std::string>& unresolved() const { return _model.unresolved; }
421
422 // -- Data management --------------------------------------------------
423
424 /// @brief Load a resource directory, replacing all data and layers
425 /// loaded before.
426 ///
427 /// The directory must contain `status.tsv`; the other tables are read
428 /// when present (a missing one leaves its table empty). A path without
429 /// `status.tsv` is ignored.
430 ///
431 /// @param dir Directory such as `resources/lite`.
432 /// @throws core::tsv_error If a file is malformed; the data is then
433 /// unchanged.
434 void load(const std::filesystem::path& dir)
435 {
436 std::error_code ec;
437 if (!std::filesystem::is_directory(dir, ec) ||
438 !std::filesystem::is_regular_file(dir / file_name_(table::status), ec))
439 {
440 return;
441 }
442 apply_(read_directory_(dir), core::layer_mode::replace, true);
443 }
444
445 /// @brief Load a dataset tier, replacing all data and layers loaded
446 /// before.
447 ///
448 /// Uses the compiled-in copy when the library is built with
449 /// `DASMIG_OCCUPATION_EMBED`, otherwise searches `resources/`,
450 /// `../resources`, `occupation-generator/resources` and
451 /// `../occupation-generator/resources`.
452 ///
453 /// @param tier The tier.
454 /// @return False when the tier was not found.
455 /// @throws core::tsv_error If a file is malformed.
456 [[nodiscard]] bool load(dataset tier)
457 {
458 const std::string tier_dir{core::tier_name(tier)};
459 const auto marker = tier_dir + "/" + std::string{file_name_(table::status)};
460 const auto root = core::find_resources(library_name_, marker);
461 documents docs;
462 for (const auto t : all_tables_)
463 {
464 docs.at(index_(t)) = core::load_resource(tier_dir + "/" + std::string{file_name_(t)},
465 embedded_files_(), root);
466 }
467 if (!docs.at(index_(table::status))) return false;
468 apply_(std::move(docs), core::layer_mode::replace, true);
469 return true;
470 }
471
472 /// @brief Add data layers from a directory or a file.
473 ///
474 /// A directory may hold any of the table files (`status.tsv`,
475 /// `inactivity.tsv`, `education.tsv`, `isco.tsv`, `shares.tsv`,
476 /// `factors.tsv`, `occupations.tsv`); all are applied together or, if any
477 /// fails, none. A file must have one of those names.
478 ///
479 /// @param path Directory or file.
480 /// @param mode How the layers combine with the data loaded before.
481 /// @throws core::tsv_error If a file cannot be read or a row is
482 /// invalid; the data is then unchanged.
483 /// @throws std::invalid_argument If @p path is a file with another name.
484 void extend(const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
485 {
486 std::error_code ec;
487 if (std::filesystem::is_directory(path, ec))
488 {
489 apply_(read_directory_(path), mode, false);
490 return;
491 }
492 for (const auto t : all_tables_)
493 {
494 if (path.filename() == file_name_(t))
495 {
496 (void)extend(t, core::tsv_document::load(path), mode);
497 return;
498 }
499 }
500 throw std::invalid_argument("extend: expected status.tsv, inactivity.tsv, education.tsv, isco.tsv, "
501 "shares.tsv, factors.tsv or occupations.tsv, got " + path.filename().string());
502 }
503
504 /// @brief Add a data layer to one table.
505 ///
506 /// Rows are keyed by the table's key columns (see table). In the default
507 /// override mode a row replaces the loaded row with the same key and new
508 /// keys are added; a row with `disabled` set to 1 removes its key.
509 ///
510 /// @param which The table.
511 /// @param doc TSV with that table's columns.
512 /// @param mode How the layer combines with the data loaded before.
513 /// @return What the layer did (rows added, replaced, removed, warnings).
514 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
515 const core::layer_info& extend(table which, core::tsv_document doc,
516 core::layer_mode mode = core::layer_mode::override)
517 {
518 documents docs;
519 docs.at(index_(which)).emplace(std::move(doc));
520 apply_(std::move(docs), mode, false);
521 return _tables.at(index_(which)).layers().back();
522 }
523
524 /// @brief Add a data layer to one table from TSV text (for example
525 /// from a WebAssembly host).
526 /// @param which The table.
527 /// @param text TSV with that table's columns.
528 /// @param source Name used in errors.
529 /// @param mode How the layer combines with the data loaded before.
530 /// @return What the layer did.
531 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
532 const core::layer_info& extend_from_string(table which, std::string_view text, std::string source,
533 core::layer_mode mode = core::layer_mode::override)
534 {
535 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
536 }
537
538 /// @brief The layers in effect for one table, in order.
539 /// @param which The table.
540 /// @return The layer records.
541 [[nodiscard]] const std::vector<core::layer_info>& layers(table which) const
542 {
543 return _tables.at(index_(which)).layers();
544 }
545
546 /// @brief Hash of the loaded data (all tables), recorded in each
547 /// result's signature().
548 /// @return The hash; 0 when nothing is loaded.
549 [[nodiscard]] std::uint64_t data_hash() const
550 {
551 std::uint64_t h = 0;
552 bool any = false;
553 for (const auto& t : _tables)
554 {
555 any = any || !t.layers().empty();
556 h = core::hash_combine(h, t.data_hash());
557 }
558 return any ? h : 0;
559 }
560
561 private:
562 // -- Tables -------------------------------------------------------------
563
564 static constexpr std::string_view library_name_ = "occupation-generator";
565 static constexpr std::size_t table_count_ = 7;
566 static constexpr std::array<table, table_count_> all_tables_{
569
570 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
571 using tables = std::array<core::layered_table, table_count_>;
572
573 static constexpr std::size_t index_(table t) { return static_cast<std::size_t>(t); }
574
575 static constexpr std::string_view file_name_(table t)
576 {
577 switch (t)
578 {
579 case table::status: return "status.tsv";
580 case table::inactivity: return "inactivity.tsv";
581 case table::education: return "education.tsv";
582 case table::isco: return "isco.tsv";
583 case table::shares: return "shares.tsv";
584 case table::factors: return "factors.tsv";
585 case table::occupations: return "occupations.tsv";
586 }
587 return "";
588 }
589
590 static core::table_schema schema_(table t)
591 {
592 switch (t)
593 {
594 case table::status:
595 return {.key = {"country_code", "sex", "band"},
596 .required = {"employed", "unemployed", "inactive"},
597 .optional = {"basis", "year"}};
599 return {.key = {"band"}, .required = {"student", "retired", "other"}, .optional = {}};
600 case table::education:
601 return {.key = {"country_code", "sex", "band"},
602 .required = {"less_than_basic", "basic", "intermediate", "advanced"},
603 .optional = {"basis", "year"}};
604 case table::isco: return {.key = {"code"}, .required = {"label", "major"}, .optional = {"skill"}};
605 case table::shares:
606 return {.key = {"country_code", "sex", "code"}, .required = {"share"}, .optional = {"basis", "year"}};
607 case table::factors:
608 return {.key = {"country_code", "major", "dimension", "value"},
609 .required = {"factor"},
610 .optional = {"basis", "year"}};
612 return {.key = {"key"},
613 .required = {"label", "isco", "weight"},
614 .optional = {"unit", "min_age", "max_age", "tags", "skills", "gear"}};
615 }
616 return {};
617 }
618
619 static tables make_tables_()
620 {
621 return {core::layered_table{schema_(table::status)}, core::layered_table{schema_(table::inactivity)},
622 core::layered_table{schema_(table::education)}, core::layered_table{schema_(table::isco)},
623 core::layered_table{schema_(table::shares)}, core::layered_table{schema_(table::factors)},
624 core::layered_table{schema_(table::occupations)}};
625 }
626
627 // -- Parsed rows --------------------------------------------------------
628
629 static constexpr int working_age_ = 15; // under this age a person is a student
630 static constexpr int school_age_ = 6; // under this age a person is inactive, with no job
631 static constexpr std::array<education_level, 4> education_levels_{
632 education_level::less_than_basic, education_level::basic, education_level::intermediate,
633 education_level::advanced};
634 // Youngest age at which a level can have been completed (a younger person has the level below).
635 static constexpr std::array<int, 4> education_min_age_{0, 14, 17, 21};
636
637 struct band
638 {
639 int min_age{0};
640 int max_age{0};
641 [[nodiscard]] bool contains(int age) const { return age >= min_age && age <= max_age; }
642 };
643
644 struct status_row
645 {
646 band ages;
647 std::array<double, 3> weights{}; // employed, unemployed, inactive
648 };
649
650 struct education_row
651 {
652 band ages;
653 std::array<double, 4> weights{};
654 };
655
656 struct inactivity_row
657 {
658 band ages;
659 std::array<double, 3> weights{}; // student, retired, other
660 };
661
662 struct isco_data
663 {
664 std::string code;
665 std::string label;
666 std::string major;
667 std::string skill;
668 };
669
670 struct job_data
671 {
672 std::string key;
673 std::string label;
674 std::string unit;
675 double weight{0.0};
676 int min_age{0};
677 int max_age{255};
678 std::vector<std::string> skills;
679 std::vector<std::string> gear;
680 std::vector<std::string> tags;
681 std::size_t isco_index{0};
682 };
683
684 struct factor_set
685 {
686 std::vector<std::pair<band, double>> age;
687 std::array<double, 4> education{1.0, 1.0, 1.0, 1.0};
688 };
689
690 struct country_data
691 {
692 std::array<std::vector<status_row>, 2> status;
693 std::array<std::vector<education_row>, 2> education;
694 std::array<core::weighted_table<std::size_t>, 2> shares; // ISCO index, by share
695 };
696
697 struct model
698 {
699 std::map<std::string, country_data, std::less<>> countries;
700 std::vector<std::string> codes; // countries with status rows for both sexes, sorted
701 std::vector<inactivity_row> inactivity;
702 std::vector<isco_data> isco;
703 std::vector<job_data> jobs;
704 std::vector<std::vector<std::size_t>> jobs_of; // per ISCO index
705 std::unordered_map<std::string, factor_set> factors; // country, major
706 std::vector<std::string> unresolved;
707 };
708
709 static std::string factor_key_(std::string_view country, std::string_view major)
710 {
711 std::string k;
712 k.reserve(country.size() + major.size() + 1);
713 k.append(country).push_back('\x1f');
714 k.append(major);
715 return k;
716 }
717
718 static std::size_t sex_index_(const core::tsv_row& row)
719 {
720 const auto s = row["sex"];
721 if (const auto parsed = core::parse_sex(s)) return static_cast<std::size_t>(*parsed);
722 throw row.error("sex", std::format("expected male or female, got '{}'", s));
723 }
724
725 // A whole number of at most three digits.
726 static std::optional<int> parse_age_(std::string_view text)
727 {
728 if (text.empty() || text.size() > 3) return std::nullopt;
729 int value = 0;
730 for (const char c : text)
731 {
732 if (c < '0' || c > '9') return std::nullopt;
733 value = value * 10 + (c - '0');
734 }
735 return value;
736 }
737
738 // "15-24" or "65+" (no upper limit).
739 static band parse_band_(const core::tsv_row& row, std::string_view column)
740 {
741 const auto text = row.get<std::string>(column);
742 const std::string_view view{text};
743 std::optional<band> parsed;
744 if (view.ends_with('+'))
745 {
746 if (const auto lo = parse_age_(view.substr(0, view.size() - 1))) parsed = band{.min_age = *lo, .max_age = 255};
747 }
748 else if (const auto dash = view.find('-'); dash != std::string_view::npos)
749 {
750 const auto lo = parse_age_(view.substr(0, dash));
751 const auto hi = parse_age_(view.substr(dash + 1));
752 if (lo && hi && *hi >= *lo) parsed = band{.min_age = *lo, .max_age = *hi};
753 }
754 if (!parsed)
755 {
756 throw row.error(column, std::format("expected an age band such as '15-24' or '65+', got '{}'", text));
757 }
758 return *parsed;
759 }
760
761 static double non_negative_(const core::tsv_row& row, std::string_view column)
762 {
763 const auto v = row.get<double>(column);
764 if (!(v >= 0.0)) throw row.error(column, "must not be negative");
765 return v;
766 }
767
768 static std::vector<std::string> split_list_(std::string_view text)
769 {
770 std::vector<std::string> out;
771 while (!text.empty())
772 {
773 const auto cut = text.find(';');
774 const auto item = text.substr(0, cut);
775 if (!item.empty()) out.emplace_back(item);
776 if (cut == std::string_view::npos) break;
777 text.remove_prefix(cut + 1);
778 }
779 return out;
780 }
781
782 static std::string check_country_(const core::tsv_row& row)
783 {
784 auto cca2 = row.get<std::string>("country_code");
785 if (!core::is_alpha2(cca2)) throw row.error("country_code", "expected two capital letters (ISO 3166-1 alpha-2)");
786 return cca2;
787 }
788
789 // Each parse_ function checks a row and throws core::tsv_error located at
790 // it; the same function validates a layer and builds the model.
791 static std::tuple<std::string, std::size_t, status_row> parse_status_(const core::tsv_row& row)
792 {
793 auto cca2 = check_country_(row);
794 status_row s;
795 s.ages = parse_band_(row, "band");
796 s.weights = {non_negative_(row, "employed"), non_negative_(row, "unemployed"), non_negative_(row, "inactive")};
797 if (!(s.weights[0] + s.weights[1] + s.weights[2] > 0.0)) throw row.error("employed", "the three shares are all 0");
798 return {std::move(cca2), sex_index_(row), s};
799 }
800
801 static inactivity_row parse_inactivity_(const core::tsv_row& row)
802 {
803 inactivity_row r;
804 r.ages = parse_band_(row, "band");
805 r.weights = {non_negative_(row, "student"), non_negative_(row, "retired"), non_negative_(row, "other")};
806 if (!(r.weights[0] + r.weights[1] + r.weights[2] > 0.0)) throw row.error("student", "the three shares are all 0");
807 return r;
808 }
809
810 static std::tuple<std::string, std::size_t, education_row> parse_education_(const core::tsv_row& row)
811 {
812 auto cca2 = check_country_(row);
813 education_row e;
814 e.ages = parse_band_(row, "band");
815 double total = 0.0;
816 for (std::size_t i = 0; i < education_levels_.size(); ++i)
817 {
818 e.weights.at(i) = non_negative_(row, to_string(education_levels_.at(i)));
819 total += e.weights.at(i);
820 }
821 if (!(total > 0.0)) throw row.error("less_than_basic", "the four shares are all 0");
822 return {std::move(cca2), sex_index_(row), e};
823 }
824
825 static isco_data parse_isco_(const core::tsv_row& row)
826 {
827 isco_data i;
828 i.code = row.get<std::string>("code");
829 i.label = row.get<std::string>("label");
830 i.major = row.get<std::string>("major");
831 i.skill = row.get_or<std::string>("skill", "");
832 return i;
833 }
834
835 struct share_data
836 {
837 std::string country;
838 std::size_t sex_index{0};
839 std::string code;
840 double share{0.0};
841 };
842
843 static share_data parse_share_(const core::tsv_row& row)
844 {
845 share_data s;
846 s.country = check_country_(row);
847 s.sex_index = sex_index_(row);
848 s.code = row.get<std::string>("code");
849 s.share = non_negative_(row, "share");
850 return s;
851 }
852
853 struct factor_data
854 {
855 std::string country;
856 std::string major;
857 bool is_age{true};
858 band ages{};
859 std::size_t level{0};
860 double factor{1.0};
861 };
862
863 static factor_data parse_factor_(const core::tsv_row& row)
864 {
865 factor_data f;
866 f.country = check_country_(row);
867 f.major = row.get<std::string>("major");
868 const auto dimension = row["dimension"];
869 if (dimension == "age_band")
870 {
871 f.is_age = true;
872 f.ages = parse_band_(row, "value");
873 }
874 else if (dimension == "education")
875 {
876 f.is_age = false;
877 const auto value = row["value"];
878 const auto it = std::ranges::find_if(education_levels_, [&](education_level l) { return to_string(l) == value; });
879 if (it == education_levels_.end())
880 {
881 throw row.error("value", std::format("expected less_than_basic, basic, intermediate or advanced, got '{}'", value));
882 }
883 f.level = static_cast<std::size_t>(it - education_levels_.begin());
884 }
885 else
886 {
887 throw row.error("dimension", std::format("expected age_band or education, got '{}'", dimension));
888 }
889 f.factor = non_negative_(row, "factor");
890 return f;
891 }
892
893 struct occupation_data
894 {
895 job_data job;
896 std::string isco;
897 };
898
899 static occupation_data parse_job_(const core::tsv_row& row)
900 {
901 occupation_data o;
902 o.job.key = row.get<std::string>("key");
903 o.job.label = row.get<std::string>("label");
904 o.isco = row.get<std::string>("isco");
905 o.job.weight = non_negative_(row, "weight");
906 o.job.unit = row.get_or<std::string>("unit", "");
907 o.job.min_age = row.get_or<int>("min_age", 0);
908 o.job.max_age = row.get_or<int>("max_age", 255);
909 if (o.job.min_age < 0) throw row.error("min_age", "must not be negative");
910 if (o.job.max_age < o.job.min_age) throw row.error("max_age", "must not be below min_age");
911 o.job.skills = split_list_(row.get_or<std::string>("skills", ""));
912 o.job.gear = split_list_(row.get_or<std::string>("gear", ""));
913 o.job.tags = split_list_(row.get_or<std::string>("tags", ""));
914 return o;
915 }
916
917 static void validate_(table t, const core::tsv_row& row)
918 {
919 switch (t)
920 {
921 case table::status: (void)parse_status_(row); break;
922 case table::inactivity: (void)parse_inactivity_(row); break;
923 case table::education: (void)parse_education_(row); break;
924 case table::isco: (void)parse_isco_(row); break;
925 case table::shares: (void)parse_share_(row); break;
926 case table::factors: (void)parse_factor_(row); break;
927 case table::occupations: (void)parse_job_(row); break;
928 }
929 }
930
931 // -- State ----------------------------------------------------------------
932
933 tables _tables;
934 model _model;
935 core::rng _engine{core::rng::from_entropy()};
936
937 struct auto_probe_tag {};
938
939 explicit ocg(auto_probe_tag /*tag*/) : _tables{make_tables_()}
940 {
941 if (!load(dataset::lite)) (void)load(dataset::full);
942 }
943
944 // -- Helpers ----------------------------------------------------------
945
946 static std::span<const core::embedded_file> embedded_files_()
947 {
948#ifdef DASMIG_OCCUPATION_EMBED
949 return embedded::occupationgen::files;
950#else
951 return {};
952#endif
953 }
954
955 std::uint64_t draw_seed_() { return _engine(); }
956
957 static documents read_directory_(const std::filesystem::path& dir)
958 {
959 documents docs;
960 for (const auto t : all_tables_)
961 {
962 const auto file = dir / file_name_(t);
963 std::error_code ec;
964 if (std::filesystem::is_regular_file(file, ec))
965 {
966 docs.at(index_(t)) = core::tsv_document::load(file);
967 }
968 }
969 return docs;
970 }
971
972 // Apply layers to copies of the tables, then commit: all or nothing.
973 // With @p reset (load), tables without a document are emptied.
974 void apply_(documents docs, core::layer_mode mode, bool reset)
975 {
976 tables next = _tables;
977 for (const auto t : all_tables_)
978 {
979 auto& doc = docs.at(index_(t));
980 auto& tbl = next.at(index_(t));
981 if (!doc)
982 {
983 if (reset) tbl.clear();
984 continue;
985 }
986 (void)tbl.extend(std::move(*doc), mode, [t](const core::tsv_row& row) { validate_(t, row); });
987 }
988 auto built = build_model_(next);
989 _tables = std::move(next);
990 _model = std::move(built);
991 }
992
993 static model build_model_(const tables& tbls)
994 {
995 model m;
996 for (const auto& row : tbls.at(index_(table::status)).rows())
997 {
998 auto [cca2, si, s] = parse_status_(row);
999 m.countries[cca2].status.at(si).push_back(s);
1000 }
1001 for (const auto& [cca2, c] : m.countries)
1002 {
1003 if (!c.status[0].empty() && !c.status[1].empty()) m.codes.push_back(cca2);
1004 }
1005 for (const auto& row : tbls.at(index_(table::inactivity)).rows()) m.inactivity.push_back(parse_inactivity_(row));
1006 for (const auto& row : tbls.at(index_(table::education)).rows())
1007 {
1008 auto [cca2, si, e] = parse_education_(row);
1009 m.countries[cca2].education.at(si).push_back(e);
1010 }
1011
1012 std::unordered_map<std::string, std::size_t> isco_index;
1013 for (const auto& row : tbls.at(index_(table::isco)).rows())
1014 {
1015 auto i = parse_isco_(row);
1016 isco_index[i.code] = m.isco.size();
1017 m.isco.push_back(std::move(i));
1018 }
1019 m.jobs_of.resize(m.isco.size());
1020
1021 for (const auto& row : tbls.at(index_(table::shares)).rows())
1022 {
1023 const auto s = parse_share_(row);
1024 const auto it = isco_index.find(s.code);
1025 if (it == isco_index.end())
1026 {
1027 m.unresolved.push_back(std::format("{}:{}: unknown ISCO group '{}'", row.source(), row.line(), s.code));
1028 continue;
1029 }
1030 m.countries[s.country].shares.at(s.sex_index).add(it->second, s.share);
1031 }
1032 for (const auto& row : tbls.at(index_(table::factors)).rows())
1033 {
1034 const auto f = parse_factor_(row);
1035 auto& set = m.factors[factor_key_(f.country, f.major)];
1036 if (f.is_age) set.age.emplace_back(f.ages, f.factor);
1037 else set.education.at(f.level) = f.factor;
1038 }
1039 for (const auto& row : tbls.at(index_(table::occupations)).rows())
1040 {
1041 auto o = parse_job_(row);
1042 const auto it = isco_index.find(o.isco);
1043 if (it == isco_index.end())
1044 {
1045 m.unresolved.push_back(std::format("{}:{}: unknown ISCO group '{}'", row.source(), row.line(), o.isco));
1046 continue;
1047 }
1048 o.job.isco_index = it->second;
1049 m.jobs_of.at(it->second).push_back(m.jobs.size());
1050 m.jobs.push_back(std::move(o.job));
1051 }
1052 return m;
1053 }
1054
1055 void require_data_() const
1056 {
1057 if (!has_data())
1058 {
1059 throw std::runtime_error("No occupation data loaded. Call load() first.");
1060 }
1061 }
1062
1063 template <class Row>
1064 static const Row* find_row_(const std::vector<Row>& rows, int age)
1065 {
1066 const auto it = std::ranges::find_if(rows, [age](const Row& r) { return r.ages.contains(age); });
1067 return it == rows.end() ? nullptr : &*it;
1068 }
1069
1070 template <class T, std::size_t N>
1071 static T draw_(core::rng& r, const std::array<double, N>& weights, const std::array<T, N>& values)
1072 {
1073 core::weighted_table<T> t;
1074 for (std::size_t i = 0; i < N; ++i) t.add(values.at(i), weights.at(i));
1075 return t.pick(r);
1076 }
1077
1078 [[nodiscard]] employment_status draw_status_(const country_data& c, std::size_t si, int age, std::string_view cca2,
1079 core::rng& r) const
1080 {
1081 if (age < school_age_) return employment_status::inactive;
1082 if (age < working_age_) return employment_status::student;
1083 const auto* row = find_row_(c.status.at(si), age);
1084 if (row == nullptr)
1085 {
1086 throw std::invalid_argument(std::format("No employment status data for age {} in {}.", age, cca2));
1087 }
1088 const auto kind = draw_(r, row->weights,
1089 std::array{employment_status::employed, employment_status::unemployed,
1090 employment_status::inactive});
1091 if (kind != employment_status::inactive) return kind;
1092 const auto* split = find_row_(_model.inactivity, age);
1093 if (split == nullptr) return employment_status::inactive;
1094 return draw_(r, split->weights,
1095 std::array{employment_status::student, employment_status::retired, employment_status::inactive});
1096 }
1097
1098 [[nodiscard]] static std::optional<education_level> draw_education_(const country_data& c, std::size_t si, int age,
1099 core::rng& r)
1100 {
1101 if (age < working_age_) return std::nullopt;
1102 const auto* row = find_row_(c.education.at(si), age);
1103 if (row == nullptr) return std::nullopt;
1104 // A level the person is too young to have completed counts as the one below it.
1105 std::array<double, 4> w = row->weights;
1106 for (std::size_t i = w.size() - 1; i > 0; --i)
1107 {
1108 if (age < education_min_age_.at(i))
1109 {
1110 w.at(i - 1) += w.at(i);
1111 w.at(i) = 0.0;
1112 }
1113 }
1114 return draw_(r, w, education_levels_);
1115 }
1116
1117 [[nodiscard]] static bool job_open_(const job_data& j, int age)
1118 {
1119 return j.weight > 0.0 && age >= j.min_age && age <= j.max_age;
1120 }
1121
1122 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
1123 [[nodiscard]] job_info draw_job_(const country_data& c, std::string_view cca2, std::size_t si, int age,
1124 std::optional<education_level> education, const core::rng& call) const
1125 {
1126 // A group is open when one of its jobs is open to this age; its weight is the
1127 // country's share for the sex, times the factors for the age class and education.
1128 const int job_age = std::max(age, working_age_);
1129 std::vector<double> factor(_model.isco.size(), 0.0);
1130 for (std::size_t i = 0; i < _model.isco.size(); ++i)
1131 {
1132 const bool open = std::ranges::any_of(_model.jobs_of.at(i), [&](std::size_t j) {
1133 return job_open_(_model.jobs.at(j), age);
1134 });
1135 if (!open) continue;
1136 double f = 1.0;
1137 const auto it = _model.factors.find(factor_key_(cca2, _model.isco.at(i).major));
1138 if (it != _model.factors.end())
1139 {
1140 for (const auto& [ages, value] : it->second.age)
1141 {
1142 if (ages.contains(job_age)) f *= value;
1143 }
1144 if (education) f *= it->second.education.at(static_cast<std::size_t>(*education));
1145 }
1146 factor.at(i) = f;
1147 }
1148 auto rg = call.split("isco");
1149 const auto group = c.shares.at(si).try_pick_index(rg, [&factor](const std::size_t& i, double base) {
1150 return base * factor.at(i);
1151 });
1152 if (!group)
1153 {
1154 throw std::invalid_argument(std::format("No occupation is open to a person aged {} in {}.", age, cca2));
1155 }
1156 const std::size_t isco = c.shares.at(si).entries()[*group].value;
1157
1158 core::weighted_table<std::size_t> jobs;
1159 for (const auto j : _model.jobs_of.at(isco))
1160 {
1161 if (job_open_(_model.jobs.at(j), age)) jobs.add(j, _model.jobs.at(j).weight);
1162 }
1163 auto rj = call.split("job");
1164 const auto& d = _model.jobs.at(jobs.pick(rj));
1165 const auto& g = _model.isco.at(isco);
1166 return {.key = d.key,
1167 .label = d.label,
1168 .isco = g.code,
1169 .isco_label = g.label,
1170 .unit = d.unit,
1171 .skill = g.skill,
1172 .skills = d.skills,
1173 .gear = d.gear,
1174 .tags = d.tags};
1175 }
1176
1177 [[nodiscard]] occupation generate_(const occupation_request& req, std::uint64_t call_seed) const
1178 {
1179 require_data_();
1180 const core::rng call{call_seed};
1181
1182 std::string cca2{req.country};
1183 if (cca2.empty())
1184 {
1185 if (_model.codes.empty())
1186 {
1187 throw std::invalid_argument("No country has employment status data for both sexes.");
1188 }
1189 auto cr = call.split("country");
1190 cca2 = _model.codes[static_cast<std::size_t>(core::bounded(cr, _model.codes.size()))];
1191 }
1192 const auto it = _model.countries.find(cca2);
1193 if (it == _model.countries.end())
1194 {
1195 throw core::unknown_country_error(cca2);
1196 }
1197 const auto& c = it->second;
1198
1199 occupation v;
1200 v._signature = {.seed = call_seed, .data = data_hash()};
1201 v.country_code = cca2;
1202 v.age = req.age;
1203 if (req.person_sex)
1204 {
1205 v.person_sex = *req.person_sex;
1206 }
1207 else
1208 {
1209 auto rs = call.split("sex");
1210 v.person_sex = core::bernoulli(rs, 0.5) ? sex::female : sex::male;
1211 }
1212 const std::size_t si = v.person_sex == sex::male ? 0 : 1;
1213 const int age = req.age;
1214
1215 if (req.status)
1216 {
1217 v.status = *req.status;
1218 }
1219 else
1220 {
1221 auto rt = call.split("status");
1222 v.status = draw_status_(c, si, age, cca2, rt);
1223 }
1224 if (req.education)
1225 {
1226 v.education = req.education;
1227 }
1228 else
1229 {
1230 auto re = call.split("education");
1231 v.education = draw_education_(c, si, age, re);
1232 }
1233 const bool preschool = age < school_age_ && v.status == employment_status::inactive;
1234 if (v.status != employment_status::student && !preschool)
1235 {
1236 v.job = draw_job_(c, cca2, si, age, v.education, call);
1237 }
1238 return v;
1239 }
1240};
1241
1242} // namespace dasmig
1243
1244#endif // DASMIG_OCCUPATIONGEN_HPP
Return type for occupation generation.
std::string country_code
ISO 3166-1 alpha-2 code of the country drawn from.
std::optional< job_info > job
sex person_sex
Sex (given or drawn).
void to_json(core::json_writer &w) const
Write this occupation as a JSON object.
const core::seed_signature & signature() const
The seed and the data this occupation was generated from.
std::string to_string() const
Human-readable summary string.
employment_status status
Employment status.
std::uint64_t seed() const
Retrieve the random seed used to generate this occupation.
friend std::ostream & operator<<(std::ostream &os, const occupation &v)
Stream the summary to an output stream.
std::uint8_t age
Age in completed years.
std::string to_json() const
This occupation as a JSON document.
std::optional< education_level > education
Education; unknown under 15 when not given.
Occupation Generator: draws a person's employment status, education and job.
bool has_data() const
Check whether status data has been loaded.
ocg & operator=(const ocg &)=delete
Not copyable.
std::size_t job_count() const
Return the number of jobs in the catalogue.
ocg(const ocg &)=delete
Not copyable.
void for_each_occupation(const occupation_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of occupation results with a batch seed drawn like get_occupation() draws its own.
void for_each_occupation(const occupation_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of occupation results to a sink without holding the whole batch.
occupation get_occupation(const occupation_request &request, std::uint64_t call_seed) const
Generate a deterministic occupation for a request.
std::size_t country_count() const
Return the number of countries that can be drawn from.
std::vector< occupation > get_occupation_batch(const occupation_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of occupation results, deterministically.
const std::vector< std::string > & unresolved() const
Rows that name something that is not loaded: a share for an unknown ISCO group, a job for an unknown ...
void load(const std::filesystem::path &dir)
Load a resource directory, replacing all data and layers loaded before.
table
The data tables.
@ occupations
occupations.tsv: key key; label, isco, weight, unit, min_age, max_age, tags, skills,...
@ inactivity
inactivity.tsv: key band; student, retired, other (shares of the inactive).
@ isco
isco.tsv: key code (ISCO-08 level 2); label, major, skill.
@ status
status.tsv: key country_code, sex, band; employed, unemployed, inactive (percent),...
@ factors
factors.tsv: key country_code, major, dimension, value; factor, basis, year.
@ education
education.tsv: key country_code, sex, band; less_than_basic, basic, intermediate, advanced,...
@ shares
shares.tsv: key country_code, sex, code; share (percent of employment), basis, year.
ocg(ocg &&) noexcept=default
Movable.
const std::vector< core::layer_info > & layers(table which) const
The layers in effect for one table, in order.
std::vector< occupation > get_occupation_batch(const occupation_request &request, const core::batch_options &options)
Generate a batch of occupation results from a seed drawn like get_occupation() draws its own.
occupation get_occupation(const occupation_request &request)
Generate a random occupation for a request.
const core::layer_info & extend_from_string(table which, std::string_view text, std::string source, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table from TSV text (for example from a WebAssembly host).
static ocg & instance()
Access the global singleton instance.
void extend(const std::filesystem::path &path, core::layer_mode mode=core::layer_mode::override)
Add data layers from a directory or a file.
bool load(dataset tier)
Load a dataset tier, replacing all data and layers loaded before.
const core::layer_info & extend(table which, core::tsv_document doc, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table.
ocg()
Default constructor — creates an empty generator with no data.
std::uint64_t data_hash() const
Hash of the loaded data (all tables), recorded in each result's signature().
employment_status
What a person does with their working-age days.
@ employed
In employment.
@ inactive
Outside the labour force for another reason: care, illness, discouragement; under 6,...
@ retired
Retired (among the inactive, by age).
@ student
Studying (always, from 6 to 14; among the inactive, by age).
@ unemployed
Without work and looking for it.
education_level
Highest education completed, in the ILO's four aggregate levels.
@ advanced
Advanced: tertiary (ISCED 5-8).
@ intermediate
Intermediate: upper secondary and post-secondary non-tertiary (ISCED 3-4).
@ less_than_basic
Less than basic: not completed primary education, or none.
@ basic
Basic: primary or lower secondary (ISCED 1-2).
constexpr std::string_view to_string(employment_status s) noexcept
The name of an employment status, as used in the data and in JSON.
A job, as drawn.
std::string isco_label
Title of that group.
std::vector< std::string > tags
Character tags such as "manual" or "caring", for other generators.
std::string key
Catalogue key (the key column of occupations.tsv).
std::string label
Display label, for example "Plumber".
std::string unit
ISCO-08 unit group (four digits); may be empty.
std::string skill
ISCO-08 skill level of the group ("1", "2", "3-4"); may be empty.
std::vector< std::string > gear
Clothing and gear keywords.
std::vector< std::string > skills
Skill keywords.
std::string isco
ISCO-08 level-2 group code, for example "71".
Per-call generation choices. Every field may be omitted.
std::string_view country
ISO 3166-1 alpha-2 code; empty draws a country (uniform over those loaded).
std::uint8_t age
Age in completed years (default 40, an age at which most people work).
std::optional< education_level > education
Education; unknown draws one from the country, sex and age.
std::optional< employment_status > status
Status; unknown draws one from the country, sex and age.
std::optional< sex > person_sex
Sex; unknown draws one (even odds).