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:
@@ -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...)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user