diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index e88d0b6..2652ad9 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -11,30 +11,30 @@ jobs:
go-version: [oldstable, stable]
runs-on: ubuntu-latest
steps:
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go-version }}
+ cache: false
- - name: Setup Go
- uses: actions/setup-go@v4
- with:
- go-version: ${{ matrix.go-version }}
+ - name: Check out code
+ uses: actions/checkout@v4
- - name: Check out code
- uses: actions/checkout@v3
+ - 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@v3
+ - 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.go-version == 'stable'
- uses: codecov/codecov-action@v3
+ - name: Upload test coverage
+ if: matrix.go-version == 'stable'
+ uses: codecov/codecov-action@v4
diff --git a/.gitignore b/.gitignore
index 66fd13c..7b62130 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,8 @@
-# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
-
-# Test binary, built with `go test -c`
*.test
-
-# Output of the go coverage tool, specifically when used with LiteIDE
*.out
-
-# Dependency directories (remove the comment below to include it)
-# vendor/
+coverage.txt
diff --git a/.golangci.yml b/.golangci.yml
index 453957e..d1cc2c5 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -11,6 +11,7 @@ linters:
- godot
- gosec
- misspell
+ - predeclared
- stylecheck
- revive
- staticcheck
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97af35e..103cb71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## [0.2.3] - 2024-07-26
+
+### Changed
+
+- Allowed rounding to the scale smaller than the currency scale.
+
## [0.2.2] - 2023-12-18
### Changed
diff --git a/README.md b/README.md
index b56d635..97fde5b 100644
--- a/README.md
+++ b/README.md
@@ -52,25 +52,25 @@ import (
func main() {
// Constructors
- a, _ := money.NewAmount("USD", 8, 0) // a = USD 8.00
- b, _ := money.ParseAmount("USD", "12.5") // b = USD 12.50
- c, _ := money.NewAmountFromFloat64("USD", 2.567) // c = USD 2.567
- d, _ := money.NewAmountFromInt64("USD", 7, 896, 3) // d = USD 7.896
- r, _ := money.NewExchRate("USD", "EUR", 9, 1) // r = USD/EUR 0.9
+ a, _ := money.NewAmount("USD", 8, 0) // a = $8.00
+ b, _ := money.ParseAmount("USD", "12.5") // b = $12.50
+ c, _ := money.NewAmountFromFloat64("USD", 2.567) // c = $2.567
+ d, _ := money.NewAmountFromInt64("USD", 7, 896, 3) // d = $7.896
+ r, _ := money.NewExchRate("USD", "EUR", 9, 1) // r = $/€ 0.9
x, _ := decimal.New(2, 0) // x = 2
// Operations
- fmt.Println(a.Add(b)) // USD 8.00 + USD 12.50
- fmt.Println(a.Sub(b)) // USD 8.00 - USD 12.50
+ fmt.Println(a.Add(b)) // $8.00 + $12.50
+ fmt.Println(a.Sub(b)) // $8.00 - $12.50
- fmt.Println(a.Mul(x)) // USD 8.00 * 2
- fmt.Println(a.FMA(x, b)) // USD 8.00 * 2 + USD 12.50
- fmt.Println(r.Conv(a)) // USD 8.00 * USD/EUR 0.9
+ fmt.Println(a.Mul(x)) // $8.00 * 2
+ fmt.Println(a.FMA(x, b)) // $8.00 * 2 + $12.50
+ fmt.Println(r.Conv(a)) // $8.00 * $/€ 0.9
- fmt.Println(a.Quo(x)) // USD 8.00 / 2
- fmt.Println(a.QuoRem(x)) // USD 8.00 div 2, USD 8.00 mod 2
- fmt.Println(a.Rat(b)) // USD 8.00 / USD 12.50
- fmt.Println(a.Split(3)) // USD 8.00 into 3 parts
+ fmt.Println(a.Quo(x)) // $8.00 ÷ 2
+ fmt.Println(a.QuoRem(x)) // $8.00 ÷ 2, $8.00 mod 2
+ fmt.Println(a.Rat(b)) // $8.00 ÷ $12.50
+ fmt.Println(a.Split(3)) // $8.00 into 3 parts
// Rounding to 2 decimal places
fmt.Println(c.RoundToCurr()) // 2.57
@@ -95,7 +95,7 @@ func main() {
## Documentation
For detailed documentation and additional examples, visit the package
-[documentation](https://pkg.go.dev/github.com/govalues/money#pkg-examples).
+[documentation](https://pkg.go.dev/github.com/govalues/money#section-documentation).
## Comparison
@@ -123,21 +123,21 @@ pkg: github.com/govalues/money-tests
cpu: AMD Ryzen 7 3700C with Radeon Vega Mobile Gfx
```
-| Test Case | Expression | govalues | [rhymond] v1.0.10 | [bojanz] v1.2.1 | govalues vs rhymond | govalues vs bojanz |
-| ----------- | ------------------------ | -------: | ----------------: | --------------: | ------------------: | -----------------: |
-| Add | USD 2.00 + USD 3.00 | 22.95n | 218.30n | 144.10n | +851.41% | +528.02% |
-| Mul | USD 2.00 * 3 | 21.80n | 133.40n | 239.60n | +511.79% | +998.83% |
-| QuoFinite | USD 2.00 / 4 | 80.12n | n/a[^nodiv] | 468.05n | n/a | +484.19% |
-| QuoInfinite | USD 2.00 / 3 | 602.1n | n/a[^nodiv] | 512.4n | n/a | -14.91% |
-| Split | USD 2.00 into 10 parts | 374.9n | 897.0n | n/a[^nosplit] | +139.28% | n/a |
-| Conv | USD 2.00 * USD/EUR 0.8 | 30.88n | n/a[^noconv] | 348.50n | n/a | +1028.38% |
-| Parse | USD 1 | 44.99n | 139.50n | 99.09n | +210.07% | +120.26% |
-| Parse | USD 123.456 | 61.45n | 148.60n | 240.90n | +141.82% | +292.03% |
-| Parse | USD 123456789.1234567890 | 131.2n | 204.4n | 253.0n | +55.85% | +92.87% |
-| String | USD 1 | 38.48n | 200.70n | 89.92n | +421.50% | +133.65% |
-| String | USD 123.456 | 56.34n | 229.90n | 127.05n | +308.02% | +125.49% |
-| String | USD 123456789.1234567890 | 84.73n | 383.30n | 277.55n | +352.38% | +227.57% |
-| Telco | see [specification] | 224.2n | n/a[^nofracmul] | 1944.0n | n/a | +766.89% |
+| Test Case | Expression | govalues | [rhymond] v1.0.10 | [bojanz] v1.2.1 | govalues vs rhymond | govalues vs bojanz |
+| ----------- | --------------------- | -------: | ----------------: | --------------: | ------------------: | -----------------: |
+| Add | $2.00 + $3.00 | 22.95n | 218.30n | 144.10n | +851.41% | +528.02% |
+| Mul | $2.00 * 3 | 21.80n | 133.40n | 239.60n | +511.79% | +998.83% |
+| QuoExact | $2.00 ÷ 4 | 80.12n | n/a[^nodiv] | 468.05n | n/a | +484.19% |
+| QuoInfinite | $2.00 ÷ 3 | 602.1n | n/a[^nodiv] | 512.4n | n/a | -14.91% |
+| Split | $2.00 into 10 parts | 374.9n | 897.0n | n/a[^nosplit] | +139.28% | n/a |
+| Conv | $2.00 to € | 30.88n | n/a[^noconv] | 348.50n | n/a | +1028.38% |
+| Parse | $1 | 44.99n | 139.50n | 99.09n | +210.07% | +120.26% |
+| Parse | $123.456 | 61.45n | 148.60n | 240.90n | +141.82% | +292.03% |
+| Parse | $123456789.1234567890 | 131.2n | 204.4n | 253.0n | +55.85% | +92.87% |
+| String | $1 | 38.48n | 200.70n | 89.92n | +421.50% | +133.65% |
+| String | $123.456 | 56.34n | 229.90n | 127.05n | +308.02% | +125.49% |
+| String | $123456789.1234567890 | 84.73n | 383.30n | 277.55n | +352.38% | +227.57% |
+| Telco | see [specification] | 224.2n | n/a[^nofracmul] | 1944.0n | n/a | +766.89% |
[^nodiv]: [rhymond] does not support division.
@@ -150,21 +150,6 @@ cpu: AMD Ryzen 7 3700C with Radeon Vega Mobile Gfx
The benchmark results shown in the table are provided for informational purposes
only and may vary depending on your specific use case.
-## Contributing
-
-Interested in contributing? Here's how to get started:
-
-1. Fork and clone the repository.
-1. Implement your changes.
-1. Write tests to cover your changes.
-1. Ensure all tests pass with `go test`.
-1. Commit and push to your fork.
-1. Open a pull request detailing your changes.
-
-**Note**: If you're considering significant changes, please open an issue first to
-discuss with the maintainers.
-This ensures alignment with the project's objectives and roadmap.
-
[codecov]: https://codecov.io/gh/govalues/money
[codecovb]: https://img.shields.io/codecov/c/github/govalues/money/main?color=brightcolor
[goreport]: https://goreportcard.com/report/github.com/govalues/money
diff --git a/amount.go b/amount.go
index fa02c0a..47d791f 100644
--- a/amount.go
+++ b/amount.go
@@ -9,10 +9,13 @@ import (
"github.com/govalues/decimal"
)
-var errCurrencyMismatch = errors.New("currency mismatch")
+var (
+ errAmountOverflow = errors.New("amount overflow")
+ errCurrencyMismatch = errors.New("currency mismatch")
+)
// Amount type represents a monetary amount.
-// Its zero value corresponds to "XXX 0", where XXX indicates an unknown currency.
+// Its zero value corresponds to "XXX 0", where [XXX] indicates an unknown currency.
// Amount is designed to be safe for concurrent use by multiple goroutines.
type Amount struct {
curr Currency // ISO 4217 currency
@@ -28,10 +31,9 @@ func newAmountUnsafe(c Currency, d decimal.Decimal) Amount {
// newAmountSafe creates a new amount and checks the scale.
func newAmountSafe(c Currency, d decimal.Decimal) (Amount, error) {
if d.Scale() < c.Scale() {
- var err error
- d, err = d.Pad(c.Scale())
- if err != nil {
- return Amount{}, fmt.Errorf("padding amount: %w", err)
+ d = d.Pad(c.Scale())
+ if d.Scale() < c.Scale() {
+ return Amount{}, fmt.Errorf("padding amount: %w", errAmountOverflow)
}
}
return newAmountUnsafe(c, d), nil
@@ -224,17 +226,17 @@ func MustParseAmount(curr, amount string) Amount {
// [rounding half to even]: https://en.wikipedia.org/wiki/Rounding#Rounding_half_to_even
func (a Amount) MinorUnits() (units int64, ok bool) {
d := a.RoundToCurr().Decimal()
- coef := d.Coef()
+ u := d.Coef()
if d.IsNeg() {
- if coef > -math.MinInt64 {
+ if u > -math.MinInt64 {
return 0, false
}
- return -int64(coef), true
+ return -int64(u), true
}
- if coef > math.MaxInt64 {
+ if u > math.MaxInt64 {
return 0, false
}
- return int64(coef), true
+ return int64(u), true
}
// Float64 returns the nearest binary floating-point number rounded
@@ -260,16 +262,11 @@ func (a Amount) Float64() (f float64, ok bool) {
// This method is useful for converting amounts to [protobuf] format.
// See also constructor [NewAmountFromInt64].
//
-// Int64 returns false if:
-// - given scale is smaller than the scale of the currency;
-// - the result cannot be represented as a pair of int64 values.
+// Int64 returns false if the result cannot be represented as a pair of int64 values.
//
// [rounding half to even]: https://en.wikipedia.org/wiki/Rounding#Rounding_half_to_even
// [protobuf]: https://github.com/googleapis/googleapis/blob/master/google/type/money.proto
func (a Amount) Int64(scale int) (whole, frac int64, ok bool) {
- if scale < a.Curr().Scale() {
- return 0, 0, false
- }
return a.Decimal().Int64(scale)
}
@@ -337,11 +334,7 @@ func (a Amount) Scale() int {
// without rounding.
// See also method [Amount.Trim].
func (a Amount) MinScale() int {
- s := a.Decimal().MinScale()
- if s < a.Curr().Scale() {
- s = a.Curr().Scale()
- }
- return s
+ return a.Decimal().MinScale()
}
// IsZero returns:
@@ -662,17 +655,12 @@ func (a Amount) ULP() Amount {
// Ceil returns an amount rounded up to the specified number of digits after
// the decimal point using [rounding toward positive infinity].
-// If the given scale is less than the scale of the currency,
-// the amount will be rounded up to the scale of the currency instead.
// See also methods [Amount.CeilToCurr], [Amount.Floor].
//
// [rounding toward positive infinity]: https://en.wikipedia.org/wiki/Rounding#Rounding_up
func (a Amount) Ceil(scale int) Amount {
c, d := a.Curr(), a.Decimal()
- if scale < c.Scale() {
- scale = c.Scale()
- }
- d = d.Ceil(scale)
+ d = d.Ceil(scale).Pad(c.Scale())
return newAmountUnsafe(c, d)
}
@@ -687,17 +675,12 @@ func (a Amount) CeilToCurr() Amount {
// Floor returns an amount rounded down to the specified number of digits after
// the decimal point using [rounding toward negative infinity].
-// If the given scale is less than the scale of the currency,
-// the amount will be rounded down to the scale of the currency instead.
// See also methods [Amount.FloorToCurr], [Amount.Ceil].
//
// [rounding toward negative infinity]: https://en.wikipedia.org/wiki/Rounding#Rounding_down
func (a Amount) Floor(scale int) Amount {
c, d := a.Curr(), a.Decimal()
- if scale < c.Scale() {
- scale = c.Scale()
- }
- d = d.Floor(scale)
+ d = d.Floor(scale).Pad(c.Scale())
return newAmountUnsafe(c, d)
}
@@ -712,17 +695,12 @@ func (a Amount) FloorToCurr() Amount {
// Trunc returns an amount truncated to the specified number of digits after
// the decimal point using [rounding toward zero].
-// If the given scale is less than the scale of the currency,
-// the amount will be truncated to the scale of the currency instead.
// See also method [Amount.TruncToCurr].
//
// [rounding toward zero]: https://en.wikipedia.org/wiki/Rounding#Rounding_toward_zero
func (a Amount) Trunc(scale int) Amount {
c, d := a.Curr(), a.Decimal()
- if scale < c.Scale() {
- scale = c.Scale()
- }
- d = d.Trunc(scale)
+ d = d.Trunc(scale).Pad(c.Scale())
return newAmountUnsafe(c, d)
}
@@ -737,17 +715,12 @@ func (a Amount) TruncToCurr() Amount {
// Round returns an amount rounded to the specified number of digits after
// the decimal point using [rounding half to even] (banker's rounding).
-// If the given scale is less than the scale of the currency,
-// the amount will be rounded to the scale of the currency instead.
// See also methods [Amount.Rescale], [Amount.RoundToCurr].
//
// [rounding half to even]: https://en.wikipedia.org/wiki/Rounding#Rounding_half_to_even
func (a Amount) Round(scale int) Amount {
c, d := a.Curr(), a.Decimal()
- if scale < c.Scale() {
- scale = c.Scale()
- }
- d = d.Round(scale)
+ d = d.Round(scale).Pad(c.Scale())
return newAmountUnsafe(c, d)
}
@@ -763,43 +736,17 @@ func (a Amount) RoundToCurr() Amount {
// Quantize returns an amount rescaled to the same scale as amount b.
// The currency and the sign of amount b are ignored.
// See also methods [Amount.Scale], [Amount.SameScale], [Amount.Rescale].
-//
-// Quantize returns an error if the integer part of the result has more than
-// ([decimal.MaxPrec] - b.Scale()) digits.
-func (a Amount) Quantize(b Amount) (Amount, error) {
- c, err := a.rescale(b.Scale())
- if err != nil {
- return Amount{}, fmt.Errorf("quantizing %v to the scale of %v: %w", a, b, err)
- }
- return c, nil
+func (a Amount) Quantize(b Amount) Amount {
+ return a.Rescale(b.Scale())
}
// Rescale returns an amount rounded or zero-padded to the given number of digits
// after the decimal point.
-// If the specified scale is less than the scale of the currency,
-// the amount will be rounded to the scale of the currency instead.
// See also method [Amount.Round].
-//
-// Rescale returns an error if the integer part of the result more than
-// ([decimal.MaxPrec] - scale) digits.
-func (a Amount) Rescale(scale int) (Amount, error) {
- c, err := a.rescale(scale)
- if err != nil {
- return Amount{}, fmt.Errorf("rescaling %v: %w", a, err)
- }
- return c, nil
-}
-
-func (a Amount) rescale(scale int) (Amount, error) {
+func (a Amount) Rescale(scale int) Amount {
c, d := a.Curr(), a.Decimal()
- if scale < c.Scale() {
- scale = c.Scale()
- }
- d, err := d.Rescale(scale)
- if err != nil {
- return Amount{}, err
- }
- return newAmountSafe(c, d)
+ d = d.Rescale(scale).Pad(c.Scale())
+ return newAmountUnsafe(c, d)
}
// Trim returns an amount with trailing zeros removed up to the given scale.
@@ -808,9 +755,7 @@ func (a Amount) rescale(scale int) (Amount, error) {
// See also method [Amount.TrimToCurr].
func (a Amount) Trim(scale int) Amount {
c, d := a.Curr(), a.Decimal()
- if scale < c.Scale() {
- scale = c.Scale()
- }
+ scale = max(scale, c.Scale())
d = d.Trim(scale)
return newAmountUnsafe(c, d)
}
@@ -1045,7 +990,7 @@ func (a Amount) Format(state fmt.State, verb rune) {
c, d := a.Curr(), a.Decimal()
// Rescaling
- tzeroes := 0
+ tzeros := 0
if verb == 'f' || verb == 'F' || verb == 'd' || verb == 'D' {
scale := 0
switch p, ok := state.Precision(); {
@@ -1056,14 +1001,12 @@ func (a Amount) Format(state fmt.State, verb rune) {
case verb == 'f' || verb == 'F':
scale = d.Scale()
}
- if scale < c.Scale() {
- scale = c.Scale()
- }
+ scale = max(scale, c.Scale())
switch {
case scale < d.Scale():
d = d.Round(scale)
case scale > d.Scale():
- tzeroes = scale - d.Scale()
+ tzeros = scale - d.Scale()
}
}
@@ -1089,7 +1032,7 @@ func (a Amount) Format(state fmt.State, verb rune) {
// Decimal point
dpoint := 0
- if fracdigs > 0 || tzeroes > 0 {
+ if fracdigs > 0 || tzeros > 0 {
dpoint = 1
}
@@ -1120,14 +1063,14 @@ func (a Amount) Format(state fmt.State, verb rune) {
}
// Calculating padding
- width := lquote + currsyms + currdel + rsign + intdigs + dpoint + fracdigs + tzeroes + tquote
- lspaces, lzeroes, tspaces := 0, 0, 0
+ width := lquote + currsyms + currdel + rsign + intdigs + dpoint + fracdigs + tzeros + tquote
+ lspaces, lzeros, tspaces := 0, 0, 0
if w, ok := state.Width(); ok && w > width {
switch {
case state.Flag('-'):
tspaces = w - width
case state.Flag('0') && verb != 'c' && verb != 'C':
- lzeroes = w - width
+ lzeros = w - width
default:
lspaces = w - width
}
@@ -1149,8 +1092,8 @@ func (a Amount) Format(state fmt.State, verb rune) {
pos--
}
- // Trailing zeroes
- for i := 0; i < tzeroes; i++ {
+ // Trailing zeros
+ for i := 0; i < tzeros; i++ {
buf[pos] = '0'
pos--
}
@@ -1176,8 +1119,8 @@ func (a Amount) Format(state fmt.State, verb rune) {
coef /= 10
}
- // Leading zeroes
- for i := 0; i < lzeroes; i++ {
+ // Leading zeros
+ for i := 0; i < lzeros; i++ {
buf[pos] = '0'
pos--
}
@@ -1219,6 +1162,7 @@ func (a Amount) Format(state fmt.State, verb rune) {
}
// Writing result
+ //nolint:errcheck
switch verb {
case 'q', 'Q', 's', 'S', 'v', 'V', 'f', 'F', 'd', 'D', 'c', 'C':
state.Write(buf)
diff --git a/amount_test.go b/amount_test.go
index 3fd0a7f..49fe1e9 100644
--- a/amount_test.go
+++ b/amount_test.go
@@ -18,7 +18,7 @@ func TestAmount_ZeroValue(t *testing.T) {
}
}
-func TestAmount_Sizeof(t *testing.T) {
+func TestAmount_Size(t *testing.T) {
a := Amount{}
got := unsafe.Sizeof(a)
want := uintptr(24)
@@ -27,6 +27,18 @@ func TestAmount_Sizeof(t *testing.T) {
}
}
+func TestAmount_Interfaces(t *testing.T) {
+ var i any = Amount{}
+ _, ok := i.(fmt.Stringer)
+ if !ok {
+ t.Errorf("%T does not implement fmt.Stringer", i)
+ }
+ _, ok = i.(fmt.Formatter)
+ if !ok {
+ t.Errorf("%T does not implement fmt.Formatter", i)
+ }
+}
+
func TestNewAmount(t *testing.T) {
t.Run("success", func(t *testing.T) {
tests := []struct {
@@ -115,7 +127,7 @@ func TestNewAmountFromInt64(t *testing.T) {
scale int
want string
}{
- // Zeroes
+ // Zeros
{"JPY", 0, 0, 0, "0"},
{"JPY", 0, 0, 19, "0"},
{"USD", 0, 0, 0, "0.00"},
@@ -237,7 +249,7 @@ func TestNewAmountFromFloat64(t *testing.T) {
f float64
want string
}{
- // Zeroes
+ // Zeros
{"JPY", 0, "0"},
{"USD", 0, "0.00"},
{"OMR", 0, "0.000"},
@@ -1046,7 +1058,7 @@ func TestAmount_String(t *testing.T) {
tests := []struct {
curr, a, want string
}{
- // Zeroes
+ // Zeros
{"JPY", "0", "JPY 0"},
{"JPY", "0.0", "JPY 0.0"},
{"USD", "0", "USD 0.00"},
@@ -1590,165 +1602,127 @@ func TestAmount_Clamp(t *testing.T) {
}
func TestAmount_Rescale(t *testing.T) {
- t.Run("success", func(t *testing.T) {
- tests := []struct {
- curr, a string
- scale int
- want string
- }{
- // Padding
- {"JPY", "0", 0, "0"},
- {"JPY", "0", 1, "0.0"},
- {"JPY", "0", 2, "0.00"},
- {"JPY", "0", 3, "0.000"},
- {"USD", "0", 0, "0.00"},
- {"USD", "0", 1, "0.00"},
- {"USD", "0", 2, "0.00"},
- {"USD", "0", 3, "0.000"},
- {"OMR", "0", 0, "0.000"},
- {"OMR", "0", 1, "0.000"},
- {"OMR", "0", 2, "0.000"},
- {"OMR", "0", 3, "0.000"},
- {"USD", "0", 17, "0.00000000000000000"},
- {"USD", "0", 18, "0.000000000000000000"},
- {"USD", "0", 19, "0.0000000000000000000"},
- {"USD", "1", 17, "1.00000000000000000"},
- {"USD", "1", 18, "1.000000000000000000"},
-
- // Half-to-even rounding
- {"USD", "0.0049", 2, "0.00"},
- {"USD", "0.0051", 2, "0.01"},
- {"USD", "0.0149", 2, "0.01"},
- {"USD", "0.0151", 2, "0.02"},
- {"USD", "-0.0049", 2, "0.00"},
- {"USD", "-0.0051", 2, "-0.01"},
- {"USD", "-0.0149", 2, "-0.01"},
- {"USD", "-0.0151", 2, "-0.02"},
- {"USD", "0.0050", 2, "0.00"},
- {"USD", "0.0150", 2, "0.02"},
- {"USD", "0.0250", 2, "0.02"},
- {"USD", "0.0350", 2, "0.04"},
- {"USD", "-0.0050", 2, "0.00"},
- {"USD", "-0.0150", 2, "-0.02"},
- {"USD", "-0.0250", 2, "-0.02"},
- {"USD", "-0.0350", 2, "-0.04"},
- {"USD", "3.0448", 2, "3.04"},
- {"USD", "3.0450", 2, "3.04"},
- {"USD", "3.0452", 2, "3.05"},
- {"USD", "3.0956", 2, "3.10"},
- }
- for _, tt := range tests {
- a := MustParseAmount(tt.curr, tt.a)
- got, err := a.Rescale(tt.scale)
- if err != nil {
- t.Errorf("%q.Rescale(%v) failed: %v", a, tt.scale, err)
- continue
- }
- want := MustParseAmount(tt.curr, tt.want)
- if got != want {
- t.Errorf("%q.Rescale(%v) = %q, want %q", a, tt.scale, got, want)
- }
- }
- })
-
- t.Run("error", func(t *testing.T) {
- tests := map[string]struct {
- curr, a string
- scale int
- }{
- "overflow 1": {"JPY", "9999999999999999999", 1},
- "overflow 2": {"USD", "99999999999999999.99", 3},
- "overflow 3": {"OMR", "9999999999999999.999", 4},
- "overflow 4": {"USD", "0", 20},
- "overflow 5": {"USD", "1", 19},
- }
- for _, tt := range tests {
- a := MustParseAmount(tt.curr, tt.a)
- _, err := a.Rescale(tt.scale)
- if err == nil {
- t.Errorf("%q.Rescale(%v) did not fail", a, tt.scale)
- }
+ tests := []struct {
+ curr, a string
+ scale int
+ want string
+ }{
+ // Padding
+ {"JPY", "0", 0, "0"},
+ {"JPY", "0", 1, "0.0"},
+ {"JPY", "0", 2, "0.00"},
+ {"JPY", "0", 3, "0.000"},
+ {"USD", "0", 0, "0.00"},
+ {"USD", "0", 1, "0.00"},
+ {"USD", "0", 2, "0.00"},
+ {"USD", "0", 3, "0.000"},
+ {"OMR", "0", 0, "0.000"},
+ {"OMR", "0", 1, "0.000"},
+ {"OMR", "0", 2, "0.000"},
+ {"OMR", "0", 3, "0.000"},
+ {"USD", "0", 17, "0.00000000000000000"},
+ {"USD", "0", 18, "0.000000000000000000"},
+ {"USD", "0", 19, "0.0000000000000000000"},
+ {"USD", "0", 20, "0.0000000000000000000"},
+ {"USD", "1", 17, "1.00000000000000000"},
+ {"USD", "1", 18, "1.000000000000000000"},
+ {"USD", "1", 19, "1.000000000000000000"},
+
+ // Half-to-even rounding
+ {"USD", "0.0049", 2, "0.00"},
+ {"USD", "0.0051", 2, "0.01"},
+ {"USD", "0.0149", 2, "0.01"},
+ {"USD", "0.0151", 2, "0.02"},
+ {"USD", "-0.0049", 2, "0.00"},
+ {"USD", "-0.0051", 2, "-0.01"},
+ {"USD", "-0.0149", 2, "-0.01"},
+ {"USD", "-0.0151", 2, "-0.02"},
+ {"USD", "0.0050", 2, "0.00"},
+ {"USD", "0.0150", 2, "0.02"},
+ {"USD", "0.0250", 2, "0.02"},
+ {"USD", "0.0350", 2, "0.04"},
+ {"USD", "-0.0050", 2, "0.00"},
+ {"USD", "-0.0150", 2, "-0.02"},
+ {"USD", "-0.0250", 2, "-0.02"},
+ {"USD", "-0.0350", 2, "-0.04"},
+ {"USD", "3.0448", 2, "3.04"},
+ {"USD", "3.0450", 2, "3.04"},
+ {"USD", "3.0452", 2, "3.05"},
+ {"USD", "3.0956", 2, "3.10"},
+
+ // Padding overflow
+ {"JPY", "9999999999999999999", 1, "9999999999999999999"},
+ {"USD", "99999999999999999.99", 3, "99999999999999999.99"},
+ {"OMR", "9999999999999999.999", 4, "9999999999999999.999"},
+ }
+ for _, tt := range tests {
+ a := MustParseAmount(tt.curr, tt.a)
+ got := a.Rescale(tt.scale)
+ want := MustParseAmount(tt.curr, tt.want)
+ if got != want {
+ t.Errorf("%q.Rescale(%v) = %q, want %q", a, tt.scale, got, want)
}
- })
+ }
}
func TestAmount_Quantize(t *testing.T) {
- t.Run("success", func(t *testing.T) {
- tests := []struct {
- curr, a, b, want string
- }{
- // Padding
- {"JPY", "0", "0", "0"},
- {"JPY", "0", "0.0", "0.0"},
- {"JPY", "0", "0.00", "0.00"},
- {"JPY", "0", "0.000", "0.000"},
- {"USD", "0", "0", "0.00"},
- {"USD", "0", "0.0", "0.00"},
- {"USD", "0", "0.00", "0.00"},
- {"USD", "0", "0.000", "0.000"},
- {"OMR", "0", "0", "0.000"},
- {"OMR", "0", "0.0", "0.000"},
- {"OMR", "0", "0.00", "0.000"},
- {"OMR", "0", "0.000", "0.000"},
- {"USD", "0", "0.00000000000000000", "0.00000000000000000"},
- {"USD", "0", "0.000000000000000000", "0.000000000000000000"},
- {"USD", "0", "0.0000000000000000000", "0.0000000000000000000"},
- {"USD", "1", "0.00000000000000000", "1.00000000000000000"},
- {"USD", "1", "0.000000000000000000", "1.000000000000000000"},
-
- // Half-to-even rounding
- {"USD", "0.0049", "0.00", "0.00"},
- {"USD", "0.0051", "0.00", "0.01"},
- {"USD", "0.0149", "0.00", "0.01"},
- {"USD", "0.0151", "0.00", "0.02"},
- {"USD", "-0.0049", "0.00", "0.00"},
- {"USD", "-0.0051", "0.00", "-0.01"},
- {"USD", "-0.0149", "0.00", "-0.01"},
- {"USD", "-0.0151", "0.00", "-0.02"},
- {"USD", "0.0050", "0.00", "0.00"},
- {"USD", "0.0150", "0.00", "0.02"},
- {"USD", "0.0250", "0.00", "0.02"},
- {"USD", "0.0350", "0.00", "0.04"},
- {"USD", "-0.0050", "0.00", "0.00"},
- {"USD", "-0.0150", "0.00", "-0.02"},
- {"USD", "-0.0250", "0.00", "-0.02"},
- {"USD", "-0.0350", "0.00", "-0.04"},
- {"USD", "3.0448", "0.00", "3.04"},
- {"USD", "3.0450", "0.00", "3.04"},
- {"USD", "3.0452", "0.00", "3.05"},
- {"USD", "3.0956", "0.00", "3.10"},
- }
- for _, tt := range tests {
- a := MustParseAmount(tt.curr, tt.a)
- b := MustParseAmount(tt.curr, tt.b)
- got, err := a.Quantize(b)
- if err != nil {
- t.Errorf("%q.Quantize(%q) failed: %v", a, b, err)
- continue
- }
- want := MustParseAmount(tt.curr, tt.want)
- if got != want {
- t.Errorf("%q.Quantize(%q) = %q, want %q", a, b, got, want)
- }
- }
- })
-
- t.Run("error", func(t *testing.T) {
- tests := map[string]struct {
- curr, a, b string
- }{
- "overflow 1": {"JPY", "9999999999999999999", "0.1"},
- "overflow 2": {"USD", "99999999999999999.99", "0.001"},
- "overflow 3": {"OMR", "9999999999999999.999", "0.0001"},
- "overflow 4": {"USD", "1", "0.0000000000000000000"},
- }
- for _, tt := range tests {
- a := MustParseAmount(tt.curr, tt.a)
- b := MustParseAmount(tt.curr, tt.b)
- _, err := a.Quantize(b)
- if err == nil {
- t.Errorf("%q.Quantize(%q) did not fail", a, b)
- }
+ tests := []struct {
+ curr, a, b, want string
+ }{
+ // Padding
+ {"JPY", "0", "0", "0"},
+ {"JPY", "0", "0.0", "0.0"},
+ {"JPY", "0", "0.00", "0.00"},
+ {"JPY", "0", "0.000", "0.000"},
+ {"USD", "0", "0", "0.00"},
+ {"USD", "0", "0.0", "0.00"},
+ {"USD", "0", "0.00", "0.00"},
+ {"USD", "0", "0.000", "0.000"},
+ {"OMR", "0", "0", "0.000"},
+ {"OMR", "0", "0.0", "0.000"},
+ {"OMR", "0", "0.00", "0.000"},
+ {"OMR", "0", "0.000", "0.000"},
+ {"USD", "0", "0.00000000000000000", "0.00000000000000000"},
+ {"USD", "0", "0.000000000000000000", "0.000000000000000000"},
+ {"USD", "0", "0.0000000000000000000", "0.0000000000000000000"},
+ {"USD", "1", "0.00000000000000000", "1.00000000000000000"},
+ {"USD", "1", "0.000000000000000000", "1.000000000000000000"},
+
+ // Half-to-even rounding
+ {"USD", "0.0049", "0.00", "0.00"},
+ {"USD", "0.0051", "0.00", "0.01"},
+ {"USD", "0.0149", "0.00", "0.01"},
+ {"USD", "0.0151", "0.00", "0.02"},
+ {"USD", "-0.0049", "0.00", "0.00"},
+ {"USD", "-0.0051", "0.00", "-0.01"},
+ {"USD", "-0.0149", "0.00", "-0.01"},
+ {"USD", "-0.0151", "0.00", "-0.02"},
+ {"USD", "0.0050", "0.00", "0.00"},
+ {"USD", "0.0150", "0.00", "0.02"},
+ {"USD", "0.0250", "0.00", "0.02"},
+ {"USD", "0.0350", "0.00", "0.04"},
+ {"USD", "-0.0050", "0.00", "0.00"},
+ {"USD", "-0.0150", "0.00", "-0.02"},
+ {"USD", "-0.0250", "0.00", "-0.02"},
+ {"USD", "-0.0350", "0.00", "-0.04"},
+ {"USD", "3.0448", "0.00", "3.04"},
+ {"USD", "3.0450", "0.00", "3.04"},
+ {"USD", "3.0452", "0.00", "3.05"},
+ {"USD", "3.0956", "0.00", "3.10"},
+
+ // Padding overflow
+ {"JPY", "9999999999999999999", "0.1", "9999999999999999999"},
+ {"USD", "99999999999999999.99", "0.001", "99999999999999999.99"},
+ {"OMR", "9999999999999999.999", "0.0001", "9999999999999999.999"},
+ {"USD", "1", "0.0000000000000000000", "1.0000000000000000000"},
+ }
+ for _, tt := range tests {
+ a := MustParseAmount(tt.curr, tt.a)
+ b := MustParseAmount(tt.curr, tt.b)
+ got := a.Quantize(b)
+ want := MustParseAmount(tt.curr, tt.want)
+ if got != want {
+ t.Errorf("%q.Quantize(%q) = %q, want %q", a, b, got, want)
}
- })
+ }
}
diff --git a/currency.go b/currency.go
index 4f9f6cc..beddda6 100644
--- a/currency.go
+++ b/currency.go
@@ -9,16 +9,18 @@ import (
//go:generate go run scripts/currency/codegen.go
// Currency type represents a currency in the global financial system.
-// The zero value is "XXX", which indicates an unknown currency.
+// The zero value is [XXX], which indicates an unknown currency.
//
// Currency is implemented as an integer index into an in-memory array that
-// stores information such as code and scale.
+// stores properties defined by [ISO 4217], such as code and scale.
// This design ensures safe concurrency for multiple goroutines accessing
// the same Currency value.
//
// When persisting a currency value, use the alphabetic code returned by
// the [Currency.Code] method, rather than the integer index, as mapping between
// index and a particular currency may change in future versions.
+//
+// [ISO 4217]: https://en.wikipedia.org/wiki/ISO_4217
type Currency uint8
var errUnknownCurrency = errors.New("unknown currency")
@@ -44,7 +46,7 @@ func ParseCurr(curr string) (Currency, error) {
func MustParseCurr(curr string) Currency {
c, err := ParseCurr(curr)
if err != nil {
- panic(fmt.Sprintf("MustParseCurr(%q) failed: %v", curr, err))
+ panic(fmt.Sprintf("ParseCurr(%q) failed: %v", curr, err))
}
return c
}
@@ -114,13 +116,17 @@ func (c Currency) MarshalText() ([]byte, error) {
// See also method [ParseCurr].
//
// [sql.Scanner]: https://pkg.go.dev/database/sql#Scanner
-func (c *Currency) Scan(v any) error {
+func (c *Currency) Scan(value any) error {
var err error
- switch v := v.(type) {
+ switch value := value.(type) {
case string:
- *c, err = ParseCurr(v)
+ *c, err = ParseCurr(value)
+ case []byte:
+ *c, err = ParseCurr(string(value))
+ case nil:
+ err = fmt.Errorf("converting to %T: nil is not supported", c)
default:
- err = fmt.Errorf("failed to convert from %T to %T", v, XXX)
+ err = fmt.Errorf("converting from %T to %T: type %T is not supported", value, c, value)
}
return err
}
@@ -203,6 +209,7 @@ func (c Currency) Format(state fmt.State, verb rune) {
}
// Writing result
+ //nolint:errcheck
switch verb {
case 'q', 'Q', 's', 'S', 'v', 'V', 'c', 'C':
state.Write(buf)
diff --git a/currency_test.go b/currency_test.go
index cf94bc3..9cf8da7 100644
--- a/currency_test.go
+++ b/currency_test.go
@@ -1,10 +1,46 @@
package money
import (
+ "database/sql"
+ "database/sql/driver"
+ "encoding"
"fmt"
"testing"
)
+func TestCurrency_Interfaces(t *testing.T) {
+ var c any
+
+ c = XXX
+ _, ok := c.(fmt.Stringer)
+ if !ok {
+ t.Errorf("%T does not implement fmt.Stringer", c)
+ }
+ _, ok = c.(fmt.Formatter)
+ if !ok {
+ t.Errorf("%T does not implement fmt.Formatter", c)
+ }
+ _, ok = c.(encoding.TextMarshaler)
+ if !ok {
+ t.Errorf("%T does not implement encoding.TextMarshaler", c)
+ }
+ _, ok = c.(driver.Valuer)
+ if !ok {
+ t.Errorf("%T does not implement driver.Valuer", c)
+ }
+
+ x := XXX
+ c = &x
+ _, ok = c.(encoding.TextUnmarshaler)
+ if !ok {
+ t.Errorf("%T does not implement encoding.TextUnmarshaler", c)
+ }
+ _, ok = c.(sql.Scanner)
+ if !ok {
+ t.Errorf("%T does not implement sql.Scanner", c)
+ }
+}
+
func TestCurrency_Parse(t *testing.T) {
t.Run("success", func(t *testing.T) {
tests := []struct {
@@ -66,12 +102,163 @@ func TestCurrency_Scale(t *testing.T) {
want int
}{
{XXX, 0},
- {JPY, 0},
+ {XTS, 2},
{AED, 2},
+ {AFN, 2},
+ {ALL, 2},
+ {AMD, 2},
+ {ANG, 2},
+ {AOA, 2},
+ {ARS, 2},
+ {AUD, 2},
+ {AWG, 2},
+ {AZN, 2},
+ {BAM, 2},
+ {BBD, 2},
+ {BDT, 2},
+ {BGN, 2},
+ {BHD, 3},
+ {BIF, 0},
+ {BMD, 2},
+ {BND, 2},
+ {BOB, 2},
+ {BRL, 2},
+ {BSD, 2},
+ {BTN, 2},
+ {BWP, 2},
+ {BYN, 2},
+ {BZD, 2},
+ {CAD, 2},
+ {CDF, 2},
+ {CHF, 2},
+ {CLP, 0},
+ {CNY, 2},
+ {COP, 2},
+ {CRC, 2},
+ {CUP, 2},
+ {CVE, 2},
+ {CZK, 2},
+ {DJF, 0},
+ {DKK, 2},
+ {DOP, 2},
+ {DZD, 2},
+ {EGP, 2},
+ {ERN, 2},
+ {ETB, 2},
{EUR, 2},
- {USD, 2},
- {OMR, 3},
+ {FJD, 2},
+ {FKP, 2},
+ {GBP, 2},
+ {GEL, 2},
+ {GHS, 2},
+ {GIP, 2},
+ {GMD, 2},
+ {GNF, 0},
+ {GTQ, 2},
+ {GWP, 2},
+ {GYD, 2},
+ {HKD, 2},
+ {HNL, 2},
+ {HRK, 2},
+ {HTG, 2},
+ {HUF, 2},
+ {IDR, 2},
+ {ILS, 2},
+ {INR, 2},
{IQD, 3},
+ {IRR, 2},
+ {ISK, 2},
+ {JMD, 2},
+ {JOD, 3},
+ {JPY, 0},
+ {KES, 2},
+ {KGS, 2},
+ {KHR, 2},
+ {KMF, 0},
+ {KPW, 2},
+ {KRW, 0},
+ {KWD, 3},
+ {KYD, 2},
+ {KZT, 2},
+ {LAK, 2},
+ {LBP, 2},
+ {LKR, 2},
+ {LRD, 2},
+ {LSL, 2},
+ {LYD, 3},
+ {MAD, 2},
+ {MDL, 2},
+ {MGA, 2},
+ {MKD, 2},
+ {MMK, 2},
+ {MNT, 2},
+ {MOP, 2},
+ {MRU, 2},
+ {MUR, 2},
+ {MVR, 2},
+ {MWK, 2},
+ {MXN, 2},
+ {MYR, 2},
+ {MZN, 2},
+ {NAD, 2},
+ {NGN, 2},
+ {NIO, 2},
+ {NOK, 2},
+ {NPR, 2},
+ {NZD, 2},
+ {OMR, 3},
+ {PAB, 2},
+ {PEN, 2},
+ {PGK, 2},
+ {PHP, 2},
+ {PKR, 2},
+ {PLN, 2},
+ {PYG, 0},
+ {QAR, 2},
+ {RON, 2},
+ {RSD, 2},
+ {RUB, 2},
+ {RWF, 0},
+ {SAR, 2},
+ {SBD, 2},
+ {SCR, 2},
+ {SDG, 2},
+ {SEK, 2},
+ {SGD, 2},
+ {SHP, 2},
+ {SLL, 2},
+ {SOS, 2},
+ {SRD, 2},
+ {SSP, 2},
+ {STN, 2},
+ {SYP, 2},
+ {SZL, 2},
+ {THB, 2},
+ {TJS, 2},
+ {TMT, 2},
+ {TND, 3},
+ {TOP, 2},
+ {TRY, 2},
+ {TTD, 2},
+ {TWD, 2},
+ {TZS, 2},
+ {UAH, 2},
+ {UGX, 0},
+ {USD, 2},
+ {UYU, 2},
+ {UZS, 2},
+ {VES, 2},
+ {VND, 0},
+ {VUV, 0},
+ {WST, 2},
+ {XAF, 0},
+ {XCD, 2},
+ {XOF, 0},
+ {XPF, 0},
+ {YER, 2},
+ {ZAR, 2},
+ {ZMW, 2},
+ {ZWL, 2},
}
for _, tt := range tests {
got := tt.curr.Scale()
@@ -87,9 +274,163 @@ func TestCurrency_Num(t *testing.T) {
want string
}{
{XXX, "999"},
+ {XTS, "963"},
+ {AED, "784"},
+ {AFN, "971"},
+ {ALL, "008"},
+ {AMD, "051"},
+ {ANG, "532"},
+ {AOA, "973"},
+ {ARS, "032"},
+ {AUD, "036"},
+ {AWG, "533"},
+ {AZN, "944"},
+ {BAM, "977"},
+ {BBD, "052"},
+ {BDT, "050"},
+ {BGN, "975"},
+ {BHD, "048"},
+ {BIF, "108"},
+ {BMD, "060"},
+ {BND, "096"},
+ {BOB, "068"},
+ {BRL, "986"},
+ {BSD, "044"},
+ {BTN, "064"},
+ {BWP, "072"},
+ {BYN, "933"},
+ {BZD, "084"},
+ {CAD, "124"},
+ {CDF, "976"},
+ {CHF, "756"},
+ {CLP, "152"},
+ {CNY, "156"},
+ {COP, "170"},
+ {CRC, "188"},
+ {CUP, "192"},
+ {CVE, "132"},
+ {CZK, "203"},
+ {DJF, "262"},
+ {DKK, "208"},
+ {DOP, "214"},
+ {DZD, "012"},
+ {EGP, "818"},
+ {ERN, "232"},
+ {ETB, "230"},
+ {EUR, "978"},
+ {FJD, "242"},
+ {FKP, "238"},
+ {GBP, "826"},
+ {GEL, "981"},
+ {GHS, "936"},
+ {GIP, "292"},
+ {GMD, "270"},
+ {GNF, "324"},
+ {GTQ, "320"},
+ {GWP, "624"},
+ {GYD, "328"},
+ {HKD, "344"},
+ {HNL, "340"},
+ {HRK, "191"},
+ {HTG, "332"},
+ {HUF, "348"},
+ {IDR, "360"},
+ {ILS, "376"},
+ {INR, "356"},
+ {IQD, "368"},
+ {IRR, "364"},
+ {ISK, "352"},
+ {JMD, "388"},
+ {JOD, "400"},
{JPY, "392"},
- {USD, "840"},
+ {KES, "404"},
+ {KGS, "417"},
+ {KHR, "116"},
+ {KMF, "174"},
+ {KPW, "408"},
+ {KRW, "410"},
+ {KWD, "414"},
+ {KYD, "136"},
+ {KZT, "398"},
+ {LAK, "418"},
+ {LBP, "422"},
+ {LKR, "144"},
+ {LRD, "430"},
+ {LSL, "426"},
+ {LYD, "434"},
+ {MAD, "504"},
+ {MDL, "498"},
+ {MGA, "969"},
+ {MKD, "807"},
+ {MMK, "104"},
+ {MNT, "496"},
+ {MOP, "446"},
+ {MRU, "929"},
+ {MUR, "480"},
+ {MVR, "462"},
+ {MWK, "454"},
+ {MXN, "484"},
+ {MYR, "458"},
+ {MZN, "943"},
+ {NAD, "516"},
+ {NGN, "566"},
+ {NIO, "558"},
+ {NOK, "578"},
+ {NPR, "524"},
+ {NZD, "554"},
{OMR, "512"},
+ {PAB, "590"},
+ {PEN, "604"},
+ {PGK, "598"},
+ {PHP, "608"},
+ {PKR, "586"},
+ {PLN, "985"},
+ {PYG, "600"},
+ {QAR, "634"},
+ {RON, "946"},
+ {RSD, "941"},
+ {RUB, "643"},
+ {RWF, "646"},
+ {SAR, "682"},
+ {SBD, "090"},
+ {SCR, "690"},
+ {SDG, "938"},
+ {SEK, "752"},
+ {SGD, "702"},
+ {SHP, "654"},
+ {SLL, "694"},
+ {SOS, "706"},
+ {SRD, "968"},
+ {SSP, "728"},
+ {STN, "930"},
+ {SYP, "760"},
+ {SZL, "748"},
+ {THB, "764"},
+ {TJS, "972"},
+ {TMT, "934"},
+ {TND, "788"},
+ {TOP, "776"},
+ {TRY, "949"},
+ {TTD, "780"},
+ {TWD, "901"},
+ {TZS, "834"},
+ {UAH, "980"},
+ {UGX, "800"},
+ {USD, "840"},
+ {UYU, "858"},
+ {UZS, "860"},
+ {VES, "928"},
+ {VND, "704"},
+ {VUV, "548"},
+ {WST, "882"},
+ {XAF, "950"},
+ {XCD, "951"},
+ {XOF, "952"},
+ {XPF, "953"},
+ {YER, "886"},
+ {ZAR, "710"},
+ {ZMW, "967"},
+ {ZWL, "932"},
}
for _, tt := range tests {
got := tt.curr.Num()
@@ -105,9 +446,163 @@ func TestCurrency_Code(t *testing.T) {
want string
}{
{XXX, "XXX"},
+ {XTS, "XTS"},
+ {AED, "AED"},
+ {AFN, "AFN"},
+ {ALL, "ALL"},
+ {AMD, "AMD"},
+ {ANG, "ANG"},
+ {AOA, "AOA"},
+ {ARS, "ARS"},
+ {AUD, "AUD"},
+ {AWG, "AWG"},
+ {AZN, "AZN"},
+ {BAM, "BAM"},
+ {BBD, "BBD"},
+ {BDT, "BDT"},
+ {BGN, "BGN"},
+ {BHD, "BHD"},
+ {BIF, "BIF"},
+ {BMD, "BMD"},
+ {BND, "BND"},
+ {BOB, "BOB"},
+ {BRL, "BRL"},
+ {BSD, "BSD"},
+ {BTN, "BTN"},
+ {BWP, "BWP"},
+ {BYN, "BYN"},
+ {BZD, "BZD"},
+ {CAD, "CAD"},
+ {CDF, "CDF"},
+ {CHF, "CHF"},
+ {CLP, "CLP"},
+ {CNY, "CNY"},
+ {COP, "COP"},
+ {CRC, "CRC"},
+ {CUP, "CUP"},
+ {CVE, "CVE"},
+ {CZK, "CZK"},
+ {DJF, "DJF"},
+ {DKK, "DKK"},
+ {DOP, "DOP"},
+ {DZD, "DZD"},
+ {EGP, "EGP"},
+ {ERN, "ERN"},
+ {ETB, "ETB"},
+ {EUR, "EUR"},
+ {FJD, "FJD"},
+ {FKP, "FKP"},
+ {GBP, "GBP"},
+ {GEL, "GEL"},
+ {GHS, "GHS"},
+ {GIP, "GIP"},
+ {GMD, "GMD"},
+ {GNF, "GNF"},
+ {GTQ, "GTQ"},
+ {GWP, "GWP"},
+ {GYD, "GYD"},
+ {HKD, "HKD"},
+ {HNL, "HNL"},
+ {HRK, "HRK"},
+ {HTG, "HTG"},
+ {HUF, "HUF"},
+ {IDR, "IDR"},
+ {ILS, "ILS"},
+ {INR, "INR"},
+ {IQD, "IQD"},
+ {IRR, "IRR"},
+ {ISK, "ISK"},
+ {JMD, "JMD"},
+ {JOD, "JOD"},
{JPY, "JPY"},
- {USD, "USD"},
+ {KES, "KES"},
+ {KGS, "KGS"},
+ {KHR, "KHR"},
+ {KMF, "KMF"},
+ {KPW, "KPW"},
+ {KRW, "KRW"},
+ {KWD, "KWD"},
+ {KYD, "KYD"},
+ {KZT, "KZT"},
+ {LAK, "LAK"},
+ {LBP, "LBP"},
+ {LKR, "LKR"},
+ {LRD, "LRD"},
+ {LSL, "LSL"},
+ {LYD, "LYD"},
+ {MAD, "MAD"},
+ {MDL, "MDL"},
+ {MGA, "MGA"},
+ {MKD, "MKD"},
+ {MMK, "MMK"},
+ {MNT, "MNT"},
+ {MOP, "MOP"},
+ {MRU, "MRU"},
+ {MUR, "MUR"},
+ {MVR, "MVR"},
+ {MWK, "MWK"},
+ {MXN, "MXN"},
+ {MYR, "MYR"},
+ {MZN, "MZN"},
+ {NAD, "NAD"},
+ {NGN, "NGN"},
+ {NIO, "NIO"},
+ {NOK, "NOK"},
+ {NPR, "NPR"},
+ {NZD, "NZD"},
{OMR, "OMR"},
+ {PAB, "PAB"},
+ {PEN, "PEN"},
+ {PGK, "PGK"},
+ {PHP, "PHP"},
+ {PKR, "PKR"},
+ {PLN, "PLN"},
+ {PYG, "PYG"},
+ {QAR, "QAR"},
+ {RON, "RON"},
+ {RSD, "RSD"},
+ {RUB, "RUB"},
+ {RWF, "RWF"},
+ {SAR, "SAR"},
+ {SBD, "SBD"},
+ {SCR, "SCR"},
+ {SDG, "SDG"},
+ {SEK, "SEK"},
+ {SGD, "SGD"},
+ {SHP, "SHP"},
+ {SLL, "SLL"},
+ {SOS, "SOS"},
+ {SRD, "SRD"},
+ {SSP, "SSP"},
+ {STN, "STN"},
+ {SYP, "SYP"},
+ {SZL, "SZL"},
+ {THB, "THB"},
+ {TJS, "TJS"},
+ {TMT, "TMT"},
+ {TND, "TND"},
+ {TOP, "TOP"},
+ {TRY, "TRY"},
+ {TTD, "TTD"},
+ {TWD, "TWD"},
+ {TZS, "TZS"},
+ {UAH, "UAH"},
+ {UGX, "UGX"},
+ {USD, "USD"},
+ {UYU, "UYU"},
+ {UZS, "UZS"},
+ {VES, "VES"},
+ {VND, "VND"},
+ {VUV, "VUV"},
+ {WST, "WST"},
+ {XAF, "XAF"},
+ {XCD, "XCD"},
+ {XOF, "XOF"},
+ {XPF, "XPF"},
+ {YER, "YER"},
+ {ZAR, "ZAR"},
+ {ZMW, "ZMW"},
+ {ZWL, "ZWL"},
}
for _, tt := range tests {
got := tt.curr.Code()
@@ -171,20 +666,37 @@ func TestCurrency_Format(t *testing.T) {
func TestCurrency_Scan(t *testing.T) {
t.Run("error", func(t *testing.T) {
- c := XXX
- err := c.Scan([]byte("USD"))
- if err == nil {
- t.Errorf("c.Scan([]byte(\"USD\")) did not fail")
+ tests := []any{"UUU", 840, []byte{0x08, 0x40}, nil}
+ for _, tt := range tests {
+ var got Currency
+ err := got.Scan(tt)
+ if err == nil {
+ t.Errorf("Scan(%q) did not fail", tt)
+ }
}
})
}
+func TestNullCurrency_Interfaces(t *testing.T) {
+ var i any = NullCurrency{}
+ _, ok := i.(driver.Valuer)
+ if !ok {
+ t.Errorf("%T does not implement driver.Valuer", i)
+ }
+
+ i = &NullCurrency{}
+ _, ok = i.(sql.Scanner)
+ if !ok {
+ t.Errorf("%T does not implement sql.Scanner", i)
+ }
+}
+
func TestNullCurrency_Scan(t *testing.T) {
- t.Run("[]byte", func(t *testing.T) {
- tests := []string{"UUU"}
+ t.Run("error", func(t *testing.T) {
+ tests := []any{"UUU", 840, []byte{0x08, 0x40}}
for _, tt := range tests {
- got := NullCurrency{}
- err := got.Scan([]byte(tt))
+ var got NullCurrency
+ err := got.Scan(tt)
if err == nil {
t.Errorf("Scan(%q) did not fail", tt)
}
diff --git a/doc.go b/doc.go
index 12d2f27..9c8adf0 100644
--- a/doc.go
+++ b/doc.go
@@ -46,7 +46,7 @@ Here are the ranges for scales 0, 2, and 3:
| US Dollar | 2 | -99,999,999,999,999,999.99 | 99,999,999,999,999,999.99 |
| Omani Rial | 3 | -9,999,999,999,999,999.999 | 9,999,999,999,999,999.999 |
-Subnoral numbers are not supported by the underlying [decimal.Decimal] type.
+Subnormal numbers are not supported by the underlying [decimal.Decimal] type.
Consequently, amounts and exchange rates between -0.00000000000000000005 and
0.00000000000000000005 inclusive are rounded to 0.
diff --git a/doc_test.go b/doc_test.go
index 74320a8..c330d4e 100644
--- a/doc_test.go
+++ b/doc_test.go
@@ -2,6 +2,7 @@ package money_test
import (
"encoding/json"
+ "encoding/xml"
"fmt"
"strconv"
"strings"
@@ -593,19 +594,19 @@ func ExampleCurrency_Scale() {
// 3
}
-type Value struct {
+type Object struct {
Currency money.Currency `json:"currency"`
}
-func ExampleCurrency_UnmarshalText() {
- var v Value
+func ExampleCurrency_UnmarshalText_json() {
+ var v Object
_ = json.Unmarshal([]byte(`{"currency":"USD"}`), &v)
fmt.Println(v)
// Output: {USD}
}
-func ExampleCurrency_MarshalText() {
- v := Value{
+func ExampleCurrency_MarshalText_json() {
+ v := Object{
Currency: money.USD,
}
b, _ := json.Marshal(v)
@@ -613,6 +614,26 @@ func ExampleCurrency_MarshalText() {
// Output: {"currency":"USD"}
}
+type Entity struct {
+ Currency money.Currency `xml:"Currency"`
+}
+
+func ExampleCurrency_UnmarshalText_xml() {
+ var v Entity
+ _ = xml.Unmarshal([]byte(`USD`), &v)
+ fmt.Println(v)
+ // Output: {USD}
+}
+
+func ExampleCurrency_MarshalText_xml() {
+ v := Entity{
+ Currency: money.USD,
+ }
+ b, _ := xml.Marshal(v)
+ fmt.Println(string(b))
+ // Output: USD
+}
+
func ExampleCurrency_Scan() {
u := money.XXX
_ = u.Scan("USD")
@@ -891,8 +912,8 @@ func ExampleAmount_Int64() {
fmt.Println(a.Int64(3))
fmt.Println(a.Int64(4))
// Output:
- // 0 0 false
- // 0 0 false
+ // 6 0 true
+ // 5 7 true
// 5 68 true
// 5 678 true
// 5 6780 true
@@ -1016,9 +1037,9 @@ func ExampleAmount_Rescale_currencies() {
fmt.Println(b.Rescale(0))
fmt.Println(c.Rescale(0))
// Output:
- // JPY 6
- // USD 5.68
- // OMR 5.678
+ // JPY 6
+ // USD 6.00
+ // OMR 6.000
}
func ExampleAmount_Rescale_scales() {
@@ -1029,11 +1050,11 @@ func ExampleAmount_Rescale_scales() {
fmt.Println(a.Rescale(3))
fmt.Println(a.Rescale(4))
// Output:
- // USD 5.68
- // USD 5.68
- // USD 5.68
- // USD 5.679
- // USD 5.6789
+ // USD 6.00
+ // USD 5.70
+ // USD 5.68
+ // USD 5.679
+ // USD 5.6789
}
func ExampleAmount_Round_currencies() {
@@ -1045,8 +1066,8 @@ func ExampleAmount_Round_currencies() {
fmt.Println(c.Round(0))
// Output:
// JPY 6
- // USD 5.68
- // OMR 5.678
+ // USD 6.00
+ // OMR 6.000
}
func ExampleAmount_Round_scales() {
@@ -1057,8 +1078,8 @@ func ExampleAmount_Round_scales() {
fmt.Println(a.Round(3))
fmt.Println(a.Round(4))
// Output:
- // USD 5.68
- // USD 5.68
+ // USD 6.00
+ // USD 5.70
// USD 5.68
// USD 5.679
// USD 5.6789
@@ -1086,9 +1107,9 @@ func ExampleAmount_Quantize() {
fmt.Println(a.Quantize(y))
fmt.Println(a.Quantize(z))
// Output:
- // JPY 6
- // JPY 5.7
- // JPY 5.68
+ // JPY 6
+ // JPY 5.7
+ // JPY 5.68
}
func ExampleAmount_Ceil_currencies() {
@@ -1100,8 +1121,8 @@ func ExampleAmount_Ceil_currencies() {
fmt.Println(c.Ceil(0))
// Output:
// JPY 6
- // USD 5.68
- // OMR 5.678
+ // USD 6.00
+ // OMR 6.000
}
func ExampleAmount_Ceil_scales() {
@@ -1112,8 +1133,8 @@ func ExampleAmount_Ceil_scales() {
fmt.Println(a.Ceil(3))
fmt.Println(a.Ceil(4))
// Output:
- // USD 5.68
- // USD 5.68
+ // USD 6.00
+ // USD 5.70
// USD 5.68
// USD 5.679
// USD 5.6789
@@ -1141,8 +1162,8 @@ func ExampleAmount_Floor_currencies() {
fmt.Println(c.Floor(0))
// Output:
// JPY 5
- // USD 5.67
- // OMR 5.678
+ // USD 5.00
+ // OMR 5.000
}
func ExampleAmount_Floor_scales() {
@@ -1153,8 +1174,8 @@ func ExampleAmount_Floor_scales() {
fmt.Println(a.Floor(3))
fmt.Println(a.Floor(4))
// Output:
- // USD 5.67
- // USD 5.67
+ // USD 5.00
+ // USD 5.60
// USD 5.67
// USD 5.678
// USD 5.6789
@@ -1182,8 +1203,8 @@ func ExampleAmount_Trunc_currencies() {
fmt.Println(c.Trunc(0))
// Output:
// JPY 5
- // USD 5.67
- // OMR 5.678
+ // USD 5.00
+ // OMR 5.000
}
func ExampleAmount_Trunc_scales() {
@@ -1194,8 +1215,8 @@ func ExampleAmount_Trunc_scales() {
fmt.Println(a.Trunc(3))
fmt.Println(a.Trunc(4))
// Output:
- // USD 5.67
- // USD 5.67
+ // USD 5.00
+ // USD 5.60
// USD 5.67
// USD 5.678
// USD 5.6789
@@ -1300,20 +1321,7 @@ func ExampleAmount_Scale() {
// 2
}
-func ExampleAmount_MinScale_currencies() {
- a := money.MustParseAmount("JPY", "5.0000")
- b := money.MustParseAmount("USD", "5.0000")
- c := money.MustParseAmount("OMR", "5.0000")
- fmt.Println(a.MinScale())
- fmt.Println(b.MinScale())
- fmt.Println(c.MinScale())
- // Output:
- // 0
- // 2
- // 3
-}
-
-func ExampleAmount_MinScale_scales() {
+func ExampleAmount_MinScale() {
a := money.MustParseAmount("USD", "5.6000")
b := money.MustParseAmount("USD", "5.6700")
c := money.MustParseAmount("USD", "5.6780")
@@ -1321,7 +1329,7 @@ func ExampleAmount_MinScale_scales() {
fmt.Println(b.MinScale())
fmt.Println(c.MinScale())
// Output:
- // 2
+ // 1
// 2
// 3
}
@@ -1758,20 +1766,7 @@ func ExampleExchangeRate_Scale() {
// 5
}
-func ExampleExchangeRate_MinScale_currencies() {
- r := money.MustParseExchRate("EUR", "JPY", "5.0000")
- q := money.MustParseExchRate("EUR", "USD", "5.0000")
- p := money.MustParseExchRate("EUR", "OMR", "5.0000")
- fmt.Println(r.MinScale())
- fmt.Println(q.MinScale())
- fmt.Println(p.MinScale())
- // Output:
- // 0
- // 2
- // 3
-}
-
-func ExampleExchangeRate_MinScale_scales() {
+func ExampleExchangeRate_MinScale() {
r := money.MustParseExchRate("EUR", "USD", "5.6000")
q := money.MustParseExchRate("EUR", "USD", "5.6700")
p := money.MustParseExchRate("EUR", "USD", "5.6780")
@@ -1779,7 +1774,7 @@ func ExampleExchangeRate_MinScale_scales() {
fmt.Println(q.MinScale())
fmt.Println(p.MinScale())
// Output:
- // 2
+ // 1
// 2
// 3
}
@@ -1862,15 +1857,15 @@ func ExampleExchangeRate_Float64() {
}
func ExampleExchangeRate_Int64() {
- a := money.MustParseExchRate("EUR", "USD", "5.678")
- fmt.Println(a.Int64(0))
- fmt.Println(a.Int64(1))
- fmt.Println(a.Int64(2))
- fmt.Println(a.Int64(3))
- fmt.Println(a.Int64(4))
- // Output:
- // 0 0 false
- // 0 0 false
+ r := money.MustParseExchRate("EUR", "USD", "5.678")
+ fmt.Println(r.Int64(0))
+ fmt.Println(r.Int64(1))
+ fmt.Println(r.Int64(2))
+ fmt.Println(r.Int64(3))
+ fmt.Println(r.Int64(4))
+ // Output:
+ // 6 0 true
+ // 5 7 true
// 5 68 true
// 5 678 true
// 5 6780 true
@@ -1913,14 +1908,14 @@ func ExampleExchangeRate_CanConv() {
}
func ExampleExchangeRate_Format_currencies() {
- a := money.MustParseExchRate("EUR", "JPY", "5")
- b := money.MustParseExchRate("EUR", "USD", "5")
- c := money.MustParseExchRate("EUR", "OMR", "5")
+ r := money.MustParseExchRate("EUR", "JPY", "5")
+ q := money.MustParseExchRate("EUR", "USD", "5")
+ p := money.MustParseExchRate("EUR", "OMR", "5")
fmt.Println("| v | f | b | c |")
fmt.Println("| ------------- | ----- | --- | --- |")
- fmt.Printf("| %-13[1]v | %5[1]f | %[1]b | %[1]c |\n", a)
- fmt.Printf("| %-13[1]v | %5[1]f | %[1]b | %[1]c |\n", b)
- fmt.Printf("| %-13[1]v | %5[1]f | %[1]b | %[1]c |\n", c)
+ fmt.Printf("| %-13[1]v | %5[1]f | %[1]b | %[1]c |\n", r)
+ fmt.Printf("| %-13[1]v | %5[1]f | %[1]b | %[1]c |\n", q)
+ fmt.Printf("| %-13[1]v | %5[1]f | %[1]b | %[1]c |\n", p)
// Output:
// | v | f | b | c |
// | ------------- | ----- | --- | --- |
@@ -2005,28 +2000,28 @@ func ExampleExchangeRate_String() {
}
func ExampleExchangeRate_Floor_currencies() {
- a := money.MustParseExchRate("EUR", "JPY", "5.678")
- b := money.MustParseExchRate("EUR", "USD", "5.678")
- c := money.MustParseExchRate("EUR", "OMR", "5.678")
- fmt.Println(a.Floor(0))
- fmt.Println(b.Floor(0))
- fmt.Println(c.Floor(0))
+ r := money.MustParseExchRate("EUR", "JPY", "5.678")
+ q := money.MustParseExchRate("EUR", "USD", "5.678")
+ p := money.MustParseExchRate("EUR", "OMR", "5.678")
+ fmt.Println(r.Floor(0))
+ fmt.Println(q.Floor(0))
+ fmt.Println(p.Floor(0))
// Output:
// EUR/JPY 5
- // EUR/USD 5.67
- // EUR/OMR 5.678
+ // EUR/USD 5.00
+ // EUR/OMR 5.000
}
func ExampleExchangeRate_Floor_scales() {
- a := money.MustParseExchRate("EUR", "USD", "5.6789")
- fmt.Println(a.Floor(0))
- fmt.Println(a.Floor(1))
- fmt.Println(a.Floor(2))
- fmt.Println(a.Floor(3))
- fmt.Println(a.Floor(4))
- // Output:
- // EUR/USD 5.67
- // EUR/USD 5.67
+ r := money.MustParseExchRate("EUR", "USD", "5.6789")
+ fmt.Println(r.Floor(0))
+ fmt.Println(r.Floor(1))
+ fmt.Println(r.Floor(2))
+ fmt.Println(r.Floor(3))
+ fmt.Println(r.Floor(4))
+ // Output:
+ // EUR/USD 5.00
+ // EUR/USD 5.60
// EUR/USD 5.67
// EUR/USD 5.678
// EUR/USD 5.6789
@@ -2041,8 +2036,8 @@ func ExampleExchangeRate_Rescale_currencies() {
fmt.Println(p.Rescale(0))
// Output:
// EUR/JPY 6
- // EUR/USD 5.68
- // EUR/OMR 5.678
+ // EUR/USD 6.00
+ // EUR/OMR 6.000
}
func ExampleExchangeRate_Rescale_scales() {
@@ -2053,8 +2048,8 @@ func ExampleExchangeRate_Rescale_scales() {
fmt.Println(r.Rescale(3))
fmt.Println(r.Rescale(4))
// Output:
- // EUR/USD 5.68
- // EUR/USD 5.68
+ // EUR/USD 6.00
+ // EUR/USD 5.70
// EUR/USD 5.68
// EUR/USD 5.679
// EUR/USD 5.6789
@@ -2083,8 +2078,8 @@ func ExampleExchangeRate_Round_currencies() {
fmt.Println(p.Round(0))
// Output:
// EUR/JPY 6
- // EUR/USD 5.68
- // EUR/OMR 5.678
+ // EUR/USD 6.00
+ // EUR/OMR 6.000
}
func ExampleExchangeRate_Round_scales() {
@@ -2095,8 +2090,8 @@ func ExampleExchangeRate_Round_scales() {
fmt.Println(r.Round(3))
fmt.Println(r.Round(4))
// Output:
- // EUR/USD 5.68
- // EUR/USD 5.68
+ // EUR/USD 6.00
+ // EUR/USD 5.70
// EUR/USD 5.68
// EUR/USD 5.679
// EUR/USD 5.6789
@@ -2116,12 +2111,12 @@ func ExampleExchangeRate_Trim_currencies() {
}
func ExampleExchangeRate_Trim_scales() {
- a := money.MustParseExchRate("EUR", "USD", "5.0000")
- fmt.Println(a.Trim(0))
- fmt.Println(a.Trim(1))
- fmt.Println(a.Trim(2))
- fmt.Println(a.Trim(3))
- fmt.Println(a.Trim(4))
+ r := money.MustParseExchRate("EUR", "USD", "5.0000")
+ fmt.Println(r.Trim(0))
+ fmt.Println(r.Trim(1))
+ fmt.Println(r.Trim(2))
+ fmt.Println(r.Trim(3))
+ fmt.Println(r.Trim(4))
// Output:
// EUR/USD 5.00
// EUR/USD 5.00
@@ -2131,28 +2126,28 @@ func ExampleExchangeRate_Trim_scales() {
}
func ExampleExchangeRate_Trunc_currencies() {
- a := money.MustParseExchRate("EUR", "JPY", "5.678")
- b := money.MustParseExchRate("EUR", "USD", "5.678")
- c := money.MustParseExchRate("EUR", "OMR", "5.678")
- fmt.Println(a.Trunc(0))
- fmt.Println(b.Trunc(0))
- fmt.Println(c.Trunc(0))
+ r := money.MustParseExchRate("EUR", "JPY", "5.678")
+ q := money.MustParseExchRate("EUR", "USD", "5.678")
+ p := money.MustParseExchRate("EUR", "OMR", "5.678")
+ fmt.Println(r.Trunc(0))
+ fmt.Println(q.Trunc(0))
+ fmt.Println(p.Trunc(0))
// Output:
// EUR/JPY 5
- // EUR/USD 5.67
- // EUR/OMR 5.678
+ // EUR/USD 5.00
+ // EUR/OMR 5.000
}
func ExampleExchangeRate_Trunc_scales() {
- a := money.MustParseExchRate("EUR", "USD", "5.6789")
- fmt.Println(a.Trunc(0))
- fmt.Println(a.Trunc(1))
- fmt.Println(a.Trunc(2))
- fmt.Println(a.Trunc(3))
- fmt.Println(a.Trunc(4))
- // Output:
- // EUR/USD 5.67
- // EUR/USD 5.67
+ r := money.MustParseExchRate("EUR", "USD", "5.6789")
+ fmt.Println(r.Trunc(0))
+ fmt.Println(r.Trunc(1))
+ fmt.Println(r.Trunc(2))
+ fmt.Println(r.Trunc(3))
+ fmt.Println(r.Trunc(4))
+ // Output:
+ // EUR/USD 5.00
+ // EUR/USD 5.60
// EUR/USD 5.67
// EUR/USD 5.678
// EUR/USD 5.6789
@@ -2167,8 +2162,8 @@ func ExampleExchangeRate_Ceil_currencies() {
fmt.Println(p.Ceil(0))
// Output:
// EUR/JPY 6
- // EUR/USD 5.68
- // EUR/OMR 5.678
+ // EUR/USD 6.00
+ // EUR/OMR 6.000
}
func ExampleExchangeRate_Ceil_scales() {
@@ -2179,8 +2174,8 @@ func ExampleExchangeRate_Ceil_scales() {
fmt.Println(r.Ceil(3))
fmt.Println(r.Ceil(4))
// Output:
- // EUR/USD 5.68
- // EUR/USD 5.68
+ // EUR/USD 6.00
+ // EUR/USD 5.70
// EUR/USD 5.68
// EUR/USD 5.679
// EUR/USD 5.6789
diff --git a/exchange_rate.go b/exchange_rate.go
index c1cfa02..d15523c 100644
--- a/exchange_rate.go
+++ b/exchange_rate.go
@@ -8,8 +8,10 @@ import (
"github.com/govalues/decimal"
)
+var errRateOverflow = fmt.Errorf("rate overflow")
+
// ExchangeRate represents a unidirectional exchange rate between two currencies.
-// The zero value corresponds to an exchange rate of "XXX/XXX 0", where XXX indicates
+// The zero value corresponds to an exchange rate of "XXX/XXX 0", where [XXX] indicates
// an unknown currency.
// This type is designed to be safe for concurrent use by multiple goroutines.
type ExchangeRate struct {
@@ -36,10 +38,9 @@ func newExchRateSafe(b, q Currency, d decimal.Decimal) (ExchangeRate, error) {
return ExchangeRate{}, fmt.Errorf("exchange rate between identical currencies must be equal to 1")
}
if d.Scale() < q.Scale() {
- var err error
- d, err = d.Pad(q.Scale())
- if err != nil {
- return ExchangeRate{}, fmt.Errorf("padding exchange rate: %w", err)
+ d = d.Pad(q.Scale())
+ if d.Scale() < q.Scale() {
+ return ExchangeRate{}, fmt.Errorf("padding exchange rate: %w", errRateOverflow)
}
}
return newExchRateUnsafe(b, q, d), nil
@@ -260,16 +261,11 @@ func (r ExchangeRate) Float64() (f float64, ok bool) {
// This method is useful for converting rates to [protobuf] format.
// See also constructor [NewExchRateFromInt64].
//
-// Int64 returns false if:
-// - given scale is smaller than the scale of the quote currency;
-// - the result cannot be represented as a pair of int64 values.
+// Int64 returns false if the result cannot be represented as a pair of int64 values.
//
// [rounding half to even]: https://en.wikipedia.org/wiki/Rounding#Rounding_half_to_even
// [protobuf]: https://github.com/googleapis/googleapis/blob/master/google/type/money.proto
func (r ExchangeRate) Int64(scale int) (whole, frac int64, ok bool) {
- if scale < r.Quote().Scale() {
- return 0, 0, false
- }
return r.Decimal().Int64(scale)
}
@@ -294,7 +290,7 @@ func (r ExchangeRate) CanConv(b Amount) bool {
func (r ExchangeRate) Conv(b Amount) (Amount, error) {
c, err := r.conv(b)
if err != nil {
- return Amount{}, fmt.Errorf("computing [%v * %v]: %w", b, r, err)
+ return Amount{}, fmt.Errorf("converting [%v] to [%v]: %w", b, r.Quote(), err)
}
return c, nil
}
@@ -387,11 +383,7 @@ func (r ExchangeRate) Scale() int {
// without rounding.
// See also method [ExchangeRate.Trim].
func (r ExchangeRate) MinScale() int {
- s := r.Decimal().MinScale()
- if s < r.Quote().Scale() {
- s = r.Quote().Scale()
- }
- return s
+ return r.Decimal().MinScale()
}
// IsZero returns:
@@ -436,24 +428,17 @@ func (r ExchangeRate) WithinOne() bool {
// Ceil returns a rate rounded up to the specified number of digits after
// the decimal point using [rounding toward positive infinity].
-// If the given scale is less than the scale of the quote currency,
-// the rate will be rounded up to the scale of the quote currency instead.
// See also method [ExchangeRate.Floor].
//
// [rounding toward positive infinity]: https://en.wikipedia.org/wiki/Rounding#Rounding_up
func (r ExchangeRate) Ceil(scale int) (ExchangeRate, error) {
b, q, d := r.Base(), r.Quote(), r.Decimal()
- if scale < q.Scale() {
- scale = q.Scale()
- }
- d = d.Ceil(scale)
+ d = d.Ceil(scale).Pad(q.Scale())
return newExchRateSafe(b, q, d)
}
// Floor returns a rate rounded down to the specified number of digits after
// the decimal point using [rounding toward negative infinity].
-// If the given scale is less than the scale of the quote currency,
-// the rate will be rounded down to the scale of the quote currency instead.
// See also method [ExchangeRate.Ceil].
//
// Floor returns an error if the result is 0.
@@ -461,10 +446,7 @@ func (r ExchangeRate) Ceil(scale int) (ExchangeRate, error) {
// [rounding toward negative infinity]: https://en.wikipedia.org/wiki/Rounding#Rounding_down
func (r ExchangeRate) Floor(scale int) (ExchangeRate, error) {
b, q, d := r.Base(), r.Quote(), r.Decimal()
- if scale < q.Scale() {
- scale = q.Scale()
- }
- d = d.Floor(scale)
+ d = d.Floor(scale).Pad(q.Scale())
p, err := newExchRateSafe(b, q, d)
if err != nil {
return ExchangeRate{}, fmt.Errorf("flooring %v: %w", r, err)
@@ -474,18 +456,13 @@ func (r ExchangeRate) Floor(scale int) (ExchangeRate, error) {
// Trunc returns a rate truncated to the specified number of digits after
// the decimal point using [rounding toward zero].
-// If the given scale is less than the scale of the quote currency,
-// the rate will be truncated to the scale of the quote currency instead.
//
// Trunc returns an error if the result is 0.
//
// [rounding toward zero]: https://en.wikipedia.org/wiki/Rounding#Rounding_toward_zero
func (r ExchangeRate) Trunc(scale int) (ExchangeRate, error) {
b, q, d := r.Base(), r.Quote(), r.Decimal()
- if scale < q.Scale() {
- scale = q.Scale()
- }
- d = d.Trunc(scale)
+ d = d.Trunc(scale).Pad(q.Scale())
p, err := newExchRateSafe(b, q, d)
if err != nil {
return ExchangeRate{}, fmt.Errorf("truncating %v: %w", r, err)
@@ -498,17 +475,13 @@ func (r ExchangeRate) Trunc(scale int) (ExchangeRate, error) {
// zeros will be removed up to the scale of the quote currency instead.
func (r ExchangeRate) Trim(scale int) ExchangeRate {
b, q, d := r.Base(), r.Quote(), r.Decimal()
- if scale < q.Scale() {
- scale = q.Scale()
- }
+ scale = max(scale, q.Scale())
d = d.Trim(scale)
return newExchRateUnsafe(b, q, d)
}
// Round returns a rate rounded to the specified number of digits after
// the decimal point using [rounding half to even] (banker's rounding).
-// If the given scale is less than the scale of the quote currency,
-// the rate will be rounded to the scale of the quote currency instead.
// See also method [ExchangeRate.Rescale].
//
// Round returns an error if the result is 0.
@@ -516,10 +489,7 @@ func (r ExchangeRate) Trim(scale int) ExchangeRate {
// [rounding half to even]: https://en.wikipedia.org/wiki/Rounding#Rounding_half_to_even
func (r ExchangeRate) Round(scale int) (ExchangeRate, error) {
b, q, d := r.Base(), r.Quote(), r.Decimal()
- if scale < q.Scale() {
- scale = q.Scale()
- }
- d = d.Round(scale)
+ d = d.Round(scale).Pad(q.Scale())
p, err := newExchRateSafe(b, q, d)
if err != nil {
return ExchangeRate{}, fmt.Errorf("rounding %v: %w", r, err)
@@ -531,10 +501,7 @@ func (r ExchangeRate) Round(scale int) (ExchangeRate, error) {
// The currency and the sign of rate q are ignored.
// See also methods [ExchangeRate.Scale], [ExchangeRate.SameScale], [ExchangeRate.Rescale].
//
-// Quantize returns an error if:
-// - the result is 0;
-// - the integer part of the result has more than
-// ([decimal.MaxPrec] - [Currency.Scale]) digits.
+// Quantize returns an error if the result is 0.
func (r ExchangeRate) Quantize(q ExchangeRate) (ExchangeRate, error) {
p, err := r.rescale(q.Scale())
if err != nil {
@@ -545,14 +512,9 @@ func (r ExchangeRate) Quantize(q ExchangeRate) (ExchangeRate, error) {
// Rescale returns a rate rounded or zero-padded to the given number of digits
// after the decimal point.
-// If the specified scale is less than the scale of the quote currency,
-// the amount will be rounded to the scale of the quote currency instead.
// See also method [ExchangeRate.Round].
//
-// Rescale returns an error if:
-// - the result is 0;
-// - the integer part of the result has more than
-// ([decimal.MaxPrec] - scale) digits.
+// Rescale returns an error if the result is 0.
func (r ExchangeRate) Rescale(scale int) (ExchangeRate, error) {
q, err := r.rescale(scale)
if err != nil {
@@ -563,13 +525,7 @@ func (r ExchangeRate) Rescale(scale int) (ExchangeRate, error) {
func (r ExchangeRate) rescale(scale int) (ExchangeRate, error) {
b, q, d := r.Base(), r.Quote(), r.Decimal()
- if scale < q.Scale() {
- scale = q.Scale()
- }
- d, err := d.Rescale(scale)
- if err != nil {
- return ExchangeRate{}, err
- }
+ d = d.Rescale(scale).Pad(q.Scale())
return newExchRateSafe(b, q, d)
}
@@ -658,28 +614,26 @@ func (r ExchangeRate) Format(state fmt.State, verb rune) {
b, q, d := r.Base(), r.Quote(), r.Decimal()
// Rescaling
- tzeroes := 0
+ var tzeros int
if verb == 'f' || verb == 'F' {
- scale := 0
+ var scale int
switch p, ok := state.Precision(); {
case ok:
scale = p
default:
scale = d.Scale()
}
- if scale < q.Scale() {
- scale = q.Scale()
- }
+ scale = max(scale, q.Scale())
switch {
case scale < d.Scale():
d = d.Round(scale)
case scale > d.Scale():
- tzeroes = scale - d.Scale()
+ tzeros = scale - d.Scale()
}
}
// Integer and fractional digits
- intdigs, fracdigs := 0, 0
+ var intdigs, fracdigs int
switch rprec := d.Prec(); verb {
case 'b', 'B', 'c', 'C':
// skip
@@ -694,14 +648,14 @@ func (r ExchangeRate) Format(state fmt.State, verb rune) {
}
// Decimal point
- dpoint := 0
- if fracdigs > 0 || tzeroes > 0 {
+ var dpoint int
+ if fracdigs > 0 || tzeros > 0 {
dpoint = 1
}
// Currency codes and delimiters
basecode, quocode := "", ""
- basesyms, quosyms, pairdel, currdel := 0, 0, 0, 0
+ var basesyms, quosyms, pairdel, currdel int
switch verb {
case 'f', 'F':
// skip
@@ -721,20 +675,20 @@ func (r ExchangeRate) Format(state fmt.State, verb rune) {
}
// Opening and closing quotes
- lquote, tquote := 0, 0
+ var lquote, tquote int
if verb == 'q' || verb == 'Q' {
lquote, tquote = 1, 1
}
// Calculating padding
- width := lquote + basesyms + pairdel + quosyms + currdel + intdigs + dpoint + fracdigs + tzeroes + tquote
- lspaces, lzeroes, tspaces := 0, 0, 0
+ width := lquote + basesyms + pairdel + quosyms + currdel + intdigs + dpoint + fracdigs + tzeros + tquote
+ var lspaces, lzeros, tspaces int
if w, ok := state.Width(); ok && w > width {
switch {
case state.Flag('-'):
tspaces = w - width
case state.Flag('0') && verb != 'c' && verb != 'C' && verb != 'b' && verb != 'B':
- lzeroes = w - width
+ lzeros = w - width
default:
lspaces = w - width
}
@@ -756,8 +710,8 @@ func (r ExchangeRate) Format(state fmt.State, verb rune) {
pos--
}
- // Trailing zeroes
- for i := 0; i < tzeroes; i++ {
+ // Trailing zeros
+ for i := 0; i < tzeros; i++ {
buf[pos] = '0'
pos--
}
@@ -783,8 +737,8 @@ func (r ExchangeRate) Format(state fmt.State, verb rune) {
coef /= 10
}
- // Leading zeroes
- for i := 0; i < lzeroes; i++ {
+ // Leading zeros
+ for i := 0; i < lzeros; i++ {
buf[pos] = '0'
pos--
}
@@ -826,6 +780,7 @@ func (r ExchangeRate) Format(state fmt.State, verb rune) {
}
// Writing result
+ //nolint:errcheck
switch verb {
case 'q', 'Q', 's', 'S', 'v', 'V', 'f', 'F', 'b', 'B', 'c', 'C':
state.Write(buf)
diff --git a/exchange_rate_test.go b/exchange_rate_test.go
index 1e73048..8bcbb7b 100644
--- a/exchange_rate_test.go
+++ b/exchange_rate_test.go
@@ -24,7 +24,7 @@ func TestExchangeRate_ZeroValue(t *testing.T) {
}
}
-func TestExchangeRate_Sizeof(t *testing.T) {
+func TestExchangeRate_Size(t *testing.T) {
r := ExchangeRate{}
got := unsafe.Sizeof(r)
want := uintptr(24)
@@ -33,6 +33,18 @@ func TestExchangeRate_Sizeof(t *testing.T) {
}
}
+func TestExchangeRate_Interfaces(t *testing.T) {
+ var i any = ExchangeRate{}
+ _, ok := i.(fmt.Stringer)
+ if !ok {
+ t.Errorf("%T does not implement fmt.Stringer", i)
+ }
+ _, ok = i.(fmt.Formatter)
+ if !ok {
+ t.Errorf("%T does not implement fmt.Formatter", i)
+ }
+}
+
func TestMustNewExchRate(t *testing.T) {
t.Run("error", func(t *testing.T) {
defer func() {
@@ -154,7 +166,8 @@ func TestNewExchRateFromInt64(t *testing.T) {
}{
"quote currency 1": {"EUR", "UUU", 1, 0, 0},
"base currency 1": {"EEE", "USD", 1, 0, 0},
- "different signs 1": {"EUR", "USD", -1, 1, 0},
+ "different signs 1": {"EUR", "USD", -1, 1, 1},
+ "different signs 2": {"EUR", "USD", 1, -1, 1},
"fraction range 1": {"EUR", "USD", 1, 1, 0},
"scale range 1": {"EUR", "USD", 1, 1, -1},
"scale range 2": {"EUR", "USD", 1, 0, -1},
@@ -592,8 +605,8 @@ func TestExchangeRate_Floor(t *testing.T) {
scale int
want string
}{
- {"USD", "EUR", "0.8000", 0, "0.80"},
- {"USD", "EUR", "0.0800", 1, "0.08"},
+ {"USD", "EUR", "0.8000", 1, "0.80"},
+ {"USD", "EUR", "0.0800", 2, "0.08"},
}
for _, tt := range tests {
r := MustParseExchRate(tt.b, tt.q, tt.r)
@@ -614,8 +627,10 @@ func TestExchangeRate_Floor(t *testing.T) {
base, quote, r string
scale int
}{
- "zero rate 1": {"USD", "EUR", "0.0080", 2},
- "zero rate 2": {"USD", "EUR", "0.0008", 3},
+ "zero rate 0": {"USD", "EUR", "0.8000", 0},
+ "zero rate 1": {"USD", "EUR", "0.0800", 1},
+ "zero rate 2": {"USD", "EUR", "0.0080", 2},
+ "zero rate 3": {"USD", "EUR", "0.0008", 3},
}
for _, tt := range tests {
r := MustParseExchRate(tt.base, tt.quote, tt.r)
@@ -769,7 +784,6 @@ func TestExchangeRate_Rescale(t *testing.T) {
}{
"zero rate 1": {"USD", "EUR", "0.0050", 2},
"zero rate 2": {"USD", "EUR", "0.0005", 3},
- "scale 1": {"USD", "EUR", "0.0005", 20},
}
for _, tt := range tests {
r := MustParseExchRate(tt.base, tt.quote, tt.r)
diff --git a/go.mod b/go.mod
index 1541ead..ad43cb0 100644
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,5 @@
module github.com/govalues/money
-go 1.20
+go 1.21
-require github.com/govalues/decimal v0.1.19
+require github.com/govalues/decimal v0.1.29
diff --git a/go.sum b/go.sum
index b047c84..dec893f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,2 @@
-github.com/govalues/decimal v0.1.19 h1:cqFTYCcEmnJ8h0flsiLTJGFHp4GLNqf71a9tfSAZPOg=
-github.com/govalues/decimal v0.1.19/go.mod h1:irMp3+UfATz5dlLhUagswX2ATLhGDmo/Hoq2MP4/9gg=
+github.com/govalues/decimal v0.1.29 h1:GKC5g9y9oWxKIy51czdHTShOABwHm/shVuOVPwG415M=
+github.com/govalues/decimal v0.1.29/go.mod h1:LUlHHucpCmA4rJfNrDvMgrWibDpYnDNWqJuNU1/gxW8=