Compare commits
2 Commits
8ca887aa7d
...
3e7127da71
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e7127da71 | |||
| 1ad9418a76 |
@@ -273,8 +273,8 @@ Writes e.g. `{"id":1,"paid":true,"total":59.97}`. A column is a field of the top
|
||||
template, or an item of a choice standing in for one; `datatype` anywhere else is a
|
||||
load error. A typed column holds one value, alone in its format: a literal, one
|
||||
`{int()}`, `{float()}`, `{seq()}` or `{calc()}` call, or a read that lands only on such
|
||||
values. `integer` is an int64 written `-?(0|[1-9][0-9]*)` — `{float()}` prints one at
|
||||
`0` decimals — `number` a JSON number, `boolean` `true` or `false`. A value its
|
||||
values. `integer` is an int64 written `0|-?[1-9][0-9]*` — `{float()}` prints one at
|
||||
`0` decimals within int64 — `number` a JSON number, `boolean` `true` or `false`. A value its
|
||||
datatype cannot hold is a load error naming it:
|
||||
|
||||
```text
|
||||
@@ -287,7 +287,7 @@ literal, an `{int()}`, `{float()}`, `{seq()}` or `{digits()}` call, a calc, or a
|
||||
such values, whose bounds keep every divisor from zero and the result within `1e300`.
|
||||
What the bounds cannot show is refused — `{calc(a / b)}: divides by b, which is not
|
||||
proven nonzero`. The calc fills an `integer` column at `0` decimals, or over whole
|
||||
operands with no `/`.
|
||||
operands with no `/`, while its bounds stay within int64.
|
||||
|
||||
### Null
|
||||
|
||||
@@ -365,7 +365,8 @@ hyphenated field can't be an operand.
|
||||
{ "format": "{net} x {qty} = {calc(net * qty, 2)}", "net": ["19.99", "5.00"], "qty": ["3", "7"] }
|
||||
```
|
||||
|
||||
Renders e.g. `19.99 x 3 = 59.97`. An operand that can never be a number (`"abc"`,
|
||||
Renders e.g. `19.99 x 3 = 59.97`. A result that rounds to zero prints unsigned — `0`,
|
||||
`0.00` — as `{float()}`'s does. An operand that can never be a number (`"abc"`,
|
||||
or a choice of such) is rejected at load, as is a division by a constant zero
|
||||
(`1/0`, or a fixed `"0"` field); an operand that sometimes is not a number yields
|
||||
`NaN`, and a division by one that is not constant `Inf` — both print rather than
|
||||
|
||||
+18
-9
@@ -31,8 +31,8 @@ var builtins = map[string]builtin{
|
||||
"ulid": {arity: 0, prep: sample(ulid)},
|
||||
"nanoid": {arity: 1, check: posIntArg, prep: chars(nanoidAlphabet)},
|
||||
"hex": {arity: 1, check: posIntArg, prep: chars(hexDigits)},
|
||||
"digits": {arity: 1, check: posIntArg, prep: chars("0123456789"), number: func(a []string) (proven, DataType) {
|
||||
return bounded(0, math.Pow(10, float64(atoi(a[0])))-1, true), DataTypeString
|
||||
"digits": {arity: 1, check: posIntArg, prep: chars("0123456789"), number: func(token string, a []string) proven {
|
||||
return printing(token, DataTypeString, bounded(0, math.Pow(10, float64(atoi(a[0])))-1, true))
|
||||
}},
|
||||
"upper": {arity: 1, check: posIntArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
|
||||
"lower": {arity: 1, check: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")},
|
||||
@@ -45,16 +45,16 @@ var builtins = map[string]builtin{
|
||||
"int": {arity: 2, check: intRangeArgs, prep: func(a []string) callFn {
|
||||
lo, span := atoi(a[0]), atoi(a[1])-atoi(a[0])+1
|
||||
return func(s *session, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) }
|
||||
}, number: func(a []string) (proven, DataType) {
|
||||
return bounded(float64(atoi(a[0])), float64(atoi(a[1])), true), DataTypeInteger
|
||||
}, number: func(token string, a []string) proven {
|
||||
return printing(token, DataTypeInteger, bounded(float64(atoi(a[0])), float64(atoi(a[1])), true))
|
||||
}},
|
||||
"float": {arity: 3, check: floatArgs, prep: func(a []string) callFn {
|
||||
lo, hi, dp := atof(a[0]), atof(a[1]), atoi(a[2])
|
||||
return func(s *session, _ string, _ []string) string {
|
||||
return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', dp, 64)
|
||||
return formatFloat(lo+s.Float64()*(hi-lo), dp)
|
||||
}
|
||||
}, number: func(a []string) (proven, DataType) {
|
||||
return printedNumber(bounded(atof(a[0]), atof(a[1]), false), atoi(a[2]))
|
||||
}, number: func(token string, a []string) proven {
|
||||
return printedNumber(token, bounded(atof(a[0]), atof(a[1]), false), atoi(a[2]))
|
||||
}},
|
||||
"iban": {arity: 1, check: ibanArg, prep: func(a []string) callFn {
|
||||
cc := a[0]
|
||||
@@ -75,8 +75,8 @@ var builtins = map[string]builtin{
|
||||
return func(s *session, _ string, _ []string) string {
|
||||
return strconv.FormatUint(s.next(key), 10)
|
||||
}
|
||||
}, number: func([]string) (proven, DataType) {
|
||||
return bounded(1, math.MaxInt64, true), DataTypeInteger
|
||||
}, number: func(token string, _ []string) proven {
|
||||
return printing(token, DataTypeInteger, bounded(1, math.MaxInt64, true))
|
||||
}},
|
||||
}
|
||||
|
||||
@@ -104,6 +104,15 @@ func chars(alphabet string) func([]string) callFn {
|
||||
|
||||
const hexDigits = "0123456789abcdef"
|
||||
|
||||
// formatFloat prints v to dp decimals, -1 for the shortest form, 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
|
||||
}
|
||||
|
||||
// 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{
|
||||
|
||||
@@ -69,6 +69,9 @@ func TestBuiltinFloat(t *testing.T) {
|
||||
if got := mustRender(t, f, `"{float(1,2,3)}"`); !re.MatchString(got) {
|
||||
t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got)
|
||||
}
|
||||
if got := mustRender(t, f, `"{float(-1,1,0)}"`); got == "-0" {
|
||||
t.Fatalf("float(-1,1,0) = %q, want a zero printed unsigned", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ func calcPrep(args []string) callFn {
|
||||
placed := indexVars(expr, at)
|
||||
dp := calcDecimals(args)
|
||||
return func(_ *session, _ string, operands []string) string {
|
||||
return strconv.FormatFloat(placed.eval(operands), 'f', dp, 64)
|
||||
return formatFloat(placed.eval(operands), dp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ func TestCalcAuto(t *testing.T) {
|
||||
`"{calc(10 / 3)}"`: "3.3333333333333335",
|
||||
`"{calc(6 / 2)}"`: "3",
|
||||
`"{calc(1 / 4)}"`: "0.25",
|
||||
`"{calc(0 * -1)}"`: "0",
|
||||
}
|
||||
for tmpl, want := range cases {
|
||||
if got := mustRender(t, f, tmpl); got != want {
|
||||
@@ -52,6 +53,7 @@ func TestCalcDecimals(t *testing.T) {
|
||||
`"{calc(10 / 3, 2)}"`: "3.33",
|
||||
`"{calc(10 / 3, 0)}"`: "3",
|
||||
`"{calc(2 * 3, 2)}"`: "6.00",
|
||||
`"{calc(-0.001, 2)}"`: "0.00",
|
||||
}
|
||||
for tmpl, want := range cases {
|
||||
if got := mustRender(t, f, tmpl); got != want {
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ func disagreement(a, b *template) error {
|
||||
switch {
|
||||
case bare.datatype != DataTypeString:
|
||||
return fmt.Errorf("its items declare %s and %s; a column holds one datatype", a.datatype, b.datatype)
|
||||
case len(bare.fields) == 0 && bare.repeat == 1:
|
||||
case bare.fields == nil: // a JSON string; an object, which may carry a weight, has a fields map
|
||||
return fmt.Errorf(`item %q declares no datatype, and a column holds one; write it as {"format":%q,"datatype":%q}`, bare.format, bare.format, typed.datatype)
|
||||
}
|
||||
return fmt.Errorf(`an item declares no datatype beside one declaring %s; a column holds one, so give it "datatype": %q`, typed.datatype, typed.datatype)
|
||||
|
||||
+16
-1
@@ -32,6 +32,7 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) {
|
||||
`null`: `so write ""`,
|
||||
`{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`,
|
||||
`{"format":"","c":[{"format":"1","datatype":"integer"},"x"]}`: `write it as {"format":"x","datatype":"integer"}`,
|
||||
`{"format":"","c":[{"format":"1","datatype":"integer"},{"format":"2","weight":3}]}`: `give it "datatype": "integer"`,
|
||||
`{"format":"","c":[{"format":"1","datatype":"integer"},{"format":"true","datatype":"boolean"}]}`: "a column holds one datatype",
|
||||
} {
|
||||
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
||||
@@ -66,6 +67,17 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) {
|
||||
{"an overflow", `{"format":"{calc(a * a)}","a":"{digits(200)}","datatype":"number"}`, "is not proven within 1e300"},
|
||||
{"a division in an integer column", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{int(1,9)}","datatype":"integer"}`, "prints a number, not an integer"},
|
||||
{"a composed operand", `{"format":"{calc(n * 2)}","n":"{int(1,99)}.{digits(2)}","datatype":"number"}`, "{seq()}, {digits()} or {calc()}"},
|
||||
{"a divisor that prints as zero", `{"format":"{calc(1 / b, 2)}","b":"{float(4.9999999999999994e-79,5e-79,78)}","datatype":"number"}`, "divides by b, which is not proven nonzero"},
|
||||
{"a divisor that rounds to zero", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{float(0.1,1,0)}","datatype":"number"}`, "divides by b, which is not proven nonzero"},
|
||||
{"a zero among a divisor's literals", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":["0","5"],"datatype":"number"}`, "divides by b, which is not proven nonzero"},
|
||||
{"a negated divisor crossing zero", `{"format":"{calc(a / (-b + 10))}","a":"{int(1,9)}","b":"{int(1,20)}","datatype":"number"}`, "which is not proven nonzero"},
|
||||
{"a subtracted divisor crossing zero", `{"format":"{calc(a / (10 - b))}","a":"{int(1,9)}","b":"{int(1,20)}","datatype":"number"}`, "which is not proven nonzero"},
|
||||
{"a quotient past the limit", `{"format":"{calc(a / b / b)}","a":"{digits(300)}","b":"{float(0.000001,1,6)}","datatype":"number"}`, "is not proven within 1e300"},
|
||||
{"an operand past the limit", `{"format":"{calc(a)}","a":"{digits(400)}","datatype":"number"}`, "is not proven within 1e300"},
|
||||
{"a whole calc past int64", `{"format":"{calc(a * 2)}","a":"{seq()}","datatype":"integer"}`, "{calc(a * 2)} is not proven within int64"},
|
||||
{"a whole float past int64", `{"format":"{float(0,1e19,0)}","datatype":"integer"}`, "{float(0,1e19,0)} is not proven within int64"},
|
||||
{"a signed zero integer", `{"format":"-0","datatype":"integer"}`, `"-0" is zero written with a sign; write "0"`},
|
||||
{"a signed zero number", `{"format":"-0.00","datatype":"number"}`, `"-0.00" is zero written with a sign; write "0.00"`},
|
||||
} {
|
||||
row := `{"format":"","col":` + c.column + `}`
|
||||
files := map[string]string{"row": row}
|
||||
@@ -83,7 +95,7 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var jsonInteger = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)
|
||||
var jsonInteger = regexp.MustCompile(`^(0|-?[1-9][0-9]*)$`)
|
||||
|
||||
func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) {
|
||||
cat := `[{"format":"{code}","code":"200"},{"format":"{code}","code":"404"}]`
|
||||
@@ -94,6 +106,9 @@ func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) {
|
||||
`{"format":"{/cat.code}","datatype":"integer"}`,
|
||||
`{"format":"{a|b}","a":"1","b":"{int(5,9)}","datatype":"integer"}`,
|
||||
`{"format":"{float(-1.5,9.5,0)}","datatype":"integer"}`,
|
||||
`{"format":"{float(-1,1,0)}","datatype":"integer"}`,
|
||||
`{"format":"{calc(a * b)}","a":"{int(-9,-1)}","b":"{int(0,9)}","datatype":"integer"}`,
|
||||
`{"format":"{calc(x + 1)}","x":"{float(0,9,0)}","datatype":"integer"}`,
|
||||
`{"format":"{float(-1,1,2)}","datatype":"number"}`,
|
||||
`{"format":"{v}","v":["1","2.5","6.022e23"],"datatype":"number"}`,
|
||||
`{"format":"{b}","b":["true","false"],"datatype":"boolean"}`,
|
||||
|
||||
@@ -32,8 +32,7 @@ type choice struct {
|
||||
|
||||
func (*choice) isNode() {}
|
||||
|
||||
// null is a record column's missing value, rendered as "". It is not zero-sized, so two
|
||||
// nulls are two map keys.
|
||||
// null is a column's missing value, rendered ""; sized so two nulls are two map keys.
|
||||
type null struct{ _ byte }
|
||||
|
||||
func (*null) isNode() {}
|
||||
@@ -216,6 +215,9 @@ func checkNoRepeatedItem(items []any) error {
|
||||
if s, isString := raw.(string); isString {
|
||||
return fmt.Errorf("choice item %q is repeated; skew the odds with a weight instead: { \"format\": %q, \"weight\": 2 }", s, s)
|
||||
}
|
||||
if raw == nil {
|
||||
return fmt.Errorf("choice item %d repeats null; a null takes no weight, so weight the other items instead", i)
|
||||
}
|
||||
return fmt.Errorf("choice item %d repeats item %d; skew the odds with a weight on one of them instead", i, j)
|
||||
}
|
||||
seen[string(key)] = i
|
||||
|
||||
@@ -18,6 +18,7 @@ func TestRepeatedChoiceItemIsRejected(t *testing.T) {
|
||||
`["a", "a", "b"]`: `{ "format": "a", "weight": 2 }`,
|
||||
`[{"format":"{x}","x":"1"},{"format":"{x}","x":"1"}]`: "repeats item",
|
||||
`{"format":"{w}","w":["", "", "x"]}`: `{ "format": "", "weight": 2 }`,
|
||||
`{"format":"","w":[null,null,"a"]}`: "a null takes no weight",
|
||||
} {
|
||||
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("compile(%s) = %v, want an error naming %s", src, err, want)
|
||||
|
||||
@@ -68,6 +68,10 @@ func TestNoRecordAllocRegression(t *testing.T) {
|
||||
if allocs := testing.AllocsPerRun(10000, func() { f.FakeRecord("x") }); allocs > base*1.10 {
|
||||
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a record fence running per draw is the usual cause", s.name, allocs, base*1.10, base)
|
||||
}
|
||||
r, _ := f.FakeRecord("x")
|
||||
if allocs := testing.AllocsPerRun(10000, func() { _ = r.CSVLine() }); allocs > 2 {
|
||||
t.Errorf("%s: CSVLine() makes %.1f allocs/op, want 2: the fields slice and the joined line", s.name, allocs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package fejkdata
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Column is one rendered column of a record. Value is the rendered text, which a
|
||||
@@ -73,15 +74,16 @@ func (r *Record) CSVLine() string {
|
||||
return strings.Join(fields, ",")
|
||||
}
|
||||
|
||||
// csvField quotes a field where encoding/csv would, and an empty one too.
|
||||
func csvField(s string) string {
|
||||
if s == "" {
|
||||
first, _ := utf8.DecodeRuneInString(s)
|
||||
switch {
|
||||
case s == "":
|
||||
return `""`
|
||||
case s == `\.` || strings.ContainsAny(s, "\",\r\n") || unicode.IsSpace(first):
|
||||
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
|
||||
}
|
||||
var b strings.Builder
|
||||
w := csv.NewWriter(&b)
|
||||
_ = w.Write([]string{s})
|
||||
w.Flush()
|
||||
return strings.TrimSuffix(b.String(), "\n")
|
||||
return s
|
||||
}
|
||||
|
||||
// SQLInsert renders the record as one INSERT statement into table, identifiers in ANSI
|
||||
@@ -104,9 +106,7 @@ func quoteIdent(s string) string {
|
||||
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
|
||||
}
|
||||
|
||||
// literal spells a column the way a serializer writes it: quoted for a string, bare for
|
||||
// any other datatype, whose every render the load check proved a literal, and nullText
|
||||
// for a null.
|
||||
// literal writes a string column quoted, a proven typed one bare, and a null as nullText.
|
||||
func literal(c Column, quote func(string) string, nullText string) string {
|
||||
switch {
|
||||
case c.Null:
|
||||
|
||||
+8
-4
@@ -134,7 +134,7 @@ func TestRecordCSV(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRecordCSVEmptyValueStaysARow(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{"blank": `{"format": "", "note": ""}`, "gone": `{"format": "", "note": null}`})
|
||||
dir := writeData(t, map[string]string{"blank": `{"format": "", "note": ""}`})
|
||||
f := newGenerator(t, dir, WithSeed(1))
|
||||
r, err := f.FakeRecord("blank")
|
||||
if err != nil {
|
||||
@@ -147,9 +147,6 @@ func TestRecordCSVEmptyValueStaysARow(t *testing.T) {
|
||||
if len(rows) != 2 || len(rows[1]) != 1 || rows[1][0] != "" {
|
||||
t.Fatalf("one empty column parsed to %v, want a header and one row of one empty field", rows)
|
||||
}
|
||||
if r, err := f.FakeRecord("gone"); err != nil || r.CSVLine() != "" {
|
||||
t.Fatalf("a lone null column wrote %q, %v; want the blank line PostgreSQL's COPY reads as null", r.CSVLine(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordWritesTypedAndNullColumns(t *testing.T) {
|
||||
@@ -183,6 +180,13 @@ func TestRecordWritesTypedAndNullColumns(t *testing.T) {
|
||||
if got := DataTypeNumber.String(); got != "number" {
|
||||
t.Errorf("DataTypeNumber.String() = %q, want the data's spelling", got)
|
||||
}
|
||||
lone, err := newGenerator(t, writeData(t, map[string]string{"gone": `{"format":"","note":null}`})).FakeRecord("gone")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := lone.CSVLine(); got != "" {
|
||||
t.Errorf("a lone null column wrote CSVLine() = %q, want the blank line PostgreSQL's COPY reads as null", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordRejectsOverlappingReferenceColumns(t *testing.T) {
|
||||
|
||||
+3
-3
@@ -72,9 +72,9 @@ type builtin struct {
|
||||
// operands names the fields the call reads, which expand renders for it; nil
|
||||
// for a builtin that reads none.
|
||||
operands func(args []string) []string
|
||||
// number bounds the number a call prints and names the datatype its text is; nil
|
||||
// for a builtin whose text is no number.
|
||||
number func(args []string) (proven, DataType)
|
||||
// number proves what a call prints, token its body: the bounds of its number and the
|
||||
// datatype of its text; nil for a builtin whose text is no number.
|
||||
number func(token string, args []string) proven
|
||||
}
|
||||
|
||||
// funcCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
||||
|
||||
@@ -11,16 +11,14 @@ import (
|
||||
// proven is what a proof knows of every render of a node: bounds on the number each
|
||||
// reads as, and per datatype why some render's text is not one ("" when none).
|
||||
type proven struct {
|
||||
lo, hi float64
|
||||
nonZero float64 // every value is at least this far from zero; 0 when one can be zero
|
||||
integral bool
|
||||
notNumber string // why some render reads as no finite number, the way calc reads it
|
||||
not [len(dataTypeNames)]string
|
||||
lo, hi float64
|
||||
nonZero float64 // every value is at least this far from zero; 0 when one can be zero
|
||||
integral bool
|
||||
notOperand string // why some render reads as no finite number, the way calc reads it
|
||||
not [len(dataTypeNames)]string
|
||||
}
|
||||
|
||||
// valueProof proves what typed columns and their calc operands hold, each node once per
|
||||
// scope. A typed column holds one value: a literal, one value builtin, one calc, or a
|
||||
// read of such values.
|
||||
// valueProof proves what typed columns and their calc operands hold, each node once per scope.
|
||||
type valueProof struct {
|
||||
memo map[node]proven
|
||||
}
|
||||
@@ -71,8 +69,8 @@ func (p *valueProof) unite(nodes []node) proven {
|
||||
w := p.of(n)
|
||||
v.lo, v.hi, v.nonZero = min(v.lo, w.lo), max(v.hi, w.hi), min(v.nonZero, w.nonZero)
|
||||
v.integral = v.integral && w.integral
|
||||
if v.notNumber == "" {
|
||||
v.notNumber = w.notNumber
|
||||
if v.notOperand == "" {
|
||||
v.notOperand = w.notOperand
|
||||
}
|
||||
for d := range v.not {
|
||||
if v.not[d] == "" {
|
||||
@@ -93,7 +91,7 @@ func (p *valueProof) template(t *template) proven {
|
||||
return literalValue(t.lit)
|
||||
case len(t.ops) != 1:
|
||||
v := unproven(notOneValue(t.format, "{int()}, {float()}, {seq()} or {calc()}"))
|
||||
v.notNumber = notOneValue(t.format, "{int()}, {float()}, {seq()}, {digits()} or {calc()}")
|
||||
v.notOperand = notOneValue(t.format, "{int()}, {float()}, {seq()}, {digits()} or {calc()}")
|
||||
return v
|
||||
}
|
||||
body := t.format[1 : len(t.format)-1]
|
||||
@@ -108,12 +106,11 @@ func (p *valueProof) template(t *template) proven {
|
||||
case name == "calc":
|
||||
return p.calc(t, body, args)
|
||||
case builtins[name].number != nil:
|
||||
v, prints := builtins[name].number(args)
|
||||
return printing(body, prints, v)
|
||||
return builtins[name].number(body, args)
|
||||
case isTransform:
|
||||
return unproven(fmt.Sprintf("{%s} rewrites text rather than printing a value; write the values it would print", body))
|
||||
}
|
||||
return printing(body, DataTypeString, proven{notNumber: fmt.Sprintf("{%s} prints text, not a number", body)})
|
||||
return printing(body, DataTypeString, proven{notOperand: fmt.Sprintf("{%s} prints text, not a number", body)})
|
||||
}
|
||||
|
||||
func (p *valueProof) calc(t *template, body string, args []string) proven {
|
||||
@@ -128,8 +125,7 @@ func (p *valueProof) calc(t *template, body string, args []string) proven {
|
||||
if doubt != "" {
|
||||
return unproven(fmt.Sprintf("{%s}: %s", body, doubt))
|
||||
}
|
||||
v, prints := printedNumber(v, calcDecimals(args))
|
||||
return printing(body, prints, v)
|
||||
return printedNumber(body, v, calcDecimals(args))
|
||||
}
|
||||
|
||||
// calcLimit is the largest magnitude a proof accepts as finite, far enough below
|
||||
@@ -144,8 +140,8 @@ func (p *valueProof) expr(n calcNode, fields map[string]node) (proven, string) {
|
||||
return bounded(v, v, v == math.Trunc(v)), ""
|
||||
case calcVar:
|
||||
v := p.of(fields[string(n)])
|
||||
if v.notNumber != "" {
|
||||
return proven{}, fmt.Sprintf("operand %q: %s", string(n), v.notNumber)
|
||||
if 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}, ""
|
||||
case calcNeg:
|
||||
@@ -205,17 +201,21 @@ func bounded(lo, hi float64, integral bool) proven {
|
||||
|
||||
func magnitude(v proven) float64 { return math.Max(math.Abs(v.lo), math.Abs(v.hi)) }
|
||||
|
||||
// printedNumber is v once strconv.FormatFloat prints it to dp decimals, and the datatype
|
||||
// that text is: an integer when whole and within int64, else a number.
|
||||
func printedNumber(v proven, dp int) (proven, DataType) {
|
||||
// printedNumber is what a token printing v to dp decimals holds: an integer when whole
|
||||
// and within int64, else a number.
|
||||
func printedNumber(token string, v proven, dp int) proven {
|
||||
if dp >= 0 {
|
||||
half := math.Pow(10, -float64(dp)) / 2
|
||||
half, _ := strconv.ParseFloat("5e-"+strconv.Itoa(dp+1), 64) // math.Pow(10, -dp) can land below the tie and let a printed zero through
|
||||
v = proven{lo: v.lo - half, hi: v.hi + half, nonZero: math.Max(0, v.nonZero-half), integral: v.integral || dp == 0}
|
||||
}
|
||||
if (dp == 0 || dp < 0 && v.integral) && magnitude(v) < math.MaxInt64 {
|
||||
return v, DataTypeInteger
|
||||
if dp != 0 && !(dp < 0 && v.integral) {
|
||||
return printing(token, DataTypeNumber, v)
|
||||
}
|
||||
return v, DataTypeNumber
|
||||
v = printing(token, DataTypeInteger, v)
|
||||
if !(magnitude(v) < math.MaxInt64) {
|
||||
v.not[DataTypeInteger] = fmt.Sprintf("{%s} is not proven within int64", token)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// printing is v for a token whose every render is text of datatype prints, with a reason
|
||||
@@ -235,7 +235,7 @@ func notOneValue(format, calls string) string {
|
||||
|
||||
// unproven is a render no datatype and no calc can take, for why.
|
||||
func unproven(why string) proven {
|
||||
v := proven{notNumber: why}
|
||||
v := proven{notOperand: why}
|
||||
for d := DataTypeInteger; d <= DataTypeBoolean; d++ {
|
||||
v.not[d] = why
|
||||
}
|
||||
@@ -251,7 +251,7 @@ var (
|
||||
func literalValue(text string) proven {
|
||||
var v proven
|
||||
if f, err := strconv.ParseFloat(strings.TrimSpace(text), 64); err != nil || math.IsNaN(f) || math.IsInf(f, 0) {
|
||||
v.notNumber = fmt.Sprintf("%q is not a number", text)
|
||||
v.notOperand = fmt.Sprintf("%q is not a number", text)
|
||||
} else {
|
||||
v = bounded(f, f, f == math.Trunc(f))
|
||||
}
|
||||
@@ -260,11 +260,24 @@ func literalValue(text string) proven {
|
||||
} else if err != nil {
|
||||
v.not[DataTypeInteger] = fmt.Sprintf("%q is past the int64 range", text)
|
||||
}
|
||||
if v.notNumber != "" || !numberText.MatchString(text) {
|
||||
if v.notOperand != "" || !numberText.MatchString(text) {
|
||||
v.not[DataTypeNumber] = fmt.Sprintf("%q is not a number", text)
|
||||
}
|
||||
if text != "true" && text != "false" {
|
||||
v.not[DataTypeBoolean] = fmt.Sprintf("%q is not a boolean", text)
|
||||
}
|
||||
return signedZero(text, v)
|
||||
}
|
||||
|
||||
// signedZero refuses a zero written with a sign as a typed value, naming it unsigned.
|
||||
func signedZero(text string, v proven) proven {
|
||||
if !strings.HasPrefix(text, "-") || v.notOperand != "" || v.lo != 0 {
|
||||
return v
|
||||
}
|
||||
for _, d := range []DataType{DataTypeInteger, DataTypeNumber} {
|
||||
if v.not[d] == "" {
|
||||
v.not[d] = fmt.Sprintf("%q is zero written with a sign; write %q", text, text[1:])
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user