Created
December 26, 2019 08:41
-
-
Save josephglanville/d1453fcf8a249721950026c0e376810a to your computer and use it in GitHub Desktop.
Logical encoding/decoding of Avro Decimal
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package logical | |
import ( | |
"github.com/shopspring/decimal" | |
"math" | |
"math/big" | |
) | |
func EncodeDecimal(d decimal.Decimal, scale int32) []byte { | |
unscaled := d.Coefficient() | |
exp := d.Exponent() | |
delta := exp + scale | |
rescaled := big.NewInt(0).Mul(bigPow10(int(delta)), unscaled) | |
data := writeBigInt(rescaled) | |
return data | |
} | |
func DecodeDecimal(data []byte, scale int32, precision int32) decimal.Decimal { | |
coefficient := readBigInt(data) | |
exp := -scale | |
return decimal.NewFromBigInt(coefficient, exp).Truncate(precision) | |
} | |
func bigPow10(a int) *big.Int { | |
powRes := big.NewFloat(math.Pow10(a)) | |
newInt, _ := powRes.Int(nil) | |
return newInt | |
} | |
var bigOne = big.NewInt(1) | |
func writeBigInt(n *big.Int) []byte { | |
data := make([]byte, (n.BitLen()+8)/8) | |
off := 0 // Current offset into data | |
if n.Sign() < 0 { | |
// A negative number has to be converted to two's-complement | |
// form. So we'll invert and subtract 1. If the | |
// most-significant-bit isn't set then we'll need to pad the | |
// beginning with 0xff in order to keep the number negative. | |
nMinus1 := new(big.Int).Neg(n) | |
nMinus1.Sub(nMinus1, bigOne) | |
bytes := nMinus1.Bytes() | |
for i := range bytes { | |
bytes[i] ^= 0xff | |
} | |
if len(bytes) == 0 || bytes[0]&0x80 == 0 { | |
data[off] = 0xff | |
off++ | |
} | |
for i := range bytes { | |
data[off] = bytes[i] | |
off++ | |
} | |
return data | |
} else if n.Sign() == 0 { | |
// Zero is written as a single 0 zero rather than no bytes. | |
data[off] = 0x00 | |
return data | |
} else { | |
bytes := n.Bytes() | |
if len(bytes) > 0 && bytes[0]&0x80 != 0 { | |
// We'll have to pad this with 0x00 in order to stop it | |
// looking like a negative number. | |
data[off] = 0x00 | |
off++ | |
} | |
for i := range bytes { | |
data[off] = bytes[i] | |
off++ | |
} | |
return data | |
} | |
} | |
func readBigInt(bytes []byte) *big.Int { | |
ret := new(big.Int) | |
if len(bytes) > 0 && bytes[0]&0x80 == 0x80 { | |
// This is a negative number. | |
notBytes := make([]byte, len(bytes)) | |
for i := range notBytes { | |
notBytes[i] = ^bytes[i] | |
} | |
ret.SetBytes(notBytes) | |
ret.Add(ret, bigOne) | |
ret.Neg(ret) | |
return ret | |
} | |
ret.SetBytes(bytes) | |
return ret | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment