name_bank 0.1.6 → 0.2.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: 8bdb30000d395f629f88e6a14d346c067b39049295d999c13556ca36145b1923
4
+ data.tar.gz: a23fc5f83c129d4a1016be83766d4a3e4e496c634254d58ff3b2e8a1df2effd8
5
5
  SHA512:
6
- metadata.gz: fcbc5f7075593a3da579e91cfaadd1503b976f6457d680b55169d66422826e08e1c6ddc1b37b873444e616a94cf393e4e72524f3f93316dc470c858f0654edbc
7
- data.tar.gz: 28de093ee1e8e751d6288d659480e240d40755c63a08fa0e59fe4afd23d1ec8e06198634b94bd3f94e9532c086321da90ef9dee10ea2a02a9ba109f2b67b2405
6
+ metadata.gz: c93d2d4749bde09cbb3717118e827370acb2f6dcf7227d388e5a84b75273122606b9887b962c1908778f505cac3630a13f40c8054bb02c7837051813c6366020
7
+ data.tar.gz: 1d176535f0208a680571a2f0156e80425e2adfbe6458ac26f956f06b53d25af41bf328f1bd5eb6b1d7e4d22ff8a58395fba4ba1a3ce09842480226780942183e
data/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ 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.2.0] - 2026-08-08
8
+
9
+ An interface release. Sampling behaviour and the name data are unchanged — the
10
+ same `country:`, `gender:`, `rng:`, `variant:` and `script:` calls return the
11
+ same names as 0.1.6. What changed is what else the interface offers, and how
12
+ failures are named. Three breaking changes are listed below.
13
+
14
+ ### Added
15
+ - `NameBank.first_names` and `NameBank.last_names` return a country's whole
16
+ frequency-ordered pool as a frozen array. They take the same `country:`,
17
+ `gender:`, `variant:` and `script:` options as the samplers.
18
+ - `NameBank` is now a class and can be instantiated: `NameBank.new(data_dir:)`
19
+ reads pools from a directory of your own. The class-level methods delegate to
20
+ a default instance over the shipped data.
21
+
22
+ ### Removed
23
+ - **Breaking:** `NameBank.repository` and the `NameBank::Repository` class. The
24
+ accessor was never documented; everything it offered is on `NameBank` itself,
25
+ with `firstnames`/`lastnames` renamed to `first_names`/`last_names`.
26
+
27
+ - `NameBank::MalformedPool`, raised when a pool file is missing a schema key or
28
+ holds something other than a list. Files are validated once when read.
29
+ - `NameBank::UnknownGender` and `NameBank::EmptyPool`, so every failure the gem
30
+ can produce has a name. The README now lists all six.
31
+
32
+ ### Changed
33
+ - `variants(country:)` is memoized like `countries`, so repeated calls no longer
34
+ hit the filesystem.
35
+ - **Breaking:** `NameBank::Error` is now a module mixed into every error rather
36
+ than their shared superclass. `rescue NameBank::Error` works as before and now
37
+ covers every failure, including the two argument errors that previously
38
+ escaped it; each error also keeps its natural superclass, so
39
+ `rescue ArgumentError` still catches a bad `gender:` or `script:`. Raising or
40
+ instantiating `NameBank::Error` itself is no longer possible.
41
+ - **Breaking:** `NameBank::UnknownScript` now means what its name says — a
42
+ `script:` that is neither `:latin` nor `:native`, previously a bare
43
+ `ArgumentError`. The empty-pool case it used to signal is now
44
+ `NameBank::EmptyPool`.
45
+
7
46
  ## [0.1.6] - 2026-07-24
8
47
 
9
48
  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,9 +157,10 @@ 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
166
  raises `NameBank::UnknownScript`.
@@ -151,6 +178,32 @@ NameBank.variants(country: "US") # => ["african_american"]
151
178
  NameBank.variants(country: "DE") # => []
