Add {calc()}: arithmetic over number literals and sibling fields
This commit is contained in:
@@ -244,10 +244,11 @@ reproducible. A time-based id (UUID v7, ULID) therefore draws its timestamp from
|
|||||||
the rng, not the clock — the result is a valid, reproducible value, not a real
|
the rng, not the clock — the result is a valid, reproducible value, not a real
|
||||||
point in time.
|
point in time.
|
||||||
|
|
||||||
There are three kinds. **Derivations** read the digits emitted so far, so put them
|
There are four kinds. **Derivations** read the digits emitted so far, so put them
|
||||||
after their payload; **generators** read only the rng, so they stand alone; one
|
after their payload; **generators** read only the rng, so they stand alone; one
|
||||||
**session counter** (`seq`) advances state held on the faker. Arguments are
|
**session counter** (`seq`) advances state held on the faker; and one
|
||||||
validated at `New` (a bad count, range, or country fails fast).
|
**computation** (`calc`) evaluates arithmetic over sibling fields. Arguments are
|
||||||
|
validated at `New` (a bad count, range, country, or expression fails fast).
|
||||||
|
|
||||||
| Function | Kind | Emits |
|
| Function | Kind | Emits |
|
||||||
|----------|------|-------|
|
|----------|------|-------|
|
||||||
@@ -264,6 +265,7 @@ validated at `New` (a bad count, range, or country fails fast).
|
|||||||
| `{float(min,max,dp)}` | generator | number in `[min, max]` with `dp` decimals |
|
| `{float(min,max,dp)}` | generator | number in `[min, max]` with `dp` decimals |
|
||||||
| `{iban(CC)}` | generator | a length- and mod-97-valid IBAN for country `CC` (BE, DE, DK, ES, FI, NO, SE) |
|
| `{iban(CC)}` | generator | a length- and mod-97-valid IBAN for country `CC` (BE, DE, DK, ES, FI, NO, SE) |
|
||||||
| `{seq()}`, `{seq(name)}` | session counter | next integer (from 1) in this faker's sequence; `name` selects an independent counter |
|
| `{seq()}`, `{seq(name)}` | session counter | next integer (from 1) in this faker's sequence; `name` selects an independent counter |
|
||||||
|
| `{calc(expr)}`, `{calc(expr,dp)}` | computation | value of an arithmetic expression over number literals and sibling fields; `dp` rounds |
|
||||||
|
|
||||||
`{ean()}` is also the ISBN-13 check (an ISBN-13 *is* an EAN-13 — build the 978/979
|
`{ean()}` is also the ISBN-13 check (an ISBN-13 *is* an EAN-13 — build the 978/979
|
||||||
prefix in data and call `{ean()}`). `{iban()}` is a generator, not a derivation:
|
prefix in data and call `{ean()}`). `{iban()}` is a generator, not a derivation:
|
||||||
@@ -275,6 +277,23 @@ length and checksum, not real bank routing).
|
|||||||
and resets when you build a new faker — `seq` is reproducible by being ordered,
|
and resets when you build a new faker — `seq` is reproducible by being ordered,
|
||||||
not random. It's the natural fit for a primary-key column in the SQL example above.
|
not random. It's the natural fit for a primary-key column in the SQL example above.
|
||||||
|
|
||||||
|
**Computation.** `{calc(expr)}` evaluates an arithmetic expression — `+ - * /`,
|
||||||
|
parentheses and unary minus, the usual precedence — and emits the result. Operands
|
||||||
|
are number literals and **sibling field names**, each rendered then read as a
|
||||||
|
number; an optional second arg rounds to that many decimals (`{calc(net * qty, 2)}`),
|
||||||
|
otherwise the value prints in minimal form. A hyphen is always subtraction, so a
|
||||||
|
hyphenated field name can't be an operand. The expression is checked at `New`
|
||||||
|
(parse, and that every name is a real field):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "format": "{net} x {qty} = {calc(net * qty, 2)}", "net": ["19.99"], "qty": ["3"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
renders `19.99 x 3 = 59.97`. Each name is rendered where it appears, so a field
|
||||||
|
shown *and* used in a `calc` is drawn twice — keep a shared operand in a
|
||||||
|
single-value field if the two must agree. A field that doesn't render to a number
|
||||||
|
yields `NaN`, which prints rather than failing the render.
|
||||||
|
|
||||||
**References.** A `{..path}` token renders a node from the **data root** instead
|
**References.** A `{..path}` token renders a node from the **data root** instead
|
||||||
of a sibling field — the dot path is the one `Fake` takes, resolved across every
|
of a sibling field — the dot path is the one `Fake` takes, resolved across every
|
||||||
loaded directory. One category can borrow another, even across folders or layered
|
loaded directory. One category can borrow another, even across folders or layered
|
||||||
@@ -380,6 +399,7 @@ docker compose run --rm test # latest
|
|||||||
fakes.go Fakes, New, options, seeding
|
fakes.go Fakes, New, options, seeding
|
||||||
template.go Fake, the recursive renderer (choices, format strings, paths)
|
template.go Fake, the recursive renderer (choices, format strings, paths)
|
||||||
builtins.go the {name()} function registry and its implementations
|
builtins.go the {name()} function registry and its implementations
|
||||||
|
calc.go the {calc()} arithmetic evaluator: parser, eval, validation
|
||||||
data.go data loading: folders/files -> namespace tree, multi-path merge
|
data.go data loading: folders/files -> namespace tree, multi-path merge
|
||||||
cmd/fakes/ the `fakes` CLI (New + Fake over stdout)
|
cmd/fakes/ the `fakes` CLI (New + Fake over stdout)
|
||||||
data/ shipped data (JSON): locale folders + a misc folder
|
data/ shipped data (JSON): locale folders + a misc folder
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
package fakes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// calc is the {calc(expr[, dp])} token: an arithmetic expression over number
|
||||||
|
// literals and sibling-field names (rendered, then parsed as numbers), with
|
||||||
|
// + - * /, unary minus and parentheses. Unlike a builtin it reads the field
|
||||||
|
// environment, so expand and checkTokens route it here rather than the builtins
|
||||||
|
// registry. The value prints in minimal decimal form, or rounded to dp decimals
|
||||||
|
// when given. A field that doesn't render to a number becomes NaN, which
|
||||||
|
// propagates and prints as "NaN" — visible, never a render error. The expression
|
||||||
|
// is re-parsed each render, mirroring how expand re-scans the format; checkCalc
|
||||||
|
// proves it parses (and names real fields) at New, so render never fails.
|
||||||
|
|
||||||
|
// calcNode is a parsed expression node.
|
||||||
|
type calcNode interface {
|
||||||
|
eval(s *session, fields map[string]node) float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type calcNum float64 // a number literal
|
||||||
|
type calcVar string // a sibling-field name
|
||||||
|
type calcNeg struct{ x calcNode }
|
||||||
|
type calcBin struct { // a + - * / b
|
||||||
|
op byte
|
||||||
|
l, r calcNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n calcNum) eval(*session, map[string]node) float64 { return float64(n) }
|
||||||
|
|
||||||
|
func (n calcVar) eval(s *session, fields map[string]node) float64 {
|
||||||
|
v, err := strconv.ParseFloat(strings.TrimSpace(render(s, fields[string(n)])), 64)
|
||||||
|
if err != nil {
|
||||||
|
return math.NaN() // a non-numeric operand stays visible, never an error
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n calcNeg) eval(s *session, fields map[string]node) float64 { return -n.x.eval(s, fields) }
|
||||||
|
|
||||||
|
func (n calcBin) eval(s *session, fields map[string]node) float64 {
|
||||||
|
l, r := n.l.eval(s, fields), n.r.eval(s, fields)
|
||||||
|
switch n.op {
|
||||||
|
case '+':
|
||||||
|
return l + r
|
||||||
|
case '-':
|
||||||
|
return l - r
|
||||||
|
case '*':
|
||||||
|
return l * r
|
||||||
|
default: // '/'
|
||||||
|
return l / r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkCalc validates a calc token at compile time: a parseable expression whose
|
||||||
|
// operands all name existing fields, and an optional non-negative integer dp.
|
||||||
|
func checkCalc(args []string, fields map[string]node) 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))
|
||||||
|
}
|
||||||
|
expr, err := parseCalc(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("calc(%q): %w", args[0], err)
|
||||||
|
}
|
||||||
|
for _, name := range calcVars(expr) {
|
||||||
|
if _, ok := fields[name]; !ok {
|
||||||
|
return fmt.Errorf("calc(%q): no field %q", args[0], name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(args) == 2 {
|
||||||
|
if dp, err := strconv.Atoi(args[1]); err != nil || dp < 0 {
|
||||||
|
return fmt.Errorf("calc decimals %q must be a non-negative integer", args[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcEval renders a calc token. checkCalc proved the expression parses and the
|
||||||
|
// decimals arg is valid, so neither step here can fail. dp -1 prints the minimal
|
||||||
|
// form; a given dp rounds to that many places.
|
||||||
|
func calcEval(s *session, args []string, fields map[string]node) string {
|
||||||
|
expr, _ := parseCalc(args[0])
|
||||||
|
dp := -1
|
||||||
|
if len(args) == 2 {
|
||||||
|
dp = atoi(args[1])
|
||||||
|
}
|
||||||
|
return strconv.FormatFloat(expr.eval(s, fields), 'f', dp, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcVars lists the field names an expression references, for the existence
|
||||||
|
// check in checkCalc.
|
||||||
|
func calcVars(n calcNode) []string {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case calcVar:
|
||||||
|
return []string{string(n)}
|
||||||
|
case calcNeg:
|
||||||
|
return calcVars(n.x)
|
||||||
|
case calcBin:
|
||||||
|
return append(calcVars(n.l), calcVars(n.r)...)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcParser is a recursive-descent parser over the expression runes, threading
|
||||||
|
// expr -> term -> factor for the standard * / before + - precedence.
|
||||||
|
type calcParser struct {
|
||||||
|
rs []rune
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCalc parses a whole expression, requiring it to consume all input.
|
||||||
|
func parseCalc(expr string) (calcNode, error) {
|
||||||
|
p := &calcParser{rs: []rune(expr)}
|
||||||
|
if p.space(); p.pos >= len(p.rs) {
|
||||||
|
return nil, fmt.Errorf("empty expression")
|
||||||
|
}
|
||||||
|
n, err := p.expr()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if p.space(); p.pos != len(p.rs) {
|
||||||
|
return nil, fmt.Errorf("unexpected %q", string(p.rs[p.pos:]))
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *calcParser) space() {
|
||||||
|
for p.pos < len(p.rs) && unicode.IsSpace(p.rs[p.pos]) {
|
||||||
|
p.pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *calcParser) expr() (calcNode, error) { return p.binary(p.term, '+', '-') }
|
||||||
|
func (p *calcParser) term() (calcNode, error) { return p.binary(p.factor, '*', '/') }
|
||||||
|
|
||||||
|
// binary parses a left-associative run of next() operands joined by the given
|
||||||
|
// operators, the one shape expr and term share.
|
||||||
|
func (p *calcParser) binary(next func() (calcNode, error), ops ...byte) (calcNode, error) {
|
||||||
|
n, err := next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
p.space()
|
||||||
|
if p.pos >= len(p.rs) || !contains(ops, byte(p.rs[p.pos])) {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
op := byte(p.rs[p.pos])
|
||||||
|
p.pos++
|
||||||
|
r, err := next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n = calcBin{op, n, r}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *calcParser) factor() (calcNode, error) {
|
||||||
|
p.space()
|
||||||
|
if p.pos >= len(p.rs) {
|
||||||
|
return nil, fmt.Errorf("unexpected end of expression")
|
||||||
|
}
|
||||||
|
switch c := p.rs[p.pos]; {
|
||||||
|
case c == '-':
|
||||||
|
p.pos++
|
||||||
|
x, err := p.factor()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return calcNeg{x}, nil
|
||||||
|
case c == '(':
|
||||||
|
p.pos++
|
||||||
|
n, err := p.expr()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if p.space(); p.pos >= len(p.rs) || p.rs[p.pos] != ')' {
|
||||||
|
return nil, fmt.Errorf("missing ')'")
|
||||||
|
}
|
||||||
|
p.pos++
|
||||||
|
return n, nil
|
||||||
|
case c == '.' || c >= '0' && c <= '9':
|
||||||
|
return p.number()
|
||||||
|
case c == '_' || unicode.IsLetter(c):
|
||||||
|
return p.ident()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected %q", string(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *calcParser) number() (calcNode, error) {
|
||||||
|
start, dot := p.pos, false
|
||||||
|
for p.pos < len(p.rs) {
|
||||||
|
if c := p.rs[p.pos]; c >= '0' && c <= '9' {
|
||||||
|
p.pos++
|
||||||
|
} else if c == '.' && !dot {
|
||||||
|
dot, p.pos = true, p.pos+1
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(string(p.rs[start:p.pos]), 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bad number %q", string(p.rs[start:p.pos]))
|
||||||
|
}
|
||||||
|
return calcNum(v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ident reads a field name: a letter or '_', then letters, digits or '_'. A '-'
|
||||||
|
// is always the minus operator, so a hyphenated field name can't be an operand.
|
||||||
|
func (p *calcParser) ident() (calcNode, error) {
|
||||||
|
start := p.pos
|
||||||
|
for p.pos < len(p.rs) {
|
||||||
|
if c := p.rs[p.pos]; c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c) {
|
||||||
|
p.pos++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calcVar(string(p.rs[start:p.pos])), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(bs []byte, b byte) bool {
|
||||||
|
for _, x := range bs {
|
||||||
|
if x == b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package fakes
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestCalcArithmetic pins the operators, precedence, parentheses and unary minus
|
||||||
|
// over number literals.
|
||||||
|
func TestCalcArithmetic(t *testing.T) {
|
||||||
|
f := engine(1)
|
||||||
|
cases := map[string]string{
|
||||||
|
`{"format":"{calc(2 + 3)}"}`: "5",
|
||||||
|
`{"format":"{calc(2 * 3 + 4)}"}`: "10", // * binds tighter than +
|
||||||
|
`{"format":"{calc(2 + 3 * 4)}"}`: "14",
|
||||||
|
`{"format":"{calc((2 + 3) * 4)}"}`: "20", // parentheses override
|
||||||
|
`{"format":"{calc(10 / 4)}"}`: "2.5",
|
||||||
|
`{"format":"{calc(-2 + 5)}"}`: "3", // unary minus
|
||||||
|
`{"format":"{calc(2 - -3)}"}`: "5",
|
||||||
|
`{"format":"{calc(1.5 * 2)}"}`: "3", // whole result drops the decimals
|
||||||
|
}
|
||||||
|
for tmpl, want := range cases {
|
||||||
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
|
t.Errorf("%s = %q, want %q", tmpl, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCalcAuto pins the default (no-dp) rendering: minimal decimal form, no
|
||||||
|
// scientific notation, whole numbers without a fraction.
|
||||||
|
func TestCalcAuto(t *testing.T) {
|
||||||
|
f := engine(1)
|
||||||
|
cases := map[string]string{
|
||||||
|
`{"format":"{calc(10 / 3)}"}`: "3.3333333333333335",
|
||||||
|
`{"format":"{calc(6 / 2)}"}`: "3",
|
||||||
|
`{"format":"{calc(1 / 4)}"}`: "0.25",
|
||||||
|
}
|
||||||
|
for tmpl, want := range cases {
|
||||||
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
|
t.Errorf("%s = %q, want %q", tmpl, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCalcDecimals pins the optional decimals arg: rounds to dp places, dp 0
|
||||||
|
// drops the fraction.
|
||||||
|
func TestCalcDecimals(t *testing.T) {
|
||||||
|
f := engine(1)
|
||||||
|
cases := map[string]string{
|
||||||
|
`{"format":"{calc(10 / 3, 2)}"}`: "3.33",
|
||||||
|
`{"format":"{calc(10 / 3, 0)}"}`: "3",
|
||||||
|
`{"format":"{calc(2 * 3, 2)}"}`: "6.00",
|
||||||
|
}
|
||||||
|
for tmpl, want := range cases {
|
||||||
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
|
t.Errorf("%s = %q, want %q", tmpl, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCalcFields pins that bare names resolve to sibling fields, rendered then
|
||||||
|
// parsed as numbers.
|
||||||
|
func TestCalcFields(t *testing.T) {
|
||||||
|
if got := mustRender(t, engine(1), `{"format":"{calc(price * qty, 2)}","price":["19.99"],"qty":["3"]}`); got != "59.97" {
|
||||||
|
t.Fatalf("calc over fields = %q, want 59.97", got)
|
||||||
|
}
|
||||||
|
// A field that is itself a template renders before parsing.
|
||||||
|
if got := mustRender(t, engine(1), `{"format":"{calc(a - b)}","a":[{"format":"{n}","n":["10"]}],"b":["3"]}`); got != "7" {
|
||||||
|
t.Fatalf("calc(a - b) = %q, want 7", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCalcNonNumericIsNaN pins the never-fail rule: a field that doesn't render
|
||||||
|
// to a number becomes NaN, which propagates and prints visibly.
|
||||||
|
func TestCalcNonNumericIsNaN(t *testing.T) {
|
||||||
|
if got := mustRender(t, engine(1), `{"format":"{calc(x * 2)}","x":["abc"]}`); got != "NaN" {
|
||||||
|
t.Fatalf("calc over non-numeric field = %q, want NaN", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCalcReproducible pins that a calc over a random operand stays seed-stable.
|
||||||
|
func TestCalcReproducible(t *testing.T) {
|
||||||
|
tmpl := `{"format":"{calc(q * 2 + 1)}","q":[{"format":"{int(1,1000000)}"}]}`
|
||||||
|
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
||||||
|
t.Fatalf("calc not reproducible: %q != %q", a, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-2
@@ -163,7 +163,11 @@ func expand(s *session, format string, fields map[string]node) string {
|
|||||||
}
|
}
|
||||||
body := string(rs[i+1 : end])
|
body := string(rs[i+1 : end])
|
||||||
if name, args, ok := funcCall(body); ok {
|
if name, args, ok := funcCall(body); ok {
|
||||||
b.WriteString(builtins[name].call(s, b.String(), args)) // b.String() is the output so far
|
if name == "calc" { // calc reads sibling fields, not just (rng, emitted)
|
||||||
|
b.WriteString(calcEval(s, args, fields))
|
||||||
|
} else {
|
||||||
|
b.WriteString(builtins[name].call(s, b.String(), args)) // b.String() is the output so far
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(resolve(s, body, fields))
|
b.WriteString(resolve(s, body, fields))
|
||||||
}
|
}
|
||||||
@@ -342,7 +346,11 @@ func checkTokens(format string, fields map[string]node) error {
|
|||||||
}
|
}
|
||||||
body := string(rs[i+1 : end])
|
body := string(rs[i+1 : end])
|
||||||
if strings.IndexByte(body, '(') >= 0 { // a function token, not a field
|
if strings.IndexByte(body, '(') >= 0 { // a function token, not a field
|
||||||
if err := checkFunc(body); err != nil {
|
if name, args, ok := funcCall(body); ok && name == "calc" {
|
||||||
|
if err := checkCalc(args, fields); err != nil {
|
||||||
|
return fmt.Errorf("token {%s}: %w", body, err)
|
||||||
|
}
|
||||||
|
} else if err := checkFunc(body); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -193,6 +193,14 @@ func TestCompileErrors(t *testing.T) {
|
|||||||
`{"format":"{float(1,2,-1)}"}`, // negative decimals
|
`{"format":"{float(1,2,-1)}"}`, // negative decimals
|
||||||
`{"format":"{iban(US)}"}`, // unsupported country
|
`{"format":"{iban(US)}"}`, // unsupported country
|
||||||
`{"format":"{seq(a,b)}"}`, // seq takes at most one name
|
`{"format":"{seq(a,b)}"}`, // seq takes at most one name
|
||||||
|
`{"format":"{calc()}"}`, // calc needs an expression
|
||||||
|
`{"format":"{calc(1 +)}"}`, // dangling operator
|
||||||
|
`{"format":"{calc((1 + 2)}"}`, // unbalanced parenthesis
|
||||||
|
`{"format":"{calc(1 2)}"}`, // two operands, no operator
|
||||||
|
`{"format":"{calc(price)}"}`, // operand names no field
|
||||||
|
`{"format":"{calc(1, 2, 3)}"}`, // too many args
|
||||||
|
`{"format":"{calc(1, x)}"}`, // decimals arg not an integer
|
||||||
|
`{"format":"{calc(1, -1)}"}`, // decimals negative
|
||||||
} {
|
} {
|
||||||
if _, err := compile(parse(t, bad)); err == nil {
|
if _, err := compile(parse(t, bad)); err == nil {
|
||||||
t.Errorf("compile(%s) = nil error, want error", bad)
|
t.Errorf("compile(%s) = nil error, want error", bad)
|
||||||
|
|||||||
Reference in New Issue
Block a user