Skip to content

Commit

Permalink
decimal: implement binary marshaling
Browse files Browse the repository at this point in the history
  • Loading branch information
eapenkin authored May 17, 2024
2 parents 3bfb5aa + dfc427b commit 7568d53
Show file tree
Hide file tree
Showing 5 changed files with 403 additions and 53 deletions.
105 changes: 54 additions & 51 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,79 +12,82 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: false

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: false
- name: Check out code
uses: actions/checkout@v4

- name: Check out code
uses: actions/checkout@v4
- name: Verify code formatting
run: gofmt -s -w . && git diff --exit-code

- name: Verify code formatting
run: gofmt -s -w . && git diff --exit-code
- name: Verify dependency consistency
run: go get -u -t . && go mod tidy && git diff --exit-code

- name: Verify dependency consistency
run: go get -u -t . && go mod tidy && git diff --exit-code
- name: Verify generated code
run: go generate ./... && git diff --exit-code

- name: Verify generated code
run: go generate ./... && git diff --exit-code
- name: Verify potential issues
uses: golangci/golangci-lint-action@v4

- name: Verify potential issues
uses: golangci/golangci-lint-action@v4
- name: Run tests with coverage
run: go test -race -shuffle=on -coverprofile="coverage.txt" -covermode=atomic ./...

- name: Run tests with coverage
run: go test -race -shuffle=on -coverprofile="coverage.txt" -covermode=atomic ./...

- name: Upload test coverage
if: matrix.os == 'ubuntu-latest' && matrix.go-version == 'stable'
uses: codecov/codecov-action@v3
- name: Upload test coverage
if: matrix.os == 'ubuntu-latest' && matrix.go-version == 'stable'
uses: codecov/codecov-action@v3

fuzz:
needs: test
runs-on: ubuntu-latest
steps:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: stable
cache: false

- name: Check out code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: stable
cache: false
- name: Run fuzzing for string parsing
run: go test -fuzztime 20s -fuzz ^FuzzParse$

- name: Check out code
uses: actions/checkout@v4
- name: Run fuzzing for binary parsing
run: go test -fuzztime 20s -fuzz ^FuzzBCD$

- name: Run fuzzing for string parsing
run: go test -fuzztime 20s -fuzz ^FuzzParse$
- name: Run fuzzing for string conversion
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_String$

- name: Run fuzzing for string conversion
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_String$
- name: Run fuzzing for binary conversion
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_BCD$

- name: Run fuzzing for float64 conversion
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Float64$
- name: Run fuzzing for float64 conversion
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Float64$

- name: Run fuzzing for int64 conversion
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Int64$
- name: Run fuzzing for int64 conversion
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Int64$

- name: Run fuzzing for addition
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Add$
- name: Run fuzzing for addition
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Add$

- name: Run fuzzing for multiplication
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Mul$
- name: Run fuzzing for multiplication
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Mul$

- name: Run fuzzing for fused multiply-addition
run: go test -fuzztime 60s -fuzz ^FuzzDecimal_FMA$
- name: Run fuzzing for fused multiply-addition
run: go test -fuzztime 60s -fuzz ^FuzzDecimal_FMA$

- name: Run fuzzing for division
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Quo$
- name: Run fuzzing for division
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Quo$

- name: Run fuzzing for integer division and remainder
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_QuoRem$
- name: Run fuzzing for integer division and remainder
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_QuoRem$

- name: Run fuzzing for comparison
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Cmp$
- name: Run fuzzing for comparison
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_Cmp$

- name: Run fuzzing for comparison and subtraction
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_CmpSub$

- name: Run fuzzing for comparison and subtraction
run: go test -fuzztime 20s -fuzz ^FuzzDecimal_CmpSub$
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.1.25] - 2024-05-17

### Added

- Implemented binary marshaling.

## [0.1.24] - 2024-05-05

### Changed
Expand Down
118 changes: 118 additions & 0 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,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("parsing \"%x\": invalid high nibble: %w", b[pos], errInvalidDecimal)
}
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("parsing \"%x\": invalid low nibble: %w", b[pos], errInvalidDecimal)
}
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("parsing \"%x\": invalid high nibble: %w", b[pos], errInvalidDecimal)
}
scale = int(hi) * 10

if lo > 9 {
return Decimal{}, fmt.Errorf("parsing \"%x\": invalid low nibble: %w", b[pos], errInvalidDecimal)
}
scale += int(lo)

pos++
}

if pos != width {
return Decimal{}, fmt.Errorf("invalid byte \"%x\": %w", b[pos], errInvalidDecimal)
}
if !hasScale {
return Decimal{}, fmt.Errorf("no scale: %w", 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].
Expand Down Expand Up @@ -602,6 +704,22 @@ func (d Decimal) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}

// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface.
//
// [encoding.BinaryUnmarshaler]: https://pkg.go.dev/encoding#BinaryUnmarshaler
func (d *Decimal) UnmarshalBinary(data []byte) error {
var err error
*d, err = parseBCD(data)
return err
}

// MarshalBinary implements the [encoding.BinaryMarshaler] interface.
//
// [encoding.BinaryMarshaler]: https://pkg.go.dev/encoding#BinaryMarshaler
func (d Decimal) MarshalBinary() ([]byte, error) {
return d.bcd(), nil
}

// Scan implements the [sql.Scanner] interface.
// See also constructor [Parse].
//
Expand Down
Loading

0 comments on commit 7568d53

Please sign in to comment.