mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-11 15:21:02 +00:00
It turns out that it's not very useful to keep the sign value as a separate argument in the pql.Decimal struct. This commit incorporates it into Value, and makes Value an `int64` (for some bone-headed reason I had made it a `uint32` before which is just dumb).
240 lines
5.2 KiB
Go
240 lines
5.2 KiB
Go
// 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 the primary
|
|
// purpose is to have a predictable way to encode such
|
|
// values used in query strings.
|
|
// 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 {
|
|
Value int64
|
|
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 = d.Value
|
|
} else {
|
|
ret = int64(float64(d.Value) * math.Pow10(int(scaleDiff)))
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Float64 returns d as a float64.
|
|
func (d Decimal) Float64() float64 {
|
|
var ret float64
|
|
if d.Scale == 0 {
|
|
ret = float64(d.Value)
|
|
} else {
|
|
ret = float64(d.Value) / math.Pow10(int(d.Scale))
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// String returns the string representation of the decimal.
|
|
func (d Decimal) String() string {
|
|
var s string
|
|
|
|
var neg bool
|
|
sval := fmt.Sprintf("%d", d.Value)
|
|
|
|
// Strip the negative sign off for now, and
|
|
// re-apply it at the end.
|
|
if sval[0] == '-' {
|
|
neg = true
|
|
sval = sval[1:]
|
|
}
|
|
|
|
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 neg {
|
|
return "-" + s
|
|
}
|
|
return s
|
|
}
|
|
|
|
const (
|
|
stateSign = "sign"
|
|
stateLeadingZeros = "zeros"
|
|
stateMantissa = "mantissa"
|
|
)
|
|
|
|
// ParseDecimal parses a string into a Decimal.
|
|
func ParseDecimal(s string) (Decimal, error) {
|
|
var sign bool
|
|
var value int64
|
|
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
|
|
var foundLeadingZero bool
|
|
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':
|
|
foundLeadingZero = true
|
|
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++
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we've gotten here and state is still in stateSign or
|
|
// it's in stateLeadingZeros without finding any zeros,
|
|
// it means no value was provided.
|
|
if state == stateSign || (state == stateLeadingZeros && !foundLeadingZero) {
|
|
return Decimal{}, errors.New("decimal string is empty")
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// If mantissa is empty, treat it as "0".
|
|
if len(mantissa) == 0 {
|
|
mantissa = []byte{'0'}
|
|
sign = false
|
|
scale = 0
|
|
}
|
|
|
|
value, err = strconv.ParseInt(string(mantissa), 10, 64)
|
|
if err != nil {
|
|
return Decimal{}, errors.Wrap(err, "converting mantissa to uint32")
|
|
}
|
|
// Because we pulled the sign off at the beginning, if value is
|
|
// negative here, it likely means the string had two "-"" characters.
|
|
if value < 0 {
|
|
return Decimal{}, errors.New("invalid negative value")
|
|
}
|
|
|
|
if sign {
|
|
value *= -1
|
|
}
|
|
|
|
return Decimal{
|
|
Value: value,
|
|
Scale: scale,
|
|
}, nil
|
|
}
|