Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Feature - NewFromString() #135

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ pound := money.New(100, money.GBP)
Or initialize Money using the direct amount.
```go
quarterEuro := money.NewFromFloat(0.25, money.EUR)
```
Or initialize Money using the plain amount from any data source.
```go

oneDollar := money.NewFromString("1", money.USD)
oneDolar.Display() // $1.00

oneHundredDollars := money.NewFromString("100", money.USD)
oneHundredDollars.Display() // $100.00

jeffBezosFortune := money.NewFromString("114,000,000,000.99", money.USD)
jeffBezosFortune.Display() // $114,000,000,000.99

```
Comparison
-
Expand Down
12 changes: 12 additions & 0 deletions main/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package main

import (
"fmt"

"github.com/Rhymond/go-money"
)

func main() {
jeffBezosFortune := money.NewFromString("114,000,000,000.99", money.USD)
fmt.Println(jeffBezosFortune.Display())
}
39 changes: 31 additions & 8 deletions money.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (

// Injection points for backward compatibility.
// If you need to keep your JSON marshal/unmarshal way, overwrite them like below.
// money.UnmarshalJSON = func (m *Money, b []byte) error { ... }
// money.MarshalJSON = func (m Money) ([]byte, error) { ... }
//
// 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
Expand Down Expand Up @@ -88,9 +89,29 @@ func New(amount int64, code string) *Money {

// NewFromFloat creates and returns new instance of Money from a float64.
// Always rounding trailing decimals down.
func NewFromFloat(amount float64, currency string) *Money {
currencyDecimals := math.Pow10(GetCurrency(currency).Fraction)
return New(int64(amount*currencyDecimals), currency)
func NewFromFloat(amount float64, code string) *Money {
currencyDecimals := math.Pow10(GetCurrency(code).Fraction)
return New(int64(amount*currencyDecimals), code)
}

// NewFromString creates and returns new instance of Money from a string.
// If we compare 'New', which takes integers and works with their minimum fraction,
// with 'NewFromString', we takes the explicit value, for example:
//
// NewFromString("100", money.USD) => $100.00 = one hundred dollars
// NewFromString("1", money.USD) => $1.00 = one dollar
//
// Always rounding trailing decimals down,
// because NewFromString use NewFromFloat under the hood
func NewFromString(amount string, code string) *Money {
currency := GetCurrency(code)
moneyInstance, err := ConvertStringAmount(amount, currency)

if err != nil {
panic(err)
}

return moneyInstance
}

// Currency returns the currency used by Money.
Expand Down Expand Up @@ -329,9 +350,11 @@ func (m Money) MarshalJSON() ([]byte, error) {
}

// Compare function compares two money of the same type
// if m.amount > om.amount returns (1, nil)
// if m.amount == om.amount returns (0, nil
// if m.amount < om.amount returns (-1, nil)
//
// if m.amount > om.amount returns (1, nil)
// if m.amount == om.amount returns (0, nil
// if m.amount < om.amount returns (-1, nil)
//
// If compare moneys from distinct currency, return (m.amount, ErrCurrencyMismatch)
func (m *Money) Compare(om *Money) (int, error) {
if err := m.assertSameCurrency(om); err != nil {
Expand Down
66 changes: 66 additions & 0 deletions string_converters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package money

import (
"errors"
"regexp"
"strconv"
)

func patternFactory(fractionNumber int) (*regexp.Regexp, error) {
switch fractionNumber {
case 0:
zeroDigitsPattern := `^\d{1,3}(,\d{3})*$`
return regexp.MustCompile(zeroDigitsPattern), nil
case 2:
twoDigitsPattern := `^\d{1,3}(,\d{3})*(\.\d{2})?$`
return regexp.MustCompile(twoDigitsPattern), nil
case 3:
threeDigitsPattern := `^\d{1,3}(,\d{3})*(\.\d{3})?$`
return regexp.MustCompile(threeDigitsPattern), nil
}
return nil, errors.New("invalid currency fraction")
}

func ConvertStringAmount(amount string, currency *Currency) (*Money, error) {

ok, err := amountIsValid(amount, currency.Fraction)

if err != nil || !ok {
return nil, errors.New("invalid amount pattern")
}

amountFormated := cleanString(amount)

amountAsFloat, err := parseStringToFloat64(amountFormated)

if err != nil {
return nil, errors.New("error parsing amount")
}

return NewFromFloat(amountAsFloat, currency.Code), nil
}

func amountIsValid(amount string, fraction int) (bool, error) {
regex, err := patternFactory(fraction)

if err != nil {
return false, err
}

return regex.MatchString(amount), nil
}

func cleanString(validAmount string) string {
cleanPattern := "[,]"

regex := regexp.MustCompile(cleanPattern)

amountFormated := regex.ReplaceAllString(validAmount, "")

return amountFormated
}

func parseStringToFloat64(validAmountFormated string) (float64, error) {
amountAsFloat, err := strconv.ParseFloat(validAmountFormated, 64)
return amountAsFloat, err
}
214 changes: 214 additions & 0 deletions string_converters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package money

import (
"reflect"
"regexp"
"testing"
)

func Test_patternFactory(t *testing.T) {
twoDigitsPattern := `^\d{1,3}(,\d{3})*(\.\d{2})?$`
regex := regexp.MustCompile(twoDigitsPattern)
type args struct {
fractionNumber int
}
tests := []struct {
name string
args args
want *regexp.Regexp
wantErr bool
}{
{
name: "wrong fractional number",
args: args{fractionNumber: 5},
wantErr: true,
},
{
name: "success fractional number",
args: args{fractionNumber: 2},
want: regex,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := patternFactory(tt.args.fractionNumber)
if (err != nil) != tt.wantErr {
t.Errorf("patternFactory() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("patternFactory() = %v, want %v", got, tt.want)
}
})
}
}

func Test_amountIsValid(t *testing.T) {
type args struct {
amount string
fraction int
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
{
name: "invalid amount - First - 2 fractions",
args: args{
amount: "1,0000.32",
fraction: 2,
},
want: false,
},
{
name: "invalid amount - Second - 2 fractions",
args: args{
amount: "1,000,00.320",
fraction: 2,
},
want: false,
},
{
name: "invalid amount - #3 - 2 fractions",
args: args{
amount: "1,000,0.32",
fraction: 2,
},
want: false,
},
{
name: "success amount - 2 fractions",
args: args{
amount: "1,000,000.32",
fraction: 2,
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := amountIsValid(tt.args.amount, tt.args.fraction)
if (err != nil) != tt.wantErr {
t.Errorf("amountIsValid() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("amountIsValid() = %v, want %v", got, tt.want)
}
})
}
}

func Test_cleanString(t *testing.T) {
assertAmountClean := "1234423677.33"

type args struct {
validAmount string
}
tests := []struct {
name string
args args
want string
}{
{
name: "valid amount to clean",
args: args{
validAmount: "1,234,423,677.33",
},
want: assertAmountClean,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cleanString(tt.args.validAmount); got != tt.want {
t.Errorf("cleanString() = %v, want %v", got, tt.want)
}
})
}
}

func Test_parseStringToFloat64(t *testing.T) {
type args struct {
validAmountFormated string
}
tests := []struct {
name string
args args
want float64
wantErr bool
}{
{
name: "valid formated amount",
args: args{
validAmountFormated: "123456789.900",
},
want: float64(123456789.900),
},
{
name: "invalid formated amount",
args: args{
validAmountFormated: "helloworld123456789.900",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseStringToFloat64(tt.args.validAmountFormated)
if (err != nil) != tt.wantErr {
t.Errorf("parseStringToFloat64() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseStringToFloat64() = %v, want %v", got, tt.want)
}
})
}
}

func TestConvertStringAmount(t *testing.T) {
usdCurrency := GetCurrency(USD)
mockAmount := NewFromFloat(10000000.99, USD)
type args struct {
amount string
currency *Currency
}
tests := []struct {
name string
args args
want *Money
wantErr bool
}{
{
name: "success amount - USD",
args: args{
amount: "10,000,000.99",
currency: usdCurrency,
},
want: mockAmount,
},
{
name: "invalid amount - USD",
args: args{
amount: "10,000,000.9999",
currency: usdCurrency,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ConvertStringAmount(tt.args.amount, tt.args.currency)
if (err != nil) != tt.wantErr {
t.Errorf("ConvertStringAmount() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ConvertStringAmount() = %v, want %v", got, tt.want)
}
})
}
}