Move the builtins, with their checksums, layouts and transforms, into internal/builtinfunc, and check calc's operands in the engine

This commit is contained in:
2026-10-05 15:30:19 +02:00
committed by Lillem4n
parent 9357abb279
commit 3bb96ded91
11 changed files with 298 additions and 238 deletions
+122
View File
@@ -0,0 +1,122 @@
// Package builtinfunc holds the {name(args)} functions a format calls: their checks, the
// draws they make, and the numbers their text is proven to read as.
package builtinfunc
import (
"fmt"
"slices"
"strings"
"github.com/larvit/fejkdata/internal/datatype"
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/proven"
)
// Call is a builtin prepared for one call site: its args already parsed. It reads the
// output emitted so far in the current expansion (a derivation's payload) and the
// values of the operands it named, which the caller read for it.
type Call func(s *drawstate.State, emitted string, operands []string) string
// builtin is a format-string function invoked as {name(args)}. It receives the
// draw state, the output emitted so far in the current expansion (for derivations
// such as a checksum over preceding digits), and the values of the operands it named
// (calc and the transforms name them). All must stay pure over (draw state, emitted, operands) so
// seeded output is reproducible. arity is the exact arg count, or -1 for variadic (then
// checkArgs does all the validation).
type builtin struct {
arity int
// prep parses validated args once, at compile time, into the closure a render calls.
prep func(args []string) Call
checkArgs func(args []string) error
// operands names the fields the call reads, which a render reads for it; nil
// for a builtin that reads none.
operands func(args []string) []string
// noRefOperands says no operand can be a reference, so a name read once there is not refused.
noRefOperands bool
// proveNumber bounds the number a call's text reads as, token its body, and says which
// datatypes that text is not; set it where every render reads as a finite number, which
// makes the call a calc operand, and leave it nil otherwise.
proveNumber func(token string, prints datatype.DataType, args []string) proven.Value
// prints is the datatype a call's text is, handed to proveNumber: datatype.String where it
// reads as a number no column should type, as digits' leading zeros; unset where
// proveNumber is nil.
prints datatype.DataType
}
// rng is the randomness a builtin sample draws from, which *drawstate.State satisfies.
type rng interface {
IntN(n int) int
Float64() float64
}
// Check validates a call at compile time: naming a known builtin, with the arg count that
// builtin takes and args its check accepts.
func Check(name string, args []string) error {
b, known := builtins[name]
if !known {
return fmt.Errorf("unknown function %q", name)
}
if b.arity >= 0 && len(args) != b.arity {
return fmt.Errorf("%s takes %d argument%s, got %d", name, b.arity, plural(b.arity), len(args))
}
if b.checkArgs != nil {
return b.checkArgs(args)
}
return nil
}
// Prep parses a call's args, which Check accepted, into the Call a render makes.
func Prep(name string, args []string) Call { return builtins[name].prep(args) }
// Operands lists the fields a call reads as operands, empty for a builtin that reads none
// or is unknown.
func Operands(name string, args []string) []string {
b, known := builtins[name]
if !known || b.operands == nil {
return nil
}
return b.operands(args)
}
// NoRefOperands reports a builtin no operand of which can be a reference.
func NoRefOperands(name string) bool { return builtins[name].noRefOperands }
// IsTransform reports a builtin that rewrites the text of its one operand.
func IsTransform(name string) bool {
_, isTransform := transforms[name]
return isTransform
}
// ProveNumber proves a call whose every render reads as a finite number, token its body;
// false for any other call. A calc's proof reads its operands, which the caller proves.
func ProveNumber(name, token string, args []string) (proven.Value, bool) {
b := builtins[name]
if b.proveNumber == nil {
return proven.Value{}, false
}
return b.proveNumber(token, b.prints, args), true
}
// NumberCalls lists the calls printing a number, or with text also those whose text is
// one, as an error names them.
func NumberCalls(text bool) string {
var calls []string
for name, b := range builtins {
if b.proveNumber != nil && (text || b.prints != datatype.String) {
calls = append(calls, "{"+name+"()}")
}
}
slices.Sort(calls)
return strings.Join(calls, ", ") + " or {calc()}"
}
func plural(n int) string {
if n == 1 {
return ""
}
return "s"
}
func internalError(format string, a ...any) string {
return "fejkdata: internal error: " + fmt.Sprintf(format, a...)
}
+279
View File
@@ -0,0 +1,279 @@
package builtinfunc
import (
"encoding/base64"
"errors"
"fmt"
"math"
"strconv"
"strings"
"github.com/larvit/fejkdata/internal/datatype"
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/proven"
)
// maxLen caps sample output lengths (hex, nanoid, base64, digits, upper, lower)
// and maxDecimals float/calc decimal places, so a fat-fingered or overflowing
// argument fails at New instead of trying to allocate gigabytes — or panicking —
// at render.
const (
maxLen = 1 << 20
maxDecimals = 1024
)
// builtins is the registry of {name(args)} functions. Derivations read the digits
// emitted so far in the current expansion (place them after their payload);
// samples read only the rng. A time-based id (uuid v7, ulid) draws its timestamp
// from the rng, not the wall clock, so seeded output stays reproducible.
var builtins = withTransforms(map[string]builtin{
"luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })},
"mod11": {arity: 0, prep: derive(mod11Check)},
"ean": {arity: 0, prep: derive(eanCheck)},
"uuid": {arity: 0, prep: sample(uuidV7)},
"ulid": {arity: 0, prep: sample(ulid)},
"nanoid": {arity: 1, checkArgs: posIntArg, prep: chars(nanoidAlphabet)},
"hex": {arity: 1, checkArgs: posIntArg, prep: chars(hexDigits)},
"digits": {arity: 1, checkArgs: posIntArg, prep: chars("0123456789"), prints: datatype.String, proveNumber: func(token string, prints datatype.DataType, a []string) proven.Value {
return proven.Printing(token, prints, proven.Bounded(0, math.Pow(10, float64(atoi(a[0])))-1, true))
}},
"upper": {arity: 1, checkArgs: posIntArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
"lower": {arity: 1, checkArgs: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")},
"base64": {arity: 1, checkArgs: posIntArg, prep: func(a []string) Call {
n := atoi(a[0])
return func(s *drawstate.State, _ string, _ []string) string {
return base64.StdEncoding.EncodeToString(randBytes(s, n))
}
}},
"int": {arity: 2, checkArgs: intRangeArgs, prep: func(a []string) Call {
lo, span := atoi(a[0]), atoi(a[1])-atoi(a[0])+1
return func(s *drawstate.State, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) }
}, prints: datatype.Integer, proveNumber: func(token string, prints datatype.DataType, a []string) proven.Value {
return proven.Printing(token, prints, proven.Bounded(float64(atoi(a[0])), float64(atoi(a[1])), true))
}},
"float": {arity: 3, checkArgs: floatArgs, prep: func(a []string) Call {
lo, hi, dp := atof(a[0]), atof(a[1]), atoi(a[2])
return func(s *drawstate.State, _ string, _ []string) string {
return formatFloat(lo+s.Float64()*(hi-lo), dp)
}
}, prints: datatype.Number, proveNumber: func(token string, _ datatype.DataType, a []string) proven.Value {
return proven.PrintedNumber(token, proven.Bounded(atof(a[0]), atof(a[1]), false), atoi(a[2]))
}},
"iban": {arity: 1, checkArgs: ibanArg, prep: func(a []string) Call {
cc := a[0]
return func(s *drawstate.State, _ string, _ []string) string { return iban(s, cc) }
}},
"date": {arity: -1, checkArgs: dateArgs, prep: datePrep},
"time": {arity: -1, checkArgs: timeArg, prep: timePrep},
"calc": {arity: -1, checkArgs: checkCalc, prep: calcPrep, operands: calcOperands, noRefOperands: true},
// seq is the one stateful builtin: a per-generator counter from 1, advancing on
// each call. An optional name selects an independent counter; no name uses the
// default one. Deterministic by construction, so seeded output stays stable.
"seq": {arity: -1, checkArgs: seqArg, prep: func(a []string) Call {
key := ""
if len(a) == 1 {
key = a[0]
}
return func(s *drawstate.State, _ string, _ []string) string {
return strconv.FormatUint(s.Seq(key), 10)
}
}, prints: datatype.Integer, proveNumber: func(token string, prints datatype.DataType, _ []string) proven.Value {
return proven.Printing(token, prints, proven.Bounded(1, math.MaxInt64, true))
}},
})
// derive and sample are the two argument-free builtin shapes: a derivation reads
// the output emitted so far, a sample reads only the rng. chars is the shape of a
// sample of n characters drawn from an alphabet.
func derive(f func(emitted string) string) func([]string) Call {
return func([]string) Call {
return func(_ *drawstate.State, emitted string, _ []string) string { return f(emitted) }
}
}
func sample(f func(rng) string) func([]string) Call {
return func([]string) Call {
return func(s *drawstate.State, _ string, _ []string) string { return f(s) }
}
}
func chars(alphabet string) func([]string) Call {
return func(a []string) Call {
n := atoi(a[0])
return func(s *drawstate.State, _ string, _ []string) string { return randChars(s, n, alphabet) }
}
}
const hexDigits = "0123456789abcdef"
// formatFloat prints v to dp decimals, or shortestDecimals, and a zero unsigned.
func formatFloat(v float64, dp int) string {
s := strconv.FormatFloat(v, 'f', dp, 64)
if strings.HasPrefix(s, "-") && strings.Trim(s, "-0.") == "" {
return s[1:]
}
return s
}
// atoi parses an arg a builtin's check already validated. It panics rather than
// returning zero, so a check that stops covering its own args is a stack trace and
// not a silently wrong length, range or decimal count.
func atoi(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(internalError("builtin arg %q reached prep unvalidated: %v", s, err))
}
return n
}
// atof is atoi for a float arg, and reports an unvalidated one the same way.
func atof(s string) float64 {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
panic(internalError("builtin arg %q reached prep unvalidated: %v", s, err))
}
return f
}
func randBytes(r rng, n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = byte(r.IntN(256))
}
return b
}
func randChars(r rng, n int, alphabet string) string {
b := make([]byte, n)
for i := range b {
b[i] = alphabet[r.IntN(len(alphabet))]
}
return string(b)
}
// plainInt parses an integer arg written the one way: no sign, no leading zero.
func plainInt(s string) (int, error) {
n, err := strconv.Atoi(s)
if errors.Is(err, strconv.ErrRange) {
return 0, fmt.Errorf("%q is past the integer range: %w", s, err)
}
if err != nil {
return 0, fmt.Errorf("%q is not an integer", s)
}
if strconv.Itoa(n) != s {
return 0, fmt.Errorf("%q is not a plain integer; write %d", s, n)
}
return n, nil
}
func posIntArg(a []string) error {
n, err := plainInt(a[0])
if errors.Is(err, strconv.ErrRange) {
return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen)
}
if err != nil {
return fmt.Errorf("count %w", err)
}
if n < 1 {
return fmt.Errorf("count %q must be positive", a[0])
}
if n > maxLen {
return fmt.Errorf("count %d exceeds the maximum %d", n, maxLen)
}
return nil
}
func intRangeArgs(a []string) error {
lo, err := plainInt(a[0])
if err != nil {
return fmt.Errorf("int(min,max): min %w", err)
}
hi, err := plainInt(a[1])
if err != nil {
return fmt.Errorf("int(min,max): max %w", err)
}
if lo > hi {
return fmt.Errorf("int(min,max): min %d > max %d", lo, hi)
}
if lo == hi {
return fmt.Errorf("int(%d,%d) is the constant %d; write it as text", lo, hi, lo)
}
if uint64(hi)-uint64(lo) >= uint64(math.MaxInt64) { // span hi-lo+1 would overflow int -> IntN panic
return fmt.Errorf("int(min,max): range %d..%d is too wide", lo, hi)
}
return nil
}
func floatArgs(a []string) error {
lo, e1 := strconv.ParseFloat(a[0], 64)
hi, e2 := strconv.ParseFloat(a[1], 64)
if e1 != nil || e2 != nil {
return fmt.Errorf("float(min,max,dp) needs numeric bounds, got %q,%q", a[0], a[1])
}
dp, err := plainInt(a[2])
if err != nil {
return fmt.Errorf("float(min,max,dp): decimals %w", err)
}
if math.IsNaN(lo) || math.IsNaN(hi) || math.IsInf(lo, 0) || math.IsInf(hi, 0) {
return fmt.Errorf("float(min,max,dp) needs finite bounds, got %q,%q", a[0], a[1])
}
if lo > hi {
return fmt.Errorf("float(min,max,dp): min %v > max %v", lo, hi)
}
if dp < 0 || dp > maxDecimals {
return fmt.Errorf("float(min,max,dp): decimals %d out of range 0..%d", dp, maxDecimals)
}
if lo == hi {
return fmt.Errorf("float(%s,%s,%d) is the constant %q; write it as text", a[0], a[1], dp, strconv.FormatFloat(lo, 'f', dp, 64))
}
if math.IsInf(hi-lo, 0) { // an overflowing span would render as "+Inf"
return fmt.Errorf("float(min,max,dp): range %v..%v is too wide", lo, hi)
}
return nil
}
func seqArg(a []string) error {
if len(a) > 1 {
return fmt.Errorf("seq takes at most one name, got %d", len(a))
}
if len(a) == 1 && a[0] == "" {
return fmt.Errorf("seq name must not be empty")
}
return nil
}
// nanoidAlphabet is the 64-char URL-safe set Nano IDs use.
const nanoidAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// uuidV7 builds an RFC 9562 v7 UUID. The 48-bit timestamp field is drawn from
// the rng (not the clock) to stay reproducible, then the version (7) and variant
// (10) bits are forced; the rest is random.
func uuidV7(r rng) string {
b := randBytes(r, 16)
b[6] = b[6]&0x0f | 0x70
b[8] = b[8]&0x3f | 0x80
var sb strings.Builder
for i, x := range b {
if i == 4 || i == 6 || i == 8 || i == 10 {
sb.WriteByte('-')
}
sb.WriteByte(hexDigits[x>>4])
sb.WriteByte(hexDigits[x&0x0f])
}
return sb.String()
}
// crockford is the ULID/Crockford base32 alphabet (no I, L, O, U).
const crockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
// ulid builds a 26-char ULID: 128 random bits (timestamp from the rng) encoded
// big-endian into base32, the 130-bit stream left-padded with two zero bits.
func ulid(r rng) string {
b := randBytes(r, 16)
out := make([]byte, 26)
for i := range out {
v := 0
for j := 0; j < 5; j++ { // 5 bits per char; the two leading pad bits are zero
real := 5*i + j - 2
bit := 0
if real >= 0 {
bit = int(b[real/8]>>(7-uint(real%8))) & 1
}
v = v<<1 | bit
}
out[i] = crockford[v]
}
return string(out)
}
+108
View File
@@ -0,0 +1,108 @@
package builtinfunc
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/proven"
)
// evalCalc evaluates an expression over the operand values a render already read, so it
// draws nothing.
func evalCalc(n grammar.CalcNode, operands []string) float64 {
switch n := n.(type) {
case grammar.CalcNum:
return float64(n)
case grammar.CalcVar:
v, err := strconv.ParseFloat(strings.TrimSpace(operands[n.At]), 64)
if err != nil {
return math.NaN() // a non-numeric operand stays visible, never an error
}
return v
case grammar.CalcNeg:
return -evalCalc(n.X, operands)
case grammar.CalcBin:
return Arith(n.Operator, evalCalc(n.L, operands), evalCalc(n.R, operands))
}
panic(internalError("calc node %#v has no evaluation", n))
}
// Arith applies a calc operator.
func Arith(operator byte, l, r float64) float64 {
switch operator {
case '+':
return l + r
case '-':
return l - r
case '*':
return l * r
default: // '/'
return l / r
}
}
// checkCalc validates a calc token's own args: a parseable expression and an optional
// non-negative integer dp. What its operands hold is the caller's to check.
func checkCalc(args []string) error {
if len(args) < 1 || len(args) > 2 {
return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args))
}
if _, err := grammar.ParseCalc(args[0]); err != nil {
return fmt.Errorf("calc(%q): %w", args[0], err)
}
if len(args) == 2 {
dp, err := plainInt(args[1])
if err != nil {
return fmt.Errorf("calc decimals %w", err)
}
if dp < 0 || dp > maxDecimals {
return fmt.Errorf("calc decimals %d must be in 0..%d", dp, maxDecimals)
}
}
return nil
}
// calcPrep parses the expression and decimals once, at compile time. checkCalc proved
// both args valid, so no step here can fail.
func calcPrep(args []string) Call {
expr := ParsedCalc(args[0]).Expr
dp := CalcDecimals(args)
return func(_ *drawstate.State, _ string, operands []string) string {
return formatFloat(evalCalc(expr, operands), dp)
}
}
// ParsedCalc parses an expression Check accepted.
func ParsedCalc(expr string) grammar.Calc {
c, err := grammar.ParseCalc(expr)
if err != nil {
panic(internalError("calc(%q) passed its check unparsed: %v", expr, err))
}
return c
}
// CalcDecimals is the decimals count of a calc Check accepted, or proven.ShortestDecimals
// where it names none.
func CalcDecimals(args []string) int {
if len(args) == 2 {
return atoi(args[1])
}
return proven.ShortestDecimals
}
// calcOperands lists the operands a calc's args read, fields or names. checkCalc reports
// an expression that does not parse, so one that does not simply names nothing.
func calcOperands(args []string) []string {
if len(args) == 0 {
return nil
}
c, err := grammar.ParseCalc(args[0])
if err != nil {
return nil
}
return c.Operands
}
+96
View File
@@ -0,0 +1,96 @@
package builtinfunc
import "fmt"
// luhnCheck returns the Luhn check digit (0-9) over the digits of s; non-digit
// runes are skipped. Doubling runs from the rightmost digit, so the result is
// correct whatever the payload length.
func luhnCheck(s string) int {
sum, double := 0, true
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
if c < '0' || c > '9' {
continue
}
d := int(c - '0')
if double {
if d *= 2; d > 9 {
d -= 9
}
}
double = !double
sum += d
}
return (10 - sum%10) % 10
}
// mod11Check returns the weighted mod-11 check character over the digits of s
// (weights 2..7 cycling from the right). A would-be value of 10 emits 'X', as in
// ISBN-10 / ISO 7064; non-digits are skipped.
func mod11Check(s string) string {
sum, w := 0, 2
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
if c < '0' || c > '9' {
continue
}
sum += int(c-'0') * w
if w++; w > 7 {
w = 2
}
}
if chk := (11 - sum%11) % 11; chk != 10 {
return string(rune('0' + chk))
}
return "X"
}
// eanCheck returns the EAN-13 / UPC-A / ISBN-13 / GTIN check digit over the
// digits of s: weights 3 and 1 alternating from the rightmost digit, mod 10.
func eanCheck(s string) string {
sum, w := 0, 3
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
if c < '0' || c > '9' {
continue
}
sum += int(c-'0') * w
w = 4 - w // 3 <-> 1
}
return string(rune('0' + (10-sum%10)%10))
}
// ibanLen maps a supported country code to the full IBAN length. The check digits
// sit between the country code and the BBAN, so — unlike luhn/ean — iban can't be
// a left-to-right derivation; it generates the whole value instead.
var ibanLen = map[string]int{"BE": 16, "DE": 22, "DK": 18, "ES": 24, "FI": 18, "NO": 15, "SE": 24}
func ibanArg(a []string) error {
if _, ok := ibanLen[a[0]]; !ok {
return fmt.Errorf("iban(%q): unsupported country code", a[0])
}
return nil
}
// iban generates a structurally valid IBAN for cc: a numeric BBAN of the right
// length, then mod-97 check digits. Real bank/branch structure isn't modelled —
// the result passes length and checksum validation, which is what fake data needs.
func iban(r rng, cc string) string {
bban := make([]byte, ibanLen[cc]-4)
for i := range bban {
bban[i] = byte('0' + r.IntN(10))
}
rem := 0
feed := func(d int) { rem = (rem*10 + d) % 97 }
for _, c := range bban {
feed(int(c - '0'))
}
for i := 0; i < len(cc); i++ { // letters A-Z -> 10..35, fed as two digits
v := int(cc[i]-'A') + 10
feed(v / 10)
feed(v % 10)
}
feed(0)
feed(0)
return fmt.Sprintf("%s%02d%s", cc, 98-rem, bban)
}
+149
View File
@@ -0,0 +1,149 @@
package builtinfunc
import (
"fmt"
"strings"
"time"
"github.com/larvit/fejkdata/internal/drawstate"
)
const dayLayout = "2006-01-02"
// 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)
layoutDay = time.Date(2010, 11, 12, 4, 5, 6, 0, time.UTC)
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] == '\''
}
// layoutArg is the Go layout a quoted arg holds, refused when unquoted or constant.
// docs/decisions.md#a-layout-is-always-quoted
func layoutArg(a string) (string, error) {
if !quotedLayout(a) {
bare := strings.Trim(a, `'"`)
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'", a, bare)
}
layout := a[1 : len(a)-1]
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
}
// layoutOf is layoutArg for an arg a check already validated.
func layoutOf(a string) string {
layout, err := layoutArg(a)
if err != nil {
panic(internalError("builtin arg %q reached prep unvalidated: %v", a, err))
}
return layout
}
// layoutArity checks a call ending in a layout takes n args, naming the quoted
// layout where an unquoted one split into more.
func layoutArity(name string, n int, a []string) error {
if len(a) == n {
return nil
}
hint := ""
if len(a) > n && !holdsQuotedLayout(a[n-1:]) {
hint = fmt.Sprintf("; a layout holding a comma is quoted: '%s'", strings.Trim(strings.Join(a[n-1:], ", "), `'"`))
}
return fmt.Errorf("%s takes %d argument%s, got %d%s", name, n, plural(n), len(a), hint)
}
// 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
}
}
return false
}
func dateArgs(a []string) error {
if err := layoutArity("date", 3, a); err != nil {
return err
}
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 to.Before(from) {
return fmt.Errorf("date(from,to,layout): from %s is after to %s", a[0], a[1])
}
layout, err := layoutArg(a[2])
if err != nil {
return err
}
if !namesADateField(layout) {
return fmt.Errorf("date(from,to,layout): '%s' names no date field; write time('%s')", layout, 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
}
func timeArg(a []string) error {
if err := layoutArity("time", 1, a); err != nil {
return err
}
layout, err := layoutArg(a[0])
if err != nil {
return err
}
if namesADateField(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) Call {
from, err := time.Parse(dayLayout, a[0])
if err != nil {
panic(internalError("builtin arg %q reached prep unvalidated: %v", a[0], err))
}
to, err := time.Parse(dayLayout, a[1])
if err != nil {
panic(internalError("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 *drawstate.State, _ string, _ []string) string {
return time.Unix(start+int64(s.IntN(span)), 0).UTC().Format(layout)
}
}
func timePrep(a []string) Call {
layout := layoutOf(a[0])
return func(s *drawstate.State, _ string, _ []string) string {
return time.Unix(int64(s.IntN(86400)), 0).UTC().Format(layout)
}
}
+105
View File
@@ -0,0 +1,105 @@
package builtinfunc
import (
"fmt"
"strings"
"unicode"
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
)
// transforms are the builtins that rewrite one operand's value; they nest, so
// {lowercase(ascii(x))} folds then lowers.
var transforms = map[string]func(string) string{
"ascii": asciiFold,
"lowercase": strings.ToLower,
"uppercase": strings.ToUpper,
}
func withTransforms(calls map[string]builtin) map[string]builtin {
for name, fn := range transforms {
if _, clash := calls[name]; clash {
panic(internalError("%s is registered as a builtin and a transform", name))
}
calls[name] = builtin{arity: 1, checkArgs: transformArg, prep: transformPrep(fn), operands: transformOperand}
}
return calls
}
// unwrapTransform peels nested transform calls off an operand arg, returning the
// field it finally names and the transforms to apply, innermost last.
func unwrapTransform(arg string) (leaf string, chain []func(string) string, err error) {
for {
name, args, isCall := grammar.FuncCall(arg)
if !isCall {
return arg, chain, nil
}
fn, isTransform := transforms[name]
if !isTransform {
return "", nil, fmt.Errorf("%s(%s) is not a transform, so it cannot be an operand", name, strings.Join(args, ","))
}
if len(args) != 1 {
return "", nil, fmt.Errorf("%s takes 1 arg, got %d", name, len(args))
}
chain = append(chain, fn)
arg = args[0]
}
}
// transformArg proves a transform's chain; checkReads proves the operand it reads.
func transformArg(a []string) error {
_, _, err := unwrapTransform(a[0])
return err
}
func transformOperand(a []string) []string {
if len(a) != 1 {
return nil
}
leaf, _, err := unwrapTransform(a[0])
if err != nil {
return nil
}
return []string{leaf}
}
func transformPrep(outer func(string) string) func([]string) Call {
return func(a []string) Call {
_, chain, err := unwrapTransform(a[0])
if err != nil {
panic(internalError("transform arg %q reached prep unvalidated: %v", a[0], err))
}
return func(_ *drawstate.State, _ string, operands []string) string {
v := operands[0]
for i := len(chain) - 1; i >= 0; i-- {
v = chain[i](v)
}
return outer(v)
}
}
}
// asciiFolds maps the Latin letters with diacritics or ligatures to ASCII.
var asciiFolds = map[rune]string{
'À': "A", 'Á': "A", 'Â': "A", 'Ã': "A", 'Ä': "A", 'Å': "A", 'Æ': "AE", 'Ç': "C",
'È': "E", 'É': "E", 'Ê': "E", 'Ë': "E", 'Ì': "I", 'Í': "I", 'Î': "I", 'Ï': "I",
'Ð': "D", 'Ñ': "N", 'Ò': "O", 'Ó': "O", 'Ô': "O", 'Õ': "O", 'Ö': "O", 'Ø': "O",
'Ù': "U", 'Ú': "U", 'Û': "U", 'Ü': "U", 'Ý': "Y", 'Þ': "Th", 'ß': "ss", 'Œ': "OE",
'à': "a", 'á': "a", 'â': "a", 'ã': "a", 'ä': "a", 'å': "a", 'æ': "ae", 'ç': "c",
'è': "e", 'é': "e", 'ê': "e", 'ë': "e", 'ì': "i", 'í': "i", 'î': "i", 'ï': "i",
'ð': "d", 'ñ': "n", 'ò': "o", 'ó': "o", 'ô': "o", 'õ': "o", 'ö': "o", 'ø': "o",
'ù': "u", 'ú': "u", 'û': "u", 'ü': "u", 'ý': "y", 'þ': "th", 'ÿ': "y", 'œ': "oe",
}
// asciiFold rewrites s to ASCII: folded Latin letters stay, any other non-ASCII
// rune is dropped.
func asciiFold(s string) string {
var b strings.Builder
for _, r := range s {
if r <= unicode.MaxASCII {
b.WriteRune(r)
} else {
b.WriteString(asciiFolds[r])
}
}
return b.String()
}