bojanz / currency Goto Github PK
View Code? Open in Web Editor NEWCurrency handling for Go.
Home Page: https://pkg.go.dev/github.com/bojanz/currency
License: MIT License
Currency handling for Go.
Home Page: https://pkg.go.dev/github.com/bojanz/currency
License: MIT License
if using CAD currency for example, and i want to display the symbol for "en-CA", GetSymbol prioritizes "en" over "en-CA" so it will return "CAD$" and not "$"
Financial APIs use minor values in order to avoid any invalid amounts or ambiguous encoding.
As this library stands, it is unable to handle parsing amounts from or writing amounts to a string in minor units. ToMinorUnits
risks overflow attacks on int64 datatype.
I'd like to add banker's rounding as a rounding option. apd supports it (RoundHalfEven), so it is a relatively trivial change, but it does mean adding new API.
WDYT?
Eg ($1.00) == -$1.00
I am using PQ driver panics at this line as the type of src is []unit8
Line 347 in 28b0298
maybe should check for types. something like the following
https://github.com/rengas/currency/blob/253a204a22fbe171270075154ea9076769c8b87c/amount.go#L349
This would allow users on Postgres to define a composite type for prices, and scan it directly into currency.Amount.
Example composite type:
CREATE TYPE price AS (
number NUMERIC,
currency_code CHAR(3)
);
Example usage in a table:
CREATE TABLE products (
id CHAR(26) PRIMARY KEY,
name TEXT NOT NULL,
price price NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ
);
Example scan:
p := Product{}
row := tx.QueryRow(ctx, `SELECT id, name, price, created_at, updated_at FROM products WHERE id = $1`, id)
err := row.Scan(&p.ID, &p.Name, &p.Price, &p.CreatedAt, &p.UpdatedAt)
The two interface implementations would look something like this:
// Value implements the database/driver.Valuer interface.
func (a Amount) Value() (driver.Value, error) {
return fmt.Sprintf("(%v,%v)", a.Number(), a.CurrencyCode()), nil
}
// Scan implements the database/sql.Scanner interface.
func (a *Amount) Scan(src interface{}) error {
// Wire format: "(9.99,USD)".
input := strings.Trim(src.(string), "()")
values := strings.Split(input, ",")
n := values[0]
currencyCode := values[1]
// Do something with the two strings.
return nil
}
I would expect both of these instructions to raise an error because dollars use only 2 digits and yen does not use any digits.
amount, err := formatter.Parse("12.433356", "USD")
amount, err := formatter.Parse("12.433356", "JPY")
package main
import (
"fmt"
"github.com/bojanz/currency"
)
func main() {
digits, _ := currency.GetDigits("USD")
fmt.Println(fmt.Sprintf("USD has %d digits", digits))
locale := currency.NewLocale("en_us")
formatter := currency.NewFormatter(locale)
fmt.Println(fmt.Sprintf("But it works with more than %d digits", digits))
amount, err := formatter.Parse("12.433356", "USD")
fmt.Println(err, amount)
}
Output:
USD has 2 digits
But it works with more than 2 digits
<nil> 12.433356 USD
Is there some reason or feature I'm missing?
In roundingContext
you're optimizing an allocation away by returning a preallocated context that extends apd.BaseContext
if the rounding mode is set to RoundHalfUp
. See here.
Unfortunately, the default rounding mode used by apd.BaseContext
is RoundDown
, not RoundHalfUp
, so the context configuration is, I believe, wrong. Given that I have not explicitly tested this, maybe I'm missing something, so please close this if that's the case.
IsValid() currently considers empty values invalid, which makes sense at a glance.
However, most validation libraries across language ecosystems actually consider empty values to be valid. This allows separating required checks from validity checks, making it easier to compose validators. Using this package in production lead me to the same need.
So, let's modify IsValid(). This will make its usage within the package more verbose, but it will provide more flexibility to callers.
When using the RoundTo method with a precision of 8 and RoundHalfUp rounding mode on a zero amount, the Number method returns "0E-8" instead of "0.00000000". This inconsistency in formatting can cause confusion. Here's a snippet to reproduce the issue:
package main
import (
"fmt"
"github.com/bojanz/currency"
)
func main() {
c, _ := currency.NewAmountFromInt64(0, "SEK")
c = c.RoundTo(8, currency.RoundHalfUp)
fmt.Printf("%s\n", c.Number())
}
The apd.Decimal String() defaults to use d.Text('G') which is intended?
// 'G' like 'E' for large exponents, like 'f' otherwise
One fix would be to change Number() to use apd.Decimal.Text('f') method but that will be a breaking change
It would be nice to be able to output Number() without exponents.
It would be very useful to expose a function like
fun currency.getCode(countryCode string) (string, error)
it would allow to retrieve the currency used by default for a given country
Let's tag a v1.0.0 before the end of September. I don't see the API changing much till then, just waiting for a bit more testing in production. Not blocked on #1.
Benchmarks show ericlagergren/decimal to be noticeably faster than cockroachdb/apd, along with doing less allocations. We should try switching.
I'd like to get an answer to ericlagergren/decimal#153 before I merge this, since the current checks feel a bit repetitive.
EDIT: Answer received, waiting on maintainer fix.
Hello I noticed that a negative amount when converted to BigInt
results in a positive number. Here is a working example.
package main
import (
"fmt"
"math/big"
"github.com/bojanz/currency"
)
func main() {
amount, _ := currency.NewAmount("-9.00", "USD")
zero, _ := currency.NewAmount("0", "USD")
isNegative, _ := amount.Cmp(zero)
fmt.Printf("amount: %v\n", amount)
fmt.Printf("amount < 0: %v\n", isNegative)
bigInt := amount.BigInt()
zeroBigInt := big.NewInt(0)
fmt.Printf("bigInt : %v\n", bigInt)
fmt.Printf("bigInt < 0 : %v\n", bigInt.Cmp(zeroBigInt))
}
amount: -9.00 USD
amount < 0: -1
bigInt: 900
bigInt < 0 : 1
Is this expected?
I ran into the problem that I only have the country name and not the Locale, so reading the code among other things in the end I don't understand what is the purpose of Locale in this package?
Why if I do this don't I get any errors or an empty output?
locale := currency.NewLocale(" ")
package main
import (
"fmt"
"github.com/bojanz/currency"
)
func main() {
locale := currency.NewLocale(" ")
formatter := currency.NewFormatter(locale)
amount, err := currency.NewAmount("1245.988", "JPY")
fmt.Println(err, formatter.Format(amount))
formatter.MaxDigits = 2
fmt.Println(formatter.Format(amount))
formatter.NoGrouping = true
amount, _ = currency.NewAmount("1245", "EUR")
fmt.Println(formatter.Format(amount))
formatter.MinDigits = 0
fmt.Println(formatter.Format(amount))
formatter.CurrencyDisplay = currency.DisplayNone
fmt.Println(formatter.Format(amount))
}
Output:
<nil> ¥1,245.988
¥1,245.99
€1245.00
€1245
1245
I think the README should document at least two more features in the code:
github.com/google/go-cmp
, making comparisons reliable and reasonably fast without having to resort to Amount.Cmp
when just testing for equalityencoding.BinaryMarshaler
interface actually produces valid strings in its []byte
results, allowing them to be stored in database as strings in SQL and compared for equality, but not for ordering.Alternatively, documenting a serialization that would allow SQL ordering would be nice too. I didn't find one in the current implementation, though.
Right now all amount methods use the same calculation precision:
ctx := apd.BaseContext.WithPrecision(16)
It is possible for that precision to not be enough, for example when dealing with amounts larger than a math.MaxInt64.
Discovered in #7.
A possible solution is to always use 39 digits of precision, matching decimal128, but that can impose a performance penalty for the majority use case (where 16 digits is enough), so we probably want to switch based on a.number.Coeff.IsInt64().
Started investigating this when I realized that de-AT and de-CH use the wrong currency symbol for USD, US$ instead of $. The tests were passing cause they were confirming the broken selection. GetSymbol() has a bug.
Hi! Thanks a bunch for this very useful package.
Consider summing a slice of amounts. The natural code to write is something like:
func Sum(amounts []currency.Amount) (currency.Amount, error) {
var sum currency.Amount
for _, x := range amounts {
var err error
sum, err = sum.Add(x)
if err != nil {
return currency.Amount{}, err
}
}
return sum, nil
}
However, this code doesn't work, because sum
gets initialized to the zero value, which has an empty currency code. Playground: https://go.dev/play/p/SzMyDcxcos7
I propose that Add and Sub the zero value (0 number, empty currency code) as special, and ignore currency mismatches in this case, adopting the non-empty currency code.
I also hit something similar in currency.Amount.Scan. I want to be able to store the zero value in the database, as a "not yet initialized, unknown currency" value. But loading such a value from the database using currency.Amount.Scan fails, because the empty currency code is not valid. I similarly propose that Scan have a special case for number = 0 and currency code consisting of all spaces ("" or " "), and allow it to be parsed without error.
Thanks again.
Are there any plans to support storing Amount in no-sql databases? (for example mongo)
If not do you have any tips on how to store that info? (mostly the number part concerns me)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.