├── go.mod ├── go.sum ├── LICENSE ├── .github └── workflows │ └── build.yml ├── locale.go ├── bench_test.go ├── README.md ├── currency.go ├── locale_test.go ├── currency_test.go ├── example_test.go ├── formatter.go ├── amount.go ├── formatter_test.go ├── gen.go ├── amount_test.go └── data.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bojanz/currency 2 | 3 | go 1.18 4 | 5 | require github.com/cockroachdb/apd/v3 v3.2.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= 2 | github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= 3 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bojan Zivanovic and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Install Go 8 | uses: actions/setup-go@v4 9 | with: 10 | go-version: 1.23.x 11 | 12 | - name: Install tools 13 | run: | 14 | go install golang.org/x/tools/cmd/goimports@latest 15 | go install honnef.co/go/tools/cmd/staticcheck@latest 16 | 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - uses: actions/cache@v4 21 | with: 22 | path: | 23 | ~/.cache/go-build 24 | ~/go/pkg/mod 25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 26 | restore-keys: | 27 | ${{ runner.os }}-go- 28 | 29 | - name: Verify code formatting 30 | run: | 31 | test -z "$(set -o pipefail && goimports -l -d . | tee goimports.out)" || { cat goimports.out && exit 1; } 32 | 33 | - name: Lint 34 | run: | 35 | go vet ./... 36 | staticcheck ./... 37 | 38 | test: 39 | strategy: 40 | matrix: 41 | go: ['1.21', '1.22', '1.23'] 42 | name: test @ Go ${{ matrix.go }} 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Install Go 46 | uses: actions/setup-go@v3 47 | with: 48 | go-version: ${{ matrix.go }} 49 | 50 | - name: Checkout code 51 | uses: actions/checkout@v4 52 | 53 | - uses: actions/cache@v4 54 | with: 55 | path: | 56 | ~/.cache/go-build 57 | ~/go/pkg/mod 58 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 59 | restore-keys: | 60 | ${{ runner.os }}-go- 61 | 62 | - name: Test 63 | run: go test -v -race -coverprofile=profile.cov ./... 64 | 65 | - name: Send coverage 66 | uses: shogo82148/actions-goveralls@v1 67 | with: 68 | path-to-profile: profile.cov 69 | flag-name: Go-${{ matrix.go }} 70 | parallel: true 71 | 72 | finish: 73 | needs: test 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: shogo82148/actions-goveralls@v1 77 | with: 78 | parallel-finished: true 79 | -------------------------------------------------------------------------------- /locale.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | package currency 5 | 6 | import ( 7 | "strings" 8 | "unicode" 9 | "unicode/utf8" 10 | ) 11 | 12 | // Locale represents a Unicode locale identifier. 13 | type Locale struct { 14 | Language string 15 | Script string 16 | Territory string 17 | } 18 | 19 | // NewLocale creates a new Locale from its string representation. 20 | func NewLocale(id string) Locale { 21 | // Normalize the ID ("SR_rs_LATN" => "sr-Latn-RS"). 22 | id = strings.ToLower(strings.TrimSpace(id)) 23 | id = strings.ReplaceAll(id, "_", "-") 24 | locale := Locale{} 25 | for i, part := range strings.Split(id, "-") { 26 | if i == 0 { 27 | locale.Language = part 28 | continue 29 | } 30 | partLen := len(part) 31 | if partLen == 4 { 32 | // Uppercase the first letter in a UTF8-safe manner. 33 | r, size := utf8.DecodeRuneInString(part) 34 | locale.Script = string(unicode.ToTitle(r)) + part[size:] 35 | continue 36 | } 37 | if partLen == 2 || partLen == 3 { 38 | locale.Territory = strings.ToUpper(part) 39 | continue 40 | } 41 | } 42 | 43 | return locale 44 | } 45 | 46 | // String returns the string representation of l. 47 | func (l Locale) String() string { 48 | b := strings.Builder{} 49 | b.WriteString(l.Language) 50 | if l.Script != "" { 51 | b.WriteString("-") 52 | b.WriteString(l.Script) 53 | } 54 | if l.Territory != "" { 55 | b.WriteString("-") 56 | b.WriteString(l.Territory) 57 | } 58 | 59 | return b.String() 60 | } 61 | 62 | // MarshalText implements the encoding.TextMarshaler interface. 63 | func (l Locale) MarshalText() ([]byte, error) { 64 | return []byte(l.String()), nil 65 | } 66 | 67 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 68 | func (l *Locale) UnmarshalText(b []byte) error { 69 | *l = NewLocale(string(b)) 70 | return nil 71 | } 72 | 73 | // IsEmpty returns whether l is empty. 74 | func (l Locale) IsEmpty() bool { 75 | return l.Language == "" && l.Script == "" && l.Territory == "" 76 | } 77 | 78 | // GetParent returns the parent locale for l. 79 | // 80 | // Order: 81 | // 1. Language - Script - Territory (e.g. "sr-Cyrl-RS") 82 | // 2. Language - Script (e.g. "sr-Cyrl") 83 | // 3. Language (e.g. "sr") 84 | // 4. English ("en") 85 | // 5. Empty locale ("") 86 | // 87 | // Note that according to CLDR rules, certain locales have special parents. 88 | // For example, the parent for "es-AR" is "es-419", and for "sr-Latn" it is "en". 89 | func (l Locale) GetParent() Locale { 90 | localeID := l.String() 91 | if localeID == "" || localeID == "en" { 92 | return Locale{} 93 | } 94 | if p, ok := parentLocales[localeID]; ok { 95 | return NewLocale(p) 96 | } 97 | 98 | if l.Territory != "" { 99 | return Locale{Language: l.Language, Script: l.Script} 100 | } else if l.Script != "" { 101 | return Locale{Language: l.Language} 102 | } else { 103 | return Locale{Language: "en"} 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package currency_test 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/bojanz/currency" 9 | ) 10 | 11 | var result currency.Amount 12 | var cmpResult int 13 | 14 | func BenchmarkNewAmount(b *testing.B) { 15 | var z currency.Amount 16 | for n := 0; n < b.N; n++ { 17 | z, _ = currency.NewAmount("99.99", "EUR") 18 | } 19 | result = z 20 | } 21 | 22 | func BenchmarkNewAmountFromBigInt(b *testing.B) { 23 | x := big.NewInt(9999) 24 | 25 | var z currency.Amount 26 | for n := 0; n < b.N; n++ { 27 | z, _ = currency.NewAmountFromBigInt(x, "EUR") 28 | } 29 | result = z 30 | } 31 | 32 | func BenchmarkNewAmountFromInt64(b *testing.B) { 33 | var z currency.Amount 34 | for n := 0; n < b.N; n++ { 35 | z, _ = currency.NewAmountFromInt64(9999, "EUR") 36 | } 37 | result = z 38 | } 39 | 40 | func BenchmarkAmount_Add(b *testing.B) { 41 | x, _ := currency.NewAmount("34.99", "USD") 42 | y, _ := currency.NewAmount("12.99", "USD") 43 | 44 | var z currency.Amount 45 | for n := 0; n < b.N; n++ { 46 | z, _ = x.Add(y) 47 | } 48 | result = z 49 | } 50 | 51 | func BenchmarkAmount_Sub(b *testing.B) { 52 | x, _ := currency.NewAmount("34.99", "USD") 53 | y, _ := currency.NewAmount("12.99", "USD") 54 | 55 | var z currency.Amount 56 | for n := 0; n < b.N; n++ { 57 | z, _ = x.Sub(y) 58 | } 59 | result = z 60 | } 61 | 62 | func BenchmarkAmount_Mul(b *testing.B) { 63 | x, _ := currency.NewAmount("34.99", "USD") 64 | 65 | var z currency.Amount 66 | for n := 0; n < b.N; n++ { 67 | z, _ = x.Mul("2") 68 | } 69 | result = z 70 | } 71 | 72 | func BenchmarkAmount_MulDec(b *testing.B) { 73 | x, _ := currency.NewAmount("34.99", "USD") 74 | 75 | var z currency.Amount 76 | for n := 0; n < b.N; n++ { 77 | z, _ = x.Mul("2.5") 78 | } 79 | result = z 80 | } 81 | 82 | func BenchmarkAmount_Div(b *testing.B) { 83 | x, _ := currency.NewAmount("34.99", "USD") 84 | 85 | var z currency.Amount 86 | for n := 0; n < b.N; n++ { 87 | z, _ = x.Div("2") 88 | } 89 | result = z 90 | } 91 | 92 | func BenchmarkAmount_DivDec(b *testing.B) { 93 | x, _ := currency.NewAmount("34.99", "USD") 94 | 95 | var z currency.Amount 96 | for n := 0; n < b.N; n++ { 97 | z, _ = x.Div("2.5") 98 | } 99 | result = z 100 | } 101 | 102 | func BenchmarkAmount_Round(b *testing.B) { 103 | x, _ := currency.NewAmount("34.9876", "USD") 104 | 105 | var z currency.Amount 106 | for n := 0; n < b.N; n++ { 107 | z = x.Round() 108 | } 109 | result = z 110 | } 111 | 112 | func BenchmarkAmount_RoundTo(b *testing.B) { 113 | x, _ := currency.NewAmount("34.9876", "USD") 114 | roundingModes := []currency.RoundingMode{ 115 | currency.RoundHalfUp, 116 | currency.RoundHalfDown, 117 | currency.RoundUp, 118 | currency.RoundDown, 119 | } 120 | 121 | for _, roundingMode := range roundingModes { 122 | b.Run(fmt.Sprintf("rounding_mode_%d", roundingMode), func(b *testing.B) { 123 | var z currency.Amount 124 | for n := 0; n < b.N; n++ { 125 | z = x.RoundTo(2, roundingMode) 126 | } 127 | result = z 128 | }) 129 | } 130 | } 131 | 132 | func BenchmarkAmount_Cmp(b *testing.B) { 133 | x, _ := currency.NewAmount("34.99", "USD") 134 | y, _ := currency.NewAmount("12.99", "USD") 135 | 136 | var z int 137 | for n := 0; n < b.N; n++ { 138 | z, _ = x.Cmp(y) 139 | } 140 | cmpResult = z 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # currency [![Build](https://github.com/bojanz/currency/actions/workflows/build.yml/badge.svg)](https://github.com/bojanz/currency/actions/workflows/build.yml) [![Coverage Status](https://coveralls.io/repos/github/bojanz/currency/badge.svg?branch=master)](https://coveralls.io/github/bojanz/currency?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/bojanz/currency)](https://goreportcard.com/report/github.com/bojanz/currency) [![PkgGoDev](https://pkg.go.dev/badge/github.com/bojanz/currency)](https://pkg.go.dev/github.com/bojanz/currency) 2 | 3 | Handles currency amounts, provides currency information and formatting. 4 | 5 | Powered by CLDR v47, in just ~40kb of data. 6 | 7 | Backstory: https://bojanz.github.io/price-currency-handling-go/ 8 | 9 | ## Features 10 | 11 | 1. All currency codes, their numeric codes and fraction digits. 12 | 2. Currency symbols and formats for all locales. 13 | 3. Country mapping (country code => currency code). 14 | 4. Amount struct, with value semantics (Fowler's Money pattern) 15 | 5. Formatter, for formatting amounts and parsing formatted amounts. 16 | 6. Ability to register custom currencies, or override existing ones. 17 | 18 | ```go 19 | amount, _ := currency.NewAmount("275.98", "EUR") 20 | total, _ := amount.Mul("4") 21 | 22 | locale := currency.NewLocale("fr") 23 | formatter := currency.NewFormatter(locale) 24 | fmt.Println(formatter.Format(total)) // 1 103,92 € 25 | 26 | // Convert the amount to Iranian rial and show it in Farsi. 27 | total, _ = total.Convert("IRR", "45.538") 28 | total = total.Round() 29 | locale = currency.NewLocale("fa") 30 | formatter = currency.NewFormatter(locale) 31 | fmt.Println(formatter.Format(total)) // ‎ریال ۵۰٬۲۷۰ 32 | ``` 33 | 34 | ## Design goals 35 | 36 | ### Real decimal implementation under the hood. 37 | 38 | Currency amounts can't be floats. Storing integer minor units (2.99 => 299) 39 | becomes problematic once there are multiple currencies (difficult to sort in the 40 | DB), or there is a need for sub-minor-unit precision (due to merchant or tax 41 | requirements, etc). A real arbitrary-precision decimal type is required. Since 42 | Go doesn't have one natively, a userspace implementation is used, provided by 43 | the [cockroachdb/apd](https://github.com/cockroachdb/apd) package. The Amount struct provides an easy to use 44 | abstraction on top of it, allowing the underlying implementation to be replaced 45 | in the future without a backwards compatibility break. 46 | 47 | ### Smart filtering of CLDR data. 48 | 49 | The "modern" subset of CLDR locales is used, reducing the list from ~560 to ~370 locales. 50 | 51 | Once gathered, locales are filtered to remove all data not used by this package, 52 | and then deduplicated by parent (e.g. don't keep `fr-CH` if `fr` has the 53 | same data). 54 | 55 | Currency symbols are grouped together to avoid repetition. For example: 56 | 57 | "ARS": { 58 | {"ARS", []string{"en", "fr-CA"}}, 59 | {"$", []string{"es-AR"}}, 60 | {"$AR", []string{"fr"}}, 61 | } 62 | 63 | Currency names are not included because they are rarely shown, but need 64 | significant space. Instead, they can be fetched on the frontend via [Intl.DisplayNames](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames). 65 | 66 | ### Easy to compare. 67 | 68 | Amount structs can be compared via [google/go-cmp](https://github.com/google/go-cmp) thanks to the built-in Equal() method. 69 | 70 | ### Usable with a PostgreSQL composite type. 71 | 72 | Thanks to the driver.Valuer and sql.Scanner interfaces, applications using the [pgx](https://github.com/jackc/pgx) driver can store amounts in a composite type. 73 | 74 | Example schema: 75 | ``` 76 | CREATE TYPE price AS ( 77 | number NUMERIC, 78 | currency_code TEXT 79 | ); 80 | 81 | CREATE TABLE products ( 82 | id CHAR(26) PRIMARY KEY, 83 | name TEXT NOT NULL, 84 | price price NOT NULL, 85 | created_at TIMESTAMPTZ NOT NULL, 86 | updated_at TIMESTAMPTZ 87 | ); 88 | ``` 89 | Note that the number and currency_code columns can have any name, only their ordering matters. 90 | 91 | Example struct: 92 | ```go 93 | type Product struct { 94 | ID string 95 | Name string 96 | Price currency.Amount 97 | CreatedAt time.Time 98 | UpdatedAt time.Time 99 | } 100 | ``` 101 | 102 | Example scan: 103 | ```go 104 | p := Product{} 105 | row := tx.QueryRow(ctx, `SELECT id, name, price, created_at, updated_at FROM products WHERE id = $1`, id) 106 | err := row.Scan(&p.ID, &p.Name, &p.Price, &p.CreatedAt, &p.UpdatedAt) 107 | ``` 108 | 109 | See our [database integration notes](https://github.com/bojanz/currency/wiki/Database-integration-notes) for other examples (MySQL/MariaDB, SQLite). 110 | -------------------------------------------------------------------------------- /currency.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package currency handles currency amounts, provides currency information and formatting. 5 | package currency 6 | 7 | import ( 8 | "slices" 9 | "sort" 10 | ) 11 | 12 | // DefaultDigits is a placeholder for each currency's number of fraction digits. 13 | const DefaultDigits uint8 = 255 14 | 15 | // ForCountryCode returns the currency code for a country code. 16 | func ForCountryCode(countryCode string) (currencyCode string, ok bool) { 17 | currencyCode, ok = countryCurrencies[countryCode] 18 | 19 | return currencyCode, ok 20 | } 21 | 22 | // GetCurrencyCodes returns all known currency codes. 23 | func GetCurrencyCodes() []string { 24 | return currencyCodes 25 | } 26 | 27 | // IsValid checks whether a currency code is valid. 28 | // 29 | // An empty currency code is considered valid. 30 | func IsValid(currencyCode string) bool { 31 | if currencyCode == "" { 32 | return true 33 | } 34 | _, ok := currencies[currencyCode] 35 | 36 | return ok 37 | } 38 | 39 | // GetNumericCode returns the numeric code for a currency code. 40 | func GetNumericCode(currencyCode string) (numericCode string, ok bool) { 41 | if currencyCode == "" || !IsValid(currencyCode) { 42 | return "000", false 43 | } 44 | return currencies[currencyCode].numericCode, true 45 | } 46 | 47 | // GetDigits returns the number of fraction digits for a currency code. 48 | func GetDigits(currencyCode string) (digits uint8, ok bool) { 49 | if currencyCode == "" || !IsValid(currencyCode) { 50 | return 0, false 51 | } 52 | return currencies[currencyCode].digits, true 53 | } 54 | 55 | // GetSymbol returns the symbol for a currency code. 56 | func GetSymbol(currencyCode string, locale Locale) (symbol string, ok bool) { 57 | if currencyCode == "" || !IsValid(currencyCode) { 58 | return currencyCode, false 59 | } 60 | symbols, ok := currencySymbols[currencyCode] 61 | if !ok { 62 | return currencyCode, true 63 | } 64 | enLocale := Locale{Language: "en"} 65 | enUSLocale := Locale{Language: "en", Territory: "US"} 66 | if locale == enLocale || locale == enUSLocale || locale.IsEmpty() { 67 | // The "en"/"en-US" symbol is always first. 68 | return symbols[0].symbol, true 69 | } 70 | 71 | for { 72 | localeID := locale.String() 73 | for _, s := range symbols { 74 | if contains(s.locales, localeID) { 75 | symbol = s.symbol 76 | break 77 | } 78 | } 79 | if symbol != "" { 80 | break 81 | } 82 | locale = locale.GetParent() 83 | if locale.IsEmpty() { 84 | break 85 | } 86 | } 87 | 88 | return symbol, true 89 | } 90 | 91 | // getFormat returns the format for a locale. 92 | func getFormat(locale Locale) currencyFormat { 93 | // CLDR considers "en" and "en-US" to be equivalent. 94 | // Fall back immediately for better performance 95 | enUSLocale := Locale{Language: "en", Territory: "US"} 96 | if locale == enUSLocale || locale.IsEmpty() { 97 | return currencyFormats["en"] 98 | } 99 | 100 | var format currencyFormat 101 | for { 102 | localeID := locale.String() 103 | if cf, ok := currencyFormats[localeID]; ok { 104 | format = cf 105 | break 106 | } 107 | locale = locale.GetParent() 108 | if locale.IsEmpty() { 109 | break 110 | } 111 | } 112 | 113 | return format 114 | } 115 | 116 | // contains returns whether the sorted slice a contains x. 117 | // The slice must be sorted in ascending order. 118 | func contains(a []string, x string) bool { 119 | if n := len(a); n > 7 { 120 | i := sort.SearchStrings(a, x) 121 | return i < n && a[i] == x 122 | } 123 | return slices.Contains(a, x) 124 | } 125 | 126 | // Definition contains information for registering a currency. 127 | type Definition struct { 128 | // NumericCode is a three-digit code such as "999". 129 | NumericCode string 130 | 131 | // Digits is the number of fraction digits. 132 | Digits uint8 133 | 134 | // DefaultSymbol is the default symbol, used for all locales. 135 | // 136 | // When overriding an existing currency, keep DefaultSymbol 137 | // empty to retain the built-in locale-specific symbols. 138 | DefaultSymbol string 139 | } 140 | 141 | // Register adds a currency to the internal registry. 142 | // 143 | // This can be a non-ISO currency such as BTC, or an existing 144 | // currency for which we want to override the predefined data. 145 | func Register(currencyCode string, d Definition) { 146 | if currencyCode == "" { 147 | return 148 | } 149 | 150 | currencies[currencyCode] = currencyInfo{ 151 | numericCode: d.NumericCode, 152 | digits: d.Digits, 153 | } 154 | currencyCodes = append(currencyCodes, currencyCode) 155 | 156 | if d.DefaultSymbol != "" { 157 | currencySymbols[currencyCode] = []symbolInfo{ 158 | {symbol: d.DefaultSymbol, locales: []string{"en"}}, 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /locale_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | package currency_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/bojanz/currency" 10 | ) 11 | 12 | func TestNewLocale(t *testing.T) { 13 | tests := []struct { 14 | id string 15 | want currency.Locale 16 | }{ 17 | {"", currency.Locale{}}, 18 | {"de", currency.Locale{Language: "de"}}, 19 | {"de-CH", currency.Locale{Language: "de", Territory: "CH"}}, 20 | {"es-419", currency.Locale{Language: "es", Territory: "419"}}, 21 | {"sr-Cyrl", currency.Locale{Language: "sr", Script: "Cyrl"}}, 22 | {"sr-Latn-RS", currency.Locale{Language: "sr", Script: "Latn", Territory: "RS"}}, 23 | {"yue-Hans", currency.Locale{Language: "yue", Script: "Hans"}}, 24 | // ID with extra spacing. 25 | {" yue-Hans ", currency.Locale{Language: "yue", Script: "Hans"}}, 26 | // ID with the wrong case, ordering, delimeter. 27 | {"SR_rs_LATN", currency.Locale{Language: "sr", Script: "Latn", Territory: "RS"}}, 28 | // ID with a variant. Variants are unsupported and ignored. 29 | {"ca-ES-VALENCIA", currency.Locale{Language: "ca", Territory: "ES"}}, 30 | } 31 | for _, tt := range tests { 32 | t.Run(tt.id, func(t *testing.T) { 33 | got := currency.NewLocale(tt.id) 34 | if got != tt.want { 35 | t.Errorf("got %v, want %v", got, tt.want) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestLocale_String(t *testing.T) { 42 | tests := []struct { 43 | locale currency.Locale 44 | want string 45 | }{ 46 | {currency.Locale{}, ""}, 47 | {currency.Locale{Language: "de"}, "de"}, 48 | {currency.Locale{Language: "de", Territory: "CH"}, "de-CH"}, 49 | {currency.Locale{Language: "sr", Script: "Cyrl"}, "sr-Cyrl"}, 50 | {currency.Locale{Language: "sr", Script: "Latn", Territory: "RS"}, "sr-Latn-RS"}, 51 | } 52 | for _, tt := range tests { 53 | t.Run("", func(t *testing.T) { 54 | id := tt.locale.String() 55 | if id != tt.want { 56 | t.Errorf("got %v, want %v", id, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestLocale_MarshalText(t *testing.T) { 63 | tests := []struct { 64 | locale currency.Locale 65 | want string 66 | }{ 67 | {currency.Locale{}, ""}, 68 | {currency.Locale{Language: "de"}, "de"}, 69 | {currency.Locale{Language: "de", Territory: "CH"}, "de-CH"}, 70 | {currency.Locale{Language: "sr", Script: "Cyrl"}, "sr-Cyrl"}, 71 | {currency.Locale{Language: "sr", Script: "Latn", Territory: "RS"}, "sr-Latn-RS"}, 72 | } 73 | for _, tt := range tests { 74 | t.Run("", func(t *testing.T) { 75 | b, _ := tt.locale.MarshalText() 76 | got := string(b) 77 | if got != tt.want { 78 | t.Errorf("got %v, want %v", got, tt.want) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestLocale_UnmarshalText(t *testing.T) { 85 | tests := []struct { 86 | id string 87 | want currency.Locale 88 | }{ 89 | {"", currency.Locale{}}, 90 | {"de", currency.Locale{Language: "de"}}, 91 | {"de-CH", currency.Locale{Language: "de", Territory: "CH"}}, 92 | {"sr-Cyrl", currency.Locale{Language: "sr", Script: "Cyrl"}}, 93 | {"sr-Latn-RS", currency.Locale{Language: "sr", Script: "Latn", Territory: "RS"}}, 94 | // ID with the wrong case, ordering, delimeter. 95 | {"SR_rs_LATN", currency.Locale{Language: "sr", Script: "Latn", Territory: "RS"}}, 96 | // ID with a variant. Variants are unsupported and ignored. 97 | {"ca-ES-VALENCIA", currency.Locale{Language: "ca", Territory: "ES"}}, 98 | } 99 | for _, tt := range tests { 100 | t.Run(tt.id, func(t *testing.T) { 101 | l := currency.Locale{} 102 | l.UnmarshalText([]byte(tt.id)) 103 | if l != tt.want { 104 | t.Errorf("got %v, want %v", l, tt.want) 105 | } 106 | }) 107 | } 108 | } 109 | 110 | func TestLocale_IsEmpty(t *testing.T) { 111 | tests := []struct { 112 | locale currency.Locale 113 | want bool 114 | }{ 115 | {currency.Locale{}, true}, 116 | {currency.Locale{Language: "de"}, false}, 117 | {currency.Locale{Language: "de", Territory: "CH"}, false}, 118 | {currency.Locale{Language: "sr", Script: "Cyrl"}, false}, 119 | {currency.Locale{Language: "sr", Script: "Latn", Territory: "RS"}, false}, 120 | } 121 | for _, tt := range tests { 122 | t.Run("", func(t *testing.T) { 123 | empty := tt.locale.IsEmpty() 124 | if empty != tt.want { 125 | t.Errorf("got %v, want %v", empty, tt.want) 126 | } 127 | }) 128 | } 129 | } 130 | 131 | func TestLocale_GetParent(t *testing.T) { 132 | tests := []struct { 133 | id string 134 | want currency.Locale 135 | }{ 136 | {"sr-Cyrl-RS", currency.Locale{Language: "sr", Script: "Cyrl"}}, 137 | {"sr-Cyrl", currency.Locale{Language: "sr"}}, 138 | {"sr", currency.Locale{Language: "en"}}, 139 | {"en", currency.Locale{}}, 140 | {"", currency.Locale{}}, 141 | // Locales with special parents. 142 | {"es-AR", currency.Locale{Language: "es", Territory: "419"}}, 143 | {"sr-Latn", currency.Locale{Language: "en"}}, 144 | } 145 | for _, tt := range tests { 146 | t.Run(tt.id, func(t *testing.T) { 147 | locale := currency.NewLocale(tt.id) 148 | parent := locale.GetParent() 149 | if parent != tt.want { 150 | t.Errorf("got %v, want %v", parent, tt.want) 151 | } 152 | }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /currency_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | package currency_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/bojanz/currency" 10 | ) 11 | 12 | func TestForCountryCode(t *testing.T) { 13 | tests := []struct { 14 | countryCode string 15 | wantCurrencyCode string 16 | wantOK bool 17 | }{ 18 | {"FR", "EUR", true}, 19 | {"RS", "RSD", true}, 20 | {"XX", "", false}, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run("", func(t *testing.T) { 25 | gotCurrencyCode, gotOK := currency.ForCountryCode(tt.countryCode) 26 | if gotOK != tt.wantOK { 27 | t.Errorf("got %v, want %v", gotOK, tt.wantOK) 28 | } 29 | if gotCurrencyCode != tt.wantCurrencyCode { 30 | t.Errorf("got %q, want %q", gotCurrencyCode, tt.wantCurrencyCode) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func TestGetCurrencyCodes(t *testing.T) { 37 | currencyCodes := currency.GetCurrencyCodes() 38 | var got [10]string 39 | copy(got[:], currencyCodes[0:10]) 40 | want := [10]string{"AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "NOK", "NZD", "SEK", "USD"} 41 | // Confirm that the first 10 currency codes are the "G10" ones. 42 | if got != want { 43 | t.Errorf("got %v, want %v", got, want) 44 | } 45 | } 46 | 47 | func TestIsValid(t *testing.T) { 48 | tests := []struct { 49 | currencyCode string 50 | want bool 51 | }{ 52 | {"", true}, 53 | {"INVALID", false}, 54 | {"XXX", false}, 55 | {"usd", false}, 56 | {"USD", true}, 57 | {"EUR", true}, 58 | } 59 | 60 | for _, tt := range tests { 61 | t.Run("", func(t *testing.T) { 62 | got := currency.IsValid(tt.currencyCode) 63 | if got != tt.want { 64 | t.Errorf("got %v, want %v", got, tt.want) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func TestGetNumericCode(t *testing.T) { 71 | numericCode, ok := currency.GetNumericCode("USD") 72 | if !ok { 73 | t.Errorf("got %v, want true", ok) 74 | } 75 | if numericCode != "840" { 76 | t.Errorf("got %v, want 840", numericCode) 77 | } 78 | 79 | // Non-existent currency code. 80 | numericCode, ok = currency.GetNumericCode("XXX") 81 | if ok { 82 | t.Errorf("got %v, want false", ok) 83 | } 84 | if numericCode != "000" { 85 | t.Errorf("got %v, want 000", numericCode) 86 | } 87 | } 88 | 89 | func TestGetDigits(t *testing.T) { 90 | digits, ok := currency.GetDigits("USD") 91 | if !ok { 92 | t.Errorf("got %v, want true", ok) 93 | } 94 | if digits != 2 { 95 | t.Errorf("got %v, want 2", digits) 96 | } 97 | 98 | // Non-existent currency code. 99 | digits, ok = currency.GetDigits("XXX") 100 | if ok { 101 | t.Errorf("got %v, want false", ok) 102 | } 103 | if digits != 0 { 104 | t.Errorf("got %v, want 0", digits) 105 | } 106 | } 107 | 108 | func TestGetSymbol(t *testing.T) { 109 | tests := []struct { 110 | currencyCode string 111 | locale currency.Locale 112 | wantSymbol string 113 | wantOk bool 114 | }{ 115 | {"XXX", currency.NewLocale("en"), "XXX", false}, 116 | {"usd", currency.NewLocale("en"), "usd", false}, 117 | {"CHF", currency.NewLocale("en"), "CHF", true}, 118 | {"USD", currency.NewLocale("en"), "$", true}, 119 | {"USD", currency.NewLocale("en-US"), "$", true}, 120 | {"USD", currency.NewLocale("en-AU"), "US$", true}, 121 | {"USD", currency.NewLocale("es"), "US$", true}, 122 | {"USD", currency.NewLocale("es-ES"), "US$", true}, 123 | // An empty locale should use "en" data. 124 | {"USD", currency.NewLocale(""), "$", true}, 125 | } 126 | 127 | for _, tt := range tests { 128 | t.Run("", func(t *testing.T) { 129 | gotSymbol, gotOk := currency.GetSymbol(tt.currencyCode, tt.locale) 130 | if gotSymbol != tt.wantSymbol { 131 | t.Errorf("got %v, want %v", gotSymbol, tt.wantSymbol) 132 | } 133 | if gotOk != tt.wantOk { 134 | t.Errorf("got %v, want %v", gotOk, tt.wantOk) 135 | } 136 | }) 137 | } 138 | } 139 | 140 | func Test_Register_NoCurrencyCode(t *testing.T) { 141 | currency.Register("", currency.Definition{ 142 | NumericCode: "123", 143 | }) 144 | 145 | numericCode, ok := currency.GetNumericCode("") 146 | if ok { 147 | t.Errorf("got %v, want false", ok) 148 | } 149 | if numericCode != "000" { 150 | t.Errorf("got %v, want 000", numericCode) 151 | } 152 | } 153 | 154 | func Test_Register_New(t *testing.T) { 155 | currency.Register("BTC", currency.Definition{ 156 | NumericCode: "1000", 157 | Digits: 8, 158 | DefaultSymbol: "₿", 159 | }) 160 | 161 | if !currency.IsValid("BTC") { 162 | t.Errorf("got false, want true") 163 | } 164 | numericCode, ok := currency.GetNumericCode("BTC") 165 | if !ok { 166 | t.Errorf("got %v, want true", ok) 167 | } 168 | if numericCode != "1000" { 169 | t.Errorf("got %v, want 1000", numericCode) 170 | } 171 | 172 | d, ok := currency.GetDigits("BTC") 173 | if !ok { 174 | t.Errorf("got %v, want true", ok) 175 | } 176 | if d != 8 { 177 | t.Errorf("got %v, want 8", d) 178 | } 179 | 180 | symbol, _ := currency.GetSymbol("BTC", currency.NewLocale("en")) 181 | if symbol != "₿" { 182 | t.Errorf("got %v, want ₿", symbol) 183 | } 184 | symbol, _ = currency.GetSymbol("BTC", currency.NewLocale("es-ES")) 185 | if symbol != "₿" { 186 | t.Errorf("got %v, want ₿", symbol) 187 | } 188 | } 189 | 190 | func Test_Register_OverrideExisting(t *testing.T) { 191 | currency.Register("CAD", currency.Definition{ 192 | NumericCode: "125", 193 | Digits: 3, 194 | }) 195 | 196 | if !currency.IsValid("CAD") { 197 | t.Errorf("got false, want true") 198 | } 199 | numericCode, ok := currency.GetNumericCode("CAD") 200 | if !ok { 201 | t.Errorf("got %v, want true", ok) 202 | } 203 | if numericCode != "125" { 204 | t.Errorf("got %v, want 125", numericCode) 205 | } 206 | 207 | d, ok := currency.GetDigits("CAD") 208 | if !ok { 209 | t.Errorf("got %v, want true", ok) 210 | } 211 | if d != 3 { 212 | t.Errorf("got %v, want 3", d) 213 | } 214 | 215 | symbol, _ := currency.GetSymbol("CAD", currency.NewLocale("en")) 216 | if symbol != "CA$" { 217 | t.Errorf("got %v, want CA$", symbol) 218 | } 219 | symbol, _ = currency.GetSymbol("CAD", currency.NewLocale("fr")) 220 | if symbol != "$CA" { 221 | t.Errorf("got %v, want $CA", symbol) 222 | } 223 | 224 | // Override the symbols. 225 | currency.Register("CAD", currency.Definition{ 226 | NumericCode: "125", 227 | Digits: 3, 228 | DefaultSymbol: "$$", 229 | }) 230 | 231 | symbol, _ = currency.GetSymbol("CAD", currency.NewLocale("en")) 232 | if symbol != "$$" { 233 | t.Errorf("got %v, want $$", symbol) 234 | } 235 | symbol, _ = currency.GetSymbol("CAD", currency.NewLocale("fr")) 236 | if symbol != "$$" { 237 | t.Errorf("got %v, want $$", symbol) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | package currency_test 5 | 6 | import ( 7 | "fmt" 8 | "strconv" 9 | 10 | "github.com/bojanz/currency" 11 | ) 12 | 13 | func ExampleNewAmount() { 14 | amount, _ := currency.NewAmount("24.49", "USD") 15 | fmt.Println(amount) 16 | fmt.Println(amount.Number()) 17 | fmt.Println(amount.CurrencyCode()) 18 | // Output: 24.49 USD 19 | // 24.49 20 | // USD 21 | } 22 | 23 | func ExampleNewAmountFromInt64() { 24 | firstAmount, _ := currency.NewAmountFromInt64(2449, "USD") 25 | secondAmount, _ := currency.NewAmountFromInt64(5000, "USD") 26 | thirdAmount, _ := currency.NewAmountFromInt64(60, "JPY") 27 | fmt.Println(firstAmount) 28 | fmt.Println(secondAmount) 29 | fmt.Println(thirdAmount) 30 | // Output: 24.49 USD 31 | // 50.00 USD 32 | // 60 JPY 33 | } 34 | 35 | func ExampleAmount_Int64() { 36 | firstAmount, _ := currency.NewAmount("24.49", "USD") 37 | secondAmount, _ := currency.NewAmount("50", "USD") 38 | thirdAmount, _ := currency.NewAmount("60", "JPY") 39 | firstInt, _ := firstAmount.Int64() 40 | secondInt, _ := secondAmount.Int64() 41 | thirdInt, _ := thirdAmount.Int64() 42 | fmt.Println(firstInt, secondInt, thirdInt) 43 | // Output: 2449 5000 60 44 | } 45 | 46 | func ExampleAmount_Convert() { 47 | amount, _ := currency.NewAmount("20.99", "USD") 48 | amount, _ = amount.Convert("EUR", "0.91") 49 | fmt.Println(amount) 50 | fmt.Println(amount.Round()) 51 | // Output: 19.1009 EUR 52 | // 19.10 EUR 53 | } 54 | 55 | func ExampleAmount_Add() { 56 | firstAmount, _ := currency.NewAmount("20.99", "USD") 57 | secondAmount, _ := currency.NewAmount("3.50", "USD") 58 | totalAmount, _ := firstAmount.Add(secondAmount) 59 | fmt.Println(totalAmount) 60 | // Output: 24.49 USD 61 | } 62 | 63 | func ExampleAmount_Add_sum() { 64 | // Any currency.Amount can be added to the zero value. 65 | var sum currency.Amount 66 | for i := 0; i <= 4; i++ { 67 | a, _ := currency.NewAmount(strconv.Itoa(i), "AUD") 68 | sum, _ = sum.Add(a) 69 | } 70 | 71 | fmt.Println(sum) // 0 + 1 + 2 + 3 + 4 = 10 72 | // Output: 10 AUD 73 | } 74 | 75 | func ExampleAmount_Sub() { 76 | baseAmount, _ := currency.NewAmount("20.99", "USD") 77 | discountAmount, _ := currency.NewAmount("5.00", "USD") 78 | amount, _ := baseAmount.Sub(discountAmount) 79 | fmt.Println(amount) 80 | // Output: 15.99 USD 81 | } 82 | 83 | func ExampleAmount_Sub_diff() { 84 | // Any currency.Amount can be subtracted from the zero value. 85 | var diff currency.Amount 86 | for i := 0; i <= 4; i++ { 87 | a, _ := currency.NewAmount(strconv.Itoa(i), "AUD") 88 | diff, _ = diff.Sub(a) 89 | } 90 | 91 | fmt.Println(diff) // 0 - 1 - 2 - 3 - 4 = -10 92 | // Output: -10 AUD 93 | } 94 | 95 | func ExampleAmount_Mul() { 96 | amount, _ := currency.NewAmount("20.99", "USD") 97 | taxAmount, _ := amount.Mul("0.20") 98 | fmt.Println(taxAmount) 99 | fmt.Println(taxAmount.Round()) 100 | // Output: 4.1980 USD 101 | // 4.20 USD 102 | } 103 | 104 | func ExampleAmount_Div() { 105 | totalAmount, _ := currency.NewAmount("99.99", "USD") 106 | amount, _ := totalAmount.Div("3") 107 | fmt.Println(amount) 108 | // Output: 33.33 USD 109 | } 110 | 111 | func ExampleAmount_Round() { 112 | firstAmount, _ := currency.NewAmount("12.345", "USD") 113 | secondAmount, _ := currency.NewAmount("12.345", "JPY") 114 | fmt.Println(firstAmount.Round()) 115 | fmt.Println(secondAmount.Round()) 116 | // Output: 12.35 USD 117 | // 12 JPY 118 | } 119 | 120 | func ExampleAmount_RoundTo() { 121 | amount, _ := currency.NewAmount("12.345", "USD") 122 | for _, digits := range []uint8{4, 3, 2, 1, 0} { 123 | fmt.Println(amount.RoundTo(digits, currency.RoundHalfUp)) 124 | } 125 | // Output: 12.3450 USD 126 | // 12.345 USD 127 | // 12.35 USD 128 | // 12.3 USD 129 | // 12 USD 130 | } 131 | 132 | func ExampleNewLocale() { 133 | firstLocale := currency.NewLocale("en-US") 134 | fmt.Println(firstLocale) 135 | fmt.Println(firstLocale.Language, firstLocale.Territory) 136 | 137 | // Locale IDs are normalized. 138 | secondLocale := currency.NewLocale("sr_rs_latn") 139 | fmt.Println(secondLocale) 140 | fmt.Println(secondLocale.Language, secondLocale.Script, secondLocale.Territory) 141 | // Output: en-US 142 | // en US 143 | // sr-Latn-RS 144 | // sr Latn RS 145 | } 146 | 147 | func ExampleLocale_GetParent() { 148 | locale := currency.NewLocale("sr-Cyrl-RS") 149 | for { 150 | fmt.Println(locale) 151 | locale = locale.GetParent() 152 | if locale.IsEmpty() { 153 | break 154 | } 155 | } 156 | // Output: sr-Cyrl-RS 157 | // sr-Cyrl 158 | // sr 159 | // en 160 | } 161 | 162 | func ExampleFormatter_Format() { 163 | locale := currency.NewLocale("tr") 164 | formatter := currency.NewFormatter(locale) 165 | amount, _ := currency.NewAmount("1245.988", "EUR") 166 | fmt.Println(formatter.Format(amount)) 167 | 168 | formatter.MaxDigits = 2 169 | fmt.Println(formatter.Format(amount)) 170 | 171 | formatter.NoGrouping = true 172 | amount, _ = currency.NewAmount("1245", "EUR") 173 | fmt.Println(formatter.Format(amount)) 174 | 175 | formatter.MinDigits = 0 176 | fmt.Println(formatter.Format(amount)) 177 | 178 | formatter.CurrencyDisplay = currency.DisplayNone 179 | fmt.Println(formatter.Format(amount)) 180 | // Output: €1.245,988 181 | // €1.245,99 182 | // €1245,00 183 | // €1245 184 | // 1245 185 | } 186 | 187 | func ExampleFormatter_Parse() { 188 | locale := currency.NewLocale("tr") 189 | formatter := currency.NewFormatter(locale) 190 | 191 | amount, _ := formatter.Parse("€1.234,59", "EUR") 192 | fmt.Println(amount) 193 | 194 | amount, _ = formatter.Parse("EUR 1.234,59", "EUR") 195 | fmt.Println(amount) 196 | 197 | amount, _ = formatter.Parse("1.234,59", "EUR") 198 | fmt.Println(amount) 199 | // Output: 1234.59 EUR 200 | // 1234.59 EUR 201 | // 1234.59 EUR 202 | } 203 | 204 | func ExampleForCountryCode() { 205 | currencyCode, ok := currency.ForCountryCode("US") 206 | fmt.Println(currencyCode, ok) 207 | 208 | currencyCode, ok = currency.ForCountryCode("FR") 209 | fmt.Println(currencyCode, ok) 210 | 211 | // Non-existent country code. 212 | _, ok = currency.ForCountryCode("XX") 213 | fmt.Println(ok) 214 | // Output: USD true 215 | // EUR true 216 | // false 217 | } 218 | 219 | func ExampleGetNumericCode() { 220 | numericCode, ok := currency.GetNumericCode("USD") 221 | fmt.Println(numericCode, ok) 222 | 223 | // Non-existent currency code. 224 | numericCode, ok = currency.GetNumericCode("XXX") 225 | fmt.Println(numericCode, ok) 226 | // Output: 840 true 227 | // 000 false 228 | } 229 | 230 | func ExampleGetDigits() { 231 | digits, ok := currency.GetDigits("USD") 232 | fmt.Println(digits, ok) 233 | 234 | // Non-existent currency code. 235 | digits, ok = currency.GetDigits("XXX") 236 | fmt.Println(digits, ok) 237 | // Output: 2 true 238 | // 0 false 239 | } 240 | 241 | func ExampleGetSymbol() { 242 | locale := currency.NewLocale("en") 243 | symbol, ok := currency.GetSymbol("USD", locale) 244 | fmt.Println(symbol, ok) 245 | 246 | // Non-existent currency code. 247 | symbol, ok = currency.GetSymbol("XXX", locale) 248 | fmt.Println(symbol, ok) 249 | // Output: $ true 250 | // XXX false 251 | } 252 | 253 | func ExampleRegister() { 254 | locale := currency.NewLocale("fr") 255 | 256 | // Add a custom currency. 257 | currency.Register("BTC", currency.Definition{ 258 | NumericCode: "1000", 259 | Digits: 8, 260 | DefaultSymbol: "₿", 261 | }) 262 | valid := currency.IsValid("BTC") 263 | fmt.Println(valid) 264 | 265 | // Override an existing currency, keeping the built-in symbols. 266 | currency.Register("AUD", currency.Definition{ 267 | NumericCode: "036", 268 | Digits: 3, 269 | }) 270 | digits, _ := currency.GetDigits("AUD") 271 | symbol, _ := currency.GetSymbol("AUD", locale) 272 | fmt.Println(digits, symbol) 273 | 274 | // Override an existing currency and its symbols. 275 | currency.Register("AUD", currency.Definition{ 276 | NumericCode: "036", 277 | Digits: 3, 278 | DefaultSymbol: "$$", 279 | }) 280 | digits, _ = currency.GetDigits("AUD") 281 | symbol, _ = currency.GetSymbol("AUD", locale) 282 | fmt.Println(digits, symbol) 283 | 284 | // Output: true 285 | // 3 $AU 286 | // 3 $$ 287 | } 288 | -------------------------------------------------------------------------------- /formatter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | package currency 5 | 6 | import ( 7 | "strconv" 8 | "strings" 9 | "unicode" 10 | "unicode/utf8" 11 | ) 12 | 13 | // Display represents the currency display type. 14 | type Display uint8 15 | 16 | const ( 17 | // DisplaySymbol shows the currency symbol. 18 | DisplaySymbol Display = iota 19 | // DisplayCode shows the currency code. 20 | DisplayCode 21 | // DisplayNone shows nothing, hiding the currency. 22 | DisplayNone 23 | ) 24 | 25 | var localDigits = map[numberingSystem]string{ 26 | numArab: "٠١٢٣٤٥٦٧٨٩", 27 | numArabExt: "۰۱۲۳۴۵۶۷۸۹", 28 | numBeng: "০১২৩৪৫৬৭৮৯", 29 | numDeva: "०१२३४५६७८९", 30 | numMymr: "၀၁၂၃၄၅၆၇၈၉", 31 | } 32 | 33 | // Formatter formats and parses currency amounts. 34 | type Formatter struct { 35 | locale Locale 36 | format currencyFormat 37 | // AccountingStyle formats the amount using the accounting style. 38 | // For example, "-3.00 USD" in the "en" locale is formatted as "($3.00)" instead of "-$3.00". 39 | // Defaults to false. 40 | AccountingStyle bool 41 | // AddPlusSign inserts the plus sign in front of positive amounts. 42 | // Defaults to false. 43 | AddPlusSign bool 44 | // NoGrouping turns off grouping of major digits. 45 | // Defaults to false. 46 | NoGrouping bool 47 | // MinDigits specifies the minimum number of fraction digits. 48 | // All zeroes past the minimum will be removed (0 => no trailing zeroes). 49 | // Defaults to currency.DefaultDigits (e.g. 2 for USD, 0 for RSD). 50 | MinDigits uint8 51 | // MaxDigits specifies the maximum number of fraction digits. 52 | // Formatted amounts will be rounded to this number of digits. 53 | // Defaults to 6, so that most amounts are shown as-is (without rounding). 54 | MaxDigits uint8 55 | // RoundingMode specifies how the formatted amount will be rounded. 56 | // Defaults to currency.RoundHalfUp. 57 | RoundingMode RoundingMode 58 | // CurrencyDisplay specifies how the currency will be displayed (symbol/code/none). 59 | // Defaults to currency.DisplaySymbol. 60 | CurrencyDisplay Display 61 | // SymbolMap specifies custom symbols for individual currency codes. 62 | // For example, "USD": "$" means that the $ symbol will be used even if 63 | // the current locale's symbol is different ("US$", "$US", etc). 64 | SymbolMap map[string]string 65 | } 66 | 67 | // NewFormatter creates a new formatter for the given locale. 68 | func NewFormatter(locale Locale) *Formatter { 69 | f := &Formatter{ 70 | locale: locale, 71 | format: getFormat(locale), 72 | MinDigits: DefaultDigits, 73 | MaxDigits: 6, 74 | RoundingMode: RoundHalfUp, 75 | CurrencyDisplay: DisplaySymbol, 76 | SymbolMap: make(map[string]string), 77 | } 78 | return f 79 | } 80 | 81 | // Locale returns the locale. 82 | func (f *Formatter) Locale() Locale { 83 | return f.locale 84 | } 85 | 86 | // Format formats a currency amount. 87 | func (f *Formatter) Format(amount Amount) string { 88 | pattern := f.getPattern(amount) 89 | if amount.IsNegative() { 90 | // The minus sign will be provided by the pattern. 91 | amount, _ = amount.Mul("-1") 92 | } 93 | formattedNumber := f.formatNumber(amount) 94 | formattedCurrency := f.formatCurrency(amount.CurrencyCode()) 95 | if formattedCurrency != "" { 96 | // CLDR requires having a space between the letters 97 | // in a currency symbol and adjacent numbers. 98 | if strings.Contains(pattern, "0¤") { 99 | r, _ := utf8.DecodeRuneInString(formattedCurrency) 100 | if unicode.IsLetter(r) { 101 | formattedCurrency = "\u00a0" + formattedCurrency 102 | } 103 | } else if strings.Contains(pattern, "¤0") { 104 | r, _ := utf8.DecodeLastRuneInString(formattedCurrency) 105 | if unicode.IsLetter(r) { 106 | formattedCurrency = formattedCurrency + "\u00a0" 107 | } 108 | } 109 | } 110 | 111 | replacements := []string{ 112 | "0.00", formattedNumber, 113 | "+", f.format.plusSign, 114 | "-", f.format.minusSign, 115 | } 116 | if formattedCurrency == "" { 117 | // Many patterns have a non-breaking space between 118 | // the number and currency, not needed in this case. 119 | replacements = append(replacements, "\u00a0¤", "", "¤\u00a0", "", "¤", "") 120 | } else { 121 | replacements = append(replacements, "¤", formattedCurrency) 122 | } 123 | r := strings.NewReplacer(replacements...) 124 | 125 | return r.Replace(pattern) 126 | } 127 | 128 | // Parse parses a formatted amount. 129 | func (f *Formatter) Parse(s, currencyCode string) (Amount, error) { 130 | symbol, _ := GetSymbol(currencyCode, f.locale) 131 | replacements := []string{ 132 | f.format.decimalSeparator, ".", 133 | f.format.groupingSeparator, "", 134 | f.format.plusSign, "+", 135 | f.format.minusSign, "-", 136 | symbol, "", 137 | currencyCode, "", 138 | "\u200e", "", 139 | "\u200f", "", 140 | "\u00a0", "", 141 | " ", "", 142 | } 143 | if f.format.numberingSystem != numLatn { 144 | digits := localDigits[f.format.numberingSystem] 145 | for i, v := range strings.Split(digits, "") { 146 | replacements = append(replacements, v, strconv.Itoa(i)) 147 | } 148 | } 149 | if f.AccountingStyle { 150 | replacements = append(replacements, "(", "-", ")", "") 151 | } 152 | r := strings.NewReplacer(replacements...) 153 | n := r.Replace(s) 154 | 155 | return NewAmount(n, currencyCode) 156 | } 157 | 158 | // getPattern returns a positive or negative pattern for a currency amount. 159 | func (f *Formatter) getPattern(amount Amount) string { 160 | var patterns []string 161 | if f.usesAccountingPattern() { 162 | patterns = strings.Split(f.format.accountingPattern, ";") 163 | } else { 164 | patterns = strings.Split(f.format.standardPattern, ";") 165 | } 166 | 167 | switch { 168 | case amount.IsNegative(): 169 | if len(patterns) == 1 { 170 | return "-" + patterns[0] 171 | } 172 | return patterns[1] 173 | case f.AddPlusSign: 174 | if len(patterns) == 1 || f.usesAccountingPattern() { 175 | return "+" + patterns[0] 176 | } 177 | return strings.Replace(patterns[1], "-", "+", 1) 178 | default: 179 | return patterns[0] 180 | } 181 | } 182 | 183 | // usesAccountingPattern returns whether the formatter needs to use the accounting pattern. 184 | func (f *Formatter) usesAccountingPattern() bool { 185 | return f.AccountingStyle && f.format.accountingPattern != "" 186 | } 187 | 188 | // formatNumber formats the number for display. 189 | func (f *Formatter) formatNumber(amount Amount) string { 190 | minDigits := f.MinDigits 191 | if minDigits == DefaultDigits { 192 | minDigits, _ = GetDigits(amount.CurrencyCode()) 193 | } 194 | maxDigits := f.MaxDigits 195 | if maxDigits == DefaultDigits { 196 | maxDigits, _ = GetDigits(amount.CurrencyCode()) 197 | } 198 | amount = amount.RoundTo(maxDigits, f.RoundingMode) 199 | numberParts := strings.Split(amount.Number(), ".") 200 | majorDigits := f.groupMajorDigits(numberParts[0]) 201 | minorDigits := "" 202 | if len(numberParts) == 2 { 203 | minorDigits = numberParts[1] 204 | } 205 | if minDigits < maxDigits { 206 | // Strip any trailing zeroes. 207 | minorDigits = strings.TrimRight(minorDigits, "0") 208 | if len(minorDigits) < int(minDigits) { 209 | // Now there are too few digits, re-add trailing zeroes 210 | // until minDigits is reached. 211 | minorDigits += strings.Repeat("0", int(minDigits)-len(minorDigits)) 212 | } 213 | } 214 | b := strings.Builder{} 215 | b.WriteString(majorDigits) 216 | if minorDigits != "" { 217 | b.WriteString(f.format.decimalSeparator) 218 | b.WriteString(minorDigits) 219 | } 220 | formatted := f.localizeDigits(b.String()) 221 | 222 | return formatted 223 | } 224 | 225 | // formatCurrency formats the currency for display. 226 | func (f *Formatter) formatCurrency(currencyCode string) string { 227 | var formatted string 228 | switch f.CurrencyDisplay { 229 | case DisplaySymbol: 230 | if symbol, ok := f.SymbolMap[currencyCode]; ok { 231 | formatted = symbol 232 | } else { 233 | formatted, _ = GetSymbol(currencyCode, f.locale) 234 | } 235 | case DisplayCode: 236 | formatted = currencyCode 237 | default: 238 | formatted = "" 239 | } 240 | 241 | return formatted 242 | } 243 | 244 | // groupMajorDigits groups major digits according to the currency format. 245 | func (f *Formatter) groupMajorDigits(majorDigits string) string { 246 | if f.NoGrouping || f.format.primaryGroupingSize == 0 { 247 | return majorDigits 248 | } 249 | numDigits := len(majorDigits) 250 | minDigits := int(f.format.minGroupingDigits) 251 | primarySize := int(f.format.primaryGroupingSize) 252 | secondarySize := int(f.format.secondaryGroupingSize) 253 | if numDigits < (minDigits + primarySize) { 254 | return majorDigits 255 | } 256 | 257 | // Digits are grouped from right to left. 258 | // First the primary group, then the secondary groups. 259 | var groups []string 260 | groups = append(groups, majorDigits[numDigits-primarySize:numDigits]) 261 | for i := numDigits - primarySize; i > 0; i = i - secondarySize { 262 | low := i - secondarySize 263 | if low < 0 { 264 | low = 0 265 | } 266 | groups = append(groups, majorDigits[low:i]) 267 | } 268 | // Reverse the groups and reconstruct the digits. 269 | for i, j := 0, len(groups)-1; i < j; i, j = i+1, j-1 { 270 | groups[i], groups[j] = groups[j], groups[i] 271 | } 272 | majorDigits = strings.Join(groups, f.format.groupingSeparator) 273 | 274 | return majorDigits 275 | } 276 | 277 | // localizeDigits replaces digits with their localized equivalents. 278 | func (f *Formatter) localizeDigits(number string) string { 279 | if f.format.numberingSystem == numLatn { 280 | return number 281 | } 282 | digits := localDigits[f.format.numberingSystem] 283 | replacements := make([]string, 0, 20) 284 | for i, v := range strings.Split(digits, "") { 285 | replacements = append(replacements, strconv.Itoa(i), v) 286 | } 287 | r := strings.NewReplacer(replacements...) 288 | number = r.Replace(number) 289 | 290 | return number 291 | } 292 | -------------------------------------------------------------------------------- /amount.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | package currency 5 | 6 | import ( 7 | "bytes" 8 | "database/sql/driver" 9 | "encoding/json" 10 | "fmt" 11 | "math/big" 12 | "strings" 13 | 14 | "github.com/cockroachdb/apd/v3" 15 | ) 16 | 17 | // RoundingMode determines how the amount will be rounded. 18 | type RoundingMode uint8 19 | 20 | const ( 21 | // RoundHalfUp rounds up if the next digit is >= 5. 22 | RoundHalfUp RoundingMode = iota 23 | // RoundHalfDown rounds up if the next digit is > 5. 24 | RoundHalfDown 25 | // RoundUp rounds away from 0. 26 | RoundUp 27 | // RoundDown rounds towards 0, truncating extra digits. 28 | RoundDown 29 | // RoundHalfEven rounds up if the next digit is > 5. If the next digit is equal 30 | // to 5, it rounds to the nearest even decimal. Also called bankers' rounding. 31 | RoundHalfEven 32 | ) 33 | 34 | // InvalidNumberError is returned when a numeric string can't be converted to a decimal. 35 | type InvalidNumberError struct { 36 | Number string 37 | } 38 | 39 | func (e InvalidNumberError) Error() string { 40 | return fmt.Sprintf("invalid number %q", e.Number) 41 | } 42 | 43 | // InvalidCurrencyCodeError is returned when a currency code is invalid or unrecognized. 44 | type InvalidCurrencyCodeError struct { 45 | CurrencyCode string 46 | } 47 | 48 | func (e InvalidCurrencyCodeError) Error() string { 49 | return fmt.Sprintf("invalid currency code %q", e.CurrencyCode) 50 | } 51 | 52 | // MismatchError is returned when two amounts have mismatched currency codes. 53 | type MismatchError struct { 54 | A Amount 55 | B Amount 56 | } 57 | 58 | func (e MismatchError) Error() string { 59 | return fmt.Sprintf("amounts %q and %q have mismatched currency codes", e.A, e.B) 60 | } 61 | 62 | // Amount stores a decimal number with its currency code. 63 | type Amount struct { 64 | number apd.Decimal 65 | currencyCode string 66 | } 67 | 68 | // NewAmount creates a new Amount from a numeric string and a currency code. 69 | func NewAmount(n, currencyCode string) (Amount, error) { 70 | number := apd.Decimal{} 71 | if _, _, err := number.SetString(n); err != nil { 72 | return Amount{}, InvalidNumberError{n} 73 | } 74 | if currencyCode == "" || !IsValid(currencyCode) { 75 | return Amount{}, InvalidCurrencyCodeError{currencyCode} 76 | } 77 | 78 | return Amount{number, currencyCode}, nil 79 | } 80 | 81 | // NewAmountFromBigInt creates a new Amount from a big.Int and a currency code. 82 | func NewAmountFromBigInt(n *big.Int, currencyCode string) (Amount, error) { 83 | if n == nil { 84 | return Amount{}, InvalidNumberError{"nil"} 85 | } 86 | d, ok := GetDigits(currencyCode) 87 | if !ok { 88 | return Amount{}, InvalidCurrencyCodeError{currencyCode} 89 | } 90 | coeff := new(apd.BigInt).SetMathBigInt(n) 91 | number := apd.NewWithBigInt(coeff, -int32(d)) 92 | 93 | return Amount{*number, currencyCode}, nil 94 | } 95 | 96 | // NewAmountFromInt64 creates a new Amount from an int64 and a currency code. 97 | func NewAmountFromInt64(n int64, currencyCode string) (Amount, error) { 98 | d, ok := GetDigits(currencyCode) 99 | if !ok { 100 | return Amount{}, InvalidCurrencyCodeError{currencyCode} 101 | } 102 | number := apd.Decimal{} 103 | number.SetFinite(n, -int32(d)) 104 | 105 | return Amount{number, currencyCode}, nil 106 | } 107 | 108 | // Number returns the number as a numeric string. 109 | func (a Amount) Number() string { 110 | return a.number.String() 111 | } 112 | 113 | // CurrencyCode returns the currency code. 114 | func (a Amount) CurrencyCode() string { 115 | return a.currencyCode 116 | } 117 | 118 | // String returns the string representation of a. 119 | func (a Amount) String() string { 120 | return a.Number() + " " + a.CurrencyCode() 121 | } 122 | 123 | // BigInt returns a in minor units, as a big.Int. 124 | func (a Amount) BigInt() *big.Int { 125 | a = a.Round() 126 | n := a.number.Coeff.MathBigInt() 127 | if a.IsNegative() { 128 | // The coefficient is always positive, apd stores the sign separately. 129 | n = n.Neg(n) 130 | } 131 | 132 | return n 133 | } 134 | 135 | // Int64 returns a in minor units, as an int64. 136 | // If a cannot be represented in an int64, an error is returned. 137 | func (a Amount) Int64() (int64, error) { 138 | n := a.Round().number 139 | n.Exponent = 0 140 | 141 | return n.Int64() 142 | } 143 | 144 | // Convert converts a to a different currency. 145 | func (a Amount) Convert(currencyCode, rate string) (Amount, error) { 146 | if currencyCode == "" || !IsValid(currencyCode) { 147 | return Amount{}, InvalidCurrencyCodeError{currencyCode} 148 | } 149 | result := apd.Decimal{} 150 | if _, _, err := result.SetString(rate); err != nil { 151 | return Amount{}, InvalidNumberError{rate} 152 | } 153 | ctx := decimalContext(&a.number, &result) 154 | ctx.Mul(&result, &a.number, &result) 155 | 156 | return Amount{result, currencyCode}, nil 157 | } 158 | 159 | // Add adds a and b together and returns the result. 160 | func (a Amount) Add(b Amount) (Amount, error) { 161 | if a.currencyCode != b.currencyCode { 162 | if a.Equal(Amount{}) { 163 | return b, nil 164 | } 165 | if b.Equal(Amount{}) { 166 | return a, nil 167 | } 168 | return Amount{}, MismatchError{a, b} 169 | } 170 | result := apd.Decimal{} 171 | ctx := decimalContext(&a.number, &b.number) 172 | ctx.Add(&result, &a.number, &b.number) 173 | 174 | return Amount{result, a.currencyCode}, nil 175 | } 176 | 177 | // Sub subtracts b from a and returns the result. 178 | func (a Amount) Sub(b Amount) (Amount, error) { 179 | if a.currencyCode != b.currencyCode { 180 | if a.Equal(Amount{}) { 181 | // 0-b == -b 182 | var result apd.Decimal 183 | result.Neg(&b.number) 184 | return Amount{result, b.currencyCode}, nil 185 | } 186 | if b.Equal(Amount{}) { 187 | return a, nil 188 | } 189 | return Amount{}, MismatchError{a, b} 190 | } 191 | result := apd.Decimal{} 192 | ctx := decimalContext(&a.number, &b.number) 193 | ctx.Sub(&result, &a.number, &b.number) 194 | 195 | return Amount{result, a.currencyCode}, nil 196 | } 197 | 198 | // Mul multiplies a by n and returns the result. 199 | func (a Amount) Mul(n string) (Amount, error) { 200 | result := apd.Decimal{} 201 | if _, _, err := result.SetString(n); err != nil { 202 | return Amount{}, InvalidNumberError{n} 203 | } 204 | ctx := decimalContext(&a.number, &result) 205 | ctx.Mul(&result, &a.number, &result) 206 | 207 | return Amount{result, a.currencyCode}, nil 208 | } 209 | 210 | // Div divides a by n and returns the result. 211 | func (a Amount) Div(n string) (Amount, error) { 212 | result := apd.Decimal{} 213 | if _, _, err := result.SetString(n); err != nil { 214 | return Amount{}, InvalidNumberError{n} 215 | } 216 | if result.IsZero() { 217 | return Amount{}, InvalidNumberError{n} 218 | } 219 | ctx := decimalContext(&a.number, &result) 220 | ctx.Quo(&result, &a.number, &result) 221 | result.Reduce(&result) 222 | 223 | return Amount{result, a.currencyCode}, nil 224 | } 225 | 226 | // Round is a shortcut for RoundTo(currency.DefaultDigits, currency.RoundHalfUp). 227 | func (a Amount) Round() Amount { 228 | return a.RoundTo(DefaultDigits, RoundHalfUp) 229 | } 230 | 231 | // RoundTo rounds a to the given number of fraction digits. 232 | func (a Amount) RoundTo(digits uint8, mode RoundingMode) Amount { 233 | if digits == DefaultDigits { 234 | digits, _ = GetDigits(a.currencyCode) 235 | } 236 | 237 | result := apd.Decimal{} 238 | ctx := roundingContext(&a.number, mode) 239 | ctx.Quantize(&result, &a.number, -int32(digits)) 240 | 241 | return Amount{result, a.currencyCode} 242 | } 243 | 244 | // Cmp compares a and b and returns: 245 | // 246 | // -1 if a < b 247 | // 0 if a == b 248 | // +1 if a > b 249 | func (a Amount) Cmp(b Amount) (int, error) { 250 | if a.currencyCode != b.currencyCode { 251 | return -1, MismatchError{a, b} 252 | } 253 | return a.number.Cmp(&b.number), nil 254 | } 255 | 256 | // Equal returns whether a and b are equal. 257 | func (a Amount) Equal(b Amount) bool { 258 | if a.currencyCode != b.currencyCode { 259 | return false 260 | } 261 | return a.number.Cmp(&b.number) == 0 262 | } 263 | 264 | // IsPositive returns whether a is positive. 265 | func (a Amount) IsPositive() bool { 266 | zero := apd.New(0, 0) 267 | return a.number.Cmp(zero) == 1 268 | } 269 | 270 | // IsNegative returns whether a is negative. 271 | func (a Amount) IsNegative() bool { 272 | zero := apd.New(0, 0) 273 | return a.number.Cmp(zero) == -1 274 | } 275 | 276 | // IsZero returns whether a is zero. 277 | func (a Amount) IsZero() bool { 278 | zero := apd.New(0, 0) 279 | return a.number.Cmp(zero) == 0 280 | } 281 | 282 | // MarshalBinary implements the encoding.BinaryMarshaler interface. 283 | func (a Amount) MarshalBinary() ([]byte, error) { 284 | buf := bytes.Buffer{} 285 | buf.WriteString(a.CurrencyCode()) 286 | buf.WriteString(a.Number()) 287 | 288 | return buf.Bytes(), nil 289 | } 290 | 291 | // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. 292 | func (a *Amount) UnmarshalBinary(data []byte) error { 293 | if len(data) < 3 { 294 | return InvalidCurrencyCodeError{string(data)} 295 | } 296 | n := string(data[3:]) 297 | currencyCode := string(data[0:3]) 298 | number := apd.Decimal{} 299 | if _, _, err := number.SetString(n); err != nil { 300 | return InvalidNumberError{n} 301 | } 302 | if currencyCode == "" || !IsValid(currencyCode) { 303 | return InvalidCurrencyCodeError{currencyCode} 304 | } 305 | a.number = number 306 | a.currencyCode = currencyCode 307 | 308 | return nil 309 | } 310 | 311 | // MarshalJSON implements the json.Marshaler interface. 312 | func (a Amount) MarshalJSON() ([]byte, error) { 313 | return json.Marshal(&struct { 314 | Number string `json:"number"` 315 | CurrencyCode string `json:"currency"` 316 | }{ 317 | Number: a.Number(), 318 | CurrencyCode: a.CurrencyCode(), 319 | }) 320 | } 321 | 322 | // UnmarshalJSON implements the json.Unmarshaler interface. 323 | func (a *Amount) UnmarshalJSON(data []byte) error { 324 | aux := struct { 325 | Number json.RawMessage `json:"number"` 326 | CurrencyCode string `json:"currency"` 327 | }{} 328 | err := json.Unmarshal(data, &aux) 329 | if err != nil { 330 | return err 331 | } 332 | 333 | var auxNumber string 334 | if err = json.Unmarshal(aux.Number, &auxNumber); err != nil { 335 | auxNumber = string(aux.Number) 336 | } 337 | 338 | number := apd.Decimal{} 339 | if _, _, err := number.SetString(auxNumber); err != nil { 340 | return InvalidNumberError{auxNumber} 341 | } 342 | if aux.CurrencyCode == "" || !IsValid(aux.CurrencyCode) { 343 | return InvalidCurrencyCodeError{aux.CurrencyCode} 344 | } 345 | a.number = number 346 | a.currencyCode = aux.CurrencyCode 347 | 348 | return nil 349 | } 350 | 351 | // Value implements the database/driver.Valuer interface. 352 | // 353 | // Allows storing amounts in a PostgreSQL composite type. 354 | func (a Amount) Value() (driver.Value, error) { 355 | return fmt.Sprintf("(%v,%v)", a.Number(), a.CurrencyCode()), nil 356 | } 357 | 358 | // Scan implements the database/sql.Scanner interface. 359 | // 360 | // Allows scanning amounts from a PostgreSQL composite type. 361 | func (a *Amount) Scan(src interface{}) error { 362 | // Wire format: "(9.99,USD)". 363 | input, ok := src.(string) 364 | if !ok { 365 | return fmt.Errorf("value is not a string: %v", src) 366 | } 367 | if len(input) == 0 { 368 | return nil 369 | } 370 | input = strings.Trim(input, "()") 371 | values := strings.Split(input, ",") 372 | n := values[0] 373 | currencyCode := values[1] 374 | number := apd.Decimal{} 375 | if _, _, err := number.SetString(n); err != nil { 376 | return InvalidNumberError{n} 377 | } 378 | // Allow the zero value (number=0, currencyCode is empty). 379 | // An empty currencyCode consists of 3 spaces when stored in a char(3). 380 | if (currencyCode == "" || currencyCode == " ") && number.IsZero() { 381 | a.number = number 382 | a.currencyCode = "" 383 | return nil 384 | } 385 | if currencyCode == "" || !IsValid(currencyCode) { 386 | return InvalidCurrencyCodeError{currencyCode} 387 | } 388 | a.number = number 389 | a.currencyCode = currencyCode 390 | 391 | return nil 392 | } 393 | 394 | var ( 395 | decimalContextPrecision19 = apd.BaseContext.WithPrecision(19) 396 | decimalContextPrecision39 = apd.BaseContext.WithPrecision(39) 397 | ) 398 | 399 | // decimalContext returns the decimal context to use for a calculation. 400 | // The returned context is not safe for concurrent modification. 401 | func decimalContext(decimals ...*apd.Decimal) *apd.Context { 402 | // Choose between decimal64 (19 digits) and decimal128 (39 digits) 403 | // based on operand size (> int32), for increased performance. 404 | for _, d := range decimals { 405 | if d.Coeff.BitLen() > 31 { 406 | return decimalContextPrecision39 407 | } 408 | } 409 | return decimalContextPrecision19 410 | } 411 | 412 | // roundingContext returns the decimal context to use for rounding. 413 | // It optimizes for the most common RoundHalfUp mode by returning a preallocated global context for it. 414 | func roundingContext(decimal *apd.Decimal, mode RoundingMode) *apd.Context { 415 | if mode == RoundHalfUp { 416 | return decimalContext(decimal) 417 | } 418 | 419 | extModes := map[RoundingMode]apd.Rounder{ 420 | RoundHalfDown: apd.RoundHalfDown, 421 | RoundUp: apd.RoundUp, 422 | RoundDown: apd.RoundDown, 423 | RoundHalfEven: apd.RoundHalfEven, 424 | } 425 | ctx := *decimalContext(decimal) 426 | ctx.Rounding = extModes[mode] 427 | 428 | return &ctx 429 | } 430 | -------------------------------------------------------------------------------- /formatter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | package currency_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/bojanz/currency" 10 | ) 11 | 12 | func TestFormatter_Locale(t *testing.T) { 13 | locale := currency.NewLocale("fr-FR") 14 | formatter := currency.NewFormatter(locale) 15 | got := formatter.Locale().String() 16 | if got != "fr-FR" { 17 | t.Errorf("got %v, want fr-FR", got) 18 | } 19 | } 20 | 21 | func TestFormatter_Format(t *testing.T) { 22 | tests := []struct { 23 | number string 24 | currencyCode string 25 | localeID string 26 | want string 27 | }{ 28 | {"1234.59", "USD", "en-US", "$1,234.59"}, 29 | {"1234.59", "USD", "en-CA", "US$1,234.59"}, 30 | {"1234.59", "USD", "de-CH", "$\u00a01’234.59"}, 31 | {"1234.59", "USD", "sr", "1.234,59\u00a0US$"}, 32 | 33 | {"-1234.59", "USD", "en-US", "-$1,234.59"}, 34 | {"-1234.59", "USD", "en-CA", "-US$1,234.59"}, 35 | {"-1234.59", "USD", "de-CH", "$-1’234.59"}, 36 | {"-1234.59", "USD", "sr", "-1.234,59\u00a0US$"}, 37 | 38 | {"1234.00", "EUR", "en", "€1,234.00"}, 39 | {"1234.00", "EUR", "de-CH", "€\u00a01’234.00"}, 40 | {"1234.00", "EUR", "sr", "1.234,00\u00a0€"}, 41 | 42 | {"1234.00", "CHF", "en", "CHF\u00a01,234.00"}, 43 | {"1234.00", "CHF", "de-CH", "CHF\u00a01’234.00"}, 44 | {"1234.00", "CHF", "sr", "1.234,00\u00a0CHF"}, 45 | 46 | // An empty locale should be equivalent to "en". 47 | {"1234.59", "USD", "", "$1,234.59"}, 48 | {"-1234.59", "USD", "", "-$1,234.59"}, 49 | 50 | // Arabic digits. 51 | {"12345678.90", "USD", "ar-EG", "\u200f١٢٬٣٤٥٬٦٧٨٫٩٠\u00a0US$"}, 52 | // Arabic extended (Persian) digits. 53 | {"12345678.90", "USD", "fa", "\u200e$۱۲٬۳۴۵٬۶۷۸٫۹۰"}, 54 | // Bengali digits. 55 | {"12345678.90", "USD", "bn", "১,২৩,৪৫,৬৭৮.৯০\u00a0US$"}, 56 | // Devanagari digits. 57 | {"12345678.90", "USD", "ne", "US$\u00a0१,२३,४५,६७८.९०"}, 58 | // Myanmar (Burmese) digits. 59 | {"12345678.90", "USD", "my", "၁၂,၃၄၅,၆၇၈.၉၀\u00a0US$"}, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run("", func(t *testing.T) { 64 | amount, _ := currency.NewAmount(tt.number, tt.currencyCode) 65 | locale := currency.NewLocale(tt.localeID) 66 | formatter := currency.NewFormatter(locale) 67 | got := formatter.Format(amount) 68 | if got != tt.want { 69 | t.Errorf("got %v, want %v", got, tt.want) 70 | } 71 | }) 72 | } 73 | } 74 | 75 | func TestFormatter_AccountingStyle(t *testing.T) { 76 | tests := []struct { 77 | number string 78 | currencyCode string 79 | localeID string 80 | AddPlusSign bool 81 | want string 82 | }{ 83 | // Locale with an accounting pattern. 84 | {"1234.59", "USD", "en", false, "$1,234.59"}, 85 | {"-1234.59", "USD", "en", false, "($1,234.59)"}, 86 | {"1234.59", "USD", "en", true, "+$1,234.59"}, 87 | 88 | // Locale without an accounting pattern. 89 | {"1234.59", "EUR", "es", false, "1234,59 €"}, 90 | {"-1234.59", "EUR", "es", false, "-1234,59 €"}, 91 | {"1234.59", "EUR", "es", true, "+1234,59 €"}, 92 | } 93 | 94 | for _, tt := range tests { 95 | t.Run("", func(t *testing.T) { 96 | amount, _ := currency.NewAmount(tt.number, tt.currencyCode) 97 | locale := currency.NewLocale(tt.localeID) 98 | formatter := currency.NewFormatter(locale) 99 | formatter.AccountingStyle = true 100 | formatter.AddPlusSign = tt.AddPlusSign 101 | got := formatter.Format(amount) 102 | if got != tt.want { 103 | t.Errorf("got %v, want %v", got, tt.want) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func TestFormatter_PlusSign(t *testing.T) { 110 | tests := []struct { 111 | number string 112 | currencyCode string 113 | localeID string 114 | AddPlusSign bool 115 | want string 116 | }{ 117 | {"123.99", "USD", "en", false, "$123.99"}, 118 | {"123.99", "USD", "en", true, "+$123.99"}, 119 | 120 | {"123.99", "USD", "de-CH", false, "$\u00a0123.99"}, 121 | {"123.99", "USD", "de-CH", true, "$+123.99"}, 122 | 123 | {"123.99", "USD", "fr-FR", false, "123,99\u00a0$US"}, 124 | {"123.99", "USD", "fr-FR", true, "+123,99\u00a0$US"}, 125 | } 126 | 127 | for _, tt := range tests { 128 | t.Run("", func(t *testing.T) { 129 | amount, _ := currency.NewAmount(tt.number, tt.currencyCode) 130 | locale := currency.NewLocale(tt.localeID) 131 | formatter := currency.NewFormatter(locale) 132 | formatter.AddPlusSign = tt.AddPlusSign 133 | got := formatter.Format(amount) 134 | if got != tt.want { 135 | t.Errorf("got %v, want %v", got, tt.want) 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestFormatter_Grouping(t *testing.T) { 142 | tests := []struct { 143 | number string 144 | currencyCode string 145 | localeID string 146 | NoGrouping bool 147 | want string 148 | }{ 149 | {"123.99", "USD", "en", false, "$123.99"}, 150 | {"1234.99", "USD", "en", false, "$1,234.99"}, 151 | {"1234567.99", "USD", "en", false, "$1,234,567.99"}, 152 | 153 | {"123.99", "USD", "en", true, "$123.99"}, 154 | {"1234.99", "USD", "en", true, "$1234.99"}, 155 | {"1234567.99", "USD", "en", true, "$1234567.99"}, 156 | 157 | // The "es" locale has a different minGroupingSize. 158 | {"123.99", "USD", "es", false, "123,99\u00a0US$"}, 159 | {"1234.99", "USD", "es", false, "1234,99\u00a0US$"}, 160 | {"12345.99", "USD", "es", false, "12.345,99\u00a0US$"}, 161 | {"1234567.99", "USD", "es", false, "1.234.567,99\u00a0US$"}, 162 | 163 | // The "hi" locale has a different secondaryGroupingSize. 164 | {"123.99", "USD", "hi", false, "$123.99"}, 165 | {"1234.99", "USD", "hi", false, "$1,234.99"}, 166 | {"1234567.99", "USD", "hi", false, "$12,34,567.99"}, 167 | {"12345678.99", "USD", "hi", false, "$1,23,45,678.99"}, 168 | } 169 | 170 | for _, tt := range tests { 171 | t.Run("", func(t *testing.T) { 172 | amount, _ := currency.NewAmount(tt.number, tt.currencyCode) 173 | locale := currency.NewLocale(tt.localeID) 174 | formatter := currency.NewFormatter(locale) 175 | formatter.NoGrouping = tt.NoGrouping 176 | got := formatter.Format(amount) 177 | if got != tt.want { 178 | t.Errorf("got %v, want %v", got, tt.want) 179 | } 180 | }) 181 | } 182 | } 183 | 184 | func TestFormatter_Digits(t *testing.T) { 185 | tests := []struct { 186 | number string 187 | currencyCode string 188 | localeID string 189 | minDigits uint8 190 | maxDigits uint8 191 | want string 192 | }{ 193 | {"59", "KRW", "en", currency.DefaultDigits, 6, "₩59"}, 194 | {"59", "USD", "en", currency.DefaultDigits, 6, "$59.00"}, 195 | {"59", "OMR", "en", currency.DefaultDigits, 6, "OMR\u00a059.000"}, 196 | 197 | {"59.6789", "KRW", "en", 0, currency.DefaultDigits, "₩60"}, 198 | {"59.6789", "USD", "en", 0, currency.DefaultDigits, "$59.68"}, 199 | {"59.6789", "OMR", "en", 0, currency.DefaultDigits, "OMR\u00a059.679"}, 200 | 201 | // minDigits:0 strips all trailing zeroes. 202 | {"59", "USD", "en", 0, 6, "$59"}, 203 | {"59.5", "USD", "en", 0, 6, "$59.5"}, 204 | {"59.56", "USD", "en", 0, 6, "$59.56"}, 205 | 206 | // minDigits can't override maxDigits. 207 | {"59.5", "USD", "en", 3, 2, "$59.50"}, 208 | {"59.567", "USD", "en", 3, 2, "$59.57"}, 209 | 210 | // maxDigits rounds the number. 211 | {"59.5", "USD", "en", 2, 3, "$59.50"}, 212 | {"59.567", "USD", "en", 2, 3, "$59.567"}, 213 | {"59.5678", "USD", "en", 2, 3, "$59.568"}, 214 | } 215 | 216 | for _, tt := range tests { 217 | t.Run("", func(t *testing.T) { 218 | amount, _ := currency.NewAmount(tt.number, tt.currencyCode) 219 | locale := currency.NewLocale(tt.localeID) 220 | formatter := currency.NewFormatter(locale) 221 | formatter.MinDigits = tt.minDigits 222 | formatter.MaxDigits = tt.maxDigits 223 | got := formatter.Format(amount) 224 | if got != tt.want { 225 | t.Errorf("got %v, want %v", got, tt.want) 226 | } 227 | }) 228 | } 229 | } 230 | 231 | func TestFormatter_RoundingMode(t *testing.T) { 232 | tests := []struct { 233 | number string 234 | currencyCode string 235 | localeID string 236 | roundingMode currency.RoundingMode 237 | want string 238 | }{ 239 | {"1234.453", "USD", "en", currency.RoundHalfUp, "$1,234.45"}, 240 | {"1234.455", "USD", "en", currency.RoundHalfUp, "$1,234.46"}, 241 | {"1234.456", "USD", "en", currency.RoundHalfUp, "$1,234.46"}, 242 | 243 | {"1234.453", "USD", "en", currency.RoundHalfDown, "$1,234.45"}, 244 | {"1234.455", "USD", "en", currency.RoundHalfDown, "$1,234.45"}, 245 | {"1234.457", "USD", "en", currency.RoundHalfDown, "$1,234.46"}, 246 | 247 | {"1234.453", "USD", "en", currency.RoundUp, "$1,234.46"}, 248 | {"1234.455", "USD", "en", currency.RoundUp, "$1,234.46"}, 249 | {"1234.457", "USD", "en", currency.RoundUp, "$1,234.46"}, 250 | 251 | {"1234.453", "USD", "en", currency.RoundDown, "$1,234.45"}, 252 | {"1234.455", "USD", "en", currency.RoundDown, "$1,234.45"}, 253 | {"1234.457", "USD", "en", currency.RoundDown, "$1,234.45"}, 254 | } 255 | 256 | for _, tt := range tests { 257 | t.Run("", func(t *testing.T) { 258 | amount, _ := currency.NewAmount(tt.number, tt.currencyCode) 259 | locale := currency.NewLocale(tt.localeID) 260 | formatter := currency.NewFormatter(locale) 261 | formatter.MaxDigits = currency.DefaultDigits 262 | formatter.RoundingMode = tt.roundingMode 263 | got := formatter.Format(amount) 264 | if got != tt.want { 265 | t.Errorf("got %v, want %v", got, tt.want) 266 | } 267 | }) 268 | } 269 | } 270 | 271 | func TestFormatter_CurrencyDisplay(t *testing.T) { 272 | tests := []struct { 273 | number string 274 | currencyCode string 275 | localeID string 276 | currencyDisplay currency.Display 277 | want string 278 | }{ 279 | {"1234.59", "USD", "en", currency.DisplaySymbol, "$1,234.59"}, 280 | {"1234.59", "USD", "en", currency.DisplayCode, "USD\u00a01,234.59"}, 281 | {"1234.59", "USD", "en", currency.DisplayNone, "1,234.59"}, 282 | 283 | {"1234.59", "USD", "de-AT", currency.DisplaySymbol, "$\u00a01.234,59"}, 284 | {"1234.59", "USD", "de-AT", currency.DisplayCode, "USD\u00a01.234,59"}, 285 | {"1234.59", "USD", "de-AT", currency.DisplayNone, "1.234,59"}, 286 | 287 | {"1234.59", "USD", "sr-Latn", currency.DisplaySymbol, "1.234,59\u00a0US$"}, 288 | {"1234.59", "USD", "sr-Latn", currency.DisplayCode, "1.234,59\u00a0USD"}, 289 | {"1234.59", "USD", "sr-Latn", currency.DisplayNone, "1.234,59"}, 290 | 291 | // Confirm that any extra spacing around the currency is stripped 292 | // even when the negative amount is formatted with the accounting style. 293 | {"-1234.59", "USD", "en", currency.DisplayNone, "(1,234.59)"}, 294 | {"-1234.59", "USD", "en-NL", currency.DisplayNone, "(1.234,59)"}, 295 | {"-1234.59", "USD", "sr-Latn", currency.DisplayNone, "(1.234,59)"}, 296 | } 297 | 298 | for _, tt := range tests { 299 | t.Run("", func(t *testing.T) { 300 | amount, _ := currency.NewAmount(tt.number, tt.currencyCode) 301 | locale := currency.NewLocale(tt.localeID) 302 | formatter := currency.NewFormatter(locale) 303 | formatter.AccountingStyle = true 304 | formatter.CurrencyDisplay = tt.currencyDisplay 305 | got := formatter.Format(amount) 306 | if got != tt.want { 307 | t.Errorf("got %v, want %v", got, tt.want) 308 | } 309 | }) 310 | } 311 | } 312 | 313 | func TestFormatter_SymbolMap(t *testing.T) { 314 | locale := currency.NewLocale("en") 315 | formatter := currency.NewFormatter(locale) 316 | formatter.SymbolMap["USD"] = "US$" 317 | formatter.SymbolMap["EUR"] = "EU" 318 | 319 | amount, _ := currency.NewAmount("6.99", "USD") 320 | got := formatter.Format(amount) 321 | if got != "US$6.99" { 322 | t.Errorf("got %v, want US$6.99", got) 323 | } 324 | 325 | amount, _ = currency.NewAmount("6.99", "EUR") 326 | got = formatter.Format(amount) 327 | if got != "EU\u00a06.99" { 328 | t.Errorf("got %v, want EU\u00a06.99", got) 329 | } 330 | } 331 | 332 | func TestFormatter_Parse(t *testing.T) { 333 | tests := []struct { 334 | s string 335 | currencyCode string 336 | localeID string 337 | want string 338 | }{ 339 | {"$1,234.59", "USD", "en", "1234.59"}, 340 | {"USD\u00a01,234.59", "USD", "en", "1234.59"}, 341 | {"1,234.59", "USD", "en", "1234.59"}, 342 | {"1234.59", "USD", "en", "1234.59"}, 343 | {"+1234.59", "USD", "en", "1234.59"}, 344 | {"1234", "USD", "en", "1234"}, 345 | 346 | {"-$1,234.59", "USD", "en", "-1234.59"}, 347 | {"-USD\u00a01,234.59", "USD", "en", "-1234.59"}, 348 | {"-1,234.59", "USD", "en", "-1234.59"}, 349 | {"-1234.59", "USD", "en", "-1234.59"}, 350 | {"(1234.59)", "USD", "en", "-1234.59"}, 351 | 352 | {"€\u00a01.234,00", "EUR", "de-AT", "1234.00"}, 353 | {"EUR\u00a01.234,00", "EUR", "de-AT", "1234.00"}, 354 | {"1.234,00", "EUR", "de-AT", "1234.00"}, 355 | {"1234,00", "EUR", "de-AT", "1234.00"}, 356 | 357 | // Arabic digits. 358 | {"١٢٬٣٤٥٬٦٧٨٫٩٠\u00a0US$", "USD", "ar-EG", "12345678.90"}, 359 | // Arabic extended (Persian) digits. 360 | {"\u200e$۱۲٬۳۴۵٬۶۷۸٫۹۰", "USD", "fa", "12345678.90"}, 361 | // Bengali digits. 362 | {"১,২৩,৪৫,৬৭৮.৯০\u00a0US$", "USD", "bn", "12345678.90"}, 363 | // Devanagari digits. 364 | {"US$\u00a0१,२३,४५,६७८.९०", "USD", "ne", "12345678.90"}, 365 | // Myanmar (Burmese) digits. 366 | {"၁၂,၃၄၅,၆၇၈.၉၀\u00a0US$", "USD", "my", "12345678.90"}, 367 | } 368 | 369 | for _, tt := range tests { 370 | t.Run("", func(t *testing.T) { 371 | locale := currency.NewLocale(tt.localeID) 372 | formatter := currency.NewFormatter(locale) 373 | // Allow parsing negative amounts formatted using parenthesis. 374 | formatter.AccountingStyle = true 375 | got, err := formatter.Parse(tt.s, tt.currencyCode) 376 | if err != nil { 377 | t.Errorf("unexpected error: %v", err) 378 | } 379 | if got.Number() != tt.want { 380 | t.Errorf("got %v, want %v", got, tt.want) 381 | } 382 | if got.CurrencyCode() != tt.currencyCode { 383 | t.Errorf("got %v, want %v", got.CurrencyCode(), tt.currencyCode) 384 | } 385 | }) 386 | } 387 | } 388 | 389 | func TestEmptyLocale(t *testing.T) { 390 | locale := currency.NewLocale("") 391 | formatter := currency.NewFormatter(locale) 392 | got := formatter.Locale().String() 393 | if got != "" { 394 | t.Errorf("got %v, want empty locale", got) 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build ignore 5 | // +build ignore 6 | 7 | package main 8 | 9 | import ( 10 | "encoding/json" 11 | "encoding/xml" 12 | "fmt" 13 | "io" 14 | "log" 15 | "net/http" 16 | "os" 17 | "os/exec" 18 | "reflect" 19 | "regexp" 20 | "slices" 21 | "sort" 22 | "strconv" 23 | "strings" 24 | "text/template" 25 | "time" 26 | 27 | "github.com/bojanz/currency" 28 | ) 29 | 30 | const assetDir = "raw" 31 | 32 | const dataTemplate = `// Code generated by go generate; DO NOT EDIT. 33 | //go:generate go run gen.go 34 | 35 | package currency 36 | 37 | // CLDRVersion is the CLDR version from which the data is derived. 38 | const CLDRVersion = "{{ .CLDRVersion }}" 39 | 40 | type numberingSystem uint8 41 | 42 | const ( 43 | numLatn numberingSystem = iota 44 | numArab 45 | numArabExt 46 | numBeng 47 | numDeva 48 | numMymr 49 | ) 50 | 51 | type currencyInfo struct { 52 | numericCode string 53 | digits uint8 54 | } 55 | 56 | type symbolInfo struct { 57 | symbol string 58 | locales []string 59 | } 60 | 61 | type currencyFormat struct { 62 | standardPattern string 63 | accountingPattern string 64 | numberingSystem numberingSystem 65 | minGroupingDigits uint8 66 | primaryGroupingSize uint8 67 | secondaryGroupingSize uint8 68 | decimalSeparator string 69 | groupingSeparator string 70 | plusSign string 71 | minusSign string 72 | } 73 | 74 | // Defined separately to ensure consistent ordering (G10, then others). 75 | var currencyCodes = []string{ 76 | // G10 currencies https://en.wikipedia.org/wiki/G10_currencies. 77 | {{ export .G10Currencies 10 "\t" }} 78 | 79 | // Other currencies. 80 | {{ export .OtherCurrencies 10 "\t" }} 81 | } 82 | 83 | var currencies = map[string]currencyInfo{ 84 | {{ export .CurrencyInfo 3 "\t" }} 85 | } 86 | 87 | var currencySymbols = map[string][]symbolInfo{ 88 | {{ export .SymbolInfo 1 "\t" }} 89 | } 90 | 91 | var currencyFormats = map[string]currencyFormat{ 92 | {{ export .Formats 1 "\t" }} 93 | } 94 | 95 | var countryCurrencies = map[string]string{ 96 | {{ export .CountryCurrencies 5 "\t" }} 97 | } 98 | 99 | var parentLocales = map[string]string{ 100 | {{ export .ParentLocales 3 "\t" }} 101 | } 102 | ` 103 | 104 | type currencyInfo struct { 105 | numericCode string 106 | digits uint8 107 | } 108 | 109 | func (c currencyInfo) GoString() string { 110 | return fmt.Sprintf("{%q, %d}", c.numericCode, int(c.digits)) 111 | } 112 | 113 | type symbolInfo struct { 114 | symbol string 115 | locales []string 116 | } 117 | 118 | func (s symbolInfo) GoString() string { 119 | return fmt.Sprintf("{%q, %#v}", s.symbol, s.locales) 120 | } 121 | 122 | type symbolInfoSlice []*symbolInfo 123 | 124 | func (ss symbolInfoSlice) GoString() string { 125 | b := strings.Builder{} 126 | b.WriteString("{\n") 127 | for _, s := range ss { 128 | b.WriteString("\t\t") 129 | fmt.Fprintf(&b, "%#v,\n", s) 130 | } 131 | b.WriteString("\t}") 132 | 133 | return b.String() 134 | } 135 | 136 | type numberingSystem uint8 137 | 138 | const ( 139 | numLatn numberingSystem = iota 140 | numArab 141 | numArabExt 142 | numBeng 143 | numDeva 144 | numMymr 145 | ) 146 | 147 | type currencyFormat struct { 148 | standardPattern string 149 | accountingPattern string 150 | numberingSystem numberingSystem 151 | minGroupingDigits uint8 152 | primaryGroupingSize uint8 153 | secondaryGroupingSize uint8 154 | decimalSeparator string 155 | groupingSeparator string 156 | plusSign string 157 | minusSign string 158 | } 159 | 160 | func (f currencyFormat) GoString() string { 161 | return fmt.Sprintf("{%q, %q, %d, %d, %d, %d, %q, %q, %q, %q}", f.standardPattern, f.accountingPattern, f.numberingSystem, f.minGroupingDigits, f.primaryGroupingSize, f.secondaryGroupingSize, f.decimalSeparator, f.groupingSeparator, f.plusSign, f.minusSign) 162 | } 163 | 164 | func main() { 165 | err := os.Mkdir(assetDir, 0755) 166 | if err != nil { 167 | log.Fatal(err) 168 | } 169 | defer os.RemoveAll(assetDir) 170 | 171 | log.Println("Fetching CLDR data...") 172 | CLDRVersion, err := fetchCLDR(assetDir) 173 | if err != nil { 174 | os.RemoveAll(assetDir) 175 | log.Fatal(err) 176 | } 177 | 178 | log.Println("Fetching ISO data...") 179 | currencies, err := fetchISO() 180 | if err != nil { 181 | os.RemoveAll(assetDir) 182 | log.Fatal(err) 183 | } 184 | 185 | log.Println("Processing...") 186 | locales, err := collectLocales(assetDir) 187 | if err != nil { 188 | os.RemoveAll(assetDir) 189 | log.Fatal(err) 190 | } 191 | symbols, err := generateSymbols(currencies, locales, assetDir) 192 | if err != nil { 193 | os.RemoveAll(assetDir) 194 | log.Fatal(err) 195 | } 196 | formats, err := generateFormats(locales, assetDir) 197 | if err != nil { 198 | os.RemoveAll(assetDir) 199 | log.Fatal(err) 200 | } 201 | countryCurrencies, err := generateCountryCurrencies(assetDir) 202 | if err != nil { 203 | os.RemoveAll(assetDir) 204 | log.Fatal(err) 205 | } 206 | parentLocales, err := generateParentLocales(locales, assetDir) 207 | if err != nil { 208 | os.RemoveAll(assetDir) 209 | log.Fatal(err) 210 | } 211 | 212 | var currencyCodes []string 213 | for currencyCode := range currencies { 214 | currencyCodes = append(currencyCodes, currencyCode) 215 | } 216 | sort.Strings(currencyCodes) 217 | 218 | g10Currencies := []string{ 219 | "AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "NOK", "NZD", "SEK", "USD", 220 | } 221 | var otherCurrencies []string 222 | for _, currencyCode := range currencyCodes { 223 | if !slices.Contains(g10Currencies, currencyCode) { 224 | otherCurrencies = append(otherCurrencies, currencyCode) 225 | } 226 | } 227 | 228 | os.Remove("data.go") 229 | f, err := os.Create("data.go") 230 | if err != nil { 231 | os.RemoveAll(assetDir) 232 | log.Fatal(err) 233 | } 234 | defer f.Close() 235 | 236 | funcMap := template.FuncMap{ 237 | "export": export, 238 | } 239 | t, err := template.New("data").Funcs(funcMap).Parse(dataTemplate) 240 | if err != nil { 241 | os.RemoveAll(assetDir) 242 | log.Fatal(err) 243 | } 244 | t.Execute(f, struct { 245 | CLDRVersion string 246 | G10Currencies []string 247 | OtherCurrencies []string 248 | CurrencyInfo map[string]*currencyInfo 249 | SymbolInfo map[string]symbolInfoSlice 250 | Formats map[string]currencyFormat 251 | CountryCurrencies map[string]string 252 | ParentLocales map[string]string 253 | }{ 254 | CLDRVersion: CLDRVersion, 255 | G10Currencies: g10Currencies, 256 | OtherCurrencies: otherCurrencies, 257 | CurrencyInfo: currencies, 258 | SymbolInfo: symbols, 259 | Formats: formats, 260 | CountryCurrencies: countryCurrencies, 261 | ParentLocales: parentLocales, 262 | }) 263 | 264 | log.Println("Done.") 265 | } 266 | 267 | // fetchCLDR fetches the CLDR data from GitHub and returns its version. 268 | // 269 | // The JSON version of the data is used because it is more convenient 270 | // to parse. See https://github.com/unicode-org/cldr-json for details. 271 | func fetchCLDR(dir string) (string, error) { 272 | repo := "https://github.com/unicode-org/cldr-json.git" 273 | cmd := exec.Command("git", "clone", repo, "--depth", "1", dir) 274 | cmd.Stderr = os.Stderr 275 | _, err := cmd.Output() 276 | if err != nil { 277 | return "", err 278 | } 279 | 280 | data, err := os.ReadFile(dir + "/cldr-json/cldr-core/package.json") 281 | if err != nil { 282 | return "", fmt.Errorf("fetchCLDR: %w", err) 283 | } 284 | aux := struct { 285 | Version string 286 | }{} 287 | if err := json.Unmarshal(data, &aux); err != nil { 288 | return "", fmt.Errorf("fetchCLDR: %w", err) 289 | } 290 | 291 | return aux.Version, nil 292 | } 293 | 294 | // fetchISO fetches currency info from ISO. 295 | // 296 | // ISO data is needed because CLDR can't be used as a reliable source 297 | // of numeric codes (e.g. BYR has no numeric code as of CLDR v36). 298 | // Furthermore, CLDR includes both active and inactive currencies, while ISO 299 | // includes only active ones, matching the needs of this package. 300 | func fetchISO() (map[string]*currencyInfo, error) { 301 | data, err := fetchURL("https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml") 302 | if err != nil { 303 | return nil, fmt.Errorf("fetchISO: %w", err) 304 | } 305 | aux := struct { 306 | Table []struct { 307 | Entry []struct { 308 | Code string `xml:"Ccy"` 309 | Number string `xml:"CcyNbr"` 310 | Digits string `xml:"CcyMnrUnts"` 311 | Country string `xml:"CtryNm"` 312 | Name struct { 313 | Value string `xml:",chardata"` 314 | IsFund bool `xml:"IsFund,attr"` 315 | } `xml:"CcyNm"` 316 | } `xml:"CcyNtry"` 317 | } `xml:"CcyTbl"` 318 | }{} 319 | if err := xml.Unmarshal(data, &aux); err != nil { 320 | return nil, fmt.Errorf("fetchISO: %w", err) 321 | } 322 | 323 | currencies := make(map[string]*currencyInfo, 170) 324 | for _, entry := range aux.Table[0].Entry { 325 | if entry.Code == "" || entry.Number == "" || entry.Digits == "N.A." { 326 | continue 327 | } 328 | 329 | digits := parseDigits(entry.Digits, 2) 330 | currencies[entry.Code] = ¤cyInfo{entry.Number, digits} 331 | } 332 | 333 | return currencies, nil 334 | } 335 | 336 | func fetchURL(url string) ([]byte, error) { 337 | client := http.Client{Timeout: 15 * time.Second} 338 | resp, err := client.Get(url) 339 | if err != nil { 340 | return nil, fmt.Errorf("fetchURL: %w", err) 341 | } 342 | defer resp.Body.Close() 343 | if resp.StatusCode != http.StatusOK { 344 | return nil, fmt.Errorf("fetchURL: Get %q: %v", url, resp.Status) 345 | } 346 | data, err := io.ReadAll(resp.Body) 347 | if err != nil { 348 | return nil, fmt.Errorf("fetchURL: Get %q: %w", url, err) 349 | } 350 | 351 | return data, nil 352 | } 353 | 354 | // collectLocales collects CLDR locales with "modern" coverage. 355 | func collectLocales(dir string) ([]string, error) { 356 | data, err := os.ReadFile(dir + "/cldr-json/cldr-core/coverageLevels.json") 357 | if err != nil { 358 | return nil, fmt.Errorf("collectLocales: %w", err) 359 | } 360 | aux := struct { 361 | EffectiveCoverageLevels map[string]string 362 | }{} 363 | if err := json.Unmarshal(data, &aux); err != nil { 364 | return nil, fmt.Errorf("collectLocales: %w", err) 365 | } 366 | 367 | locales := make([]string, 0, len(aux.EffectiveCoverageLevels)) 368 | for locale, level := range aux.EffectiveCoverageLevels { 369 | if !shouldIgnoreLocale(locale) && level == "modern" { 370 | locales = append(locales, locale) 371 | } 372 | } 373 | 374 | return locales, nil 375 | } 376 | 377 | // generateCountryCurrencies generates the map of country codes to currency codes. 378 | func generateCountryCurrencies(dir string) (map[string]string, error) { 379 | data, err := os.ReadFile(dir + "/cldr-json/cldr-core/supplemental/currencyData.json") 380 | if err != nil { 381 | return nil, fmt.Errorf("generateCountryCurrencies: %w", err) 382 | } 383 | 384 | aux := struct { 385 | Supplemental struct { 386 | CurrencyData struct { 387 | Region map[string][]map[string]struct { 388 | From string `json:"_from"` 389 | To string `json:"_to"` 390 | Tender string `json:"_tender"` 391 | } 392 | } 393 | } 394 | }{} 395 | if err := json.Unmarshal(data, &aux); err != nil { 396 | return nil, fmt.Errorf("generateCountryCurrencies: %w", err) 397 | } 398 | 399 | countryCurrencies := make(map[string]string) 400 | for countryCode, currencies := range aux.Supplemental.CurrencyData.Region { 401 | if slices.Contains([]string{"EA", "EU", "ZZ"}, countryCode) { 402 | // EA, EU and ZZ are not countries. 403 | continue 404 | } 405 | 406 | lastCurrencyCode := "" 407 | lastFrom := "" 408 | for _, currencyUsage := range currencies { 409 | for currencyCode, usageInfo := range currencyUsage { 410 | if usageInfo.To != "" || usageInfo.Tender == "false" { 411 | // Currency no longer in use, skip. 412 | continue 413 | } 414 | if lastFrom == "" || usageInfo.From > lastFrom { 415 | lastCurrencyCode = currencyCode 416 | lastFrom = usageInfo.From 417 | } 418 | } 419 | } 420 | 421 | if lastCurrencyCode != "" && lastCurrencyCode != "XXX" { 422 | countryCurrencies[countryCode] = lastCurrencyCode 423 | } 424 | } 425 | 426 | return countryCurrencies, nil 427 | } 428 | 429 | // generateSymbols generates currency symbols for all locales. 430 | // 431 | // Symbols are grouped by locale, and deduplicated by parent. 432 | func generateSymbols(currencies map[string]*currencyInfo, locales []string, dir string) (map[string]symbolInfoSlice, error) { 433 | symbols := make(map[string]map[string][]string) 434 | for _, locale := range locales { 435 | localSymbols, err := readSymbols(currencies, dir, locale) 436 | if err != nil { 437 | return nil, fmt.Errorf("generateSymbols: %w", err) 438 | } 439 | 440 | for currencyCode, symbol := range localSymbols { 441 | if _, ok := symbols[currencyCode]; !ok { 442 | symbols[currencyCode] = make(map[string][]string) 443 | } 444 | symbols[currencyCode][symbol] = append(symbols[currencyCode][symbol], locale) 445 | } 446 | } 447 | 448 | for currencyCode, localSymbols := range symbols { 449 | if _, ok := localSymbols[currencyCode]; !ok { 450 | continue 451 | } 452 | // currency.GetSymbol always returns the currency code when no 453 | // symbol is available, so there is no need to store a currency 454 | // whose only symbol is the currency code. 455 | if len(localSymbols) == 1 { 456 | delete(symbols, currencyCode) 457 | continue 458 | } 459 | // Diverge from CLDR by preventing child locales from using the 460 | // currency code as a symbol when the "en" locale has a real one. 461 | // E.g. the "hr", "hu", "ro" locales should use $, £, € instead 462 | // of USD, GBP, EUR, just like the "en" locale does. 463 | // This noticeably decreases the size of generated data. 464 | for symbol, locales := range localSymbols { 465 | if slices.Contains(locales, "en") && currencyCode != symbol { 466 | symbols[currencyCode][symbol] = append(symbols[currencyCode][symbol], localSymbols[currencyCode]...) 467 | delete(symbols[currencyCode], currencyCode) 468 | break 469 | } 470 | } 471 | // The logic above results in "en-AU" using the same $ symbol for AUD and USD. 472 | // Related: https://unicode-org.atlassian.net/projects/CLDR/issues/CLDR-10710 473 | if currencyCode == "USD" { 474 | // Move en-AU from symbols["USD"]["$"] to symbols["USD"]["US$"]. 475 | li := 0 476 | for i, locale := range symbols["USD"]["$"] { 477 | if locale == "en-AU" { 478 | li = i 479 | break 480 | } 481 | } 482 | symbols["USD"]["$"] = append(symbols["USD"]["$"][:li], symbols["USD"]["$"][li+1:]...) 483 | symbols["USD"]["US$"] = append(symbols["USD"]["US$"], "en-AU") 484 | } 485 | } 486 | 487 | // Child locales don't need to be listed if the parent is present. 488 | for currencyCode, localSymbols := range symbols { 489 | for symbol, locales := range localSymbols { 490 | var deleteLocales []string 491 | for _, localeID := range locales { 492 | locale := currency.NewLocale(localeID) 493 | parent := locale.GetParent() 494 | if slices.Contains(locales, parent.String()) { 495 | deleteLocales = append(deleteLocales, localeID) 496 | } 497 | } 498 | symbols[currencyCode][symbol] = []string{} 499 | for _, localeID := range locales { 500 | if !slices.Contains(deleteLocales, localeID) { 501 | symbols[currencyCode][symbol] = append(symbols[currencyCode][symbol], localeID) 502 | } 503 | } 504 | sort.Strings(symbols[currencyCode][symbol]) 505 | } 506 | } 507 | 508 | // Convert to the final data structure. 509 | currencySymbols := make(map[string]symbolInfoSlice) 510 | for currencyCode, localSymbols := range symbols { 511 | // Always put the "en" symbol first, then the other sorted symbols. 512 | for symbol, locales := range localSymbols { 513 | if slices.Contains(locales, "en") { 514 | currencySymbols[currencyCode] = append(currencySymbols[currencyCode], &symbolInfo{symbol, locales}) 515 | break 516 | } 517 | } 518 | var symbolKeys []string 519 | for symbol := range localSymbols { 520 | symbolKeys = append(symbolKeys, symbol) 521 | } 522 | sort.Strings(symbolKeys) 523 | for _, symbol := range symbolKeys { 524 | locales := symbols[currencyCode][symbol] 525 | if !slices.Contains(locales, "en") { 526 | currencySymbols[currencyCode] = append(currencySymbols[currencyCode], &symbolInfo{symbol, locales}) 527 | } 528 | } 529 | } 530 | 531 | return currencySymbols, nil 532 | } 533 | 534 | // readSymbols reads the given locale's currency symbols from CLDR data. 535 | // 536 | // Discards symbols belonging to inactive currencies. 537 | func readSymbols(currencies map[string]*currencyInfo, dir string, locale string) (map[string]string, error) { 538 | filename := fmt.Sprintf("%v/cldr-json/cldr-numbers-full/main/%v/currencies.json", dir, locale) 539 | data, err := os.ReadFile(filename) 540 | if err != nil { 541 | return nil, fmt.Errorf("readSymbols: %w", err) 542 | } 543 | 544 | type cldrData struct { 545 | Numbers struct { 546 | Currencies map[string]map[string]string 547 | } 548 | } 549 | aux := struct { 550 | Main map[string]cldrData 551 | }{} 552 | if err := json.Unmarshal(data, &aux); err != nil { 553 | return nil, fmt.Errorf("readSymbols: %w", err) 554 | } 555 | 556 | symbols := make(map[string]string) 557 | for currencyCode, data := range aux.Main[locale].Numbers.Currencies { 558 | if _, ok := currencies[currencyCode]; ok { 559 | symbols[currencyCode] = data["symbol"] 560 | // CLDR omits the symbol when it matches the currency code. 561 | if symbols[currencyCode] == "" { 562 | symbols[currencyCode] = currencyCode 563 | } 564 | } 565 | } 566 | 567 | return symbols, nil 568 | } 569 | 570 | // generateFormats generates currency formats from CLDR data. 571 | // 572 | // Formats are deduplicated by parent. 573 | func generateFormats(locales []string, dir string) (map[string]currencyFormat, error) { 574 | formats := make(map[string]currencyFormat) 575 | for _, locale := range locales { 576 | format, err := readFormat(dir, locale) 577 | if err != nil { 578 | return nil, fmt.Errorf("generateFormats: %w", err) 579 | } 580 | formats[locale] = format 581 | } 582 | 583 | // Remove formats which are identical to their parents. 584 | var deleteLocales []string 585 | for localeID, format := range formats { 586 | locale := currency.NewLocale(localeID) 587 | parentID := locale.GetParent().String() 588 | if parentID != "" && format == formats[parentID] { 589 | deleteLocales = append(deleteLocales, localeID) 590 | } 591 | } 592 | for _, localeID := range deleteLocales { 593 | delete(formats, localeID) 594 | } 595 | 596 | return formats, nil 597 | } 598 | 599 | // readFormat reads the given locale's currency format from CLDR data. 600 | func readFormat(dir string, locale string) (currencyFormat, error) { 601 | filename := fmt.Sprintf("%v/cldr-json/cldr-numbers-full/main/%v/numbers.json", dir, locale) 602 | data, err := os.ReadFile(filename) 603 | if err != nil { 604 | return currencyFormat{}, fmt.Errorf("readFormat: %w", err) 605 | } 606 | 607 | type cldrPattern struct { 608 | Standard string 609 | Accounting string 610 | } 611 | type cldrData struct { 612 | Numbers struct { 613 | MinimumGroupingDigits string 614 | DefaultNumberingSystem string 615 | PatternLatn cldrPattern `json:"currencyFormats-numberSystem-latn"` 616 | PatternArab cldrPattern `json:"currencyFormats-numberSystem-arab"` 617 | PatternArabExt cldrPattern `json:"currencyFormats-numberSystem-arabext"` 618 | PatternBeng cldrPattern `json:"currencyFormats-numberSystem-beng"` 619 | PatternDeva cldrPattern `json:"currencyFormats-numberSystem-deva"` 620 | PatternMymr cldrPattern `json:"currencyFormats-numberSystem-mymr"` 621 | SymbolsLatn map[string]string `json:"symbols-numberSystem-latn"` 622 | SymbolsArab map[string]string `json:"symbols-numberSystem-arab"` 623 | SymbolsArabExt map[string]string `json:"symbols-numberSystem-arabext"` 624 | SymbolsBeng map[string]string `json:"symbols-numberSystem-beng"` 625 | SymbolsDeva map[string]string `json:"symbols-numberSystem-deva"` 626 | SymbolsMymr map[string]string `json:"symbols-numberSystem-mymr"` 627 | } 628 | } 629 | aux := struct { 630 | Main map[string]cldrData 631 | }{} 632 | if err := json.Unmarshal(data, &aux); err != nil { 633 | return currencyFormat{}, fmt.Errorf("readFormat: %w", err) 634 | } 635 | 636 | var numSystem numberingSystem 637 | var standardPattern string 638 | var accountingPattern string 639 | var symbols map[string]string 640 | extFormat := aux.Main[locale].Numbers 641 | switch extFormat.DefaultNumberingSystem { 642 | case "latn": 643 | numSystem = numLatn 644 | standardPattern = extFormat.PatternLatn.Standard 645 | accountingPattern = extFormat.PatternLatn.Accounting 646 | symbols = extFormat.SymbolsLatn 647 | case "arab": 648 | numSystem = numArab 649 | standardPattern = extFormat.PatternArab.Standard 650 | accountingPattern = extFormat.PatternArab.Accounting 651 | symbols = extFormat.SymbolsArab 652 | case "arabext": 653 | numSystem = numArabExt 654 | standardPattern = extFormat.PatternArabExt.Standard 655 | accountingPattern = extFormat.PatternArabExt.Accounting 656 | symbols = extFormat.SymbolsArabExt 657 | case "beng": 658 | numSystem = numBeng 659 | standardPattern = extFormat.PatternBeng.Standard 660 | accountingPattern = extFormat.PatternBeng.Accounting 661 | symbols = extFormat.SymbolsBeng 662 | case "deva": 663 | numSystem = numDeva 664 | standardPattern = extFormat.PatternDeva.Standard 665 | accountingPattern = extFormat.PatternDeva.Accounting 666 | symbols = extFormat.SymbolsDeva 667 | case "mymr": 668 | numSystem = numMymr 669 | standardPattern = extFormat.PatternMymr.Standard 670 | accountingPattern = extFormat.PatternMymr.Accounting 671 | symbols = extFormat.SymbolsMymr 672 | default: 673 | return currencyFormat{}, fmt.Errorf("readFormat: unknown numbering system %q in locale %q", extFormat.DefaultNumberingSystem, locale) 674 | } 675 | primaryGroupingSize := 0 676 | secondaryGroupingSize := 0 677 | patternParts := strings.Split(standardPattern, ";") 678 | if strings.Contains(patternParts[0], ",") { 679 | r, _ := regexp.Compile("#+0") 680 | primaryGroup := r.FindString(patternParts[0]) 681 | primaryGroupingSize = len(primaryGroup) 682 | secondaryGroupingSize = primaryGroupingSize 683 | numberGroups := strings.Split(patternParts[0], ",") 684 | if len(numberGroups) > 2 { 685 | // This pattern has a distinct secondary group size. 686 | secondaryGroupingSize = len(numberGroups[1]) 687 | } 688 | } 689 | // Strip the grouping info from the patterns, now that it is available separately. 690 | standardPattern = processPattern(standardPattern) 691 | accountingPattern = processPattern(accountingPattern) 692 | // To save memory, the accounting pattern is stored 693 | // only if it's different from the standard pattern. 694 | if accountingPattern == standardPattern { 695 | accountingPattern = "" 696 | } 697 | decimalSeparator := symbols["decimal"] 698 | groupingSeparator := symbols["group"] 699 | // Most locales use the same separators for decimal and currency 700 | // formatting, with the exception of de-AT and fr-CH (as of CLDR v37). 701 | if _, ok := symbols["currencyDecimal"]; ok { 702 | decimalSeparator = symbols["currencyDecimal"] 703 | } 704 | if _, ok := symbols["currencyGroup"]; ok { 705 | groupingSeparator = symbols["currencyGroup"] 706 | } 707 | 708 | format := currencyFormat{} 709 | format.standardPattern = standardPattern 710 | format.accountingPattern = accountingPattern 711 | format.numberingSystem = numSystem 712 | format.minGroupingDigits = parseDigits(extFormat.MinimumGroupingDigits, 1) 713 | format.primaryGroupingSize = uint8(primaryGroupingSize) 714 | format.secondaryGroupingSize = uint8(secondaryGroupingSize) 715 | format.decimalSeparator = decimalSeparator 716 | format.groupingSeparator = groupingSeparator 717 | format.plusSign = symbols["plusSign"] 718 | format.minusSign = symbols["minusSign"] 719 | 720 | return format, nil 721 | } 722 | 723 | // processPattern processes the pattern. 724 | func processPattern(pattern string) string { 725 | // Strip the grouping info. 726 | pattern = strings.ReplaceAll(pattern, "#,##,##", "") 727 | pattern = strings.ReplaceAll(pattern, "#,##", "") 728 | 729 | return pattern 730 | } 731 | 732 | // generateParentLocales generates parent locales from CLDR data. 733 | // 734 | // Ensures ignored locales are skipped. 735 | // Replaces "und" with "en", since this package treats them as equivalent. 736 | func generateParentLocales(locales []string, dir string) (map[string]string, error) { 737 | data, err := os.ReadFile(dir + "/cldr-json/cldr-core/supplemental/parentLocales.json") 738 | if err != nil { 739 | return nil, fmt.Errorf("generateParentLocales: %w", err) 740 | } 741 | aux := struct { 742 | Supplemental struct { 743 | ParentLocales struct { 744 | ParentLocale map[string]string 745 | } 746 | } 747 | }{} 748 | if err := json.Unmarshal(data, &aux); err != nil { 749 | return nil, fmt.Errorf("generateParentLocales: %w", err) 750 | } 751 | 752 | parentLocales := make(map[string]string) 753 | for locale, parent := range aux.Supplemental.ParentLocales.ParentLocale { 754 | // Avoid exposing the concept of "und" to users. 755 | if parent == "und" { 756 | parent = "en" 757 | } 758 | if slices.Contains(locales, locale) && !shouldIgnoreLocale(locale) { 759 | parentLocales[locale] = parent 760 | } 761 | } 762 | // Dsrt and Shaw are made up scripts. 763 | delete(parentLocales, "en-Dsrt") 764 | delete(parentLocales, "en-Shaw") 765 | 766 | return parentLocales, nil 767 | } 768 | 769 | func shouldIgnoreLocale(locale string) bool { 770 | ignoredLocales := []string{ 771 | // English is our fallback, we don't need another. 772 | "und", 773 | // Esperanto, Interlingua, Volapuk are made up languages. 774 | "eo", "ia", "vo", 775 | // Belarus (Classical orthography), Church Slavic, Manx, Prussian are historical. 776 | "be-tarask", "cu", "gv", "prg", 777 | // Valencian differs from its parent only by a single character (è/é). 778 | "ca-ES-valencia", 779 | } 780 | localeParts := strings.Split(locale, "-") 781 | ignore := false 782 | for _, ignoredLocale := range ignoredLocales { 783 | if ignoredLocale == locale || ignoredLocale == localeParts[0] { 784 | ignore = true 785 | break 786 | } 787 | } 788 | 789 | return ignore 790 | } 791 | 792 | func parseDigits(n string, fallback uint8) uint8 { 793 | digits, err := strconv.Atoi(n) 794 | if err != nil { 795 | return fallback 796 | } 797 | return uint8(digits) 798 | } 799 | 800 | func export(i interface{}, width int, indent string) string { 801 | v := reflect.ValueOf(i) 802 | switch v.Kind() { 803 | case reflect.Map: 804 | return exportMap(v, width, indent) 805 | case reflect.Slice: 806 | return exportSlice(v, width, indent) 807 | default: 808 | return fmt.Sprintf("%#v", i) 809 | } 810 | } 811 | 812 | func exportMap(v reflect.Value, width int, indent string) string { 813 | var maxKeyLen int 814 | var keys []string 815 | for _, k := range v.MapKeys() { 816 | key := k.Interface().(string) 817 | keys = append(keys, key) 818 | if len(key) > maxKeyLen { 819 | maxKeyLen = len(key) 820 | } 821 | } 822 | sort.Strings(keys) 823 | 824 | b := strings.Builder{} 825 | i := 0 826 | for _, key := range keys { 827 | spaceWidth := 1 828 | if width == 1 { 829 | spaceWidth += maxKeyLen - len(key) 830 | } 831 | space := strings.Repeat(" ", spaceWidth) 832 | value := v.MapIndex(reflect.ValueOf(key)) 833 | fmt.Fprintf(&b, `%q:%v%#v,`, key, space, value) 834 | if i+1 != v.Len() { 835 | if (i+1)%width == 0 { 836 | b.WriteString("\n") 837 | b.WriteString(indent) 838 | } else { 839 | b.WriteString(" ") 840 | } 841 | } 842 | i++ 843 | } 844 | 845 | return b.String() 846 | } 847 | 848 | func exportSlice(v reflect.Value, width int, indent string) string { 849 | b := strings.Builder{} 850 | for i := 0; i < v.Len(); i++ { 851 | fmt.Fprintf(&b, `%#v,`, v.Index(i).Interface()) 852 | if i+1 != v.Len() { 853 | if (i+1)%width == 0 { 854 | b.WriteString("\n") 855 | b.WriteString(indent) 856 | } else { 857 | b.WriteString(" ") 858 | } 859 | } 860 | } 861 | 862 | return b.String() 863 | } 864 | -------------------------------------------------------------------------------- /amount_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Bojan Zivanovic and contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | package currency_test 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "math/big" 10 | "sync" 11 | "testing" 12 | 13 | "github.com/bojanz/currency" 14 | ) 15 | 16 | func TestNewAmount(t *testing.T) { 17 | _, err := currency.NewAmount("INVALID", "USD") 18 | if e, ok := err.(currency.InvalidNumberError); ok { 19 | if e.Number != "INVALID" { 20 | t.Errorf("got %v, want INVALID", e.Number) 21 | } 22 | wantError := `invalid number "INVALID"` 23 | if e.Error() != wantError { 24 | t.Errorf("got %v, want %v", e.Error(), wantError) 25 | } 26 | } else { 27 | t.Errorf("got %T, want currency.InvalidNumberError", err) 28 | } 29 | 30 | _, err = currency.NewAmount("10.99", "usd") 31 | if e, ok := err.(currency.InvalidCurrencyCodeError); ok { 32 | if e.CurrencyCode != "usd" { 33 | t.Errorf("got %v, want usd", e.CurrencyCode) 34 | } 35 | wantError := `invalid currency code "usd"` 36 | if e.Error() != wantError { 37 | t.Errorf("got %v, want %v", e.Error(), wantError) 38 | } 39 | } else { 40 | t.Errorf("got %T, want currency.InvalidCurrencyCodeError", err) 41 | } 42 | 43 | a, err := currency.NewAmount("10.99", "USD") 44 | if err != nil { 45 | t.Errorf("unexpected error %v", err) 46 | } 47 | if a.Number() != "10.99" { 48 | t.Errorf("got %v, want 10.99", a.Number()) 49 | } 50 | if a.CurrencyCode() != "USD" { 51 | t.Errorf("got %v, want USD", a.CurrencyCode()) 52 | } 53 | if a.String() != "10.99 USD" { 54 | t.Errorf("got %v, want 10.99 USD", a.String()) 55 | } 56 | } 57 | 58 | func TestNewAmountFromBigInt(t *testing.T) { 59 | _, err := currency.NewAmountFromBigInt(nil, "USD") 60 | if e, ok := err.(currency.InvalidNumberError); ok { 61 | if e.Number != "nil" { 62 | t.Errorf("got %v, want nil", e.Number) 63 | } 64 | wantError := `invalid number "nil"` 65 | if e.Error() != wantError { 66 | t.Errorf("got %v, want %v", e.Error(), wantError) 67 | } 68 | } else { 69 | t.Errorf("got %T, want currency.InvalidNumberError", err) 70 | } 71 | 72 | _, err = currency.NewAmountFromBigInt(big.NewInt(1099), "usd") 73 | if e, ok := err.(currency.InvalidCurrencyCodeError); ok { 74 | if e.CurrencyCode != "usd" { 75 | t.Errorf("got %v, want usd", e.CurrencyCode) 76 | } 77 | wantError := `invalid currency code "usd"` 78 | if e.Error() != wantError { 79 | t.Errorf("got %v, want %v", e.Error(), wantError) 80 | } 81 | } else { 82 | t.Errorf("got %T, want currency.InvalidCurrencyCodeError", err) 83 | } 84 | 85 | // An integer larger than math.MaxInt64. 86 | hugeInt, _ := big.NewInt(0).SetString("922337203685477598799", 10) 87 | tests := []struct { 88 | n *big.Int 89 | currencyCode string 90 | wantNumber string 91 | }{ 92 | {big.NewInt(2099), "USD", "20.99"}, 93 | {big.NewInt(-2099), "USD", "-20.99"}, 94 | {big.NewInt(5000), "USD", "50.00"}, 95 | {big.NewInt(50), "JPY", "50"}, 96 | {hugeInt, "USD", "9223372036854775987.99"}, 97 | } 98 | 99 | for _, tt := range tests { 100 | t.Run("", func(t *testing.T) { 101 | a, err := currency.NewAmountFromBigInt(tt.n, tt.currencyCode) 102 | if err != nil { 103 | t.Errorf("unexpected error %v", err) 104 | } 105 | if a.Number() != tt.wantNumber { 106 | t.Errorf("got %v, want %v", a.Number(), tt.wantNumber) 107 | } 108 | if a.CurrencyCode() != tt.currencyCode { 109 | t.Errorf("got %v, want %v", a.CurrencyCode(), tt.currencyCode) 110 | } 111 | }) 112 | } 113 | } 114 | 115 | func TestNewAmountFromInt64(t *testing.T) { 116 | _, err := currency.NewAmountFromInt64(1099, "usd") 117 | if e, ok := err.(currency.InvalidCurrencyCodeError); ok { 118 | if e.CurrencyCode != "usd" { 119 | t.Errorf("got %v, want usd", e.CurrencyCode) 120 | } 121 | wantError := `invalid currency code "usd"` 122 | if e.Error() != wantError { 123 | t.Errorf("got %v, want %v", e.Error(), wantError) 124 | } 125 | } else { 126 | t.Errorf("got %T, want currency.InvalidCurrencyCodeError", err) 127 | } 128 | 129 | tests := []struct { 130 | n int64 131 | currencyCode string 132 | wantNumber string 133 | }{ 134 | {2099, "USD", "20.99"}, 135 | {5000, "USD", "50.00"}, 136 | {50, "JPY", "50"}, 137 | } 138 | 139 | for _, tt := range tests { 140 | t.Run("", func(t *testing.T) { 141 | a, err := currency.NewAmountFromInt64(tt.n, tt.currencyCode) 142 | if err != nil { 143 | t.Errorf("unexpected error %v", err) 144 | } 145 | if a.Number() != tt.wantNumber { 146 | t.Errorf("got %v, want %v", a.Number(), tt.wantNumber) 147 | } 148 | if a.CurrencyCode() != tt.currencyCode { 149 | t.Errorf("got %v, want %v", a.CurrencyCode(), tt.currencyCode) 150 | } 151 | }) 152 | } 153 | } 154 | 155 | func TestAmount_BigInt(t *testing.T) { 156 | tests := []struct { 157 | number string 158 | currencyCode string 159 | want *big.Int 160 | }{ 161 | {"20.99", "USD", big.NewInt(2099)}, 162 | // Number with additional decimals. 163 | {"12.3564", "USD", big.NewInt(1236)}, 164 | // Number with no decimals. 165 | {"50", "USD", big.NewInt(5000)}, 166 | {"50", "JPY", big.NewInt(50)}, 167 | // Negative number. 168 | {"-12.3564", "USD", big.NewInt(-1236)}, 169 | {"-50", "JPY", big.NewInt(-50)}, 170 | } 171 | 172 | for _, tt := range tests { 173 | t.Run("", func(t *testing.T) { 174 | a, _ := currency.NewAmount(tt.number, tt.currencyCode) 175 | got := a.BigInt() 176 | if got.Cmp(tt.want) != 0 { 177 | t.Errorf("got %v, want %v", got, tt.want) 178 | } 179 | // Confirm that a is unchanged. 180 | if a.Number() != tt.number { 181 | t.Errorf("got %v, want %v", a.Number(), tt.number) 182 | } 183 | }) 184 | } 185 | } 186 | 187 | func TestAmount_Int64(t *testing.T) { 188 | // Number that can't be represented as an int64. 189 | a, _ := currency.NewAmount("922337203685477598799", "USD") 190 | n, err := a.Int64() 191 | if n != 0 { 192 | t.Error("expected a.Int64() to be 0") 193 | } 194 | if err == nil { 195 | t.Error("expected a.Int64() to return an error") 196 | } 197 | 198 | tests := []struct { 199 | number string 200 | currencyCode string 201 | want int64 202 | }{ 203 | {"20.99", "USD", 2099}, 204 | // Number with additional decimals. 205 | {"12.3564", "USD", 1236}, 206 | // Number with no decimals. 207 | {"50", "USD", 5000}, 208 | {"50", "JPY", 50}, 209 | } 210 | 211 | for _, tt := range tests { 212 | t.Run("", func(t *testing.T) { 213 | a, _ := currency.NewAmount(tt.number, tt.currencyCode) 214 | got, _ := a.Int64() 215 | if got != tt.want { 216 | t.Errorf("got %v, want %v", got, tt.want) 217 | } 218 | // Confirm that a is unchanged. 219 | if a.Number() != tt.number { 220 | t.Errorf("got %v, want %v", a.Number(), tt.number) 221 | } 222 | }) 223 | } 224 | } 225 | 226 | func TestAmount_Convert(t *testing.T) { 227 | a, _ := currency.NewAmount("20.99", "USD") 228 | 229 | _, err := a.Convert("eur", "0.91") 230 | if e, ok := err.(currency.InvalidCurrencyCodeError); ok { 231 | if e.CurrencyCode != "eur" { 232 | t.Errorf("got %v, want eur", e.CurrencyCode) 233 | } 234 | wantError := `invalid currency code "eur"` 235 | if e.Error() != wantError { 236 | t.Errorf("got %v, want %v", e.Error(), wantError) 237 | } 238 | } else { 239 | t.Errorf("got %T, want currency.InvalidCurrencyCodeError", err) 240 | } 241 | 242 | _, err = a.Convert("EUR", "INVALID") 243 | if e, ok := err.(currency.InvalidNumberError); ok { 244 | if e.Number != "INVALID" { 245 | t.Errorf("got %v, want INVALID", e.Number) 246 | } 247 | wantError := `invalid number "INVALID"` 248 | if e.Error() != wantError { 249 | t.Errorf("got %v, want %v", e.Error(), wantError) 250 | } 251 | } else { 252 | t.Errorf("got %T, want currency.InvalidNumberError", err) 253 | } 254 | 255 | b, err := a.Convert("EUR", "0.91") 256 | if err != nil { 257 | t.Errorf("unexpected error: %v", err) 258 | } 259 | if b.String() != "19.1009 EUR" { 260 | t.Errorf("got %v, want 19.1009 EUR", b.String()) 261 | } 262 | // Confirm that a is unchanged. 263 | if a.String() != "20.99 USD" { 264 | t.Errorf("got %v, want 20.99 USD", a.String()) 265 | } 266 | 267 | // An amount larger than math.MaxInt64. 268 | c, _ := currency.NewAmount("922337203685477598799", "USD") 269 | d, err := c.Convert("RSD", "100") 270 | if err != nil { 271 | t.Errorf("unexpected error: %v", err) 272 | } 273 | if d.String() != "92233720368547759879900 RSD" { 274 | t.Errorf("got %v, want 92233720368547759879900 RSD", d.String()) 275 | } 276 | } 277 | 278 | func TestAmount_Add(t *testing.T) { 279 | a, _ := currency.NewAmount("20.99", "USD") 280 | b, _ := currency.NewAmount("3.50", "USD") 281 | x, _ := currency.NewAmount("99.99", "EUR") 282 | var z currency.Amount 283 | 284 | _, err := a.Add(x) 285 | if e, ok := err.(currency.MismatchError); ok { 286 | if e.A != a { 287 | t.Errorf("got %v, want %v", e.A, a) 288 | } 289 | if e.B != x { 290 | t.Errorf("got %v, want %v", e.B, x) 291 | } 292 | wantError := `amounts "20.99 USD" and "99.99 EUR" have mismatched currency codes` 293 | if e.Error() != wantError { 294 | t.Errorf("got %v, want %v", e.Error(), wantError) 295 | } 296 | } else { 297 | t.Errorf("got %T, want currency.MismatchError", err) 298 | } 299 | 300 | c, err := a.Add(b) 301 | if err != nil { 302 | t.Errorf("unexpected error: %v", err) 303 | } 304 | if c.String() != "24.49 USD" { 305 | t.Errorf("got %v, want 24.49 USD", c.String()) 306 | } 307 | // Confirm that a and b are unchanged. 308 | if a.String() != "20.99 USD" { 309 | t.Errorf("got %v, want 20.99 USD", a.String()) 310 | } 311 | if b.String() != "3.50 USD" { 312 | t.Errorf("got %v, want 3.50 USD", b.String()) 313 | } 314 | 315 | // An amount equal to math.MaxInt64. 316 | d, _ := currency.NewAmount("9223372036854775807", "USD") 317 | e, err := d.Add(a) 318 | if err != nil { 319 | t.Errorf("unexpected error: %v", err) 320 | } 321 | if e.String() != "9223372036854775827.99 USD" { 322 | t.Errorf("got %v, want 9223372036854775827.99 USD", e.String()) 323 | } 324 | 325 | // Test that addition with the zero value works and yields the other operand. 326 | f, err := a.Add(z) 327 | if err != nil { 328 | t.Errorf("unexpected error: %v", err) 329 | } 330 | if !f.Equal(a) { 331 | t.Errorf("%v + zero = %v, want %v", a, f, a) 332 | } 333 | 334 | g, err := z.Add(a) 335 | if err != nil { 336 | t.Errorf("unexpected error: %v", err) 337 | } 338 | if !g.Equal(a) { 339 | t.Errorf("%v + zero = %v, want %v", a, g, a) 340 | } 341 | } 342 | 343 | func TestAmount_Sub(t *testing.T) { 344 | a, _ := currency.NewAmount("20.99", "USD") 345 | b, _ := currency.NewAmount("3.50", "USD") 346 | x, _ := currency.NewAmount("99.99", "EUR") 347 | var z currency.Amount 348 | 349 | _, err := a.Sub(x) 350 | if e, ok := err.(currency.MismatchError); ok { 351 | if e.A != a { 352 | t.Errorf("got %v, want %v", e.A, a) 353 | } 354 | if e.B != x { 355 | t.Errorf("got %v, want %v", e.B, x) 356 | } 357 | wantError := `amounts "20.99 USD" and "99.99 EUR" have mismatched currency codes` 358 | if e.Error() != wantError { 359 | t.Errorf("got %v, want %v", e.Error(), wantError) 360 | } 361 | } else { 362 | t.Errorf("got %T, want currency.MismatchError", err) 363 | } 364 | 365 | c, err := a.Sub(b) 366 | if err != nil { 367 | t.Errorf("unexpected error: %v", err) 368 | } 369 | if c.String() != "17.49 USD" { 370 | t.Errorf("got %v, want 17.49 USD", c.String()) 371 | } 372 | // Confirm that a and b are unchanged. 373 | if a.String() != "20.99 USD" { 374 | t.Errorf("got %v, want 20.99 USD", a.String()) 375 | } 376 | if b.String() != "3.50 USD" { 377 | t.Errorf("got %v, want 3.50 USD", b.String()) 378 | } 379 | 380 | // An amount larger than math.MaxInt64. 381 | d, _ := currency.NewAmount("922337203685477598799", "USD") 382 | e, err := d.Sub(a) 383 | if err != nil { 384 | t.Errorf("unexpected error: %v", err) 385 | } 386 | if e.String() != "922337203685477598778.01 USD" { 387 | t.Errorf("got %v, want 922337203685477598778.01 USD", e.String()) 388 | } 389 | 390 | // Test that subtraction with the zero value works. 391 | f, err := a.Sub(z) 392 | if err != nil { 393 | t.Errorf("unexpected error: %v", err) 394 | } 395 | if !f.Equal(a) { 396 | t.Errorf("%v - zero = %v, want %v", a, f, a) 397 | } 398 | 399 | g, err := z.Sub(a) 400 | if err != nil { 401 | t.Errorf("unexpected error: %v", err) 402 | } 403 | negA, err := a.Mul("-1") 404 | if err != nil { 405 | t.Errorf("unexpected error: %v", err) 406 | } 407 | if !g.Equal(negA) { 408 | t.Errorf("zero - %v = %v, want %v", a, g, negA) 409 | } 410 | } 411 | 412 | func TestAmount_Mul(t *testing.T) { 413 | a, _ := currency.NewAmount("20.99", "USD") 414 | 415 | _, err := a.Mul("INVALID") 416 | if e, ok := err.(currency.InvalidNumberError); ok { 417 | if e.Number != "INVALID" { 418 | t.Errorf("got %v, want INVALID", e.Number) 419 | } 420 | wantError := `invalid number "INVALID"` 421 | if e.Error() != wantError { 422 | t.Errorf("got %v, want %v", e.Error(), wantError) 423 | } 424 | } else { 425 | t.Errorf("got %T, want currency.InvalidNumberError", err) 426 | } 427 | 428 | b, err := a.Mul("0.20") 429 | if err != nil { 430 | t.Errorf("unexpected error: %v", err) 431 | } 432 | if b.String() != "4.1980 USD" { 433 | t.Errorf("got %v, want 4.1980 USD", b.String()) 434 | } 435 | // Confirm that a is unchanged. 436 | if a.String() != "20.99 USD" { 437 | t.Errorf("got %v, want 20.99 USD", a.String()) 438 | } 439 | 440 | // An amount equal to math.MaxInt64. 441 | d, _ := currency.NewAmount("9223372036854775807", "USD") 442 | e, err := d.Mul("10") 443 | if err != nil { 444 | t.Errorf("unexpected error: %v", err) 445 | } 446 | if e.String() != "92233720368547758070 USD" { 447 | t.Errorf("got %v, want 92233720368547758070 USD", e.String()) 448 | } 449 | } 450 | 451 | func TestAmount_Div(t *testing.T) { 452 | a, _ := currency.NewAmount("99.99", "USD") 453 | 454 | for _, n := range []string{"INVALID", "0"} { 455 | _, err := a.Div(n) 456 | if e, ok := err.(currency.InvalidNumberError); ok { 457 | if e.Number != n { 458 | t.Errorf("got %v, want %v", e.Number, n) 459 | } 460 | } else { 461 | t.Errorf("got %T, want currency.InvalidNumberError", err) 462 | } 463 | } 464 | 465 | b, err := a.Div("3") 466 | if err != nil { 467 | t.Errorf("unexpected error: %v", err) 468 | } 469 | if b.String() != "33.33 USD" { 470 | t.Errorf("got %v, want 33.33 USD", b.String()) 471 | } 472 | // Confirm that a is unchanged. 473 | if a.String() != "99.99 USD" { 474 | t.Errorf("got %v, want 99.99 USD", a.String()) 475 | } 476 | 477 | // An amount equal to math.MaxInt64. 478 | d, _ := currency.NewAmount("9223372036854775807", "USD") 479 | e, err := d.Div("0.5") 480 | if err != nil { 481 | t.Errorf("unexpected error: %v", err) 482 | } 483 | if e.String() != "18446744073709551614 USD" { 484 | t.Errorf("got %v, want 18446744073709551614 USD", e.String()) 485 | } 486 | } 487 | 488 | func TestAmount_Round(t *testing.T) { 489 | tests := []struct { 490 | number string 491 | currencyCode string 492 | want string 493 | }{ 494 | {"12.345", "USD", "12.35"}, 495 | {"12.345", "JPY", "12"}, 496 | } 497 | 498 | for _, tt := range tests { 499 | t.Run("", func(t *testing.T) { 500 | a, _ := currency.NewAmount(tt.number, tt.currencyCode) 501 | b := a.Round() 502 | if b.Number() != tt.want { 503 | t.Errorf("got %v, want %v", b.Number(), tt.want) 504 | } 505 | // Confirm that a is unchanged. 506 | if a.Number() != tt.number { 507 | t.Errorf("got %v, want %v", a.Number(), tt.number) 508 | } 509 | }) 510 | } 511 | } 512 | 513 | func TestAmount_RoundTo(t *testing.T) { 514 | tests := []struct { 515 | number string 516 | digits uint8 517 | mode currency.RoundingMode 518 | want string 519 | }{ 520 | {"12.343", 2, currency.RoundHalfUp, "12.34"}, 521 | {"12.345", 2, currency.RoundHalfUp, "12.35"}, 522 | {"12.347", 2, currency.RoundHalfUp, "12.35"}, 523 | 524 | {"12.343", 2, currency.RoundHalfDown, "12.34"}, 525 | {"12.345", 2, currency.RoundHalfDown, "12.34"}, 526 | {"12.347", 2, currency.RoundHalfDown, "12.35"}, 527 | 528 | {"12.343", 2, currency.RoundUp, "12.35"}, 529 | {"12.345", 2, currency.RoundUp, "12.35"}, 530 | {"12.347", 2, currency.RoundUp, "12.35"}, 531 | 532 | {"12.343", 2, currency.RoundDown, "12.34"}, 533 | {"12.345", 2, currency.RoundDown, "12.34"}, 534 | {"12.347", 2, currency.RoundDown, "12.34"}, 535 | 536 | {"12.344", 2, currency.RoundHalfEven, "12.34"}, 537 | {"12.345", 2, currency.RoundHalfEven, "12.34"}, 538 | {"12.346", 2, currency.RoundHalfEven, "12.35"}, 539 | 540 | {"12.334", 2, currency.RoundHalfEven, "12.33"}, 541 | {"12.335", 2, currency.RoundHalfEven, "12.34"}, 542 | {"12.336", 2, currency.RoundHalfEven, "12.34"}, 543 | 544 | // Negative amounts. 545 | {"-12.345", 2, currency.RoundHalfUp, "-12.35"}, 546 | {"-12.345", 2, currency.RoundHalfDown, "-12.34"}, 547 | {"-12.345", 2, currency.RoundUp, "-12.35"}, 548 | {"-12.345", 2, currency.RoundDown, "-12.34"}, 549 | {"-12.345", 2, currency.RoundHalfEven, "-12.34"}, 550 | {"-12.335", 2, currency.RoundHalfEven, "-12.34"}, 551 | 552 | // More digits that the amount has. 553 | {"12.345", 4, currency.RoundHalfUp, "12.3450"}, 554 | {"12.345", 4, currency.RoundHalfDown, "12.3450"}, 555 | 556 | // Same number of digits that the amount has. 557 | {"12.345", 3, currency.RoundHalfUp, "12.345"}, 558 | {"12.345", 3, currency.RoundHalfDown, "12.345"}, 559 | {"12.345", 3, currency.RoundUp, "12.345"}, 560 | {"12.345", 3, currency.RoundDown, "12.345"}, 561 | 562 | // 0 digits. 563 | {"12.345", 0, currency.RoundHalfUp, "12"}, 564 | {"12.345", 0, currency.RoundHalfDown, "12"}, 565 | {"12.345", 0, currency.RoundUp, "13"}, 566 | {"12.345", 0, currency.RoundDown, "12"}, 567 | 568 | // Amounts larger than math.MaxInt64. 569 | {"12345678901234567890.0345", 3, currency.RoundHalfUp, "12345678901234567890.035"}, 570 | {"12345678901234567890.0345", 3, currency.RoundHalfDown, "12345678901234567890.034"}, 571 | {"12345678901234567890.0345", 3, currency.RoundUp, "12345678901234567890.035"}, 572 | {"12345678901234567890.0345", 3, currency.RoundDown, "12345678901234567890.034"}, 573 | } 574 | 575 | for _, tt := range tests { 576 | t.Run("", func(t *testing.T) { 577 | a, _ := currency.NewAmount(tt.number, "USD") 578 | b := a.RoundTo(tt.digits, tt.mode) 579 | if b.Number() != tt.want { 580 | t.Errorf("got %v, want %v", b.Number(), tt.want) 581 | } 582 | // Confirm that a is unchanged. 583 | if a.Number() != tt.number { 584 | t.Errorf("got %v, want %v", a.Number(), tt.number) 585 | } 586 | }) 587 | } 588 | } 589 | 590 | func TestAmount_RoundToWithConcurrency(t *testing.T) { 591 | n := 2 592 | roundingModes := []currency.RoundingMode{ 593 | currency.RoundHalfUp, 594 | currency.RoundHalfDown, 595 | currency.RoundUp, 596 | currency.RoundDown, 597 | } 598 | 599 | for _, roundingMode := range roundingModes { 600 | roundingMode := roundingMode 601 | 602 | t.Run(fmt.Sprintf("rounding_mode_%d", roundingMode), func(t *testing.T) { 603 | t.Parallel() 604 | 605 | var allDone sync.WaitGroup 606 | allDone.Add(n) 607 | 608 | for i := 0; i < n; i++ { 609 | go func() { 610 | defer allDone.Done() 611 | amount, _ := currency.NewAmount("10.99", "EUR") 612 | amount.RoundTo(1, roundingMode) 613 | }() 614 | } 615 | 616 | allDone.Wait() 617 | }) 618 | } 619 | } 620 | 621 | func TestAmount_Cmp(t *testing.T) { 622 | a, _ := currency.NewAmount("3.33", "USD") 623 | b, _ := currency.NewAmount("3.33", "EUR") 624 | _, err := a.Cmp(b) 625 | if e, ok := err.(currency.MismatchError); ok { 626 | if e.A != a { 627 | t.Errorf("got %v, want %v", e.A, a) 628 | } 629 | if e.B != b { 630 | t.Errorf("got %v, want %v", e.B, b) 631 | } 632 | wantError := `amounts "3.33 USD" and "3.33 EUR" have mismatched currency codes` 633 | if e.Error() != wantError { 634 | t.Errorf("got %v, want %v", e.Error(), wantError) 635 | } 636 | } else { 637 | t.Errorf("got %T, want currency.MismatchError", err) 638 | } 639 | 640 | tests := []struct { 641 | aNumber string 642 | bNumber string 643 | want int 644 | }{ 645 | {"3.33", "6.66", -1}, 646 | {"3.33", "3.33", 0}, 647 | {"6.66", "3.33", 1}, 648 | } 649 | 650 | for _, tt := range tests { 651 | t.Run("", func(t *testing.T) { 652 | a, _ := currency.NewAmount(tt.aNumber, "USD") 653 | b, _ := currency.NewAmount(tt.bNumber, "USD") 654 | got, err := a.Cmp(b) 655 | if err != nil { 656 | t.Errorf("unexpected error: %v", err) 657 | } 658 | if got != tt.want { 659 | t.Errorf("got %v, want %v", got, tt.want) 660 | } 661 | }) 662 | } 663 | } 664 | 665 | func TestAmount_Equal(t *testing.T) { 666 | tests := []struct { 667 | aNumber string 668 | aCurrencyCode string 669 | bNumber string 670 | bCurrencyCode string 671 | want bool 672 | }{ 673 | {"3.33", "USD", "6.66", "EUR", false}, 674 | {"3.33", "USD", "3.33", "EUR", false}, 675 | {"3.33", "USD", "3.33", "USD", true}, 676 | {"3.33", "USD", "6.66", "USD", false}, 677 | } 678 | 679 | for _, tt := range tests { 680 | t.Run("", func(t *testing.T) { 681 | a, _ := currency.NewAmount(tt.aNumber, tt.aCurrencyCode) 682 | b, _ := currency.NewAmount(tt.bNumber, tt.bCurrencyCode) 683 | got := a.Equal(b) 684 | if got != tt.want { 685 | t.Errorf("got %v, want %v", got, tt.want) 686 | } 687 | }) 688 | } 689 | } 690 | 691 | func TestAmount_Checks(t *testing.T) { 692 | tests := []struct { 693 | number string 694 | wantPositive bool 695 | wantNegative bool 696 | wantZero bool 697 | }{ 698 | {"9.99", true, false, false}, 699 | {"-9.99", false, true, false}, 700 | {"0", false, false, true}, 701 | } 702 | 703 | for _, tt := range tests { 704 | t.Run("", func(t *testing.T) { 705 | a, _ := currency.NewAmount(tt.number, "USD") 706 | gotPositive := a.IsPositive() 707 | gotNegative := a.IsNegative() 708 | gotZero := a.IsZero() 709 | if gotPositive != tt.wantPositive { 710 | t.Errorf("positive: got %v, want %v", gotPositive, tt.wantPositive) 711 | } 712 | if gotNegative != tt.wantNegative { 713 | t.Errorf("negative: got %v, want %v", gotNegative, tt.wantNegative) 714 | } 715 | if gotZero != tt.wantZero { 716 | t.Errorf("zero: got %v, want %v", gotZero, tt.wantZero) 717 | } 718 | }) 719 | } 720 | } 721 | 722 | func TestAmount_MarshalBinary(t *testing.T) { 723 | a, _ := currency.NewAmount("3.45", "USD") 724 | d, err := a.MarshalBinary() 725 | if err != nil { 726 | t.Errorf("unexpected error: %v", err) 727 | } 728 | got := string(d) 729 | want := "USD3.45" 730 | if got != want { 731 | t.Errorf("got %v, want %v", got, want) 732 | } 733 | } 734 | 735 | func TestAmount_UnmarshalBinary(t *testing.T) { 736 | d := []byte("US") 737 | a := ¤cy.Amount{} 738 | err := a.UnmarshalBinary(d) 739 | if e, ok := err.(currency.InvalidCurrencyCodeError); ok { 740 | if e.CurrencyCode != "US" { 741 | t.Errorf("got %v, want US", e.CurrencyCode) 742 | } 743 | wantError := `invalid currency code "US"` 744 | if e.Error() != wantError { 745 | t.Errorf("got %v, want %v", e.Error(), wantError) 746 | } 747 | } else { 748 | t.Errorf("got %T, want currency.InvalidCurrencyCodeError", err) 749 | } 750 | 751 | d = []byte("USD3,60") 752 | err = a.UnmarshalBinary(d) 753 | if e, ok := err.(currency.InvalidNumberError); ok { 754 | if e.Number != "3,60" { 755 | t.Errorf("got %v, want 3,60", e.Number) 756 | } 757 | wantError := `invalid number "3,60"` 758 | if e.Error() != wantError { 759 | t.Errorf("got %v, want %v", e.Error(), wantError) 760 | } 761 | } else { 762 | t.Errorf("got %T, want currency.InvalidNumberError", err) 763 | } 764 | 765 | d = []byte("XXX2.60") 766 | err = a.UnmarshalBinary(d) 767 | if e, ok := err.(currency.InvalidCurrencyCodeError); ok { 768 | if e.CurrencyCode != "XXX" { 769 | t.Errorf("got %v, want XXX", e.CurrencyCode) 770 | } 771 | wantError := `invalid currency code "XXX"` 772 | if e.Error() != wantError { 773 | t.Errorf("got %v, want %v", e.Error(), wantError) 774 | } 775 | } else { 776 | t.Errorf("got %T, want currency.InvalidCurrencyCodeError", err) 777 | } 778 | 779 | d = []byte("USD3.45") 780 | err = a.UnmarshalBinary(d) 781 | if err != nil { 782 | t.Errorf("unexpected error: %v", err) 783 | } 784 | if a.Number() != "3.45" { 785 | t.Errorf("got %v, want 3.45", a.Number()) 786 | } 787 | if a.CurrencyCode() != "USD" { 788 | t.Errorf("got %v, want USD", a.CurrencyCode()) 789 | } 790 | } 791 | 792 | func TestAmount_MarshalJSON(t *testing.T) { 793 | a, _ := currency.NewAmount("3.45", "USD") 794 | d, err := json.Marshal(a) 795 | if err != nil { 796 | t.Errorf("unexpected error: %v", err) 797 | } 798 | got := string(d) 799 | want := `{"number":"3.45","currency":"USD"}` 800 | if got != want { 801 | t.Errorf("got %v, want %v", got, want) 802 | } 803 | } 804 | 805 | func TestAmount_UnmarshalJSON(t *testing.T) { 806 | d := []byte(`{"number":"INVALID","currency":"USD"}`) 807 | unmarshalled := ¤cy.Amount{} 808 | err := json.Unmarshal(d, unmarshalled) 809 | if e, ok := err.(currency.InvalidNumberError); ok { 810 | if e.Number != "INVALID" { 811 | t.Errorf("got %v, want INVALID", e.Number) 812 | } 813 | wantError := `invalid number "INVALID"` 814 | if e.Error() != wantError { 815 | t.Errorf("got %v, want %v", e.Error(), wantError) 816 | } 817 | } else { 818 | t.Errorf("got %T, want currency.InvalidNumberError", err) 819 | } 820 | 821 | d = []byte(`{"number": {"key": "value"}, "currency": "USD"}`) 822 | err = json.Unmarshal(d, unmarshalled) 823 | if e, ok := err.(currency.InvalidNumberError); ok { 824 | if e.Number != `{"key": "value"}` { 825 | t.Errorf(`got %v, "want {"key": "value"}"`, e.Number) 826 | } 827 | wantError := `invalid number "{\"key\": \"value\"}"` 828 | if e.Error() != wantError { 829 | t.Errorf("got %v, want %v", e.Error(), wantError) 830 | } 831 | } else { 832 | t.Errorf("got %T, want currency.InvalidNumberError", err) 833 | } 834 | 835 | d = []byte(`{"number":3.45,"currency":"USD"}`) 836 | err = json.Unmarshal(d, unmarshalled) 837 | if err != nil { 838 | t.Errorf("unexpected error: %v", err) 839 | } 840 | if unmarshalled.Number() != "3.45" { 841 | t.Errorf("got %v, want 3.45", unmarshalled.Number()) 842 | } 843 | if unmarshalled.CurrencyCode() != "USD" { 844 | t.Errorf("got %v, want USD", unmarshalled.CurrencyCode()) 845 | } 846 | 847 | d = []byte(`{"number":"3.45","currency":"usd"}`) 848 | err = json.Unmarshal(d, unmarshalled) 849 | if e, ok := err.(currency.InvalidCurrencyCodeError); ok { 850 | if e.CurrencyCode != "usd" { 851 | t.Errorf("got %v, want usd", e.CurrencyCode) 852 | } 853 | wantError := `invalid currency code "usd"` 854 | if e.Error() != wantError { 855 | t.Errorf("got %v, want %v", e.Error(), wantError) 856 | } 857 | } else { 858 | t.Errorf("got %T, want currency.InvalidCurrencyCodeError", err) 859 | } 860 | 861 | d = []byte(`{"number":"3.45","currency":"USD"}`) 862 | err = json.Unmarshal(d, unmarshalled) 863 | if err != nil { 864 | t.Errorf("unexpected error: %v", err) 865 | } 866 | if unmarshalled.Number() != "3.45" { 867 | t.Errorf("got %v, want 3.45", unmarshalled.Number()) 868 | } 869 | if unmarshalled.CurrencyCode() != "USD" { 870 | t.Errorf("got %v, want USD", unmarshalled.CurrencyCode()) 871 | } 872 | 873 | d = []byte(`{'break_please'}`) 874 | amount := ¤cy.Amount{} 875 | err = amount.UnmarshalJSON(d) 876 | if err == nil { 877 | t.Errorf("error expected") 878 | } 879 | 880 | } 881 | 882 | func TestAmount_Value(t *testing.T) { 883 | a, _ := currency.NewAmount("3.45", "USD") 884 | got, _ := a.Value() 885 | want := "(3.45,USD)" 886 | if got != want { 887 | t.Errorf("got %v, want %v", got, want) 888 | } 889 | 890 | var b currency.Amount 891 | got, _ = b.Value() 892 | want = "(0,)" 893 | if got != want { 894 | t.Errorf("got %v, want %v", got, want) 895 | } 896 | } 897 | 898 | func TestAmount_Scan(t *testing.T) { 899 | tests := []struct { 900 | src string 901 | wantNumber string 902 | wantCurrencyCode string 903 | wantError string 904 | }{ 905 | {"", "0", "", ""}, 906 | {"(3.45,USD)", "3.45", "USD", ""}, 907 | {"(3.45,)", "0", "", `invalid currency code ""`}, 908 | {"(,USD)", "0", "", `invalid number ""`}, 909 | {"(0,)", "0", "", ""}, 910 | {"(0, )", "0", "", ""}, 911 | } 912 | 913 | for _, tt := range tests { 914 | t.Run("", func(t *testing.T) { 915 | var a currency.Amount 916 | err := a.Scan(tt.src) 917 | if a.Number() != tt.wantNumber { 918 | t.Errorf("number: got %v, want %v", a.Number(), tt.wantNumber) 919 | } 920 | if a.CurrencyCode() != tt.wantCurrencyCode { 921 | t.Errorf("currency code: got %v, want %v", a.CurrencyCode(), tt.wantCurrencyCode) 922 | } 923 | errStr := "" 924 | if err != nil { 925 | errStr = err.Error() 926 | } 927 | if errStr != tt.wantError { 928 | t.Errorf("error: got %v, want %v", errStr, tt.wantError) 929 | } 930 | }) 931 | } 932 | } 933 | 934 | func TestAmount_ScanNonString(t *testing.T) { 935 | var a currency.Amount 936 | err := a.Scan(123) 937 | 938 | wantError := "value is not a string: 123" 939 | errStr := "" 940 | if err != nil { 941 | errStr = err.Error() 942 | } 943 | if errStr != wantError { 944 | t.Errorf("error: got %v, want %v", errStr, wantError) 945 | } 946 | } 947 | -------------------------------------------------------------------------------- /data.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | //go:generate go run gen.go 3 | 4 | package currency 5 | 6 | // CLDRVersion is the CLDR version from which the data is derived. 7 | const CLDRVersion = "47.0.0" 8 | 9 | type numberingSystem uint8 10 | 11 | const ( 12 | numLatn numberingSystem = iota 13 | numArab 14 | numArabExt 15 | numBeng 16 | numDeva 17 | numMymr 18 | ) 19 | 20 | type currencyInfo struct { 21 | numericCode string 22 | digits uint8 23 | } 24 | 25 | type symbolInfo struct { 26 | symbol string 27 | locales []string 28 | } 29 | 30 | type currencyFormat struct { 31 | standardPattern string 32 | accountingPattern string 33 | numberingSystem numberingSystem 34 | minGroupingDigits uint8 35 | primaryGroupingSize uint8 36 | secondaryGroupingSize uint8 37 | decimalSeparator string 38 | groupingSeparator string 39 | plusSign string 40 | minusSign string 41 | } 42 | 43 | // Defined separately to ensure consistent ordering (G10, then others). 44 | var currencyCodes = []string{ 45 | // G10 currencies https://en.wikipedia.org/wiki/G10_currencies. 46 | "AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "NOK", "NZD", "SEK", "USD", 47 | 48 | // Other currencies. 49 | "AED", "AFN", "ALL", "AMD", "AOA", "ARS", "AWG", "AZN", "BAM", "BBD", 50 | "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", 51 | "BTN", "BWP", "BYN", "BZD", "CDF", "CHE", "CHW", "CLF", "CLP", "CNY", 52 | "COP", "COU", "CRC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", 53 | "EGP", "ERN", "ETB", "FJD", "FKP", "GEL", "GHS", "GIP", "GMD", "GNF", 54 | "GTQ", "GYD", "HKD", "HNL", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", 55 | "IRR", "ISK", "JMD", "JOD", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", 56 | "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", 57 | "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", 58 | "MXN", "MXV", "MYR", "MZN", "NAD", "NGN", "NIO", "NPR", "OMR", "PAB", 59 | "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", 60 | "RWF", "SAR", "SBD", "SCR", "SDG", "SGD", "SHP", "SLE", "SOS", "SRD", 61 | "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", 62 | "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USN", "UYI", "UYU", "UYW", 63 | "UZS", "VED", "VES", "VND", "VUV", "WST", "XAD", "XAF", "XCD", "XCG", 64 | "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWG", 65 | } 66 | 67 | var currencies = map[string]currencyInfo{ 68 | "AED": {"784", 2}, "AFN": {"971", 2}, "ALL": {"008", 2}, 69 | "AMD": {"051", 2}, "AOA": {"973", 2}, "ARS": {"032", 2}, 70 | "AUD": {"036", 2}, "AWG": {"533", 2}, "AZN": {"944", 2}, 71 | "BAM": {"977", 2}, "BBD": {"052", 2}, "BDT": {"050", 2}, 72 | "BGN": {"975", 2}, "BHD": {"048", 3}, "BIF": {"108", 0}, 73 | "BMD": {"060", 2}, "BND": {"096", 2}, "BOB": {"068", 2}, 74 | "BOV": {"984", 2}, "BRL": {"986", 2}, "BSD": {"044", 2}, 75 | "BTN": {"064", 2}, "BWP": {"072", 2}, "BYN": {"933", 2}, 76 | "BZD": {"084", 2}, "CAD": {"124", 2}, "CDF": {"976", 2}, 77 | "CHE": {"947", 2}, "CHF": {"756", 2}, "CHW": {"948", 2}, 78 | "CLF": {"990", 4}, "CLP": {"152", 0}, "CNY": {"156", 2}, 79 | "COP": {"170", 2}, "COU": {"970", 2}, "CRC": {"188", 2}, 80 | "CUP": {"192", 2}, "CVE": {"132", 2}, "CZK": {"203", 2}, 81 | "DJF": {"262", 0}, "DKK": {"208", 2}, "DOP": {"214", 2}, 82 | "DZD": {"012", 2}, "EGP": {"818", 2}, "ERN": {"232", 2}, 83 | "ETB": {"230", 2}, "EUR": {"978", 2}, "FJD": {"242", 2}, 84 | "FKP": {"238", 2}, "GBP": {"826", 2}, "GEL": {"981", 2}, 85 | "GHS": {"936", 2}, "GIP": {"292", 2}, "GMD": {"270", 2}, 86 | "GNF": {"324", 0}, "GTQ": {"320", 2}, "GYD": {"328", 2}, 87 | "HKD": {"344", 2}, "HNL": {"340", 2}, "HTG": {"332", 2}, 88 | "HUF": {"348", 2}, "IDR": {"360", 2}, "ILS": {"376", 2}, 89 | "INR": {"356", 2}, "IQD": {"368", 3}, "IRR": {"364", 2}, 90 | "ISK": {"352", 0}, "JMD": {"388", 2}, "JOD": {"400", 3}, 91 | "JPY": {"392", 0}, "KES": {"404", 2}, "KGS": {"417", 2}, 92 | "KHR": {"116", 2}, "KMF": {"174", 0}, "KPW": {"408", 2}, 93 | "KRW": {"410", 0}, "KWD": {"414", 3}, "KYD": {"136", 2}, 94 | "KZT": {"398", 2}, "LAK": {"418", 2}, "LBP": {"422", 2}, 95 | "LKR": {"144", 2}, "LRD": {"430", 2}, "LSL": {"426", 2}, 96 | "LYD": {"434", 3}, "MAD": {"504", 2}, "MDL": {"498", 2}, 97 | "MGA": {"969", 2}, "MKD": {"807", 2}, "MMK": {"104", 2}, 98 | "MNT": {"496", 2}, "MOP": {"446", 2}, "MRU": {"929", 2}, 99 | "MUR": {"480", 2}, "MVR": {"462", 2}, "MWK": {"454", 2}, 100 | "MXN": {"484", 2}, "MXV": {"979", 2}, "MYR": {"458", 2}, 101 | "MZN": {"943", 2}, "NAD": {"516", 2}, "NGN": {"566", 2}, 102 | "NIO": {"558", 2}, "NOK": {"578", 2}, "NPR": {"524", 2}, 103 | "NZD": {"554", 2}, "OMR": {"512", 3}, "PAB": {"590", 2}, 104 | "PEN": {"604", 2}, "PGK": {"598", 2}, "PHP": {"608", 2}, 105 | "PKR": {"586", 2}, "PLN": {"985", 2}, "PYG": {"600", 0}, 106 | "QAR": {"634", 2}, "RON": {"946", 2}, "RSD": {"941", 2}, 107 | "RUB": {"643", 2}, "RWF": {"646", 0}, "SAR": {"682", 2}, 108 | "SBD": {"090", 2}, "SCR": {"690", 2}, "SDG": {"938", 2}, 109 | "SEK": {"752", 2}, "SGD": {"702", 2}, "SHP": {"654", 2}, 110 | "SLE": {"925", 2}, "SOS": {"706", 2}, "SRD": {"968", 2}, 111 | "SSP": {"728", 2}, "STN": {"930", 2}, "SVC": {"222", 2}, 112 | "SYP": {"760", 2}, "SZL": {"748", 2}, "THB": {"764", 2}, 113 | "TJS": {"972", 2}, "TMT": {"934", 2}, "TND": {"788", 3}, 114 | "TOP": {"776", 2}, "TRY": {"949", 2}, "TTD": {"780", 2}, 115 | "TWD": {"901", 2}, "TZS": {"834", 2}, "UAH": {"980", 2}, 116 | "UGX": {"800", 0}, "USD": {"840", 2}, "USN": {"997", 2}, 117 | "UYI": {"940", 0}, "UYU": {"858", 2}, "UYW": {"927", 4}, 118 | "UZS": {"860", 2}, "VED": {"926", 2}, "VES": {"928", 2}, 119 | "VND": {"704", 0}, "VUV": {"548", 0}, "WST": {"882", 2}, 120 | "XAD": {"396", 2}, "XAF": {"950", 0}, "XCD": {"951", 2}, 121 | "XCG": {"532", 2}, "XOF": {"952", 0}, "XPF": {"953", 0}, 122 | "YER": {"886", 2}, "ZAR": {"710", 2}, "ZMW": {"967", 2}, 123 | "ZWG": {"924", 2}, 124 | } 125 | 126 | var currencySymbols = map[string][]symbolInfo{ 127 | "AED": { 128 | {"AED", []string{"en"}}, 129 | {"د.إ.\u200f", []string{"ar"}}, 130 | }, 131 | "AFN": { 132 | {"AFN", []string{"en"}}, 133 | {"؋", []string{"fa", "ps"}}, 134 | }, 135 | "ALL": { 136 | {"ALL", []string{"en"}}, 137 | {"Lekë", []string{"sq"}}, 138 | }, 139 | "AMD": { 140 | {"AMD", []string{"en"}}, 141 | {"֏", []string{"hy"}}, 142 | }, 143 | "AOA": { 144 | {"AOA", []string{"en"}}, 145 | {"Kz", []string{"pt-AO"}}, 146 | }, 147 | "ARS": { 148 | {"ARS", []string{"en", "fr-CA"}}, 149 | {"$", []string{"es-AR"}}, 150 | {"$AR", []string{"fr"}}, 151 | }, 152 | "AUD": { 153 | {"A$", []string{"en"}}, 154 | {"$", []string{"en-AU", "en-CC", "en-CX", "en-KI", "en-NF", "en-NR", "en-TV"}}, 155 | {"$AU", []string{"fr"}}, 156 | {"$\u00a0AU", []string{"fr-CA"}}, 157 | {"AU$", []string{"am", "ar", "ca", "cs", "da", "de", "et", "id", "ko", "lv", "nl", "pt", "th", "tr", "vi", "yue", "yue-Hans", "zh", "zh-Hant"}}, 158 | }, 159 | "AWG": { 160 | {"AWG", []string{"en"}}, 161 | {"Afl", []string{"my"}}, 162 | {"Afl.", []string{"nl-AW"}}, 163 | }, 164 | "AZN": { 165 | {"AZN", []string{"en"}}, 166 | {"₼", []string{"az"}}, 167 | }, 168 | "BAM": { 169 | {"BAM", []string{"en"}}, 170 | {"KM", []string{"bs", "hr-BA", "sr-Latn"}}, 171 | {"КМ", []string{"sr"}}, 172 | }, 173 | "BBD": { 174 | {"BBD", []string{"en"}}, 175 | {"$", []string{"en-BB"}}, 176 | {"Bds$", []string{"sv"}}, 177 | {"DBB", []string{"so"}}, 178 | }, 179 | "BDT": { 180 | {"BDT", []string{"en"}}, 181 | {"৳", []string{"bn"}}, 182 | }, 183 | "BGN": { 184 | {"BGN", []string{"en"}}, 185 | {"лв.", []string{"bg"}}, 186 | }, 187 | "BHD": { 188 | {"BHD", []string{"en"}}, 189 | {"د.ب.\u200f", []string{"ar"}}, 190 | }, 191 | "BIF": { 192 | {"BIF", []string{"en"}}, 193 | {"FBu", []string{"en-BI", "fr-BI"}}, 194 | }, 195 | "BMD": { 196 | {"BMD", []string{"en", "fr-CA"}}, 197 | {"$", []string{"en-BM"}}, 198 | {"$BM", []string{"fr"}}, 199 | {"BM$", []string{"sv"}}, 200 | }, 201 | "BND": { 202 | {"BND", []string{"en", "fr-CA"}}, 203 | {"$", []string{"ms-BN"}}, 204 | {"$BN", []string{"fr"}}, 205 | }, 206 | "BOB": { 207 | {"BOB", []string{"en"}}, 208 | {"Bs", []string{"es-BO"}}, 209 | }, 210 | "BRL": { 211 | {"R$", []string{"en"}}, 212 | {"BR$", []string{"sv"}}, 213 | }, 214 | "BSD": { 215 | {"BSD", []string{"en"}}, 216 | {"$", []string{"en-BS"}}, 217 | {"BS$", []string{"sv"}}, 218 | }, 219 | "BWP": { 220 | {"BWP", []string{"en"}}, 221 | {"P", []string{"en-BW"}}, 222 | }, 223 | "BYN": { 224 | {"BYN", []string{"en"}}, 225 | {"Br", []string{"be", "ru-BY"}}, 226 | }, 227 | "BZD": { 228 | {"BZD", []string{"en", "fr-CA"}}, 229 | {"$", []string{"en-BZ", "es-BZ"}}, 230 | {"$BZ", []string{"fr"}}, 231 | {"BZ$", []string{"sv"}}, 232 | }, 233 | "CAD": { 234 | {"CA$", []string{"en"}}, 235 | {"$", []string{"en-CA", "fr-CA"}}, 236 | {"$CA", []string{"fa", "fr"}}, 237 | {"C$", []string{"nl"}}, 238 | {"KA$", []string{"pcm"}}, 239 | }, 240 | "CDF": { 241 | {"CDF", []string{"en"}}, 242 | {"FC", []string{"fr-CD", "sw-CD"}}, 243 | }, 244 | "CLP": { 245 | {"CLP", []string{"en", "fr-CA"}}, 246 | {"$", []string{"es-CL"}}, 247 | {"$CL", []string{"fr"}}, 248 | }, 249 | "CNY": { 250 | {"CN¥", []string{"en", "zh-Hans-HK", "zh-Hans-MO", "zh-Hans-SG"}}, 251 | {"¥", []string{"zh"}}, 252 | {"¥CN", []string{"fa"}}, 253 | {"\u200eCN¥\u200e", []string{"he"}}, 254 | {"元", []string{"ja"}}, 255 | {"¥", []string{"yue-Hans"}}, 256 | }, 257 | "COP": { 258 | {"COP", []string{"en", "fr-CA"}}, 259 | {"$", []string{"es-CO"}}, 260 | {"$CO", []string{"fr"}}, 261 | }, 262 | "CRC": { 263 | {"CRC", []string{"en"}}, 264 | {"₡", []string{"es-CR"}}, 265 | }, 266 | "CUP": { 267 | {"CUP", []string{"en"}}, 268 | {"$", []string{"es-CU"}}, 269 | }, 270 | "CVE": { 271 | {"CVE", []string{"en"}}, 272 | {"\u200b", []string{"pt-CV"}}, 273 | }, 274 | "CZK": { 275 | {"CZK", []string{"en"}}, 276 | {"Kč", []string{"cs"}}, 277 | }, 278 | "DJF": { 279 | {"DJF", []string{"en"}}, 280 | {"Fdj", []string{"ar-DJ", "fr-DJ", "so-DJ"}}, 281 | }, 282 | "DKK": { 283 | {"DKK", []string{"en"}}, 284 | {"Dkr", []string{"sv"}}, 285 | {"kr.", []string{"da", "en-DK"}}, 286 | }, 287 | "DOP": { 288 | {"DOP", []string{"en"}}, 289 | {"RD$", []string{"es-DO", "sv"}}, 290 | }, 291 | "DZD": { 292 | {"DZD", []string{"en"}}, 293 | {"DA", []string{"fr-DZ"}}, 294 | {"د.ج.\u200f", []string{"ar"}}, 295 | }, 296 | "EGP": { 297 | {"EGP", []string{"en"}}, 298 | {"EG£", []string{"sv"}}, 299 | {"ج.م.\u200f", []string{"ar"}}, 300 | }, 301 | "ERN": { 302 | {"ERN", []string{"en"}}, 303 | {"Nfk", []string{"ar-ER", "en-ER", "ti-ER"}}, 304 | }, 305 | "ETB": { 306 | {"ETB", []string{"en"}}, 307 | {"Br", []string{"so-ET", "ti"}}, 308 | {"ብር", []string{"am"}}, 309 | }, 310 | "EUR": { 311 | {"€", []string{"en"}}, 312 | }, 313 | "FJD": { 314 | {"FJD", []string{"en", "fr-CA"}}, 315 | {"$", []string{"en-FJ"}}, 316 | {"$FJ", []string{"fr"}}, 317 | {"FJ$", []string{"nl"}}, 318 | }, 319 | "FKP": { 320 | {"FKP", []string{"en", "fr-CA"}}, 321 | {"£", []string{"en-FK"}}, 322 | {"£FK", []string{"fr"}}, 323 | }, 324 | "GBP": { 325 | {"£", []string{"en", "fr-CA"}}, 326 | {"GB£", []string{"ar-SS", "en-FK", "en-GI", "en-MT", "en-SH", "en-SS"}}, 327 | {"UK£", []string{"ar"}}, 328 | {"£GB", []string{"fr"}}, 329 | }, 330 | "GEL": { 331 | {"GEL", []string{"en"}}, 332 | {"₾", []string{"ka"}}, 333 | }, 334 | "GHS": { 335 | {"GHS", []string{"en"}}, 336 | {"GH₵", []string{"en-GH", "ha-GH"}}, 337 | }, 338 | "GIP": { 339 | {"GIP", []string{"en", "fr-CA"}}, 340 | {"£", []string{"en-GI"}}, 341 | {"£GI", []string{"fr"}}, 342 | }, 343 | "GMD": { 344 | {"GMD", []string{"en"}}, 345 | {"D", []string{"en-GM"}}, 346 | }, 347 | "GNF": { 348 | {"GNF", []string{"en"}}, 349 | {"FG", []string{"fr-GN"}}, 350 | }, 351 | "GTQ": { 352 | {"GTQ", []string{"en"}}, 353 | {"Q", []string{"es-GT"}}, 354 | }, 355 | "GYD": { 356 | {"GYD", []string{"en"}}, 357 | {"$", []string{"en-GY"}}, 358 | }, 359 | "HKD": { 360 | {"HK$", []string{"en"}}, 361 | {"$HK", []string{"fa"}}, 362 | {"$\u00a0HK", []string{"fr-CA"}}, 363 | }, 364 | "HNL": { 365 | {"HNL", []string{"en"}}, 366 | {"L", []string{"es-HN"}}, 367 | }, 368 | "HTG": { 369 | {"HTG", []string{"en"}}, 370 | {"G", []string{"fr-HT", "my"}}, 371 | }, 372 | "HUF": { 373 | {"HUF", []string{"en"}}, 374 | {"Ft", []string{"hu"}}, 375 | }, 376 | "IDR": { 377 | {"IDR", []string{"en"}}, 378 | {"Rp", []string{"en-ID", "id", "jv", "ms-ID"}}, 379 | }, 380 | "ILS": { 381 | {"₪", []string{"en"}}, 382 | {"NIS", []string{"sk"}}, 383 | }, 384 | "INR": { 385 | {"₹", []string{"en"}}, 386 | {"Rs", []string{"id"}}, 387 | }, 388 | "IQD": { 389 | {"IQD", []string{"en"}}, 390 | {"د.ع.\u200f", []string{"ar"}}, 391 | }, 392 | "IRR": { 393 | {"IRR", []string{"en"}}, 394 | {"ر.إ.", []string{"ar"}}, 395 | {"ریال", []string{"fa"}}, 396 | }, 397 | "ISK": { 398 | {"ISK", []string{"en"}}, 399 | {"Ikr", []string{"sv"}}, 400 | {"kr.", []string{"is"}}, 401 | }, 402 | "JMD": { 403 | {"JMD", []string{"en"}}, 404 | {"$", []string{"en-JM"}}, 405 | {"JM$", []string{"sv"}}, 406 | }, 407 | "JOD": { 408 | {"JOD", []string{"en"}}, 409 | {"د.أ.\u200f", []string{"ar"}}, 410 | }, 411 | "JPY": { 412 | {"¥", []string{"en", "en-AU"}}, 413 | {"JP¥", []string{"af", "am", "ar", "as", "az", "bn", "chr", "cs", "cy", "da", "el", "el-polyton", "en-001", "en-CA", "eu", "gd", "gl", "gu", "hi", "hy", "id", "is", "jv", "kk", "km", "kn", "ko", "kok", "ky", "lo", "mn", "mr", "ms", "my", "ne", "nl", "pa", "pcm", "ps", "pt", "sd", "si", "so", "sq", "sw", "te", "tk", "ur", "uz", "yo", "yue-Hans", "zh", "zu"}}, 414 | {"¥", []string{"ja"}}, 415 | }, 416 | "KES": { 417 | {"KES", []string{"en"}}, 418 | {"Ksh", []string{"en-KE", "so-KE", "sw"}}, 419 | }, 420 | "KGS": { 421 | {"KGS", []string{"en"}}, 422 | {"сом", []string{"ky", "ru-KG"}}, 423 | }, 424 | "KHR": { 425 | {"KHR", []string{"en"}}, 426 | {"៛", []string{"km"}}, 427 | }, 428 | "KMF": { 429 | {"KMF", []string{"en"}}, 430 | {"CF", []string{"ar-KM", "fr-KM"}}, 431 | }, 432 | "KRW": { 433 | {"₩", []string{"en", "zh-Hant-HK"}}, 434 | {"₩", []string{"yue", "yue-Hans", "zh-Hant"}}, 435 | }, 436 | "KWD": { 437 | {"KWD", []string{"en"}}, 438 | {"د.ك.\u200f", []string{"ar"}}, 439 | }, 440 | "KYD": { 441 | {"KYD", []string{"en"}}, 442 | {"$", []string{"en-KY"}}, 443 | }, 444 | "KZT": { 445 | {"KZT", []string{"en"}}, 446 | {"₸", []string{"kk", "ru-KZ"}}, 447 | }, 448 | "LAK": { 449 | {"LAK", []string{"en"}}, 450 | {"₭", []string{"lo"}}, 451 | }, 452 | "LBP": { 453 | {"LBP", []string{"en", "fr-CA"}}, 454 | {"£LB", []string{"fr"}}, 455 | {"ل.ل.\u200f", []string{"ar"}}, 456 | }, 457 | "LKR": { 458 | {"LKR", []string{"en"}}, 459 | {"Rs.", []string{"ta-LK"}}, 460 | {"රු.", []string{"si"}}, 461 | }, 462 | "LRD": { 463 | {"LRD", []string{"en"}}, 464 | {"$", []string{"en-LR"}}, 465 | }, 466 | "LSL": { 467 | {"LSL", []string{"en"}}, 468 | {"ЛСЛ", []string{"kk"}}, 469 | {"ឡូទី", []string{"km"}}, 470 | }, 471 | "LYD": { 472 | {"LYD", []string{"en"}}, 473 | {"د.ل.\u200f", []string{"ar"}}, 474 | }, 475 | "MAD": { 476 | {"MAD", []string{"en"}}, 477 | {"د.م.\u200f", []string{"ar"}}, 478 | }, 479 | "MDL": { 480 | {"MDL", []string{"en"}}, 481 | {"L", []string{"ro-MD", "ru-MD"}}, 482 | }, 483 | "MGA": { 484 | {"MGA", []string{"en"}}, 485 | {"Ar", []string{"en-MG", "fr-MG"}}, 486 | }, 487 | "MKD": { 488 | {"MKD", []string{"en"}}, 489 | {"den", []string{"sq-MK"}}, 490 | {"ден.", []string{"mk"}}, 491 | }, 492 | "MMK": { 493 | {"MMK", []string{"en"}}, 494 | {"K", []string{"my"}}, 495 | }, 496 | "MNT": { 497 | {"MNT", []string{"en"}}, 498 | {"₮", []string{"mn"}}, 499 | }, 500 | "MOP": { 501 | {"MOP", []string{"en"}}, 502 | {"MOP$", []string{"en-MO", "pt-MO", "zh-Hans-MO", "zh-Hant-MO"}}, 503 | }, 504 | "MRU": { 505 | {"MRU", []string{"en"}}, 506 | {"UM", []string{"es-MX", "fr-MR"}}, 507 | {"أ.م.", []string{"ar"}}, 508 | }, 509 | "MUR": { 510 | {"MUR", []string{"en"}}, 511 | {"Rs", []string{"en-MU", "fr-MU"}}, 512 | }, 513 | "MVR": { 514 | {"MVR", []string{"en"}}, 515 | {"Rf", []string{"en-MV"}}, 516 | }, 517 | "MWK": { 518 | {"MWK", []string{"en"}}, 519 | {"MK", []string{"en-MW"}}, 520 | }, 521 | "MXN": { 522 | {"MX$", []string{"en", "fr-CA"}}, 523 | {"$", []string{"es-MX"}}, 524 | {"$MX", []string{"fa", "fr", "gl"}}, 525 | }, 526 | "MYR": { 527 | {"MYR", []string{"en"}}, 528 | {"RM", []string{"en-MY", "ms", "ta-MY", "ta-SG"}}, 529 | }, 530 | "MZN": { 531 | {"MZN", []string{"en"}}, 532 | {"MTn", []string{"pt-MZ"}}, 533 | }, 534 | "NAD": { 535 | {"NAD", []string{"en", "fr-CA"}}, 536 | {"$", []string{"af-NA", "en-NA"}}, 537 | {"$NA", []string{"fr"}}, 538 | }, 539 | "NGN": { 540 | {"NGN", []string{"en"}}, 541 | {"₦", []string{"en-NG", "ha", "ig", "pcm", "yo"}}, 542 | }, 543 | "NIO": { 544 | {"NIO", []string{"en"}}, 545 | {"C$", []string{"es-NI"}}, 546 | }, 547 | "NOK": { 548 | {"NOK", []string{"en"}}, 549 | {"Nkr", []string{"sv"}}, 550 | {"kr", []string{"en-NO", "no"}}, 551 | }, 552 | "NPR": { 553 | {"NPR", []string{"en"}}, 554 | {"नेरू", []string{"ne"}}, 555 | }, 556 | "NZD": { 557 | {"NZ$", []string{"en"}}, 558 | {"$", []string{"en-CK", "en-NU", "en-NZ", "en-PN", "en-TK"}}, 559 | {"$NZ", []string{"fa", "fr"}}, 560 | {"$\u00a0NZ", []string{"fr-CA"}}, 561 | }, 562 | "OMR": { 563 | {"OMR", []string{"en"}}, 564 | {"ر.ع.\u200f", []string{"ar"}}, 565 | }, 566 | "PAB": { 567 | {"PAB", []string{"en"}}, 568 | {"B/.", []string{"es-PA", "my"}}, 569 | }, 570 | "PEN": { 571 | {"PEN", []string{"en"}}, 572 | {"S/", []string{"es-PE"}}, 573 | }, 574 | "PGK": { 575 | {"PGK", []string{"en"}}, 576 | {"K", []string{"en-PG"}}, 577 | }, 578 | "PHP": { 579 | {"₱", []string{"en"}}, 580 | }, 581 | "PKR": { 582 | {"PKR", []string{"en", "ur-IN"}}, 583 | {"Rs", []string{"en-PK", "ps-PK", "sd", "ur"}}, 584 | }, 585 | "PLN": { 586 | {"PLN", []string{"en"}}, 587 | {"zł", []string{"dsb", "hsb", "pl"}}, 588 | }, 589 | "PYG": { 590 | {"PYG", []string{"en"}}, 591 | {"Gs.", []string{"es-PY"}}, 592 | }, 593 | "QAR": { 594 | {"QAR", []string{"en"}}, 595 | {"ر.ق.\u200f", []string{"ar"}}, 596 | }, 597 | "RON": { 598 | {"RON", []string{"en"}}, 599 | {"रॉन", []string{"kok"}}, 600 | }, 601 | "RSD": { 602 | {"RSD", []string{"en"}}, 603 | {"din.", []string{"bs"}}, 604 | }, 605 | "RUB": { 606 | {"RUB", []string{"en"}}, 607 | {"₽", []string{"be", "kk", "ru", "yo"}}, 608 | }, 609 | "RWF": { 610 | {"RWF", []string{"en"}}, 611 | {"RF", []string{"en-RW", "fr-RW"}}, 612 | }, 613 | "SAR": { 614 | {"SAR", []string{"en"}}, 615 | {"ر.س.\u200f", []string{"ar"}}, 616 | }, 617 | "SBD": { 618 | {"SBD", []string{"en", "fr-CA"}}, 619 | {"$", []string{"en-SB"}}, 620 | {"$SB", []string{"fr"}}, 621 | {"SI$", []string{"nl"}}, 622 | }, 623 | "SCR": { 624 | {"SCR", []string{"en"}}, 625 | {"Rs", []string{"en-AU"}}, 626 | {"SR", []string{"en-SC", "fr-SC"}}, 627 | }, 628 | "SDG": { 629 | {"SDG", []string{"ar-LB", "en"}}, 630 | {"ج.س.", []string{"ar"}}, 631 | }, 632 | "SEK": { 633 | {"SEK", []string{"en"}}, 634 | {"kr", []string{"en-SE", "sv"}}, 635 | }, 636 | "SGD": { 637 | {"SGD", []string{"en"}}, 638 | {"$", []string{"en-SG", "ms-SG", "ta-SG", "zh-Hans-SG"}}, 639 | {"$SG", []string{"fr"}}, 640 | {"$\u00a0SG", []string{"fr-CA"}}, 641 | {"S$", []string{"ta-MY"}}, 642 | }, 643 | "SHP": { 644 | {"SHP", []string{"en"}}, 645 | {"£", []string{"en-SH"}}, 646 | }, 647 | "SLE": { 648 | {"SLE", []string{"en"}}, 649 | {"Le", []string{"en-SL"}}, 650 | }, 651 | "SOS": { 652 | {"SOS", []string{"en"}}, 653 | {"S", []string{"ar-SO", "so"}}, 654 | }, 655 | "SRD": { 656 | {"SRD", []string{"en", "fr-CA"}}, 657 | {"$", []string{"nl-SR"}}, 658 | {"$SR", []string{"fr"}}, 659 | }, 660 | "SSP": { 661 | {"SSP", []string{"en"}}, 662 | {"£", []string{"ar-SS", "en-SS"}}, 663 | }, 664 | "STN": { 665 | {"STN", []string{"en"}}, 666 | {"Db", []string{"pt-ST"}}, 667 | }, 668 | "SYP": { 669 | {"SYP", []string{"en"}}, 670 | {"LS", []string{"fr-SY"}}, 671 | {"ل.س.\u200f", []string{"ar"}}, 672 | }, 673 | "SZL": { 674 | {"SZL", []string{"en"}}, 675 | {"E", []string{"en-SZ"}}, 676 | }, 677 | "THB": { 678 | {"THB", []string{"en", "es-419"}}, 679 | {"฿", []string{"af", "am", "ar", "az", "bn", "bs", "ca", "cy", "da", "de", "dsb", "el", "el-polyton", "es", "et", "eu", "fa", "fil", "ga", "gd", "gl", "gu", "he", "hi", "hsb", "hy", "id", "it", "kk", "km", "kn", "ky", "lo", "lv", "ml", "mn", "mr", "my", "ne", "nl", "pa", "pt", "ru", "si", "sq", "sw", "ta", "te", "th", "tr", "ur", "vi", "zu"}}, 680 | }, 681 | "TMT": { 682 | {"TMT", []string{"en"}}, 683 | {"ТМТ", []string{"ru"}}, 684 | }, 685 | "TND": { 686 | {"TND", []string{"en"}}, 687 | {"DT", []string{"fr-TN"}}, 688 | {"د.ت.\u200f", []string{"ar"}}, 689 | }, 690 | "TOP": { 691 | {"TOP", []string{"en"}}, 692 | {"T$", []string{"en-TO"}}, 693 | }, 694 | "TRY": { 695 | {"TRY", []string{"en"}}, 696 | {"₺", []string{"tr"}}, 697 | }, 698 | "TTD": { 699 | {"TTD", []string{"en", "fr-CA"}}, 700 | {"$", []string{"en-TT"}}, 701 | {"$TT", []string{"fr"}}, 702 | {"TT$", []string{"my"}}, 703 | }, 704 | "TWD": { 705 | {"NT$", []string{"en", "zh-Hant-HK"}}, 706 | {"$", []string{"zh-Hant"}}, 707 | }, 708 | "TZS": { 709 | {"TZS", []string{"en"}}, 710 | {"TSh", []string{"en-TZ", "sw"}}, 711 | }, 712 | "UAH": { 713 | {"UAH", []string{"en"}}, 714 | {"₴", []string{"ru", "uk"}}, 715 | }, 716 | "UGX": { 717 | {"UGX", []string{"en"}}, 718 | {"USh", []string{"en-UG", "sw-UG"}}, 719 | }, 720 | "USD": { 721 | {"$", []string{"bn-IN", "en", "en-IN", "es-419", "nl-BQ", "sw-KE"}}, 722 | {"$US", []string{"fr"}}, 723 | {"$\u00a0US", []string{"fr-CA"}}, 724 | {"US$", []string{"am", "ar", "as", "az", "bn", "cs", "cy", "da", "en-001", "en-CA", "es", "es-AR", "es-CL", "es-CO", "es-CU", "es-DO", "es-UY", "eu", "gu", "id", "jv", "ka", "ko", "kok", "lo", "mk", "my", "ne", "nl", "pa", "pcm", "pt", "sd", "si", "so", "sq", "sr", "sr-Latn", "sv", "sw", "ta-SG", "th", "ti", "tk", "uz", "vi", "yue", "yue-Hans", "zh", "zh-Hant"}}, 725 | {"щ.д.", []string{"bg"}}, 726 | }, 727 | "UYU": { 728 | {"UYU", []string{"en", "fr-CA"}}, 729 | {"$", []string{"es-UY"}}, 730 | {"$UY", []string{"fr"}}, 731 | }, 732 | "UYW": { 733 | {"UYW", []string{"en"}}, 734 | {"UP", []string{"es-UY"}}, 735 | }, 736 | "UZS": { 737 | {"UZS", []string{"en"}}, 738 | {"soʻm", []string{"uz"}}, 739 | }, 740 | "VES": { 741 | {"VES", []string{"en"}}, 742 | {"Bs.S", []string{"es-VE"}}, 743 | }, 744 | "VND": { 745 | {"₫", []string{"en"}}, 746 | }, 747 | "VUV": { 748 | {"VUV", []string{"en"}}, 749 | {"VT", []string{"en-VU", "fr-VU"}}, 750 | }, 751 | "WST": { 752 | {"WST", []string{"en", "fr-CA"}}, 753 | {"$WS", []string{"fr"}}, 754 | {"WS$", []string{"en-WS"}}, 755 | }, 756 | "XAF": { 757 | {"FCFA", []string{"en"}}, 758 | }, 759 | "XCD": { 760 | {"EC$", []string{"en"}}, 761 | {"$", []string{"en-AG", "en-AI", "en-DM", "en-GD", "en-KN", "en-LC", "en-MS", "en-VC"}}, 762 | {"$EC", []string{"fa"}}, 763 | }, 764 | "XCG": { 765 | {"Cg.", []string{"en"}}, 766 | }, 767 | "XOF": { 768 | {"F\u202fCFA", []string{"en"}}, 769 | {"فرانک\u202fCFA", []string{"fa"}}, 770 | {"සිෆ්එ", []string{"si"}}, 771 | }, 772 | "XPF": { 773 | {"CFPF", []string{"en", "fr-CA"}}, 774 | {"CFP", []string{"en-AU"}}, 775 | {"FCFP", []string{"fr"}}, 776 | }, 777 | "YER": { 778 | {"YER", []string{"en"}}, 779 | {"ر.ي.\u200f", []string{"ar"}}, 780 | }, 781 | "ZAR": { 782 | {"ZAR", []string{"en"}}, 783 | {"R", []string{"af", "en-LS", "en-ZA", "zu"}}, 784 | }, 785 | "ZMW": { 786 | {"ZMW", []string{"en"}}, 787 | {"K", []string{"en-ZM"}}, 788 | }, 789 | } 790 | 791 | var currencyFormats = map[string]currencyFormat{ 792 | "af": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 793 | "ar": {"\u200f0.00\u00a0¤;\u200f-0.00\u00a0¤", "\u061c0.00¤;(\u061c0.00¤)", 0, 1, 3, 3, ".", ",", "\u200e+", "\u200e-"}, 794 | "ar-BH": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 795 | "ar-DJ": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 796 | "ar-DZ": {"\u200f0.00\u00a0¤;\u200f-0.00\u00a0¤", "\u061c0.00¤;(\u061c0.00¤)", 0, 1, 3, 3, ",", ".", "\u200e+", "\u200e-"}, 797 | "ar-EG": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 798 | "ar-ER": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 799 | "ar-IL": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 800 | "ar-IQ": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 801 | "ar-JO": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 802 | "ar-KM": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 803 | "ar-KW": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 804 | "ar-LB": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 805 | "ar-LY": {"\u200f0.00\u00a0¤;\u200f-0.00\u00a0¤", "\u061c0.00¤;(\u061c0.00¤)", 0, 1, 3, 3, ",", ".", "\u200e+", "\u200e-"}, 806 | "ar-MA": {"\u200f0.00\u00a0¤;\u200f-0.00\u00a0¤", "\u061c0.00¤;(\u061c0.00¤)", 0, 1, 3, 3, ",", ".", "\u200e+", "\u200e-"}, 807 | "ar-MR": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 808 | "ar-OM": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 809 | "ar-PS": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 810 | "ar-QA": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 811 | "ar-SA": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 812 | "ar-SD": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 813 | "ar-SO": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 814 | "ar-SS": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 815 | "ar-SY": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 816 | "ar-TD": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 817 | "ar-TN": {"\u200f0.00\u00a0¤;\u200f-0.00\u00a0¤", "\u061c0.00¤;(\u061c0.00¤)", 0, 1, 3, 3, ",", ".", "\u200e+", "\u200e-"}, 818 | "ar-YE": {"\u200f0.00\u00a0¤", "", 1, 1, 3, 3, "٫", "٬", "\u061c+", "\u061c-"}, 819 | "as": {"¤\u00a00.00", "¤0.00;(¤0.00)", 3, 1, 3, 2, ".", ",", "+", "-"}, 820 | "az": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 821 | "be": {"0.00\u00a0¤", "", 0, 2, 3, 3, ",", "\u00a0", "+", "-"}, 822 | "bg": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 2, 3, 3, ",", "\u00a0", "+", "-"}, 823 | "bn": {"0.00¤", "0.00¤;(0.00¤)", 3, 1, 3, 2, ".", ",", "+", "-"}, 824 | "bn-IN": {"¤0.00", "¤0.00;(¤0.00)", 3, 1, 3, 2, ".", ",", "+", "-"}, 825 | "bs": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 826 | "ca": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "-"}, 827 | "cs": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 828 | "da": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 829 | "de": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 830 | "de-AT": {"¤\u00a00.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 831 | "de-CH": {"¤\u00a00.00;¤-0.00", "", 0, 1, 3, 3, ".", "’", "+", "-"}, 832 | "de-LI": {"¤\u00a00.00", "", 0, 1, 3, 3, ".", "’", "+", "-"}, 833 | "dsb": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 834 | "el": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 835 | "el-polyton": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 836 | "en": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ".", ",", "+", "-"}, 837 | "en-150": {"0.00\u00a0¤", "", 0, 1, 3, 3, ".", ",", "+", "-"}, 838 | "en-AT": {"¤\u00a00.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 839 | "en-BE": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 840 | "en-CH": {"¤\u00a00.00;¤-0.00", "", 0, 1, 3, 3, ".", "’", "+", "-"}, 841 | "en-CZ": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 842 | "en-DE": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 843 | "en-DK": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 844 | "en-ES": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 845 | "en-FI": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 846 | "en-FR": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u202f", "+", "-"}, 847 | "en-HU": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 848 | "en-ID": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ",", ".", "+", "-"}, 849 | "en-IN": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 2, ".", ",", "+", "-"}, 850 | "en-IT": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 851 | "en-MV": {"¤\u00a00.00", "", 0, 1, 3, 3, ".", ",", "+", "-"}, 852 | "en-NL": {"¤\u00a00.00;¤\u00a0-0.00", "¤\u00a00.00;(¤\u00a00.00)", 0, 1, 3, 3, ",", ".", "+", "-"}, 853 | "en-NO": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 854 | "en-PL": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "-"}, 855 | "en-PT": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 856 | "en-RO": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "-"}, 857 | "en-SE": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 858 | "en-SI": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "-"}, 859 | "en-SK": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 860 | "en-ZA": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 861 | "es": {"0.00\u00a0¤", "", 0, 2, 3, 3, ",", ".", "+", "-"}, 862 | "es-419": {"¤0.00", "", 0, 1, 3, 3, ".", ",", "+", "-"}, 863 | "es-AR": {"¤\u00a00.00", "¤\u00a00.00;(¤\u00a00.00)", 0, 1, 3, 3, ",", ".", "+", "-"}, 864 | "es-BO": {"¤0.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 865 | "es-CL": {"¤0.00;¤-0.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 866 | "es-CO": {"¤\u00a00.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 867 | "es-CR": {"¤0.00", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 868 | "es-DO": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ".", ",", "+", "-"}, 869 | "es-EC": {"¤0.00;¤-0.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 870 | "es-GQ": {"¤0.00", "", 0, 2, 3, 3, ",", ".", "+", "-"}, 871 | "es-PE": {"¤\u00a00.00", "", 0, 1, 3, 3, ".", ",", "+", "-"}, 872 | "es-PY": {"¤\u00a00.00;¤\u00a0-0.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 873 | "es-UY": {"¤\u00a00.00", "¤\u00a00.00;(¤\u00a00.00)", 0, 1, 3, 3, ",", ".", "+", "-"}, 874 | "es-VE": {"¤0.00;¤-0.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 875 | "et": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 2, 3, 3, ",", "\u00a0", "+", "−"}, 876 | "eu": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "−"}, 877 | "fa": {"\u200e¤0.00", "\u200e¤\u00a00.00;\u200e(¤\u00a00.00)", 2, 1, 3, 3, "٫", "٬", "\u200e+", "\u200e−"}, 878 | "fa-AF": {"¤\u00a00.00", "¤\u00a00.00;\u200e(¤\u00a00.00)", 2, 1, 3, 3, "٫", "٬", "\u200e+", "\u200e−"}, 879 | "fi": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "−"}, 880 | "fr": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", "\u202f", "+", "-"}, 881 | "fr-CA": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 882 | "fr-CH": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ".", "\u202f", "+", "-"}, 883 | "fr-LU": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "-"}, 884 | "fr-MA": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "-"}, 885 | "gl": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 886 | "gu": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 2, ".", ",", "+", "-"}, 887 | "ha": {"¤\u00a00.00", "", 0, 1, 3, 3, ".", ",", "+", "-"}, 888 | "he": {"\u200f0.00\u00a0\u200f¤;\u200f-0.00\u00a0\u200f¤", "", 0, 1, 3, 3, ".", ",", "\u200e+", "\u200e-"}, 889 | "hi": {"¤0.00", "", 0, 1, 3, 2, ".", ",", "+", "-"}, 890 | "hi-Latn": {"¤0.00", "", 0, 1, 3, 2, ".", ",", "+", "-"}, 891 | "hr": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "−"}, 892 | "hsb": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 893 | "ht": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 894 | "hu": {"0.00\u00a0¤", "", 0, 2, 3, 3, ",", "\u00a0", "+", "-"}, 895 | "hy": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 896 | "id": {"¤0.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 897 | "is": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 898 | "it": {"0.00\u00a0¤", "", 0, 2, 3, 3, ",", ".", "+", "-"}, 899 | "it-CH": {"¤\u00a00.00;¤-0.00", "", 0, 2, 3, 3, ".", "’", "+", "-"}, 900 | "jv": {"¤\u00a00.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 901 | "ka": {"0.00\u00a0¤", "", 0, 2, 3, 3, ",", "\u00a0", "+", "-"}, 902 | "kk": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 903 | "km": {"0.00¤", "0.00¤;(0.00¤)", 0, 1, 3, 3, ".", ",", "+", "-"}, 904 | "kok": {"¤\u00a00.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ".", ",", "+", "-"}, 905 | "ky": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 906 | "lo": {"¤0.00;¤-0.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 907 | "lt": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "−"}, 908 | "lv": {"0.00\u00a0¤", "", 0, 2, 3, 3, ",", "\u00a0", "+", "-"}, 909 | "mk": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 910 | "mn": {"¤\u00a00.00", "", 0, 1, 3, 3, ".", ",", "+", "-"}, 911 | "mr": {"¤0.00", "¤0.00;(¤0.00)", 4, 1, 3, 3, ".", ",", "+", "-"}, 912 | "ms-BN": {"¤\u00a00.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ",", ".", "+", "-"}, 913 | "ms-ID": {"¤0.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 914 | "my": {"0.00\u00a0¤", "¤\u00a00.00", 5, 1, 3, 3, ".", ",", "+", "-"}, 915 | "ne": {"¤\u00a00.00", "", 4, 1, 3, 2, ".", ",", "+", "-"}, 916 | "nl": {"¤\u00a00.00;¤\u00a0-0.00", "¤\u00a00.00;(¤\u00a00.00)", 0, 1, 3, 3, ",", ".", "+", "-"}, 917 | "no": {"0.00\u00a0¤;-0.00\u00a0¤", "¤\u00a00.00;(¤\u00a00.00)", 0, 1, 3, 3, ",", "\u00a0", "+", "−"}, 918 | "pa": {"¤0.00", "¤\u00a00.00", 0, 1, 3, 2, ".", ",", "+", "-"}, 919 | "pcm": {"¤0.00", "", 0, 1, 3, 3, ".", ",", "+", "-"}, 920 | "pl": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 2, 3, 3, ",", "\u00a0", "+", "-"}, 921 | "ps": {"¤\u00a00.00", "¤0.00;(¤0.00)", 2, 1, 3, 3, "٫", "٬", "\u200e+\u200e", "\u200e-\u200e"}, 922 | "pt": {"¤\u00a00.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 923 | "pt-AO": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 924 | "pt-PT": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 2, 3, 3, ",", "\u00a0", "+", "-"}, 925 | "ro": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "-"}, 926 | "ru": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 927 | "ru-UA": {"0.00\u00a0¤", "", 0, 2, 3, 3, ",", "\u00a0", "+", "-"}, 928 | "sd": {"0.00\u00a0¤", "", 1, 1, 3, 3, ".", "٬", "\u061c+", "\u061c-"}, 929 | "sk": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 930 | "sl": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 2, 3, 3, ",", ".", "+", "−"}, 931 | "sq": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 2, 3, 3, ",", "\u00a0", "+", "-"}, 932 | "sr": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "-"}, 933 | "sr-Latn": {"0.00\u00a0¤", "0.00\u00a0¤;(0.00\u00a0¤)", 0, 1, 3, 3, ",", ".", "+", "-"}, 934 | "sv": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "−"}, 935 | "sw": {"¤\u00a00.00", "", 0, 1, 3, 3, ".", ",", "+", "-"}, 936 | "sw-CD": {"¤\u00a00.00", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 937 | "ta": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 2, ".", ",", "+", "-"}, 938 | "ta-MY": {"¤\u00a00.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ".", ",", "+", "-"}, 939 | "ta-SG": {"¤\u00a00.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ".", ",", "+", "-"}, 940 | "te": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 2, ".", ",", "+", "-"}, 941 | "ti": {"¤0.00", "", 0, 1, 3, 3, ".", ",", "+", "-"}, 942 | "tk": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 943 | "tr": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ",", ".", "+", "-"}, 944 | "uk": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 945 | "ur": {"¤0.00", "¤0.00;(¤0.00)", 0, 1, 3, 3, ".", ",", "\u200e+", "\u200e-"}, 946 | "ur-IN": {"¤\u00a00.00", "¤0.00;(¤0.00)", 2, 1, 3, 2, "٫", "٬", "\u200e+\u200e", "\u200e-\u200e"}, 947 | "uz": {"0.00\u00a0¤", "¤0.00;(¤0.00)", 0, 1, 3, 3, ",", "\u00a0", "+", "-"}, 948 | "vi": {"0.00\u00a0¤", "", 0, 1, 3, 3, ",", ".", "+", "-"}, 949 | } 950 | 951 | var countryCurrencies = map[string]string{ 952 | "AC": "SHP", "AD": "EUR", "AE": "AED", "AF": "AFN", "AG": "XCD", 953 | "AI": "XCD", "AL": "ALL", "AM": "AMD", "AO": "AOA", "AR": "ARS", 954 | "AS": "USD", "AT": "EUR", "AU": "AUD", "AW": "AWG", "AX": "EUR", 955 | "AZ": "AZN", "BA": "BAM", "BB": "BBD", "BD": "BDT", "BE": "EUR", 956 | "BF": "XOF", "BG": "BGN", "BH": "BHD", "BI": "BIF", "BJ": "XOF", 957 | "BL": "EUR", "BM": "BMD", "BN": "BND", "BO": "BOB", "BQ": "USD", 958 | "BR": "BRL", "BS": "BSD", "BT": "BTN", "BV": "NOK", "BW": "BWP", 959 | "BY": "BYN", "BZ": "BZD", "CA": "CAD", "CC": "AUD", "CD": "CDF", 960 | "CF": "XAF", "CG": "XAF", "CH": "CHF", "CI": "XOF", "CK": "NZD", 961 | "CL": "CLP", "CM": "XAF", "CN": "CNY", "CO": "COP", "CR": "CRC", 962 | "CU": "CUP", "CV": "CVE", "CW": "XCG", "CX": "AUD", "CY": "EUR", 963 | "CZ": "CZK", "DE": "EUR", "DG": "USD", "DJ": "DJF", "DK": "DKK", 964 | "DM": "XCD", "DO": "DOP", "DZ": "DZD", "EC": "USD", "EE": "EUR", 965 | "EG": "EGP", "EH": "MAD", "ER": "ERN", "ES": "EUR", "ET": "ETB", 966 | "FI": "EUR", "FJ": "FJD", "FK": "FKP", "FM": "USD", "FO": "DKK", 967 | "FR": "EUR", "GA": "XAF", "GB": "GBP", "GD": "XCD", "GE": "GEL", 968 | "GF": "EUR", "GG": "GBP", "GH": "GHS", "GI": "GIP", "GL": "DKK", 969 | "GM": "GMD", "GN": "GNF", "GP": "EUR", "GQ": "XAF", "GR": "EUR", 970 | "GS": "GBP", "GT": "GTQ", "GU": "USD", "GW": "XOF", "GY": "GYD", 971 | "HK": "HKD", "HM": "AUD", "HN": "HNL", "HR": "EUR", "HT": "USD", 972 | "HU": "HUF", "IC": "EUR", "ID": "IDR", "IE": "EUR", "IL": "ILS", 973 | "IM": "GBP", "IN": "INR", "IO": "USD", "IQ": "IQD", "IR": "IRR", 974 | "IS": "ISK", "IT": "EUR", "JE": "GBP", "JM": "JMD", "JO": "JOD", 975 | "JP": "JPY", "KE": "KES", "KG": "KGS", "KH": "KHR", "KI": "AUD", 976 | "KM": "KMF", "KN": "XCD", "KP": "KPW", "KR": "KRW", "KW": "KWD", 977 | "KY": "KYD", "KZ": "KZT", "LA": "LAK", "LB": "LBP", "LC": "XCD", 978 | "LI": "CHF", "LK": "LKR", "LR": "LRD", "LS": "LSL", "LT": "EUR", 979 | "LU": "EUR", "LV": "EUR", "LY": "LYD", "MA": "MAD", "MC": "EUR", 980 | "MD": "MDL", "ME": "EUR", "MF": "EUR", "MG": "MGA", "MH": "USD", 981 | "MK": "MKD", "ML": "XOF", "MM": "MMK", "MN": "MNT", "MO": "MOP", 982 | "MP": "USD", "MQ": "EUR", "MR": "MRU", "MS": "XCD", "MT": "EUR", 983 | "MU": "MUR", "MV": "MVR", "MW": "MWK", "MX": "MXN", "MY": "MYR", 984 | "MZ": "MZN", "NA": "NAD", "NC": "XPF", "NE": "XOF", "NF": "AUD", 985 | "NG": "NGN", "NI": "NIO", "NL": "EUR", "NO": "NOK", "NP": "NPR", 986 | "NR": "AUD", "NU": "NZD", "NZ": "NZD", "OM": "OMR", "PA": "USD", 987 | "PE": "PEN", "PF": "XPF", "PG": "PGK", "PH": "PHP", "PK": "PKR", 988 | "PL": "PLN", "PM": "EUR", "PN": "NZD", "PR": "USD", "PS": "JOD", 989 | "PT": "EUR", "PW": "USD", "PY": "PYG", "QA": "QAR", "RE": "EUR", 990 | "RO": "RON", "RS": "RSD", "RU": "RUB", "RW": "RWF", "SA": "SAR", 991 | "SB": "SBD", "SC": "SCR", "SD": "SDG", "SE": "SEK", "SG": "SGD", 992 | "SH": "SHP", "SI": "EUR", "SJ": "NOK", "SK": "EUR", "SL": "SLE", 993 | "SM": "EUR", "SN": "XOF", "SO": "SOS", "SR": "SRD", "SS": "SSP", 994 | "ST": "STN", "SV": "USD", "SX": "XCG", "SY": "SYP", "SZ": "SZL", 995 | "TA": "GBP", "TC": "USD", "TD": "XAF", "TF": "EUR", "TG": "XOF", 996 | "TH": "THB", "TJ": "TJS", "TK": "NZD", "TL": "USD", "TM": "TMT", 997 | "TN": "TND", "TO": "TOP", "TR": "TRY", "TT": "TTD", "TV": "AUD", 998 | "TW": "TWD", "TZ": "TZS", "UA": "UAH", "UG": "UGX", "UM": "USD", 999 | "US": "USD", "UY": "UYU", "UZ": "UZS", "VA": "EUR", "VC": "XCD", 1000 | "VE": "VES", "VG": "USD", "VI": "USD", "VN": "VND", "VU": "VUV", 1001 | "WF": "XPF", "WS": "WST", "XK": "EUR", "YE": "YER", "YT": "EUR", 1002 | "ZA": "ZAR", "ZM": "ZMW", "ZW": "ZWG", 1003 | } 1004 | 1005 | var parentLocales = map[string]string{ 1006 | "en-150": "en-001", "en-AG": "en-001", "en-AI": "en-001", 1007 | "en-AT": "en-150", "en-AU": "en-001", "en-BB": "en-001", 1008 | "en-BE": "en-150", "en-BM": "en-001", "en-BS": "en-001", 1009 | "en-BW": "en-001", "en-BZ": "en-001", "en-CC": "en-001", 1010 | "en-CH": "en-150", "en-CK": "en-001", "en-CM": "en-001", 1011 | "en-CX": "en-001", "en-CY": "en-001", "en-CZ": "en-150", 1012 | "en-DE": "en-150", "en-DG": "en-001", "en-DK": "en-150", 1013 | "en-DM": "en-001", "en-ER": "en-001", "en-ES": "en-150", 1014 | "en-FI": "en-150", "en-FJ": "en-001", "en-FK": "en-001", 1015 | "en-FM": "en-001", "en-FR": "en-150", "en-GB": "en-001", 1016 | "en-GD": "en-001", "en-GG": "en-001", "en-GH": "en-001", 1017 | "en-GI": "en-001", "en-GM": "en-001", "en-GS": "en-001", 1018 | "en-GY": "en-001", "en-HK": "en-001", "en-HU": "en-150", 1019 | "en-ID": "en-001", "en-IE": "en-001", "en-IL": "en-001", 1020 | "en-IM": "en-001", "en-IN": "en-001", "en-IO": "en-001", 1021 | "en-IT": "en-150", "en-JE": "en-001", "en-JM": "en-001", 1022 | "en-KE": "en-001", "en-KI": "en-001", "en-KN": "en-001", 1023 | "en-KY": "en-001", "en-LC": "en-001", "en-LR": "en-001", 1024 | "en-LS": "en-001", "en-MG": "en-001", "en-MO": "en-001", 1025 | "en-MS": "en-001", "en-MT": "en-001", "en-MU": "en-001", 1026 | "en-MV": "en-001", "en-MW": "en-001", "en-MY": "en-001", 1027 | "en-NA": "en-001", "en-NF": "en-001", "en-NG": "en-001", 1028 | "en-NL": "en-150", "en-NO": "en-150", "en-NR": "en-001", 1029 | "en-NU": "en-001", "en-NZ": "en-001", "en-PG": "en-001", 1030 | "en-PK": "en-001", "en-PL": "en-150", "en-PN": "en-001", 1031 | "en-PT": "en-150", "en-PW": "en-001", "en-RO": "en-150", 1032 | "en-RW": "en-001", "en-SB": "en-001", "en-SC": "en-001", 1033 | "en-SD": "en-001", "en-SE": "en-150", "en-SG": "en-001", 1034 | "en-SH": "en-001", "en-SI": "en-150", "en-SK": "en-150", 1035 | "en-SL": "en-001", "en-SS": "en-001", "en-SX": "en-001", 1036 | "en-SZ": "en-001", "en-TC": "en-001", "en-TK": "en-001", 1037 | "en-TO": "en-001", "en-TT": "en-001", "en-TV": "en-001", 1038 | "en-TZ": "en-001", "en-UG": "en-001", "en-VC": "en-001", 1039 | "en-VG": "en-001", "en-VU": "en-001", "en-WS": "en-001", 1040 | "en-ZA": "en-001", "en-ZM": "en-001", "en-ZW": "en-001", 1041 | "es-AR": "es-419", "es-BO": "es-419", "es-BR": "es-419", 1042 | "es-BZ": "es-419", "es-CL": "es-419", "es-CO": "es-419", 1043 | "es-CR": "es-419", "es-CU": "es-419", "es-DO": "es-419", 1044 | "es-EC": "es-419", "es-GT": "es-419", "es-HN": "es-419", 1045 | "es-MX": "es-419", "es-NI": "es-419", "es-PA": "es-419", 1046 | "es-PE": "es-419", "es-PR": "es-419", "es-PY": "es-419", 1047 | "es-SV": "es-419", "es-US": "es-419", "es-UY": "es-419", 1048 | "es-VE": "es-419", "hi-Latn": "en-IN", "ht": "fr-HT", 1049 | "nb": "no", "nn": "no", "pt-AO": "pt-PT", 1050 | "pt-CH": "pt-PT", "pt-CV": "pt-PT", "pt-GQ": "pt-PT", 1051 | "pt-GW": "pt-PT", "pt-LU": "pt-PT", "pt-MO": "pt-PT", 1052 | "pt-MZ": "pt-PT", "pt-ST": "pt-PT", "pt-TL": "pt-PT", 1053 | "sr-Latn": "en", "yue-Hans": "en", "zh-Hant": "en", 1054 | "zh-Hant-MO": "zh-Hant-HK", 1055 | } 1056 | --------------------------------------------------------------------------------