name_bank 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 184aced32d90cfa194129bdb48895055b2738ed7bef92c1c1a42532421e6e4c6
4
- data.tar.gz: 6305e8defeb53e8a6bbb2d7fd47e73a41c6e870cd83659dfad56f24a96f3ebd8
3
+ metadata.gz: 6e238aa5013b3c3a8513f0bd233bb7365f7703c6f852270d93044ee424f6df95
4
+ data.tar.gz: 697fe182bc1222111617ca9dd2803d208cff375aff171b41d42bf32047cef45c
5
5
  SHA512:
6
- metadata.gz: 4eae078ebdd3bab2e6c3d455d3c783a6d03b5d119c386017c81d76e5ead0312e066cc24911621506b8c3b7637a88f679a1f5b8b1dfd3202427c0f274a3d3ef87
7
- data.tar.gz: 3c7ea3d49e618280f40734193e487c61fc5c6a9c762a16925843533099e7e727feb693dd9c22a1453a29d63316c962506199a493f7469a898ef559f5f5f3020a
6
+ metadata.gz: 12411b5e09c5e83bcecefc555c7568843832ed4a37c42553d5fc8efef2c1f4699f828731b1ac68c7e5cd1db93fdbc2baf8be34207553f67bb901b9e2fbc05c6f
7
+ data.tar.gz: f959d2ec630d8d53cf461c22dfa261935e8369e866d95ce513d5e64c86b42082c0c5622ce129d4e41bce9d42a87a7182c82202baa8c842b34b478e34b61ad2ab
data/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.4.0] - 2026-08-16
8
+
9
+ Many names in one call. Nothing that worked before changes: sampling from the
10
+ existing methods is byte-identical to 0.3.0 over 2830 samples across every
11
+ country, script and variant, and the name data is untouched.
12
+
13
+ ### Added
14
+ - `full_names(country:, gender:, rng:, count:)` draws that many distinct
15
+ given/family pairs in one call — the shape a database seed needs. No pair
16
+ repeats; single names do, because only the pair is unique. The guarantee
17
+ holds within one call, so no state is kept between calls and sampling stays
18
+ a function of the caller's RNG. `count:` is required, and asking for more
19
+ pairs than the two pools can form raises the new `NameBank::PoolExhausted`
20
+ rather than silently returning fewer.
21
+
22
+ ### Fixed
23
+ - README: the first usage example showed a name pair that the seed above it
24
+ never produces. Every `# =>` in the README is now the value the code actually
25
+ returns, checked against the shipped pools. The sampling examples under
26
+ *Scripts* and *Variants* name their seed too, so all of them can be
27
+ reproduced rather than taken on trust. No code or data changed.
28
+
7
29
  ## [0.3.0] - 2026-08-11
8
30
 
9
31
  Lookup, documentation and one platform bug. Sampling behaviour and the name
