diff --git a/.golangci.yml b/.golangci.yml index 9e75ab6..453957e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,8 +8,12 @@ linters: - goimports - govet - ineffassign + - godot + - gosec - misspell + - stylecheck - revive - staticcheck - typecheck - unused + - gocyclo diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c967c..d8d6e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.1.3] - 2023-08-21 + +### Added + +- Implemented `Currency.Scan` and `Currency.Value`. + +### Changed + +- `Amount.CopySign` treats 0 as a positive. +- Enabled `gocyclo`, `gosec`, `godot`, and `stylecheck` linters. + ## [0.1.2] - 2023-08-15 ### Added diff --git a/README.md b/README.md index 5fed30f..e88f3d2 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ [![githubb]][github] [![codecovb]][codecov] [![goreportb]][goreport] -[![licenseb]][license] [![godocb]][godoc] +[![licenseb]][license] [![versionb]][version] Package money implements immutable monetary amounts for Go. diff --git a/amount.go b/amount.go index 2f70383..509ffed 100644 --- a/amount.go +++ b/amount.go @@ -161,7 +161,7 @@ func (a Amount) Neg() Amount { } // CopySign returns the amount with the same sign as amount b. -// If amount b is zero, the sign of the result remains unchanged. +// Zero is always treated as positive. func (a Amount) CopySign(b Amount) Amount { d, e := a.value, b.value return newAmountUnsafe(a.Curr(), d.CopySign(e)) @@ -572,7 +572,7 @@ func (a Amount) SameScale(b Amount) bool { } // SameScaleAsCurr returns true if the scale of the amount matches the scale of its currency. -// See also method [Amount.RoundToCurr]. +// See also methods [Amount.Scale] and [Currency.Scale]. func (a Amount) SameScaleAsCurr() bool { return a.Scale() == a.Curr().Scale() } @@ -667,6 +667,8 @@ func (a Amount) Max(b Amount) (Amount, error) { // // [format verbs]: https://pkg.go.dev/fmt#hdr-Printing // [fmt.Formatter]: https://pkg.go.dev/fmt#Formatter +// +//gocyclo:ignore func (a Amount) Format(state fmt.State, verb rune) { // Rescaling tzeroes := 0 @@ -732,13 +734,13 @@ func (a Amount) Format(state fmt.State, verb rune) { } currlen := len(curr) - // Quotes + // Opening and closing quotes lquote, tquote := 0, 0 if verb == 'q' || verb == 'Q' { lquote, tquote = 1, 1 } - // Padding + // Calculating padding width := lquote + rsign + intdigs + dpoint + fracdigs + tzeroes + currlen + tquote lspaces, lzeroes, tspaces := 0, 0, 0 if w, ok := state.Width(); ok && w > width { @@ -753,40 +755,55 @@ func (a Amount) Format(state fmt.State, verb rune) { width = w } - // Writing buffer buf := make([]byte, width) pos := width - 1 + + // Trailing spaces for i := 0; i < tspaces; i++ { buf[pos] = ' ' pos-- } + + // Closing quote if tquote > 0 { buf[pos] = '"' pos-- } + + // Trailing zeroes for i := 0; i < tzeroes; i++ { buf[pos] = '0' pos-- } + + // Fractional digits coef := a.Coef() for i := 0; i < fracdigs; i++ { buf[pos] = byte(coef%10) + '0' pos-- coef /= 10 } + + // Decimal point if dpoint > 0 { buf[pos] = '.' pos-- } + + // Integer digits for i := 0; i < intdigs; i++ { buf[pos] = byte(coef%10) + '0' pos-- coef /= 10 } + + // Leading zeroes for i := 0; i < lzeroes; i++ { buf[pos] = '0' pos-- } + + // Arithmetic sign if rsign > 0 { if a.IsNeg() { buf[pos] = '-' @@ -797,14 +814,20 @@ func (a Amount) Format(state fmt.State, verb rune) { } pos-- } + + // Currency symbols for i := currlen; i > 0; i-- { buf[pos] = curr[i-1] pos-- } + + // Opening quote if lquote > 0 { buf[pos] = '"' pos-- } + + // Leading spaces for i := 0; i < lspaces; i++ { buf[pos] = ' ' pos-- diff --git a/amount_test.go b/amount_test.go index bf8b7c4..4d9fe60 100644 --- a/amount_test.go +++ b/amount_test.go @@ -169,8 +169,6 @@ func TestAmount_SameScaleAsCurr(t *testing.T) { c, a string want bool }{ - {"USD", "1", true}, - {"USD", "1.0", true}, {"USD", "1.00", true}, {"USD", "1.000", false}, {"USD", "1.0000", false}, diff --git a/currency.go b/currency.go index 1c28e7a..b602349 100644 --- a/currency.go +++ b/currency.go @@ -1,6 +1,7 @@ package money import ( + "database/sql/driver" "errors" "fmt" ) @@ -20,9 +21,7 @@ import ( // index and a particular currency may change in future versions. type Currency uint8 -var ( - errUnknownCurrency = errors.New("unknown currency") -) +var errUnknownCurrency = errors.New("unknown currency") // ParseCurr converts a string to currency. // The input string must be in one of the following formats: @@ -50,16 +49,6 @@ func MustParseCurr(curr string) Currency { return c } -// UnmarshalText implements [encoding.TextUnmarshaler] interface. -// Also see method [ParseCurr]. -// -// [encoding.TextUnmarshaler]: https://pkg.go.dev/encoding#TextUnmarshaler -func (c *Currency) UnmarshalText(text []byte) error { - var err error - *c, err = ParseCurr(string(text)) - return err -} - // Scale returns the number of digits after the decimal point required for // the minor unit of the currency. // This represents the [ratio] of the minor unit to the major unit. @@ -96,6 +85,16 @@ func (c Currency) String() string { return c.Code() } +// UnmarshalText implements [encoding.TextUnmarshaler] interface. +// Also see method [ParseCurr]. +// +// [encoding.TextUnmarshaler]: https://pkg.go.dev/encoding#TextUnmarshaler +func (c *Currency) UnmarshalText(text []byte) error { + var err error + *c, err = ParseCurr(string(text)) + return err +} + // MarshalText implements [encoding.TextMarshaler] interface. // Also see method [Currency.String]. // @@ -104,7 +103,30 @@ func (c Currency) MarshalText() ([]byte, error) { return []byte(c.String()), nil } -// Format implements [fmt.Formatter] interface. +// Scan implements the [sql.Scanner] interface. +// See also method [ParseCurr]. +// +// [sql.Scanner]: https://pkg.go.dev/database/sql#Scanner +func (c *Currency) Scan(v any) error { + var err error + switch v := v.(type) { + case string: + *c, err = ParseCurr(v) + default: + err = fmt.Errorf("failed to convert from %T to %T", v, XXX) + } + return err +} + +// Value implements the [driver.Valuer] interface. +// See also method [Currency.String]. +// +// [driver.Valuer]: https://pkg.go.dev/database/sql/driver#Valuer +func (c Currency) Value() (driver.Value, error) { + return c.String(), nil +} + +// Format implements the [fmt.Formatter] interface. // The following [verbs] are available: // // %s, %v: USD @@ -116,18 +138,17 @@ func (c Currency) MarshalText() ([]byte, error) { // [verbs]: https://pkg.go.dev/fmt#hdr-Printing // [fmt.Formatter]: https://pkg.go.dev/fmt#Formatter func (c Currency) Format(state fmt.State, verb rune) { - // Currency symbols curr := c.Code() currlen := len(curr) - // Quotes + // Opening and closing quotes lquote, tquote := 0, 0 if verb == 'q' || verb == 'Q' { lquote, tquote = 1, 1 } - // Padding + // Calculating padding width := lquote + currlen + tquote lspaces, tspaces := 0, 0 if w, ok := state.Width(); ok && w > width { @@ -140,25 +161,34 @@ func (c Currency) Format(state fmt.State, verb rune) { width = w } - // Writing buffer buf := make([]byte, width) pos := width - 1 + + // Trailing spaces for i := 0; i < tspaces; i++ { buf[pos] = ' ' pos-- } + + // Closing quote if tquote > 0 { buf[pos] = '"' pos-- } + + // Currency symbols for i := currlen; i > 0; i-- { buf[pos] = curr[i-1] pos-- } + + // Opening quote if lquote > 0 { buf[pos] = '"' pos-- } + + // Leading spaces for i := 0; i < lspaces; i++ { buf[pos] = ' ' pos-- diff --git a/currency_test.go b/currency_test.go index 16a5f63..3df98b9 100644 --- a/currency_test.go +++ b/currency_test.go @@ -6,8 +6,7 @@ import ( ) func TestCurrency_Parse(t *testing.T) { - - t.Run("valid", func(t *testing.T) { + t.Run("success", func(t *testing.T) { tests := []struct { code string want Currency @@ -37,7 +36,7 @@ func TestCurrency_Parse(t *testing.T) { } }) - t.Run("invalid", func(t *testing.T) { + t.Run("error", func(t *testing.T) { tests := []string{ "", "000", "test", "xbt", "$", "AU$", "BTC", } @@ -158,3 +157,13 @@ 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") + } + }) +} diff --git a/doc_test.go b/doc_test.go index 18c4055..d7d3420 100644 --- a/doc_test.go +++ b/doc_test.go @@ -89,7 +89,7 @@ func (s Statement) OutgoingBalance() (money.Amount, error) { return s[len(s)-1].Balance, nil } -// PercChange method calculates (OutgoingBalance - IncomingBalance) / IncomingBalance +// PercChange method calculates (OutgoingBalance - IncomingBalance) / IncomingBalance. func (s Statement) PercChange() (decimal.Decimal, error) { inc, err := s.IncomingBalance() if err != nil { @@ -296,7 +296,7 @@ func MonthlyRate(yearlyRate decimal.Decimal) (decimal.Decimal, error) { return yearlyRate.Quo(monthsInYear) } -// AnnuityPayment function calculates Amount * Rate / (1 - (1 + Rate)^(-Periods)) +// AnnuityPayment function calculates Amount * Rate / (1 - (1 + Rate)^(-Periods)). func AnnuityPayment(amount money.Amount, rate decimal.Decimal, periods int) (money.Amount, error) { one := rate.One() // Numerator @@ -638,7 +638,7 @@ func ExampleAmount_Quo() { // Output: USD -7.835 } -func ExampleDecimal_QuoRem() { +func ExampleAmount_QuoRem() { a := money.MustParseAmount("USD", "-15.67") e := decimal.MustParse("2") fmt.Println(a.QuoRem(e)) @@ -1081,6 +1081,17 @@ func ExampleCurrency_Scale() { // 3 } +func ExampleCurrency_UnmarshalText() { + c := money.XXX + b := []byte("USD") + err := c.UnmarshalText(b) + if err != nil { + panic(err) + } + fmt.Println(c) + // Output: USD +} + func ExampleCurrency_MarshalText() { c := money.MustParseCurr("USD") b, err := c.MarshalText() @@ -1091,10 +1102,9 @@ func ExampleCurrency_MarshalText() { // Output: USD } -func ExampleCurrency_UnmarshalText() { +func ExampleCurrency_Scan() { c := money.XXX - b := []byte("USD") - err := c.UnmarshalText(b) + err := c.Scan("USD") if err != nil { panic(err) } @@ -1102,6 +1112,16 @@ func ExampleCurrency_UnmarshalText() { // Output: USD } +func ExampleCurrency_Value() { + c := money.MustParseCurr("USD") + v, err := c.Value() + if err != nil { + panic(err) + } + fmt.Println(v) + // Output: USD +} + func ExampleCurrency_Format() { fmt.Printf("%c\n", money.USD) // Output: diff --git a/exchange_rate.go b/exchange_rate.go index 5a46d72..6b8aac8 100644 --- a/exchange_rate.go +++ b/exchange_rate.go @@ -237,7 +237,7 @@ func (r ExchangeRate) String() string { return r.Base().String() + "/" + r.Quote().String() + " " + r.value.String() } -// Format implements [fmt.Formatter] interface. +// Format implements the [fmt.Formatter] interface. // The following [format verbs] are available: // // %s, %v: USD/EUR 1.2345 @@ -253,6 +253,8 @@ func (r ExchangeRate) String() string { // // [format verbs]: https://pkg.go.dev/fmt#hdr-Printing // [fmt.Formatter]: https://pkg.go.dev/fmt#Formatter +// +//gocyclo:ignore func (r ExchangeRate) Format(state fmt.State, verb rune) { // Rescaling tzeroes := 0 @@ -274,7 +276,6 @@ func (r ExchangeRate) Format(state fmt.State, verb rune) { // Integer and fractional digits intdigs, fracdigs := 0, 0 - switch rprec := r.Prec(); verb { case 'c', 'C': // skip @@ -306,13 +307,13 @@ func (r ExchangeRate) Format(state fmt.State, verb rune) { } currlen := len(curr) - // Quotes + // Opening and closing quotes lquote, tquote := 0, 0 if verb == 'q' || verb == 'Q' { lquote, tquote = 1, 1 } - // Padding + // Calculating padding width := lquote + intdigs + dpoint + fracdigs + tzeroes + currlen + tquote lspaces, lzeroes, tspaces := 0, 0, 0 if w, ok := state.Width(); ok && w > width { @@ -327,48 +328,67 @@ func (r ExchangeRate) Format(state fmt.State, verb rune) { width = w } - // Writing buffer buf := make([]byte, width) pos := width - 1 + + // Trailing spaces for i := 0; i < tspaces; i++ { buf[pos] = ' ' pos-- } + + // Closing quote if tquote > 0 { buf[pos] = '"' pos-- } + + // Trailing zeroes for i := 0; i < tzeroes; i++ { buf[pos] = '0' pos-- } + + // Fractional digits coef := r.value.Coef() for i := 0; i < fracdigs; i++ { buf[pos] = byte(coef%10) + '0' pos-- coef /= 10 } + + // Decimal point if dpoint > 0 { buf[pos] = '.' pos-- } + + // Integer digits for i := 0; i < intdigs; i++ { buf[pos] = byte(coef%10) + '0' pos-- coef /= 10 } + + // Leading zeroes for i := 0; i < lzeroes; i++ { buf[pos] = '0' pos-- } + + // Currency symbols for i := currlen; i > 0; i-- { buf[pos] = curr[i-1] pos-- } + + // Opening quote if lquote > 0 { buf[pos] = '"' pos-- } + + // Leading spaces for i := 0; i < lspaces; i++ { buf[pos] = ' ' pos-- diff --git a/go.mod b/go.mod index b50965f..08bbef7 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/govalues/money -go 1.19 +go 1.20 -require github.com/govalues/decimal v0.1.5 +require github.com/govalues/decimal v0.1.7 diff --git a/go.sum b/go.sum index c433455..6c8d8b0 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -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= +github.com/govalues/decimal v0.1.7 h1:4lN8OR2cjvKxLAQ/EGA2Ah6q3+TZubljK02SgWNjMzA= +github.com/govalues/decimal v0.1.7/go.mod h1:irMp3+UfATz5dlLhUagswX2ATLhGDmo/Hoq2MP4/9gg=