Weighted person names, valid personal ids and date() in both locales #21
+87
@@ -7,6 +7,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,6 +61,8 @@ var builtins = map[string]builtin{
|
|||||||
cc := a[0]
|
cc := a[0]
|
||||||
return func(s *session, _ string, _ []string) string { return iban(s, cc) }
|
return func(s *session, _ string, _ []string) string { return iban(s, cc) }
|
||||||
}},
|
}},
|
||||||
|
"date": {arity: 3, check: dateArgs, prep: datePrep},
|
||||||
|
"time": {arity: 1, check: timeArg, prep: timePrep},
|
||||||
"calc": {arity: -1, check: checkCalc, prep: calcPrep, operands: calcOperands},
|
"calc": {arity: -1, check: checkCalc, prep: calcPrep, operands: calcOperands},
|
||||||
"lowercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToLower), operands: transformOperand},
|
"lowercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToLower), operands: transformOperand},
|
||||||
"uppercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToUpper), operands: transformOperand},
|
"uppercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToUpper), operands: transformOperand},
|
||||||
@@ -466,3 +469,87 @@ func iban(r rng, cc string) string {
|
|||||||
feed(0)
|
feed(0)
|
||||||
return fmt.Sprintf("%s%02d%s", cc, 98-rem, bban)
|
return fmt.Sprintf("%s%02d%s", cc, 98-rem, bban)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dayLayout = "2006-01-02"
|
||||||
|
|
||||||
|
// The two instants a layout is proved against: alike in nothing, so a layout that
|
||||||
|
// formats them alike names no field, and one that tells their days apart at the same
|
||||||
|
// clock names a date field.
|
||||||
|
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)
|
||||||
|
layoutDay = time.Date(2010, 11, 12, 4, 5, 6, 0, time.UTC)
|
||||||
|
)
|
||||||
|
|
||||||
|
// layoutArg is the Go layout a quoted arg holds; unquoted, it is refused naming the
|
||||||
|
// quoted spelling, since a layout may carry the comma that splits args.
|
||||||
|
func layoutArg(a string) (string, error) {
|
||||||
|
if len(a) < 2 || a[0] != '\'' || a[len(a)-1] != '\'' {
|
||||||
|
return "", fmt.Errorf("layout %s is not quoted; write '%s'", a, strings.Trim(a, "'"))
|
||||||
|
}
|
||||||
|
layout := a[1 : len(a)-1]
|
||||||
|
if layoutProbe.Format(layout) == layoutProbe2.Format(layout) {
|
||||||
|
return "", fmt.Errorf("layout '%s' names no field, so it is the constant %q; write it as text", layout, layout)
|
||||||
|
}
|
||||||
|
return layout, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// layoutOf is layoutArg for an arg a check already validated.
|
||||||
|
func layoutOf(a string) string {
|
||||||
|
layout, err := layoutArg(a)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", a, err))
|
||||||
|
}
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func dateArgs(_ map[string]node, a []string) error {
|
||||||
|
from, err := time.Parse(dayLayout, a[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("date(from,to,layout): from %q is not a YYYY-MM-DD date", a[0])
|
||||||
|
}
|
||||||
|
to, err := time.Parse(dayLayout, a[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("date(from,to,layout): to %q is not a YYYY-MM-DD date", a[1])
|
||||||
|
}
|
||||||
|
if !from.Before(to) {
|
||||||
|
return fmt.Errorf("date(from,to,layout): from %s is not before to %s", a[0], a[1])
|
||||||
|
}
|
||||||
|
_, err = layoutArg(a[2])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeArg(_ map[string]node, a []string) error {
|
||||||
|
layout, err := layoutArg(a[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if layoutProbe.Format(layout) != layoutDay.Format(layout) {
|
||||||
|
return fmt.Errorf("time(layout): '%s' names a date field; write date(from,to,layout)", layout)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// datePrep draws a second in [from 00:00:00, to 23:59:59] UTC; the span is counted
|
||||||
|
// in seconds, since a Duration overflows past 292 years.
|
||||||
|
func datePrep(a []string) callFn {
|
||||||
|
from, err := time.Parse(dayLayout, a[0])
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", a[0], err))
|
||||||
|
}
|
||||||
|
to, err := time.Parse(dayLayout, a[1])
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", a[1], err))
|
||||||
|
}
|
||||||
|
layout, start, span := layoutOf(a[2]), from.Unix(), int(to.Unix()-from.Unix())+86400
|
||||||
|
return func(s *session, _ string, _ []string) string {
|
||||||
|
return time.Unix(start+int64(s.IntN(span)), 0).UTC().Format(layout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func timePrep(a []string) callFn {
|
||||||
|
layout := layoutOf(a[0])
|
||||||
|
return func(s *session, _ string, _ []string) string {
|
||||||
|
return time.Unix(int64(s.IntN(86400)), 0).UTC().Format(layout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -222,8 +222,8 @@ func (t *table) bindOptions(o tableOptionValues) error {
|
|||||||
}
|
}
|
||||||
*opt.into = i
|
*opt.into = i
|
||||||
}
|
}
|
||||||
if t.name >= 0 && t.key < 0 {
|
if t.name >= 0 && t.key < 0 && t.parent < 0 {
|
||||||
return fmt.Errorf("name selects a row as a key does, and lists the rows it matches by their keys, so it needs a key column; add key")
|
return fmt.Errorf("name selects a row as a key does, and lists the rows it matches by their keys, so it needs a key column, or a parent inside which each name is one row; add key")
|
||||||
}
|
}
|
||||||
if err := t.indexKeys(); err != nil {
|
if err := t.indexKeys(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -231,9 +231,18 @@ func (t *table) bindOptions(o tableOptionValues) error {
|
|||||||
return t.sumWeights()
|
return t.sumWeights()
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexKeys proves every key names one row, and keeps the index a link is proved by.
|
// indexKeys proves every key names one row, and keeps the index a link is proved by;
|
||||||
|
// without a key, a name names one row inside its parent.
|
||||||
func (t *table) indexKeys() error {
|
func (t *table) indexKeys() error {
|
||||||
if t.key < 0 {
|
if t.key < 0 {
|
||||||
|
inside := make(map[string]int, t.rows())
|
||||||
|
for r := 0; r < t.rows() && t.name >= 0; r++ {
|
||||||
|
k := t.cell(r, t.parent) + "\t" + t.cell(r, t.name)
|
||||||
|
if first, dup := inside[k]; dup {
|
||||||
|
return fmt.Errorf("%s line %d: name %q repeats line %d inside %s %q; a name selects one row inside its parent", t.file, r+2, t.cell(r, t.name), first+2, t.columns[t.parent], t.cell(r, t.parent))
|
||||||
|
}
|
||||||
|
inside[k] = r
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
t.byKey = make(map[string]int, t.rows())
|
t.byKey = make(map[string]int, t.rows())
|
||||||
@@ -488,8 +497,8 @@ func (t *table) under(r int, a *table, pr int) bool {
|
|||||||
// find is the row a selector names: by key first, then by name, where a name
|
// find is the row a selector names: by key first, then by name, where a name
|
||||||
// naming several rows resolves inside the ancestors pinned in d.
|
// naming several rows resolves inside the ancestors pinned in d.
|
||||||
func (t *table) find(sel string, d *draws) (int, error) {
|
func (t *table) find(sel string, d *draws) (int, error) {
|
||||||
if t.key < 0 {
|
if t.key < 0 && t.name < 0 {
|
||||||
return 0, fmt.Errorf("%s has no key column to select a row by", t.category)
|
return 0, fmt.Errorf("%s has no key or name column to select a row by", t.category)
|
||||||
}
|
}
|
||||||
if r, ok := t.byKey[sel]; ok {
|
if r, ok := t.byKey[sel]; ok {
|
||||||
return r, nil
|
return r, nil
|
||||||
@@ -506,7 +515,14 @@ func (t *table) find(sel string, d *draws) (int, error) {
|
|||||||
}
|
}
|
||||||
keys := make([]string, len(rows))
|
keys := make([]string, len(rows))
|
||||||
for i, r := range rows {
|
for i, r := range rows {
|
||||||
keys[i] = t.cell(r, t.key)
|
if t.key < 0 {
|
||||||
|
keys[i] = t.selectorSpelling(r)
|
||||||
|
} else {
|
||||||
|
keys[i] = t.cell(r, t.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.key < 0 {
|
||||||
|
return 0, fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %v", sel, len(rows), t.category, t.parentT.category, keys)
|
||||||
}
|
}
|
||||||
inside := ""
|
inside := ""
|
||||||
if t.parentT != nil {
|
if t.parentT != nil {
|
||||||
@@ -515,7 +531,14 @@ func (t *table) find(sel string, d *draws) (int, error) {
|
|||||||
return 0, fmt.Errorf("%q names %d rows of %s; select one by key, one of %v%s", sel, len(rows), t.category, keys, inside)
|
return 0, fmt.Errorf("%q names %d rows of %s; select one by key, one of %v%s", sel, len(rows), t.category, keys, inside)
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectorSpelling is how a path writes a selected row, for messages.
|
// selectorSpelling is how a path writes a selected row, for messages: by key, or
|
||||||
|
// by name inside its parent's row.
|
||||||
func (t *table) selectorSpelling(r int) string {
|
func (t *table) selectorSpelling(r int) string {
|
||||||
return t.category + "[" + t.cell(r, t.key) + "]"
|
switch {
|
||||||
|
case t.key >= 0:
|
||||||
|
return t.category + "[" + t.cell(r, t.key) + "]"
|
||||||
|
case t.name >= 0:
|
||||||
|
return t.parentT.selectorSpelling(t.parentRow(r)) + "." + t.category + "[" + t.cell(r, t.name) + "]"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s line %d", t.file, r+2)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-6
@@ -88,17 +88,28 @@ func funcCall(body string) (name string, args []string, ok bool) {
|
|||||||
return body[:lp], splitArgs(body[lp+1 : len(body)-1]), true
|
return body[:lp], splitArgs(body[lp+1 : len(body)-1]), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitArgs parses a function arg list: comma-separated outside a selector,
|
// splitArgs parses a function arg list: comma-separated outside a selector or a
|
||||||
// trimmed; empty -> none.
|
// quoted layout, trimmed; empty -> none.
|
||||||
func splitArgs(s string) []string {
|
func splitArgs(s string) []string {
|
||||||
if strings.TrimSpace(s) == "" {
|
if strings.TrimSpace(s) == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
args := splitOutside(s, ',')
|
var args []string
|
||||||
for i := range args {
|
depth, quoted, start := 0, false, 0
|
||||||
args[i] = strings.TrimSpace(args[i])
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch c := s[i]; {
|
||||||
|
case c == '\'':
|
||||||
|
quoted = !quoted
|
||||||
|
case quoted:
|
||||||
|
case c == '[':
|
||||||
|
depth++
|
||||||
|
case c == ']' && depth > 0:
|
||||||
|
depth--
|
||||||
|
case c == ',' && depth == 0:
|
||||||
|
args, start = append(args, strings.TrimSpace(s[start:i])), i+1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return args
|
return append(args, strings.TrimSpace(s[start:]))
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkFunc validates a function token at compile time: well-formed, naming a
|
// checkFunc validates a function token at compile time: well-formed, naming a
|
||||||
|
|||||||
Reference in New Issue
Block a user