diff --git a/decimal.go b/decimal.go index eb0c833..49e6d56 100644 --- a/decimal.go +++ b/decimal.go @@ -530,6 +530,108 @@ func (d Decimal) String() string { return string(buf[pos+1:]) } +// ParseBCD converts a [packed BCD] representation to a decimal. +// +// [packed BCD]: https://en.wikipedia.org/wiki/Binary-coded_decimal#Packed_BCD +func ParseBCD(b []byte) (Decimal, error) { + var pos int + width := len(b) + + // Coefficient and sign + var neg bool + var coef fint + var ok bool + for pos < width { + hi := b[pos] >> 4 + lo := b[pos] & 0x0f + + if hi > 9 { + return Decimal{}, fmt.Errorf("%w: invalid high nibble \"%x\"", errInvalidDecimal, b[pos]) + } + coef, ok = coef.fsa(1, hi) + if !ok { + return Decimal{}, errDecimalOverflow + } + + if lo > 9 { + if lo == 0x0d { + neg = true + } else if lo != 0x0c { + return Decimal{}, fmt.Errorf("%w: invalid low nibble \"%x\"", errInvalidDecimal, b[pos]) + } + pos++ + break + } + coef, ok = coef.fsa(1, lo) + if !ok { + return Decimal{}, errDecimalOverflow + } + pos++ + } + + // Scale + var scale int + var hasScale bool + if pos < width { + hi := b[pos] >> 4 + lo := b[pos] & 0x0f + hasScale = true + + if hi > 1 { + return Decimal{}, fmt.Errorf("%w: invalid high nibble \"%x\"", errInvalidDecimal, b[pos]) + } + scale = int(hi) * 10 + + if lo > 9 { + return Decimal{}, fmt.Errorf("%w: invalid low nibble \"%x\"", errInvalidDecimal, b[pos]) + } + scale += int(lo) + + pos++ + } + + if pos != width { + return Decimal{}, fmt.Errorf("%w: unexpected byte \"%x\"", errInvalidDecimal, b[pos]) + } + if !hasScale { + return Decimal{}, fmt.Errorf("%w: no scale", errInvalidDecimal) + } + + return newSafe(neg, coef, scale) +} + +// BCD returns a [packed BCD] representation of a decimal. +// +// [packed BCD]: https://en.wikipedia.org/wiki/Binary-coded_decimal#Packed_BCD +func (d Decimal) BCD() []byte { + var buf [11]byte + pos := len(buf) - 1 + coef := d.Coef() + scale := d.Scale() + + // Scale + buf[pos] = byte(scale/10)<<4 | byte(scale%10) + pos-- + + // Sign and first digit + if d.IsNeg() { + buf[pos] = byte(coef%10)<<4 | 0x0d + } else { + buf[pos] = byte(coef%10)<<4 | 0x0c + } + pos-- + coef /= 10 + + // Coefficient + for coef > 0 { + buf[pos] = byte(coef/10%10)<<4 | byte(coef%10) + pos-- + coef /= 100 + } + + return buf[pos+1:] +} + // Float64 returns the nearest binary floating-point number rounded // using [rounding half to even] (banker's rounding). // See also constructor [NewFromFloat64]. diff --git a/decimal_test.go b/decimal_test.go index e77cda5..a07f2f5 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -1,6 +1,7 @@ package decimal import ( + "bytes" "database/sql" "database/sql/driver" "encoding" @@ -559,6 +560,136 @@ func TestDecimal_String(t *testing.T) { }) } +func TestParseBCD(t *testing.T) { + t.Run("success", func(t *testing.T) { + tests := []struct { + bcd []byte + want string + }{ + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x00}, "-9999999999999999999"}, + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x01}, "-999999999999999999.9"}, + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x02}, "-99999999999999999.99"}, + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x03}, "-9999999999999999.999"}, + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x19}, "-0.9999999999999999999"}, + {[]byte{0x1d, 0x00}, "-1"}, + {[]byte{0x1d, 0x01}, "-0.1"}, + {[]byte{0x1d, 0x02}, "-0.01"}, + {[]byte{0x1d, 0x19}, "-0.0000000000000000001"}, + {[]byte{0x0c, 0x00}, "0"}, + {[]byte{0x0c, 0x01}, "0.0"}, + {[]byte{0x0c, 0x02}, "0.00"}, + {[]byte{0x0c, 0x19}, "0.0000000000000000000"}, + {[]byte{0x1c, 0x00}, "1"}, + {[]byte{0x1c, 0x01}, "0.1"}, + {[]byte{0x1c, 0x02}, "0.01"}, + {[]byte{0x1c, 0x19}, "0.0000000000000000001"}, + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x00}, "9999999999999999999"}, + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x01}, "999999999999999999.9"}, + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x02}, "99999999999999999.99"}, + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x03}, "9999999999999999.999"}, + {[]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x19}, "0.9999999999999999999"}, + + // Exported constants + {[]byte{0x1d, 0x00}, NegOne.String()}, + {[]byte{0x0c, 0x00}, Zero.String()}, + {[]byte{0x1c, 0x00}, One.String()}, + {[]byte{0x2c, 0x00}, Two.String()}, + {[]byte{0x01, 0x0c, 0x00}, Ten.String()}, + {[]byte{0x10, 0x0c, 0x00}, Hundred.String()}, + {[]byte{0x01, 0x00, 0x0c, 0x00}, Thousand.String()}, + {[]byte{0x27, 0x18, 0x28, 0x18, 0x28, 0x45, 0x90, 0x45, 0x23, 0x5c, 0x18}, E.String()}, + {[]byte{0x31, 0x41, 0x59, 0x26, 0x53, 0x58, 0x97, 0x93, 0x23, 0x8c, 0x18}, Pi.String()}, + } + for _, tt := range tests { + got, err := ParseBCD(tt.bcd) + if err != nil { + t.Errorf("parseBCD(% x) failed: %v", tt.bcd, err) + continue + } + want := MustParse(tt.want) + if got != want { + t.Errorf("parseBCD(% x) = %q, want %q", tt.bcd, got, want) + } + } + }) + + t.Run("error", func(t *testing.T) { + tests := map[string][]byte{ + "empty": {}, + "invalid nibble 1": {0x0f}, + "invalid nibble 2": {0xf0}, + "invalid nibble 3": {0x0c, 0x0f}, + "invalid nibble 4": {0x0c, 0xf0}, + "decimal overflow 1": {0x09, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x00}, + "decimal overflow 2": {0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x00}, + "no sign": {0x00}, + "scale overflow": {0x0c, 0x00, 0x00}, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + _, err := ParseBCD(tt) + if err == nil { + t.Errorf("parseBCD(% x) did not fail", tt) + } + }) + } + }) +} + +func TestDecimal_BCD(t *testing.T) { + t.Run("success", func(t *testing.T) { + tests := []struct { + d string + want []byte + }{ + {"-9999999999999999999", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x00}}, + {"-999999999999999999.9", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x01}}, + {"-99999999999999999.99", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x02}}, + {"-9999999999999999.999", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x03}}, + {"-0.9999999999999999999", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9d, 0x19}}, + {"-1", []byte{0x1d, 0x00}}, + {"-0.1", []byte{0x1d, 0x01}}, + {"-0.01", []byte{0x1d, 0x02}}, + {"-0.0000000000000000001", []byte{0x1d, 0x19}}, + {"0", []byte{0x0c, 0x00}}, + {"0.0", []byte{0x0c, 0x01}}, + {"0.00", []byte{0x0c, 0x02}}, + {"0.0000000000000000000", []byte{0x0c, 0x19}}, + {"1", []byte{0x1c, 0x00}}, + {"0.1", []byte{0x1c, 0x01}}, + {"0.01", []byte{0x1c, 0x02}}, + {"0.0000000000000000001", []byte{0x1c, 0x19}}, + {"9999999999999999999", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x00}}, + {"999999999999999999.9", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x01}}, + {"99999999999999999.99", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x02}}, + {"9999999999999999.999", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x03}}, + {"0.9999999999999999999", []byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9c, 0x19}}, + + // Exported constants + {NegOne.String(), []byte{0x1d, 0x00}}, + {Zero.String(), []byte{0x0c, 0x00}}, + {One.String(), []byte{0x1c, 0x00}}, + {Two.String(), []byte{0x2c, 0x00}}, + {Ten.String(), []byte{0x01, 0x0c, 0x00}}, + {Hundred.String(), []byte{0x10, 0x0c, 0x00}}, + {Thousand.String(), []byte{0x01, 0x00, 0x0c, 0x00}}, + {E.String(), []byte{0x27, 0x18, 0x28, 0x18, 0x28, 0x45, 0x90, 0x45, 0x23, 0x5c, 0x18}}, + {Pi.String(), []byte{0x31, 0x41, 0x59, 0x26, 0x53, 0x58, 0x97, 0x93, 0x23, 0x8c, 0x18}}, + } + for _, tt := range tests { + d, err := Parse(tt.d) + if err != nil { + t.Errorf("Parse(%q) failed: %v", tt.d, err) + continue + } + got := d.BCD() + if !bytes.Equal(got, tt.want) { + t.Errorf("Parse(%q).bcd() = % x, want % x", tt.d, got, tt.want) + } + } + }) +} + func TestDecimal_Float64(t *testing.T) { tests := []struct { d string