data/README.md CHANGED
@@ -27,23 +27,23 @@ gem install name_bank
27
27
  ```ruby
28
28
  require "name_bank"
29
29
 
30
- rng = Random.new(1234)
30
+ rng = Random.new(10246)
31
31
 
32
32
  # A full name (given + family) for a country and gender:
33
33
  NameBank.full_name(country: "DE", gender: :female, rng: rng)
34
- # => { firstname: "Sabine", lastname: "Müller" }
34
+ # => { firstname: "Alexandra", lastname: "Werner" }
35
35
 
36
36
  # Just a given name or a surname:
37
- NameBank.first_name(country: "IT", gender: :male, rng: rng) # => "Amor"
38
- NameBank.last_name(country: "JP", rng: rng) # => "Tsuru"
37
+ NameBank.first_name(country: "IT", gender: :male, rng: rng) # => "Settimio"
38
+ NameBank.last_name(country: "JP", rng: rng) # => "Katsuta"
39
39
 
40
40
  # Latin (default), or the country's native script where it has one:
41
- NameBank.first_name(country: "JP", gender: :female, rng: rng) # => "Sasahara"
42
- NameBank.first_name(country: "JP", gender: :female, rng: rng, script: :native) # => "ちゃこ"
41
+ NameBank.first_name(country: "JP", gender: :female, rng: rng) # => "Misako"
42
+ NameBank.first_name(country: "JP", gender: :female, rng: rng, script: :native) # => "さえ"
43
43
 
44
44
  # Default pool, or an alternate cultural pool where one exists:
45
- NameBank.first_name(country: "US", gender: :male, rng: rng) # => "Abe"
46
- NameBank.first_name(country: "US", gender: :male, rng: rng, variant: "african_american") # => "Roosevelt"
45
+ NameBank.first_name(country: "US", gender: :male, rng: rng) # => "Jonah"
46
+ NameBank.first_name(country: "US", gender: :male, rng: rng, variant: "african_american") # => "Tyrone"
47
47
 
48
48
  # List available countries (ISO alpha-2 codes):
49
49
  NameBank.countries.size # => 106
@@ -54,10 +54,38 @@ Sampling is uniform over each pool and fully deterministic for a given
54
54
  `rng` — the same seed always yields the same name. `gender:` is `:male`
55
55
  or `:female`.
56
56
 
57
+ ## Many at once
58
+
59
+ Seeding a table wants many names in one call, and wants them to differ.
60
+ `full_names` draws distinct given/family pairs:
61
+
62
+ ```ruby
63
+ rng = Random.new(24134)
64
+
65
+ NameBank.full_names(country: "DE", gender: :female, rng: rng, count: 3)
66
+ # => [{ firstname: "Gerlinde", lastname: "Nowak" },
67
+ # { firstname: "Juliane", lastname: "Stark" },
68
+ # { firstname: "Kornelia", lastname: "Lenz" }]
69
+ ```
70
+
71
+ No pair is drawn twice. Single names do recur once the draw is large enough —
72
+ only the pair is unique, which is what a real population looks like. The
73
+ guarantee holds within one call; two calls know nothing of each other.
74
+
75
+ `count:` is required, because there is no pool of pairs to hand back without
76
+ one. A country can form as many pairs as given names × surnames: 1500 × 1500
77
+ for most, 30 × 30 for the hand-curated `CN` and `UA` pools. Asking for more
78
+ raises rather than quietly returning fewer:
79
+
80
+ ```ruby
81
+ NameBank.full_names(country: "UA", gender: :male, rng: rng, count: 901)
82
+ # => NameBank::PoolExhausted: UA: 901 pairs requested, 900 available
83
+ ```
84
+
57
85
  ## Whole pools
58
86
 
59
- Where sampling one name is not enough — drawing many names without repeats,
60
- applying your own weighting, or checking what a country actually ships —
87
+ Where sampling is not enough — applying your own weighting, drawing single
88
+ names without repeats, or checking what a country actually ships —
61
89
  take the pool itself. `first_names` and `last_names` accept the same
62
90
  `country:`, `variant:` and `script:` options as the samplers, and return the
63
91
  frequency-ordered pool as a frozen array:
@@ -68,6 +96,11 @@ NameBank.first_names(country: "DE", gender: :female).first(3) # => ["Nicole", "
68
96
  NameBank.last_names(country: "JP", script: :native).first(3) # => ["佐藤", "鈴木", "田中"]
69
97
  ```
70
98
 
99
+ For single names without repeats, the pool plus Ruby is the whole answer:
100
+ `pool.sample(200, random: rng)` returns 200 different names, deterministically.
101
+ Note that `Array#sample` returns fewer than asked for rather than complaining
102
+ when the pool is too small — unlike `full_names`, which raises.
103
+
71
104
  Every method shown so far is also available on an instance, which lets you
72
105
  point name_bank at your own directory of pool files:
73
106
  `NameBank.new(data_dir: "…").first_name(country: "DE", gender: :male, rng: rng)`.
@@ -153,6 +186,8 @@ Names come in Latin (default) and, for countries with a non-Latin writing
153
186
  system, their native script. Pass `script:`:
154
187
 
155
188
  ```ruby
189
+ rng = Random.new(325089)
190
+
156
191
  NameBank.first_name(country: "RU", gender: :male, rng: rng) # => "Dmitry"
157
192
  NameBank.first_name(country: "RU", gender: :male, rng: rng, script: :native) # => "Алексей"
158
193
 
@@ -179,6 +214,8 @@ Some countries offer an alternate cultural name pool layered on the default.
179
214
  Pass `variant:`:
180
215
 
181
216
  ```ruby
