Giter Site home page Giter Site logo

go-money's Introduction

Money

alt text

Go Report Card Coverage Status GoDoc License: MIT

GoMoney provides ability to work with monetary value using a currency's smallest unit. This package provides basic and precise Money operations such as rounding, splitting and allocating. Monetary values should not be stored as floats due to small rounding differences.

package main

import (
  "log"

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

func main() {
    pound := money.New(100, money.GBP)
    twoPounds, err := pound.Add(pound)

    if err != nil {
        log.Fatal(err)
    }

    parties, err := twoPounds.Split(3)

    if err != nil {
        log.Fatal(err)
    }

    parties[0].Display() // £0.67
    parties[1].Display() // £0.67
    parties[2].Display() // £0.66
}

Quick start

Get the package:

$ go get github.com/Rhymond/go-money

Features

  • Provides a Money struct which stores information about an Money amount value and its currency.
  • Provides a Money.Amount struct which encapsulates all information about a monetary unit.
  • Represents monetary values as integers, in cents. This avoids floating point rounding errors.
  • Represents currency as Money.Currency instances providing a high level of flexibility.

Usage

Initialization

Initialize Money by using smallest unit value (e.g 100 represents 1 pound). Use ISO 4217 Currency Code to set money Currency. Note that constants are also provided for all ISO 4217 currency codes.

pound := money.New(100, money.GBP)

Or initialize Money using the direct amount.

quarterEuro := money.NewFromFloat(0.25, money.EUR)

Comparison

Go-money provides base compare operations like:

  • Equals
  • GreaterThan
  • GreaterThanOrEqual
  • LessThan
  • LessThanOrEqual
  • Compare

Comparisons must be made between the same currency units.

pound := money.New(100, money.GBP)
twoPounds := money.New(200, money.GBP)
twoEuros := money.New(200, money.EUR)

pound.GreaterThan(twoPounds) // false, nil
pound.LessThan(twoPounds) // true, nil
twoPounds.Equals(twoEuros) // false, error: Currencies don't match
twoPounds.Compare(pound) // 1, nil
pound.Compare(twoPounds) // -1, nil
pound.Compare(pound) // 0, nil
pound.Compare(twoEuros) // pound.amount, ErrCurrencyMismatch

Asserts

  • IsZero
  • IsNegative
  • IsPositive

Zero value

To assert if Money value is equal to zero use IsZero()

pound := money.New(100, money.GBP)
result := pound.IsZero() // false

Positive value

To assert if Money value is more than zero use IsPositive()

pound := money.New(100, money.GBP)
pound.IsPositive() // true

Negative value

To assert if Money value is less than zero use IsNegative()

pound := money.New(100, money.GBP)
pound.IsNegative() // false

Operations

  • Add
  • Subtract
  • Multiply
  • Absolute
  • Negative

Comparisons must be made between the same currency units.

Addition

Additions can be performed using Add().

pound := money.New(100, money.GBP)
twoPounds := money.New(200, money.GBP)

result, err := pound.Add(twoPounds) // £3.00, nil

Subtraction

Subtraction can be performed using Subtract().

pound := money.New(100, money.GBP)
twoPounds := money.New(200, money.GBP)

result, err := pound.Subtract(twoPounds) // -£1.00, nil

Multiplication

Multiplication can be performed using Multiply().

pound := money.New(100, money.GBP)

result := pound.Multiply(2) // £2.00

Absolute

Return absolute value of Money structure

pound := money.New(-100, money.GBP)

result := pound.Absolute() // £1.00

Negative

Return negative value of Money structure

pound := money.New(100, money.GBP)

result := pound.Negative() // -£1.00

Allocation

  • Split
  • Allocate

Splitting

In order to split Money for parties without losing any pennies due to rounding differences, use Split().

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.

pound := money.New(100, money.GBP)
parties, err := pound.Split(3)

if err != nil {
    log.Fatal(err)
}

parties[0].Display() // £0.34
parties[1].Display() // £0.33
parties[2].Display() // £0.33

Allocation

To perform allocation operation use Allocate().

It splits money using the given ratios without losing pennies and as Split operations distributes leftover pennies amongst the parties with round-robin principle.

pound := money.New(100, money.GBP)
// Allocate is variadic function which can receive ratios as
// slice (int[]{33, 33, 33}...) or separated by a comma integers
parties, err := pound.Allocate(33, 33, 33)

if err != nil {
    log.Fatal(err)
}

parties[0].Display() // £0.34
parties[1].Display() // £0.33
parties[2].Display() // £0.33

Format

To format and return Money as a string use Display().

money.New(123456789, money.EUR).Display() // €1,234,567.89

To format and return Money as a float64 representing the amount value in the currency's subunit use AsMajorUnits().

money.New(123456789, money.EUR).AsMajorUnits() // 1234567.89

Contributing

Thank you for considering contributing! Please use GitHub issues and Pull Requests for contributing.

License

The MIT License (MIT). Please see License File for more information.

forthebadge

go-money's People

Contributors

batuhansk avatar benchr267 avatar bencrouse avatar conradkurth avatar danielwitz avatar davidalpert avatar dkasenka avatar evilnoxx avatar hieven avatar hubarthurcoelho avatar jamesallured avatar jasonharrison avatar jeffandersen avatar k315k1010 avatar kalledk avatar labala-sravani avatar llonchj avatar lombartec avatar mathiasgr avatar npinochet avatar pabloreszczynski avatar rhymond avatar tegk avatar toasterson avatar totemcaf avatar trunkol avatar tsauvajon avatar twmbx avatar tylerb avatar unkiwii avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-money's Issues

digit group separation and decimal split separator

This is a great package.
Would you mind updating the documentation on how we can use different formats used around the world, like other European countries, not to mention India where they separate digit groups differently?

I would mainly like to get

1.244,90 €

instead of

€1,244.90

JSON Marshal & Unmarshal

Hi everyone,

I'm actually trying to marshal / unmarshal the Money structure but, obviously because of the current implementation, it is not possible yet.

I saw that someone has already required a PR regarding that topic :

#38

Will it be validated ? I think it is a really important feature 👍
Thanks by advance !

@Rhymond

Issues with first example in Readme.md

I tried to run the first example in the Readme.md text.
Two issues immediately show up:

  1. The Example does not import "log" (I had to add the import).
  2. When I compiled and ran the example, absolutely nothing printed on the console.
    Am I missing something?
    Regards,
    -P.

Suggestion: Provide constant values for currency codes

Like the net/http package status codes and the use of time.Second etc. in the time package, I suggest providing currency codes as public constants.

I understand that they would represent a string of the same name but would make it easier to ensure that the currency code you're using is correct through autocompletion in editors.

I wanted to gather some feedback on this before having a go at implementing it. It shouldn't change the API as the constants will just act as helpers that can be used instead of passing a string literal as an argument, for example:

pound := money.New(100, money.GBP)

Format money value as major units

I've added a functionality that allows the user to get the value of the money in major units (float64).
for example:

money.New(123456789, "EUR").AsMajorUnits() // 1234567.89

Also, I've filled out some more currencies and fixed some that we're incorrect like COP that supposed to have fraction of 2 but was set as 0.

Update the README.md

package main

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

func main() {
    pound := money.New(100, money.GBP)
    twoPounds, err := pound.Add(pound)

    if err != nil {
        log.Fatal(err)
    }

    parties, err := twoPounds.Split(3)

    if err != nil {
        log.Fatal(err)
    }

    parties[0].Display() // £0.67
    parties[1].Display() // £0.67
    parties[2].Display() // £0.66
}

Could be updated to

package main

import (
    "fmt"
    "log"
    
    "github.com/Rhymond/go-money"
)

func main() {
    pound := money.New(100, "GBP")
    twoPounds, err := pound.Add(pound)

    if err != nil {
        log.Fatal(err)
    }

    parties, err := twoPounds.Split(3)

    if err != nil {
        log.Fatal(err)
    }

    for _, v := range parties {
        fmt.Println(v.Display())
    }
}

Support formatting and parsing of money strings with currency code

It would be fantastic if this library supported the formatting and parsing of Money as strings with the currency code instead of symbol (e.g. USD 1.00) which eliminates any ambiguity caused by symbols being shared across currencies (e.g. $1.00 may refer to USD, AUD, CAD, etc).

This also enables the library to be used when storing money amounts in a database as a string, as these can be parsed when being retrieved instead of just being formatted when inserted.

Embedd the currencies.json file into the go code

If i want to use this package in a application, I need to remember to copy the currencies.json file in a resource folder on the target machine. That's not very idiomatic.

Since the list isn't that big I think it would be better to just include it directly in the go codebase.

Future development of go-money

Hi @Rhymond,

I was wondering how do you envision the future of go-money in terms of:

  • Integration with existing golang.org/x/text/currency
  • Complete currency list support (fiat, crypto, commodities).
  • Supporting exchange rates from an external service

Thanks!

go get fails

Running go get github.com/rhymond/go-money fails and spits out the message:

go: github.com/rhymond/[email protected]: parsing go.mod: unexpected module path "github.com/Rhymond/go-money"
go: error loading module requirements

seems to be related to golang/go#27154

Global calculator variable will cause races

Right now there is a calculator variable that is global and created anew every time a money instance is created.

This is an issue and will cause races. Multiple money objects being created and used on multiple goroutines will cause indeterminate results due to the races.

The calculator instance should be inside the money instance, and be created in the money constructor.

Tag latests release

Hello,
is there any plan to make a new release/tag following the semantic versioning?
Seems like the latest release was v0.3.6 on Oct 31, 2017.
I'm specifically interested on the PR #36 that adds CHF currency, merged to master on Jul 25, 2018.

Split of small negative values doesn't work properly

I found that a small negative amount will be split wrongly.

Example

package main

import (
	"fmt"

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

func main() {
	var val = money.New(-2, "EUR")
	parts, err := val.Split(3)
	if err != nil {
		fmt.Println(err)
	}

	for _, part := range parts {
		fmt.Println(part.Display())
	}
}

Expected output

-€0.01
-€0.01
-€0.00

Actual output

€0.01
€0.01
€0.00

It seems that if a money value is less than the number of split parts it returns a wrong sign because 0.04/3 returns 0.02,0.01,0.01.

Non-idiomatic error handling / log.Fatal

Packages should return errors so that the consumer of the package can choose what happens.

Packages should not log things, and they should not exit the application via log.Fatal

Panics in packages are somewhat acceptable if some startup dependency is incorrect and the package cannot work without it.

Fixing this will require a change in the API, and thus should require a version increment.

Add money comparisson

There are several methods to compare two Money object but there is no Money.Compare(Money) function.

The proposal is to add a Compare function for Money.

// Compare returns an integer comparing two Money objects
// If Money objects are of different currency, an ErrCurrencyMismatch is returned.
// If both objects are for same currency, their amounts are compared.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b.
func (m *Money) Compare(om *Money) int {
    if err := m.assertSameCurrency(om); err != nil {
        return false, err
    }

    return m.compare(om), nil
}

I'll provide a PR.

Any reason for Amount being a data-structure?

Would s/o please explain to me why Amount is a struct and not just a type alias which I expected to be more simple and sufficient?

// Amount is a datastructure that stores the amount being used for calculations.
type Amount struct {
	val int64
}

It seems more odd when I saw when the Amount is fetched we are getting the value.

func (m *Money) Amount() int64 {
	return m.amount.val
}

Add currency of Myanmar (MMK)

Hi Rhymond,
thank you for creating this amazing tool!
I noticed that there is no support for the currency of Myanmar, may I create a PR and add it? thanks!

Use of int causes overflows and arch-specific variability

Using the architecture-independent int type will cause 32-bit binaries to overflow if more than 21,474,836.47 is used in a base-100 currency, introducing architecture-dependent behaviour differences which may not be caught.

This can be resolved by switching amounts to use the int64 data type. This will still work on 32-bit architectures, at the slight expense of performance, and allow base-100 currency values up to an amount of 92,233,720,368,547,758.07 ... More than likely enough for most consumers of this package :)

Use number of minor units in one major instead of a fraction

I know that most majority of currencies have 100 minor units in one major or don't have a minor at all and it's okay to save their fraction (2, 0 or 3) and use them for converting:

func (f *Formatter) ToMajorUnits(amount int64) float64 {
	if f.Fraction == 0 {
		return float64(amount)
	}

	return float64(amount) / float64(math.Pow10(f.Fraction))
}

But my suggestion is to be more pragmatic and remember about non-decimal currency and etc:

  • Malagasy ariary (1 ariary = 5 iraimbilanja), Mauritanian ouguiya and etc.;
  • Old currencies: 1 Thaler = 36 Mariengroschen = 288 Pfennige;
  • Fictional currencies: 1 Galleon = 17 Sickles = 493 Knuts;
  • Cryptocurrencies: 1 BTC = 100'000'000 Satoshi

And if we use the number of minor units we can remove this math.Pow10 function and the result will look like:

func (f *Formatter) ToMajorUnits(amount int64) float64 {
	if f.Units == 0 {
		return float64(amount)
	}

	return float64(amount) / float64(f.Units)
}

Add/Subtract multiple amounts together

It would be nice if the Money struct has methods to add or subtract multiple values. For example:

func( *m Money) AddMultiple(om... *Money) (*Money, error){

}

func( *m Money) SubtractMultiple(om... *Money) (*Money, error){

}

I write a lot of test cases that calculates money and invoices and I often need to add up multiple values in a test case to check if the calculated amount is correct. It's possible to do this using the Add() method, but it's quite tedious to have to check the error for each addition. I am currently using a helper function to do this, but I think this would be a very useful addition to the library.

Does not have i18n for formatting ?

Am building a templating system and need to handle currency properly.
Does this library have functionality for this ?
E.g in Sweden and Germany money is displayed differently

If you know of I lib I can combine and PR if you want too

Add go.mod

Creation of a go.mod file in compliance with go new way of handling dependencies.

@Rhymond what do you think of it ?

Change getDefault behaviour

Hi, would it be possible (and make sense of course) the money.New function to return an error if the provided currency code is not in the currencies list (e.g., "XYZ") instead of just creating it with default values? Less important and debatable, would making the default configurable (as more of a fallback) an interesting feature (e.g., set default as USD for example)?

Panic on money.Allocate()

Hello,

The Allocate function can panic in the if the given ratios are all zero.
This makes sense, since the function will try to divide de given amount in parts of zero.

However, in my application it can make sense to try to allocate zero money, which would be the sum of some item prices, using those same item prices as ratios. Since the items add up to zero, because they are free, you get a call like

amount := money.New(0, money.EUR)
parties, err := amount.Allocate(0,0,0)

I wonder if, in this case, the Allocate function shouldn't return a slice filled with zeros with the same length as the given ratios, instead of panicking while dividing by zero.

On the other hand, Allocating is just another word for dividing, and zero divided by zero should be undefined, and not zero.

Multiply and Divide only accept an int

Multiply and Divide only accept an int. What was the reason for this? It seems to lessen the usefulness of these functions. For example, calculating the net value from gross is impossible.

gross := money.New(599, "GBP")
net := gross.Divide(1.2) // error
tax, _ := gross.Subtract(net)

or am I missing something and this is possible in another way?

Question about the rounding for NewFromFloat

Love the new addition of NewFromFloat. Just wanted to get a clearer understanding of always rounding trailing decimals down when given.

 cents := money.NewFromFloat(99.999999, money.USD)
 //->  cents: 9999

Just want to know the why between getting 9999 vs 10000?

Thanks!

SQL Compatibility (GORM and other)

Hello

I am a happy user of go-money and use it in a private webapp with database.

I had to make some adaptations to make go-money usefull most notably I had to add sql compatibility. As I did not want to make changes to go-money at that time I just wrapped go-money into another struct and copy data into and out of the go money struct on read/write to the database. Needless to say I am not a fan of that solution but it worked for my needs.

Now that I have some time on my hands, I would like to implement that Feature into go-money directly so that Life is easier for me.

What I did was implement Scanner and Valuer Interface and save the Data as semicolon delimited string in the Database. That was enough to save amount and currency information for later Object reconstruction during Read.

Would you like to keep that implementation or do you have another idea in what format to save the Data to database?

I could also make Inherited types so that the Main type does not force a certain usage over another.

what are your thoughts on this?

Add possibility for custom formatter

When calling the Display() method for a given object i get the following output:
€1,000.00
But the correct german format is 1.000,00 € (in most cases; sometimes it would be € 1.000,-). This format varies even between different EURO countries. And then there is the problem with dot and comma as decimal marks.

That's why it would be nice to define a custom formatter before calling Display().

Digit separation in INR amount

Hi,

When using INR currency format, we are not getting the desired result:

Result:

1000.00 -> ₹1,000.00 (As expected)
10000.00 -> ₹10,000.00 (As expected)
100000.00 -> ₹100,000.00 (Expected ₹1,00,000.00)
1000000.00 -> ₹1,000,000.00 (Expected ₹10,00,000.00)

and so on

Can you provide a solution for this since this is regarding the placement of separator not the value of separator?

Fix README example

I just noticed that the readme has a flawed example:

The import isn't good, we have "github.com/rhymond/go-money" instead of "github.com/Rhymond/go-money"

wrong fraction length

@Rhymond friend
please take a look at:

VND: {Decimal: ".", Thousand: ",", Code: VND, Fraction: 0, NumericCode: "704", Grapheme: "\u20ab", Template: "1 $"},

i think the Fraction field is wrong.
It should be 2 instead of 0

E.g: If i check it in python:

from babel.numbers import get_currency_precision

CODE = "VND"

print(get_currency_precision(CODE))

#result 
>>> 2

Why does money.New() take an int64

By making money.New() accept an int64 rather than a float or more inline with other libraries do (possibly more idiomatic?) a *big.Float it cuts off the ability to use a value with a decimal amount during allocation.

dollar := money.New(3.50, "USD") // type error
dollar := money.New(big.NewFloat(3.50), "USD") // would work

SQL scanner/valuer

I'd like to do the implementations of scanner and valuer for Money:

Consider this:

Money {
  Amount: 10,
  Currency: EUR,
}

I see several ways to represent it in database.

Solution A

// Scanner and Valuer implemented on `money.Money`.
type someModel struct {
  Money money.Money,     `db:"money"`
}
INSERT INTO foo (money) VALUE ('EUR 10');

A string field in database holding both the amount and the currency.

  • Not suitable for search in SQL environment
  • Conveniently aggregates the currency and the amount in the model.

Solution B

Represent two distinct fields in database, one for the amount and the other for the currency.

  • Efficient select queries can be performed on amount and/or currency.
  • Compels you to use in the model money.Amount and money.Currency.
// Scanner and Valuer implemented on `money.Amount` and `money.Currency`.
type someModel struct {
  Amount money.Amount,     `db:"amount"`
  Currency money.Currency  `db:"currency"`
}
INSERT INTO foo (amount, currency) VALUE (10, 'EUR');

I prefer solution B, what do you think @Rhymond ?

Proposal: Add function NewFromFloat

It would be really useful to be able to create a new Money struct from a float or maybe a decimal string ('1.243'). Formatting the decimal amount from a source (API response, file, ...) to a Money struct can get really tedious, that's why I propose a util function inside the money package for dealing with this cases.

This is my current implementation:

func NewFromFloat(amount float64, currency string) *money.Money {
	currencyDecimals := math.Pow(10, float64(money.GetCurrency(currency).Fraction))
	amountCents := int64(amount * currencyDecimals)
	return money.New(amountCents, currency)
}

To not lose precision dealing with floats there can also be a NewFromString, but I haven gotten around to implementing it.

What do you guys think?

Add Must* versions of methods that return error

Thanks for a great package!

It would be quite convenient to have methods on Money that panic instead of returning error. These could wrap the existing methods, with the Must prefix. For example, MustAdd would wrap Money.Add.

In my program, for example, I deal with just one currency (and am using go-money for correct handling of cents in the dollar). The ability to avoid explicit error checking, and use the result of Add, Subtract, etc. as an unary expression would be very helpful.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.