1
0
Fork 0
bocadillo/mysql/decimal.go

133 lines
3.3 KiB
Go

package mysql
import (
"bytes"
"database/sql/driver"
"encoding/binary"
"encoding/json"
"fmt"
"strconv"
"strings"
)
// Decimal represents a decimal type that retains precision until converted to
// a float. It is designed to be marshaled into JSON without losing precision.
type Decimal struct {
str string
}
// DecodeDecimal decodes a decimal value.
// Implementation borrowed from https://github.com/siddontang/go-mysql/
func DecodeDecimal(data []byte, precision int, decimals int) (Decimal, int) {
const digitsPerInteger int = 9
var compressedBytes = [...]int{0, 1, 1, 2, 2, 3, 3, 4, 4, 4}
decodeDecimalDecompressValue := func(compIndx int, data []byte, mask uint8) (size int, value uint32) {
size = compressedBytes[compIndx]
databuff := make([]byte, size)
for i := 0; i < size; i++ {
databuff[i] = data[i] ^ mask
}
value = uint32(DecodeVarLen64BigEndian(databuff))
return
}
// See python mysql replication and https://github.com/jeremycole/mysql_binlog
integral := (precision - decimals)
uncompIntegral := int(integral / digitsPerInteger)
uncompFractional := int(decimals / digitsPerInteger)
compIntegral := integral - (uncompIntegral * digitsPerInteger)
compFractional := decimals - (uncompFractional * digitsPerInteger)
binSize := uncompIntegral*4 + compressedBytes[compIntegral] +
uncompFractional*4 + compressedBytes[compFractional]
buf := make([]byte, binSize)
copy(buf, data[:binSize])
// Must copy the data for later change
data = buf
// Support negative
// The sign is encoded in the high bit of the the byte
// But this bit can also be used in the value
value := uint32(data[0])
var res bytes.Buffer
var mask uint32
if value&0x80 == 0 {
mask = uint32((1 << 32) - 1)
res.WriteString("-")
}
// Clear sign
data[0] ^= 0x80
pos, value := decodeDecimalDecompressValue(compIntegral, data, uint8(mask))
res.WriteString(fmt.Sprintf("%d", value))
for i := 0; i < uncompIntegral; i++ {
value = binary.BigEndian.Uint32(data[pos:]) ^ mask
pos += 4
res.WriteString(fmt.Sprintf("%09d", value))
}
res.WriteString(".")
for i := 0; i < uncompFractional; i++ {
value = binary.BigEndian.Uint32(data[pos:]) ^ mask
pos += 4
res.WriteString(fmt.Sprintf("%09d", value))
}
if size, value := decodeDecimalDecompressValue(compFractional, data[pos:], uint8(mask)); size > 0 {
res.WriteString(fmt.Sprintf("%0*d", compFractional, value))
pos += size
}
return NewDecimal(res.String()), pos
}
// NewDecimal creates a new decimal with given value.
func NewDecimal(str string) Decimal {
var sign string
if str[0] == '-' {
str = str[1:]
sign = "-"
}
str = strings.Trim(str, "0")
if str[0] == '.' {
str = "0" + str
}
if str[len(str)-1] == '.' {
str += "0"
}
return Decimal{sign + str}
}
// Float64 returns a float representation of the decimal. Precision could be
// lost.
func (d Decimal) Float64() float64 {
f, _ := strconv.ParseFloat(d.str, 64)
return f
}
var _ json.Marshaler = Decimal{}
// MarshalJSON returns the JSON encoding of the decimal.
func (d Decimal) MarshalJSON() ([]byte, error) {
return []byte(d.str), nil
}
var _ fmt.Stringer = Decimal{}
func (d Decimal) String() string {
return d.str
}
var _ driver.Valuer = Decimal{}
// Value returns a driver Value.
func (d Decimal) Value() (driver.Value, error) {
return d.str, nil
}