diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1b48290..16f7c78 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.14', '1.15', '1.16', '1.17' ] + go: [ '1.18' ] name: Running Tests on Go ${{ matrix.go }} steps: - uses: actions/checkout@v2 diff --git a/calculator.go b/calculator.go index 996fc22..e361332 100644 --- a/calculator.go +++ b/calculator.go @@ -2,55 +2,55 @@ package money import "math" -type calculator struct{} +type calculator[T Numeric] struct{} -func (c *calculator) add(a, b *Amount) *Amount { - return &Amount{a.val + b.val} +func (c *calculator[T]) add(a, b *Amount[T]) *Amount[T] { + return &Amount[T]{a.val + b.val} } -func (c *calculator) subtract(a, b *Amount) *Amount { - return &Amount{a.val - b.val} +func (c *calculator[T]) subtract(a, b *Amount[T]) *Amount[T] { + return &Amount[T]{a.val - b.val} } -func (c *calculator) multiply(a *Amount, m int64) *Amount { - return &Amount{a.val * m} +func (c *calculator[T]) multiply(a *Amount[T], m T) *Amount[T] { + return &Amount[T]{a.val * m} } -func (c *calculator) divide(a *Amount, d int64) *Amount { - return &Amount{a.val / d} +func (c *calculator[T]) divide(a *Amount[T], d T) *Amount[T] { + return &Amount[T]{a.val / d} } -func (c *calculator) modulus(a *Amount, d int64) *Amount { - return &Amount{a.val % d} +func (c *calculator[T]) modulus(a *Amount[T], d T) *Amount[T] { + return &Amount[T]{a.val % d} } -func (c *calculator) allocate(a *Amount, r, s int) *Amount { - return &Amount{a.val * int64(r) / int64(s)} +func (c *calculator[T]) allocate(a *Amount[T], r, s T) *Amount[T] { + return &Amount[T]{a.val * r / s} } -func (c *calculator) absolute(a *Amount) *Amount { +func (c *calculator[T]) absolute(a *Amount[T]) *Amount[T] { if a.val < 0 { - return &Amount{-a.val} + return &Amount[T]{-a.val} } - return &Amount{a.val} + return &Amount[T]{a.val} } -func (c *calculator) negative(a *Amount) *Amount { +func (c *calculator[T]) negative(a *Amount[T]) *Amount[T] { if a.val > 0 { - return &Amount{-a.val} + return &Amount[T]{-a.val} } - return &Amount{a.val} + return &Amount[T]{a.val} } -func (c *calculator) round(a *Amount, e int) *Amount { +func (c *calculator[T]) round(a *Amount[T], e int) *Amount[T] { if a.val == 0 { - return &Amount{0} + return &Amount[T]{0} } absam := c.absolute(a) - exp := int64(math.Pow(10, float64(e))) + exp := T(math.Pow(10, float64(e))) m := absam.val % exp if m > (exp / 2) { @@ -65,5 +65,5 @@ func (c *calculator) round(a *Amount, e int) *Amount { a.val = absam.val } - return &Amount{a.val} + return &Amount[T]{a.val} } diff --git a/go.mod b/go.mod index 6d4223b..a6ced4d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/Rhymond/go-money -go 1.13 +go 1.18 diff --git a/money.go b/money.go index e0cbe12..d2b1739 100644 --- a/money.go +++ b/money.go @@ -12,11 +12,6 @@ import ( // money.UnmarshalJSON = func (m *Money, b []byte) error { ... } // money.MarshalJSON = func (m Money) ([]byte, error) { ... } var ( - // UnmarshalJSONFunc is injection point of json.Unmarshaller for money.Money - UnmarshalJSON = defaultUnmarshalJSON - // MarshalJSONFunc is injection point of json.Marshaller for money.Money - MarshalJSON = defaultMarshalJSON - // ErrCurrencyMismatch happens when two compared Money don't have the same currency. ErrCurrencyMismatch = errors.New("currencies don't match") @@ -24,7 +19,7 @@ var ( ErrInvalidJSONUnmarshal = errors.New("invalid json unmarshal") ) -func defaultUnmarshalJSON(m *Money, b []byte) error { +func defaultUnmarshalJSON[T Numeric](m *Money[T], b []byte) error { data := make(map[string]interface{}) err := json.Unmarshal(b, &data) if err != nil { @@ -47,20 +42,20 @@ func defaultUnmarshalJSON(m *Money, b []byte) error { } } - var ref *Money + var ref *Money[T] if amount == 0 && currency == "" { - ref = &Money{} + ref = &Money[T]{} } else { - ref = New(int64(amount), currency) + ref = New(T(amount), currency) } *m = *ref return nil } -func defaultMarshalJSON(m Money) ([]byte, error) { - if m == (Money{}) { - m = *New(0, "") +func defaultMarshalJSON[T Numeric](m Money[T]) ([]byte, error) { + if m == (Money[T]{}) { + m = *New(T(0), "") } buff := bytes.NewBufferString(fmt.Sprintf(`{"amount": %d, "currency": "%s"}`, m.Amount(), m.Currency().Code)) @@ -68,41 +63,46 @@ func defaultMarshalJSON(m Money) ([]byte, error) { } // Amount is a datastructure that stores the amount being used for calculations. -type Amount struct { - val int64 +type Amount[T Numeric] struct { + val T +} + +type Numeric interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 } // Money represents monetary value information, stores // currency and amount value. -type Money struct { - amount *Amount +type Money[T Numeric] struct { + calc calculator[T] + amount *Amount[T] currency *Currency } // New creates and returns new instance of Money. -func New(amount int64, code string) *Money { - return &Money{ - amount: &Amount{val: amount}, +func New[T Numeric](amount T, code string) *Money[T] { + return &Money[T]{ + amount: &Amount[T]{val: amount}, currency: newCurrency(code).get(), } } // Currency returns the currency used by Money. -func (m *Money) Currency() *Currency { +func (m *Money[T]) Currency() *Currency { return m.currency } // Amount returns a copy of the internal monetary value as an int64. -func (m *Money) Amount() int64 { +func (m *Money[T]) Amount() T { return m.amount.val } // SameCurrency check if given Money is equals by currency. -func (m *Money) SameCurrency(om *Money) bool { +func (m *Money[T]) SameCurrency(om *Money[T]) bool { return m.currency.equals(om.currency) } -func (m *Money) assertSameCurrency(om *Money) error { +func (m *Money[T]) assertSameCurrency(om *Money[T]) error { if !m.SameCurrency(om) { return ErrCurrencyMismatch } @@ -110,7 +110,7 @@ func (m *Money) assertSameCurrency(om *Money) error { return nil } -func (m *Money) compare(om *Money) int { +func (m *Money[T]) compare(om *Money[T]) int { switch { case m.amount.val > om.amount.val: return 1 @@ -122,7 +122,7 @@ func (m *Money) compare(om *Money) int { } // Equals checks equality between two Money types. -func (m *Money) Equals(om *Money) (bool, error) { +func (m *Money[T]) Equals(om *Money[T]) (bool, error) { if err := m.assertSameCurrency(om); err != nil { return false, err } @@ -131,7 +131,7 @@ func (m *Money) Equals(om *Money) (bool, error) { } // GreaterThan checks whether the value of Money is greater than the other. -func (m *Money) GreaterThan(om *Money) (bool, error) { +func (m *Money[T]) GreaterThan(om *Money[T]) (bool, error) { if err := m.assertSameCurrency(om); err != nil { return false, err } @@ -140,7 +140,7 @@ func (m *Money) GreaterThan(om *Money) (bool, error) { } // GreaterThanOrEqual checks whether the value of Money is greater or equal than the other. -func (m *Money) GreaterThanOrEqual(om *Money) (bool, error) { +func (m *Money[T]) GreaterThanOrEqual(om *Money[T]) (bool, error) { if err := m.assertSameCurrency(om); err != nil { return false, err } @@ -149,7 +149,7 @@ func (m *Money) GreaterThanOrEqual(om *Money) (bool, error) { } // LessThan checks whether the value of Money is less than the other. -func (m *Money) LessThan(om *Money) (bool, error) { +func (m *Money[T]) LessThan(om *Money[T]) (bool, error) { if err := m.assertSameCurrency(om); err != nil { return false, err } @@ -158,7 +158,7 @@ func (m *Money) LessThan(om *Money) (bool, error) { } // LessThanOrEqual checks whether the value of Money is less or equal than the other. -func (m *Money) LessThanOrEqual(om *Money) (bool, error) { +func (m *Money[T]) LessThanOrEqual(om *Money[T]) (bool, error) { if err := m.assertSameCurrency(om); err != nil { return false, err } @@ -167,82 +167,85 @@ func (m *Money) LessThanOrEqual(om *Money) (bool, error) { } // IsZero returns boolean of whether the value of Money is equals to zero. -func (m *Money) IsZero() bool { +func (m *Money[T]) IsZero() bool { return m.amount.val == 0 } // IsPositive returns boolean of whether the value of Money is positive. -func (m *Money) IsPositive() bool { +func (m *Money[T]) IsPositive() bool { return m.amount.val > 0 } // IsNegative returns boolean of whether the value of Money is negative. -func (m *Money) IsNegative() bool { +func (m *Money[T]) IsNegative() bool { return m.amount.val < 0 } // Absolute returns new Money struct from given Money using absolute monetary value. -func (m *Money) Absolute() *Money { - return &Money{amount: mutate.calc.absolute(m.amount), currency: m.currency} +func (m *Money[T]) Absolute() *Money[T] { + return &Money[T]{amount: m.calc.absolute(m.amount), currency: m.currency} } // Negative returns new Money struct from given Money using negative monetary value. -func (m *Money) Negative() *Money { - return &Money{amount: mutate.calc.negative(m.amount), currency: m.currency} +func (m *Money[T]) Negative() *Money[T] { + return &Money[T]{amount: m.calc.negative(m.amount), currency: m.currency} } // Add returns new Money struct with value representing sum of Self and Other Money. -func (m *Money) Add(om *Money) (*Money, error) { +func (m *Money[T]) Add(om *Money[T]) (*Money[T], error) { if err := m.assertSameCurrency(om); err != nil { return nil, err } - return &Money{amount: mutate.calc.add(m.amount, om.amount), currency: m.currency}, nil + return &Money[T]{amount: m.calc.add(m.amount, om.amount), currency: m.currency}, nil } // Subtract returns new Money struct with value representing difference of Self and Other Money. -func (m *Money) Subtract(om *Money) (*Money, error) { +func (m *Money[T]) Subtract(om *Money[T]) (*Money[T], error) { if err := m.assertSameCurrency(om); err != nil { return nil, err } - return &Money{amount: mutate.calc.subtract(m.amount, om.amount), currency: m.currency}, nil + return &Money[T]{amount: m.calc.subtract(m.amount, om.amount), currency: m.currency}, nil } // Multiply returns new Money struct with value representing Self multiplied value by multiplier. -func (m *Money) Multiply(mul int64) *Money { - return &Money{amount: mutate.calc.multiply(m.amount, mul), currency: m.currency} +func (m *Money[T]) Multiply(mul T) *Money[T] { + return &Money[T]{amount: m.calc.multiply(m.amount, mul), currency: m.currency} } // Round returns new Money struct with value rounded to nearest zero. -func (m *Money) Round() *Money { - return &Money{amount: mutate.calc.round(m.amount, m.currency.Fraction), currency: m.currency} +func (m *Money[T]) Round() *Money[T] { + return &Money[T]{amount: m.calc.round(m.amount, m.currency.Fraction), currency: m.currency} } // Split returns slice of Money structs with split Self value in given number. // After division leftover pennies will be distributed round-robin amongst the parties. // This means that parties listed first will likely receive more pennies than ones that are listed later. -func (m *Money) Split(n int) ([]*Money, error) { +func (m *Money[T]) Split(n T) ([]*Money[T], error) { if n <= 0 { return nil, errors.New("split must be higher than zero") } - a := mutate.calc.divide(m.amount, int64(n)) - ms := make([]*Money, n) + a := m.calc.divide(m.amount, n) + ms := make([]*Money[T], n) - for i := 0; i < n; i++ { - ms[i] = &Money{amount: a, currency: m.currency} + var i T + for i = 0; i < n; i++ { + ms[i] = &Money[T]{amount: a, currency: m.currency} } - r := mutate.calc.modulus(m.amount, int64(n)) - l := mutate.calc.absolute(r).val + r := m.calc.modulus(m.amount, n) + l := m.calc.absolute(r).val // Add leftovers to the first parties. - for p := 0; l != 0; p++ { - v := int64(1) - if a.val < 0 { - v = -1 - } - ms[p].amount = mutate.calc.add(ms[p].amount, &Amount{v}) + var p T + v := T(1) + if a.val < 0 { + v = -1 + } + + for p = 0; l != T(0); p++ { + ms[p].amount = m.calc.add(ms[p].amount, &Amount[T]{v}) l-- } @@ -252,22 +255,22 @@ func (m *Money) Split(n int) ([]*Money, error) { // Allocate returns slice of Money structs with split Self value in given ratios. // It lets split money by given ratios without losing pennies and as Split operations distributes // leftover pennies amongst the parties with round-robin principle. -func (m *Money) Allocate(rs ...int) ([]*Money, error) { +func (m *Money[T]) Allocate(rs ...T) ([]*Money[T], error) { if len(rs) == 0 { return nil, errors.New("no ratios specified") } // Calculate sum of ratios. - var sum int + var sum T for _, r := range rs { sum += r } - var total int64 - ms := make([]*Money, 0, len(rs)) + var total T + ms := make([]*Money[T], 0, len(rs)) for _, r := range rs { - party := &Money{ - amount: mutate.calc.allocate(m.amount, r, sum), + party := &Money[T]{ + amount: m.calc.allocate(m.amount, r, sum), currency: m.currency, } @@ -277,13 +280,13 @@ func (m *Money) Allocate(rs ...int) ([]*Money, error) { // Calculate leftover value and divide to first parties. lo := m.amount.val - total - sub := int64(1) + sub := T(1) if lo < 0 { sub = -sub } for p := 0; lo != 0; p++ { - ms[p].amount = mutate.calc.add(ms[p].amount, &Amount{sub}) + ms[p].amount = m.calc.add(ms[p].amount, &Amount[T]{sub}) lo -= sub } @@ -291,23 +294,23 @@ func (m *Money) Allocate(rs ...int) ([]*Money, error) { } // Display lets represent Money struct as string in given Currency value. -func (m *Money) Display() string { +func (m *Money[T]) Display() string { c := m.currency.get() - return c.Formatter().Format(m.amount.val) + return c.Formatter().Format(int64(m.amount.val)) } // AsMajorUnits lets represent Money struct as subunits (float64) in given Currency value -func (m *Money) AsMajorUnits() float64 { +func (m *Money[T]) AsMajorUnits() float64 { c := m.currency.get() - return c.Formatter().ToMajorUnits(m.amount.val) + return c.Formatter().ToMajorUnits(int64(m.amount.val)) } // UnmarshalJSON is implementation of json.Unmarshaller -func (m *Money) UnmarshalJSON(b []byte) error { - return UnmarshalJSON(m, b) +func (m *Money[T]) UnmarshalJSON(b []byte) error { + return defaultUnmarshalJSON(m, b) } // MarshalJSON is implementation of json.Marshaller -func (m Money) MarshalJSON() ([]byte, error) { - return MarshalJSON(m) +func (m Money[T]) MarshalJSON() ([]byte, error) { + return defaultMarshalJSON(m) } diff --git a/money_test.go b/money_test.go index a13e4fd..39f87f3 100644 --- a/money_test.go +++ b/money_test.go @@ -1,10 +1,8 @@ package money import ( - "bytes" "encoding/json" "errors" - "fmt" "reflect" "testing" ) @@ -60,7 +58,7 @@ func TestMoney_SameCurrency(t *testing.T) { func TestMoney_Equals(t *testing.T) { m := New(0, EUR) tcs := []struct { - amount int64 + amount int expected bool }{ {-1, false}, @@ -94,7 +92,7 @@ func TestMoney_Equals_DifferentCurrencies(t *testing.T) { func TestMoney_GreaterThan(t *testing.T) { m := New(0, EUR) tcs := []struct { - amount int64 + amount int expected bool }{ {-1, true}, @@ -116,7 +114,7 @@ func TestMoney_GreaterThan(t *testing.T) { func TestMoney_GreaterThanOrEqual(t *testing.T) { m := New(0, EUR) tcs := []struct { - amount int64 + amount int expected bool }{ {-1, true}, @@ -138,7 +136,7 @@ func TestMoney_GreaterThanOrEqual(t *testing.T) { func TestMoney_LessThan(t *testing.T) { m := New(0, EUR) tcs := []struct { - amount int64 + amount int expected bool }{ {-1, false}, @@ -160,7 +158,7 @@ func TestMoney_LessThan(t *testing.T) { func TestMoney_LessThanOrEqual(t *testing.T) { m := New(0, EUR) tcs := []struct { - amount int64 + amount int expected bool }{ {-1, false}, @@ -424,19 +422,21 @@ func TestMoney_RoundWithExponential(t *testing.T) { func TestMoney_Split(t *testing.T) { tcs := []struct { - amount int64 + amount int split int - expected []int64 + expected []int }{ - {100, 3, []int64{34, 33, 33}}, - {100, 4, []int64{25, 25, 25, 25}}, - {5, 3, []int64{2, 2, 1}}, - {-101, 4, []int64{-26, -25, -25, -25}}, + {100, 3, []int{34, 33, 33}}, + {100, 4, []int{25, 25, 25, 25}}, + {5, 3, []int{2, 2, 1}}, + {-101, 4, []int{-26, -25, -25, -25}}, + {-99, 4, []int{-25, -25, -25, -24}}, + {1, 3, []int{1, 0, 0}}, } for _, tc := range tcs { m := New(tc.amount, EUR) - var rs []int64 + var rs []int split, _ := m.Split(tc.split) for _, party := range split { @@ -460,19 +460,19 @@ func TestMoney_Split2(t *testing.T) { func TestMoney_Allocate(t *testing.T) { tcs := []struct { - amount int64 + amount int ratios []int - expected []int64 + expected []int }{ - {100, []int{50, 50}, []int64{50, 50}}, - {100, []int{30, 30, 30}, []int64{34, 33, 33}}, - {200, []int{25, 25, 50}, []int64{50, 50, 100}}, - {5, []int{50, 25, 25}, []int64{3, 1, 1}}, + {100, []int{50, 50}, []int{50, 50}}, + {100, []int{30, 30, 30}, []int{34, 33, 33}}, + {200, []int{25, 25, 50}, []int{50, 50, 100}}, + {5, []int{50, 25, 25}, []int{3, 1, 1}}, } for _, tc := range tcs { m := New(tc.amount, EUR) - var rs []int64 + var rs []int split, _ := m.Allocate(tc.ratios...) for _, party := range split { @@ -641,7 +641,7 @@ func TestDefaultMarshal(t *testing.T) { t.Errorf("Expected %s got %s", expected, string(b)) } - given = &Money{} + given = &Money[int]{} expected = `{"amount":0,"currency":""}` b, err = json.Marshal(given) @@ -655,29 +655,10 @@ func TestDefaultMarshal(t *testing.T) { } } -func TestCustomMarshal(t *testing.T) { - given := New(12345, IQD) - expected := `{"amount":12345,"currency_code":"IQD","currency_fraction":3}` - MarshalJSON = func(m Money) ([]byte, error) { - buff := bytes.NewBufferString(fmt.Sprintf(`{"amount": %d, "currency_code": "%s", "currency_fraction": %d}`, m.Amount(), m.Currency().Code, m.Currency().Fraction)) - return buff.Bytes(), nil - } - - b, err := json.Marshal(given) - - if err != nil { - t.Error(err) - } - - if string(b) != expected { - t.Errorf("Expected %s got %s", expected, string(b)) - } -} - func TestDefaultUnmarshal(t *testing.T) { given := `{"amount": 10012, "currency":"USD"}` expected := "$100.12" - var m Money + var m Money[int] err := json.Unmarshal([]byte(given), &m) if err != nil { t.Error(err) @@ -693,7 +674,7 @@ func TestDefaultUnmarshal(t *testing.T) { t.Error(err) } - if m != (Money{}) { + if m != (Money[int]{}) { t.Errorf("Expected zero value, got %+v", m) } @@ -703,7 +684,7 @@ func TestDefaultUnmarshal(t *testing.T) { t.Error(err) } - if m != (Money{}) { + if m != (Money[int]{}) { t.Errorf("Expected zero value, got %+v", m) } @@ -720,27 +701,27 @@ func TestDefaultUnmarshal(t *testing.T) { } } -func TestCustomUnmarshal(t *testing.T) { - given := `{"amount": 10012, "currency_code":"USD", "currency_fraction":2}` - expected := "$100.12" - UnmarshalJSON = func(m *Money, b []byte) error { - data := make(map[string]interface{}) - err := json.Unmarshal(b, &data) - if err != nil { - return err - } - ref := New(int64(data["amount"].(float64)), data["currency_code"].(string)) - *m = *ref - return nil - } - - var m Money - err := json.Unmarshal([]byte(given), &m) - if err != nil { - t.Error(err) - } - - if m.Display() != expected { - t.Errorf("Expected %s got %s", expected, m.Display()) - } -} +// func TestCustomUnmarshal(t *testing.T) { +// given := `{"amount": 10012, "currency_code":"USD", "currency_fraction":2}` +// expected := "$100.12" +// UnmarshalJSON = func(m *Money, b []byte) error { +// data := make(map[string]interface{}) +// err := json.Unmarshal(b, &data) +// if err != nil { +// return err +// } +// ref := New(int64(data["amount"].(float64)), data["currency_code"].(string)) +// *m = *ref +// return nil +// } +// +// var m Money +// err := json.Unmarshal([]byte(given), &m) +// if err != nil { +// t.Error(err) +// } +// +// if m.Display() != expected { +// t.Errorf("Expected %s got %s", expected, m.Display()) +// } +// } diff --git a/mutator.go b/mutator.go deleted file mode 100644 index 4989ca6..0000000 --- a/mutator.go +++ /dev/null @@ -1,8 +0,0 @@ -package money - -type mutator struct { - calc *calculator -} - -// initialize our default mutator here. -var mutate = mutator{calc: &calculator{}}