Skip to content

Commit

Permalink
Merge pull request #6 from govalues/quo-rem-method
Browse files Browse the repository at this point in the history
money: implement Amount.QuoRem method
  • Loading branch information
eapenkin authored Aug 15, 2023
2 parents af61748 + 8a1fdd0 commit 9beff69
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 113 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.19.x, 1.20.x]
go-version: [oldstable, stable]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
Expand All @@ -23,7 +23,7 @@ jobs:

- 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

Expand All @@ -32,7 +32,7 @@ jobs:

- name: Verify potential issues
uses: golangci/golangci-lint-action@v3

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

Expand Down
16 changes: 11 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
# Changelog

## [0.1.2] - 2023-08-15

### Added

- Implemented `Amount.QuoRem` method.

## [0.1.1] - 2023-08-04

### Changed

- Implemented 'scale' argument for Amount.Int64 method.
- Implemented `scale` argument for `Amount.Int64` method.

## [0.1.0] - 2023-06-04

### Changed

- All methods return error instead of panicing.
- Renamed Amount.Round to Amount.Rescale.
- Renamed ExchangeRate.Round to ExchangeRate.Rescale.
- Renamed `Amount.Round` to `Amount.Rescale`.
- Renamed `ExchangeRate.Round` to `ExchangeRate.Rescale`.

## [0.0.3] - 2023-04-22

Expand All @@ -24,8 +30,8 @@

### Added

- Implemented Amount.Int64 method.
- Implemented Amount.Float64 method.
- Implemented `Amount.Int64` method.
- Implemented `Amount.Float64` method.

### Changed

Expand Down
45 changes: 43 additions & 2 deletions amount.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,40 @@ func (a Amount) Quo(e decimal.Decimal) (Amount, error) {
return NewAmount(a.Curr(), d)
}

// QuoRem returns the quotient q and remainder r of decimals d and e
// such that d = e * q + r, where q has scale equal to the scale of the currency.
//
// QuoRem returns an error if:
// - the integer part of the quotient q has more than [MaxPrec] digits;
// - the divisor e is zero.
func (a Amount) QuoRem(e decimal.Decimal) (q, r Amount, err error) {
q, r, err = a.quoRem(e)
if err != nil {
return Amount{}, Amount{}, fmt.Errorf("[⌊%q / %v⌋ %q mod %v]: %w", a, e, a, e, err)
}
return q, r, nil
}

func (a Amount) quoRem(e decimal.Decimal) (q, r Amount, err error) {
// Quotient
q, err = a.Quo(e)
if err != nil {
return Amount{}, Amount{}, err
}
q = q.Trunc(a.Curr().Scale())

// Reminder
r, err = q.Mul(e)
if err != nil {
return Amount{}, Amount{}, err
}
r, err = a.Sub(r)
if err != nil {
return Amount{}, Amount{}, err
}
return q, r, nil
}

// Rat returns the (possibly rounded) ratio between amounts a and b.
// This method is particularly useful for calculating exchange rates between
// two currencies or determining percentages within a single currency.
Expand All @@ -323,6 +357,14 @@ func (a Amount) Rat(b Amount) (decimal.Decimal, error) {
//
// Split returns an error if the number of parts is not a positive integer.
func (a Amount) Split(parts int) ([]Amount, error) {
r, err := a.split(parts)
if err != nil {
return nil, fmt.Errorf("splitting %q into %v parts: %w", a, parts, err)
}
return r, nil
}

func (a Amount) split(parts int) ([]Amount, error) {
// Parts
div, err := decimal.New(int64(parts), 0)
if err != nil {
Expand Down Expand Up @@ -350,7 +392,7 @@ func (a Amount) Split(parts int) ([]Amount, error) {
}
ulp := rem.ULP().CopySign(rem)

// Distribute remainder
// Reminder distribution
res := make([]Amount, parts)
for i := 0; i < parts; i++ {
res[i] = quo
Expand All @@ -365,7 +407,6 @@ func (a Amount) Split(parts int) ([]Amount, error) {
}
}
}

return res, nil
}

Expand Down
51 changes: 51 additions & 0 deletions amount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,57 @@ func TestAmount_Quo(t *testing.T) {
})
}

func TestAmount_QuoRem(t *testing.T) {
t.Run("success", func(t *testing.T) {
tests := []struct {
c, a, e, wantQuo, wantRem string
}{
{"USD", "1.00", "1", "1.00", "0.00"},
{"USD", "2.00", "1", "2.00", "0.00"},
{"USD", "1.00", "2", "0.50", "0.00"},
{"USD", "2.00", "2", "1.00", "0.00"},
{"USD", "0.00", "1", "0.00", "0.00"},
{"USD", "1.510", "3", "0.50", "0.010"},
{"USD", "3.333", "3", "1.11", "0.003"},
{"USD", "2.401", "1", "2.40", "0.001"},
{"USD", "2.401", "-1", "-2.40", "0.001"},
{"USD", "-2.401", "1", "-2.40", "-0.001"},
{"USD", "-2.401", "-1", "2.40", "-0.001"},
}
for _, tt := range tests {
a := MustParseAmount(tt.c, tt.a)
e := decimal.MustParse(tt.e)
gotQuo, gotRem, err := a.QuoRem(e)
if err != nil {
t.Errorf("%q.QuoRem(%q) failed: %v", a, e, err)
continue
}
wantQuo := MustParseAmount(tt.c, tt.wantQuo)
wantRem := MustParseAmount(tt.c, tt.wantRem)
if gotQuo != wantQuo || gotRem != wantRem {
t.Errorf("%q.QuoRem(%q) = [%q %q], want [%q %q]", a, e, gotQuo, gotRem, wantQuo, wantRem)
}
}
})

t.Run("error", func(t *testing.T) {
tests := map[string]struct {
c, a, e string
}{
"zero 1": {"USD", "1", "0"},
"overflow 1": {"USD", "99999999999999999", "0.1"},
}
for _, tt := range tests {
a := MustParseAmount(tt.c, tt.a)
e := decimal.MustParse(tt.e)
_, _, err := a.QuoRem(e)
if err == nil {
t.Errorf("%q.QuoRem(%q) did not fail", a, e)
}
}
})
}

func TestAmount_Mul(t *testing.T) {
t.Run("success", func(t *testing.T) {
tests := []struct {
Expand Down
Loading

0 comments on commit 9beff69

Please sign in to comment.