217
+ rng = Random.new(41)
218
+
182
219
  NameBank.first_name(country: "US", gender: :male, rng: rng, variant: "african_american")
183
220
  # => "DeShawn"
184
221
 
@@ -199,10 +236,11 @@ still catches the two that are genuinely caller mistakes:
199
236
  | `NameBank::UnknownCountry` | `StandardError` | no pool file for that country code |
200
237
  | `NameBank::UnknownVariant` | `StandardError` | no pool file for that variant |
201
238
  | `NameBank::EmptyPool` | `StandardError` | the pool exists but holds no names |
239
+ | `NameBank::PoolExhausted` | `StandardError` | `full_names` was asked for more distinct pairs than the pools can form |
202
240
  | `NameBank::MalformedPool` | `StandardError` | a pool file is missing a key, or holds something other than a list |
203
241
 
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.
242
+ `EmptyPool` and `MalformedPool` cannot occur with the shipped data; they matter
243
+ when you point `NameBank.new(data_dir:)` at pool files of your own.
206
244
 
207
245
  ```ruby
208
246
  begin
@@ -28,6 +28,11 @@ class NameBank
28
28
  include Error
29
29
  end
30
30
 
31
+ # More distinct pairs were asked for than the pools can form.
32
+ class PoolExhausted < StandardError
33
+ include Error
34
+ end
35
+
31
36
  # script: was neither :latin nor :native.
32
37
  class UnknownScript < ArgumentError
33
38
  include Error
@@ -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.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
data/lib/name_bank.rb CHANGED
@@ -27,6 +27,7 @@ class NameBank
27
27
  def first_name(...) = default.first_name(...)
28
28
  def last_name(...) = default.last_name(...)
29
29
  def full_name(...) = default.full_name(...)
30
+ def full_names(...) = default.full_names(...)
30
31
  def first_names(...) = default.first_names(...)
31
32
  def last_names(...) = default.last_names(...)
32
33
  def countries(...) = default.countries(...)
@@ -53,6 +54,21 @@ class NameBank
53
54
  }
54
55
  end
55
56
 
57
+ # Distinct given/family pairs, for seeding many rows at once. No pair is drawn
58
+ # twice; single names do recur, which is what a population looks like. The
59
+ # guarantee holds within one call — two calls know nothing of each other.
60
+ # Asking for more pairs than the two pools can form raises PoolExhausted.
61
+ def full_names(country:, gender:, rng:, count:, variant: nil, script: :latin)
62
+ firsts = first_names(country: country, gender: gender, variant: variant, script: script)
63
+ lasts = last_names(country: country, variant: variant, script: script)
64
+ available = firsts.size * lasts.size
65
+ raise PoolExhausted, "#{country}: #{count} pairs requested, #{available} available" if count > available
66
+
67
+ distinct_indices(available, count, rng).map do |index|
68
+ { firstname: firsts[index / lasts.size], lastname: lasts[index % lasts.size] }
69
+ end
70
+ end
71
+
56
72
  # The whole frequency-ordered pool, frozen. The name strings stay mutable.
57
73
  def first_names(country:, gender:, variant: nil, script: :latin)
58
74
  pool(country, variant, PoolSchema.gender_key(gender), script)
@@ -79,6 +95,20 @@ class NameBank
79
95
 
80
96
  private
81
97
 
98
+ # Partial Fisher-Yates over the space of pair indices, with the swaps kept in
99
+ # a Hash so that the untouched majority of a two-million-pair space is never
100
+ # built. Drawing every pair a country has takes count steps; drawing at random
101
+ # until enough distinct pairs turn up would stall on the last few.
102
+ def distinct_indices(available, count, rng)
103
+ swapped = {}
104
+ Array.new(count) do |drawn|
105
+ index = rng.rand(drawn...available)
106
+ picked = swapped.fetch(index, index)
107
+ swapped[index] = swapped.fetch(drawn, drawn)
108
+ picked
109
+ end
110
+ end
111
+
82
112
  def pool(country, variant, key, script)
83
113
  names = names_for_script(@store.pools(country, variant), key, script)
84
114
  raise EmptyPool, "#{country}/#{script}" if names.empty?
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Bartels