name_bank 0.2.0 → 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: 8bdb30000d395f629f88e6a14d346c067b39049295d999c13556ca36145b1923
4
- data.tar.gz: a23fc5f83c129d4a1016be83766d4a3e4e496c634254d58ff3b2e8a1df2effd8
3
+ metadata.gz: 184aced32d90cfa194129bdb48895055b2738ed7bef92c1c1a42532421e6e4c6
4
+ data.tar.gz: 6305e8defeb53e8a6bbb2d7fd47e73a41c6e870cd83659dfad56f24a96f3ebd8
5
5
  SHA512:
6
- metadata.gz: c93d2d4749bde09cbb3717118e827370acb2f6dcf7227d388e5a84b75273122606b9887b962c1908778f505cac3630a13f40c8054bb02c7837051813c6366020
7
- data.tar.gz: 1d176535f0208a680571a2f0156e80425e2adfbe6458ac26f956f06b53d25af41bf328f1bd5eb6b1d7e4d22ff8a58395fba4ba1a3ce09842480226780942183e
6
+ metadata.gz: 4eae078ebdd3bab2e6c3d455d3c783a6d03b5d119c386017c81d76e5ead0312e066cc24911621506b8c3b7637a88f679a1f5b8b1dfd3202427c0f274a3d3ef87
7
+ data.tar.gz: 3c7ea3d49e618280f40734193e487c61fc5c6a9c762a16925843533099e7e727feb693dd9c22a1453a29d63316c962506199a493f7469a898ef559f5f5f3020a
data/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ 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
+
7
41
  ## [0.2.0] - 2026-08-08
8
42
 
9
43
  An interface release. Sampling behaviour and the name data are unchanged — the
data/README.md CHANGED
@@ -163,7 +163,15 @@ NameBank.scripts(country: "DE") # => [:latin]
163
163
  Germany's script is Latin, so `DE` reports `:latin` only.
164
164
  `:latin` and `:native` sample from independent pools. For Latin-script countries
165
165
  `:native` returns the same (Latin) pool. Requesting a script with no names
166
- 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
+ ```
167
175
 
168
176
  ## Variants
169
177
 
@@ -204,6 +212,94 @@ rescue NameBank::UnknownCountry => e
204
212
  end
205
213
  ```
206
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
+
207
303
  ## Supported countries
208
304
 
209
305
  106 countries: Afghanistan, Albania, Algeria, Angola, Argentina, Austria,
@@ -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. Shared by the runtime NameBank and the
9
- # 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.
10
15
  module PoolSchema
11
16
  GIVEN_MALE = "firstnames_male"
12
17
  GIVEN_FEMALE = "firstnames_female"
@@ -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
- @variant_names[country] ||= yml_basenames(File.join(@data_dir, "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))
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
- @country_pools[country] ||= begin
38
- path = File.join(@data_dir, "countries", "#{country}.yml")
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
- read(path)
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
- @variant_pools[[country, variant]] ||= begin
47
- path = File.join(@data_dir, "variants", country, "#{variant}.yml")
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
- read(path)
51
- end
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
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Declared as a class, not a module: NameBank itself is one (see lib/name_bank.rb).
4
4
  class NameBank
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
data/lib/name_bank.rb CHANGED
@@ -71,8 +71,9 @@ class NameBank
71
71
  end
72
72
 
73
73
  # The script forms this country offers, not writing systems — see CONTEXT.md.
74
- def scripts(country:)
75
- data = @store.pools(country, nil)
74
+ # With a variant, the forms that variant offers, which may differ.
75
+ def scripts(country:, variant: nil)
76
+ data = @store.pools(country, variant)
76
77
  PoolSchema::KEYS.any? { |k| data[PoolSchema.native_key(k)]&.any? } ? %i[latin native] : %i[latin]
77
78
  end
78
79
 
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Bartels