Code Monkey home page Code Monkey logo

currency's People

Contributors

bojanz avatar czeslavo avatar dacohen avatar euanwm avatar floodfx avatar josharian avatar kunde21 avatar nvanbenschoten 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

currency's Issues

Handle minor amount values

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.

support banker's rounding

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?

Amount should implement driver.Valuer and sql.Scanner interfaces

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
}

Why does this invalid price not give an error?

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?

Optimized version of roundingContext is wrong

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() should consider empty values valid

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.

Number() gives exponent formatting sometimes

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.

Tag v1.0.0

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.

Switch to ericlagergren/decimal

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.

Negative currency converted to `BigInt` becomes positive

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?

Why does this code work? What is the purpose of Locale?

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

Document some more features

I think the README should document at least two more features in the code:

  • the fact that the package has builtin support for github.com/google/go-cmp, making comparisons reliable and reasonably fast without having to resort to Amount.Cmp when just testing for equality
  • the implementation of the encoding.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.

The calculation precision (16) is sometimes not enough

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().

allow more uses of the zero value

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.

NoSql Support

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)

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.