Skip to content

Commit

Permalink
Allow converting an amount to/from int64 and big.Int.
Browse files Browse the repository at this point in the history
These types are often seen in financial and banking APIs.

Added:
- NewAmountFromBigInt() and NewAmountFromInt64()
- Amount.BigInt() and Amount.Int64()

Removed:
- Amount.ToMinorUnits() -> use Amount.Int64() instead.
  • Loading branch information
Kunde21 authored and bojanz committed May 17, 2021
1 parent 0b1a240 commit e18a432
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 20 deletions.
43 changes: 37 additions & 6 deletions amount.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"database/sql/driver"
"encoding/json"
"fmt"
"math/big"
"strings"

"github.com/cockroachdb/apd/v2"
Expand Down Expand Up @@ -77,6 +78,31 @@ func NewAmount(n, currencyCode string) (Amount, error) {
return Amount{number, currencyCode}, nil
}

// NewAmountFromBigInt creates a new Amount from a big.Int and a currency code.
func NewAmountFromBigInt(n *big.Int, currencyCode string) (Amount, error) {
if n == nil {
return Amount{}, InvalidNumberError{"NewAmountFromBigInt", "nil"}
}
d, ok := GetDigits(currencyCode)
if !ok {
return Amount{}, InvalidCurrencyCodeError{"NewAmountFromBigInt", currencyCode}
}
number := apd.NewWithBigInt(n, -int32(d))

return Amount{number, currencyCode}, nil
}

// NewAmountFromInt64 creates a new Amount from an int64 and a currency code.
func NewAmountFromInt64(n int64, currencyCode string) (Amount, error) {
d, ok := GetDigits(currencyCode)
if !ok {
return Amount{}, InvalidCurrencyCodeError{"NewAmountFromInt64", currencyCode}
}
number := apd.New(n, -int32(d))

return Amount{number, currencyCode}, nil
}

