Accept a name as a calc operand
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@ replacement, and each removed path, column or flag.
|
|||||||
- Names: `{/misc.territory as t}` binds one row of `misc.territory` to `t` and prints
|
- Names: `{/misc.territory as t}` binds one row of `misc.territory` to `t` and prints
|
||||||
nothing; `{t}`, `{t.capital}` and `{lowercase(t.name)}` read that one row. A name lives
|
nothing; `{t}`, `{t.capital}` and `{lowercase(t.name)}` read that one row. A name lives
|
||||||
in the category binding it, and a `repeat` binding it picks again on every iteration.
|
in the category binding it, and a `repeat` binding it picks again on every iteration.
|
||||||
What `New` refuses is listed under [Names](README.md#names).
|
A name is a `{calc()}` operand: `{/n as k}{k} x 2 = {calc(k * 2)}`. What `New` refuses is listed under [Names](README.md#names).
|
||||||
- First release: the CLI, the library and the shipped data set.
|
- First release: the CLI, the library and the shipped data set.
|
||||||
- Table categories: a category JSON naming a `rows` TSV beside it, with the options
|
- Table categories: a category JSON naming a `rows` TSV beside it, with the options
|
||||||
`key`, `name`, `weight` and `parent`; a path selects a row by key or name,
|
`key`, `name`, `weight` and `parent`; a path selects a row by key or name,
|
||||||
|
|||||||
@@ -676,9 +676,10 @@ the SQL example above.
|
|||||||
### Computation
|
### Computation
|
||||||
|
|
||||||
`{calc(expr)}` evaluates `+ - * /`, parentheses and unary minus over number
|
`{calc(expr)}` evaluates `+ - * /`, parentheses and unary minus over number
|
||||||
literals and sibling field names, each rendered then read as a number; a second
|
literals, sibling field names and [names](#names), each rendered then read as a number;
|
||||||
argument rounds to that many decimals. A hyphen is always subtraction, so a
|
a second argument rounds to that many decimals. A name is the operand `{k}` prints:
|
||||||
hyphenated field can't be an operand.
|
`{/n as k}{k} x 2 = {calc(k * 2)}` doubles the pick it shows. A hyphen is always
|
||||||
|
subtraction, so a hyphenated field or name can't be an operand.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "format": "{net} x {qty} = {calc(net * qty, 2)}", "net": ["19.99", "5.00"], "qty": ["3", "7"] }
|
{ "format": "{net} x {qty} = {calc(net * qty, 2)}", "net": ["19.99", "5.00"], "qty": ["3", "7"] }
|
||||||
@@ -770,7 +771,8 @@ Refused at `New`, each error naming what to write instead:
|
|||||||
- a binding of anything but a reference;
|
- a binding of anything but a reference;
|
||||||
- a binding in a choice's item, which every other item would leave unbound;
|
- a binding in a choice's item, which every other item would leave unbound;
|
||||||
- a binding nothing reads, or a binding of a whole category read once whole where it is
|
- a binding nothing reads, or a binding of a whole category read once whole where it is
|
||||||
bound, which the bare reference spells: `{/word as w}{w}` is `{/word}`;
|
bound, which the bare reference spells: `{/word as w}{w}` is `{/word}`, while
|
||||||
|
`{/n as k}{calc(k * 2)}` stands, since a calc operand is no reference;
|
||||||
- a path through a name that selects a row;
|
- a path through a name that selects a row;
|
||||||
- `{n}` beside `{n.w}` where `n`'s category renders `w` twice, as in `{w}-{w}`;
|
- `{n}` beside `{n.w}` where `n`'s category renders `w` twice, as in `{w}-{w}`;
|
||||||
- reads of a name in two draw groups, or inside a `repeat` nested where the name is bound,
|
- reads of a name in two draw groups, or inside a `repeat` nested where the name is bound,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package fejkdata
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@@ -15,7 +16,7 @@ type calcNode interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type calcNum float64 // a number literal
|
type calcNum float64 // a number literal
|
||||||
type calcVar string // a sibling-field name, before indexVars places it
|
type calcVar string // an operand, a sibling field or a name, before indexVars places it
|
||||||
type calcIdx int // an operand, by its position in the values expand read
|
type calcIdx int // an operand, by its position in the values expand read
|
||||||
type calcNeg struct{ x calcNode }
|
type calcNeg struct{ x calcNode }
|
||||||
type calcBin struct { // a + - * / b
|
type calcBin struct { // a + - * / b
|
||||||
@@ -53,8 +54,9 @@ func (n calcBin) eval(operands []string) float64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkCalc validates a calc token at compile time: a parseable expression whose
|
// checkCalc validates a calc token at compile time: a parseable expression, operands
|
||||||
// operands all name existing fields, and an optional non-negative integer dp.
|
// that can be numbers, and an optional non-negative integer dp. An operand no field
|
||||||
|
// holds is left for a name to answer, and checkCalcNames checks it once names link.
|
||||||
func checkCalc(fields map[string]node, args []string) error {
|
func checkCalc(fields map[string]node, args []string) error {
|
||||||
if len(args) < 1 || len(args) > 2 {
|
if len(args) < 1 || len(args) > 2 {
|
||||||
return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args))
|
return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args))
|
||||||
@@ -63,17 +65,13 @@ func checkCalc(fields map[string]node, args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("calc(%q): %w", args[0], err)
|
return fmt.Errorf("calc(%q): %w", args[0], err)
|
||||||
}
|
}
|
||||||
for _, name := range calcVars(expr) {
|
if err := checkOperands(args[0], expr, func(name string) []node {
|
||||||
operand, ok := fields[name]
|
if n, ok := fields[name]; ok {
|
||||||
if !ok {
|
return []node{n}
|
||||||
return fmt.Errorf("calc(%q): no field %q", args[0], name)
|
|
||||||
}
|
}
|
||||||
if text, never := neverNumeric(operand); never {
|
return nil
|
||||||
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", args[0], name, text)
|
}); err != nil {
|
||||||
}
|
return err
|
||||||
}
|
|
||||||
if divisor, zero := constantZeroDivisor(expr, fields); zero {
|
|
||||||
return fmt.Errorf("calc(%q) divides by %s, which is always zero", args[0], divisor)
|
|
||||||
}
|
}
|
||||||
if len(args) == 2 {
|
if len(args) == 2 {
|
||||||
dp, err := plainInt(args[1])
|
dp, err := plainInt(args[1])
|
||||||
@@ -87,33 +85,76 @@ func checkCalc(fields map[string]node, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkCalcNames holds each calc of t reading a name to the checks checkCalc makes of a field.
|
||||||
|
func checkCalcNames(path string, t *template) error {
|
||||||
|
for _, o := range t.compiled.ops {
|
||||||
|
if o.fn != "calc" || !slices.ContainsFunc(o.operands, func(a arm) bool { return a.kind == namedRead }) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := checkOperands(o.args[0], parsedCalc(o.args[0]), operandNodes(t, o)); err != nil {
|
||||||
|
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// operandNodes lists every node each operand of o may render: its field, or each node its name's
|
||||||
|
// path lands on.
|
||||||
|
func operandNodes(t *template, o op) func(name string) []node {
|
||||||
|
return func(name string) []node {
|
||||||
|
i := slices.IndexFunc(o.operands, func(a arm) bool { return a.head == name })
|
||||||
|
if a := o.operands[i]; a.kind == namedRead {
|
||||||
|
return a.leaves
|
||||||
|
}
|
||||||
|
return []node{t.fields[name]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOperands refuses an operand that is never a number and a division by a constant zero.
|
||||||
|
// operand lists every node an operand may render, nil while that is unknown.
|
||||||
|
func checkOperands(text string, expr calcNode, operand func(name string) []node) error {
|
||||||
|
for _, name := range calcVars(expr) {
|
||||||
|
if rendered, never := neverNumber(operand(name)); never {
|
||||||
|
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", text, name, rendered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if divisor, zero := constantZeroDivisor(expr, operand); zero {
|
||||||
|
return fmt.Errorf("calc(%q) divides by %s, which is always zero", text, divisor)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// constantZeroDivisor finds a division whose right side is a constant zero: number
|
// constantZeroDivisor finds a division whose right side is a constant zero: number
|
||||||
// literals and fixed operands folded, anything that varies left unknown.
|
// literals and fixed operands folded, anything that varies left unknown.
|
||||||
func constantZeroDivisor(n calcNode, fields map[string]node) (string, bool) {
|
func constantZeroDivisor(n calcNode, operand func(string) []node) (string, bool) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case calcNeg:
|
case calcNeg:
|
||||||
return constantZeroDivisor(n.x, fields)
|
return constantZeroDivisor(n.x, operand)
|
||||||
case calcBin:
|
case calcBin:
|
||||||
if n.operator == '/' {
|
if n.operator == '/' {
|
||||||
if v, known := constantValue(n.r, fields); known && v == 0 {
|
if v, known := constantValue(n.r, operand); known && v == 0 {
|
||||||
return calcText(n.r), true
|
return calcText(n.r), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d, zero := constantZeroDivisor(n.l, fields); zero {
|
if d, zero := constantZeroDivisor(n.l, operand); zero {
|
||||||
return d, true
|
return d, true
|
||||||
}
|
}
|
||||||
return constantZeroDivisor(n.r, fields)
|
return constantZeroDivisor(n.r, operand)
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// constantValue evaluates an expression whose every operand is fixed.
|
// constantValue evaluates an expression whose every operand is fixed.
|
||||||
func constantValue(n calcNode, fields map[string]node) (float64, bool) {
|
func constantValue(n calcNode, operand func(string) []node) (float64, bool) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case calcNum:
|
case calcNum:
|
||||||
return float64(n), true
|
return float64(n), true
|
||||||
case calcVar:
|
case calcVar:
|
||||||
t, ok := fields[string(n)].(*template)
|
nodes := operand(string(n))
|
||||||
|
if len(nodes) != 1 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
t, ok := nodes[0].(*template)
|
||||||
if !ok || t.repeat > 1 {
|
if !ok || t.repeat > 1 {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
@@ -124,11 +165,11 @@ func constantValue(n calcNode, fields map[string]node) (float64, bool) {
|
|||||||
v, err := strconv.ParseFloat(strings.TrimSpace(lit), 64)
|
v, err := strconv.ParseFloat(strings.TrimSpace(lit), 64)
|
||||||
return v, err == nil
|
return v, err == nil
|
||||||
case calcNeg:
|
case calcNeg:
|
||||||
v, ok := constantValue(n.x, fields)
|
v, ok := constantValue(n.x, operand)
|
||||||
return -v, ok
|
return -v, ok
|
||||||
case calcBin:
|
case calcBin:
|
||||||
l, lok := constantValue(n.l, fields)
|
l, lok := constantValue(n.l, operand)
|
||||||
r, rok := constantValue(n.r, fields)
|
r, rok := constantValue(n.r, operand)
|
||||||
if !lok || !rok {
|
if !lok || !rok {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
@@ -167,26 +208,28 @@ func neverNumeric(n node) (text string, never bool) {
|
|||||||
return lit, true
|
return lit, true
|
||||||
}
|
}
|
||||||
case *choice:
|
case *choice:
|
||||||
for _, it := range n.items {
|
return neverNumber(n.items)
|
||||||
t, itemNever := neverNumeric(it)
|
|
||||||
if !itemNever {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
text = t
|
|
||||||
}
|
|
||||||
return text, true
|
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// neverNumber reports nodes no render of which is a number, and none where there are none.
|
||||||
|
func neverNumber(nodes []node) (text string, never bool) {
|
||||||
|
for _, n := range nodes {
|
||||||
|
t, nodeNever := neverNumeric(n)
|
||||||
|
if !nodeNever {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
text = t
|
||||||
|
}
|
||||||
|
return text, len(nodes) > 0
|
||||||
|
}
|
||||||
|
|
||||||
// calcPrep parses the expression and decimals once, at compile time, and places each
|
// calcPrep parses the expression and decimals once, at compile time, and places each
|
||||||
// operand name at the position expand will read it into. checkCalc proved both args
|
// operand name at the position expand will read it into. checkCalc proved both args
|
||||||
// valid, so no step here can fail.
|
// valid, so no step here can fail.
|
||||||
func calcPrep(args []string) callFn {
|
func calcPrep(args []string) callFn {
|
||||||
expr, err := parseCalc(args[0])
|
expr := parsedCalc(args[0])
|
||||||
if err != nil { // a nil AST would be a nil dereference per render, with no message
|
|
||||||
panic(internalError("calc(%q) reached prep unparsed: %v", args[0], err))
|
|
||||||
}
|
|
||||||
at := make(map[string]int)
|
at := make(map[string]int)
|
||||||
for i, name := range calcVars(expr) {
|
for i, name := range calcVars(expr) {
|
||||||
at[name] = i
|
at[name] = i
|
||||||
@@ -198,6 +241,16 @@ func calcPrep(args []string) callFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsedCalc parses an expression checkCalc accepted. A nil AST would be a nil dereference
|
||||||
|
// per render, with no message.
|
||||||
|
func parsedCalc(expr string) calcNode {
|
||||||
|
n, err := parseCalc(expr)
|
||||||
|
if err != nil {
|
||||||
|
panic(internalError("calc(%q) passed its check unparsed: %v", expr, err))
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
// calcDecimals is a calc's decimals count, or shortestDecimals where it names none.
|
// calcDecimals is a calc's decimals count, or shortestDecimals where it names none.
|
||||||
func calcDecimals(args []string) int {
|
func calcDecimals(args []string) int {
|
||||||
if len(args) == 2 {
|
if len(args) == 2 {
|
||||||
@@ -224,7 +277,7 @@ func indexVars(n calcNode, at map[string]int) calcNode {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcOperands lists the sibling-field names a calc's args read. checkCalc reports
|
// 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.
|
// an expression that does not parse, so one that does not simply names nothing.
|
||||||
func calcOperands(args []string) []string {
|
func calcOperands(args []string) []string {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
@@ -237,7 +290,7 @@ func calcOperands(args []string) []string {
|
|||||||
return calcVars(expr)
|
return calcVars(expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcVars lists the distinct field names an expression reads, in the order it first
|
// calcVars lists the distinct operands an expression reads, in the order it first
|
||||||
// names each. That order is the contract between expand, which reads the operands
|
// names each. That order is the contract between expand, which reads the operands
|
||||||
// into a slice, and indexVars, which places each name at its position in it.
|
// into a slice, and indexVars, which places each name at its position in it.
|
||||||
func calcVars(n calcNode) []string {
|
func calcVars(n calcNode) []string {
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, pass := range []func(string, *template) error{linkNames, checkNameReads} {
|
for _, pass := range []func(string, *template) error{linkNames, checkNameReads, checkCalcNames} {
|
||||||
if err := scope(func(path string, m node) error {
|
if err := scope(func(path string, m node) error {
|
||||||
if t, ok := m.(*template); ok {
|
if t, ok := m.(*template); ok {
|
||||||
return pass(path, t)
|
return pass(path, t)
|
||||||
|
|||||||
+3
-7
@@ -26,15 +26,11 @@ func checkNameReads(path string, t *template) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkUses refuses a binding of a category read once whole, and reads of b in two draw groups,
|
// checkUses refuses a binding of a category read once whole, outside a calc, which takes no reference, and reads of b in two draw groups,
|
||||||
// or inside a repeat, where what b names reads a reference path.
|
// or inside a repeat, where what b names reads a reference path.
|
||||||
func (b *nameBinding) checkUses() error {
|
func (b *nameBinding) checkUses() error {
|
||||||
if r := b.uses[0]; len(b.uses) == 1 && r.tail == "" && !r.nested && len(b.tail) == 0 {
|
if r := b.uses[0]; len(b.uses) == 1 && r.tail == "" && !r.nested && !r.operand && len(b.tail) == 0 {
|
||||||
spelling := b.ref
|
return fmt.Errorf("name %q is read once, whole, which the bare reference draws the same way; write {%s} where it is read, and drop the token", b.name, b.ref)
|
||||||
if !r.operand {
|
|
||||||
spelling = "{" + spelling + "}"
|
|
||||||
}
|
|
||||||
return fmt.Errorf("name %q is read once, whole, which the bare reference draws the same way; write %s where it is read, and drop the token", b.name, spelling)
|
|
||||||
}
|
}
|
||||||
if !readsHeld(b.head, map[node]bool{}) {
|
if !readsHeld(b.head, map[node]bool{}) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+5
-3
@@ -77,10 +77,12 @@ func linkRefs(sites []categorySite, root map[string]node) error {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := eachTemplate(sites, func(_ categorySite, path string, t *template) error { return linkNames(path, t) }); err != nil {
|
for _, pass := range []func(string, *template) error{linkNames, checkNameReads, checkCalcNames} {
|
||||||
return err
|
if err := eachTemplate(sites, func(_ categorySite, path string, t *template) error { return pass(path, t) }); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return eachTemplate(sites, func(_ categorySite, path string, t *template) error { return checkNameReads(path, t) })
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// linkTemplate resolves t's references in category, "" for an inline template, and
|
// linkTemplate resolves t's references in category, "" for an inline template, and
|
||||||
|
|||||||
+8
-10
@@ -210,11 +210,9 @@ func (p *valueProof) proveTemplate(t *template) proven {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *valueProof) proveCalc(t *template, body string, args []string) proven {
|
func (p *valueProof) proveCalc(t *template, body string, args []string) proven {
|
||||||
expr, err := parseCalc(args[0])
|
expr := parsedCalc(args[0])
|
||||||
if err != nil {
|
nodes := operandNodes(t, t.compiled.ops[0])
|
||||||
panic(internalError("calc(%q) reached a proof unparsed: %v", args[0], err))
|
v, doubt := p.proveExpr(expr, func(name string) proven { return p.proveUnion(nodes(name)) })
|
||||||
}
|
|
||||||
v, doubt := p.proveExpr(expr, t.fields)
|
|
||||||
if doubt == "" && !(magnitude(v) <= calcLimit) {
|
if doubt == "" && !(magnitude(v) <= calcLimit) {
|
||||||
doubt = calcText(expr) + " is not proven within 1e300"
|
doubt = calcText(expr) + " is not proven within 1e300"
|
||||||
}
|
}
|
||||||
@@ -229,27 +227,27 @@ func (p *valueProof) proveCalc(t *template, body string, args []string) proven {
|
|||||||
const calcLimit = 1e300
|
const calcLimit = 1e300
|
||||||
|
|
||||||
// proveExpr bounds a calc expression from its operands, or says why it cannot.
|
// proveExpr bounds a calc expression from its operands, or says why it cannot.
|
||||||
func (p *valueProof) proveExpr(n calcNode, fields map[string]node) (proven, string) {
|
func (p *valueProof) proveExpr(n calcNode, operand func(name string) proven) (proven, string) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case calcNum:
|
case calcNum:
|
||||||
v := float64(n)
|
v := float64(n)
|
||||||
return bounded(v, v, v == math.Trunc(v)), ""
|
return bounded(v, v, v == math.Trunc(v)), ""
|
||||||
case calcVar:
|
case calcVar:
|
||||||
v := p.prove(fields[string(n)])
|
v := operand(string(n))
|
||||||
if v.notOperand != "" {
|
if v.notOperand != "" {
|
||||||
return proven{}, fmt.Sprintf("operand %q: %s", string(n), v.notOperand)
|
return proven{}, fmt.Sprintf("operand %q: %s", string(n), v.notOperand)
|
||||||
}
|
}
|
||||||
return proven{lo: v.lo, hi: v.hi, nonZero: v.nonZero, integral: v.integral}, ""
|
return proven{lo: v.lo, hi: v.hi, nonZero: v.nonZero, integral: v.integral}, ""
|
||||||
case calcNeg:
|
case calcNeg:
|
||||||
v, doubt := p.proveExpr(n.x, fields)
|
v, doubt := p.proveExpr(n.x, operand)
|
||||||
v.lo, v.hi = -v.hi, -v.lo
|
v.lo, v.hi = -v.hi, -v.lo
|
||||||
return v, doubt
|
return v, doubt
|
||||||
case calcBin:
|
case calcBin:
|
||||||
l, doubt := p.proveExpr(n.l, fields)
|
l, doubt := p.proveExpr(n.l, operand)
|
||||||
if doubt != "" {
|
if doubt != "" {
|
||||||
return l, doubt
|
return l, doubt
|
||||||
}
|
}
|
||||||
r, doubt := p.proveExpr(n.r, fields)
|
r, doubt := p.proveExpr(n.r, operand)
|
||||||
if doubt != "" {
|
if doubt != "" {
|
||||||
return r, doubt
|
return r, doubt
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user