Weighted person names, valid personal ids and date() in both locales #21
+24
-13
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) + "]"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user