name_bank 0.1.6 → 0.3.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 +73 -0
- data/README.md +155 -6
- data/lib/name_bank/errors.rb +40 -0
- data/lib/name_bank/pool_schema.rb +19 -7
- data/lib/name_bank/pool_store.rb +107 -0
- data/lib/name_bank/version.rb +3 -2
- data/lib/name_bank.rb +65 -16
- metadata +3 -2
- data/lib/name_bank/repository.rb +0 -84
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 184aced32d90cfa194129bdb48895055b2738ed7bef92c1c1a42532421e6e4c6
|
|
4
|
+
data.tar.gz: 6305e8defeb53e8a6bbb2d7fd47e73a41c6e870cd83659dfad56f24a96f3ebd8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4eae078ebdd3bab2e6c3d455d3c783a6d03b5d119c386017c81d76e5ead0312e066cc24911621506b8c3b7637a88f679a1f5b8b1dfd3202427c0f274a3d3ef87
|
|
7
|
+
data.tar.gz: 3c7ea3d49e618280f40734193e487c61fc5c6a9c762a16925843533099e7e727feb693dd9c22a1453a29d63316c962506199a493f7469a898ef559f5f5f3020a
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,79 @@ 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.3.0] - 2026-08-11
|
|
8
|
+
|
|
9
|
+
Lookup, documentation and one platform bug. Sampling behaviour and the name
|
|
10
|
+
data are unchanged — verified byte-identical over 1415 samples against 0.2.0.
|
|
11
|
+
Nothing that worked before stops working; on Linux, one thing that failed now
|
|
12
|
+
works.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `scripts` takes an optional `variant:`. A variant can offer different script
|
|
16
|
+
forms from the country it is layered on; previously `scripts(country:)` only
|
|
17
|
+
ever reported the country's own.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Country and variant names now resolve against the directory listing instead
|
|
21
|
+
of being handed to `File.exist?`, so lookup no longer inherits the
|
|
22
|
+
filesystem's case-sensitivity. `country: "de"` found `DE.yml` on macOS and
|
|
23
|
+
raised `UnknownCountry` on Linux; it now finds it on both. An exact match
|
|
24
|
+
still wins, and names that differ only in case are reported as ambiguous
|
|
25
|
+
rather than guessed at.
|
|
26
|
+
- README: a pool with no names was documented as raising
|
|
27
|
+
`NameBank::UnknownScript`. It has raised `NameBank::EmptyPool` since 0.2.0.
|
|
28
|
+
|
|
29
|
+
### Documented
|
|
30
|
+
- The pool file format: the directory layout `NameBank.new(data_dir:)` expects,
|
|
31
|
+
which keys a file must carry, how `_native` pools are named, and which
|
|
32
|
+
failures a malformed file produces. Variant files may carry `_native` pools —
|
|
33
|
+
supported by the reader since 0.1.4, now stated and covered by a spec.
|
|
34
|
+
- `NameBank::PoolSchema` is now documented in the README and is supported
|
|
35
|
+
public API: `KEYS`, `GIVEN_MALE`, `GIVEN_FEMALE`, `SURNAMES`, `native_key`
|
|
36
|
+
and `gender_key`. It shipped before but was described nowhere. Callers
|
|
37
|
+
building pool files for `NameBank.new(data_dir:)` should address the YAML
|
|
38
|
+
keys through it. The key names are covered by semantic versioning from here
|
|
39
|
+
on: renaming one is a breaking change.
|
|
40
|
+
|
|
41
|
+
## [0.2.0] - 2026-08-08
|
|
42
|
+
|
|
43
|
+
An interface release. Sampling behaviour and the name data are unchanged — the
|
|
44
|
+
same `country:`, `gender:`, `rng:`, `variant:` and `script:` calls return the
|
|
45
|
+
same names as 0.1.6. What changed is what else the interface offers, and how
|
|
46
|
+
failures are named. Three breaking changes are listed below.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
- `NameBank.first_names` and `NameBank.last_names` return a country's whole
|
|
50
|
+
frequency-ordered pool as a frozen array. They take the same `country:`,
|
|
51
|
+
`gender:`, `variant:` and `script:` options as the samplers.
|
|
52
|
+
- `NameBank` is now a class and can be instantiated: `NameBank.new(data_dir:)`
|
|
53
|
+
reads pools from a directory of your own. The class-level methods delegate to
|
|
54
|
+
a default instance over the shipped data.
|
|
55
|
+
|
|
56
|
+
### Removed
|
|
57
|
+
- **Breaking:** `NameBank.repository` and the `NameBank::Repository` class. The
|
|
58
|
+
accessor was never documented; everything it offered is on `NameBank` itself,
|
|
59
|
+
with `firstnames`/`lastnames` renamed to `first_names`/`last_names`.
|
|
60
|
+
|
|
61
|
+
- `NameBank::MalformedPool`, raised when a pool file is missing a schema key or
|
|
62
|
+
holds something other than a list. Files are validated once when read.
|
|
63
|
+
- `NameBank::UnknownGender` and `NameBank::EmptyPool`, so every failure the gem
|
|
64
|
+
can produce has a name. The README now lists all six.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
- `variants(country:)` is memoized like `countries`, so repeated calls no longer
|
|
68
|
+
hit the filesystem.
|
|
69
|
+
- **Breaking:** `NameBank::Error` is now a module mixed into every error rather
|
|
70
|
+
than their shared superclass. `rescue NameBank::Error` works as before and now
|
|
71
|
+
covers every failure, including the two argument errors that previously
|
|
72
|
+
escaped it; each error also keeps its natural superclass, so
|
|
73
|
+
`rescue ArgumentError` still catches a bad `gender:` or `script:`. Raising or
|
|
74
|
+
instantiating `NameBank::Error` itself is no longer possible.
|
|
75
|
+
- **Breaking:** `NameBank::UnknownScript` now means what its name says — a
|
|
76
|
+
`script:` that is neither `:latin` nor `:native`, previously a bare
|
|
77
|
+
`ArgumentError`. The empty-pool case it used to signal is now
|
|
78
|
+
`NameBank::EmptyPool`.
|
|
79
|
+
|
|
7
80
|
## [0.1.6] - 2026-07-24
|
|
8
81
|
|
|
9
82
|
Tooling and internals only — no API, data or behaviour change.
|
data/README.md
CHANGED
|
@@ -34,18 +34,44 @@ NameBank.full_name(country: "DE", gender: :female, rng: rng)
|
|
|
34
34
|
# => { firstname: "Sabine", lastname: "Müller" }
|
|
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) # => "Amor"
|
|
38
|
+
NameBank.last_name(country: "JP", rng: rng) # => "Tsuru"
|
|
39
|
+
|
|
40
|
+
# Latin (default), or the country's native script where it has one:
|
|
41
|
+
NameBank.first_name(country: "JP", gender: :female, rng: rng) # => "Sasahara"
|
|
42
|
+
NameBank.first_name(country: "JP", gender: :female, rng: rng, script: :native) # => "ちゃこ"
|
|
43
|
+
|
|
44
|
+
# Default pool, or an alternate cultural pool where one exists:
|
|
45
|
+
NameBank.first_name(country: "US", gender: :male, rng: rng) # => "Abe"
|
|
46
|
+
NameBank.first_name(country: "US", gender: :male, rng: rng, variant: "african_american") # => "Roosevelt"
|
|
39
47
|
|
|
40
48
|
# List available countries (ISO alpha-2 codes):
|
|
41
|
-
NameBank.countries
|
|
42
|
-
# => ["AE", "AF", "AL"
|
|
49
|
+
NameBank.countries.size # => 106
|
|
50
|
+
NameBank.countries.first(3) # => ["AE", "AF", "AL"]
|
|
43
51
|
```
|
|
44
52
|
|
|
45
53
|
Sampling is uniform over each pool and fully deterministic for a given
|
|
46
54
|
`rng` — the same seed always yields the same name. `gender:` is `:male`
|
|
47
55
|
or `:female`.
|
|
48
56
|
|
|
57
|
+
## Whole pools
|
|
58
|
+
|
|
59
|
+
Where sampling one name is not enough — drawing many names without repeats,
|
|
60
|
+
applying your own weighting, or checking what a country actually ships —
|
|
61
|
+
take the pool itself. `first_names` and `last_names` accept the same
|
|
62
|
+
`country:`, `variant:` and `script:` options as the samplers, and return the
|
|
63
|
+
frequency-ordered pool as a frozen array:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
NameBank.first_names(country: "DE", gender: :female).size # => 1500
|
|
67
|
+
NameBank.first_names(country: "DE", gender: :female).first(3) # => ["Nicole", "Sandra", "Sabine"]
|
|
68
|
+
NameBank.last_names(country: "JP", script: :native).first(3) # => ["佐藤", "鈴木", "田中"]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Every method shown so far is also available on an instance, which lets you
|
|
72
|
+
point name_bank at your own directory of pool files:
|
|
73
|
+
`NameBank.new(data_dir: "…").first_name(country: "DE", gender: :male, rng: rng)`.
|
|
74
|
+
|
|
49
75
|
## Where it fits
|
|
50
76
|
|
|
51
77
|
A factory_bot factory:
|
|
@@ -131,12 +157,21 @@ NameBank.first_name(country: "RU", gender: :male, rng: rng) # =
|
|
|
131
157
|
NameBank.first_name(country: "RU", gender: :male, rng: rng, script: :native) # => "Алексей"
|
|
132
158
|
|
|
133
159
|
NameBank.scripts(country: "RU") # => [:latin, :native]
|
|
134
|
-
NameBank.scripts(country: "DE") # => [:latin]
|
|
160
|
+
NameBank.scripts(country: "DE") # => [:latin]
|
|
135
161
|
```
|
|
136
162
|
|
|
163
|
+
Germany's script is Latin, so `DE` reports `:latin` only.
|
|
137
164
|
`:latin` and `:native` sample from independent pools. For Latin-script countries
|
|
138
165
|
`:native` returns the same (Latin) pool. Requesting a script with no names
|
|
139
|
-
raises `NameBank::
|
|
166
|
+
raises `NameBank::EmptyPool`; a `script:` that is neither `:latin` nor
|
|
167
|
+
`:native` raises `NameBank::UnknownScript`.
|
|
168
|
+
|
|
169
|
+
`scripts` takes an optional `variant:`, because a variant can offer different
|
|
170
|
+
forms from the country it is layered on:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
NameBank.scripts(country: "US", variant: "african_american") # => [:latin]
|
|
174
|
+
```
|
|
140
175
|
|
|
141
176
|
## Variants
|
|
142
177
|
|
|
@@ -151,6 +186,120 @@ NameBank.variants(country: "US") # => ["african_american"]
|
|
|
151
186
|
NameBank.variants(country: "DE") # => []
|
|
152
187
|
```
|
|
153
188
|
|
|
189
|
+
## Errors
|
|
190
|
+
|
|
191
|
+
Every error name_bank raises carries `NameBank::Error`, so one `rescue` covers
|
|
192
|
+
the lot. Each also keeps its natural Ruby superclass, so `rescue ArgumentError`
|
|
193
|
+
still catches the two that are genuinely caller mistakes:
|
|
194
|
+
|
|
195
|
+
| Error | Superclass | Raised when |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| `NameBank::UnknownGender` | `ArgumentError` | `gender:` is neither `:male` nor `:female` |
|
|
198
|
+
| `NameBank::UnknownScript` | `ArgumentError` | `script:` is neither `:latin` nor `:native` |
|
|
199
|
+
| `NameBank::UnknownCountry` | `StandardError` | no pool file for that country code |
|
|
200
|
+
| `NameBank::UnknownVariant` | `StandardError` | no pool file for that variant |
|
|
201
|
+
| `NameBank::EmptyPool` | `StandardError` | the pool exists but holds no names |
|
|
202
|
+
| `NameBank::MalformedPool` | `StandardError` | a pool file is missing a key, or holds something other than a list |
|
|
203
|
+
|
|
204
|
+
The last two cannot occur with the shipped data; they matter when you point
|
|
205
|
+
`NameBank.new(data_dir:)` at pool files of your own.
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
begin
|
|
209
|
+
NameBank.first_name(country: "ZZ", gender: :male, rng: rng)
|
|
210
|
+
rescue NameBank::UnknownCountry => e
|
|
211
|
+
warn "no pool for #{e.message}"
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Pool file format
|
|
216
|
+
|
|
217
|
+
`NameBank.new(data_dir:)` reads pools from a directory of your own. It holds
|
|
218
|
+
two subdirectories:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
my_names/
|
|
222
|
+
countries/
|
|
223
|
+
DE.yml
|
|
224
|
+
RU.yml
|
|
225
|
+
variants/
|
|
226
|
+
US/
|
|
227
|
+
african_american.yml
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `countries/<code>.yml` — the basename is what you pass as `country:`, and
|
|
231
|
+
what `countries` lists.
|
|
232
|
+
- `variants/<code>/<name>.yml` — the basename is what you pass as `variant:`.
|
|
233
|
+
No directory for a country means `variants(country:)` returns `[]`.
|
|
234
|
+
- Files that do not end in `.yml` are ignored, and codes are not validated
|
|
235
|
+
against ISO 3166 — any basename works.
|
|
236
|
+
|
|
237
|
+
Names resolve the same way on every filesystem: an exact match wins, otherwise
|
|
238
|
+
a unique match ignoring case, so `country: "de"` finds `DE.yml` on Linux as
|
|
239
|
+
well as macOS. Two files whose basenames differ only in case are ambiguous, and
|
|
240
|
+
raise `NameBank::UnknownCountry` rather than being guessed at.
|
|
241
|
+
|
|
242
|
+
A pool file must carry the three keys in `NameBank::PoolSchema::KEYS`, each a
|
|
243
|
+
list. Any of them may have a native-script counterpart under the same key plus
|
|
244
|
+
`_native`:
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
firstnames_male: [Dmitry, Ivan]
|
|
248
|
+
firstnames_female: [Anna, Olga]
|
|
249
|
+
lastnames: [Ivanov, Petrov]
|
|
250
|
+
|
|
251
|
+
firstnames_male_native: [Дмитрий, Иван]
|
|
252
|
+
lastnames_native: [Иванов, Петров]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Variant files are validated the same way and need all three keys too. They may
|
|
256
|
+
carry `_native` pools, which `script: :native` will use — the shipped variants
|
|
257
|
+
do not, because the build pipeline strips them.
|
|
258
|
+
|
|
259
|
+
Missing a required key, or holding something other than a list under it, raises
|
|
260
|
+
`NameBank::MalformedPool` when the file is read. A key that is present but
|
|
261
|
+
empty is accepted at read time and raises `NameBank::EmptyPool` when sampled.
|
|
262
|
+
Any other key is ignored; the shipped files carry `source:`, which nothing
|
|
263
|
+
reads at runtime.
|
|
264
|
+
|
|
265
|
+
Sampling is uniform over the whole list, so order does not weight anything. It
|
|
266
|
+
is preserved, and visible through `first_names` and `last_names`.
|
|
267
|
+
|
|
268
|
+
## Pool schema
|
|
269
|
+
|
|
270
|
+
`NameBank::PoolSchema` names the YAML keys a pool file uses. It is supported
|
|
271
|
+
public API from 0.2.0 on: the constants and their values will not change
|
|
272
|
+
without a version bump. Use it when you build pool files for
|
|
273
|
+
`NameBank.new(data_dir:)`, so you are not typing key names by hand.
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
NameBank::PoolSchema::KEYS
|
|
277
|
+
# => ["firstnames_male", "firstnames_female", "lastnames"]
|
|
278
|
+
|
|
279
|
+
NameBank::PoolSchema::GIVEN_MALE # => "firstnames_male"
|
|
280
|
+
NameBank::PoolSchema::GIVEN_FEMALE # => "firstnames_female"
|
|
281
|
+
NameBank::PoolSchema::SURNAMES # => "lastnames"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`KEYS` is frozen and lists the three pools every file must carry. A file may
|
|
285
|
+
also carry a native-script pool for any of them, under the same key with a
|
|
286
|
+
`_native` suffix:
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
NameBank::PoolSchema.native_key("lastnames") # => "lastnames_native"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`gender_key` maps a `gender:` argument to its key, and raises
|
|
293
|
+
`NameBank::UnknownGender` for anything else:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
NameBank::PoolSchema.gender_key(:female) # => "firstnames_female"
|
|
297
|
+
NameBank::PoolSchema.gender_key(:x) # raises NameBank::UnknownGender
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Reading a file that is missing one of `KEYS`, or that holds something other
|
|
301
|
+
than a list under it, raises `NameBank::MalformedPool`.
|
|
302
|
+
|
|
154
303
|
## Supported countries
|
|
155
304
|
|
|
156
305
|
106 countries: Afghanistan, Albania, Algeria, Angola, Argentina, Austria,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Declared as a class, not a module: NameBank itself is one (see lib/name_bank.rb).
|
|
4
|
+
class NameBank
|
|
5
|
+
# Mixed into every error this gem raises, so `rescue NameBank::Error` catches
|
|
6
|
+
# all of them while each error keeps its natural superclass. Errors whose
|
|
7
|
+
# valid values are a fixed, countable set are ArgumentErrors; errors that
|
|
8
|
+
# depend on what a data dir happens to hold are not.
|
|
9
|
+
module Error; end
|
|
10
|
+
|
|
11
|
+
# No pool file for this country code.
|
|
12
|
+
class UnknownCountry < StandardError
|
|
13
|
+
include Error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# No pool file for this variant of this country.
|
|
17
|
+
class UnknownVariant < StandardError
|
|
18
|
+
include Error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A pool file is missing a schema key, or holds something other than a list.
|
|
22
|
+
class MalformedPool < StandardError
|
|
23
|
+
include Error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The requested pool exists but holds no names.
|
|
27
|
+
class EmptyPool < StandardError
|
|
28
|
+
include Error
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# script: was neither :latin nor :native.
|
|
32
|
+
class UnknownScript < ArgumentError
|
|
33
|
+
include Error
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# gender: was neither :male nor :female.
|
|
37
|
+
class UnknownGender < ArgumentError
|
|
38
|
+
include Error
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
# Declared as a class, not a module: NameBank itself is one (see lib/name_bank.rb).
|
|
6
|
+
class NameBank
|
|
4
7
|
# The pool-key schema: the base YAML keys, how native-script keys are named,
|
|
5
|
-
# and how a gender maps to its key.
|
|
6
|
-
# 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.
|
|
7
15
|
module PoolSchema
|
|
8
|
-
|
|
16
|
+
GIVEN_MALE = "firstnames_male"
|
|
17
|
+
GIVEN_FEMALE = "firstnames_female"
|
|
18
|
+
SURNAMES = "lastnames"
|
|
19
|
+
|
|
20
|
+
KEYS = [GIVEN_MALE, GIVEN_FEMALE, SURNAMES].freeze
|
|
9
21
|
|
|
10
22
|
module_function
|
|
11
23
|
|
|
@@ -15,9 +27,9 @@ module NameBank
|
|
|
15
27
|
|
|
16
28
|
def gender_key(gender)
|
|
17
29
|
case gender
|
|
18
|
-
when :male then
|
|
19
|
-
when :female then
|
|
20
|
-
else raise
|
|
30
|
+
when :male then GIVEN_MALE
|
|
31
|
+
when :female then GIVEN_FEMALE
|
|
32
|
+
else raise UnknownGender, "gender must be :male or :female, got #{gender.inspect}"
|
|
21
33
|
end
|
|
22
34
|
end
|
|
23
35
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "pool_schema"
|
|
7
|
+
|
|
8
|
+
# Declared as a class, not a module: NameBank itself is one (see lib/name_bank.rb).
|
|
9
|
+
class NameBank
|
|
10
|
+
# Everything that touches the filesystem: reading pool files, checking their
|
|
11
|
+
# shape against PoolSchema, memoizing them, and listing what a data dir holds.
|
|
12
|
+
# NameBank itself never opens a file. Files are read once per instance, and
|
|
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.
|
|
18
|
+
class PoolStore
|
|
19
|
+
def initialize(data_dir)
|
|
20
|
+
@data_dir = data_dir
|
|
21
|
+
@country_pools = {}
|
|
22
|
+
@variant_pools = {}
|
|
23
|
+
@variant_names = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pools(country, variant)
|
|
27
|
+
variant ? variant_pools(country, variant) : country_pools(country)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def countries
|
|
31
|
+
@countries ||= yml_basenames(File.join(@data_dir, "countries"))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def variants(country)
|
|
35
|
+
code = resolve(country, variant_countries)
|
|
36
|
+
return [] if code.nil?
|
|
37
|
+
|
|
38
|
+
@variant_names[code] ||= yml_basenames(File.join(@data_dir, "variants", code))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
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
|
+
|
|
63
|
+
def country_pools(country)
|
|
64
|
+
code = resolve(country, countries)
|
|
65
|
+
raise UnknownCountry, unresolved(country, countries) if code.nil?
|
|
66
|
+
|
|
67
|
+
@country_pools[code] ||= read(File.join(@data_dir, "countries", "#{code}.yml"))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def variant_pools(country, variant)
|
|
71
|
+
code = resolve(country, variant_countries)
|
|
72
|
+
raise UnknownVariant, "#{country}/#{variant}" if code.nil?
|
|
73
|
+
|
|
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"))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def read(path)
|
|
81
|
+
data = YAML.safe_load_file(path)
|
|
82
|
+
PoolSchema::KEYS.each do |key|
|
|
83
|
+
raise MalformedPool, "#{path}: #{key} must be a list of names" unless data[key].is_a?(Array)
|
|
84
|
+
end
|
|
85
|
+
data
|
|
86
|
+
end
|
|
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
|
+
|
|
98
|
+
def yml_basenames(dir)
|
|
99
|
+
return [] unless Dir.exist?(dir)
|
|
100
|
+
|
|
101
|
+
Dir.children(dir).select { |f| f.end_with?(".yml") }
|
|
102
|
+
.map { |f| File.basename(f, ".yml") }.sort
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private_constant :PoolStore
|
|
107
|
+
end
|
data/lib/name_bank/version.rb
CHANGED
data/lib/name_bank.rb
CHANGED
|
@@ -1,28 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "name_bank/version"
|
|
4
|
+
require_relative "name_bank/errors"
|
|
4
5
|
require_relative "name_bank/pool_schema"
|
|
5
|
-
require_relative "name_bank/
|
|
6
|
+
require_relative "name_bank/pool_store"
|
|
6
7
|
|
|
7
8
|
# Authentic, gender-matched given names and surnames for 106 countries,
|
|
8
9
|
# addressed by ISO alpha-2 country code. Sampling is uniform and deterministic
|
|
9
10
|
# from a caller-supplied RNG — no global locale or random state.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
#
|
|
12
|
+
# The class-level methods delegate to a default instance over the shipped data
|
|
13
|
+
# dir; instantiate with another data_dir to read pools from elsewhere. Country
|
|
14
|
+
# and variant files are read lazily and memoized per instance.
|
|
15
|
+
#
|
|
16
|
+
# Every error raised here is a NameBank::Error; see the README for the list.
|
|
17
|
+
class NameBank
|
|
16
18
|
DATA_DIR = File.expand_path("../data", __dir__)
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
# Every class-level method forwards to the default instance, so the signatures
|
|
21
|
+
# live once — on the instance methods below.
|
|
22
|
+
class << self
|
|
23
|
+
def default
|
|
24
|
+
@default ||= new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def first_name(...) = default.first_name(...)
|
|
28
|
+
def last_name(...) = default.last_name(...)
|
|
29
|
+
def full_name(...) = default.full_name(...)
|
|
30
|
+
def first_names(...) = default.first_names(...)
|
|
31
|
+
def last_names(...) = default.last_names(...)
|
|
32
|
+
def countries(...) = default.countries(...)
|
|
33
|
+
def variants(...) = default.variants(...)
|
|
34
|
+
def scripts(...) = default.scripts(...)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(data_dir: DATA_DIR)
|
|
38
|
+
@store = PoolStore.new(data_dir)
|
|
39
|
+
end
|
|
19
40
|
|
|
20
41
|
def first_name(country:, gender:, rng:, variant: nil, script: :latin)
|
|
21
|
-
|
|
42
|
+
first_names(country: country, gender: gender, variant: variant, script: script).sample(random: rng)
|
|
22
43
|
end
|
|
23
44
|
|
|
24
45
|
def last_name(country:, rng:, variant: nil, script: :latin)
|
|
25
|
-
|
|
46
|
+
last_names(country: country, variant: variant, script: script).sample(random: rng)
|
|
26
47
|
end
|
|
27
48
|
|
|
28
49
|
def full_name(country:, gender:, rng:, variant: nil, script: :latin)
|
|
@@ -32,19 +53,47 @@ module NameBank
|
|
|
32
53
|
}
|
|
33
54
|
end
|
|
34
55
|
|
|
56
|
+
# The whole frequency-ordered pool, frozen. The name strings stay mutable.
|
|
57
|
+
def first_names(country:, gender:, variant: nil, script: :latin)
|
|
58
|
+
pool(country, variant, PoolSchema.gender_key(gender), script)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def last_names(country:, variant: nil, script: :latin)
|
|
62
|
+
pool(country, variant, PoolSchema::SURNAMES, script)
|
|
63
|
+
end
|
|
64
|
+
|
|
35
65
|
def countries
|
|
36
|
-
|
|
66
|
+
@store.countries
|
|
37
67
|
end
|
|
38
68
|
|
|
39
69
|
def variants(country:)
|
|
40
|
-
|
|
70
|
+
@store.variants(country)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The script forms this country offers, not writing systems — see CONTEXT.md.
|
|
74
|
+
# With a variant, the forms that variant offers, which may differ.
|
|
75
|
+
def scripts(country:, variant: nil)
|
|
76
|
+
data = @store.pools(country, variant)
|
|
77
|
+
PoolSchema::KEYS.any? { |k| data[PoolSchema.native_key(k)]&.any? } ? %i[latin native] : %i[latin]
|
|
41
78
|
end
|
|
42
79
|
|
|
43
|
-
|
|
44
|
-
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def pool(country, variant, key, script)
|
|
83
|
+
names = names_for_script(@store.pools(country, variant), key, script)
|
|
84
|
+
raise EmptyPool, "#{country}/#{script}" if names.empty?
|
|
85
|
+
|
|
86
|
+
names.freeze
|
|
45
87
|
end
|
|
46
88
|
|
|
47
|
-
def
|
|
48
|
-
|
|
89
|
+
def names_for_script(data, key, script)
|
|
90
|
+
case script
|
|
91
|
+
when :latin then data.fetch(key)
|
|
92
|
+
when :native
|
|
93
|
+
native = data[PoolSchema.native_key(key)]
|
|
94
|
+
native && !native.empty? ? native : data.fetch(key)
|
|
95
|
+
else
|
|
96
|
+
raise UnknownScript, "script must be :latin or :native, got #{script.inspect}"
|
|
97
|
+
end
|
|
49
98
|
end
|
|
50
99
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: name_bank
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick Bartels
|
|
@@ -136,8 +136,9 @@ files:
|
|
|
136
136
|
- data/variants/US/african_american.yml
|
|
137
137
|
- docs/name-counts.md
|
|
138
138
|
- lib/name_bank.rb
|
|
139
|
+
- lib/name_bank/errors.rb
|
|
139
140
|
- lib/name_bank/pool_schema.rb
|
|
140
|
-
- lib/name_bank/
|
|
141
|
+
- lib/name_bank/pool_store.rb
|
|
141
142
|
- lib/name_bank/version.rb
|
|
142
143
|
homepage: https://github.com/roughneck/name_bank
|
|
143
144
|
licenses:
|
data/lib/name_bank/repository.rb
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
|
-
module NameBank
|
|
6
|
-
# Lazy, memoized loader of per-country name pools stored as YAML in data_dir.
|
|
7
|
-
class Repository
|
|
8
|
-
def initialize(data_dir:)
|
|
9
|
-
@data_dir = data_dir
|
|
10
|
-
@countries_cache = {}
|
|
11
|
-
@variants_cache = {}
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def firstnames(country:, gender:, variant: nil, script: :latin)
|
|
15
|
-
pool(load(country, variant), PoolSchema.gender_key(gender), script, country)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def lastnames(country:, variant: nil, script: :latin)
|
|
19
|
-
pool(load(country, variant), "lastnames", script, country)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def scripts(country:)
|
|
23
|
-
data = load(country, nil)
|
|
24
|
-
PoolSchema::KEYS.any? { |k| data[PoolSchema.native_key(k)]&.any? } ? %i[latin native] : %i[latin]
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def countries
|
|
28
|
-
@countries ||= yml_basenames(File.join(@data_dir, "countries"))
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def variants(country:)
|
|
32
|
-
yml_basenames(File.join(@data_dir, "variants", country))
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def pool(data, key, script, country)
|
|
38
|
-
names = names_for_script(data, key, script)
|
|
39
|
-
raise UnknownScript, "#{country}/#{script}" if names.nil? || names.empty?
|
|
40
|
-
|
|
41
|
-
names
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def names_for_script(data, key, script)
|
|
45
|
-
case script
|
|
46
|
-
when :latin then data.fetch(key)
|
|
47
|
-
when :native
|
|
48
|
-
native = data[PoolSchema.native_key(key)]
|
|
49
|
-
native && !native.empty? ? native : data.fetch(key)
|
|
50
|
-
else
|
|
51
|
-
raise ArgumentError, "script must be :latin or :native, got #{script.inspect}"
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def load(country, variant)
|
|
56
|
-
variant ? load_variant(country, variant) : load_country(country)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def load_country(country)
|
|
60
|
-
@countries_cache[country] ||= begin
|
|
61
|
-
path = File.join(@data_dir, "countries", "#{country}.yml")
|
|
62
|
-
raise UnknownCountry, country unless File.exist?(path)
|
|
63
|
-
|
|
64
|
-
YAML.safe_load_file(path)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def load_variant(country, variant)
|
|
69
|
-
@variants_cache[[country, variant]] ||= begin
|
|
70
|
-
path = File.join(@data_dir, "variants", country, "#{variant}.yml")
|
|
71
|
-
raise UnknownVariant, "#{country}/#{variant}" unless File.exist?(path)
|
|
72
|
-
|
|
73
|
-
YAML.safe_load_file(path)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def yml_basenames(dir)
|
|
78
|
-
return [] unless Dir.exist?(dir)
|
|
79
|
-
|
|
80
|
-
Dir.children(dir).select { |f| f.end_with?(".yml") }
|
|
81
|
-
.map { |f| File.basename(f, ".yml") }.sort
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|