name_bank 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/README.md +147 -13
- data/lib/name_bank/errors.rb +5 -0
- data/lib/name_bank/pool_schema.rb +7 -2
- data/lib/name_bank/pool_store.rb +47 -11
- data/lib/name_bank/version.rb +1 -1
- data/lib/name_bank.rb +33 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e238aa5013b3c3a8513f0bd233bb7365f7703c6f852270d93044ee424f6df95
|
|
4
|
+
data.tar.gz: 697fe182bc1222111617ca9dd2803d208cff375aff171b41d42bf32047cef45c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12411b5e09c5e83bcecefc555c7568843832ed4a37c42553d5fc8efef2c1f4699f828731b1ac68c7e5cd1db93fdbc2baf8be34207553f67bb901b9e2fbc05c6f
|
|
7
|
+
data.tar.gz: f959d2ec630d8d53cf461c22dfa261935e8369e866d95ce513d5e64c86b42082c0c5622ce129d4e41bce9d42a87a7182c82202baa8c842b34b478e34b61ad2ab
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,62 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] - 2026-08-16
|
|
8
|
+
|
|
9
|
+
Many names in one call. Nothing that worked before changes: sampling from the
|
|
10
|
+
existing methods is byte-identical to 0.3.0 over 2830 samples across every
|
|
11
|
+
country, script and variant, and the name data is untouched.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `full_names(country:, gender:, rng:, count:)` draws that many distinct
|
|
15
|
+
given/family pairs in one call — the shape a database seed needs. No pair
|
|
16
|
+
repeats; single names do, because only the pair is unique. The guarantee
|
|
17
|
+
holds within one call, so no state is kept between calls and sampling stays
|
|
18
|
+
a function of the caller's RNG. `count:` is required, and asking for more
|
|
19
|
+
pairs than the two pools can form raises the new `NameBank::PoolExhausted`
|
|
20
|
+
rather than silently returning fewer.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- README: the first usage example showed a name pair that the seed above it
|
|
24
|
+
never produces. Every `# =>` in the README is now the value the code actually
|
|
25
|
+
returns, checked against the shipped pools. The sampling examples under
|
|
26
|
+
*Scripts* and *Variants* name their seed too, so all of them can be
|
|
27
|
+
reproduced rather than taken on trust. No code or data changed.
|
|
28
|
+
|
|
29
|
+
## [0.3.0] - 2026-08-11
|
|
30
|
+
|
|
31
|
+
Lookup, documentation and one platform bug. Sampling behaviour and the name
|
|
32
|
+
data are unchanged — verified byte-identical over 1415 samples against 0.2.0.
|
|
33
|
+
Nothing that worked before stops working; on Linux, one thing that failed now
|
|
34
|
+
works.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- `scripts` takes an optional `variant:`. A variant can offer different script
|
|
38
|
+
forms from the country it is layered on; previously `scripts(country:)` only
|
|
39
|
+
ever reported the country's own.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- Country and variant names now resolve against the directory listing instead
|
|
43
|
+
of being handed to `File.exist?`, so lookup no longer inherits the
|
|
44
|
+
filesystem's case-sensitivity. `country: "de"` found `DE.yml` on macOS and
|
|
45
|
+
raised `UnknownCountry` on Linux; it now finds it on both. An exact match
|
|
46
|
+
still wins, and names that differ only in case are reported as ambiguous
|
|
47
|
+
rather than guessed at.
|
|
48
|
+
- README: a pool with no names was documented as raising
|
|
49
|
+
`NameBank::UnknownScript`. It has raised `NameBank::EmptyPool` since 0.2.0.
|
|
50
|
+
|
|
51
|
+
### Documented
|
|
52
|
+
- The pool file format: the directory layout `NameBank.new(data_dir:)` expects,
|
|
53
|
+
which keys a file must carry, how `_native` pools are named, and which
|
|
54
|
+
failures a malformed file produces. Variant files may carry `_native` pools —
|
|
55
|
+
supported by the reader since 0.1.4, now stated and covered by a spec.
|
|
56
|
+
- `NameBank::PoolSchema` is now documented in the README and is supported
|
|
57
|
+
public API: `KEYS`, `GIVEN_MALE`, `GIVEN_FEMALE`, `SURNAMES`, `native_key`
|
|
58
|
+
and `gender_key`. It shipped before but was described nowhere. Callers
|
|
59
|
+
building pool files for `NameBank.new(data_dir:)` should address the YAML
|
|
60
|
+
keys through it. The key names are covered by semantic versioning from here
|
|
61
|
+
on: renaming one is a breaking change.
|
|
62
|
+
|
|
7
63
|
## [0.2.0] - 2026-08-08
|
|
8
64
|
|
|
9
65
|
An interface release. Sampling behaviour and the name data are unchanged — the
|
data/README.md
CHANGED
|
@@ -27,23 +27,23 @@ gem install name_bank
|
|
|
27
27
|
```ruby
|
|
28
28
|
require "name_bank"
|
|
29
29
|
|
|
30
|
-
rng = Random.new(
|
|
30
|
+
rng = Random.new(10246)
|
|
31
31
|
|
|
32
32
|
# A full name (given + family) for a country and gender:
|
|
33
33
|
NameBank.full_name(country: "DE", gender: :female, rng: rng)
|
|
34
|
-
# => { firstname: "
|
|
34
|
+
# => { firstname: "Alexandra", lastname: "Werner" }
|
|
35
35
|
|
|
36
36
|
# Just a given name or a surname:
|
|
37
|
-
NameBank.first_name(country: "IT", gender: :male, rng: rng) # => "
|
|
38
|
-
NameBank.last_name(country: "JP", rng: rng) # => "
|
|
37
|
+
NameBank.first_name(country: "IT", gender: :male, rng: rng) # => "Settimio"
|
|
38
|
+
NameBank.last_name(country: "JP", rng: rng) # => "Katsuta"
|
|
39
39
|
|
|
40
40
|
# Latin (default), or the country's native script where it has one:
|
|
41
|
-
NameBank.first_name(country: "JP", gender: :female, rng: rng) # => "
|
|
42
|
-
NameBank.first_name(country: "JP", gender: :female, rng: rng, script: :native) # => "
|
|
41
|
+
NameBank.first_name(country: "JP", gender: :female, rng: rng) # => "Misako"
|
|
42
|
+
NameBank.first_name(country: "JP", gender: :female, rng: rng, script: :native) # => "さえ"
|
|
43
43
|
|
|
44
44
|
# Default pool, or an alternate cultural pool where one exists:
|
|
45
|
-
NameBank.first_name(country: "US", gender: :male, rng: rng) # => "
|
|
46
|
-
NameBank.first_name(country: "US", gender: :male, rng: rng, variant: "african_american") # => "
|
|
45
|
+
NameBank.first_name(country: "US", gender: :male, rng: rng) # => "Jonah"
|
|
46
|
+
NameBank.first_name(country: "US", gender: :male, rng: rng, variant: "african_american") # => "Tyrone"
|
|
47
47
|
|
|
48
48
|
# List available countries (ISO alpha-2 codes):
|
|
49
49
|
NameBank.countries.size # => 106
|
|
@@ -54,10 +54,38 @@ Sampling is uniform over each pool and fully deterministic for a given
|
|
|
54
54
|
`rng` — the same seed always yields the same name. `gender:` is `:male`
|
|
55
55
|
or `:female`.
|
|
56
56
|
|
|
57
|
+
## Many at once
|
|
58
|
+
|
|
59
|
+
Seeding a table wants many names in one call, and wants them to differ.
|
|
60
|
+
`full_names` draws distinct given/family pairs:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
rng = Random.new(24134)
|
|
64
|
+
|
|
65
|
+
NameBank.full_names(country: "DE", gender: :female, rng: rng, count: 3)
|
|
66
|
+
# => [{ firstname: "Gerlinde", lastname: "Nowak" },
|
|
67
|
+
# { firstname: "Juliane", lastname: "Stark" },
|
|
68
|
+
# { firstname: "Kornelia", lastname: "Lenz" }]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
No pair is drawn twice. Single names do recur once the draw is large enough —
|
|
72
|
+
only the pair is unique, which is what a real population looks like. The
|
|
73
|
+
guarantee holds within one call; two calls know nothing of each other.
|
|
74
|
+
|
|
75
|
+
`count:` is required, because there is no pool of pairs to hand back without
|
|
76
|
+
one. A country can form as many pairs as given names × surnames: 1500 × 1500
|
|
77
|
+
for most, 30 × 30 for the hand-curated `CN` and `UA` pools. Asking for more
|
|
78
|
+
raises rather than quietly returning fewer:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
NameBank.full_names(country: "UA", gender: :male, rng: rng, count: 901)
|
|
82
|
+
# => NameBank::PoolExhausted: UA: 901 pairs requested, 900 available
|
|
83
|
+
```
|
|
84
|
+
|
|
57
85
|
## Whole pools
|
|
58
86
|
|
|
59
|
-
Where sampling
|
|
60
|
-
|
|
87
|
+
Where sampling is not enough — applying your own weighting, drawing single
|
|
88
|
+
names without repeats, or checking what a country actually ships —
|
|
61
89
|
take the pool itself. `first_names` and `last_names` accept the same
|
|
62
90
|
`country:`, `variant:` and `script:` options as the samplers, and return the
|
|
63
91
|
frequency-ordered pool as a frozen array:
|
|
@@ -68,6 +96,11 @@ NameBank.first_names(country: "DE", gender: :female).first(3) # => ["Nicole", "
|
|
|
68
96
|
NameBank.last_names(country: "JP", script: :native).first(3) # => ["佐藤", "鈴木", "田中"]
|
|
69
97
|
```
|
|
70
98
|
|
|
99
|
+
For single names without repeats, the pool plus Ruby is the whole answer:
|
|
100
|
+
`pool.sample(200, random: rng)` returns 200 different names, deterministically.
|
|
101
|
+
Note that `Array#sample` returns fewer than asked for rather than complaining
|
|
102
|
+
when the pool is too small — unlike `full_names`, which raises.
|
|
103
|
+
|
|
71
104
|
Every method shown so far is also available on an instance, which lets you
|
|
72
105
|
point name_bank at your own directory of pool files:
|
|
73
106
|
`NameBank.new(data_dir: "…").first_name(country: "DE", gender: :male, rng: rng)`.
|
|
@@ -153,6 +186,8 @@ Names come in Latin (default) and, for countries with a non-Latin writing
|
|
|
153
186
|
system, their native script. Pass `script:`:
|
|
154
187
|
|
|
155
188
|
```ruby
|
|
189
|
+
rng = Random.new(325089)
|
|
190
|
+
|
|
156
191
|
NameBank.first_name(country: "RU", gender: :male, rng: rng) # => "Dmitry"
|
|
157
192
|
NameBank.first_name(country: "RU", gender: :male, rng: rng, script: :native) # => "Алексей"
|
|
158
193
|
|
|
@@ -163,7 +198,15 @@ NameBank.scripts(country: "DE") # => [:latin]
|
|
|
163
198
|
Germany's script is Latin, so `DE` reports `:latin` only.
|
|
164
199
|
`:latin` and `:native` sample from independent pools. For Latin-script countries
|
|
165
200
|
`:native` returns the same (Latin) pool. Requesting a script with no names
|
|
166
|
-
raises `NameBank::
|
|
201
|
+
raises `NameBank::EmptyPool`; a `script:` that is neither `:latin` nor
|
|
202
|
+
`:native` raises `NameBank::UnknownScript`.
|
|
203
|
+
|
|
204
|
+
`scripts` takes an optional `variant:`, because a variant can offer different
|
|
205
|
+
forms from the country it is layered on:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
NameBank.scripts(country: "US", variant: "african_american") # => [:latin]
|
|
209
|
+
```
|
|
167
210
|
|
|
168
211
|
## Variants
|
|
169
212
|
|
|
@@ -171,6 +214,8 @@ Some countries offer an alternate cultural name pool layered on the default.
|
|
|
171
214
|
Pass `variant:`:
|
|
172
215
|
|
|
173
216
|
```ruby
|
|
217
|
+
rng = Random.new(41)
|
|
218
|
+
|
|
174
219
|
NameBank.first_name(country: "US", gender: :male, rng: rng, variant: "african_american")
|
|
175
220
|
# => "DeShawn"
|
|
176
221
|
|
|
@@ -191,10 +236,11 @@ still catches the two that are genuinely caller mistakes:
|
|
|
191
236
|
| `NameBank::UnknownCountry` | `StandardError` | no pool file for that country code |
|
|
192
237
|
| `NameBank::UnknownVariant` | `StandardError` | no pool file for that variant |
|
|
193
238
|
| `NameBank::EmptyPool` | `StandardError` | the pool exists but holds no names |
|
|
239
|
+
| `NameBank::PoolExhausted` | `StandardError` | `full_names` was asked for more distinct pairs than the pools can form |
|
|
194
240
|
| `NameBank::MalformedPool` | `StandardError` | a pool file is missing a key, or holds something other than a list |
|
|
195
241
|
|
|
196
|
-
|
|
197
|
-
`NameBank.new(data_dir:)` at pool files of your own.
|
|
242
|
+
`EmptyPool` and `MalformedPool` cannot occur with the shipped data; they matter
|
|
243
|
+
when you point `NameBank.new(data_dir:)` at pool files of your own.
|
|
198
244
|
|
|
199
245
|
```ruby
|
|
200
246
|
begin
|
|
@@ -204,6 +250,94 @@ rescue NameBank::UnknownCountry => e
|
|
|
204
250
|
end
|
|
205
251
|
```
|
|
206
252
|
|
|
253
|
+
## Pool file format
|
|
254
|
+
|
|
255
|
+
`NameBank.new(data_dir:)` reads pools from a directory of your own. It holds
|
|
256
|
+
two subdirectories:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
my_names/
|
|
260
|
+
countries/
|
|
261
|
+
DE.yml
|
|
262
|
+
RU.yml
|
|
263
|
+
variants/
|
|
264
|
+
US/
|
|
265
|
+
african_american.yml
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- `countries/<code>.yml` — the basename is what you pass as `country:`, and
|
|
269
|
+
what `countries` lists.
|
|
270
|
+
- `variants/<code>/<name>.yml` — the basename is what you pass as `variant:`.
|
|
271
|
+
No directory for a country means `variants(country:)` returns `[]`.
|
|
272
|
+
- Files that do not end in `.yml` are ignored, and codes are not validated
|
|
273
|
+
against ISO 3166 — any basename works.
|
|
274
|
+
|
|
275
|
+
Names resolve the same way on every filesystem: an exact match wins, otherwise
|
|
276
|
+
a unique match ignoring case, so `country: "de"` finds `DE.yml` on Linux as
|
|
277
|
+
well as macOS. Two files whose basenames differ only in case are ambiguous, and
|
|
278
|
+
raise `NameBank::UnknownCountry` rather than being guessed at.
|
|
279
|
+
|
|
280
|
+
A pool file must carry the three keys in `NameBank::PoolSchema::KEYS`, each a
|
|
281
|
+
list. Any of them may have a native-script counterpart under the same key plus
|
|
282
|
+
`_native`:
|
|
283
|
+
|
|
284
|
+
```yaml
|
|
285
|
+
firstnames_male: [Dmitry, Ivan]
|
|
286
|
+
firstnames_female: [Anna, Olga]
|
|
287
|
+
lastnames: [Ivanov, Petrov]
|
|
288
|
+
|
|
289
|
+
firstnames_male_native: [Дмитрий, Иван]
|
|
290
|
+
lastnames_native: [Иванов, Петров]
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Variant files are validated the same way and need all three keys too. They may
|
|
294
|
+
carry `_native` pools, which `script: :native` will use — the shipped variants
|
|
295
|
+
do not, because the build pipeline strips them.
|
|
296
|
+
|
|
297
|
+
Missing a required key, or holding something other than a list under it, raises
|
|
298
|
+
`NameBank::MalformedPool` when the file is read. A key that is present but
|
|
299
|
+
empty is accepted at read time and raises `NameBank::EmptyPool` when sampled.
|
|
300
|
+
Any other key is ignored; the shipped files carry `source:`, which nothing
|
|
301
|
+
reads at runtime.
|
|
302
|
+
|
|
303
|
+
Sampling is uniform over the whole list, so order does not weight anything. It
|
|
304
|
+
is preserved, and visible through `first_names` and `last_names`.
|
|
305
|
+
|
|
306
|
+
## Pool schema
|
|
307
|
+
|
|
308
|
+
`NameBank::PoolSchema` names the YAML keys a pool file uses. It is supported
|
|
309
|
+
public API from 0.2.0 on: the constants and their values will not change
|
|
310
|
+
without a version bump. Use it when you build pool files for
|
|
311
|
+
`NameBank.new(data_dir:)`, so you are not typing key names by hand.
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
NameBank::PoolSchema::KEYS
|
|
315
|
+
# => ["firstnames_male", "firstnames_female", "lastnames"]
|
|
316
|
+
|
|
317
|
+
NameBank::PoolSchema::GIVEN_MALE # => "firstnames_male"
|
|
318
|
+
NameBank::PoolSchema::GIVEN_FEMALE # => "firstnames_female"
|
|
319
|
+
NameBank::PoolSchema::SURNAMES # => "lastnames"
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`KEYS` is frozen and lists the three pools every file must carry. A file may
|
|
323
|
+
also carry a native-script pool for any of them, under the same key with a
|
|
324
|
+
`_native` suffix:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
NameBank::PoolSchema.native_key("lastnames") # => "lastnames_native"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`gender_key` maps a `gender:` argument to its key, and raises
|
|
331
|
+
`NameBank::UnknownGender` for anything else:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
NameBank::PoolSchema.gender_key(:female) # => "firstnames_female"
|
|
335
|
+
NameBank::PoolSchema.gender_key(:x) # raises NameBank::UnknownGender
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Reading a file that is missing one of `KEYS`, or that holds something other
|
|
339
|
+
than a list under it, raises `NameBank::MalformedPool`.
|
|
340
|
+
|
|
207
341
|
## Supported countries
|
|
208
342
|
|
|
209
343
|
106 countries: Afghanistan, Albania, Algeria, Angola, Argentina, Austria,
|
data/lib/name_bank/errors.rb
CHANGED
|
@@ -28,6 +28,11 @@ class NameBank
|
|
|
28
28
|
include Error
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# More distinct pairs were asked for than the pools can form.
|
|
32
|
+
class PoolExhausted < StandardError
|
|
33
|
+
include Error
|
|
34
|
+
end
|
|
35
|
+
|
|
31
36
|
# script: was neither :latin nor :native.
|
|
32
37
|
class UnknownScript < ArgumentError
|
|
33
38
|
include Error
|
|
@@ -5,8 +5,13 @@ require_relative "errors"
|
|
|
5
5
|
# Declared as a class, not a module: NameBank itself is one (see lib/name_bank.rb).
|
|
6
6
|
class NameBank
|
|
7
7
|
# The pool-key schema: the base YAML keys, how native-script keys are named,
|
|
8
|
-
# and how a gender maps to its key.
|
|
9
|
-
# build-time SplitScripts tool
|
|
8
|
+
# and how a gender maps to its key. One home for the key names, shared by the
|
|
9
|
+
# runtime NameBank and the build-time SplitScripts tool.
|
|
10
|
+
#
|
|
11
|
+
# Supported public API from 0.2.0 on, documented in the README — callers
|
|
12
|
+
# building pool files for NameBank.new(data_dir:) address them through here.
|
|
13
|
+
# The constants and their values are therefore covered by semantic
|
|
14
|
+
# versioning: changing a key name is a breaking change.
|
|
10
15
|
module PoolSchema
|
|
11
16
|
GIVEN_MALE = "firstnames_male"
|
|
12
17
|
GIVEN_FEMALE = "firstnames_female"
|
data/lib/name_bank/pool_store.rb
CHANGED
|
@@ -11,6 +11,10 @@ class NameBank
|
|
|
11
11
|
# shape against PoolSchema, memoizing them, and listing what a data dir holds.
|
|
12
12
|
# NameBank itself never opens a file. Files are read once per instance, and
|
|
13
13
|
# validated once when read rather than on every pool access.
|
|
14
|
+
#
|
|
15
|
+
# Country and variant names are resolved against the directory listing rather
|
|
16
|
+
# than handed to File.exist?, so the result does not depend on whether the
|
|
17
|
+
# filesystem happens to be case-sensitive.
|
|
14
18
|
class PoolStore
|
|
15
19
|
def initialize(data_dir)
|
|
16
20
|
@data_dir = data_dir
|
|
@@ -28,27 +32,49 @@ class NameBank
|
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def variants(country)
|
|
31
|
-
|
|
35
|
+
code = resolve(country, variant_countries)
|
|
36
|
+
return [] if code.nil?
|
|
37
|
+
|
|
38
|
+
@variant_names[code] ||= yml_basenames(File.join(@data_dir, "variants", code))
|
|
32
39
|
end
|
|
33
40
|
|
|
34
41
|
private
|
|
35
42
|
|
|
43
|
+
# An exact match wins; otherwise a unique case-insensitive one; otherwise
|
|
44
|
+
# nil, because guessing between DE and de would be worse than refusing.
|
|
45
|
+
def resolve(name, candidates)
|
|
46
|
+
return name if candidates.include?(name)
|
|
47
|
+
|
|
48
|
+
hits = matches(name, candidates)
|
|
49
|
+
hits.size == 1 ? hits.first : nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def matches(name, candidates)
|
|
53
|
+
candidates.select { |candidate| candidate.casecmp?(name) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# What to put in the error: the name alone when nothing matched, or the
|
|
57
|
+
# rival spellings when several did.
|
|
58
|
+
def unresolved(name, candidates)
|
|
59
|
+
hits = matches(name, candidates)
|
|
60
|
+
hits.empty? ? name : "#{name} matches #{hits.join(", ")}"
|
|
61
|
+
end
|
|
62
|
+
|
|
36
63
|
def country_pools(country)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
raise UnknownCountry, country unless File.exist?(path)
|
|
64
|
+
code = resolve(country, countries)
|
|
65
|
+
raise UnknownCountry, unresolved(country, countries) if code.nil?
|
|
40
66
|
|
|
41
|
-
|
|
42
|
-
end
|
|
67
|
+
@country_pools[code] ||= read(File.join(@data_dir, "countries", "#{code}.yml"))
|
|
43
68
|
end
|
|
44
69
|
|
|
45
70
|
def variant_pools(country, variant)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
raise UnknownVariant, "#{country}/#{variant}" unless File.exist?(path)
|
|
71
|
+
code = resolve(country, variant_countries)
|
|
72
|
+
raise UnknownVariant, "#{country}/#{variant}" if code.nil?
|
|
49
73
|
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
name = resolve(variant, variants(code))
|
|
75
|
+
raise UnknownVariant, "#{country}/#{unresolved(variant, variants(code))}" if name.nil?
|
|
76
|
+
|
|
77
|
+
@variant_pools[[code, name]] ||= read(File.join(@data_dir, "variants", code, "#{name}.yml"))
|
|
52
78
|
end
|
|
53
79
|
|
|
54
80
|
def read(path)
|
|
@@ -59,6 +85,16 @@ class NameBank
|
|
|
59
85
|
data
|
|
60
86
|
end
|
|
61
87
|
|
|
88
|
+
def variant_countries
|
|
89
|
+
@variant_countries ||= subdirectories(File.join(@data_dir, "variants"))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def subdirectories(dir)
|
|
93
|
+
return [] unless Dir.exist?(dir)
|
|
94
|
+
|
|
95
|
+
Dir.children(dir).select { |child| File.directory?(File.join(dir, child)) }.sort
|
|
96
|
+
end
|
|
97
|
+
|
|
62
98
|
def yml_basenames(dir)
|
|
63
99
|
return [] unless Dir.exist?(dir)
|
|
64
100
|
|
data/lib/name_bank/version.rb
CHANGED
data/lib/name_bank.rb
CHANGED
|
@@ -27,6 +27,7 @@ class NameBank
|
|
|
27
27
|
def first_name(...) = default.first_name(...)
|
|
28
28
|
def last_name(...) = default.last_name(...)
|
|
29
29
|
def full_name(...) = default.full_name(...)
|
|
30
|
+
def full_names(...) = default.full_names(...)
|
|
30
31
|
def first_names(...) = default.first_names(...)
|
|
31
32
|
def last_names(...) = default.last_names(...)
|
|
32
33
|
def countries(...) = default.countries(...)
|
|
@@ -53,6 +54,21 @@ class NameBank
|
|
|
53
54
|
}
|
|
54
55
|
end
|
|
55
56
|
|
|
57
|
+
# Distinct given/family pairs, for seeding many rows at once. No pair is drawn
|
|
58
|
+
# twice; single names do recur, which is what a population looks like. The
|
|
59
|
+
# guarantee holds within one call — two calls know nothing of each other.
|
|
60
|
+
# Asking for more pairs than the two pools can form raises PoolExhausted.
|
|
61
|
+
def full_names(country:, gender:, rng:, count:, variant: nil, script: :latin)
|
|
62
|
+
firsts = first_names(country: country, gender: gender, variant: variant, script: script)
|
|
63
|
+
lasts = last_names(country: country, variant: variant, script: script)
|
|
64
|
+
available = firsts.size * lasts.size
|
|
65
|
+
raise PoolExhausted, "#{country}: #{count} pairs requested, #{available} available" if count > available
|
|
66
|
+
|
|
67
|
+
distinct_indices(available, count, rng).map do |index|
|
|
68
|
+
{ firstname: firsts[index / lasts.size], lastname: lasts[index % lasts.size] }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
56
72
|
# The whole frequency-ordered pool, frozen. The name strings stay mutable.
|
|
57
73
|
def first_names(country:, gender:, variant: nil, script: :latin)
|
|
58
74
|
pool(country, variant, PoolSchema.gender_key(gender), script)
|
|
@@ -71,13 +87,28 @@ class NameBank
|
|
|
71
87
|
end
|
|
72
88
|
|
|
73
89
|
# The script forms this country offers, not writing systems — see CONTEXT.md.
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
# With a variant, the forms that variant offers, which may differ.
|
|
91
|
+
def scripts(country:, variant: nil)
|
|
92
|
+
data = @store.pools(country, variant)
|
|
76
93
|
PoolSchema::KEYS.any? { |k| data[PoolSchema.native_key(k)]&.any? } ? %i[latin native] : %i[latin]
|
|
77
94
|
end
|
|
78
95
|
|
|
79
96
|
private
|
|
80
97
|
|
|
98
|
+
# Partial Fisher-Yates over the space of pair indices, with the swaps kept in
|
|
99
|
+
# a Hash so that the untouched majority of a two-million-pair space is never
|
|
100
|
+
# built. Drawing every pair a country has takes count steps; drawing at random
|
|
101
|
+
# until enough distinct pairs turn up would stall on the last few.
|
|
102
|
+
def distinct_indices(available, count, rng)
|
|
103
|
+
swapped = {}
|
|
104
|
+
Array.new(count) do |drawn|
|
|
105
|
+
index = rng.rand(drawn...available)
|
|
106
|
+
picked = swapped.fetch(index, index)
|
|
107
|
+
swapped[index] = swapped.fetch(drawn, drawn)
|
|
108
|
+
picked
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
81
112
|
def pool(country, variant, key, script)
|
|
82
113
|
names = names_for_script(@store.pools(country, variant), key, script)
|
|
83
114
|
raise EmptyPool, "#{country}/#{script}" if names.empty?
|