Skip to content

Commit

Permalink
chore: v2 folder
Browse files Browse the repository at this point in the history
  • Loading branch information
ladydascalie committed Sep 16, 2024
1 parent 3c85757 commit da0ea31
Show file tree
Hide file tree
Showing 24 changed files with 6,094 additions and 1 deletion.
1 change: 0 additions & 1 deletion v2
Submodule v2 deleted from 74ed80
30 changes: 30 additions & 0 deletions v2/.github/workflows/flat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: data
on:
schedule:
- cron: 0 0 * * *
workflow_dispatch: {}
push:
paths:
- .github/workflows/flat.yml
jobs:
scheduled:
runs-on: ubuntu-latest
steps:
- name: Setup deno
uses: denoland/setup-deno@main
with:
deno-version: v1.x
- name: Check out repo
uses: actions/checkout@v2
- name: Fetch data
uses: githubocto/flat@v3
with:
http_url: https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml
downloaded_filename: cmd/list_one.xml
# Build the libraries.
- name: build application
run: make build
- name: Git Auto Commit
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: apply automatic changes
1 change: 1 addition & 0 deletions v2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/
21 changes: 21 additions & 0 deletions v2/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Benjamin Cable

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
4 changes: 4 additions & 0 deletions v2/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build:
go run cmd/main.go
gofmt -w std.go
go test -v -cover ./...
32 changes: 32 additions & 0 deletions v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# currency

This package generates structs containing all the up-to-date `ISO4217` currency codes and minor units, along with a very simple validator.

Data is graciously provided by:

