From 1afcbc1a83df1917d9393da46e06b8caf20f5106 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 5 Oct 2026 00:38:20 +0200 Subject: [PATCH] Answer stability round 1: bind the locality so population still weighs an address, test the field paths agree, and restate why birth-number sits under sex --- CHANGELOG.md | 4 ++-- README.md | 9 ++++----- data/geo/SE/address.json | 6 +++--- data/geo/US/address.json | 8 ++++---- data_test.go | 33 ++++++++++++++++++++++++--------- docs/decisions.md | 4 ++-- 6 files changed, 39 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6de4514..973a7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,8 +69,8 @@ replacement, and each removed path, column or flag. - `geo.SE` and `geo.US`: five linked tables per country, `region`, `municipality`, `locality`, `postal-code` and `street`, weighted by population and address counts and built from SCB, GeoNames, Trafikverket NVDB and the US Census Bureau, and an - `address` record binding one street and reading its locality, postal code and region - through it. `sv_SE.address` and `en_US.address` bind those records once, so + `address` record binding one locality and drawing its street and postal code inside + it. `sv_SE.address` and `en_US.address` bind those records once, so `en_US.address.street` no longer carries `name` and `suffix`, and a locale folder loads only beside `geo`. - `{date(from,to,'layout')}` and `{time('layout')}`: a second between two days, or diff --git a/README.md b/README.md index a2642ef..5adbc5d 100644 --- a/README.md +++ b/README.md @@ -238,8 +238,7 @@ too, so a person's prefix never contradicts it. A `personnummer`'s birth number, for a man, which no real person is ever given. A person of a chosen sex is assembled from the tables — `sex[f].first-name` beside -`last-name` — while a shipped `personnummer` draws its own sex, apart from any `person` -beside it. That test series is also small: a personnummer +`last-name`. Skatteverket's test series is also small: a personnummer is one of about 70,000 values, a day in 1930–2025 against the two birth numbers, so a fixture past a few hundred rows repeats one and a `UNIQUE` column needs a category of your own. `sv_SE.date` and `en_US.date` are uniform over 1970-01-01 to 2029-12-31, @@ -249,9 +248,9 @@ your own. `sv_SE.date` and `en_US.date` are uniform over 1970-01-01 to 2029-12-3 inside a sex. A `geo` folder holds one tree per country under its alpha-2 code: five -[linked tables](#linked-tables) named alike, and an `address` record binding one street and -reading its locality, postal code and region through it, which the locale's `address` -binds once and prints whole. +[linked tables](#linked-tables) named alike, and an `address` record binding one locality +and drawing its street and postal code inside it, which the locale's `address` binds once +and prints whole. | Table | `geo.SE` | `geo.US` | Weight | |-------|----------|----------|--------| diff --git a/data/geo/SE/address.json b/data/geo/SE/address.json index ff13dd1..2b188b4 100644 --- a/data/geo/SE/address.json +++ b/data/geo/SE/address.json @@ -1,6 +1,6 @@ { "format": "{street} {street-number}\n{postal-code} {locality}", - "street": "{.street as s}{s.name}", + "street": "{.locality as l}{l.street.name}", "street-number": [ "{int(1,9)}", "{int(10,99)}", @@ -9,6 +9,6 @@ { "format": "{int(10,99)}{upper(1)}", "weight": 0.1 }, { "format": "{int(100,999)}{upper(1)}", "weight": 0.05 } ], - "postal-code": "{s..locality.postal-code.code}", - "locality": "{s..locality.name}" + "postal-code": "{l.postal-code.code}", + "locality": "{l.name}" } diff --git a/data/geo/US/address.json b/data/geo/US/address.json index 8ca3cd9..27c909f 100644 --- a/data/geo/US/address.json +++ b/data/geo/US/address.json @@ -1,13 +1,13 @@ { "format": "{street-number} {street}\n{locality}, {region} {postal-code}", - "street": "{.street as s}{s.name}", + "street": "{.locality as l}{l.street.name}", "street-number": [ "{int(10,99)}", { "format": "{int(100,999)}", "weight": 2 }, { "format": "{int(1000,9999)}", "weight": 0.5 }, { "format": "{int(10,99)}{int(100,999)}", "weight": 0.3 } ], - "locality": "{s..locality.name}", - "region": "{s..locality..municipality..region.abbr}", - "postal-code": "{s..locality.postal-code.code}" + "locality": "{l.name}", + "region": "{l..municipality..region.abbr}", + "postal-code": "{l.postal-code.code}" } diff --git a/data_test.go b/data_test.go index 8017a47..1e9a38a 100644 --- a/data_test.go +++ b/data_test.go @@ -268,9 +268,10 @@ func TestShippedPersonNames(t *testing.T) { seen := map[string]bool{} for i := 0; i < 2000; i++ { seen[fake(t, f, locale+".sex[f].first-name")] = true - first, sex := fake(t, f, locale+".person.first"), fake(t, f, locale+".person.sex") - if first == "" || sex == "" { - t.Fatalf("%s.person lacks a first name or a sex", locale) + got := fakeTemplate(t, f, `{/`+locale+`.person.first}|{/`+locale+`.person.sex}`) + first, sex, _ := strings.Cut(got, "|") + if v := fake(t, f, locale+".sex["+sex+"].first-name["+first+"]"); v != first { + t.Fatalf("%s: %q: %q is not a %s name", locale, got, first, sex) } } if len(seen) < 300 || !seen["Anna"] && locale == "sv_SE" || !seen["Mary"] && locale == "en_US" { @@ -339,22 +340,36 @@ func TestShippedSwedishAddress(t *testing.T) { // one address category are two addresses. func TestShippedAddressIsOneNamedPick(t *testing.T) { f := newGenerator(t, "data", WithSeed(3)) - for _, c := range []struct{ locale, country, template string }{ - {"sv_SE", "SE", "{a.street}|{a.postal-code}|{a.locality}||{b.street}"}, - {"en_US", "US", "{a.street}|{a.postal-code}|{a.locality}|{a.region}|{b.street}"}, + for _, c := range []struct{ locale, country, template, largest string }{ + {"sv_SE", "SE", "{a.street}|{a.postal-code}|{a.locality}||{b.street}", "Stockholm"}, + {"en_US", "US", "{a.street}|{a.postal-code}|{a.locality}|{a.region}|{b.street}", "New York"}, } { places := geoPlaces(t, c.country) - apart := false - for i := 0; i < 300; i++ { + apart, localities := false, map[string]int{} + for i := 0; i < 2000; i++ { got := fakeTemplate(t, f, `{/`+c.locale+`.address as a}{/`+c.locale+`.address as b}`+c.template) parts := strings.Split(got, "|") if !places[strings.Join(parts[:4], "|")] { t.Fatalf("%s.address: %q is no street, postal code, locality and region of one place", c.locale, got) } apart = apart || parts[0] != parts[4] + localities[parts[2]]++ + region := "{/" + c.locale + ".address.region}" + if c.country == "SE" { + region = "" + } + got = fakeTemplate(t, f, `{/`+c.locale+`.address.street}|{/`+c.locale+`.address.postal-code}|{/`+c.locale+`.address.locality}|`+region) + if !places[got] { + t.Fatalf("%s.address: field paths %q are no street, postal code, locality and region of one place", c.locale, got) + } } if !apart { - t.Errorf("%s: two names bound to address drew one street in 300 renders, want two addresses", c.locale) + t.Errorf("%s: two names bound to address drew one street in 2000 renders, want two addresses", c.locale) + } + for l, n := range localities { + if n > localities[c.largest] { + t.Errorf("%s.address drew %s %d times and %s %d, want the locality weighted by population", c.locale, l, n, c.largest, localities[c.largest]) + } } } } diff --git a/docs/decisions.md b/docs/decisions.md index 0c6258b..a2f5786 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -861,8 +861,8 @@ name is an error listing both keys; select by type instead. A Luhn-valid personnummer over a random birth number may be a living person's; 238 and 239 after any date are blocked from assignment, so the shipped `personnummer` and `samordningsnummer` use those. They sit in a `birth-number` table under `sex` rather -than as a column of it: the render's shared draw of the family is what makes the number -and the name agree on sex, and `sex` stays one shape across locales instead of +than as a column of it, so a pick of a sex can read the number, which `todo.md` item 28 +has `personnummer` do to agree with a person's sex, and `sex` stays one shape across locales instead of collecting every sex-keyed id fact. A samordningsnummer's day, the birthday plus 60, is drawn from 61 to 88, valid in every month, rather than computed from the date drawn.