Weighted person names, valid personal ids and date() in both locales #21

Merged
lilleman merged 38 commits from person-ids-date into main 2026-09-18 20:45:51 +02:00
4 changed files with 116 additions and 4 deletions
Showing only changes of commit bb43345d63 - Show all commits
+48 -2
View File
@@ -298,8 +298,8 @@ func TestBuiltinDateAndTime(t *testing.T) {
func TestBuiltinDateArgs(t *testing.T) { func TestBuiltinDateArgs(t *testing.T) {
for tmpl, want := range map[string]string{ for tmpl, want := range map[string]string{
`"{date(1990-13-01,1990-12-31,'2006-01-02')}"`: "1990-13-01", `"{date(1990-13-01,1990-12-31,'2006-01-02')}"`: "1990-13-01",
`"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "before", `"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "is after",
`"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: "before", `"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: "write it as text",
`"{date(1990-01-01,1990-12-31,2006-01-02)}"`: "'2006-01-02'", `"{date(1990-01-01,1990-12-31,2006-01-02)}"`: "'2006-01-02'",
`"{date(1990-01-01,1990-12-31,'January 2, 2006)}"`: "'", `"{date(1990-01-01,1990-12-31,'January 2, 2006)}"`: "'",
`"{date(1990-01-01,1990-12-31,'x')}"`: "text", `"{date(1990-01-01,1990-12-31,'x')}"`: "text",
@@ -318,3 +318,49 @@ func TestBuiltinDateArgs(t *testing.T) {
} }
} }
} }
// TestBuiltinLayoutErrorsNameARunnableSpelling pins the two layout errors a user
// can follow: a double-quoted layout is named single-quoted, without its own
// quotes carried into the suggestion, and a bare one says a shell ate them.
func TestBuiltinLayoutErrorsNameARunnableSpelling(t *testing.T) {
_, err := compile(parse(t, `"{date(1990-01-01,1990-12-31,\"2006-01-02\")}"`))
if err == nil || !strings.Contains(err.Error(), "write '2006-01-02'") {
t.Fatalf("a double-quoted layout = %v, want it named single-quoted", err)
}
if strings.Contains(err.Error(), `'"`) {
t.Errorf("%v names a layout that renders its own quotes", err)
}
_, err = compile(parse(t, `"{time(15:04)}"`))
if err == nil || !strings.Contains(err.Error(), "write '15:04'") || !strings.Contains(err.Error(), "shell") {
t.Fatalf("a bare layout = %v, want it named quoted and the shell explained", err)
}
// The comma hint belongs to a layout that split, not to a call given extra args.
_, err = compile(parse(t, `"{time(0,12,'15:04')}"`))
if err == nil || !strings.Contains(err.Error(), "takes 1 argument, got 3") {
t.Fatalf("time with three args = %v, want the count named in the singular", err)
}
if strings.Contains(err.Error(), "holding a comma") {
t.Errorf("%v offers the comma hint though the layout is already quoted", err)
}
}
// TestBuiltinDateSpansOneDay pins that from == to is a day: with a clock layout it
// draws every second of it, and without one it could only emit one value.
func TestBuiltinDateSpansOneDay(t *testing.T) {
f := engine(1)
seen := map[string]bool{}
for i := 0; i < 500; i++ {
got := mustRender(t, f, `"{date(2026-01-01,2026-01-01,'2006-01-02 15:04:05')}"`)
if !strings.HasPrefix(got, "2026-01-01 ") {
t.Fatalf("date over one day = %q, out of range", got)
}
seen[got] = true
}
if len(seen) < 400 {
t.Fatalf("date over one day drew %d distinct seconds in 500, want the whole day", len(seen))
}
_, err := compile(parse(t, `"{date(2026-01-01,2026-01-01,'2006-01-02')}"`))
if err == nil || !strings.Contains(err.Error(), "write it as text") {
t.Fatalf("one day in a date-only layout = %v, want it named a constant", err)
}
}
+22
View File
@@ -392,3 +392,25 @@ func TestShippedStreetNumberFormats(t *testing.T) {
} }
} }
} }
// TestShippedUSTitleAgreesWithSex pins that one person's title and sex agree, and
// that the everyday titles are reachable.
func TestShippedUSTitleAgreesWithSex(t *testing.T) {
f := newGenerator(t, "data", WithSeed(4))
female := map[string]bool{"Miss": true, "Mrs": true, "Ms": true}
seen := map[string]bool{}
for i := 0; i < 3000; i++ {
got := fakeTemplate(t, f, `{/en_US.person.prefix}|{/en_US.person.sex}`)
title, sex, _ := strings.Cut(got, "|")
title = strings.TrimSpace(title)
seen[title] = true
if title == "Mr" && sex != "male" || female[title] && sex != "female" {
t.Fatalf("%q: the title contradicts the sex", got)
}
}
for _, want := range []string{"", "Mr", "Ms"} {
if !seen[want] {
t.Errorf("en_US.person.prefix never drew %q in 3000 draws", want)
}
}
}
+15
View File
@@ -547,3 +547,18 @@ func TestRecordTemplateRejectsATopLevelRepeat(t *testing.T) {
t.Errorf("NewTemplate on the same input = %v, want the string view to still compile", err) t.Errorf("NewTemplate on the same input = %v, want the string view to still compile", err)
} }
} }
// TestRecordOfAFieldlessCategoryNamesTheWrapper pins the way out of the first
// command a fixture author types: a category of one value has no columns, and the
// error names the record that gives it one.
func TestRecordOfAFieldlessCategoryNamesTheWrapper(t *testing.T) {
f := newGenerator(t, "data", WithSeed(1))
want := `'{"format":"","personnummer":"{/sv_SE.personnummer}"}'`
_, err := f.FakeRecord("sv_SE.personnummer")
if err == nil || !strings.Contains(err.Error(), want) {
t.Fatalf("FakeRecord(sv_SE.personnummer) = %v, want it to name %s", err, want)
}
if _, err := f.FakeRecordTemplate(`{"format":"","personnummer":"{/sv_SE.personnummer}"}`); err != nil {
t.Fatalf("the named wrapper does not render: %v", err)
}
}
+31 -2
View File
@@ -172,7 +172,7 @@ func TestTableSelectsARowByKeyOrName(t *testing.T) {
} }
for path, want := range map[string]string{ for path, want := range map[string]string{
"region[99]": `"99"`, "region[99]": `"99"`,
"locality[Sandby]": "L7 L8", "locality[Sandby]": "L7, L8",
"region[12].code[1]": "not a table", "region[12].code[1]": "not a table",
"region[]": "empty", "region[]": "empty",
"region[12": "]", "region[12": "]",
@@ -334,7 +334,7 @@ func TestTableSelectorInAReference(t *testing.T) {
} }
for name, c := range map[string]struct{ json, want string }{ for name, c := range map[string]struct{ json, want string }{
"unknown row": {`"{/region[99].name}"`, `"99"`}, "unknown row": {`"{/region[99].name}"`, `"99"`},
"ambiguous name": {`"{/locality[Sandby].name}"`, "L7 L8"}, "ambiguous name": {`"{/locality[Sandby].name}"`, "L7, L8"},
"not inside": {`"{/region[12].municipality[0180].name}"`, "not inside"}, "not inside": {`"{/region[12].municipality[0180].name}"`, "not inside"},
"selector on template": {`"{/x[1].a}"`, "not a table"}, "selector on template": {`"{/x[1].a}"`, "not a table"},
} { } {
@@ -744,3 +744,32 @@ func TestNamedTableWithoutAKeyResolvesInsideItsParent(t *testing.T) {
t.Fatalf("a name selected inside a pinned parent = %q", got) t.Fatalf("a name selected inside a pinned parent = %q", got)
} }
} }
// TestAmbiguousNameNamesARunnablePath pins what a shell user reads: each row is
// named as the path they can type, from the root, and the rows are listed plainly.
func TestAmbiguousNameNamesARunnablePath(t *testing.T) {
files := map[string]string{}
for name, body := range siblings() {
files["se/"+name] = body
}
files["se/street.json"] = `{"format":"{name}","rows":"street.tsv","name":"name","parent":"locality","weight":"segments"}`
f := newGenerator(t, writeFiles(t, files), WithSeed(1))
_, err := f.Fake("se.street[Sandbyvägen]")
if err == nil {
t.Fatal("se.street[Sandbyvägen] resolved, though two rows carry that name")
}
for _, want := range []string{"se.locality[L7].street[Sandbyvägen]", "se.locality[L8].street[Sandbyvägen]"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("%v does not name %s", err, want)
}
if got := fake(t, f, want); got != "Sandbyvägen" {
t.Errorf("Fake(%q) = %q, want the named path to run", want, got)
}
}
if strings.Contains(err.Error(), "[se.locality") {
t.Errorf("%v lists the rows as a Go slice; separate them with commas", err)
}
if _, err := f.Fake("se.locality[Sandby]"); err == nil || !strings.Contains(err.Error(), "one of L7, L8") {
t.Fatalf("se.locality[Sandby] = %v, want its keys listed plainly", err)
}
}