- [International Organization for Standardization](https://www.iso.org/iso-4217-currency-codes.html)
- [Currency Code Services – ISO 4217 Maintenance Agency](https://www.currency-iso.org)

## Usage:

```go
package main

import (
"fmt"
"github.com/ladydascalie/currency"
)

func main() {
// Validate currency code.
if !currency.Valid("ABC") {
// Handle invalid currency code.
fmt.Println("Invalid currency code")
}

// Retrieve and print currency values.
fmt.Println(currency.USD.Code()) // Output: USD
fmt.Println(currency.USD.MinorUnit()) // Output: 2
}

```
50 changes: 50 additions & 0 deletions v2/accounting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Accounting

The accounting package is used when dealing with currency math that requires a high degree of precision.

A few business rules and assumptions are made:

- Banker's Rounding (or round half to even) is applied whenever rounding mode is available.
- see: [https://en.wikipedia.org/wiki/Rounding#Round_half_to_even](https://en.wikipedia.org/wiki/Rounding#Round_half_to_even)
- The maximum precision allowed after the decimal dot is **2**.

Examples:

```go
package main

import (
"fmt"

"github.com/ladydascalie/currency/accounting"
"github.com/ladydascalie/currency"
)

func main() {
// Create an amount from an int64 and a currency
amount := accounting.MakeAmount(currency.GBP, 1234)

// Amounts contain the minor representation of that currency.
fmt.Println(amount.MinorValue)
// output: 1234

// alternatively floats are also ok!
gbp := accounting.Float64ToAmount(currency.GBP, 32.32)
fmt.Printf("minor value: %d, stringer: %s", gbp.MinorValue, gbp)
// output: minor value: 3232, stringer: 32.32 GBP

// Currencies with no minor decimals drop the invalid precision.
jpy := accounting.Float64ToAmount(currency.JPY, 32.32)
fmt.Printf("minor value: %d, stringer: %s", jpy.MinorValue, jpy)
// output: minor value: 32, stringer: 32 JPY

// Exchanging currencies is also supported.
usd := accounting.Float64ToAmount(currency.USD, 100.0)
eur, err := accounting.Exchange(usd, currency.EUR, 1.08968)
if err != nil {
// handle...
}
fmt.Println(eur)
// output: 91.77 EUR
}
```
219 changes: 219 additions & 0 deletions v2/accounting/accounting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package accounting

import (
"fmt"
"math"
"math/big"
"strconv"
"strings"

"github.com/ladydascalie/currency/v2"
)

const (
// QuadruplePrecision describes 128 bits of precision for IEEE 754 decimals
// see: https://en.wikipedia.org/wiki/Quadruple-precision_floating-point_format
QuadruplePrecision uint = 128

// OctuplePrecision describes 256 bits of precision for for IEEE 754 decimals
// see: https://en.wikipedia.org/wiki/Octuple-precision_floating-point_format
OctuplePrecision uint = 256
)

var (
// Minimum rate of tax must be 0.
// As far as we know, there are as of writing, no markets with a negative sales tax.
min = itof(0)

// Baseline for creating a rate divisor.
base = itof(1)
)

// Amount defines an amount in a given currency, in it's minor unit form.
type Amount struct {
Currency currency.Currency
MinorValue int64
}

// MakeAmount returns an Amount from the provided currency and minor unit value.
func MakeAmount(c currency.Currency, minorValue int64) Amount {
return Amount{
Currency: c,
MinorValue: minorValue,
}
}

// String implements a default stringer for an Amount
// Note that the string will be in "human readable" format, rather than
// using the minor currency unit, this conversion is done using the
// AmountToFloat64 function, also available within this package.
// ISO_4217 does not regulate spacing or prefixing vs. suffixing.
// Strings produced using this method always follow this pattern:
//
// ┏━━ always decimal dot separated.
// ┃
// 123.45 GBP
// ┃ ┗━━ ISO currency code.
// ┗━━ maximum 2 digits precision.
func (a Amount) String() string {
f64 := AmountToFloat64(a)
format := fmt.Sprintf("%%.%df %%s", a.Currency.MinorUnits())
return fmt.Sprintf(format, f64, a.Currency.Code())
}

// ValidateManyFloatsArePrecise tests that the given float64 arguments
// have the desired precision, this is a convenience wrapper
// around ValidateFloatIsPrecise.
// NOTE: 2 digits past the dot is a business rule.
func ValidateManyFloatsArePrecise(args ...float64) error {
for _, f := range args {
if err := ValidateFloatIsPrecise(f); err != nil {
return err
}
}
return nil
}

// ValidateFloatIsPrecise ensures that a float64 value does not exceed
// a precision of 2 digits past the decimal period. This ensures we do not
// store incorrect currency data.
// NOTE: 2 digits past the decimal period is a business rule.
func ValidateFloatIsPrecise(f float64) error {
// parse the float, with the smallest number of digits necessary
parsed := strconv.FormatFloat(f, 'f', -1, 64)

// split the parsed number on the decimal period
parts := strings.Split(parsed, ".")

// in case of exact number...
if len(parts) == 1 {
return nil
}

// check the float's precision
if prec := len([]rune(parts[1])); prec > 2 {
return ErrFloatPrecision{
Value: parsed,
Precision: prec,
}
}
return nil
}

// Float64ToAmount returns an amount from the provided currency and value.
func Float64ToAmount(c currency.Currency, value float64) Amount {
minor := int64(math.Round(value * float64(c.Factor())))
return Amount{
Currency: c,
MinorValue: minor,
}
}

// AmountToFloat64 returns the currency data as a floating point from it's
// minor currency unit format.
func AmountToFloat64(amount Amount) float64 {
// fast path for currencies like JPY with a factor of 1.
if amount.Currency.Factor() == 1 {
return float64(amount.MinorValue)
}
var (
v = itof(amount.MinorValue)
f = ftof(amount.Currency.FactorF64())
)
f64, _ := newf().Quo(v, f).Float64()
return f64
}

// Exchange - Apply currency exchange rates to an amount.
//
// rate - should always be given from the approved finance list.
// NOTE: A rate of zero will return the amount you put in, unchanged.
//
// Rounding to the nearest even is a defined business rule.
// Tills may round up to the nearest penny, but for reporting, the rule is
// always to use banker's rounding.
//
// If unclear, see: // http://wiki.c2.com/?BankersRounding.
func Exchange(amount Amount, c currency.Currency, rate float64) (Amount, error) {
switch {
case rate < 0:
return Amount{}, ErrSubZeroRate
case rate == 0:
// Decomissioned currencies might trigger that case,
// but that should really not be the general rule.
return MakeAmount(c, 0), nil
}

from := ftof(AmountToFloat64(amount))
bigRate := ftof(rate)

// Here we divide the value, by it's minor currency
// unit factor, then divide it once more by the
// exchange rate.
// -> v / e
to, _ := from.Quo(from, bigRate).Float64()

// http://wiki.c2.com/?BankersRounding
// This part guarantees that we will not have more than 2 decimals after the dot.
to = math.RoundToEven(to*100) / 100

return Float64ToAmount(c, to), nil
}

// RatNetAmount applies a VAT rate to a big.Rat value. This method returns a big.Float
// so it's accuracy can be checked, and it's value applied with .Rat(some.field)
func RatNetAmount(gross, rate *big.Rat) (*big.Float, error) {
// Here we go for octuple precision as we are dealing with rational numbers.
bf := func(rat *big.Rat) *big.Float {
return big.NewFloat(0).SetRat(rat).SetPrec(OctuplePrecision).SetMode(big.ToNearestEven)
}

v := bf(gross)
r := bf(rate)

// Guard against impossible (negative) tax rates.
switch r.Cmp(min) {
case -1:
return min, ErrSubZeroRate
case 0:
return v, nil
}
// Turn the rate into a divisor by making it superior to 1.
divisor := newf().Add(base, r)

// Here we divide the gross by it's vat:
// -> val / vat
// where vat is a gross superior to 1.
return newf().Quo(v, divisor), nil
}

// NetAmount derives the net amount before tax is applied using the given rate.
func NetAmount(gross int64, rate float64) (int64, error) {
grossRat, _ := itof(gross).Rat(nil)
rateRat, _ := ftof(rate).Rat(nil)

bf, err := RatNetAmount(grossRat, rateRat)
if err != nil {
return 0, err
}

netStr := bf.Text('f', 0)
net, _ := new(big.Int).SetString(netStr, 10)
return net.Int64(), nil
}

// TaxAmount returns the difference between the gross and the net amounts.
func TaxAmount(gross, net int64) (int64, error) {
// Guard against values that are not allowed in this context.
if gross < 0 {
return 0, ErrSubZeroGross
}
if net < 0 {
return 0, ErrSubZeroNet
}
if net > gross {
return 0, ErrNetOverGrossAmount
}
// list amount - net amount = tax amount
return gross - net, nil
}
Loading

0 comments on commit da0ea31

Please sign in to comment.