persona-generator 4.1.3
Complete characters composed from the other dasmig generators for C++23
Loading...
Searching...
No Matches
personagen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Diego Dasso Migotto
3
4#ifndef DASMIG_PERSONAGEN_HPP
5#define DASMIG_PERSONAGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <any>
10#include <array>
11#include <chrono>
12#include <cstddef>
13#include <cstdint>
14#include <memory>
15#include <optional>
16#include <ostream>
17#include <span>
18#include <stdexcept>
19#include <string>
20#include <string_view>
21#include <utility>
22#include <vector>
23
24#include <dasmig/biodatagen.hpp>
25#include <dasmig/birthgen.hpp>
26#include <dasmig/citygen.hpp>
27#include <dasmig/countrygen.hpp>
28#include <dasmig/entitygen.hpp>
29#include <dasmig/interestgen.hpp>
30#include <dasmig/namegen.hpp>
31#include <dasmig/occupationgen.hpp>
32#include <dasmig/relgen.hpp>
33#include <dasmig/traitgen.hpp>
34
35/// @file personagen.hpp
36/// @brief persona-generator library — complete characters composed from the other dasmig generators for C++23.
37/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
38/// @see See doc/usage.md for the narrative tutorial.
39
40DASMIG_CORE_REQUIRE(1, 2);
41
42namespace dasmig
43{
44
45/// @brief What a persona call asks for.
46///
47/// Every generation call takes one; `persona_request{}` asks for the
48/// defaults. Choices are never stored on the generator, so one call cannot
49/// change the next. Every field is optional: what is left out is drawn, and
50/// what is given conditions the draws that depend on it (the country sets the
51/// city, the name culture, the body and the job; the age and sex set the rest).
53{
54 /// @brief ISO 3166-1 alpha-2 code (any case). Unset draws one of
55 /// psg::supported_countries(), weighted by population.
56 std::optional<std::string> country{}; // NOLINT(readability-redundant-member-init)
57
58 /// @brief Biological sex; unset draws it from the birth.
59 std::optional<sex> person_sex{}; // NOLINT(readability-redundant-member-init)
60
61 /// @brief Inclusive age range in completed years; unset draws from the
62 /// country's age pyramid.
63 std::optional<age_range> ages{}; // NOLINT(readability-redundant-member-init)
64
65 /// @brief The date ages are computed at. The default is birth-generator's
66 /// (2023-07-01); the library never reads the clock.
67 std::chrono::year_month_day reference_date{birth_request::default_reference_date};
68
69 /// @brief How many personality traits to draw.
70 std::size_t traits{3};
71
72 /// @brief Also draw the people around the persona: parents, siblings,
73 /// partners and children (relationship-generator). Unset by
74 /// default, and then nothing about the persona changes.
75 ///
76 /// The `anchor` of this request is replaced by the persona (its country,
77 /// sex, birth date and name) and its `reference_date` by this request's;
78 /// the rest (`up`, `down`, `siblings`, `partners`, `names`, `name_culture`)
79 /// is used as given. The family is drawn from its own seed, split from the
80 /// persona's, so the rest of the persona is the same with and without it.
81 std::optional<family_request> relations{}; // NOLINT(readability-redundant-member-init)
82};
83
84/// @brief Return type for persona generation.
85///
86/// Holds the typed result of every library in the pipeline. Supports
87/// implicit conversion to std::string (a one-line summary), streaming via
88/// operator<< and JSON output via to_json().
90{
91 public:
92 std::string name; ///< Full name (UTF-8), from the country's culture and the sex.
93 dasmig::country country_info; ///< The country.
94 dasmig::city city_info; ///< The city of residence, in the country.
95 std::string admin1; ///< Name of the city's state or province; empty when city-generator has no admin1 names loaded.
96 dasmig::birth birthday; ///< Birth date, completed-year age and sex.
97 dasmig::biodata body; ///< Height, weight, eyes, hair, skin and blood type, at the persona's age and sex.
98 dasmig::personality traits; ///< Personality traits and axes.
99 dasmig::interest interests; ///< Religion, hobbies and media.
100 dasmig::occupation job; ///< Employment status, education and job.
101 std::optional<family> relatives{}; ///< The persona's family; set only when `persona_request::relations` was. // NOLINT(readability-redundant-member-init)
102
103 /// @brief Retrieve the random seed used to generate this persona.
104 /// @return The call seed; pass it to get_persona(request, seed) with
105 /// the same request and data to replay this persona.
106 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
107
108 /// @brief The seed and the data this persona was generated from.
109 /// @return The replay signature; `data` combines the data hashes of the
110 /// eight libraries and the request.
111 [[nodiscard]] const core::seed_signature& signature() const
112 {
113 return _signature;
114 }
115
116 /// @brief Biological sex of the persona.
117 /// @return The sex drawn with the birth.
118 [[nodiscard]] sex person_sex() const { return birthday.bio_sex; }
119
120 /// @brief Age in completed years at the request's reference date.
121 /// @return The age.
122 [[nodiscard]] unsigned age() const { return birthday.age; }
123
124 /// @brief One-line summary: name, age, sex, city, country and job.
125 /// @return For example `Ana Silva, 34, female, Porto, Portugal, Teacher`.
126 [[nodiscard]] std::string to_string() const
127 {
128 std::string out = name + ", " + std::to_string(age()) + ", "
129 + std::string{core::sex_name(person_sex())} + ", "
130 + city_info.name + ", " + country_info.name_common;
131 const auto text = job.to_string();
132 if (!text.empty()) out += ", " + text;
133 return out;
134 }
135
136 /// @brief Write this persona as a JSON object.
137 ///
138 /// Each library's result is written as that library writes it
139 /// (`country`, `city`, `birth`, `biodata`, `personality`, `interest`,
140 /// `occupation`), next to `name`, `sex`, `age`, `admin1` and `signature`,
141 /// and `relatives` (the family) when the request asked for it.
142 /// @param w Writer positioned where a value is allowed.
143 void to_json(core::json_writer& w) const
144 {
145 w.begin_object()
146 .field("name", name)
147 .field("sex", core::sex_name(person_sex()))
148 .field("age", age())
149 .field("admin1", admin1);
150 w.key("country");
151 country_info.to_json(w);
152 w.key("city");
153 city_info.to_json(w);
154 w.key("birth");
155 birthday.to_json(w);
156 w.key("biodata");
157 body.to_json(w);
158 w.key("personality");
159 traits.to_json(w);
160 w.key("interest");
161 interests.to_json(w);
162 w.key("occupation");
163 job.to_json(w);
164 if (relatives)
165 {
166 w.key("relatives");
167 relatives->to_json(w);
168 }
169 w.key("signature");
170 _signature.to_json(w);
171 w.end_object();
172 }
173
174 /// @brief This persona as a JSON document.
175 /// @return Compact JSON.
176 [[nodiscard]] std::string to_json() const
177 {
178 core::json_writer w;
179 to_json(w);
180 return w.str();
181 }
182
183 /// @brief Implicit conversion to std::string.
184 /// @return Summary string.
185 operator std::string() const // NOLINT(hicpp-explicit-conversions)
186 {
187 return to_string();
188 }
189
190 /// @brief Stream the summary to an output stream.
191 /// @param os Output stream.
192 /// @param v The persona to stream.
193 /// @return The stream.
194 friend std::ostream& operator<<(std::ostream& os, const persona& v)
195 {
196 os << v.to_string();
197 return os;
198 }
199
200 private:
201 core::seed_signature _signature;
202 friend class psg;
203};
204
205/// @brief persona-generator: runs the persona pipeline.
206///
207/// The pipeline draws a country, then a city, a birth, a body, a name,
208/// interests, an occupation and a personality, each conditioned on what was
209/// drawn before (the order and dependencies are declared as entity-generator
210/// fields, so they are checked, not assumed). Each library gets its own call
211/// seed, split from the persona's seed by the field's key, so the persona's
212/// seed replays all of it, and forcing one choice (a country, a sex) does not
213/// shift the draws of the others. Output is identical on every compiler and
214/// platform (dasmig-core).
215///
216/// The generator holds no data of its own: it calls the instances of the eight
217/// libraries it was built on (the singletons by default), so their data, their
218/// layers and their `lite` or `full` tiers are the persona's.
219///
220/// @par Thread safety
221/// get_persona(request, seed) is const and only calls const members of the
222/// libraries, so it may run concurrently. get_persona(request) draws from the
223/// generator's engine and needs external synchronisation on the same instance.
224class psg
225{
226 public:
227 /// @brief The library instances a persona is drawn from.
228 struct sources
229 {
230 cntg* country{&cntg::instance()}; ///< Countries.
231 cg* city{&cg::instance()}; ///< Cities.
232 bthg* birth{&bthg::instance()}; ///< Birth dates and ages.
233 bdg* biodata{&bdg::instance()}; ///< Bodies.
234 ng* name{&ng::instance()}; ///< Names.
235 intg* interest{&intg::instance()}; ///< Religion, hobbies and media.
236 ocg* occupation{&ocg::instance()}; ///< Employment, education and jobs.
237 trtg* trait{&trtg::instance()}; ///< Personality.
238 relg* relationship{&relg::instance()}; ///< Family; used only when a request asks for relations.
239 };
240
241 /// @brief Draws from the singletons of the eight libraries.
242 psg() { build_(); }
243
244 /// @brief Draws from the given library instances (which must outlive this
245 /// generator).
246 /// @param from The instances to use.
247 explicit psg(sources from) : _from{from} { build_(); }
248
249 psg(const psg&) = delete; ///< Not copyable.
250 psg& operator=(const psg&) = delete; ///< Not copyable.
251 psg(psg&&) noexcept = default; ///< Movable.
252 psg& operator=(psg&&) noexcept = default; ///< Movable.
253 ~psg() = default; ///< Destructor.
254
255 /// @brief Access the global singleton instance.
256 /// @return A reference to the singleton.
257 static psg& instance()
258 {
259 static psg instance;
260 return instance;
261 }
262
263 /// @brief The countries a random persona is drawn from.
264 ///
265 /// The countries for which all eight libraries have data in their `lite`
266 /// tier. A country can be asked for by `persona_request::country` outside
267 /// this list; the libraries then use what they have for it, or reject it
268 /// (South Korea, for example, needs name-generator's `full` tier: its
269 /// `lite` tier has no Korean male names).
270 /// @return ISO 3166-1 alpha-2 codes, sorted.
271 [[nodiscard]] static std::span<const std::string_view> supported_countries()
272 {
273 return supported_;
274 }
275
276 /// @brief Check whether every library has data.
277 /// @return True when all eight libraries can generate.
278 [[nodiscard]] bool has_data() const
279 {
280 return _from.country->has_data() && _from.city->has_data() && _from.birth->has_data()
281 && _from.biodata->has_data() && _from.name->has_resources()
282 && _from.interest->has_data() && _from.occupation->has_data()
283 && _from.trait->has_data();
284 }
285
286 /// @brief Load a tier into every library.
287 /// @param tier `dataset::lite` or `dataset::full`.
288 /// @return True when all eight libraries found their data.
289 [[nodiscard]] bool load(dataset tier) // NOLINT(readability-make-member-function-const)
290 {
291 bool ok = _from.country->load(tier);
292 ok = _from.city->load(tier) && ok;
293 ok = _from.birth->load(tier) && ok;
294 ok = _from.biodata->load(tier) && ok;
295 ok = _from.name->load(tier) && ok;
296 ok = _from.interest->load(tier) && ok;
297 ok = _from.occupation->load(tier) && ok;
298 ok = _from.trait->load(tier) && ok;
299 // Only a request for relations needs this one, so a missing tier of it
300 // does not fail the load.
301 static_cast<void>(_from.relationship->load(tier));
302 return ok;
303 }
304
305 /// @brief Hash of the data of all eight libraries.
306 /// @return A hash that changes when any library's loaded data does.
307 [[nodiscard]] std::uint64_t data_hash() const
308 {
309 std::uint64_t h = 0;
310 for (const auto part : {_from.country->data_hash(), _from.city->data_hash(),
311 _from.birth->data_hash(), _from.biodata->data_hash(),
312 _from.name->data_hash(), _from.interest->data_hash(),
313 _from.occupation->data_hash(), _from.trait->data_hash()})
314 {
315 h = core::hash_combine(h, part);
316 }
317 return h;
318 }
319
320 /// @brief Generate a persona with a new random seed.
321 /// @param request What to ask for; `{}` for the defaults.
322 /// @return The persona; seed() replays it.
323 /// @throws std::runtime_error If a library has no data.
324 /// @throws std::invalid_argument If the request is invalid.
325 [[nodiscard]] persona get_persona(const persona_request& request)
326 {
327 return get_persona(request, _engine());
328 }
329
330 /// @brief Generate the persona for a call seed. Deterministic; does not
331 /// touch the generator's engine.
332 /// @param request What to ask for.
333 /// @param call_seed Any 64-bit seed.
334 /// @return The same persona for the same seed, request and data.
335 /// @throws std::runtime_error If a library has no data.
336 /// @throws std::invalid_argument If the request is invalid.
337 [[nodiscard]] persona get_persona(const persona_request& request,
338 std::uint64_t call_seed) const
339 {
340 if (!has_data()) throw std::runtime_error("persona-generator: a library has no data loaded");
341 if (request.ages && request.ages->min > request.ages->max)
342 throw std::invalid_argument("persona_request: ages.min must be <= ages.max");
343
344 entity_request er;
345 std::string country_code;
346 if (request.country)
347 {
348 country_code = *request.country;
349 std::ranges::transform(country_code, country_code.begin(), [](char c) {
350 return static_cast<char>(c >= 'a' && c <= 'z' ? c - 'a' + 'A' : c);
351 });
352 er.inputs["country"] = country_code;
353 }
354 if (request.person_sex)
355 er.inputs["sex"] = std::string{core::sex_name(*request.person_sex)};
356 if (request.ages)
357 {
358 er.inputs["age_min"] = static_cast<std::int64_t>(request.ages->min);
359 er.inputs["age_max"] = static_cast<std::int64_t>(request.ages->max);
360 }
361 er.inputs["reference_date"] =
362 static_cast<std::int64_t>(static_cast<int>(request.reference_date.year())) * 10000
363 + static_cast<std::int64_t>(static_cast<unsigned>(request.reference_date.month())) * 100
364 + static_cast<std::int64_t>(static_cast<unsigned>(request.reference_date.day()));
365 er.inputs["traits"] = static_cast<std::int64_t>(request.traits);
366
367 if (request.relations && !_from.relationship->has_family_data())
368 throw std::runtime_error("persona-generator: relationship-generator has no data loaded");
369
370 const auto e = _pipeline.generate(er, call_seed);
371
372 persona p;
373 p.country_info = e.get<dasmig::country>(L"country");
374 p.city_info = e.get<dasmig::city>(L"city");
375 p.birthday = e.get<dasmig::birth>(L"birth");
376 p.body = e.get<dasmig::biodata>(L"biodata");
377 const auto& named = e.get<named_>(L"name");
378 p.name = named.full;
379 p.traits = e.get<dasmig::personality>(L"traits");
380 p.interests = e.get<dasmig::interest>(L"interest");
381 p.job = e.get<dasmig::occupation>(L"occupation");
382 if (_from.city->has_admin1())
383 p.admin1 = _from.city->resolve_admin1(p.city_info.country_code, p.city_info.admin1_code);
384 p._signature = e.signature();
385 if (request.relations)
386 {
387 auto family = *request.relations;
388 family.anchor = anchor_spec{.country_code = p.country_info.cca2,
389 .bio_sex = p.birthday.bio_sex,
390 .birth = std::chrono::year_month_day{
391 std::chrono::year{p.birthday.year},
392 std::chrono::month{p.birthday.month},
393 std::chrono::day{p.birthday.day}},
394 .name = named.parts};
395 family.reference_date = request.reference_date;
396 p.relatives = _from.relationship->get_family(family, core::rng{call_seed}.split("relations").seed());
397 }
398 return p;
399 }
400
401 // -- Batch generation -------------------------------------------------
402
403 /// @brief Generate a batch of persona results from a seed drawn like
404 /// get_persona() draws its own.
405 ///
406 /// Item `i` is `get_persona(request, core::item_seed(batch_seed, options.first + i))`
407 /// for the drawn batch seed; see the seeded overload.
408 ///
409 /// @param request What every item is generated from.
410 /// @param options Count, first index, threads and uniqueness.
411 /// @return The results, in index order.
412 [[nodiscard]] std::vector<persona> get_persona_batch(const persona_request& request,
413 const core::batch_options& options)
414 {
415 return get_persona_batch(request, options, _engine());
416 }
417
418 /// @brief Generate a batch of persona results, deterministically.
419 ///
420 /// Item `i` is exactly `get_persona(request, core::item_seed(batch_seed,
421 /// options.first + i))`, so an item replays on its own, a range can be
422 /// generated directly, two batches that follow each other equal one, and
423 /// the output does not depend on `options.threads`.
424 ///
425 /// @param request What every item is generated from.
426 /// @param options Count, first index, threads and uniqueness (see
427 /// core::unique_rule; the rule must be made for `persona`).
428 /// @param batch_seed Seed of the whole batch.
429 /// @return The results, in index order.
430 /// @throws std::invalid_argument If the request or options are invalid.
431 /// @throws std::runtime_error If no data has been loaded, or a unique
432 /// batch runs out of attempts.
433 [[nodiscard]] std::vector<persona> get_persona_batch(const persona_request& request,
434 const core::batch_options& options,
435 std::uint64_t batch_seed) const
436 {
437 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
438 { return get_persona(request, item_seed); });
439 }
440
441 /// @brief Stream a batch of persona results to a sink without holding the
442 /// whole batch.
443 ///
444 /// Generates the same items as get_persona_batch() and calls
445 /// `sink(index, item)` in index order on the calling thread; `index`
446 /// includes `options.first`.
447 ///
448 /// @tparam Sink Callable taking `(std::uint64_t, persona&)`; for example
449 /// core::jsonl_writer or core::csv_writer.
450 /// @param request What every item is generated from.
451 /// @param options Count, first index, threads and uniqueness.
452 /// @param batch_seed Seed of the whole batch.
453 /// @param sink Receives the items.
454 template <typename Sink>
455 void for_each_persona(const persona_request& request, const core::batch_options& options,
456 std::uint64_t batch_seed, Sink&& sink) const
457 {
458 core::for_each_batch(options, batch_seed,
459 [&](std::uint64_t item_seed) { return get_persona(request, item_seed); },
460 std::forward<Sink>(sink));
461 }
462
463 /// @brief Stream a batch of persona results with a batch seed drawn like
464 /// get_persona() draws its own.
465 /// @tparam Sink Callable taking `(std::uint64_t, persona&)`.
466 /// @param request What every item is generated from.
467 /// @param options Count, first index, threads and uniqueness.
468 /// @param sink Receives the items.
469 template <typename Sink>
470 void for_each_persona(const persona_request& request, const core::batch_options& options,
471 Sink&& sink)
472 {
473 for_each_persona(request, options, _engine(), std::forward<Sink>(sink));
474 }
475
476
477 private:
478 static constexpr std::array<std::string_view, 79> supported_ = {
479 "AE", "AL", "AR", "AT", "AZ", "BD", "BE", "BG", "BH", "BO", "BR", "CA", "CH", "CL", "CM", "CN",
480 "CO", "CR", "CY", "CZ", "DE", "DK", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FR", "GB", "GE",
481 "GH", "GR", "HN", "HR", "HU", "ID", "IE", "IL", "IN", "IQ", "IR", "IS", "IT", "JM", "JO", "JP",
482 "KZ", "LB", "LT", "LU", "LY", "MA", "MD", "MT", "MX", "MY", "NG", "NL", "NO", "PE", "PH",
483 "PL", "PT", "RS", "RU", "SA", "SD", "SE", "SG", "SI", "SV", "SY", "TN", "TR", "US", "YE", "ZA"};
484
485 static constexpr int max_country_attempts_ = 10000;
486
487 // The name step's value: the full name the persona shows, and its parts,
488 // which the family of a persona is named to match.
489 struct named_
490 {
491 std::string full;
492 person_name parts;
493 };
494
495 sources _from{};
496 core::rng _engine{core::rng::from_entropy()};
497 eg _pipeline; // Built once, by build_().
498
499 // One library's step in the pipeline. Unlike entity-generator's field(),
500 // it reads the library's data hash at each call, so the pipeline is built
501 // once and data loaded into a library later still reaches the signature;
502 // and it does not format the value, which only the persona shows.
503 template <typename Lib, typename Fn>
504 class step_ final : public component
505 {
506 public:
507 step_(std::wstring key, std::vector<std::wstring> depends, const Lib* lib, Fn fn)
508 : _key{std::move(key)}, _depends{std::move(depends)}, _lib{lib}, _fn{std::move(fn)}
509 {
510 }
511
512 [[nodiscard]] std::wstring key() const override { return _key; }
513 [[nodiscard]] std::vector<std::wstring> depends_on() const override { return _depends; }
514 [[nodiscard]] std::uint64_t data_hash() const override { return _lib->data_hash(); }
515 [[nodiscard]] std::any generate(const generation_context& ctx) const override { return std::any{_fn(ctx)}; }
516 [[nodiscard]] std::wstring to_string(const std::any& /*value*/) const override { return {}; }
517 [[nodiscard]] json_value to_json(const std::any& /*value*/) const override { return {}; }
518
519 private:
520 std::wstring _key;
521 std::vector<std::wstring> _depends;
522 const Lib* _lib;
523 Fn _fn;
524 };
525
526 template <typename Lib, typename Fn>
527 void add_(std::wstring key, std::vector<std::wstring> depends, const Lib* lib, Fn fn)
528 {
529 _pipeline.add(std::make_unique<step_<Lib, Fn>>(std::move(key), std::move(depends), lib, std::move(fn)));
530 }
531
532 [[nodiscard]] static bool is_supported_(std::string_view cca2)
533 {
534 return std::ranges::binary_search(supported_, cca2);
535 }
536
537 [[nodiscard]] static std::optional<sex> sex_input_(const generation_context& ctx)
538 {
539 if (!ctx.has_input("sex")) return std::nullopt;
540 return core::parse_sex(ctx.input<std::string>("sex")).value_or(sex::male);
541 }
542
543 /// The fields of the pipeline, in the order they depend on each other.
544 void build_()
545 {
546 const auto from = _from;
547
548 add_(L"country", {}, from.country, [from](const generation_context& ctx) {
549 if (ctx.has_input("country"))
550 return from.country->find_country(ctx.input<std::string>("country"));
551 for (int i = 0; i < max_country_attempts_; ++i)
552 {
553 auto c = from.country->get_country({}, ctx.random()());
554 if (is_supported_(c.cca2)) return c;
555 }
556 throw std::runtime_error("persona-generator: no supported country in the country data");
557 });
558
559 add_(L"city", {L"country"}, from.city, [from](const generation_context& ctx) {
560 return from.city->get_city(
561 {.country = ctx.get<const dasmig::country&>(L"country").cca2}, ctx.random()());
562 });
563
564 add_(L"birth", {L"country"}, from.birth, [from](const generation_context& ctx) {
565 birth_request r;
566 r.country_code = ctx.get<const dasmig::country&>(L"country").cca2;
567 r.bio_sex = sex_input_(ctx);
568 if (ctx.has_input("age_min"))
569 r.ages = age_range{.min = static_cast<std::uint8_t>(ctx.input<std::int64_t>("age_min")),
570 .max = static_cast<std::uint8_t>(ctx.input<std::int64_t>("age_max"))};
571 const auto d = ctx.input<std::int64_t>("reference_date");
572 r.reference_date = std::chrono::year_month_day{
573 std::chrono::year{static_cast<int>(d / 10000)},
574 std::chrono::month{static_cast<unsigned>(d / 100 % 100)},
575 std::chrono::day{static_cast<unsigned>(d % 100)}};
576 return from.birth->get_birth(r, ctx.random()());
577 });
578
579 add_(L"biodata", {L"country", L"birth"}, from.biodata, [from](const generation_context& ctx) {
580 const auto& cca2 = ctx.get<const dasmig::country&>(L"country").cca2;
581 const auto& b = ctx.get<const dasmig::birth&>(L"birth");
582 return from.biodata->get_biodata(
583 {.country = cca2, .bio_sex = b.bio_sex, .age = std::min<unsigned>(b.age, 120)},
584 ctx.random()());
585 });
586
587 add_(L"name", {L"country", L"birth"}, from.name, [from](const generation_context& ctx) {
588 const auto& cca2 = ctx.get<const dasmig::country&>(L"country").cca2;
589 const auto& b = ctx.get<const dasmig::birth&>(L"birth");
590 std::string lower = cca2;
591 std::ranges::transform(lower, lower.begin(), [](char c) {
592 return static_cast<char>(c >= 'A' && c <= 'Z' ? c - 'A' + 'a' : c);
593 });
594 // An unknown code is culture::any, which would draw a
595 // name from a random culture: reject it instead.
596 const auto name_culture = ng::to_culture(core::from_utf8(lower));
597 if (name_culture == culture::any)
598 throw std::invalid_argument("persona-generator: name-generator has no names for country "
599 + cca2);
600 const auto n = from.name->get_full_name(
601 {.gender = b.bio_sex == sex::female ? gender::f : gender::m, .culture = name_culture},
602 ctx.random()());
603 return named_{core::to_utf8(static_cast<std::wstring>(n)),
604 person_name{.given = core::to_utf8(n.given()),
605 .patronymic = core::to_utf8(n.patronymic()),
606 .family = core::to_utf8(n.family())}};
607 });
608
609 add_(L"traits", {}, from.trait, [from](const generation_context& ctx) {
610 return from.trait->get_personality(
611 {.count = static_cast<std::size_t>(ctx.input<std::int64_t>("traits"))},
612 ctx.random()());
613 });
614
615 add_(L"interest", {L"country", L"birth"}, from.interest, [from](const generation_context& ctx) {
616 const auto& b = ctx.get<const dasmig::birth&>(L"birth");
617 interest_request r;
618 const auto& cca2 = ctx.get<const dasmig::country&>(L"country").cca2;
619 r.country = cca2;
620 r.age = static_cast<std::uint8_t>(std::min<unsigned>(b.age, 120));
621 r.person_sex = b.bio_sex;
622 return from.interest->get_interest(r, ctx.random()());
623 });
624
625 add_(L"occupation", {L"country", L"birth"}, from.occupation, [from](const generation_context& ctx) {
626 const auto& b = ctx.get<const dasmig::birth&>(L"birth");
627 occupation_request r;
628 const auto& cca2 = ctx.get<const dasmig::country&>(L"country").cca2;
629 r.country = cca2;
630 r.age = static_cast<std::uint8_t>(std::min<unsigned>(b.age, 120));
631 r.person_sex = b.bio_sex;
632 return from.occupation->get_occupation(r, ctx.random()());
633 });
634 }
635};
636
637} // namespace dasmig
638
639#endif // DASMIG_PERSONAGEN_HPP
Return type for persona generation.
sex person_sex() const
Biological sex of the persona.
dasmig::interest interests
Religion, hobbies and media.
std::string admin1
Name of the city's state or province; empty when city-generator has no admin1 names loaded.
std::string to_json() const
This persona as a JSON document.
friend std::ostream & operator<<(std::ostream &os, const persona &v)
Stream the summary to an output stream.
void to_json(core::json_writer &w) const
Write this persona as a JSON object.
std::uint64_t seed() const
Retrieve the random seed used to generate this persona.
dasmig::biodata body
Height, weight, eyes, hair, skin and blood type, at the persona's age and sex.
std::string to_string() const
One-line summary: name, age, sex, city, country and job.
std::string name
Full name (UTF-8), from the country's culture and the sex.
std::optional< family > relatives
The persona's family; set only when persona_request::relations was. // NOLINT(readability-redundant-m...
dasmig::country country_info
The country.
dasmig::city city_info
The city of residence, in the country.
dasmig::personality traits
Personality traits and axes.
unsigned age() const
Age in completed years at the request's reference date.
const core::seed_signature & signature() const
The seed and the data this persona was generated from.
dasmig::birth birthday
Birth date, completed-year age and sex.
dasmig::occupation job
Employment status, education and job.
persona-generator: runs the persona pipeline.
psg & operator=(const psg &)=delete
Not copyable.
void for_each_persona(const persona_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of persona results with a batch seed drawn like get_persona() draws its own.
psg(psg &&) noexcept=default
Movable.
psg()
Draws from the singletons of the eight libraries.
void for_each_persona(const persona_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of persona results to a sink without holding the whole batch.
static psg & instance()
Access the global singleton instance.
std::uint64_t data_hash() const
Hash of the data of all eight libraries.
std::vector< persona > get_persona_batch(const persona_request &request, const core::batch_options &options)
Generate a batch of persona results from a seed drawn like get_persona() draws its own.
persona get_persona(const persona_request &request, std::uint64_t call_seed) const
Generate the persona for a call seed. Deterministic; does not touch the generator's engine.
std::vector< persona > get_persona_batch(const persona_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of persona results, deterministically.
bool load(dataset tier)
Load a tier into every library.
static std::span< const std::string_view > supported_countries()
The countries a random persona is drawn from.
psg(const psg &)=delete
Not copyable.
persona get_persona(const persona_request &request)
Generate a persona with a new random seed.
bool has_data() const
Check whether every library has data.
psg(sources from)
Draws from the given library instances (which must outlive this generator).
What a persona call asks for.
std::optional< sex > person_sex
Biological sex; unset draws it from the birth.
std::optional< std::string > country
ISO 3166-1 alpha-2 code (any case). Unset draws one of psg::supported_countries(),...
std::optional< family_request > relations
Also draw the people around the persona: parents, siblings, partners and children (relationship-gener...
std::optional< age_range > ages
Inclusive age range in completed years; unset draws from the country's age pyramid.
std::size_t traits
How many personality traits to draw.
std::chrono::year_month_day reference_date
The date ages are computed at. The default is birth-generator's (2023-07-01); the library never reads...
The library instances a persona is drawn from.
intg * interest
Religion, hobbies and media.
relg * relationship
Family; used only when a request asks for relations.
ocg * occupation
Employment, education and jobs.
bthg * birth
Birth dates and ages.
cntg * country
Countries.
trtg * trait
Personality.