From 8a1fdd0c5aba529b947cd96a11f6f95e941559a9 Mon Sep 17 00:00:00 2001 From: Eugene Apenkin Date: Tue, 15 Aug 2023 21:23:02 +0400 Subject: [PATCH] money: implement Amount.QuoRem method --- .github/workflows/go.yml | 6 +- CHANGELOG.md | 16 ++- amount.go | 45 ++++++++- amount_test.go | 51 ++++++++++ doc_test.go | 207 ++++++++++++++++++++------------------- go.mod | 2 +- go.sum | 4 +- 7 files changed, 218 insertions(+), 113 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 945ec3b..c2b4a5d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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: @@ -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 @@ -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 ./... diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf0bb8..b5c967c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -24,8 +30,8 @@ ### Added -- Implemented Amount.Int64 method. -- Implemented Amount.Float64 method. +- Implemented `Amount.Int64` method. +- Implemented `Amount.Float64` method. ### Changed diff --git a/amount.go b/amount.go index 7029aea..2f70383 100644 --- a/amount.go +++ b/amount.go @@ -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. @@ -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 { @@ -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 @@ -365,7 +407,6 @@ func (a Amount) Split(parts int) ([]Amount, error) { } } } - return res, nil } diff --git a/amount_test.go b/amount_test.go index aa99792..bf8b7c4 100644 --- a/amount_test.go +++ b/amount_test.go @@ -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 { diff --git a/doc_test.go b/doc_test.go index b7631af..18c4055 100644 --- a/doc_test.go +++ b/doc_test.go @@ -417,6 +417,91 @@ func Example_loanAmortization() { // Total 12659.88 11999.98 659.90 } +func ParseISO8583(s string) (money.Amount, error) { + // Currency + c, err := money.ParseCurr(s[:3]) + if err != nil { + return money.Amount{}, err + } + // Amount + n, err := strconv.ParseInt(s[4:], 10, 64) + if err != nil { + return money.Amount{}, err + } + d, err := decimal.New(n, c.Scale()) + if err != nil { + return money.Amount{}, err + } + // Sign + if s[3:4] == "D" { + d = d.Neg() + } + return money.NewAmount(c, d) +} + +// In this example, we parse the string "840D000000001234", which represents -12.34 USD, +// according to the specification for "DE54, Additional Amounts" in ISO 8583. +func Example_parsingISO8583() { + a, err := ParseISO8583("840D000000001234") + if err != nil { + panic(err) + } + fmt.Println(a) + // Output: USD -12.34 +} + +func ParseMoneyProto(curr string, units int64, nanos int32) (money.Amount, error) { + // Currency + c, err := money.ParseCurr(curr) + if err != nil { + return money.Amount{}, err + } + // Amount + d, err := decimal.NewFromInt64(units, int64(nanos), 9) + if err != nil { + return money.Amount{}, err + } + d = d.Trim(c.Scale()) + return money.NewAmount(c, d) +} + +// This is an example of how to a parse a monetary amount formatted as [MoneyProto]. +// +// [MoneyProto]: https://github.com/googleapis/googleapis/blob/master/google/type/money.proto +func Example_parsingProtobuf() { + a, err := ParseMoneyProto("840", -12, -340000000) + if err != nil { + panic(err) + } + fmt.Println(a) + // Output: USD -12.34 +} + +func ParseStripe(currency string, amount int64) (money.Amount, error) { + // Currency + c, err := money.ParseCurr(currency) + if err != nil { + return money.Amount{}, err + } + // Amount + d, err := decimal.New(amount, c.Scale()) + if err != nil { + return money.Amount{}, err + } + return money.NewAmount(c, d) +} + +// This is an example of how to a parse a monetary amount +// formatted according to Stripe API specification. +func Example_parsingStripe() { + a, err := ParseStripe("usd", -1234) + if err != nil { + panic(err) + } + fmt.Println(a) + // Output: USD -12.34 +} + func ExampleMustNewAmount() { c := money.USD d := decimal.MustNew(12345, 2) @@ -553,6 +638,28 @@ func ExampleAmount_Quo() { // Output: USD -7.835 } +func ExampleDecimal_QuoRem() { + a := money.MustParseAmount("USD", "-15.67") + e := decimal.MustParse("2") + fmt.Println(a.QuoRem(e)) + // Output: USD -7.83 USD -0.01 +} + +func ExampleAmount_Split() { + a := money.MustParseAmount("USD", "1.01") + fmt.Println(a.Split(5)) + fmt.Println(a.Split(4)) + fmt.Println(a.Split(3)) + fmt.Println(a.Split(2)) + fmt.Println(a.Split(1)) + // Output: + // [USD 0.21 USD 0.20 USD 0.20 USD 0.20 USD 0.20] + // [USD 0.26 USD 0.25 USD 0.25 USD 0.25] + // [USD 0.34 USD 0.34 USD 0.33] + // [USD 0.51 USD 0.50] + // [USD 1.01] +} + func ExampleAmount_Rat() { a := money.MustParseAmount("USD", "8") b := money.MustParseAmount("USD", "10") @@ -788,21 +895,6 @@ func ExampleAmount_Scale() { // 3 } -func ExampleAmount_Split() { - a := money.MustParseAmount("USD", "1.01") - fmt.Println(a.Split(5)) - fmt.Println(a.Split(4)) - fmt.Println(a.Split(3)) - fmt.Println(a.Split(2)) - fmt.Println(a.Split(1)) - // Output: - // [USD 0.21 USD 0.20 USD 0.20 USD 0.20 USD 0.20] - // [USD 0.26 USD 0.25 USD 0.25 USD 0.25] - // [USD 0.34 USD 0.34 USD 0.33] - // [USD 0.51 USD 0.50] - // [USD 1.01] -} - func ExampleAmount_Format() { a := money.MustParseAmount("USD", "-123.456") fmt.Printf("%v\n", a) @@ -1016,91 +1108,6 @@ func ExampleCurrency_Format() { // USD } -func ParseISO8583(s string) (money.Amount, error) { - // Currency - c, err := money.ParseCurr(s[:3]) - if err != nil { - return money.Amount{}, err - } - // Amount - n, err := strconv.ParseInt(s[4:], 10, 64) - if err != nil { - return money.Amount{}, err - } - d, err := decimal.New(n, c.Scale()) - if err != nil { - return money.Amount{}, err - } - // Sign - if s[3:4] == "D" { - d = d.Neg() - } - return money.NewAmount(c, d) -} - -// In this example, we parse the string "840D000000001234", which represents -12.34 USD, -// according to the specification for "DE54, Additional Amounts" in ISO 8583. -func ExampleNewAmount_iso8583() { - a, err := ParseISO8583("840D000000001234") - if err != nil { - panic(err) - } - fmt.Println(a) - // Output: USD -12.34 -} - -func ParseMoneyProto(curr string, units int64, nanos int32) (money.Amount, error) { - // Currency - c, err := money.ParseCurr(curr) - if err != nil { - return money.Amount{}, err - } - // Amount - d, err := decimal.NewFromInt64(units, int64(nanos), 9) - if err != nil { - return money.Amount{}, err - } - d = d.Trim(c.Scale()) - return money.NewAmount(c, d) -} - -// This is an example of how to a parse a monetary amount formatted as [MoneyProto]. -// -// [MoneyProto]: https://github.com/googleapis/googleapis/blob/master/google/type/money.proto -func ExampleNewAmount_protobuf() { - a, err := ParseMoneyProto("840", -12, -340000000) - if err != nil { - panic(err) - } - fmt.Println(a) - // Output: USD -12.34 -} - -func ParseStripe(currency string, amount int64) (money.Amount, error) { - // Currency - c, err := money.ParseCurr(currency) - if err != nil { - return money.Amount{}, err - } - // Amount - d, err := decimal.New(amount, c.Scale()) - if err != nil { - return money.Amount{}, err - } - return money.NewAmount(c, d) -} - -// This is an example of how to a parse a monetary amount -// formatted according to Stripe API specification. -func ExampleNewAmount_stripe() { - a, err := ParseStripe("usd", -1234) - if err != nil { - panic(err) - } - fmt.Println(a) - // Output: USD -12.34 -} - func ExampleAmount_Zero() { a := money.MustParseAmount("JPY", "23") b := money.MustParseAmount("JPY", "23.5") diff --git a/go.mod b/go.mod index a9c4d29..b50965f 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/govalues/money go 1.19 -require github.com/govalues/decimal v0.1.4 +require github.com/govalues/decimal v0.1.5 diff --git a/go.sum b/go.sum index 0688c21..c433455 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/govalues/decimal v0.1.4 h1:MbFSBjswROtTZhsK3Fvbamn2IutpMoUSqDyD8mejuFg= -github.com/govalues/decimal v0.1.4/go.mod h1:NfqNdX/GQBotCdmXtzckjhq54itVCX1Git3psSgom8A= +github.com/govalues/decimal v0.1.5 h1:pPQg5GrzqnK//KY8MMqQQVM3Oth/On/D3WcOcJgQr+c= +github.com/govalues/decimal v0.1.5/go.mod h1:NfqNdX/GQBotCdmXtzckjhq54itVCX1Git3psSgom8A=