// Number returns the number as a numeric string.
func (a Amount) Number() string {
if a.number == nil {
Expand All @@ -95,12 +121,17 @@ func (a Amount) String() string {
return a.Number() + " " + a.CurrencyCode()
}

// ToMinorUnits returns a in minor units.
func (a Amount) ToMinorUnits() int64 {
if a.number == nil {
return 0
}
return a.Round().number.Coeff.Int64()
// BigInt returns a in minor units, as a big.Int.
func (a Amount) BigInt() *big.Int {
return &a.Round().number.Coeff
}

// Int64 returns a in minor units, as an int64.
// If a cannot be represented in an int64, an error is returned.
func (a Amount) Int64() (int64, error) {
n := *a.Round().number
n.Exponent = 0
return n.Int64()
}

// Convert converts a to a different currency.
Expand Down
163 changes: 155 additions & 8 deletions amount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package currency_test

import (
"encoding/json"
"math/big"
"testing"

"github.com/bojanz/currency"
Expand Down Expand Up @@ -58,22 +59,168 @@ func TestNewAmount(t *testing.T) {
}
}

func TestAmount_ToMinorUnits(t *testing.T) {
func TestNewAmountFromBigInt(t *testing.T) {
_, err := currency.NewAmountFromBigInt(nil, "USD")
if e, ok := err.(currency.InvalidNumberError); ok {
if e.Op != "NewAmountFromBigInt" {
t.Errorf("got %v, want NewAmountFromBigInt", e.Op)
}
if e.Number != "nil" {
t.Errorf("got %v, want nil", e.Number)
}
wantError := `currency/NewAmountFromBigInt: invalid number "nil"`
if e.Error() != wantError {
t.Errorf("got %v, want %v", e.Error(), wantError)
}
} else {
t.Errorf("got %T, want currency.InvalidNumberError", err)
}

_, err = currency.NewAmountFromBigInt(big.NewInt(1099), "usd")
if e, ok := err.(currency.InvalidCurrencyCodeError); ok {
if e.Op != "NewAmountFromBigInt" {
t.Errorf("got %v, want NewAmountFromBigInt", e.Op)
}
if e.CurrencyCode != "usd" {
t.Errorf("got %v, want usd", e.CurrencyCode)
}
wantError := `currency/NewAmountFromBigInt: invalid currency code "usd"`
if e.Error() != wantError {
t.Errorf("got %v, want %v", e.Error(), wantError)
}
} else {
t.Errorf("got %T, want currency.InvalidCurrencyCodeError", err)
}

// An integer larger than math.MaxInt64.
hugeInt, _ := big.NewInt(0).SetString("922337203685477598799", 10)
tests := []struct {
number string
want int64
n *big.Int
currencyCode string
wantNumber string
}{
{"20.99", 2099},
{big.NewInt(2099), "USD", "20.99"},
{big.NewInt(5000), "USD", "50.00"},
{big.NewInt(50), "JPY", "50"},
{hugeInt, "USD", "9223372036854775987.99"},
}

for _, tt := range tests {
t.Run("", func(t *testing.T) {
a, err := currency.NewAmountFromBigInt(tt.n, tt.currencyCode)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if a.Number() != tt.wantNumber {
t.Errorf("got %v, want %v", a.Number(), tt.wantNumber)
}
if a.CurrencyCode() != tt.currencyCode {
t.Errorf("got %v, want %v", a.CurrencyCode(), tt.currencyCode)
}
})
}
}

func TestNewAmountFromInt64(t *testing.T) {
_, err := currency.NewAmountFromInt64(1099, "usd")
if e, ok := err.(currency.InvalidCurrencyCodeError); ok {
if e.Op != "NewAmountFromInt64" {
t.Errorf("got %v, want NewAmountFromInt64", e.Op)
}
if e.CurrencyCode != "usd" {
t.Errorf("got %v, want usd", e.CurrencyCode)
}
wantError := `currency/NewAmountFromInt64: invalid currency code "usd"`
if e.Error() != wantError {
t.Errorf("got %v, want %v", e.Error(), wantError)
}
} else {
t.Errorf("got %T, want currency.InvalidCurrencyCodeError", err)
}

tests := []struct {
n int64
currencyCode string
wantNumber string
}{
{2099, "USD", "20.99"},
{5000, "USD", "50.00"},
{50, "JPY", "50"},
}

for _, tt := range tests {
t.Run("", func(t *testing.T) {
a, err := currency.NewAmountFromInt64(tt.n, tt.currencyCode)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if a.Number() != tt.wantNumber {
t.Errorf("got %v, want %v", a.Number(), tt.wantNumber)
}
if a.CurrencyCode() != tt.currencyCode {
t.Errorf("got %v, want %v", a.CurrencyCode(), tt.currencyCode)
}
})
}
}

func TestAmount_BigInt(t *testing.T) {
tests := []struct {
number string
currencyCode string
want *big.Int
}{
{"20.99", "USD", big.NewInt(2099)},
// Number with additional decimals.
{"12.3564", 1236},
{"12.3564", "USD", big.NewInt(1236)},
// Number with no decimals.
{"50", 5000},
{"50", "USD", big.NewInt(5000)},
{"50", "JPY", big.NewInt(50)},
}

for _, tt := range tests {
t.Run("", func(t *testing.T) {
a, _ := currency.NewAmount(tt.number, "USD")
got := a.ToMinorUnits()
a, _ := currency.NewAmount(tt.number, tt.currencyCode)
got := a.BigInt()
if got.Cmp(tt.want) != 0 {
t.Errorf("got %v, want %v", got, tt.want)
}
// Confirm that a is unchanged.
if a.Number() != tt.number {
t.Errorf("got %v, want %v", a.Number(), tt.number)
}
})
}
}

func TestAmount_Int64(t *testing.T) {
// Number that can't be represented as an int64.
a, _ := currency.NewAmount("922337203685477598799", "USD")
n, err := a.Int64()
if n != 0 {
t.Error("expected a.Int64() to be 0")
}
if err == nil {
t.Error("expected a.Int64() to return an error")
}

tests := []struct {
number string
currencyCode string
want int64
}{
{"20.99", "USD", 2099},
// Number with additional decimals.
{"12.3564", "USD", 1236},
// Number with no decimals.
{"50", "USD", 5000},
{"50", "JPY", 50},
}

for _, tt := range tests {
t.Run("", func(t *testing.T) {
a, _ := currency.NewAmount(tt.number, tt.currencyCode)
got, _ := a.Int64()
if got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
Expand Down
26 changes: 20 additions & 6 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,27 @@ func ExampleNewAmount() {
// USD
}

func ExampleAmount_ToMinorUnits() {
firstAmount, _ := currency.NewAmount("20.99", "USD")
func ExampleNewAmountFromInt64() {
firstAmount, _ := currency.NewAmountFromInt64(2449, "USD")
secondAmount, _ := currency.NewAmountFromInt64(5000, "USD")
thirdAmount, _ := currency.NewAmountFromInt64(60, "JPY")
fmt.Println(firstAmount)
fmt.Println(secondAmount)
fmt.Println(thirdAmount)
// Output: 24.49 USD
// 50.00 USD
// 60 JPY
}

func ExampleAmount_Int64() {
firstAmount, _ := currency.NewAmount("24.49", "USD")
secondAmount, _ := currency.NewAmount("50", "USD")
fmt.Println(firstAmount.ToMinorUnits())
fmt.Println(secondAmount.ToMinorUnits())
// Output: 2099
// 5000
thirdAmount, _ := currency.NewAmount("60", "JPY")
firstInt, _ := firstAmount.Int64()
secondInt, _ := secondAmount.Int64()
thirdInt, _ := thirdAmount.Int64()
fmt.Println(firstInt, secondInt, thirdInt)
// Output: 2449 5000 60
}

func ExampleAmount_Convert() {
Expand Down

0 comments on commit e18a432

Please sign in to comment.