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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe2659cbdd15df894e97db3be809f9040aa3d58f4c1a3df0353e977053e59dca
4
- data.tar.gz: 9172dafa9ee2f5fab0d51eb0a0eb7c7c127241631c06a9306d5899349ccab83b
3
+ metadata.gz: 184aced32d90cfa194129bdb48895055b2738ed7bef92c1c1a42532421e6e4c6
4
+ data.tar.gz: 6305e8defeb53e8a6bbb2d7fd47e73a41c6e870cd83659dfad56f24a96f3ebd8
5
5
  SHA512:
6
- metadata.gz: fcbc5f7075593a3da579e91cfaadd1503b976f6457d680b55169d66422826e08e1c6ddc1b37b873444e616a94cf393e4e72524f3f93316dc470c858f0654edbc
7
- data.tar.gz: 28de093ee1e8e751d6288d659480e240d40755c63a08fa0e59fe4afd23d1ec8e06198634b94bd3f94e9532c086321da90ef9dee10ea2a02a9ba109f2b67b2405
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) # => "Giuseppe"
38
- NameBank.last_name(country: "JP", rng: rng) # => "Tanaka"
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", ..., "ZA"]
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] (Latin is Germany's script)
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::UnknownScript`.
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
- module NameBank
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. Shared by the runtime Repository and the
6
- # build-time SplitScripts tool so the key names live in one place.
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
- KEYS = %w[firstnames_male firstnames_female lastnames].freeze
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 "firstnames_male"
19
- when :female then "firstnames_female"
20
- else raise ArgumentError, "gender must be :male or :female, got #{gender.inspect}"
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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module NameBank
4
- VERSION = "0.1.6"
3
+ # Declared as a class, not a module: NameBank itself is one (see lib/name_bank.rb).
4
+ class NameBank
5
+ VERSION = "0.3.0"
5
6
  end
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/repository"
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
- module NameBank
11
- class Error < StandardError; end
12
- class UnknownCountry < Error; end
13
- class UnknownVariant < Error; end
14
- class UnknownScript < Error; end
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
- module_function
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
- repository.firstnames(country: country, gender: gender, variant: variant, script: script).sample(random: rng)
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
- repository.lastnames(country: country, variant: variant, script: script).sample(random: rng)
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
- repository.countries
66
+ @store.countries
37
67
  end
38
68
 
39
69
  def variants(country:)
40
- repository.variants(country: country)
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
- def scripts(country:)
44
- repository.scripts(country: country)
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 repository
48
- @repository ||= Repository.new(data_dir: DATA_DIR)
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.1.6
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/repository.rb
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:
@@ -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