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
7 changed files with 72 additions and 37 deletions
Showing only changes of commit d5d9c2655e - Show all commits
+24 -13
View File
@@ -472,8 +472,9 @@ func iban(r rng, cc string) string {
const dayLayout = "2006-01-02"
// Alike in no field; layoutDay differs from layoutProbe in its date fields only,
// layoutClock in its clock fields only.
// The instants a layout is proved against: layoutProbe2 is alike in no field, while
// layoutDay differs from layoutProbe in its date fields alone and layoutClock in its
// clock fields alone, so formatting two of them tells which kind a layout names.
var (
layoutProbe = time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC)
layoutProbe2 = time.Date(2010, 11, 12, 13, 14, 15, 0, time.UTC)
@@ -481,6 +482,18 @@ var (
layoutClock = time.Date(2001, 2, 3, 13, 14, 15, 0, time.UTC)
)
func namesAField(layout string) bool {
return layoutProbe.Format(layout) != layoutProbe2.Format(layout)
}
func namesADateField(layout string) bool {
return layoutProbe.Format(layout) != layoutDay.Format(layout)
}
func namesAClockField(layout string) bool {
return layoutProbe.Format(layout) != layoutClock.Format(layout)
}
// quotedLayout reports whether an arg carries the single quotes a layout is written in.
func quotedLayout(a string) bool {
return len(a) >= 2 && a[0] == '\'' && a[len(a)-1] == '\''
@@ -493,10 +506,10 @@ func layoutArg(a string) (string, error) {
if strings.HasPrefix(a, `"`) || strings.HasSuffix(a, `"`) {
return "", fmt.Errorf("layout %s is double-quoted; write '%s'", a, bare)
}
return "", fmt.Errorf("layout %s is not quoted; write '%s', which a shell keeps only inside a double-quoted argument", a, bare)
return "", fmt.Errorf("layout %s is not quoted; write '%s'", a, bare)
}
layout := a[1 : len(a)-1]
if layoutProbe.Format(layout) == layoutProbe2.Format(layout) {
if !namesAField(layout) {
return "", fmt.Errorf("layout '%s' names no field, so it is the constant %q; write it as text", layout, layout)
}
return layout, nil
@@ -518,15 +531,15 @@ func layoutArity(name string, n int, a []string) error {
return nil
}
hint := ""
if len(a) > n && !split(a[n-1:]) {
if len(a) > n && !holdsQuotedLayout(a[n-1:]) {
hint = fmt.Sprintf("; a layout holding a comma is quoted: '%s'", strings.Join(a[n-1:], ", "))
}
return fmt.Errorf("%s takes %d argument%s, got %d%s", name, n, plural(n), len(a), hint)
}
// split reports whether the surplus args already hold a quoted layout, which no
// comma split apart.
func split(a []string) bool {
// holdsQuotedLayout reports whether the surplus args already carry a quoted layout,
// which no comma split apart.
func holdsQuotedLayout(a []string) bool {
for _, arg := range a {
if quotedLayout(arg) {
return true
@@ -535,8 +548,6 @@ func split(a []string) bool {
return false
}
func plural(n int) string { return map[bool]string{true: "s"}[n != 1] }
func dateArgs(_ map[string]node, a []string) error {
if err := layoutArity("date", 3, a); err != nil {
return err
@@ -556,10 +567,10 @@ func dateArgs(_ map[string]node, a []string) error {
if err != nil {
return err
}
if layoutProbe.Format(layout) == layoutDay.Format(layout) {
if !namesADateField(layout) {
return fmt.Errorf("date(from,to,layout): '%s' names no date field; write time('%s')", layout, layout)
}
if from.Equal(to) && layoutProbe.Format(layout) == layoutClock.Format(layout) {
if from.Equal(to) && !namesAClockField(layout) {
return fmt.Errorf("date(%s,%s,'%s') is the constant %q; write it as text", a[0], a[1], layout, from.Format(layout))
}
return nil
@@ -573,7 +584,7 @@ func timeArg(_ map[string]node, a []string) error {
if err != nil {
return err
}
if layoutProbe.Format(layout) != layoutDay.Format(layout) {
if namesADateField(layout) {
return fmt.Errorf("time(layout): '%s' names a date field; write date(from,to,layout)", layout)
}
return nil
+3
View File
@@ -28,6 +28,9 @@ const usage = `Usage: fejkdata [flags] <path|template>
<template> a format string or JSON value to render inline, e.g.
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}'
A layout inside a template is single-quoted, so quote the whole argument with " to
keep it: "{date(1990-01-01,2010-12-31,'2006-01-02')}".
An argument containing a { token, or a JSON object, array or string, is a
template; any other argument is a path (a path never contains a brace or a quote,
and a bracket only as a [selector] after a table's name). Templates reach the
+1
View File
@@ -62,6 +62,7 @@ func loadData(sources []dataSource) (map[string]node, error) {
if len(root) == 0 {
return nil, fmt.Errorf("no .json data found")
}
setTablePaths(root)
if err := linkTables(root); err != nil {
return nil, err
}
+1 -1
View File
@@ -97,7 +97,7 @@ func (d *draws) rowOf(t *table) int {
// pinRow pins row r of t where it agrees with the rows pinned before it.
func (d *draws) pinRow(t *table, r int) error {
if pr, ok := d.pinned(t); ok && pr != r {
return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.category)
return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.path)
}
for a := t.parentT; a != nil; a = a.parentT {
if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) {
+2 -2
View File
@@ -141,8 +141,8 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
}
shape := f.recordShapeOf(n)
if errors.Is(shape.err, errNoColumns) {
column := path[strings.LastIndex(path, ".")+1:]
return nil, fmt.Errorf("fejkdata: %s %w; render it as a column of one: '{\"format\":\"\",\"%s\":\"{/%s}\"}'", path, shape.err, column, path)
ns := names(segments)
return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, shape.err, ns[len(ns)-1], path)
}
if shape.err != nil {
return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err)
+34 -21
View File
@@ -14,7 +14,7 @@ import (
// cell of the row a render pinned; a cell carrying tokens compiles to a string node.
type table struct {
category string
path string // the category's path from the data root, set when the tables are linked
path string // the category's path from the data root, which a selector is written at
file string
format *template // fields are the column nodes
columns []string
@@ -340,6 +340,23 @@ func (t *table) compileFormat(format string) error {
// linkTables binds every table's parent to the table beside it, and proves the
// links: a parent has a key, every link cell is one, every parent row is linked
// to, no chain of parents closes, and no child is named like a parent's column.
// setTablePaths gives every table the path a selector on it is written at, before a
// link or a draw can name one.
func setTablePaths(root map[string]node) {
var walk func(dir string, children map[string]node)
walk = func(dir string, children map[string]node) {
for name, child := range children {
switch n := child.(type) {
case *folder:
walk(join(dir, name), n.children)
case *table:
n.path = join(dir, name)
}
}
}
walk("", root)
}
func linkTables(root map[string]node) error {
var walk func(dir string, children map[string]node) error
walk = func(dir string, children map[string]node) error {
@@ -351,7 +368,6 @@ func linkTables(root map[string]node) error {
return err
}
case *table:
n.path = path
if n.parent < 0 {
continue
}
@@ -510,7 +526,7 @@ func (t *table) under(r int, a *table, pr int) bool {
// naming several rows resolves inside the ancestors pinned in d.
func (t *table) find(sel string, d *draws) (int, error) {
if t.key < 0 && t.name < 0 {
return 0, fmt.Errorf("%s has no key or name column to select a row by", t.spelledPath())
return 0, fmt.Errorf("%s has no key or name column to select a row by", t.path)
}
if r, ok := t.byKey[sel]; ok {
return r, nil
@@ -523,34 +539,31 @@ func (t *table) find(sel string, d *draws) (int, error) {
case 1:
return rows[0], nil
case 0:
return 0, fmt.Errorf("no row of %s has key or name %q", t.spelledPath(), sel)
return 0, fmt.Errorf("no row of %s has key or name %q", t.path, sel)
}
keys := make([]string, len(rows))
return 0, t.ambiguous(sel, rows)
}
// ambiguous names each row a selector could have meant: a keyed table by its keys,
// and one told apart only by its parent by the path that selects it.
func (t *table) ambiguous(sel string, rows []int) error {
spellings := make([]string, len(rows))
for i, r := range rows {
if t.key < 0 {
keys[i] = t.selectorSpelling(r)
spellings[i] = t.selectorSpelling(r)
} else {
keys[i] = t.cell(r, t.key)
spellings[i] = t.cell(r, t.key)
}
}
listed := strings.Join(keys, ", ")
listed := strings.Join(spellings, ", ")
if t.key < 0 {
return 0, fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %s", sel, len(rows), t.spelledPath(), t.parentT.spelledPath(), listed)
return fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %s", sel, len(rows), t.path, t.parentT.path, listed)
}
inside := ""
if t.parentT != nil {
inside = fmt.Sprintf(", or select it inside its %s", t.parentT.category)
inside = fmt.Sprintf(", or select it inside its %s", t.parentT.path)
}
return 0, fmt.Errorf("%q names %d rows of %s; select one by key, one of %s%s", sel, len(rows), t.spelledPath(), listed, inside)
}
// spelledPath is the path a selector on t is written at, the category's own name
// until the tables are linked.
func (t *table) spelledPath() string {
if t.path == "" {
return t.category
}
return t.path
return fmt.Errorf("%q names %d rows of %s; select one by key, one of %s%s", sel, len(rows), t.path, listed, inside)
}
// selectorSpelling is how a path writes a selected row, for messages: by key, or
@@ -558,7 +571,7 @@ func (t *table) spelledPath() string {
func (t *table) selectorSpelling(r int) string {
switch {
case t.key >= 0:
return t.spelledPath() + "[" + t.cell(r, t.key) + "]"
return t.path + "[" + t.cell(r, t.key) + "]"
case t.name >= 0:
return t.parentT.selectorSpelling(t.parentRow(r)) + "." + t.category + "[" + t.cell(r, t.name) + "]"
}
+7
View File
@@ -112,6 +112,13 @@ func splitArgs(s string) []string {
return append(args, strings.TrimSpace(s[start:]))
}
func plural(n int) string {
if n == 1 {
return ""
}
return "s"
}
// checkFunc validates a function token at compile time: well-formed, naming a
// known builtin, with the arg count that builtin takes and args its check accepts.
// fields is passed through for the one builtin (calc) that validates against them.