WIP: use pql.Decimal instead of float64

This commit introduces a new type: pql.Decimal
We use that instead of float64 in order to ensure
that the string representation is consistent.

One unfortunate discovery during implementation is
that the RowAttrs and ColAttrs support floats, and
the PEG file was treating them as such. So I had
to split the PEG definitions into float-specific
items and decimal-specific items.
This commit is contained in:
Travis 2020-03-13 18:59:05 -05:00
parent 334eb3cd08
commit 963affcc30
7 changed files with 2182 additions and 1414 deletions

View file

@ -2926,7 +2926,7 @@ func (e *executor) executeSet(ctx context.Context, index string, c *pql.Call, op
// Read row value.
rowVal, err := getScaledInt(f, v)
if err != nil {
return false, fmt.Errorf("reading Set() row: %v", err)
return false, fmt.Errorf("reading Set() row (int/decimal): %v", err)
}
return e.executeSetValueField(ctx, index, c, f, colID, rowVal, opt)
@ -4477,6 +4477,8 @@ func getScaledInt(f *Field, v interface{}) (int64, error) {
value = int64(float64(tv) * math.Pow10(int(scale)))
case uint64:
value = int64(float64(tv) * math.Pow10(int(scale)))
case pql.Decimal:
value = tv.ToInt64(scale)
case float64:
value = int64(tv * math.Pow10(int(scale)))
default:

View file

@ -62,7 +62,7 @@ func (q *Query) lastCallStackElem() *callStackElem {
func (q *Query) addPosNum(key, value string) {
q.addField(key)
q.addNumVal(value)
q.addNumVal(value, false)
}
func (q *Query) addPosStr(key, value string) {
@ -87,9 +87,9 @@ func (q *Query) endConditional() {
if len(q.conditional) != 5 {
panic(fmt.Sprintf("conditional of wrong length: %#v", q.conditional))
}
low := parseNum(q.conditional[0])
low := parseNum(q.conditional[0], false)
field := q.conditional[2]
high := parseNum(q.conditional[4])
high := parseNum(q.conditional[4], false)
var op Token
switch q.conditional[1] + q.conditional[3] {
@ -157,12 +157,12 @@ func (q *Query) addVal(val interface{}) {
elem.lastCond = ILLEGAL
}
func (q *Query) addNumVal(val string) {
func (q *Query) addNumVal(val string, asFloat bool) {
elem := q.lastCallStackElem()
if elem == nil || elem.lastField == "" {
panic(fmt.Sprintf("addIntVal called with '%s' when lastField is empty", val))
}
ival := parseNum(val)
ival := parseNum(val, asFloat)
if elem.inList {
if elem.lastCond != ILLEGAL {
list := elem.call.Args[elem.lastField].(*Condition).Value.([]interface{})
@ -968,11 +968,15 @@ func joinUint64Slice(a []uint64) string {
return "[" + strings.Join(other, ",") + "]"
}
func parseNum(val string) interface{} {
func parseNum(val string, asFloat bool) interface{} {
var ival interface{}
var err error
if strings.Contains(val, ".") {
ival, err = strconv.ParseFloat(val, 64)
if asFloat {
ival, err = strconv.ParseFloat(val, 64)
} else {
ival, err = ParseDecimal(val)
}
} else {
ival, err = strconv.ParseInt(val, 10, 64)
}

204
pql/decimal.go Normal file
View file

@ -0,0 +1,204 @@
// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pql
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/pkg/errors"
)
// Decimal represents a decimal value; the intention
// is to avoid relying on float64, and they primary
// purpose is to have a predictable way to encode such
// values used in query strings.
// Sign = true represents a negative value.
// Scale is the number of digits to the right of the
// decimal point.
// Precision is currently not considered; precision, for
// our purposes is implied to be the complete, known value.
type Decimal struct {
Sign bool
Value uint32
Scale int64
}
// ToInt64 returns d as an int64 adjusted to the
// provided scale.
func (d Decimal) ToInt64(scale int64) int64 {
var ret int64
scaleDiff := scale - d.Scale
if scaleDiff == 0 {
ret = int64(d.Value)
} else {
ret = int64(float64(d.Value) * math.Pow10(int(scaleDiff)))
}
if d.Sign {
ret *= -1
}
return ret
}
// String returns the string representation of the decimal.
func (d Decimal) String() string {
var s string
sval := fmt.Sprintf("%d", d.Value)
if d.Scale == 0 {
s = sval
} else if d.Scale < 0 {
s = sval + strings.Repeat("0", int(-1*d.Scale))
} else {
var bufLen int
if int(d.Scale) < len(sval) {
bufLen = len(sval) + 1
} else {
bufLen = int(d.Scale) + 2
}
buf := make([]byte, bufLen)
j := 0
for i := range buf {
z := len(buf) - 1 - i // index into buf from the end
if i == int(d.Scale) {
buf[z] = '.'
continue
}
if len(sval) > j {
buf[z] = sval[len(sval)-1-j]
j++
} else {
buf[z] = '0'
}
}
s = string(buf)
}
if d.Sign {
return "-" + s
}
return s
}
const (
stateSign = "sign"
stateLeadingZeros = "zeros"
stateMantissa = "mantissa"
)
// ParseDecimal parses a string into a Decimal.
func ParseDecimal(s string) (Decimal, error) {
if s == "" {
return Decimal{}, nil
}
var sign bool
var value uint64
var scale int64
var err error
// General steps:
// - Trim leading whitespace/zeros
// - Get the sign value
// - Trim leading zeros
// - Push characters into a buffer
// - Track position of decimal point
// - Trim trailing zeros of buffer
// - value = buffer -> int
// - scale = len(buffer) - tracked position
var decimalPos int = -1
var pos int
mantissa := make([]byte, len(s))
state := stateSign
for i := 0; i < len(s); i++ {
switch state {
case stateSign:
switch s[i] {
case ' ':
continue
case '-':
sign = true
fallthrough
case '+':
state = stateLeadingZeros
default:
state = stateLeadingZeros
i--
}
case stateLeadingZeros:
switch s[i] {
case '0':
continue
default:
state = stateMantissa
i--
}
case stateMantissa:
switch s[i] {
case '.':
if decimalPos == -1 {
decimalPos = pos
} else {
return Decimal{}, errors.Errorf("invalid decimal string: %s", s)
}
continue
default:
mantissa[pos] = s[i]
pos++
}
}
}
// Trim trailing zeros/spaces of mantissa
// for any portion that would have been to the
// right of the decimal.
trimZeroCnt := 0
trimSpaceCnt := 0
for i := len(mantissa) - 1; i >= 0; i-- {
switch mantissa[i] {
case uint8(0), uint8(32): // nil/space
trimSpaceCnt++
continue
case uint8(48): // zero
trimZeroCnt++
continue
}
break
}
mantissa = mantissa[:len(mantissa)-trimSpaceCnt-trimZeroCnt]
// Based on where (or if) the decimal was found,
// calculate scale.
if decimalPos == -1 {
scale = -1 * int64(trimZeroCnt)
} else {
scale = int64(len(mantissa) - decimalPos)
}
value, err = strconv.ParseUint(string(mantissa), 10, 32)
if err != nil {
return Decimal{}, errors.Wrap(err, "converting mantissa to uint32")
}
return Decimal{
Sign: sign,
Value: uint32(value),
Scale: scale,
}, nil
}

134
pql/decimal_test.go Normal file
View file

@ -0,0 +1,134 @@
// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pql_test
import (
"testing"
"github.com/pilosa/pilosa/v2/pql"
)
// Ensure call can be converted into a string.
func TestDecimal(t *testing.T) {
t.Run("Parse", func(t *testing.T) {
tests := []struct {
s string
exp pql.Decimal
expErr error // TODO: add tests for errors
}{
{"123.4567", pql.Decimal{false, 1234567, 4}, nil},
{" 123.4567", pql.Decimal{false, 1234567, 4}, nil},
{" 123.4567 ", pql.Decimal{false, 1234567, 4}, nil},
{"123.456700", pql.Decimal{false, 1234567, 4}, nil},
{"00123.4567", pql.Decimal{false, 1234567, 4}, nil},
{"+123.4567", pql.Decimal{false, 1234567, 4}, nil},
{"-123.4567", pql.Decimal{true, 1234567, 4}, nil},
{"-00123.4567", pql.Decimal{true, 1234567, 4}, nil},
{"-12.25", pql.Decimal{true, 1225, 2}, nil},
{"123", pql.Decimal{false, 123, 0}, nil},
{"-12300", pql.Decimal{true, 123, -2}, nil},
{"+012300", pql.Decimal{false, 123, -2}, nil},
{"12300", pql.Decimal{false, 123, -2}, nil},
{"12300.", pql.Decimal{false, 123, -2}, nil},
{"12300.0", pql.Decimal{false, 123, -2}, nil},
{"123.0", pql.Decimal{false, 123, 0}, nil},
{"0.123", pql.Decimal{false, 123, 3}, nil},
{"0.001230", pql.Decimal{false, 123, 5}, nil},
{" 0.001230 ", pql.Decimal{false, 123, 5}, nil},
{"-0.001230 ", pql.Decimal{true, 123, 5}, nil},
}
for i, test := range tests {
dec, err := pql.ParseDecimal(test.s)
if err != nil {
t.Fatalf("parsing string `%s`: %s", test.s, err)
} else if dec != test.exp {
t.Fatalf("test %d expected: %v, but got: %v", i, test.exp, dec)
}
}
})
t.Run("ToInt64", func(t *testing.T) {
tests := []struct {
dec pql.Decimal
scale int64
exp int64
}{
{pql.Decimal{false, 0, 0}, 0, 0}, // 0 : 0
{pql.Decimal{false, 0, 0}, 1, 0}, // 0 : 0.0
{pql.Decimal{false, 0, 0}, -1, 0}, // 0 : 0
{pql.Decimal{false, 1234567, 4}, 5, 12345670}, // 123.4567 : 123.45670
{pql.Decimal{false, 1234567, 4}, 4, 1234567}, // 123.4567 : 123.4567
{pql.Decimal{false, 1234567, 4}, 3, 123456}, // 123.4567 : 123.456
{pql.Decimal{true, 1234567, 4}, 5, -12345670}, // -123.4567 : -123.45670
{pql.Decimal{true, 1234567, 4}, 4, -1234567}, // -123.4567 : -123.4567
{pql.Decimal{true, 1234567, 4}, 3, -123456}, // -123.4567 : -123.456
{pql.Decimal{false, 123, -2}, 5, 1230000000}, // 12300 : 12300.00000
{pql.Decimal{false, 123, -2}, -1, 1230}, // 12300 : 1230
{pql.Decimal{false, 123, 1}, -1, 1}, // 12.3 : 1
{pql.Decimal{false, 123, 1}, -2, 0}, // 12.3 : 0
}
for i, test := range tests {
v := test.dec.ToInt64(test.scale)
if v != test.exp {
t.Fatalf("test %d expected: %d, but got: %d", i, test.exp, v)
}
}
})
t.Run("String", func(t *testing.T) {
tests := []struct {
s string
exp string
}{
{"123.4567", "123.4567"},
{" 123.4567", "123.4567"},
{" 123.4567 ", "123.4567"},
{"123.456700", "123.4567"},
{"00123.4567", "123.4567"},
{"+123.4567", "123.4567"},
{"-123.4567", "-123.4567"},
{"-00123.4567", "-123.4567"},
{"-12.25", "-12.25"},
{"123", "123"},
{"-12300", "-12300"},
{"+012300", "12300"},
{"12300", "12300"},
{"12300.", "12300"},
{"12300.0", "12300"},
{"123.0", "123"},
{"0.123", "0.123"},
{"0.001230", "0.00123"},
{" 0.001230 ", "0.00123"},
{"-0.001230 ", "-0.00123"},
}
for i, test := range tests {
dec, err := pql.ParseDecimal(test.s)
if err != nil {
t.Fatalf("parsing string `%s`: %s", test.s, err)
}
if str := dec.String(); str != test.exp {
t.Fatalf("test %d expected: %s, but got: %s", i, test.exp, str)
}
}
})
}

View file

@ -96,8 +96,8 @@ func TestParser_Parse(t *testing.T) {
}
})
// Parse with float arguments.
t.Run("WithFloatArgs", func(t *testing.T) {
// Parse with decimal arguments.
t.Run("WithDecimalArgs", func(t *testing.T) {
q, err := pql.ParseString(`Row( key=12.25, foo= 13.167, bar=2., baz=0.9)`)
if err != nil {
t.Fatal(err)
@ -105,10 +105,10 @@ func TestParser_Parse(t *testing.T) {
&pql.Call{
Name: "Row",
Args: map[string]interface{}{
"key": 12.25,
"foo": 13.167,
"bar": 2.,
"baz": 0.9,
"key": pql.Decimal{false, 1225, 2},
"foo": pql.Decimal{false, 13167, 3},
"bar": pql.Decimal{false, 2, 0},
"baz": pql.Decimal{false, 9, 1},
},
},
) {
@ -125,7 +125,7 @@ func TestParser_Parse(t *testing.T) {
&pql.Call{
Name: "Row",
Args: map[string]interface{}{
"key": -12.25,
"key": pql.Decimal{true, 1225, 2},
"foo": int64(-13),
},
},
@ -181,7 +181,7 @@ func TestParser_Parse(t *testing.T) {
Name: "Row",
Args: map[string]interface{}{
"key": "foo",
"x": &pql.Condition{Op: pql.EQ, Value: 12.25},
"x": &pql.Condition{Op: pql.EQ, Value: pql.Decimal{false, 1225, 2}},
"y": &pql.Condition{Op: pql.GTE, Value: int64(100)},
"z": &pql.Condition{Op: pql.BETWEEN, Value: []interface{}{int64(4), int64(8)}},
"m": &pql.Condition{Op: pql.NEQ, Value: nil},

View file

@ -6,22 +6,27 @@ type PQL Peg {
Calls <- sp (Call sp)* !.
Call <- 'Set' {p.startCall("Set")} open col comma args (comma timestamp)? close {p.endCall()}
/ 'SetRowAttrs' {p.startCall("SetRowAttrs")} open posfield comma row comma args close {p.endCall()}
/ 'SetColumnAttrs' {p.startCall("SetColumnAttrs")} open col comma args close {p.endCall()}
/ 'Clear' {p.startCall("Clear")} open col comma args close {p.endCall()}
/ 'ClearRow' {p.startCall("ClearRow")} open arg close {p.endCall()}
/ 'Store' {p.startCall("Store")} open Call comma arg close {p.endCall()}
Call <- 'Set' {p.startCall("Set")} open col comma dargs (comma timestamp)? close {p.endCall()}
/ 'SetRowAttrs' {p.startCall("SetRowAttrs")} open posfield comma row comma fargs close {p.endCall()}
/ 'SetColumnAttrs' {p.startCall("SetColumnAttrs")} open col comma fargs close {p.endCall()}
/ 'Clear' {p.startCall("Clear")} open col comma dargs close {p.endCall()}
/ 'ClearRow' {p.startCall("ClearRow")} open darg close {p.endCall()}
/ 'Store' {p.startCall("Store")} open Call comma darg close {p.endCall()}
/ 'TopN' {p.startCall("TopN")} open posfield (comma allargs)? close {p.endCall()}
/ 'Rows' {p.startCall("Rows")} open posfield (comma allargs)? close {p.endCall()}
/ 'Range' {p.startCall("Range")} open field sp '=' sp value comma 'from='? {p.addField("from")} timestampfmt {p.addVal(buffer[begin:end])} comma 'to='? sp {p.addField("to")} timestampfmt {p.addVal(buffer[begin:end])} close {p.endCall()}
/ 'Range' {p.startCall("Range")} open field sp '=' sp fvalue comma 'from='? {p.addField("from")} timestampfmt {p.addVal(buffer[begin:end])} comma 'to='? sp {p.addField("to")} timestampfmt {p.addVal(buffer[begin:end])} close {p.endCall()}
/ < IDENT > { p.startCall(buffer[begin:end] ) } open allargs comma? close { p.endCall() }
allargs <- Call (comma Call)* (comma args)? / args / sp
args <- arg (comma args)? sp
arg <- ( field sp '=' sp value
/ field sp COND sp value
/ conditional
)
allargs <- Call (comma Call)* (comma dargs)? / dargs / sp
fargs <- farg (comma fargs)? sp
farg <- ( field sp '=' sp fvalue
/ field sp COND sp fvalue
/ conditional
)
dargs <- darg (comma dargs)? sp
darg <- ( field sp '=' sp dvalue
/ field sp COND sp dvalue
/ conditional
)
COND <- ( '><' { p.addBTWN() }
/ '<=' { p.addLTE() }
/ '>=' { p.addGTE() }
@ -36,21 +41,38 @@ condint <- < '-'? [0-9]* '.' [0-9]+ / '0' / '-'? [1-9] [0-9]* > sp {p.condAdd(bu
condLT <- <('<=' / '<')> sp {p.condAdd(buffer[begin:end])}
condfield <- <fieldExpr> sp {p.condAdd(buffer[begin:end])}
value <- ( item
/ lbrack { p.startList() } list rbrack { p.endList() }
dvalue <- ( ditem
/ lbrack { p.startList() } dlist rbrack { p.endList() }
)
list <- item (comma list)?
item <- ( 'null' &(comma / sp close) { p.addVal(nil) }
fvalue <- ( fitem
/ lbrack { p.startList() } flist rbrack { p.endList() }
)
dlist <- ditem (comma dlist)?
flist <- fitem (comma flist)?
ditem <- ( itema
/ decimal
/ itemb
)
fitem <- ( itema
/ float
/ itemb
)
itema <- ( 'null' &(comma / sp close) { p.addVal(nil) }
/ 'true' &(comma / sp close) { p.addVal(true) }
/ 'false' &(comma / sp close) { p.addVal(false) }
/ timestampfmt { p.addVal(buffer[begin:end]) }
/ < '-'? [0-9]+ ('.'[0-9]*)? > { p.addNumVal(buffer[begin:end]) }
/ < '-'? '.'[0-9]+ > { p.addNumVal(buffer[begin:end]) }
/ < IDENT > { p.startCall(buffer[begin:end]) } open allargs comma? close { p.addVal(p.endCall()) }
)
itemb <- ( < IDENT > { p.startCall(buffer[begin:end]) } open allargs comma? close { p.addVal(p.endCall()) }
/ < ([[A-Z]] / [0-9] / '-' / '_' / ':')+ > { p.addVal(buffer[begin:end]) }
/ < '"' doublequotedstring '"' > { s, _ := strconv.Unquote(buffer[begin:end]); p.addVal(s) }
/ '\'' < singlequotedstring > '\'' { p.addVal(buffer[begin:end]) }
)
float <- ( < '-'? [0-9]+ ('.'[0-9]*)? > { p.addNumVal(buffer[begin:end], true) }
/ < '-'? '.'[0-9]+ > { p.addNumVal(buffer[begin:end], true) }
)
decimal <- ( < '-'? [0-9]+ ('.'[0-9]*)? > { p.addNumVal(buffer[begin:end], false) }
/ < '-'? '.'[0-9]+ > { p.addNumVal(buffer[begin:end], false) }
)
doublequotedstring <- ( '\\"' / '\\\\' / [^"] )*
singlequotedstring <- ( '\\\'' / '\\\\' / [^'] )*

File diff suppressed because it is too large Load diff