From 6e3d519c8f3858f4c8e7a4b2ec0fc880abca6b9b Mon Sep 17 00:00:00 2001 From: Raymond Date: Thu, 31 Mar 2022 12:47:14 +0300 Subject: [PATCH 1/5] playground with generics, almost all tests passed, there are some issues with JSON marshalling --- calculator.go | 46 ++++++++-------- go.mod | 2 +- money.go | 149 ++++++++++++++++++++++++++------------------------ money_test.go | 61 ++++++++++++--------- mutator.go | 8 --- 5 files changed, 136 insertions(+), 130 deletions(-) delete mode 100644 mutator.go 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..b1a6881 100644 --- a/money.go +++ b/money.go @@ -13,9 +13,9 @@ import ( // money.MarshalJSON = func (m Money) ([]byte, error) { ... } var ( // UnmarshalJSONFunc is injection point of json.Unmarshaller for money.Money - UnmarshalJSON = defaultUnmarshalJSON + UnmarshalJSON = defaultUnmarshalJSON[int64] // MarshalJSONFunc is injection point of json.Marshaller for money.Money - MarshalJSON = defaultMarshalJSON + MarshalJSON = defaultMarshalJSON[int64] // ErrCurrencyMismatch happens when two compared Money don't have the same currency. ErrCurrencyMismatch = errors.New("currencies don't match") @@ -24,7 +24,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 +47,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](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 +68,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 | ~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 +115,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 +127,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 +136,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 +145,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 +154,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 +163,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 +172,84 @@ 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) + var p T + for p = 0; l != T(0); p++ { + v := T(1) if a.val < 0 { v = -1 } - ms[p].amount = mutate.calc.add(ms[p].amount, &Amount{v}) + ms[p].amount = m.calc.add(ms[p].amount, &Amount[T]{v}) l-- } @@ -252,22 +259,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 +284,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 +298,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) -} - -// MarshalJSON is implementation of json.Marshaller -func (m Money) MarshalJSON() ([]byte, error) { - return MarshalJSON(m) -} +// 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) +// } diff --git a/money_test.go b/money_test.go index a13e4fd..0f08e71 100644 --- a/money_test.go +++ b/money_test.go @@ -9,8 +9,11 @@ import ( "testing" ) +// tn defines test numeric type, to ensure same type for all tests. +type tn int + func TestNew(t *testing.T) { - m := New(1, EUR) + m := New[tn](1, EUR) if m.amount.val != 1 { t.Errorf("Expected %d got %d", 1, m.amount.val) @@ -20,7 +23,7 @@ func TestNew(t *testing.T) { t.Errorf("Expected currency %s got %s", EUR, m.currency.Code) } - m = New(-100, EUR) + m = New[tn](-100, EUR) if m.amount.val != -100 { t.Errorf("Expected %d got %d", -100, m.amount.val) @@ -58,9 +61,9 @@ func TestMoney_SameCurrency(t *testing.T) { } func TestMoney_Equals(t *testing.T) { - m := New(0, EUR) + m := New[tn](0, EUR) tcs := []struct { - amount int64 + amount tn expected bool }{ {-1, false}, @@ -92,9 +95,9 @@ func TestMoney_Equals_DifferentCurrencies(t *testing.T) { } func TestMoney_GreaterThan(t *testing.T) { - m := New(0, EUR) + m := New[int](0, EUR) tcs := []struct { - amount int64 + amount int expected bool }{ {-1, true}, @@ -103,7 +106,7 @@ func TestMoney_GreaterThan(t *testing.T) { } for _, tc := range tcs { - om := New(tc.amount, EUR) + om := New[int](tc.amount, EUR) r, err := m.GreaterThan(om) if err != nil || r != tc.expected { @@ -114,9 +117,9 @@ func TestMoney_GreaterThan(t *testing.T) { } func TestMoney_GreaterThanOrEqual(t *testing.T) { - m := New(0, EUR) + m := New[int](0, EUR) tcs := []struct { - amount int64 + amount int expected bool }{ {-1, true}, @@ -125,7 +128,7 @@ func TestMoney_GreaterThanOrEqual(t *testing.T) { } for _, tc := range tcs { - om := New(tc.amount, EUR) + om := New[int](tc.amount, EUR) r, err := m.GreaterThanOrEqual(om) if err != nil || r != tc.expected { @@ -136,9 +139,9 @@ func TestMoney_GreaterThanOrEqual(t *testing.T) { } func TestMoney_LessThan(t *testing.T) { - m := New(0, EUR) + m := New[tn](0, EUR) tcs := []struct { - amount int64 + amount tn expected bool }{ {-1, false}, @@ -147,7 +150,7 @@ func TestMoney_LessThan(t *testing.T) { } for _, tc := range tcs { - om := New(tc.amount, EUR) + om := New[tn](tc.amount, EUR) r, err := m.LessThan(om) if err != nil || r != tc.expected { @@ -158,7 +161,7 @@ func TestMoney_LessThan(t *testing.T) { } func TestMoney_LessThanOrEqual(t *testing.T) { - m := New(0, EUR) + m := New[int64](0, EUR) tcs := []struct { amount int64 expected bool @@ -425,7 +428,7 @@ func TestMoney_RoundWithExponential(t *testing.T) { func TestMoney_Split(t *testing.T) { tcs := []struct { amount int64 - split int + split int64 expected []int64 }{ {100, 3, []int64{34, 33, 33}}, @@ -461,13 +464,13 @@ func TestMoney_Split2(t *testing.T) { func TestMoney_Allocate(t *testing.T) { tcs := []struct { amount int64 - ratios []int + ratios []int64 expected []int64 }{ - {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, []int64{50, 50}, []int64{50, 50}}, + {100, []int64{30, 30, 30}, []int64{34, 33, 33}}, + {200, []int64{25, 25, 50}, []int64{50, 50, 100}}, + {5, []int64{50, 25, 25}, []int64{3, 1, 1}}, } for _, tc := range tcs { @@ -628,6 +631,7 @@ func TestMoney_Amount(t *testing.T) { } func TestDefaultMarshal(t *testing.T) { + t.Skip("panic memory nil") given := New(12345, IQD) expected := `{"amount":12345,"currency":"IQD"}` @@ -641,7 +645,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) @@ -656,9 +660,10 @@ func TestDefaultMarshal(t *testing.T) { } func TestCustomMarshal(t *testing.T) { + t.Skip("panic memory nil") given := New(12345, IQD) expected := `{"amount":12345,"currency_code":"IQD","currency_fraction":3}` - MarshalJSON = func(m Money) ([]byte, error) { + MarshalJSON = func(m Money[int64]) ([]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 } @@ -675,9 +680,10 @@ func TestCustomMarshal(t *testing.T) { } func TestDefaultUnmarshal(t *testing.T) { + t.Skip("panic memory nil") 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 +699,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 +709,7 @@ func TestDefaultUnmarshal(t *testing.T) { t.Error(err) } - if m != (Money{}) { + if m != (Money[int]{}) { t.Errorf("Expected zero value, got %+v", m) } @@ -721,9 +727,10 @@ func TestDefaultUnmarshal(t *testing.T) { } func TestCustomUnmarshal(t *testing.T) { + t.Skip("panic memory nil") given := `{"amount": 10012, "currency_code":"USD", "currency_fraction":2}` expected := "$100.12" - UnmarshalJSON = func(m *Money, b []byte) error { + UnmarshalJSON = func(m *Money[int64], b []byte) error { data := make(map[string]interface{}) err := json.Unmarshal(b, &data) if err != nil { @@ -734,7 +741,7 @@ func TestCustomUnmarshal(t *testing.T) { return nil } - var m Money + var m Money[int64] err := json.Unmarshal([]byte(given), &m) if err != nil { t.Error(err) 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{}} From d67445c3abe462c917fb004e96c9b080478ce2f7 Mon Sep 17 00:00:00 2001 From: Raymond Date: Thu, 31 Mar 2022 13:03:01 +0300 Subject: [PATCH 2/5] update git workflow to only tests against go version 1.18 --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 8f2d9a7353ef3555c37e827b48e6b6597b72a4e9 Mon Sep 17 00:00:00 2001 From: Raymond Date: Sun, 3 Apr 2022 20:48:25 +0300 Subject: [PATCH 3/5] fix money_test test cases --- money.go | 24 ++++---- money_test.go | 151 ++++++++++++++++++++++++-------------------------- 2 files changed, 83 insertions(+), 92 deletions(-) diff --git a/money.go b/money.go index b1a6881..7d47e3e 100644 --- a/money.go +++ b/money.go @@ -12,10 +12,10 @@ 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[int64] - // MarshalJSONFunc is injection point of json.Marshaller for money.Money - MarshalJSON = defaultMarshalJSON[int64] + // UnmarshalJSON is injection point of json.Unmarshaller for money.Money + // UnmarshalJSON = defaultUnmarshalJSON[int64] + // MarshalJSON is injection point of json.Marshaller for money.Money + // MarshalJSON = defaultMarshalJSON[int64] // ErrCurrencyMismatch happens when two compared Money don't have the same currency. ErrCurrencyMismatch = errors.New("currencies don't match") @@ -310,11 +310,11 @@ func (m *Money[T]) AsMajorUnits() float64 { } // UnmarshalJSON is implementation of json.Unmarshaller -// 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]) UnmarshalJSON(b []byte) error { + return defaultUnmarshalJSON(m, b) +} + +// MarshalJSON is implementation of json.Marshaller +func (m Money[T]) MarshalJSON() ([]byte, error) { + return defaultMarshalJSON(m) +} diff --git a/money_test.go b/money_test.go index 0f08e71..9764ce6 100644 --- a/money_test.go +++ b/money_test.go @@ -1,19 +1,14 @@ package money import ( - "bytes" "encoding/json" "errors" - "fmt" "reflect" "testing" ) -// tn defines test numeric type, to ensure same type for all tests. -type tn int - func TestNew(t *testing.T) { - m := New[tn](1, EUR) + m := New(1, EUR) if m.amount.val != 1 { t.Errorf("Expected %d got %d", 1, m.amount.val) @@ -23,7 +18,7 @@ func TestNew(t *testing.T) { t.Errorf("Expected currency %s got %s", EUR, m.currency.Code) } - m = New[tn](-100, EUR) + m = New(-100, EUR) if m.amount.val != -100 { t.Errorf("Expected %d got %d", -100, m.amount.val) @@ -61,9 +56,9 @@ func TestMoney_SameCurrency(t *testing.T) { } func TestMoney_Equals(t *testing.T) { - m := New[tn](0, EUR) + m := New(0, EUR) tcs := []struct { - amount tn + amount int expected bool }{ {-1, false}, @@ -95,7 +90,7 @@ func TestMoney_Equals_DifferentCurrencies(t *testing.T) { } func TestMoney_GreaterThan(t *testing.T) { - m := New[int](0, EUR) + m := New(0, EUR) tcs := []struct { amount int expected bool @@ -106,7 +101,7 @@ func TestMoney_GreaterThan(t *testing.T) { } for _, tc := range tcs { - om := New[int](tc.amount, EUR) + om := New(tc.amount, EUR) r, err := m.GreaterThan(om) if err != nil || r != tc.expected { @@ -117,7 +112,7 @@ func TestMoney_GreaterThan(t *testing.T) { } func TestMoney_GreaterThanOrEqual(t *testing.T) { - m := New[int](0, EUR) + m := New(0, EUR) tcs := []struct { amount int expected bool @@ -128,7 +123,7 @@ func TestMoney_GreaterThanOrEqual(t *testing.T) { } for _, tc := range tcs { - om := New[int](tc.amount, EUR) + om := New(tc.amount, EUR) r, err := m.GreaterThanOrEqual(om) if err != nil || r != tc.expected { @@ -139,9 +134,9 @@ func TestMoney_GreaterThanOrEqual(t *testing.T) { } func TestMoney_LessThan(t *testing.T) { - m := New[tn](0, EUR) + m := New(0, EUR) tcs := []struct { - amount tn + amount int expected bool }{ {-1, false}, @@ -150,7 +145,7 @@ func TestMoney_LessThan(t *testing.T) { } for _, tc := range tcs { - om := New[tn](tc.amount, EUR) + om := New(tc.amount, EUR) r, err := m.LessThan(om) if err != nil || r != tc.expected { @@ -161,9 +156,9 @@ func TestMoney_LessThan(t *testing.T) { } func TestMoney_LessThanOrEqual(t *testing.T) { - m := New[int64](0, EUR) + m := New(0, EUR) tcs := []struct { - amount int64 + amount int expected bool }{ {-1, false}, @@ -427,19 +422,19 @@ func TestMoney_RoundWithExponential(t *testing.T) { func TestMoney_Split(t *testing.T) { tcs := []struct { - amount int64 - split int64 - expected []int64 + amount int + split int + 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}}, } for _, tc := range tcs { m := New(tc.amount, EUR) - var rs []int64 + var rs []int split, _ := m.Split(tc.split) for _, party := range split { @@ -463,19 +458,19 @@ func TestMoney_Split2(t *testing.T) { func TestMoney_Allocate(t *testing.T) { tcs := []struct { - amount int64 - ratios []int64 - expected []int64 + amount int + ratios []int + expected []int }{ - {100, []int64{50, 50}, []int64{50, 50}}, - {100, []int64{30, 30, 30}, []int64{34, 33, 33}}, - {200, []int64{25, 25, 50}, []int64{50, 50, 100}}, - {5, []int64{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 { @@ -631,7 +626,6 @@ func TestMoney_Amount(t *testing.T) { } func TestDefaultMarshal(t *testing.T) { - t.Skip("panic memory nil") given := New(12345, IQD) expected := `{"amount":12345,"currency":"IQD"}` @@ -659,28 +653,26 @@ func TestDefaultMarshal(t *testing.T) { } } -func TestCustomMarshal(t *testing.T) { - t.Skip("panic memory nil") - given := New(12345, IQD) - expected := `{"amount":12345,"currency_code":"IQD","currency_fraction":3}` - MarshalJSON = func(m Money[int64]) ([]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 TestCustomMarshal(t *testing.T) { +// given := New(12345, IQD) +// expected := `{"amount":12345,"currency_code":"IQD","currency_fraction":3}` +// MarshalJSON = func(m Money[int]) ([]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) { - t.Skip("panic memory nil") given := `{"amount": 10012, "currency":"USD"}` expected := "$100.12" var m Money[int] @@ -726,28 +718,27 @@ func TestDefaultUnmarshal(t *testing.T) { } } -func TestCustomUnmarshal(t *testing.T) { - t.Skip("panic memory nil") - given := `{"amount": 10012, "currency_code":"USD", "currency_fraction":2}` - expected := "$100.12" - UnmarshalJSON = func(m *Money[int64], 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[int64] - 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()) +// } +// } From e51aec97b8769de48bb897aa1422aece1f058942 Mon Sep 17 00:00:00 2001 From: Raymond Date: Sun, 3 Apr 2022 21:08:57 +0300 Subject: [PATCH 4/5] quick optimization --- money.go | 20 ++++++++------------ money_test.go | 19 ------------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/money.go b/money.go index 7d47e3e..4297bb1 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 ( - // UnmarshalJSON is injection point of json.Unmarshaller for money.Money - // UnmarshalJSON = defaultUnmarshalJSON[int64] - // MarshalJSON is injection point of json.Marshaller for money.Money - // MarshalJSON = defaultMarshalJSON[int64] - // ErrCurrencyMismatch happens when two compared Money don't have the same currency. ErrCurrencyMismatch = errors.New("currencies don't match") @@ -51,7 +46,7 @@ func defaultUnmarshalJSON[T Numeric](m *Money[T], b []byte) error { if amount == 0 && currency == "" { ref = &Money[T]{} } else { - ref = New[T](T(amount), currency) + ref = New(T(amount), currency) } *m = *ref @@ -60,7 +55,7 @@ func defaultUnmarshalJSON[T Numeric](m *Money[T], b []byte) error { func defaultMarshalJSON[T Numeric](m Money[T]) ([]byte, error) { if m == (Money[T]{}) { - m = *New[T](0, "") + m = *New(T(0), "") } buff := bytes.NewBufferString(fmt.Sprintf(`{"amount": %d, "currency": "%s"}`, m.Amount(), m.Currency().Code)) @@ -73,7 +68,7 @@ type Amount[T Numeric] struct { } type Numeric interface { - ~int | ~int64 + ~int | ~int8 | ~int16 | ~int32 | ~int64 } // Money represents monetary value information, stores @@ -244,11 +239,12 @@ func (m *Money[T]) Split(n T) ([]*Money[T], error) { l := m.calc.absolute(r).val // Add leftovers to the first parties. var p T + v := T(1) + if a.val < 0 { + v = T(-1) + } + for p = 0; l != T(0); p++ { - v := T(1) - if a.val < 0 { - v = -1 - } ms[p].amount = m.calc.add(ms[p].amount, &Amount[T]{v}) l-- } diff --git a/money_test.go b/money_test.go index 9764ce6..9e2e8cf 100644 --- a/money_test.go +++ b/money_test.go @@ -653,25 +653,6 @@ 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[int]) ([]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" From cec568cfe7e9e951add22091267e1025121de966 Mon Sep 17 00:00:00 2001 From: Raymond Date: Sun, 3 Apr 2022 21:12:45 +0300 Subject: [PATCH 5/5] added split tests --- money.go | 2 +- money_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/money.go b/money.go index 4297bb1..d2b1739 100644 --- a/money.go +++ b/money.go @@ -241,7 +241,7 @@ func (m *Money[T]) Split(n T) ([]*Money[T], error) { var p T v := T(1) if a.val < 0 { - v = T(-1) + v = -1 } for p = 0; l != T(0); p++ { diff --git a/money_test.go b/money_test.go index 9e2e8cf..39f87f3 100644 --- a/money_test.go +++ b/money_test.go @@ -430,6 +430,8 @@ func TestMoney_Split(t *testing.T) { {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 {