152
179
  ```
153
180
 
181
+ ## Errors
182
+
183
+ Every error name_bank raises carries `NameBank::Error`, so one `rescue` covers
184
+ the lot. Each also keeps its natural Ruby superclass, so `rescue ArgumentError`
185
+ still catches the two that are genuinely caller mistakes:
186
+
187
+ | Error | Superclass | Raised when |
188
+ | --- | --- | --- |
189
+ | `NameBank::UnknownGender` | `ArgumentError` | `gender:` is neither `:male` nor `:female` |
190
+ | `NameBank::UnknownScript` | `ArgumentError` | `script:` is neither `:latin` nor `:native` |
191
+ | `NameBank::UnknownCountry` | `StandardError` | no pool file for that country code |
192
+ | `NameBank::UnknownVariant` | `StandardError` | no pool file for that variant |
193
+ | `NameBank::EmptyPool` | `StandardError` | the pool exists but holds no names |
194
+ | `NameBank::MalformedPool` | `StandardError` | a pool file is missing a key, or holds something other than a list |
195
+
196
+ The last two cannot occur with the shipped data; they matter when you point
197
+ `NameBank.new(data_dir:)` at pool files of your own.
198
+
199
+ ```ruby
200
+ begin
201
+ NameBank.first_name(country: "ZZ", gender: :male, rng: rng)
202
+ rescue NameBank::UnknownCountry => e
203
+ warn "no pool for #{e.message}"
204
+ end
205
+ ```
206
+
154
207
  ## Supported countries
155
208
 
156
209
  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,18 @@
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
8
+ # and how a gender maps to its key. Shared by the runtime NameBank and the
6
9
  # build-time SplitScripts tool so the key names live in one place.
7
10
  module PoolSchema
8
- KEYS = %w[firstnames_male firstnames_female lastnames].freeze
11
+ GIVEN_MALE = "firstnames_male"
12
+ GIVEN_FEMALE = "firstnames_female"
13
+ SURNAMES = "lastnames"
14
+
15
+ KEYS = [GIVEN_MALE, GIVEN_FEMALE, SURNAMES].freeze
9
16
 
10
17
  module_function
11
18
 
@@ -15,9 +22,9 @@ module NameBank
15
22
 
16
23
  def gender_key(gender)
17
24
  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}"
25
+ when :male then GIVEN_MALE
26
+ when :female then GIVEN_FEMALE
27
+ else raise UnknownGender, "gender must be :male or :female, got #{gender.inspect}"
21
28
  end
22
29
  end
23
30
  end
@@ -0,0 +1,71 @@
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
+ class PoolStore
15
+ def initialize(data_dir)
16
+ @data_dir = data_dir
17
+ @country_pools = {}
18
+ @variant_pools = {}
19
+ @variant_names = {}
20
+ end
21
+
22
+ def pools(country, variant)
23
+ variant ? variant_pools(country, variant) : country_pools(country)
24
+ end
25
+
26
+ def countries
27
+ @countries ||= yml_basenames(File.join(@data_dir, "countries"))
28
+ end
29
+
30
+ def variants(country)
31
+ @variant_names[country] ||= yml_basenames(File.join(@data_dir, "variants", country))
32
+ end
33
+
34
+ private
35
+
36
+ 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)
40
+
41
+ read(path)
42
+ end
43
+ end
44
+
45
+ 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)
49
+
50
+ read(path)
51
+ end
52
+ end
53
+
54
+ def read(path)
55
+ data = YAML.safe_load_file(path)
56
+ PoolSchema::KEYS.each do |key|
57
+ raise MalformedPool, "#{path}: #{key} must be a list of names" unless data[key].is_a?(Array)
58
+ end
59
+ data
60
+ end
61
+
62
+ def yml_basenames(dir)
63
+ return [] unless Dir.exist?(dir)
64
+
65
+ Dir.children(dir).select { |f| f.end_with?(".yml") }
66
+ .map { |f| File.basename(f, ".yml") }.sort
67
+ end
68
+ end
69
+
70
+ private_constant :PoolStore
71
+ 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.2.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,46 @@ 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)
41
71
  end
42
72
 
73
+ # The script forms this country offers, not writing systems — see CONTEXT.md.
43
74
  def scripts(country:)
44
- repository.scripts(country: country)
75
+ data = @store.pools(country, nil)
76
+ PoolSchema::KEYS.any? { |k| data[PoolSchema.native_key(k)]&.any? } ? %i[latin native] : %i[latin]
77
+ end
78
+
79
+ private
80
+
81
+ def pool(country, variant, key, script)
82
+ names = names_for_script(@store.pools(country, variant), key, script)
83
+ raise EmptyPool, "#{country}/#{script}" if names.empty?
84
+
85
+ names.freeze
45
86
  end
46
87
 
47
- def repository
48
- @repository ||= Repository.new(data_dir: DATA_DIR)
88
+ def names_for_script(data, key, script)
89
+ case script
90
+ when :latin then data.fetch(key)
91
+ when :native
92
+ native = data[PoolSchema.native_key(key)]
93
+ native && !native.empty? ? native : data.fetch(key)
94
+ else
95
+ raise UnknownScript, "script must be :latin or :native, got #{script.inspect}"
96
+ end
49
97
  end
50
98
  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.2.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