├── .gitattributes ├── .ameba.yml ├── src ├── money │ ├── version.cr │ ├── currency │ │ ├── type.cr │ │ ├── error.cr │ │ ├── rate_provider │ │ │ ├── providers │ │ │ │ ├── null.cr │ │ │ │ ├── coingecko.cr │ │ │ │ ├── ecb.cr │ │ │ │ ├── nbp.cr │ │ │ │ ├── bitpay.cr │ │ │ │ ├── cbr.cr │ │ │ │ ├── float_rates.cr │ │ │ │ ├── currency_api.cr │ │ │ │ ├── uni_rate_api.cr │ │ │ │ ├── freecurrency_api.cr │ │ │ │ ├── currency_beacon.cr │ │ │ │ ├── abstract_api.cr │ │ │ │ ├── fx_rates_api.cr │ │ │ │ ├── fixer.cr │ │ │ │ ├── open_exchange_rates.cr │ │ │ │ ├── fx_feed.cr │ │ │ │ ├── coinbase.cr │ │ │ │ ├── exchange_rate_api.cr │ │ │ │ ├── metalprice_api.cr │ │ │ │ ├── compound.cr │ │ │ │ ├── exchangerate.cr │ │ │ │ ├── currency_layer.cr │ │ │ │ ├── coinlayer.cr │ │ │ │ └── coin_market_cap.cr │ │ │ ├── many_to_one.cr │ │ │ ├── one_to_many.cr │ │ │ ├── error.cr │ │ │ └── http.cr │ │ ├── yaml.cr │ │ ├── json.cr │ │ ├── exchange │ │ │ └── single_currency.cr │ │ ├── loader.cr │ │ ├── rate_store │ │ │ ├── memory.cr │ │ │ └── file.cr │ │ ├── converter.cr │ │ ├── rate_provider.cr │ │ ├── validation.cr │ │ ├── rate.cr │ │ └── enumeration.cr │ ├── money │ │ ├── yaml.cr │ │ ├── json.cr │ │ ├── casting.cr │ │ ├── exchange.cr │ │ ├── constructors.cr │ │ ├── allocate.cr │ │ └── rounding.cr │ ├── registry │ │ └── converter │ │ │ ├── json.cr │ │ │ └── yaml.cr │ ├── error.cr │ ├── context.cr │ └── registry.cr ├── core_ext │ ├── fiber.cr │ ├── kernel.cr │ └── to_money.cr └── money.cr ├── .editorconfig ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ └── ci.yml ├── data └── currencies │ ├── xba.json │ ├── xbb.json │ ├── xbc.json │ ├── xbd.json │ ├── gbx.json │ ├── xts.json │ ├── xpt.json │ ├── lsk.json │ ├── historical │ ├── xfu.json │ ├── eek.json │ ├── hrk.json │ ├── lvl.json │ ├── skk.json │ ├── zmk.json │ ├── ltl.json │ ├── tmm.json │ ├── std.json │ ├── mtl.json │ ├── byr.json │ ├── mro.json │ ├── vef.json │ ├── ang.json │ ├── zwd.json │ ├── zwl.json │ ├── zwn.json │ ├── zwr.json │ ├── ghc.json │ └── cuc.json │ ├── xdr.json │ ├── xpd.json │ ├── xau.json │ ├── clf.json │ ├── ggp.json │ ├── jep.json │ ├── krw.json │ ├── trx.json │ ├── vuv.json │ ├── xag.json │ ├── xrp.json │ ├── ada.json │ ├── btc.json │ ├── huf.json │ ├── irr.json │ ├── ltc.json │ ├── usdc.json │ ├── bnb.json │ ├── doge.json │ ├── dot.json │ ├── eth.json │ ├── sol.json │ ├── xlm.json │ ├── xmr.json │ ├── etc.json │ ├── bwp.json │ ├── clp.json │ ├── mmk.json │ ├── thb.json │ ├── brl.json │ ├── czk.json │ ├── ern.json │ ├── etb.json │ ├── gip.json │ ├── gmd.json │ ├── htg.json │ ├── khr.json │ ├── kpw.json │ ├── mur.json │ ├── myr.json │ ├── ngn.json │ ├── omr.json │ ├── pen.json │ ├── pln.json │ ├── ron.json │ ├── sdg.json │ ├── sle.json │ ├── srd.json │ ├── szl.json │ ├── vnd.json │ ├── yer.json │ ├── zmw.json │ ├── zwg.json │ ├── aoa.json │ ├── djf.json │ ├── gtq.json │ ├── hnl.json │ ├── idr.json │ ├── iqd.json │ ├── kzt.json │ ├── mad.json │ ├── mga.json │ ├── mkd.json │ ├── mnt.json │ ├── mru.json │ ├── mwk.json │ ├── pgk.json │ ├── pyg.json │ ├── shp.json │ ├── tjs.json │ ├── tmt.json │ ├── tzs.json │ ├── uah.json │ ├── ugx.json │ ├── uyu.json │ ├── xcg.json │ ├── zar.json │ ├── eur.json │ ├── imp.json │ ├── isk.json │ ├── pab.json │ ├── ssp.json │ ├── usdt.json │ ├── gbp.json │ ├── bam.json │ ├── stn.json │ ├── bnd.json │ ├── jpy.json │ ├── lak.json │ ├── amd.json │ ├── awg.json │ ├── bdt.json │ ├── bhd.json │ ├── bsd.json │ ├── cnh.json │ ├── dkk.json │ ├── gel.json │ ├── mdl.json │ ├── qar.json │ ├── sek.json │ ├── top.json │ ├── try.json │ ├── xpf.json │ ├── chf.json │ ├── crc.json │ ├── dzd.json │ ├── jod.json │ ├── kes.json │ ├── kgs.json │ ├── kwd.json │ ├── lyd.json │ ├── mop.json │ ├── nok.json │ ├── pkr.json │ ├── sos.json │ ├── svc.json │ ├── afn.json │ ├── azn.json │ ├── bob.json │ ├── btn.json │ ├── ghs.json │ ├── mzn.json │ ├── nio.json │ ├── rub.json │ ├── rwf.json │ ├── sar.json │ ├── sll.json │ ├── ils.json │ ├── lbp.json │ ├── npr.json │ ├── php.json │ ├── tnd.json │ ├── xof.json │ ├── bch.json │ ├── cny.json │ ├── egp.json │ ├── ves.json │ ├── aed.json │ ├── inr.json │ ├── mvr.json │ ├── bgn.json │ ├── uzs.json │ ├── byn.json │ ├── bzd.json │ ├── cup.json │ ├── fjd.json │ ├── jmd.json │ ├── lrd.json │ ├── lsl.json │ ├── nad.json │ ├── sgd.json │ ├── syp.json │ ├── all.json │ ├── bbd.json │ ├── bmd.json │ ├── cdf.json │ ├── dop.json │ ├── fkp.json │ ├── gyd.json │ ├── hkd.json │ ├── kmf.json │ ├── mxn.json │ ├── nzd.json │ ├── twd.json │ ├── bif.json │ ├── cop.json │ ├── gnf.json │ ├── kyd.json │ ├── sbd.json │ ├── scr.json │ ├── xcd.json │ ├── ars.json │ ├── cve.json │ ├── rsd.json │ ├── ttd.json │ ├── wst.json │ ├── xaf.json │ ├── aud.json │ ├── lkr.json │ ├── cad.json │ └── usd.json ├── spec ├── core_ext │ ├── fiber_spec.cr │ └── to_money_spec.cr ├── currency │ ├── exchange │ │ └── single_currency_spec.cr │ ├── rate_provider │ │ └── null_spec.cr │ ├── loader_spec.cr │ ├── yaml_spec.cr │ ├── converter_spec.cr │ ├── json_spec.cr │ └── rate_spec.cr ├── spec_helper.cr ├── money │ ├── casting_spec.cr │ ├── yaml_spec.cr │ ├── json_spec.cr │ └── exchange_spec.cr └── context_spec.cr ├── shard.yml └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.ameba.yml: -------------------------------------------------------------------------------- 1 | Lint/Typos: 2 | Excluded: 3 | - src/money/money.cr 4 | -------------------------------------------------------------------------------- /src/money/version.cr: -------------------------------------------------------------------------------- 1 | struct Money 2 | VERSION = {{ `shards version "#{__DIR__}"`.chomp.stringify }} 3 | end 4 | -------------------------------------------------------------------------------- /src/money/currency/type.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | # Currency type. 3 | enum Type 4 | Metal 5 | Fiat 6 | Crypto 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cr] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | 6 | # Libraries don't need dependency lock 7 | # Dependencies will be locked in application that uses them 8 | /shard.lock 9 | -------------------------------------------------------------------------------- /src/core_ext/fiber.cr: -------------------------------------------------------------------------------- 1 | class Fiber 2 | # `Money::Context` object holding global settings for `Money` objects. 3 | property money_context : Money::Context { Money::Context.new } 4 | end 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: daily 8 | -------------------------------------------------------------------------------- /src/money/currency/error.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | # Raised when trying to find an unknown currency. 3 | class NotFoundError < Error 4 | def initialize(key) 5 | super("Unknown currency: #{key}") 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /data/currencies/xba.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_numeric": 955, 3 | "code": "XBA", 4 | "name": "European Composite Unit", 5 | "subunit_to_unit": 1, 6 | "symbol_first": false, 7 | "decimal_mark": ".", 8 | "thousands_separator": "," 9 | } 10 | -------------------------------------------------------------------------------- /data/currencies/xbb.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_numeric": 956, 3 | "code": "XBB", 4 | "name": "European Monetary Unit", 5 | "subunit_to_unit": 1, 6 | "symbol_first": false, 7 | "decimal_mark": ".", 8 | "thousands_separator": "," 9 | } 10 | -------------------------------------------------------------------------------- /data/currencies/xbc.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_numeric": 957, 3 | "code": "XBC", 4 | "name": "European Unit of Account 9", 5 | "subunit_to_unit": 1, 6 | "symbol_first": false, 7 | "decimal_mark": ".", 8 | "thousands_separator": "," 9 | } 10 | -------------------------------------------------------------------------------- /data/currencies/xbd.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_numeric": 958, 3 | "code": "XBD", 4 | "name": "European Unit of Account 17", 5 | "subunit_to_unit": 1, 6 | "symbol_first": false, 7 | "decimal_mark": ".", 8 | "thousands_separator": "," 9 | } 10 | -------------------------------------------------------------------------------- /data/currencies/gbx.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "code": "GBX", 4 | "name": "British Penny", 5 | "subunit_to_unit": 1, 6 | "symbol_first": true, 7 | "decimal_mark": ".", 8 | "thousands_separator": ",", 9 | "smallest_denomination": 1 10 | } 11 | -------------------------------------------------------------------------------- /spec/core_ext/fiber_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Fiber do 4 | describe "#money_context" do 5 | it "returns a `Money::Context` object" do 6 | Fiber.current.money_context.should be_a Money::Context 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /data/currencies/xts.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_numeric": 963, 3 | "code": "XTS", 4 | "name": "Codes specifically reserved for testing purposes", 5 | "subunit_to_unit": 1, 6 | "symbol_first": false, 7 | "decimal_mark": ".", 8 | "thousands_separator": "," 9 | } 10 | -------------------------------------------------------------------------------- /data/currencies/xpt.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "metal", 3 | "iso_numeric": 962, 4 | "code": "XPT", 5 | "name": "Platinum", 6 | "symbol": "oz t", 7 | "subunit_to_unit": 1, 8 | "symbol_first": false, 9 | "decimal_mark": ".", 10 | "thousands_separator": "," 11 | } 12 | -------------------------------------------------------------------------------- /data/currencies/lsk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "LSK", 4 | "name": "Lisk", 5 | "symbol": "Ⱡ", 6 | "subunit_to_unit": 100000000, 7 | "symbol_first": true, 8 | "decimal_mark": ".", 9 | "thousands_separator": ",", 10 | "smallest_denomination": 1 11 | } 12 | -------------------------------------------------------------------------------- /data/currencies/historical/xfu.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2013-11-07", 3 | "replaced_by": "EUR", 4 | "type": "fiat", 5 | "code": "XFU", 6 | "name": "UIC Franc", 7 | "subunit_to_unit": 100, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": "," 11 | } 12 | -------------------------------------------------------------------------------- /data/currencies/xdr.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso_numeric": 960, 3 | "code": "XDR", 4 | "name": "Special Drawing Rights", 5 | "symbol": "SDR", 6 | "alternate_symbols": ["XDR"], 7 | "subunit_to_unit": 1, 8 | "symbol_first": false, 9 | "decimal_mark": ".", 10 | "thousands_separator": "," 11 | } 12 | -------------------------------------------------------------------------------- /data/currencies/xpd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "metal", 3 | "iso_numeric": 964, 4 | "code": "XPD", 5 | "name": "Palladium", 6 | "symbol": "oz t", 7 | "subunit": "Oz", 8 | "subunit_to_unit": 1, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": "," 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/xau.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "metal", 3 | "iso_numeric": 959, 4 | "code": "XAU", 5 | "name": "Gold (Troy Ounce)", 6 | "symbol": "oz t", 7 | "subunit": "Oz", 8 | "subunit_to_unit": 1, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": "," 12 | } 13 | -------------------------------------------------------------------------------- /src/money.cr: -------------------------------------------------------------------------------- 1 | require "big" 2 | require "json" 3 | {% if flag?(:docs) %} 4 | require "yaml" 5 | {% end %} 6 | require "tssc" 7 | require "atomic_write" 8 | 9 | struct Money 10 | end 11 | 12 | require "./core_ext/**" 13 | require "./money/error" 14 | require "./money/registry" 15 | require "./money/*" 16 | -------------------------------------------------------------------------------- /data/currencies/clf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 990, 4 | "code": "CLF", 5 | "name": "Unidad de Fomento", 6 | "symbol": "UF", 7 | "subunit": "Peso", 8 | "subunit_to_unit": 10000, 9 | "symbol_first": true, 10 | "decimal_mark": ",", 11 | "thousands_separator": "." 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/ggp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "code": "GGP", 4 | "name": "Guernsey Pound", 5 | "symbol": "£", 6 | "subunit": "Penny", 7 | "subunit_to_unit": 100, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/jep.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "code": "JEP", 4 | "name": "Jersey Pound", 5 | "symbol": "£", 6 | "subunit": "Penny", 7 | "subunit_to_unit": 100, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/krw.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 410, 4 | "code": "KRW", 5 | "name": "South Korean Won", 6 | "symbol": "₩", 7 | "subunit_to_unit": 1, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/trx.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "TRX", 4 | "name": "Tron", 5 | "symbol": "TRX", 6 | "subunit": "Sun", 7 | "subunit_to_unit": 1000000, 8 | "symbol_first": false, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/vuv.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 548, 4 | "code": "VUV", 5 | "name": "Vanuatu Vatu", 6 | "symbol": "Vt", 7 | "subunit_to_unit": 1, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/xag.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "metal", 3 | "iso_numeric": 961, 4 | "code": "XAG", 5 | "name": "Silver (Troy Ounce)", 6 | "symbol": "oz t", 7 | "subunit": "Oz", 8 | "subunit_to_unit": 1, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": "," 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/xrp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "XRP", 4 | "name": "Ripple", 5 | "symbol": "✕", 6 | "subunit": "Drop", 7 | "subunit_to_unit": 1000000, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/ada.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "ADA", 4 | "name": "Cardano", 5 | "symbol": "ADA", 6 | "subunit": "Lovelace", 7 | "subunit_to_unit": 1000000, 8 | "symbol_first": false, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/btc.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "BTC", 4 | "name": "Bitcoin", 5 | "symbol": "₿", 6 | "subunit": "Satoshi", 7 | "subunit_to_unit": 100000000, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/huf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 348, 4 | "code": "HUF", 5 | "name": "Hungarian Forint", 6 | "symbol": "Ft", 7 | "subunit_to_unit": 1, 8 | "symbol_first": false, 9 | "decimal_mark": ",", 10 | "thousands_separator": " ", 11 | "smallest_denomination": 5 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/irr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 364, 4 | "code": "IRR", 5 | "name": "Iranian Rial", 6 | "symbol": "﷼", 7 | "subunit_to_unit": 100, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 5000 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/ltc.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "LTC", 4 | "name": "Litecoin", 5 | "symbol": "Ł", 6 | "subunit": "Litoshi", 7 | "subunit_to_unit": 100000000, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/usdc.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "USDC", 4 | "name": "USD Coin", 5 | "symbol": "USDC", 6 | "subunit": "Cent", 7 | "subunit_to_unit": 1000000, 8 | "symbol_first": false, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/bnb.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "BNB", 4 | "name": "Binance Coin", 5 | "symbol": "BNB", 6 | "subunit": "Jager", 7 | "subunit_to_unit": 100000000, 8 | "symbol_first": false, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/doge.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "DOGE", 4 | "name": "Dogecoin", 5 | "symbol": "Ð", 6 | "subunit": "Satoshi", 7 | "subunit_to_unit": 100000000, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/dot.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "DOT", 4 | "name": "Polkadot", 5 | "symbol": "DOT", 6 | "subunit": "Planck", 7 | "subunit_to_unit": 10000000000, 8 | "symbol_first": false, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/eth.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "ETH", 4 | "name": "Ethereum", 5 | "symbol": "Ξ", 6 | "subunit": "Wei", 7 | "subunit_to_unit": 1000000000000000000, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/sol.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "SOL", 4 | "name": "Solana", 5 | "symbol": "SOL", 6 | "subunit": "Lamport", 7 | "subunit_to_unit": 1000000000, 8 | "symbol_first": false, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/xlm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "XLM", 4 | "name": "Stellar Lumens", 5 | "symbol": "*", 6 | "subunit": "Stroop", 7 | "subunit_to_unit": 10000000, 8 | "symbol_first": false, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/xmr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "XMR", 4 | "name": "Monero", 5 | "symbol": "ɱ", 6 | "subunit": "Piconero", 7 | "subunit_to_unit": 1000000000000, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/etc.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "ETC", 4 | "name": "Ethereum Classic", 5 | "symbol": "Ξ", 6 | "subunit": "Wei", 7 | "subunit_to_unit": 1000000000000000000, 8 | "symbol_first": true, 9 | "decimal_mark": ".", 10 | "thousands_separator": ",", 11 | "smallest_denomination": 1 12 | } 13 | -------------------------------------------------------------------------------- /data/currencies/bwp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 72, 4 | "code": "BWP", 5 | "name": "Botswana Pula", 6 | "symbol": "P", 7 | "subunit": "Thebe", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/clp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 152, 4 | "code": "CLP", 5 | "name": "Chilean Peso", 6 | "symbol": "$", 7 | "subunit": "Peso", 8 | "subunit_to_unit": 1, 9 | "symbol_first": true, 10 | "decimal_mark": ",", 11 | "thousands_separator": ".", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/mmk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 104, 4 | "code": "MMK", 5 | "name": "Myanmar Kyat", 6 | "symbol": "K", 7 | "subunit": "Pya", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 50 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/thb.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 764, 4 | "code": "THB", 5 | "name": "Thai Baht", 6 | "symbol": "฿", 7 | "subunit": "Satang", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/brl.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 986, 4 | "code": "BRL", 5 | "name": "Brazilian Real", 6 | "symbol": "R$", 7 | "subunit": "Centavo", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ",", 11 | "thousands_separator": ".", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/czk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 203, 4 | "code": "CZK", 5 | "name": "Czech Koruna", 6 | "symbol": "Kč", 7 | "subunit": "Haléř", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ",", 11 | "thousands_separator": " ", 12 | "smallest_denomination": 100 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/ern.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 232, 4 | "code": "ERN", 5 | "name": "Eritrean Nakfa", 6 | "symbol": "Nfk", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/etb.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 230, 4 | "code": "ETB", 5 | "name": "Ethiopian Birr", 6 | "symbol": "Br", 7 | "subunit": "Santim", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/gip.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 292, 4 | "code": "GIP", 5 | "name": "Gibraltar Pound", 6 | "symbol": "£", 7 | "subunit": "Penny", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/gmd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 270, 4 | "code": "GMD", 5 | "name": "Gambian Dalasi", 6 | "symbol": "D", 7 | "subunit": "Butut", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/htg.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 332, 4 | "code": "HTG", 5 | "name": "Haitian Gourde", 6 | "symbol": "G", 7 | "subunit": "Centime", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/khr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 116, 4 | "code": "KHR", 5 | "name": "Cambodian Riel", 6 | "symbol": "៛", 7 | "subunit": "Sen", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5000 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/kpw.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 408, 4 | "code": "KPW", 5 | "name": "North Korean Won", 6 | "symbol": "₩", 7 | "subunit": "Chŏn", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/mur.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 480, 4 | "code": "MUR", 5 | "name": "Mauritian Rupee", 6 | "symbol": "₨", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 100 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/myr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 458, 4 | "code": "MYR", 5 | "name": "Malaysian Ringgit", 6 | "symbol": "RM", 7 | "subunit": "Sen", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/ngn.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 566, 4 | "code": "NGN", 5 | "name": "Nigerian Naira", 6 | "symbol": "₦", 7 | "subunit": "Kobo", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 50 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/omr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 512, 4 | "code": "OMR", 5 | "name": "Omani Rial", 6 | "symbol": "ر.ع.", 7 | "subunit": "Baisa", 8 | "subunit_to_unit": 1000, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/pen.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 604, 4 | "code": "PEN", 5 | "name": "Peruvian Sol", 6 | "symbol": "S/", 7 | "subunit": "Céntimo", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ",", 11 | "thousands_separator": ".", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/pln.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 985, 4 | "code": "PLN", 5 | "name": "Polish Złoty", 6 | "symbol": "zł", 7 | "subunit": "Grosz", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ",", 11 | "thousands_separator": " ", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/ron.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 946, 4 | "code": "RON", 5 | "name": "Romanian Leu", 6 | "symbol": "Lei", 7 | "subunit": "Bani", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ",", 11 | "thousands_separator": ".", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/sdg.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 938, 4 | "code": "SDG", 5 | "name": "Sudanese Pound", 6 | "symbol": "£", 7 | "subunit": "Piastre", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/sle.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 925, 4 | "code": "SLE", 5 | "name": "New Leone", 6 | "symbol": "Le", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1000 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/srd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 968, 4 | "code": "SRD", 5 | "name": "Surinamese Dollar", 6 | "symbol": "$", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/szl.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 748, 4 | "code": "SZL", 5 | "name": "Swazi Lilangeni", 6 | "symbol": "E", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/vnd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 704, 4 | "code": "VND", 5 | "name": "Vietnamese Đồng", 6 | "symbol": "₫", 7 | "subunit": "Hào", 8 | "subunit_to_unit": 1, 9 | "symbol_first": false, 10 | "decimal_mark": ",", 11 | "thousands_separator": ".", 12 | "smallest_denomination": 100 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/yer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 886, 4 | "code": "YER", 5 | "name": "Yemeni Rial", 6 | "symbol": "﷼", 7 | "subunit": "Fils", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 100 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/zmw.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 967, 4 | "code": "ZMW", 5 | "name": "Zambian Kwacha", 6 | "symbol": "K", 7 | "subunit": "Ngwee", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/zwg.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 924, 4 | "code": "ZWG", 5 | "name": "Zimbabwe Gold", 6 | "symbol": "ZiG", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/aoa.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 973, 4 | "code": "AOA", 5 | "name": "Angolan Kwanza", 6 | "symbol": "Kz", 7 | "subunit": "Cêntimo", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 10 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/djf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 262, 4 | "code": "DJF", 5 | "name": "Djiboutian Franc", 6 | "symbol": "Fdj", 7 | "subunit": "Centime", 8 | "subunit_to_unit": 1, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 100 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/gtq.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 320, 4 | "code": "GTQ", 5 | "name": "Guatemalan Quetzal", 6 | "symbol": "Q", 7 | "subunit": "Centavo", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/hnl.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 340, 4 | "code": "HNL", 5 | "name": "Honduran Lempira", 6 | "symbol": "L", 7 | "subunit": "Centavo", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/idr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 360, 4 | "code": "IDR", 5 | "name": "Indonesian Rupiah", 6 | "symbol": "Rp", 7 | "subunit": "Sen", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ",", 11 | "thousands_separator": ".", 12 | "smallest_denomination": 5000 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/iqd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 368, 4 | "code": "IQD", 5 | "name": "Iraqi Dinar", 6 | "symbol": "ع.د", 7 | "subunit": "Fils", 8 | "subunit_to_unit": 1000, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 50000 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/kzt.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 398, 4 | "code": "KZT", 5 | "name": "Kazakhstani Tenge", 6 | "symbol": "₸", 7 | "subunit": "Tiyn", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 100 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/mad.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 504, 4 | "code": "MAD", 5 | "name": "Moroccan Dirham", 6 | "symbol": "د.م.", 7 | "subunit": "Centime", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/mga.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 969, 4 | "code": "MGA", 5 | "name": "Malagasy Ariary", 6 | "symbol": "Ar", 7 | "subunit": "Iraimbilanja", 8 | "subunit_to_unit": 1, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/mkd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 807, 4 | "code": "MKD", 5 | "name": "Macedonian Denar", 6 | "symbol": "ден", 7 | "subunit": "Deni", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 100 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/mnt.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 496, 4 | "code": "MNT", 5 | "name": "Mongolian Tögrög", 6 | "symbol": "₮", 7 | "subunit": "Möngö", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 2000 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/mru.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 929, 4 | "code": "MRU", 5 | "name": "Mauritanian Ouguiya", 6 | "symbol": "UM", 7 | "subunit": "Khoums", 8 | "subunit_to_unit": 5, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/mwk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 454, 4 | "code": "MWK", 5 | "name": "Malawian Kwacha", 6 | "symbol": "MK", 7 | "subunit": "Tambala", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/pgk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 598, 4 | "code": "PGK", 5 | "name": "Papua New Guinean Kina", 6 | "symbol": "K", 7 | "subunit": "Toea", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/pyg.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 600, 4 | "code": "PYG", 5 | "name": "Paraguayan Guaraní", 6 | "symbol": "₲", 7 | "subunit": "Céntimo", 8 | "subunit_to_unit": 1, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5000 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/shp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 654, 4 | "code": "SHP", 5 | "name": "Saint Helenian Pound", 6 | "symbol": "£", 7 | "subunit": "Penny", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/tjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 972, 4 | "code": "TJS", 5 | "name": "Tajikistani Somoni", 6 | "symbol": "ЅМ", 7 | "subunit": "Diram", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/tmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 934, 4 | "code": "TMT", 5 | "name": "Turkmenistani Manat", 6 | "symbol": "m", 7 | "subunit": "Tenge", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/tzs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 834, 4 | "code": "TZS", 5 | "name": "Tanzanian Shilling", 6 | "symbol": "Sh", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5000 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/uah.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 980, 4 | "code": "UAH", 5 | "name": "Ukrainian Hryvnia", 6 | "symbol": "₴", 7 | "subunit": "Kopiyka", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/ugx.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 800, 4 | "code": "UGX", 5 | "name": "Ugandan Shilling", 6 | "symbol": "USh", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 1, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1000 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/uyu.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 858, 4 | "code": "UYU", 5 | "name": "Uruguayan Peso", 6 | "symbol": "$U", 7 | "subunit": "Centésimo", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ",", 11 | "thousands_separator": ".", 12 | "smallest_denomination": 100 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/xcg.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 532, 4 | "code": "XCG", 5 | "name": "Caribbean Guilder", 6 | "symbol": "Cg", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/zar.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 710, 4 | "code": "ZAR", 5 | "name": "South African Rand", 6 | "symbol": "R", 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ",", 11 | "thousands_separator": " ", 12 | "smallest_denomination": 10 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/eur.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "priority": 2, 4 | "iso_numeric": 978, 5 | "code": "EUR", 6 | "name": "Euro", 7 | "symbol": "€", 8 | "subunit": "Cent", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ",", 12 | "thousands_separator": ".", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/imp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "code": "IMP", 4 | "name": "Isle of Man Pound", 5 | "symbol": "£", 6 | "alternate_symbols": ["M£"], 7 | "subunit": "Penny", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/isk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 352, 4 | "code": "ISK", 5 | "name": "Icelandic Króna", 6 | "symbol": "kr.", 7 | "alternate_symbols": ["Íkr"], 8 | "subunit_to_unit": 1, 9 | "symbol_first": false, 10 | "decimal_mark": ",", 11 | "thousands_separator": ".", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/pab.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 590, 4 | "code": "PAB", 5 | "name": "Panamanian Balboa", 6 | "symbol": "B/.", 7 | "subunit": "Centésimo", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/ssp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 728, 4 | "code": "SSP", 5 | "name": "South Sudanese Pound", 6 | "symbol": "£", 7 | "subunit": "Piaster", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/usdt.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "USDT", 4 | "name": "Tether", 5 | "symbol": "₮", 6 | "alternate_symbols": ["USDT", "USD₮"], 7 | "subunit": "Cent", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/gbp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "priority": 3, 4 | "iso_numeric": 826, 5 | "code": "GBP", 6 | "name": "British Pound", 7 | "symbol": "£", 8 | "subunit": "Penny", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/bam.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 977, 4 | "code": "BAM", 5 | "name": "Bosnia and Herzegovina Convertible Mark", 6 | "symbol": "КМ", 7 | "subunit": "Fening", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 5 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/stn.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 930, 4 | "code": "STN", 5 | "name": "São Tomé and Príncipe Second Dobra", 6 | "symbol": "Db", 7 | "subunit": "Cêntimo", 8 | "subunit_to_unit": 100, 9 | "symbol_first": false, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 10 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/bnd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 96, 4 | "code": "BND", 5 | "name": "Brunei Dollar", 6 | "symbol": "$", 7 | "alternate_symbols": ["B$"], 8 | "subunit": "Sen", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/jpy.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "priority": 6, 4 | "iso_numeric": 392, 5 | "code": "JPY", 6 | "name": "Japanese Yen", 7 | "symbol": "¥", 8 | "alternate_symbols": ["円", "圓"], 9 | "subunit_to_unit": 1, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/lak.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 418, 4 | "code": "LAK", 5 | "name": "Lao Kip", 6 | "symbol": "₭", 7 | "alternate_symbols": ["₭N"], 8 | "subunit": "Att", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 10 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/amd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 51, 4 | "code": "AMD", 5 | "name": "Armenian Dram", 6 | "symbol": "֏", 7 | "alternate_symbols": ["dram"], 8 | "subunit": "Luma", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 10 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/awg.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 533, 4 | "code": "AWG", 5 | "name": "Aruban Florin", 6 | "symbol": "ƒ", 7 | "alternate_symbols": ["Afl"], 8 | "subunit": "Cent", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 5 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/bdt.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 50, 4 | "code": "BDT", 5 | "name": "Bangladeshi Taka", 6 | "symbol": "৳", 7 | "alternate_symbols": ["Tk"], 8 | "subunit": "Paisa", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/bhd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 48, 4 | "code": "BHD", 5 | "name": "Bahraini Dinar", 6 | "symbol": "د.ب", 7 | "alternate_symbols": ["BD"], 8 | "subunit": "Fils", 9 | "subunit_to_unit": 1000, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 5 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/bsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 44, 4 | "code": "BSD", 5 | "name": "Bahamian Dollar", 6 | "symbol": "$", 7 | "alternate_symbols": ["B$"], 8 | "subunit": "Cent", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/cnh.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "code": "CNH", 4 | "name": "Chinese Renminbi Yuan Offshore", 5 | "symbol": "¥", 6 | "alternate_symbols": ["CN¥", "元", "CN元"], 7 | "subunit": "Fen", 8 | "subunit_to_unit": 100, 9 | "symbol_first": true, 10 | "decimal_mark": ".", 11 | "thousands_separator": ",", 12 | "smallest_denomination": 1 13 | } 14 | -------------------------------------------------------------------------------- /data/currencies/dkk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 208, 4 | "code": "DKK", 5 | "name": "Danish Krone", 6 | "symbol": "kr.", 7 | "alternate_symbols": [",-"], 8 | "subunit": "Øre", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ",", 12 | "thousands_separator": ".", 13 | "smallest_denomination": 50 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/gel.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 981, 4 | "code": "GEL", 5 | "name": "Georgian Lari", 6 | "symbol": "₾", 7 | "alternate_symbols": ["lari"], 8 | "subunit": "Tetri", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/mdl.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 498, 4 | "code": "MDL", 5 | "name": "Moldovan Leu", 6 | "symbol": "L", 7 | "alternate_symbols": ["lei"], 8 | "subunit": "Ban", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/qar.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 634, 4 | "code": "QAR", 5 | "name": "Qatari Riyal", 6 | "symbol": "ر.ق", 7 | "alternate_symbols": ["QR"], 8 | "subunit": "Dirham", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/sek.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 752, 4 | "code": "SEK", 5 | "name": "Swedish Krona", 6 | "symbol": "kr", 7 | "alternate_symbols": [":-"], 8 | "subunit": "Öre", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ",", 12 | "thousands_separator": " ", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/top.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 776, 4 | "code": "TOP", 5 | "name": "Tongan Paʻanga", 6 | "symbol": "T$", 7 | "alternate_symbols": ["PT"], 8 | "subunit": "Seniti", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/try.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 949, 4 | "code": "TRY", 5 | "name": "Turkish Lira", 6 | "symbol": "₺", 7 | "alternate_symbols": ["TL"], 8 | "subunit": "Kuruş", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ",", 12 | "thousands_separator": ".", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/xpf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 953, 4 | "code": "XPF", 5 | "name": "Cfp Franc", 6 | "symbol": "Fr", 7 | "alternate_symbols": ["F"], 8 | "subunit": "Centime", 9 | "subunit_to_unit": 1, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/chf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 756, 4 | "code": "CHF", 5 | "name": "Swiss Franc", 6 | "symbol": "CHF", 7 | "alternate_symbols": ["SFr", "Fr"], 8 | "subunit": "Rappen", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": "'", 13 | "smallest_denomination": 5 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/crc.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 188, 4 | "code": "CRC", 5 | "name": "Costa Rican Colón", 6 | "symbol": "₡", 7 | "alternate_symbols": ["¢"], 8 | "subunit": "Céntimo", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ",", 12 | "thousands_separator": ".", 13 | "smallest_denomination": 500 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/dzd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 12, 4 | "code": "DZD", 5 | "name": "Algerian Dinar", 6 | "symbol": "د.ج", 7 | "alternate_symbols": ["DA"], 8 | "subunit": "Centime", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/jod.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 400, 4 | "code": "JOD", 5 | "name": "Jordanian Dinar", 6 | "symbol": "د.ا", 7 | "alternate_symbols": ["JD"], 8 | "subunit": "Fils", 9 | "subunit_to_unit": 1000, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 5 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/kes.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 404, 4 | "code": "KES", 5 | "name": "Kenyan Shilling", 6 | "symbol": "KSh", 7 | "alternate_symbols": ["Sh"], 8 | "subunit": "Cent", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 50 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/kgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 417, 4 | "code": "KGS", 5 | "name": "Kyrgyzstani Som", 6 | "symbol": "som", 7 | "alternate_symbols": ["сом"], 8 | "subunit": "Tyiyn", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/kwd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 414, 4 | "code": "KWD", 5 | "name": "Kuwaiti Dinar", 6 | "symbol": "د.ك", 7 | "alternate_symbols": ["K.D."], 8 | "subunit": "Fils", 9 | "subunit_to_unit": 1000, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 5 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/lyd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 434, 4 | "code": "LYD", 5 | "name": "Libyan Dinar", 6 | "symbol": "ل.د", 7 | "alternate_symbols": ["LD"], 8 | "subunit": "Dirham", 9 | "subunit_to_unit": 1000, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 50 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/mop.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 446, 4 | "code": "MOP", 5 | "name": "Macanese Pataca", 6 | "symbol": "P", 7 | "alternate_symbols": ["MOP$"], 8 | "subunit": "Avo", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 10 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/nok.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 578, 4 | "code": "NOK", 5 | "name": "Norwegian Krone", 6 | "symbol": "kr", 7 | "alternate_symbols": [",-"], 8 | "subunit": "Øre", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ",", 12 | "thousands_separator": ".", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/pkr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 586, 4 | "code": "PKR", 5 | "name": "Pakistani Rupee", 6 | "symbol": "₨", 7 | "alternate_symbols": ["Rs"], 8 | "subunit": "Paisa", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/sos.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 706, 4 | "code": "SOS", 5 | "name": "Somali Shilling", 6 | "symbol": "Sh", 7 | "alternate_symbols": ["Sh.So"], 8 | "subunit": "Cent", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/svc.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 222, 4 | "code": "SVC", 5 | "name": "Salvadoran Colón", 6 | "symbol": "₡", 7 | "alternate_symbols": ["¢"], 8 | "subunit": "Centavo", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/afn.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 971, 4 | "code": "AFN", 5 | "name": "Afghan Afghani", 6 | "symbol": "؋", 7 | "alternate_symbols": ["Af", "Afs"], 8 | "subunit": "Pul", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/azn.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 944, 4 | "code": "AZN", 5 | "name": "Azerbaijani Manat", 6 | "symbol": "₼", 7 | "alternate_symbols": ["m", "man"], 8 | "subunit": "Qəpik", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/bob.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 68, 4 | "code": "BOB", 5 | "name": "Bolivian Boliviano", 6 | "symbol": "Bs.", 7 | "alternate_symbols": ["Bs"], 8 | "subunit": "Centavo", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 10 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/btn.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 64, 4 | "code": "BTN", 5 | "name": "Bhutanese Ngultrum", 6 | "symbol": "Nu.", 7 | "alternate_symbols": ["Nu"], 8 | "subunit": "Chertrum", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 5 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/ghs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 936, 4 | "code": "GHS", 5 | "name": "Ghanaian Cedi", 6 | "symbol": "₵", 7 | "alternate_symbols": ["GH¢", "GH₵"], 8 | "subunit": "Pesewa", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/mzn.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 943, 4 | "code": "MZN", 5 | "name": "Mozambican Metical", 6 | "symbol": "MTn", 7 | "alternate_symbols": ["MZN"], 8 | "subunit": "Centavo", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ",", 12 | "thousands_separator": ".", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/nio.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 558, 4 | "code": "NIO", 5 | "name": "Nicaraguan Córdoba", 6 | "symbol": "C$", 7 | "disambiguate_symbol": "NIO$", 8 | "subunit": "Centavo", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 5 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/rub.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 643, 4 | "code": "RUB", 5 | "name": "Russian Ruble", 6 | "symbol": "₽", 7 | "alternate_symbols": ["руб.", "р."], 8 | "subunit": "Kopeck", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ",", 12 | "thousands_separator": ".", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/rwf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 646, 4 | "code": "RWF", 5 | "name": "Rwandan Franc", 6 | "symbol": "FRw", 7 | "alternate_symbols": ["RF", "R₣"], 8 | "subunit": "Centime", 9 | "subunit_to_unit": 1, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/sar.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 682, 4 | "code": "SAR", 5 | "name": "Saudi Riyal", 6 | "symbol": "ر.س", 7 | "alternate_symbols": ["SR", "﷼"], 8 | "subunit": "Hallallah", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 5 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/sll.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2022-07-01", 3 | "type": "fiat", 4 | "iso_numeric": 694, 5 | "code": "SLL", 6 | "name": "Sierra Leonean Leone", 7 | "symbol": "Le", 8 | "subunit": "Cent", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1000 14 | } 15 | -------------------------------------------------------------------------------- /src/money/money/yaml.cr: -------------------------------------------------------------------------------- 1 | {% skip_file unless @top_level.has_constant?(:YAML) %} 2 | 3 | require "big/yaml" 4 | 5 | struct Money 6 | include YAML::Serializable 7 | 8 | def self.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) 9 | if node.is_a?(YAML::Nodes::Scalar) 10 | parse(node.value) 11 | else 12 | previous_def 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /data/currencies/ils.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 376, 4 | "code": "ILS", 5 | "name": "Israeli New Shekel", 6 | "symbol": "₪", 7 | "alternate_symbols": ["ש״ח", "NIS"], 8 | "subunit": "Agora", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 10 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/lbp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 422, 4 | "code": "LBP", 5 | "name": "Lebanese Pound", 6 | "symbol": "ل.ل", 7 | "alternate_symbols": ["£", "L£"], 8 | "subunit": "Piastre", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 25000 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/npr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 524, 4 | "code": "NPR", 5 | "name": "Nepalese Rupee", 6 | "symbol": "Rs.", 7 | "alternate_symbols": ["Rs", "रू", "₨"], 8 | "subunit": "Paisa", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/php.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 608, 4 | "code": "PHP", 5 | "name": "Philippine Peso", 6 | "symbol": "₱", 7 | "alternate_symbols": ["PHP", "PhP", "P"], 8 | "subunit": "Centavo", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/tnd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 788, 4 | "code": "TND", 5 | "name": "Tunisian Dinar", 6 | "symbol": "د.ت", 7 | "alternate_symbols": ["TD", "DT"], 8 | "subunit": "Millime", 9 | "subunit_to_unit": 1000, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 10 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/xof.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 952, 4 | "code": "XOF", 5 | "name": "West African Cfa Franc", 6 | "symbol": "CFA", 7 | "alternate_symbols": ["FCFA"], 8 | "subunit": "Centime", 9 | "subunit_to_unit": 1, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/bch.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "crypto", 3 | "code": "BCH", 4 | "name": "Bitcoin Cash", 5 | "symbol": "₿", 6 | "disambiguate_symbol": "₿CH", 7 | "alternate_symbols": ["BCH"], 8 | "subunit": "Satoshi", 9 | "subunit_to_unit": 100000000, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/cny.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 156, 4 | "code": "CNY", 5 | "name": "Chinese Renminbi Yuan", 6 | "symbol": "¥", 7 | "alternate_symbols": ["CN¥", "元", "CN元"], 8 | "subunit": "Fen", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/egp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 818, 4 | "code": "EGP", 5 | "name": "Egyptian Pound", 6 | "symbol": "ج.م", 7 | "alternate_symbols": ["LE", "E£", "L.E."], 8 | "subunit": "Piastre", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 25 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/ves.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 928, 4 | "code": "VES", 5 | "name": "Venezuelan Bolívar Soberano", 6 | "symbol": "Bs", 7 | "alternate_symbols": ["Bs.S"], 8 | "subunit": "Céntimo", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ",", 12 | "thousands_separator": ".", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/aed.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 784, 4 | "code": "AED", 5 | "name": "United Arab Emirates Dirham", 6 | "symbol": "د.إ", 7 | "alternate_symbols": ["DH", "Dhs"], 8 | "subunit": "Fils", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 25 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/inr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 356, 4 | "code": "INR", 5 | "name": "Indian Rupee", 6 | "symbol": "₹", 7 | "alternate_symbols": ["Rs", "৳", "૱", "௹", "रु", "₨"], 8 | "subunit": "Paisa", 9 | "subunit_to_unit": 100, 10 | "symbol_first": true, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 50 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/mvr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 462, 4 | "code": "MVR", 5 | "name": "Maldivian Rufiyaa", 6 | "symbol": "MVR", 7 | "alternate_symbols": ["MRF", "Rf", "/-", "ރ"], 8 | "subunit": "Laari", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/bgn.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 975, 4 | "code": "BGN", 5 | "name": "Bulgarian Lev", 6 | "symbol": "лв.", 7 | "alternate_symbols": ["lev", "leva", "лев", "лева"], 8 | "subunit": "Stotinka", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/uzs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 860, 4 | "code": "UZS", 5 | "name": "Uzbekistan Som", 6 | "symbol": "so'm", 7 | "alternate_symbols": ["so‘m", "сўм", "сум", "s", "с"], 8 | "subunit": "Tiyin", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/byn.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 933, 4 | "code": "BYN", 5 | "name": "Belarusian Ruble", 6 | "symbol": "Br", 7 | "alternate_symbols": ["бел. руб.", "б.р.", "руб.", "р."], 8 | "subunit": "Kapeyka", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ",", 12 | "thousands_separator": " ", 13 | "smallest_denomination": 1 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/bzd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 84, 4 | "code": "BZD", 5 | "name": "Belize Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "BZ$", 8 | "alternate_symbols": ["BZ$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/cup.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 192, 4 | "code": "CUP", 5 | "name": "Cuban Peso", 6 | "symbol": "$", 7 | "disambiguate_symbol": "$MN", 8 | "alternate_symbols": ["$MN"], 9 | "subunit": "Centavo", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/fjd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 242, 4 | "code": "FJD", 5 | "name": "Fijian Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "FJ$", 8 | "alternate_symbols": ["FJ$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 5 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/jmd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 388, 4 | "code": "JMD", 5 | "name": "Jamaican Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "J$", 8 | "alternate_symbols": ["J$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/lrd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 430, 4 | "code": "LRD", 5 | "name": "Liberian Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "L$", 8 | "alternate_symbols": ["L$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 5 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/lsl.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 426, 4 | "code": "LSL", 5 | "name": "Lesotho Loti", 6 | "symbol": "L", 7 | "disambiguate_symbol": "M", 8 | "alternate_symbols": ["M"], 9 | "subunit": "Sente", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/nad.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 516, 4 | "code": "NAD", 5 | "name": "Namibian Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "N$", 8 | "alternate_symbols": ["N$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 5 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/sgd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 702, 4 | "code": "SGD", 5 | "name": "Singapore Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "S$", 8 | "alternate_symbols": ["S$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/syp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 760, 4 | "code": "SYP", 5 | "name": "Syrian Pound", 6 | "symbol": "£S", 7 | "alternate_symbols": ["£", "ل.س", "LS", "الليرة السورية"], 8 | "subunit": "Piastre", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ".", 12 | "thousands_separator": ",", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/all.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 8, 4 | "code": "ALL", 5 | "name": "Albanian Lek", 6 | "symbol": "L", 7 | "disambiguate_symbol": "Lek", 8 | "alternate_symbols": ["Lek"], 9 | "subunit": "Qintar", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/bbd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 52, 4 | "code": "BBD", 5 | "name": "Barbadian Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "Bds$", 8 | "alternate_symbols": ["Bds$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/bmd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 60, 4 | "code": "BMD", 5 | "name": "Bermudian Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "BD$", 8 | "alternate_symbols": ["BD$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/cdf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 976, 4 | "code": "CDF", 5 | "name": "Congolese Franc", 6 | "symbol": "Fr", 7 | "disambiguate_symbol": "FC", 8 | "alternate_symbols": ["FC"], 9 | "subunit": "Centime", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/dop.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 214, 4 | "code": "DOP", 5 | "name": "Dominican Peso", 6 | "symbol": "$", 7 | "disambiguate_symbol": "RD$", 8 | "alternate_symbols": ["RD$"], 9 | "subunit": "Centavo", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/fkp.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 238, 4 | "code": "FKP", 5 | "name": "Falkland Pound", 6 | "symbol": "£", 7 | "disambiguate_symbol": "FK£", 8 | "alternate_symbols": ["FK£"], 9 | "subunit": "Penny", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/gyd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 328, 4 | "code": "GYD", 5 | "name": "Guyanese Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "G$", 8 | "alternate_symbols": ["G$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/historical/eek.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2011-01-01", 3 | "replaced_by": "EUR", 4 | "type": "fiat", 5 | "iso_numeric": 233, 6 | "code": "EEK", 7 | "name": "Estonian Kroon", 8 | "symbol": "KR", 9 | "subunit": "Sent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 5 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/historical/hrk.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2023-01-01", 3 | "replaced_by": "EUR", 4 | "type": "fiat", 5 | "iso_numeric": 191, 6 | "code": "HRK", 7 | "name": "Croatian Kuna", 8 | "symbol": "kn", 9 | "subunit": "Lipa", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ",", 13 | "thousands_separator": ".", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/historical/lvl.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2014-01-01", 3 | "replaced_by": "EUR", 4 | "type": "fiat", 5 | "iso_numeric": 428, 6 | "code": "LVL", 7 | "name": "Latvian Lats", 8 | "symbol": "Ls", 9 | "subunit": "Santīms", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/historical/skk.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2009-01-01", 3 | "replaced_by": "EUR", 4 | "type": "fiat", 5 | "iso_numeric": 703, 6 | "code": "SKK", 7 | "name": "Slovak Koruna", 8 | "symbol": "Sk", 9 | "subunit": "Halier", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 50 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/historical/zmk.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2013-01-01", 3 | "replaced_by": "ZMW", 4 | "type": "fiat", 5 | "iso_numeric": 894, 6 | "code": "ZMK", 7 | "name": "Zambian Kwacha", 8 | "symbol": "ZK", 9 | "subunit": "Ngwee", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 5 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/hkd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 344, 4 | "code": "HKD", 5 | "name": "Hong Kong Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "HK$", 8 | "alternate_symbols": ["HK$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 10 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/kmf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 174, 4 | "code": "KMF", 5 | "name": "Comorian Franc", 6 | "symbol": "Fr", 7 | "disambiguate_symbol": "CF", 8 | "alternate_symbols": ["CF"], 9 | "subunit": "Centime", 10 | "subunit_to_unit": 1, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/mxn.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 484, 4 | "code": "MXN", 5 | "name": "Mexican Peso", 6 | "symbol": "$", 7 | "disambiguate_symbol": "MEX$", 8 | "alternate_symbols": ["MEX$"], 9 | "subunit": "Centavo", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 5 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/nzd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 554, 4 | "code": "NZD", 5 | "name": "New Zealand Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "NZ$", 8 | "alternate_symbols": ["NZ$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 10 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/twd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 901, 4 | "code": "TWD", 5 | "name": "New Taiwan Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "NT$", 8 | "alternate_symbols": ["NT$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 50 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/bif.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 108, 4 | "code": "BIF", 5 | "name": "Burundian Franc", 6 | "symbol": "Fr", 7 | "disambiguate_symbol": "FBu", 8 | "alternate_symbols": ["FBu"], 9 | "subunit": "Centime", 10 | "subunit_to_unit": 1, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/cop.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 170, 4 | "code": "COP", 5 | "name": "Colombian Peso", 6 | "symbol": "$", 7 | "disambiguate_symbol": "COL$", 8 | "alternate_symbols": ["COL$"], 9 | "subunit": "Centavo", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ",", 13 | "thousands_separator": ".", 14 | "smallest_denomination": 20 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/gnf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 324, 4 | "code": "GNF", 5 | "name": "Guinean Franc", 6 | "symbol": "Fr", 7 | "disambiguate_symbol": "FG", 8 | "alternate_symbols": ["FG", "GFr"], 9 | "subunit": "Centime", 10 | "subunit_to_unit": 1, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/historical/ltl.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2015-01-01", 3 | "replaced_by": "EUR", 4 | "type": "fiat", 5 | "iso_numeric": 440, 6 | "code": "LTL", 7 | "name": "Lithuanian Litas", 8 | "symbol": "Lt", 9 | "subunit": "Centas", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/kyd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 136, 4 | "code": "KYD", 5 | "name": "Cayman Islands Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "CI$", 8 | "alternate_symbols": ["CI$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/sbd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 90, 4 | "code": "SBD", 5 | "name": "Solomon Islands Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "SI$", 8 | "alternate_symbols": ["SI$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 10 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/scr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 690, 4 | "code": "SCR", 5 | "name": "Seychellois Rupee", 6 | "symbol": "₨", 7 | "disambiguate_symbol": "SRe", 8 | "alternate_symbols": ["SRe", "SR"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/xcd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 951, 4 | "code": "XCD", 5 | "name": "East Caribbean Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "EX$", 8 | "alternate_symbols": ["EC$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/ars.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 32, 4 | "code": "ARS", 5 | "name": "Argentine Peso", 6 | "symbol": "$", 7 | "disambiguate_symbol": "$m/n", 8 | "alternate_symbols": ["$m/n", "m$n"], 9 | "subunit": "Centavo", 10 | "subunit_to_unit": 100, 11 | "symbol_first": true, 12 | "decimal_mark": ",", 13 | "thousands_separator": ".", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/cve.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 132, 4 | "code": "CVE", 5 | "name": "Cape Verdean Escudo", 6 | "symbol": "$", 7 | "disambiguate_symbol": "Esc", 8 | "alternate_symbols": ["Esc"], 9 | "subunit": "Centavo", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/historical/tmm.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2009-01-01", 3 | "replaced_by": "TMT", 4 | "type": "fiat", 5 | "code": "TMM", 6 | "iso_numeric": 795, 7 | "name": "Turkmenistani Manat", 8 | "symbol": "m", 9 | "subunit": "Tennesi", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/rsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 941, 4 | "code": "RSD", 5 | "name": "Serbian Dinar", 6 | "symbol": "RSD", 7 | "alternate_symbols": ["РСД", "din", "din.", "DIN", "Din", "дин", "дин."], 8 | "subunit": "Para", 9 | "subunit_to_unit": 100, 10 | "symbol_first": false, 11 | "decimal_mark": ",", 12 | "thousands_separator": ".", 13 | "smallest_denomination": 100 14 | } 15 | -------------------------------------------------------------------------------- /data/currencies/ttd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 780, 4 | "code": "TTD", 5 | "name": "Trinidad and Tobago Dollar", 6 | "symbol": "$", 7 | "disambiguate_symbol": "TT$", 8 | "alternate_symbols": ["TT$"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 1 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/wst.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 882, 4 | "code": "WST", 5 | "name": "Samoan Tala", 6 | "symbol": "T", 7 | "disambiguate_symbol": "WS$", 8 | "alternate_symbols": ["WS$", "SAT", "ST"], 9 | "subunit": "Sene", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 10 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/xaf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 950, 4 | "code": "XAF", 5 | "name": "Central African Cfa Franc", 6 | "symbol": "CFA", 7 | "disambiguate_symbol": "FCFA", 8 | "alternate_symbols": ["FCFA"], 9 | "subunit": "Centime", 10 | "subunit_to_unit": 1, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/aud.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "priority": 4, 4 | "iso_numeric": 36, 5 | "code": "AUD", 6 | "name": "Australian Dollar", 7 | "symbol": "$", 8 | "disambiguate_symbol": "A$", 9 | "alternate_symbols": ["A$"], 10 | "subunit": "Cent", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "decimal_mark": ".", 14 | "thousands_separator": ",", 15 | "smallest_denomination": 5 16 | } 17 | -------------------------------------------------------------------------------- /data/currencies/historical/std.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2018-04-01", 3 | "replaced_by": "STN", 4 | "type": "fiat", 5 | "iso_numeric": 678, 6 | "code": "STD", 7 | "name": "São Tomé and Príncipe Dobra", 8 | "symbol": "Db", 9 | "subunit": "Cêntimo", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 10000 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/lkr.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "iso_numeric": 144, 4 | "code": "LKR", 5 | "name": "Sri Lankan Rupee", 6 | "symbol": "₨", 7 | "disambiguate_symbol": "SLRs", 8 | "alternate_symbols": ["රු", "ரூ", "SLRs", "/-"], 9 | "subunit": "Cent", 10 | "subunit_to_unit": 100, 11 | "symbol_first": false, 12 | "decimal_mark": ".", 13 | "thousands_separator": ",", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/null.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | # Currency rate provider that always returns `nil`. 3 | class RateProvider::Null < RateProvider 4 | def initialize 5 | end 6 | 7 | getter base_currency_codes : Array(String) do 8 | [] of String 9 | end 10 | 11 | def exchange_rate?(base : Currency, target : Currency) : Rate? 12 | nil 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /data/currencies/cad.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "priority": 5, 4 | "iso_numeric": 124, 5 | "code": "CAD", 6 | "name": "Canadian Dollar", 7 | "symbol": "$", 8 | "disambiguate_symbol": "C$", 9 | "alternate_symbols": ["C$", "CAD$"], 10 | "subunit": "Cent", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "decimal_mark": ".", 14 | "thousands_separator": ",", 15 | "smallest_denomination": 5 16 | } 17 | -------------------------------------------------------------------------------- /data/currencies/usd.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "fiat", 3 | "priority": 1, 4 | "iso_numeric": 840, 5 | "code": "USD", 6 | "name": "United States Dollar", 7 | "symbol": "$", 8 | "disambiguate_symbol": "US$", 9 | "alternate_symbols": ["US$"], 10 | "subunit": "Cent", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "decimal_mark": ".", 14 | "thousands_separator": ",", 15 | "smallest_denomination": 1 16 | } 17 | -------------------------------------------------------------------------------- /data/currencies/historical/mtl.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2006-01-01", 3 | "replaced_by": "EUR", 4 | "type": "fiat", 5 | "code": "MTL", 6 | "iso_numeric": 470, 7 | "name": "Maltese Lira", 8 | "symbol": "₤", 9 | "alternate_symbols": ["Lm"], 10 | "subunit": "Cent", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "decimal_mark": ".", 14 | "thousands_separator": ",", 15 | "smallest_denomination": 1 16 | } 17 | -------------------------------------------------------------------------------- /data/currencies/historical/byr.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2016-07-01", 3 | "replaced_by": "BYN", 4 | "type": "fiat", 5 | "iso_numeric": 974, 6 | "code": "BYR", 7 | "name": "Belarusian Ruble", 8 | "symbol": "Br", 9 | "alternate_symbols": ["бел. руб.", "б.р.", "руб.", "р."], 10 | "subunit_to_unit": 1, 11 | "symbol_first": false, 12 | "decimal_mark": ",", 13 | "thousands_separator": " ", 14 | "smallest_denomination": 100 15 | } 16 | -------------------------------------------------------------------------------- /data/currencies/historical/mro.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2018-01-01", 3 | "replaced_by": "MRU", 4 | "type": "fiat", 5 | "iso_numeric": 478, 6 | "code": "MRO", 7 | "name": "Mauritanian Ouguiya", 8 | "symbol": "UM", 9 | "disambiguate_symbol": "A-UM", 10 | "subunit": "Khoums", 11 | "subunit_to_unit": 5, 12 | "symbol_first": false, 13 | "decimal_mark": ".", 14 | "thousands_separator": ",", 15 | "smallest_denomination": 1 16 | } 17 | -------------------------------------------------------------------------------- /data/currencies/historical/vef.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2018-08-20", 3 | "replaced_by": "VES", 4 | "type": "fiat", 5 | "iso_numeric": 937, 6 | "code": "VEF", 7 | "name": "Venezuelan Bolívar", 8 | "symbol": "Bs.F", 9 | "alternate_symbols": ["Bs"], 10 | "subunit": "Céntimo", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "decimal_mark": ",", 14 | "thousands_separator": ".", 15 | "smallest_denomination": 1 16 | } 17 | -------------------------------------------------------------------------------- /data/currencies/historical/ang.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2025-04-01", 3 | "replaced_by": "XCG", 4 | "type": "fiat", 5 | "iso_numeric": 532, 6 | "code": "ANG", 7 | "name": "Netherlands Antillean Gulden", 8 | "symbol": "ƒ", 9 | "alternate_symbols": ["NAƒ", "NAf", "f"], 10 | "subunit": "Cent", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "decimal_mark": ",", 14 | "thousands_separator": ".", 15 | "smallest_denomination": 1 16 | } 17 | -------------------------------------------------------------------------------- /data/currencies/historical/zwd.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2006-08-01", 3 | "replaced_by": "ZWN", 4 | "type": "fiat", 5 | "iso_numeric": 716, 6 | "code": "ZWD", 7 | "name": "Zimbabwean Dollar", 8 | "symbol": "$", 9 | "alternate_symbols": ["Z$"], 10 | "subunit": "Cent", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "html_entity": "$", 14 | "decimal_mark": ".", 15 | "thousands_separator": ",", 16 | "smallest_denomination": 100 17 | } 18 | -------------------------------------------------------------------------------- /data/currencies/historical/zwl.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2024-09-01", 3 | "replaced_by": "ZWG", 4 | "type": "fiat", 5 | "iso_numeric": 932, 6 | "code": "ZWL", 7 | "name": "Zimbabwean Dollar", 8 | "symbol": "$", 9 | "alternate_symbols": ["Z$"], 10 | "subunit": "Cent", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "html_entity": "$", 14 | "decimal_mark": ".", 15 | "thousands_separator": ",", 16 | "smallest_denomination": 100 17 | } 18 | -------------------------------------------------------------------------------- /data/currencies/historical/zwn.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2008-08-01", 3 | "replaced_by": "ZWR", 4 | "type": "fiat", 5 | "iso_numeric": 942, 6 | "code": "ZWN", 7 | "name": "Zimbabwean Dollar", 8 | "symbol": "$", 9 | "alternate_symbols": ["Z$"], 10 | "subunit": "Cent", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "html_entity": "$", 14 | "decimal_mark": ".", 15 | "thousands_separator": ",", 16 | "smallest_denomination": 100 17 | } 18 | -------------------------------------------------------------------------------- /data/currencies/historical/zwr.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2009-02-02", 3 | "replaced_by": "ZWL", 4 | "type": "fiat", 5 | "iso_numeric": 935, 6 | "code": "ZWR", 7 | "name": "Zimbabwean Dollar", 8 | "symbol": "$", 9 | "alternate_symbols": ["Z$"], 10 | "subunit": "Cent", 11 | "subunit_to_unit": 100, 12 | "symbol_first": true, 13 | "html_entity": "$", 14 | "decimal_mark": ".", 15 | "thousands_separator": ",", 16 | "smallest_denomination": 100 17 | } 18 | -------------------------------------------------------------------------------- /src/core_ext/kernel.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | # 3 | # Yields given block if the given constant *name* is defined 4 | # in the top-level scope. 5 | # 6 | # ``` 7 | # class Foo 8 | # if_defined?(:JSON) do 9 | # include JSON::Serializable 10 | # end 11 | # if_defined?(:YAML) do 12 | # include YAML::Serializable 13 | # end 14 | # end 15 | # ``` 16 | macro if_defined?(name, &block) 17 | {% if @top_level.has_constant?(name) %} 18 | {{ yield }} 19 | {% end %} 20 | end 21 | -------------------------------------------------------------------------------- /data/currencies/historical/ghc.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2007-07-01", 3 | "replaced_by": "GHS", 4 | "type": "fiat", 5 | "iso_numeric": 288, 6 | "code": "GHS", 7 | "name": "Ghanaian Cedi", 8 | "symbol": "₵", 9 | "disambiguate_symbol": "GH₵", 10 | "alternate_symbols": ["GH₵"], 11 | "subunit": "Pesewa", 12 | "subunit_to_unit": 100, 13 | "symbol_first": true, 14 | "decimal_mark": ".", 15 | "thousands_separator": ",", 16 | "smallest_denomination": 1 17 | } 18 | -------------------------------------------------------------------------------- /data/currencies/historical/cuc.json: -------------------------------------------------------------------------------- 1 | { 2 | "archived_at": "2021-07-01", 3 | "replaced_by": "CUP", 4 | "type": "fiat", 5 | "iso_numeric": 931, 6 | "code": "CUC", 7 | "name": "Cuban Convertible Peso", 8 | "symbol": "$", 9 | "disambiguate_symbol": "CUC$", 10 | "alternate_symbols": ["CUC$"], 11 | "subunit": "Centavo", 12 | "subunit_to_unit": 100, 13 | "symbol_first": false, 14 | "decimal_mark": ".", 15 | "thousands_separator": ",", 16 | "smallest_denomination": 1 17 | } 18 | -------------------------------------------------------------------------------- /spec/currency/exchange/single_currency_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe Money::Currency::Exchange::SingleCurrency do 4 | context "#exchange" do 5 | exchange = Money::Currency::Exchange::SingleCurrency.new 6 | 7 | it "raises when called" do 8 | expect_raises(Money::DifferentCurrencyError, "No exchanging of currencies allowed for USD -> EUR") do 9 | exchange.exchange(Money.new(1_00, "USD"), Money::Currency.find("EUR")) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: money 2 | version: 2.0.0-dev 3 | 4 | authors: 5 | - Sijawusz Pur Rahnama 6 | 7 | dependencies: 8 | baked_file_system: 9 | github: schovi/baked_file_system 10 | version: ~> 0.11.0 11 | tssc: 12 | github: Sija/tssc.cr 13 | version: ~> 1.0.0 14 | atomic_write: 15 | github: chris-huxtable/atomic_write.cr 16 | version: ~> 0.2.0 17 | 18 | development_dependencies: 19 | ameba: 20 | github: crystal-ameba/ameba 21 | branch: master 22 | 23 | crystal: ~> 1.18 24 | 25 | license: MIT 26 | -------------------------------------------------------------------------------- /src/money/money/json.cr: -------------------------------------------------------------------------------- 1 | {% skip_file unless @top_level.has_constant?(:JSON) %} 2 | 3 | require "big/json" 4 | 5 | struct Money 6 | include JSON::Serializable 7 | 8 | def self.new(pull : JSON::PullParser) 9 | if pull.kind.string? 10 | parse(pull.read_string) 11 | else 12 | previous_def 13 | end 14 | end 15 | 16 | # :nodoc: 17 | def self.from_json_object_key?(value : String) : Money 18 | parse(value) 19 | end 20 | 21 | # :nodoc: 22 | def to_json_object_key : String 23 | to_s 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/money/currency/yaml.cr: -------------------------------------------------------------------------------- 1 | {% skip_file unless @top_level.has_constant?(:YAML) %} 2 | 3 | require "big/yaml" 4 | require "uri/yaml" 5 | 6 | class Money::Currency 7 | include YAML::Serializable 8 | 9 | def self.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) 10 | if node.is_a?(YAML::Nodes::Scalar) 11 | find(node.value) 12 | else 13 | previous_def 14 | end 15 | end 16 | 17 | struct Rate 18 | include YAML::Serializable 19 | end 20 | 21 | class Exchange 22 | include YAML::Serializable 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/many_to_one.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | module RateProvider::ManyToOne 3 | abstract def target_currency_code : String 4 | 5 | getter target_currency_codes : Array(String) do 6 | [target_currency_code] 7 | end 8 | 9 | getter base_currency_codes : Array(String) do 10 | base_exchange_rates.map(&.base) 11 | end 12 | 13 | def exchange_rate?(base : Currency, target : Currency) : Rate? 14 | return unless target.code == target_currency_code 15 | 16 | base_exchange_rates 17 | .find(&.base.==(base.code)) 18 | end 19 | 20 | protected abstract def base_exchange_rates : Array(Rate) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/one_to_many.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | module RateProvider::OneToMany 3 | abstract def base_currency_code : String 4 | 5 | getter base_currency_codes : Array(String) do 6 | [base_currency_code] 7 | end 8 | 9 | getter target_currency_codes : Array(String) do 10 | target_exchange_rates.map(&.target) 11 | end 12 | 13 | def exchange_rate?(base : Currency, target : Currency) : Rate? 14 | return unless base.code == base_currency_code 15 | 16 | target_exchange_rates 17 | .find(&.target.==(target.code)) 18 | end 19 | 20 | protected abstract def target_exchange_rates : Array(Rate) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /src/money/currency/json.cr: -------------------------------------------------------------------------------- 1 | {% skip_file unless @top_level.has_constant?(:JSON) %} 2 | 3 | require "big/json" 4 | require "uri/json" 5 | 6 | class Money::Currency 7 | include JSON::Serializable 8 | 9 | def self.new(pull : JSON::PullParser) 10 | if pull.kind.string? 11 | find(pull.read_string) 12 | else 13 | previous_def 14 | end 15 | end 16 | 17 | # :nodoc: 18 | def self.from_json_object_key?(value : String) : Currency 19 | find(value) 20 | end 21 | 22 | # :nodoc: 23 | def to_json_object_key : String 24 | to_s 25 | end 26 | 27 | struct Rate 28 | include JSON::Serializable 29 | end 30 | 31 | class Exchange 32 | include JSON::Serializable 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/currency/rate_provider/null_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe Money::Currency::RateProvider::Null do 4 | provider = Money::Currency::RateProvider::Null.new 5 | 6 | describe "#base_currency_codes" do 7 | it "returns an empty array" do 8 | provider.base_currency_codes.should be_empty 9 | end 10 | end 11 | 12 | describe "#target_currency_codes" do 13 | it "returns an empty array" do 14 | provider.target_currency_codes.should be_empty 15 | end 16 | end 17 | 18 | describe "#exchange_rate?" do 19 | it "returns `nil`" do 20 | provider 21 | .exchange_rate?(Money::Currency.find("USD"), Money::Currency.find("EUR")) 22 | .should be_nil 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/currency/loader_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Money::Currency::Loader do 4 | context ".load_defaults" do 5 | defaults = Money::Currency.load_defaults 6 | 7 | it "returns non-empty currency table hash" do 8 | defaults.present?.should be_true 9 | end 10 | 11 | it "sets the hash key to the currency code" do 12 | defaults.each do |key, currency| 13 | key.should eq currency.code 14 | end 15 | end 16 | 17 | it "returns common currencies within currency table hash" do 18 | currencies = defaults.values.map(&.code) 19 | currencies.should contain "USD" 20 | currencies.should contain "EUR" 21 | currencies.should contain "BTC" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/money/money/casting.cr: -------------------------------------------------------------------------------- 1 | struct Money 2 | module Casting 3 | # Returns the amount of money as a `BigDecimal`. 4 | # 5 | # ``` 6 | # Money.us_dollar(1_00).to_big_d # => BigDecimal.new("1.0") 7 | # ``` 8 | def to_big_d : BigDecimal 9 | amount 10 | end 11 | 12 | # Returns the amount of money as a `BigFloat`. 13 | # 14 | # WARNING: Floating points cannot guarantee precision. Therefore, this 15 | # function should only be used when you no longer need to represent 16 | # currency or working with another system that requires floats. 17 | # 18 | # ``` 19 | # Money.us_dollar(1_00).to_big_f # => BigFloat.new("1.0") 20 | # ``` 21 | def to_big_f : BigFloat 22 | amount.to_big_f 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "yaml" 3 | require "spec" 4 | require "../src/money" 5 | 6 | def with_default_currency(currency = nil, &) 7 | previous_currency = Money.default_currency 8 | begin 9 | Money.default_currency = currency if currency 10 | yield 11 | ensure 12 | Money.default_currency = previous_currency 13 | end 14 | end 15 | 16 | def with_registered_currency(*currencies, &) 17 | currencies.each do |currency| 18 | Money::Currency.register(currency) 19 | end 20 | yield 21 | ensure 22 | currencies.each do |currency| 23 | Money::Currency.unregister(currency) 24 | end 25 | end 26 | 27 | Spec.around_each do |example| 28 | Money.default_currency = "USD" 29 | example.run 30 | ensure 31 | Money.default_currency = nil 32 | end 33 | -------------------------------------------------------------------------------- /src/money/currency/exchange/single_currency.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | # Class to ensure client code is operating in a single currency 3 | # by raising if an exchange attempts to happen. 4 | # 5 | # This is useful when an application uses multiple currencies but 6 | # it usually deals with only one currency at a time so any arithmetic 7 | # where exchanges happen are erroneous. Using this as the default exchange 8 | # means that that these mistakes don't silently do the wrong thing. 9 | class Exchange::SingleCurrency < Exchange 10 | # Raises a `DifferentCurrencyError` to remove possibility of accidentally 11 | # exchanging currencies. 12 | def exchange(from : Money, to : Currency) : Money 13 | raise DifferentCurrencyError.new(from.currency, to) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /src/money/currency/loader.cr: -------------------------------------------------------------------------------- 1 | require "baked_file_system" 2 | 3 | class Money::Currency 4 | private class FileStorage 5 | extend BakedFileSystem 6 | 7 | if_defined?(:JSON) do 8 | bake_folder "../../../data/currencies" 9 | end 10 | end 11 | 12 | module Loader 13 | # Loads and returns the currencies stored in JSON files 14 | # inside of `data/currencies` directory. 15 | def load_defaults : Hash(String, Currency) 16 | currency_table = {} of String => Currency 17 | 18 | if_defined?(:JSON) do 19 | FileStorage.files.each do |file| 20 | currency = Currency.from_json(file) 21 | currency_table[currency.code] = currency 22 | ensure 23 | file.rewind 24 | end 25 | end 26 | currency_table 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/money/casting_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Money::Casting do 4 | describe "#to_big_d" do 5 | it "works as documented" do 6 | money = Money.new(10_00).to_big_d 7 | money.should be_a BigDecimal 8 | money.should eq 10.0 9 | end 10 | 11 | it "respects :subunit_to_unit currency property" do 12 | money = Money.new(10_00, "BHD").to_big_d 13 | money.should eq 1.0 14 | end 15 | end 16 | 17 | describe "#to_big_f" do 18 | it "works as documented" do 19 | money = Money.new(10_00).to_big_f 20 | money.should be_a BigFloat 21 | money.should eq 10.0 22 | end 23 | 24 | it "respects :subunit_to_unit currency property" do 25 | money = Money.new(10_00, "BHD").to_big_f 26 | money.should eq 1.0 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/money/registry/converter/json.cr: -------------------------------------------------------------------------------- 1 | {% skip_file unless @top_level.has_constant?(:JSON) %} 2 | 3 | struct Money 4 | module Registry::Converter::JSON(V) 5 | private struct Wrapper(T) 6 | include ::JSON::Serializable 7 | 8 | getter name : String 9 | getter options : Hash(String, ::JSON::Any::Type)? 10 | 11 | def unbox : T 12 | klass = T.find(name) 13 | klass.from_json(options.try(&.to_json) || "{}") 14 | end 15 | end 16 | 17 | extend self 18 | 19 | def from_json(pull : ::JSON::PullParser) : V 20 | wrapper = Wrapper(V).new(pull) 21 | wrapper.unbox 22 | end 23 | 24 | def to_json(value : V, json : ::JSON::Builder) 25 | { 26 | name: value.class.key, 27 | options: value, 28 | }.to_json(json) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /src/money/registry/converter/yaml.cr: -------------------------------------------------------------------------------- 1 | {% skip_file unless @top_level.has_constant?(:YAML) %} 2 | 3 | struct Money 4 | module Registry::Converter::YAML(V) 5 | private struct Wrapper(T) 6 | include ::YAML::Serializable 7 | 8 | getter name : String 9 | getter options : Hash(String, ::YAML::Any::Type)? 10 | 11 | def unbox : T 12 | klass = T.find(name) 13 | klass.from_yaml(options.try(&.to_yaml) || "{}") 14 | end 15 | end 16 | 17 | extend self 18 | 19 | def from_yaml(ctx : ::YAML::ParseContext, node : ::YAML::Nodes::Node) : V 20 | wrapper = Wrapper(V).new(ctx, node) 21 | wrapper.unbox 22 | end 23 | 24 | def to_yaml(value : V, yaml : ::YAML::Nodes::Builder) 25 | { 26 | name: value.class.key, 27 | options: value, 28 | }.to_yaml(yaml) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/error.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | abstract class RateProvider 3 | # Base class for errors raised by rate providers. 4 | class Error < Money::Error 5 | end 6 | 7 | # Raised when a rate provider is missing a required option. 8 | class RequiredOptionError < Error 9 | end 10 | 11 | # Raised when a request to a rate provider fails. 12 | class RequestError < Error 13 | def initialize(status) 14 | super("Request failed with status: #{status}") 15 | end 16 | end 17 | 18 | # Raised when a rate provider returns an error. 19 | class ResponseError < Error 20 | def initialize(code, detail = nil) 21 | if detail = detail.try(&.to_s.presence) 22 | super("Rate provider error (#{code}): #{detail}") 23 | else 24 | super("Rate provider error (#{code})") 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | build-and-deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Inject slug/short variables 15 | uses: rlespinasse/github-slug-action@v5 16 | 17 | - name: Install Crystal 18 | uses: crystal-lang/install-crystal@v1 19 | 20 | - name: Download source 21 | uses: actions/checkout@v6 22 | 23 | - name: Install dependencies 24 | run: shards install 25 | 26 | - name: Build docs 27 | run: | 28 | crystal docs \ 29 | --project-version="${{ env.GITHUB_REF_SLUG }}" \ 30 | --source-refname="${{ env.GITHUB_SHA_SHORT }}" 31 | 32 | - name: Deploy docs 🚀 33 | uses: JamesIves/github-pages-deploy-action@v4 34 | with: 35 | branch: gh-pages 36 | folder: docs 37 | clean: true 38 | -------------------------------------------------------------------------------- /src/core_ext/to_money.cr: -------------------------------------------------------------------------------- 1 | class String 2 | # Returns a `Money` instance parsed from `self` if possible, `nil` otherwise. 3 | # 4 | # See also `Money.parse?`. 5 | def to_money?(*, allow_ambiguous = true) : Money? 6 | Money.parse?(self, allow_ambiguous: allow_ambiguous) 7 | end 8 | 9 | # Returns a `Money` instance parsed from `self`. 10 | # 11 | # See also `Money.parse`. 12 | def to_money(*, allow_ambiguous = true) : Money 13 | Money.parse(self, allow_ambiguous: allow_ambiguous) 14 | end 15 | end 16 | 17 | struct Number 18 | # Returns a `Money` instance parsed from `self` if possible, `nil` otherwise. 19 | # 20 | # See also `Money.from_amount`. 21 | def to_money?(currency = Money.default_currency) : Money? 22 | Money.from_amount(self, currency) rescue nil 23 | end 24 | 25 | # Returns a `Money` instance parsed from `self`. 26 | # 27 | # See also `Money.from_amount`. 28 | def to_money(currency = Money.default_currency) : Money 29 | Money.from_amount(self, currency) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | crystal: [latest, nightly] 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - name: Install Crystal 24 | uses: crystal-lang/install-crystal@v1 25 | with: 26 | crystal: ${{ matrix.crystal }} 27 | 28 | - name: Download source 29 | uses: actions/checkout@v6 30 | 31 | - name: Install dependencies 32 | run: shards install 33 | 34 | - name: Run specs 35 | run: crystal spec 36 | 37 | - name: Check formatting 38 | run: crystal tool format --check 39 | if: success() || failure() 40 | 41 | - name: Run ameba linter 42 | run: bin/ameba 43 | if: success() || failure() 44 | -------------------------------------------------------------------------------- /src/money/money/exchange.cr: -------------------------------------------------------------------------------- 1 | struct Money 2 | module Exchange 3 | # Exchanges `self` to a new `Money` object in *other_currency*. 4 | # 5 | # ``` 6 | # Money.default_exchange.rate_store["USD", "EUR"] = 1.23 7 | # Money.default_exchange.rate_store["EUR", "USD"] = 0.82 8 | # 9 | # Money.new(1_00, "USD").exchange_to("EUR") # => Money(@amount=1.23, @currency="EUR") 10 | # Money.new(1_00, "EUR").exchange_to("USD") # => Money(@amount=0.82, @currency="USD") 11 | # ``` 12 | def exchange_to(other_currency : String | Symbol | Currency, exchange = Money.default_exchange) : Money 13 | other_currency = Currency[other_currency] 14 | case 15 | when other_currency == currency 16 | self 17 | when zero? 18 | with_currency(other_currency) 19 | else 20 | exchange.exchange(self, other_currency) 21 | end 22 | end 23 | 24 | # Yields *other* `Money` object exchanged to `self.currency`. 25 | def with_same_currency(other : Money, &) 26 | yield other.exchange_to(currency) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/money/yaml_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Money do 4 | money_yaml = <<-YAML 5 | --- 6 | amount: 10.0 7 | currency: USD\n 8 | YAML 9 | 10 | describe ".from_yaml" do 11 | context "(object)" do 12 | it "returns unserialized Money object" do 13 | money = Money.from_yaml(money_yaml) 14 | money.fractional.should eq 10_00 15 | money.amount.should eq 10.0 16 | money.currency.should eq Money::Currency.find("USD") 17 | money.to_yaml.should eq money_yaml 18 | end 19 | end 20 | 21 | context "(string)" do 22 | it "returns Money object parsed from string" do 23 | Money.from_yaml("$10.00").should eq Money.new(10_00, "USD") 24 | end 25 | it "raises Parse::Error when invalid string is given" do 26 | expect_raises(Money::Parse::Error) { Money.from_yaml("10 FOO") } 27 | end 28 | end 29 | end 30 | 31 | describe "#to_yaml" do 32 | it "works as intended" do 33 | Money.from_yaml(money_yaml).to_yaml 34 | .should eq Money.new(10_00, "USD").to_yaml 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/currency/yaml_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Money::Currency do 4 | currency_yaml = <<-YAML 5 | --- 6 | code: FOO 7 | subunit_to_unit: 100\n 8 | YAML 9 | 10 | describe ".from_yaml" do 11 | context "(object)" do 12 | it "returns unserialized Currency object" do 13 | currency = Money::Currency.from_yaml(currency_yaml) 14 | currency.code.should eq "FOO" 15 | currency.decimal_places.should eq 2 16 | end 17 | end 18 | 19 | context "(string)" do 20 | it "returns Currency object using Currency.find when known key is given" do 21 | Money::Currency.from_yaml("USD").should eq Money::Currency.find("USD") 22 | end 23 | it "raises NotFoundError when unknown key is given" do 24 | expect_raises(Money::Currency::NotFoundError) do 25 | Money::Currency.from_yaml("FOO") 26 | end 27 | end 28 | end 29 | end 30 | 31 | describe "#to_yaml" do 32 | it "works as intended" do 33 | Money::Currency.from_yaml(currency_yaml).to_yaml 34 | .should eq currency_yaml 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2025 Sijawusz Pur Rahnama 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/coingecko.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [CoinGecko](https://www.coingecko.com/) currency rate provider. 5 | # 6 | # NOTE: Supports only `BTC`-based conversions. 7 | class RateProvider::CoinGecko < RateProvider 8 | include RateProvider::HTTP 9 | include RateProvider::OneToMany 10 | 11 | Log = ::Log.for(self) 12 | 13 | def base_currency_code : String 14 | "BTC" 15 | end 16 | 17 | getter host : URI do 18 | URI.parse("https://api.coingecko.com") 19 | end 20 | 21 | def initialize(*, @host = nil) 22 | end 23 | 24 | # 25 | protected def target_exchange_rates : Array(Rate) 26 | Log.debug { "Fetching rates for base currency #{base_currency_code}" } 27 | 28 | request("/api/v3/exchange_rates") do |response| 29 | result = JSON.parse(response.body_io).as_h 30 | rates = 31 | result["rates"].as_h 32 | 33 | rates.map do |currency_code, rate| 34 | Rate.new(base_currency_code, currency_code.upcase, rate["value"].to_s.to_big_d) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/ecb.cr: -------------------------------------------------------------------------------- 1 | require "xml" 2 | require "log" 3 | 4 | class Money::Currency 5 | # Currency rate provider using data sourced from a daily feed of 6 | # [European Central Bank](https://www.ecb.europa.eu/). 7 | # 8 | # NOTE: Supports only `EUR`-based conversions. 9 | class RateProvider::ECB < RateProvider 10 | include RateProvider::HTTP 11 | include RateProvider::OneToMany 12 | 13 | Log = ::Log.for(self) 14 | 15 | def base_currency_code : String 16 | "EUR" 17 | end 18 | 19 | getter host : URI do 20 | URI.parse("https://www.ecb.europa.eu") 21 | end 22 | 23 | def initialize(*, @host = nil) 24 | end 25 | 26 | # 27 | protected def target_exchange_rates : Array(Rate) 28 | Log.debug { "Fetching rates for base currency #{base_currency_code}" } 29 | 30 | request("/stats/eurofxref/eurofxref-daily.xml") do |response| 31 | result = XML.parse(response.body_io) 32 | rates = 33 | result.xpath_nodes("//*[@currency and @rate]") 34 | 35 | rates.map do |node| 36 | Rate.new(base_currency_code, node["currency"], node["rate"].to_big_d) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/nbp.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # Currency rate provider using data sourced from a daily feed of 5 | # [Polish National Bank](https://nbp.pl/). 6 | # 7 | # NOTE: Supports only `PLN`-targeted conversions. 8 | class RateProvider::NBP < RateProvider 9 | include RateProvider::HTTP 10 | include RateProvider::ManyToOne 11 | 12 | Log = ::Log.for(self) 13 | 14 | def target_currency_code : String 15 | "PLN" 16 | end 17 | 18 | getter host : URI do 19 | URI.parse("https://api.nbp.pl") 20 | end 21 | 22 | def initialize(*, @host = nil) 23 | end 24 | 25 | # 26 | protected def base_exchange_rates : Array(Rate) 27 | Log.debug { "Fetching rates for target currency #{target_currency_code}" } 28 | 29 | params = { 30 | "format": "json", 31 | } 32 | request("/api/exchangerates/tables/a/", params) do |response| 33 | result = JSON.parse(response.body_io).as_a 34 | rates = result 35 | .find!(&.as_h["table"].==("A")) 36 | .as_h["rates"] 37 | .as_a 38 | 39 | rates.map do |rate| 40 | Rate.new(rate["code"].as_s, target_currency_code, rate["mid"].to_s.to_big_d) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/bitpay.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [BitPay](https://www.bitpay.com/) currency rate provider. 5 | # 6 | # NOTE: Supports only some of the crypto-based conversions. 7 | class RateProvider::BitPay < RateProvider 8 | include RateProvider::HTTP 9 | include RateProvider::OneToMany 10 | 11 | Log = ::Log.for(self) 12 | 13 | # Supported values: `BTC`, `BCH`, `ETH`, `XRP`, `DOGE`, `LTC`. 14 | # 15 | # See 16 | getter base_currency_code : String = "BTC" 17 | 18 | getter host : URI do 19 | URI.parse("https://bitpay.com") 20 | end 21 | 22 | def initialize(*, @base_currency_code = "BTC", @host = nil) 23 | end 24 | 25 | # 26 | protected def target_exchange_rates : Array(Rate) 27 | Log.debug { "Fetching rates for base currency #{base_currency_code}" } 28 | 29 | path = 30 | "/api/rates/%s" % URI.encode_path_segment(base_currency_code) 31 | 32 | request(path) do |response| 33 | result = JSON.parse(response.body_io).as_a 34 | rates = 35 | result.map do |rate| 36 | Rate.new(base_currency_code, rate["code"].as_s, rate["rate"].to_s.to_big_d) 37 | end 38 | 39 | rates 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/cbr.cr: -------------------------------------------------------------------------------- 1 | require "xml" 2 | require "log" 3 | 4 | class Money::Currency 5 | # Currency rate provider using data sourced from a daily feed of 6 | # [Bank of Russia](https://www.cbr.ru/). 7 | # 8 | # NOTE: Supports only `RUB`-targeted conversions. 9 | class RateProvider::CBR < RateProvider 10 | include RateProvider::HTTP 11 | include RateProvider::ManyToOne 12 | 13 | Log = ::Log.for(self) 14 | 15 | def target_currency_code : String 16 | "RUB" 17 | end 18 | 19 | getter host : URI do 20 | URI.parse("https://www.cbr.ru") 21 | end 22 | 23 | def initialize(*, @host = nil) 24 | end 25 | 26 | # 27 | protected def base_exchange_rates : Array(Rate) 28 | Log.debug { "Fetching rates for target currency #{target_currency_code}" } 29 | 30 | request("/scripts/XML_daily.asp") do |response| 31 | result = XML.parse(response.body_io) 32 | rates = 33 | result.xpath_nodes("//Valute") 34 | 35 | rates.compact_map do |node| 36 | next unless currency_code = node.xpath_string("string(CharCode)").presence 37 | next unless rate = node.xpath_string("string(VunitRate)").presence 38 | 39 | rate = 40 | rate.sub(',', '.').to_big_d 41 | 42 | Rate.new(currency_code, target_currency_code, rate) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /src/money/currency/rate_store/memory.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | # Class for in-memory concurrency-safe storage of exchange rate pairs. 3 | # 4 | # ``` 5 | # store = Money::Currency::RateStore::Memory.new 6 | # store["USD", "CAD"] = 0.98 7 | # store["USD", "CAD"] # => 0.98 8 | # 9 | # # Iterates rates 10 | # store.each do |rate| 11 | # puts rate 12 | # end 13 | # ``` 14 | class RateStore::Memory < RateStore 15 | private getter! index : Hash(String, Rate) 16 | 17 | protected def after_initialize 18 | super 19 | @index ||= {} of String => Rate 20 | end 21 | 22 | protected def set_rate(rate : Rate) : Nil 23 | index[Rate.key(rate.base, rate.target)] = rate 24 | end 25 | 26 | protected def get_rate?(base : Currency, target : Currency) : Rate? 27 | index[Rate.key(base, target)]? 28 | end 29 | 30 | protected def each_rate(& : Rate ->) 31 | index.each_value { |rate| yield rate } 32 | end 33 | 34 | protected def clear_rates : Nil 35 | index.clear 36 | end 37 | 38 | protected def clear_rates(base : Currency?, target : Currency?) : Nil 39 | index.reject! do |_, rate| 40 | (!base || (base && rate.base == base.code)) && 41 | (!target || (target && rate.target == target.code)) 42 | end 43 | end 44 | 45 | protected def clear_rates(rates : Enumerable(Rate)) : Nil 46 | index.reject! do |_, rate| 47 | rate.in?(rates) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/money/currency/converter.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | # Currency converter to be used with JSON and YAML serialization. 3 | # 4 | # ``` 5 | # require "json" 6 | # require "yaml" 7 | # require "money" 8 | # 9 | # record FooWithCurrency, currency : Money::Currency do 10 | # include JSON::Serializable 11 | # include YAML::Serializable 12 | # 13 | # @[JSON::Field(converter: Money::Currency::Converter)] 14 | # @[YAML::Field(converter: Money::Currency::Converter)] 15 | # @currency : Money::Currency 16 | # end 17 | # 18 | # foo = FooWithCurrency.new(Money::Currency.find("USD")) 19 | # foo.to_json # => "{\"currency\":\"USD\"}" 20 | # foo.to_yaml # => "---\ncurrency: USD\n" 21 | # ``` 22 | module Converter 23 | extend self 24 | 25 | if_defined?(:JSON) do 26 | def from_json(pull : JSON::PullParser) : Currency 27 | Currency.find(pull.read_string) 28 | end 29 | 30 | def to_json(currency : Currency, json : JSON::Builder) 31 | json.string(currency.code) 32 | end 33 | end 34 | 35 | if_defined?(:YAML) do 36 | def from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Currency 37 | unless node.is_a?(YAML::Nodes::Scalar) 38 | node.raise "Expected scalar, not #{node.kind}" 39 | end 40 | Currency.find(node.value) 41 | end 42 | 43 | def to_yaml(currency : Currency, yaml : YAML::Nodes::Builder) 44 | yaml.scalar(currency.code) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | abstract class RateProvider 3 | extend Money::Registry 4 | 5 | # Raised when trying to find an unknown rate provider. 6 | class NotFoundError < Registry::NotFoundError 7 | def initialize(*, key : String) 8 | super("Unknown rate provider: #{key}") 9 | end 10 | end 11 | 12 | # Returns the value of the environment variable *key* or raises 13 | # `RequiredOptionError` if the variable is not set. 14 | protected def option_from_env(key : String) : String 15 | ENV[key]? || 16 | raise RequiredOptionError.new \ 17 | "Environment variable #{key.inspect} is required" 18 | end 19 | 20 | # Returns an array of supported base currency codes. 21 | abstract def base_currency_codes : Array(String) 22 | 23 | # Returns an array of supported target currency codes. 24 | def target_currency_codes : Array(String) 25 | base_currency_codes 26 | end 27 | 28 | # Returns the exchange rate between *base* and *target* currency, or `nil` if not found. 29 | abstract def exchange_rate?(base : Currency, target : Currency) : Rate? 30 | 31 | # Returns `true` if the provider supports the given currency pair. 32 | def supports_currency_pair?(base : Currency, target : Currency) : Bool 33 | base_currency_codes.includes?(base.code) && 34 | target_currency_codes.includes?(target.code) 35 | end 36 | end 37 | end 38 | 39 | require "./rate_provider/*" 40 | require "./rate_provider/providers/*" 41 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/float_rates.cr: -------------------------------------------------------------------------------- 1 | require "xml" 2 | require "log" 3 | 4 | class Money::Currency 5 | # [FloatRates](https://www.floatrates.com/) currency rate provider. 6 | class RateProvider::FloatRates < RateProvider 7 | include RateProvider::HTTP 8 | 9 | Log = ::Log.for(self) 10 | 11 | getter host : URI do 12 | URI.parse("https://www.floatrates.com") 13 | end 14 | 15 | def initialize(*, @host = nil) 16 | end 17 | 18 | # 19 | getter base_currency_codes : Array(String) do 20 | Log.debug { "Fetching supported currencies" } 21 | 22 | request("/json-feeds.html") do |response| 23 | result = XML.parse_html(response.body_io) 24 | currencies = 25 | result.xpath_nodes("//li/a[starts-with(@href, 'https://www.floatrates.com/daily/')]") 26 | 27 | currencies.map do |node| 28 | node["href"].match!(/(?\w+)\.json$/)["code"].upcase 29 | end 30 | end 31 | end 32 | 33 | def exchange_rate?(base : Currency, target : Currency) : Rate? 34 | Log.debug { "Fetching rate for #{base} -> #{target}" } 35 | 36 | path = 37 | "/daily/%s.json" % URI.encode_path_segment(base.code.downcase) 38 | 39 | request(path) do |response| 40 | result = JSON.parse(response.body_io).as_h 41 | rate = 42 | result.dig(target.code.downcase, "rate").to_s.to_big_d 43 | 44 | Rate.new(base, target, rate) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/money/currency/validation.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | module Validation 3 | protected def normalize! : Nil 4 | @name = @name.presence 5 | @symbol = @symbol.presence 6 | @disambiguate_symbol = @disambiguate_symbol.presence 7 | @subunit = @subunit.presence 8 | @format = @format.presence 9 | end 10 | 11 | protected def validate! : Nil 12 | validate_code 13 | validate_subunit_to_unit 14 | validate_iso_numeric 15 | validate_smallest_denomination 16 | end 17 | 18 | private def validate_positive_number(value : Number?, label : String) 19 | if value && !value.positive? 20 | raise ArgumentError.new "#{label} value must be positive: #{value}" 21 | end 22 | end 23 | 24 | private def validate_code 25 | return if code.presence && 26 | code.size >= 3 && 27 | code[0].ascii_uppercase? && 28 | code.chars.all? { |char| char.ascii_uppercase? || char.ascii_number? } 29 | 30 | raise ArgumentError.new \ 31 | "Code must be all uppercase 3+ letters and/or digits: #{code.inspect}" 32 | end 33 | 34 | private def validate_subunit_to_unit 35 | validate_positive_number subunit_to_unit, "Subunit to unit" 36 | end 37 | 38 | private def validate_iso_numeric 39 | validate_positive_number iso_numeric, "ISO numeric" 40 | end 41 | 42 | private def validate_smallest_denomination 43 | validate_positive_number smallest_denomination, "Smallest denomination" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/currency/converter_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | private record FooWithCurrency, currency : Money::Currency do 4 | include JSON::Serializable 5 | include YAML::Serializable 6 | 7 | @[JSON::Field(converter: Money::Currency::Converter)] 8 | @[YAML::Field(converter: Money::Currency::Converter)] 9 | @currency : Money::Currency 10 | end 11 | 12 | describe Money::Currency::Converter do 13 | pln = Money::Currency.find("PLN") 14 | 15 | describe ".from_json/yaml" do 16 | it "raises NotFoundError on unknown currency" do 17 | expect_raises(Money::Currency::NotFoundError) do 18 | FooWithCurrency.from_json(%({"currency": "FOO"})) 19 | end 20 | 21 | expect_raises(Money::Currency::NotFoundError) do 22 | FooWithCurrency.from_yaml("currency: FOO") 23 | end 24 | end 25 | 26 | it "deserializes" do 27 | FooWithCurrency.from_json(<<-JSON).currency.should eq pln 28 | { 29 | "currency": "PLN" 30 | } 31 | JSON 32 | 33 | FooWithCurrency.from_yaml(<<-YAML).currency.should eq pln 34 | --- 35 | currency: PLN\n 36 | YAML 37 | end 38 | 39 | describe "#to_json/yaml" do 40 | it "serializes" do 41 | FooWithCurrency.new(pln).to_pretty_json.should eq <<-JSON 42 | { 43 | "currency": "PLN" 44 | } 45 | JSON 46 | 47 | FooWithCurrency.new(pln).to_yaml.should eq <<-YAML 48 | --- 49 | currency: PLN\n 50 | YAML 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/money/json_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Money do 4 | money_json = <<-JSON 5 | { 6 | "amount": 10.0, 7 | "currency": "USD" 8 | } 9 | JSON 10 | 11 | describe ".from_json" do 12 | context "(object)" do 13 | it "returns unserialized Money object" do 14 | money = Money.from_json(money_json) 15 | money.fractional.should eq 10_00 16 | money.amount.should eq 10.0 17 | money.currency.should eq Money::Currency.find("USD") 18 | money.to_pretty_json.should eq money_json 19 | end 20 | end 21 | 22 | context "(string)" do 23 | it "returns Money object parsed from string" do 24 | Money.from_json(%q("$10.00")).should eq Money.new(10_00, "USD") 25 | end 26 | it "raises Parse::Error when invalid string is given" do 27 | expect_raises(Money::Parse::Error) { Money.from_json(%q("10 FOO")) } 28 | end 29 | end 30 | end 31 | 32 | describe ".from_json_object_key?" do 33 | it "works as intended" do 34 | hash = Hash(Money, String).from_json(%({"10 USD": "foo"})) 35 | hash.should eq({Money.new(10_00, "USD") => "foo"}) 36 | end 37 | end 38 | 39 | describe "#to_json" do 40 | it "works as intended" do 41 | Money.from_json(money_json).to_json 42 | .should eq Money.new(10_00, "USD").to_json 43 | end 44 | end 45 | 46 | describe "#to_json_object_key" do 47 | it "works as intended" do 48 | hash = {Money.new(10_00, "USD") => "foo"} 49 | hash.to_json.should eq %({"10 USD":"foo"}) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/http.cr: -------------------------------------------------------------------------------- 1 | require "uri" 2 | require "uri/params" 3 | require "http/headers" 4 | require "http/client" 5 | 6 | class Money::Currency 7 | # Module containing common HTTP methods used by currency rate providers. 8 | module RateProvider::HTTP 9 | # Returns the host URI used by the HTTP `#client` instance. 10 | abstract def host : URI 11 | 12 | # Returns a new `HTTP::Client` instance. 13 | protected def client : ::HTTP::Client 14 | ::HTTP::Client.new(host) 15 | end 16 | 17 | # Makes a HTTP request to the specified *path* and yields the response. 18 | protected def request( 19 | method : String, 20 | path : String, 21 | params : Hash | NamedTuple? = nil, 22 | headers : ::HTTP::Headers? = nil, 23 | body = nil, 24 | & : ::HTTP::Client::Response -> _ 25 | ) 26 | if params && !params.empty? 27 | path += "?#{URI::Params.encode(params)}" 28 | end 29 | client.exec(method, path, headers, body) do |response| 30 | unless response.success? 31 | raise RequestError.new(response.status) 32 | end 33 | yield response 34 | end 35 | end 36 | 37 | # Makes a `GET` request to the specified *path* and yields the response. 38 | protected def request( 39 | path : String, 40 | params : Hash | NamedTuple? = nil, 41 | headers : ::HTTP::Headers? = nil, 42 | & : ::HTTP::Client::Response -> _ 43 | ) 44 | request("GET", path, params, headers) do |response| 45 | yield response 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/currency/json_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Money::Currency do 4 | currency_json = <<-JSON 5 | { 6 | "code": "FOO", 7 | "subunit_to_unit": 100 8 | } 9 | JSON 10 | 11 | describe ".from_json" do 12 | context "(object)" do 13 | it "returns unserialized Currency object" do 14 | currency = Money::Currency.from_json(currency_json) 15 | currency.code.should eq "FOO" 16 | currency.decimal_places.should eq 2 17 | end 18 | end 19 | 20 | context "(string)" do 21 | it "returns Currency object using Currency.find when known key is given" do 22 | Money::Currency.from_json(%q("USD")).should eq Money::Currency.find("USD") 23 | end 24 | it "raises NotFoundError when unknown key is given" do 25 | expect_raises(Money::Currency::NotFoundError) do 26 | Money::Currency.from_json(%q("FOO")) 27 | end 28 | end 29 | end 30 | end 31 | 32 | describe ".from_json_object_key?" do 33 | it "works as intended" do 34 | hash = Hash(Money::Currency, String).from_json(%({"USD": "foo"})) 35 | hash.should eq({Money::Currency.find("USD") => "foo"}) 36 | end 37 | end 38 | 39 | describe "#to_json" do 40 | it "works as intended" do 41 | Money::Currency.from_json(currency_json).to_pretty_json 42 | .should eq currency_json 43 | end 44 | end 45 | 46 | describe "#to_json_object_key" do 47 | it "works as intended" do 48 | hash = {Money::Currency.find("USD") => "foo"} 49 | hash.to_json.should eq %({"USD":"foo"}) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /src/money/currency/rate.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | struct Rate 3 | include Comparable(Rate) 4 | 5 | # :nodoc: 6 | def self.key(base : Currency, target : Currency) : String 7 | "#{base.code}_#{target.code}" 8 | end 9 | 10 | # :nodoc: 11 | def self.key(base : String, target : String) : String 12 | "#{base}_#{target}" 13 | end 14 | 15 | def self.new(base : Currency, target : Currency, value, updated_at = Time.utc) : Rate 16 | new(base.code, target.code, value, updated_at) 17 | end 18 | 19 | getter base : String 20 | getter target : String 21 | getter value : BigDecimal 22 | getter updated_at : Time 23 | 24 | def initialize(@base, @target, @value, @updated_at = Time.utc) 25 | after_initialize 26 | end 27 | 28 | protected def after_initialize 29 | validate! 30 | end 31 | 32 | protected def validate! 33 | raise ArgumentError.new("Invalid rate: #{value}") unless value.positive? 34 | end 35 | 36 | def_equals_and_hash base, target, value, updated_at 37 | 38 | def <=>(other : Rate) : Int32 39 | {base, target, other.updated_at, other.value} <=> 40 | {other.base, other.target, updated_at, value} 41 | end 42 | 43 | def to_s(*, include_updated_at = false) : String 44 | String.build do |io| 45 | to_s(io, include_updated_at: include_updated_at) 46 | end 47 | end 48 | 49 | def to_s(io : IO, *, include_updated_at = false) : Nil 50 | io << base << " -> " << target << ": " << value 51 | io << " (" << updated_at << ')' if include_updated_at 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/currency_api.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [Currency API](https://currencyapi.com/) currency rate provider. 5 | class RateProvider::CurrencyAPI < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("CURRENCY_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.currencyapi.com") 15 | end 16 | 17 | def initialize(*, @api_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "apikey": api_key, 26 | } 27 | request("/v3/currencies", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | currencies = 30 | result["data"].as_h.keys 31 | 32 | currencies 33 | end 34 | end 35 | 36 | # 37 | def exchange_rate?(base : Currency, target : Currency) : Rate? 38 | Log.debug { "Fetching rate for #{base} -> #{target}" } 39 | 40 | params = { 41 | "apikey": api_key, 42 | "base_currency": base.code, 43 | "currencies": target.code, 44 | } 45 | request("/v3/latest", params) do |response| 46 | result = JSON.parse(response.body_io).as_h 47 | rate = 48 | result.dig("data", target.code, "value").to_s.to_big_d 49 | 50 | Rate.new(base, target, rate) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/uni_rate_api.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [UniRateAPI](https://unirateapi.com/) currency rate provider. 5 | class RateProvider::UniRateAPI < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("UNIRATE_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.unirateapi.com") 15 | end 16 | 17 | def initialize(*, @api_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "api_key": api_key, 26 | } 27 | request("/api/currencies", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | currencies = 30 | result["currencies"].as_a.map(&.as_s) 31 | 32 | currencies 33 | end 34 | end 35 | 36 | # 37 | def exchange_rate?(base : Currency, target : Currency) : Rate? 38 | Log.debug { "Fetching rate for #{base} -> #{target}" } 39 | 40 | params = { 41 | "api_key": api_key, 42 | "from": base.code, 43 | "to": target.code, 44 | } 45 | request("/api/rates", params) do |response| 46 | result = JSON.parse(response.body_io).as_h 47 | rate = 48 | result["rate"].to_s.to_big_d 49 | 50 | Rate.new(base, target, rate) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/freecurrency_api.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [FreecurrencyAPI](https://freecurrencyapi.com/) currency rate provider. 5 | class RateProvider::FreecurrencyAPI < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("FREECURRENCY_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.freecurrencyapi.com") 15 | end 16 | 17 | def initialize(*, @api_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "apikey": api_key, 26 | } 27 | request("/v1/currencies", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | currencies = 30 | result["data"].as_h.keys 31 | 32 | currencies 33 | end 34 | end 35 | 36 | # 37 | def exchange_rate?(base : Currency, target : Currency) : Rate? 38 | Log.debug { "Fetching rate for #{base} -> #{target}" } 39 | 40 | params = { 41 | "apikey": api_key, 42 | "base_currency": base.code, 43 | "currencies": target.code, 44 | } 45 | request("/v1/latest", params) do |response| 46 | result = JSON.parse(response.body_io).as_h 47 | rate = 48 | result.dig("data", target.code).to_s.to_big_d 49 | 50 | Rate.new(base, target, rate) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/currency_beacon.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [CurrencyBeacon](https://currencybeacon.com/) currency rate provider. 5 | class RateProvider::CurrencyBeacon < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("CURRENCY_BEACON_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.currencybeacon.com") 15 | end 16 | 17 | def initialize(*, @api_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "api_key": api_key, 26 | } 27 | request("/v1/currencies", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | currencies = 30 | result["response"].as_a.map(&.as_h["short_code"].as_s) 31 | 32 | currencies 33 | end 34 | end 35 | 36 | # 37 | def exchange_rate?(base : Currency, target : Currency) : Rate? 38 | Log.debug { "Fetching rate for #{base} -> #{target}" } 39 | 40 | params = { 41 | "api_key": api_key, 42 | "base": base.code, 43 | "symbols": target.code, 44 | } 45 | request("/v1/latest", params) do |response| 46 | result = JSON.parse(response.body_io).as_h 47 | 48 | if rate = result.dig?("response", "rates", target.code) 49 | Rate.new(base, target, rate.to_s.to_big_d) 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/abstract_api.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [Abstract API](https://www.abstractapi.com/api/exchange-rate-api) currency rate provider. 5 | class RateProvider::AbstractAPI < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("ABSTRACT_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://exchange-rates.abstractapi.com") 15 | end 16 | 17 | def initialize(*, @api_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "api_key": api_key, 26 | "base": "USD", 27 | } 28 | request("/v1/live/", params) do |response| 29 | result = JSON.parse(response.body_io).as_h 30 | currencies = 31 | result["exchange_rates"].as_h.keys 32 | 33 | currencies 34 | end 35 | end 36 | 37 | # 38 | def exchange_rate?(base : Currency, target : Currency) : Rate? 39 | Log.debug { "Fetching rate for #{base} -> #{target}" } 40 | 41 | params = { 42 | "api_key": api_key, 43 | "base": base.code, 44 | "target": target.code, 45 | } 46 | request("/v1/live/", params) do |response| 47 | result = JSON.parse(response.body_io).as_h 48 | rate = 49 | result.dig("exchange_rates", target.code).to_s.to_big_d 50 | 51 | Rate.new(base, target, rate) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/money/exchange_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Money::Exchange do 4 | exchange = Money::Currency::Exchange.new(Money::Currency::RateStore::Memory.new) 5 | exchange.rate_store["EUR", "USD"] = 1.23 6 | exchange.rate_store["USD", "EUR"] = 3.21 7 | 8 | describe "#exchange_to" do 9 | it "exchanges the amount properly with given exchange" do 10 | Money.new(100_00, "EUR").exchange_to("USD", exchange).should eq Money.new(123_00, "USD") 11 | Money.new(100_00, "USD").exchange_to("EUR", exchange).should eq Money.new(321_00, "EUR") 12 | end 13 | 14 | it "exchanges the amount properly" do 15 | Money.with_default_exchange(exchange) do 16 | Money.new(100_00, "EUR").exchange_to("USD").should eq Money.new(123_00, "USD") 17 | Money.new(100_00, "USD").exchange_to("EUR").should eq Money.new(321_00, "EUR") 18 | end 19 | end 20 | 21 | it "returns self when the currencies are the same" do 22 | Money.with_default_exchange(exchange) do 23 | money = Money.new(1, "USD") 24 | money.exchange_to("USD").should eq money 25 | end 26 | end 27 | 28 | it "does not exchange when the amount is zero" do 29 | Money.with_default_exchange(Money::Currency::Exchange::SingleCurrency.new) do 30 | Money.zero("USD").exchange_to("EUR") 31 | .should eq Money.zero("EUR") 32 | end 33 | end 34 | end 35 | 36 | describe "#with_same_currency" do 37 | it "yields the other object exchanged to self.currency" do 38 | Money.with_default_exchange(exchange) do 39 | Money.zero("USD").with_same_currency(Money.new(100_00, "EUR")) do |other| 40 | other.should eq Money.new(123_00, "USD") 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/fx_rates_api.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [FXRatesAPI](https://fxratesapi.com/) currency rate provider. 5 | class RateProvider::FXRatesAPI < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("FXRATES_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.fxratesapi.com") 15 | end 16 | 17 | def initialize(*, @api_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "api_key": api_key, 26 | } 27 | request("/currencies", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | currencies = 30 | result.keys 31 | 32 | currencies 33 | end 34 | end 35 | 36 | # 37 | def exchange_rate?(base : Currency, target : Currency) : Rate? 38 | Log.debug { "Fetching rate for #{base} -> #{target}" } 39 | 40 | params = { 41 | "api_key": api_key, 42 | "base": base.code, 43 | "currencies": target.code, 44 | } 45 | request("/latest", params) do |response| 46 | result = JSON.parse(response.body_io).as_h 47 | 48 | unless result["success"].as_bool 49 | raise ResponseError.new( 50 | result["error"], 51 | result["description"]?) 52 | end 53 | 54 | if rate = result.dig?("rates", target.code) 55 | Rate.new(base, target, rate.to_s.to_big_d) 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/fixer.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [Fixer](https://fixer.io/) currency rate provider. 5 | class RateProvider::Fixer < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter access_key : String do 11 | option_from_env("FIXER_ACCESS_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://data.fixer.io") 15 | end 16 | 17 | def initialize(*, @access_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "access_key": access_key, 26 | } 27 | request("/api/symbols", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | 30 | unless result["success"].as_bool 31 | raise ResponseError.new(result.dig("error", "type")) 32 | end 33 | 34 | currencies = 35 | result["symbols"].as_h.keys 36 | 37 | currencies 38 | end 39 | end 40 | 41 | # 42 | def exchange_rate?(base : Currency, target : Currency) : Rate? 43 | Log.debug { "Fetching rate for #{base} -> #{target}" } 44 | 45 | params = { 46 | "access_key": access_key, 47 | "base": base.code, 48 | "symbols": target.code, 49 | } 50 | request("/api/latest", params) do |response| 51 | result = JSON.parse(response.body_io).as_h 52 | 53 | unless result["success"].as_bool 54 | raise ResponseError.new(result.dig("error", "type")) 55 | end 56 | 57 | rate = 58 | result.dig("rates", target.code).to_s.to_big_d 59 | 60 | Rate.new(base, target, rate) 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/open_exchange_rates.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [Open Exchange Rates](https://openexchangerates.org/) currency rate provider. 5 | class RateProvider::OpenExchangeRates < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter app_id : String do 11 | option_from_env("OPEN_EXCHANGE_RATES_APP_ID") 12 | end 13 | getter host : URI do 14 | URI.parse("https://openexchangerates.org") 15 | end 16 | getter? show_alternative = true 17 | 18 | def initialize( 19 | *, 20 | @app_id = nil, 21 | @host = nil, 22 | @show_alternative = true, 23 | ) 24 | end 25 | 26 | # 27 | getter base_currency_codes : Array(String) do 28 | Log.debug { "Fetching supported currencies" } 29 | 30 | params = { 31 | "app_id": app_id, 32 | "show_alternative": show_alternative?.to_s, 33 | } 34 | request("/api/currencies.json", params) do |response| 35 | result = JSON.parse(response.body_io).as_h 36 | currencies = 37 | result.keys 38 | 39 | currencies 40 | end 41 | end 42 | 43 | # 44 | def exchange_rate?(base : Currency, target : Currency) : Rate? 45 | Log.debug { "Fetching rate for #{base} -> #{target}" } 46 | 47 | params = { 48 | "app_id": app_id, 49 | "base": base.code, 50 | "symbols": target.code, 51 | "show_alternative": show_alternative?.to_s, 52 | } 53 | request("/api/latest.json", params) do |response| 54 | result = JSON.parse(response.body_io).as_h 55 | rate = 56 | result.dig("rates", target.code).to_s.to_big_d 57 | 58 | Rate.new(base, target, rate) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/money/currency/enumeration.cr: -------------------------------------------------------------------------------- 1 | class Money::Currency 2 | module Enumeration 3 | include Enumerable(Currency) 4 | 5 | # Looks up a currency with the given *key* and returns a `Currency` instance 6 | # on success, `nil` otherwise. 7 | # 8 | # ``` 9 | # Money::Currency.find?("EUR") # => # 10 | # Money::Currency.find?("FOO") # => nil 11 | # ``` 12 | def find?(key : String | Symbol) : Currency? 13 | @@registry_mutex.synchronize do 14 | registry[key.to_s.upcase]? 15 | end 16 | end 17 | 18 | # Alias of `#find?`. 19 | @[AlwaysInline] 20 | def []?(key : String | Symbol) : Currency? 21 | find?(key) 22 | end 23 | 24 | # Returns given `Currency` instance. 25 | @[AlwaysInline] 26 | def []?(key : Currency) : Currency? 27 | key 28 | end 29 | 30 | # Looks up a currency with the given *key* and returns a `Currency` instance 31 | # on success, raises `NotFoundError` otherwise. 32 | # 33 | # ``` 34 | # Money::Currency.find("EUR") # => # 35 | # Money::Currency.find("FOO") # raises Money::Currency::NotFoundError 36 | # ``` 37 | def find(key : String | Symbol) : Currency 38 | find?(key) || 39 | raise Currency::NotFoundError.new(key.to_s) 40 | end 41 | 42 | # Alias of `#find`. 43 | @[AlwaysInline] 44 | def [](key : String | Symbol) : Currency 45 | find(key) 46 | end 47 | 48 | # Returns given `Currency` instance. 49 | @[AlwaysInline] 50 | def [](key : Currency) : Currency 51 | key 52 | end 53 | 54 | # Returns a sorted list of all registered currencies. 55 | def all : Array(Currency) 56 | @@registry_mutex.synchronize do 57 | registry.values.sort! 58 | end 59 | end 60 | 61 | # Iterates over all registered currencies. 62 | def each(& : Currency ->) : Nil 63 | all.each do |currency| 64 | yield currency 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/money/error.cr: -------------------------------------------------------------------------------- 1 | struct Money 2 | # Base exception class. 3 | class Error < Exception 4 | end 5 | 6 | # Exception class for aggregating individual errors. 7 | class AggregateError < Error 8 | getter errors : Array(Exception) 9 | 10 | def initialize(message : String, @errors = [] of Exception) 11 | super(message) 12 | end 13 | 14 | def initialize(@errors, message : String = "Aggregate Error") 15 | super(message) 16 | end 17 | 18 | def to_s(io : IO) : Nil 19 | super 20 | return if errors.empty? 21 | 22 | io << ": " 23 | errors.each_with_index do |error, index| 24 | io << ", " unless index.zero? 25 | error.to_s(io) 26 | end 27 | end 28 | 29 | def inspect_with_backtrace(io : IO) : Nil 30 | super 31 | return if errors.empty? 32 | 33 | io << "\n" 34 | errors.each_with_index do |error, index| 35 | io << "\n" unless index.zero? 36 | error.inspect_with_backtrace(io) 37 | end 38 | end 39 | end 40 | 41 | # Raised when `Money.default_currency` is not set. 42 | class UndefinedCurrencyError < Error 43 | def initialize 44 | super("No default currency set") 45 | end 46 | end 47 | 48 | # Raised when trying to find an unknown exchange rate. 49 | class UnknownRateError < Error 50 | def initialize(base, target) 51 | super("No conversion rate known for #{base} -> #{target}") 52 | end 53 | end 54 | 55 | # Raised by `Currency::Exchange::SingleCurrency` when trying to exchange currencies. 56 | class DifferentCurrencyError < Error 57 | def initialize(base : Currency, target : Currency) 58 | super("No exchanging of currencies allowed for #{base} -> #{target}") 59 | end 60 | end 61 | 62 | # Raised when smallest denomination of a currency is not defined. 63 | class UndefinedSmallestDenominationError < Error 64 | def initialize(currency : Currency) 65 | super("Smallest denomination of #{currency} currency is not defined") 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/fx_feed.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [FXFeed](https://fxfeed.io/) currency rate provider. 5 | class RateProvider::FXFeed < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("FXFEED_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.fxfeed.io") 15 | end 16 | 17 | def initialize(*, @api_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "api_key": api_key, 26 | } 27 | request("/v1/latest", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | 30 | unless result["success"]?.try(&.as_bool) 31 | raise ResponseError.new( 32 | result.dig("error", "code"), 33 | result.dig?("error", "message")) 34 | end 35 | 36 | currencies = 37 | result["rates"].as_h.keys 38 | 39 | currencies 40 | end 41 | end 42 | 43 | # 44 | def exchange_rate?(base : Currency, target : Currency) : Rate? 45 | Log.debug { "Fetching rate for #{base} -> #{target}" } 46 | 47 | params = { 48 | "api_key": api_key, 49 | "base": base.code, 50 | "currencies": target.code, 51 | } 52 | request("/v1/latest", params) do |response| 53 | result = JSON.parse(response.body_io).as_h 54 | 55 | unless result["success"]?.try(&.as_bool) 56 | raise ResponseError.new( 57 | result.dig("error", "code"), 58 | result.dig?("error", "message")) 59 | end 60 | 61 | rate = 62 | result.dig("rates", target.code).to_s.to_big_d 63 | 64 | Rate.new(base, target, rate) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/coinbase.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [Coinbase](https://www.coinbase.com/) currency rate provider. 5 | class RateProvider::Coinbase < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter host : URI do 11 | URI.parse("https://api.coinbase.com") 12 | end 13 | 14 | def initialize(*, @host = nil) 15 | end 16 | 17 | getter base_currency_codes : Array(String) do 18 | fiat_currency_codes + crypto_currency_codes 19 | end 20 | 21 | # 22 | protected def fiat_currency_codes : Array(String) 23 | fetch_currency_codes "fiat", "/v2/currencies", "id" 24 | end 25 | 26 | # 27 | protected def crypto_currency_codes : Array(String) 28 | fetch_currency_codes "crypto", "/v2/currencies/crypto", "code" 29 | end 30 | 31 | private def fetch_currency_codes(type : String, path : String, prop : String) : Array(String) 32 | Log.debug { "Fetching supported #{type} currencies" } 33 | 34 | request(path) do |response| 35 | result = JSON.parse(response.body_io).as_h 36 | currencies = 37 | result["data"].as_a.map(&.as_h[prop].as_s) 38 | 39 | currencies 40 | end 41 | end 42 | 43 | # 44 | def exchange_rate?(base : Currency, target : Currency) : Rate? 45 | Log.debug { "Fetching rate for #{base} -> #{target}" } 46 | 47 | params = { 48 | "currency": base.code, 49 | } 50 | request("/v2/exchange-rates", params) do |response| 51 | result = JSON.parse(response.body_io).as_h 52 | rate = 53 | result.dig("data", "rates", target.code).to_s.to_big_d 54 | 55 | Rate.new(base, target, rate) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/exchange_rate_api.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [Exchange Rate API](https://www.exchangerate-api.com/) currency rate provider. 5 | class RateProvider::ExchangeRateAPI < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("EXCHANGE_RATE_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://v6.exchangerate-api.com") 15 | end 16 | 17 | def initialize(*, @api_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | path = 25 | "/v6/%s/codes" % URI.encode_path_segment(api_key) 26 | 27 | request(path) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | 30 | unless result["result"].as_s == "success" 31 | raise ResponseError.new(result["error-type"]) 32 | end 33 | 34 | currencies = 35 | result["supported_codes"].as_a.map(&.as_a.first.as_s) 36 | 37 | currencies 38 | end 39 | end 40 | 41 | # 42 | def exchange_rate?(base : Currency, target : Currency) : Rate? 43 | Log.debug { "Fetching rate for #{base} -> #{target}" } 44 | 45 | path = 46 | "/v6/%s/pair/%s/%s" % { 47 | URI.encode_path_segment(api_key), 48 | URI.encode_path_segment(base.code), 49 | URI.encode_path_segment(target.code), 50 | } 51 | 52 | request(path) do |response| 53 | result = JSON.parse(response.body_io).as_h 54 | 55 | unless result["result"].as_s == "success" 56 | raise ResponseError.new(result["error-type"]) 57 | end 58 | 59 | rate = 60 | result["conversion_rate"].to_s.to_big_d 61 | 62 | Rate.new(base, target, rate) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/metalprice_api.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [MetalpriceAPI](https://metalpriceapi.com/) currency rate provider. 5 | class RateProvider::MetalpriceAPI < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("METALPRICE_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.metalpriceapi.com") 15 | end 16 | 17 | def initialize(*, @api_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "api_key": api_key, 26 | } 27 | request("/v1/symbols", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | 30 | unless result["success"].as_bool 31 | raise ResponseError.new( 32 | result.dig("error", "statusCode"), 33 | result.dig?("error", "message")) 34 | end 35 | 36 | currencies = 37 | result["symbols"].as_h.keys 38 | 39 | currencies 40 | end 41 | end 42 | 43 | # 44 | def exchange_rate?(base : Currency, target : Currency) : Rate? 45 | Log.debug { "Fetching rate for #{base} -> #{target}" } 46 | 47 | params = { 48 | "api_key": api_key, 49 | "base": base.code, 50 | "currencies": target.code, 51 | } 52 | request("/v1/latest", params) do |response| 53 | result = JSON.parse(response.body_io).as_h 54 | 55 | unless result["success"].as_bool 56 | raise ResponseError.new( 57 | result.dig("error", "statusCode"), 58 | result.dig?("error", "message")) 59 | end 60 | 61 | rate = 62 | result.dig("rates", target.code).to_s.to_big_d 63 | 64 | Rate.new(base, target, rate) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/compound.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # Currency rate provider composed of multiple other rate providers. 5 | class RateProvider::Compound < RateProvider 6 | Log = ::Log.for(self) 7 | 8 | if_defined?(:JSON) do 9 | @[JSON::Field(converter: JSON::ArrayConverter(Money::Currency::RateProvider::Converter))] 10 | end 11 | if_defined?(:YAML) do 12 | @[YAML::Field(converter: YAML::ArrayConverter(Money::Currency::RateProvider::Converter))] 13 | end 14 | getter providers : Array(RateProvider) 15 | 16 | def initialize(@providers = [] of RateProvider) 17 | end 18 | 19 | def base_currency_codes : Array(String) 20 | currency_codes "Fetching base currencies failed", &.base_currency_codes 21 | end 22 | 23 | def target_currency_codes : Array(String) 24 | currency_codes "Fetching target currencies failed", &.target_currency_codes 25 | end 26 | 27 | private def currency_codes(failure_msg : String, &) : Array(String) 28 | providers 29 | .each_with_object([] of String) do |provider, currency_codes| 30 | if codes = yield provider 31 | currency_codes.concat(codes) 32 | end 33 | rescue ex 34 | Log.debug(exception: ex) do 35 | "#{failure_msg} (#{provider.class})" 36 | end 37 | end 38 | .uniq! 39 | end 40 | 41 | def exchange_rate?(base : Currency, target : Currency) : Rate? 42 | errors = [] of Exception 43 | 44 | providers.each do |provider| 45 | next unless provider.supports_currency_pair?(base, target) 46 | 47 | if rate = provider.exchange_rate?(base, target) 48 | return rate 49 | end 50 | rescue ex 51 | Log.debug(exception: ex) do 52 | "Fetching rate for #{base} -> #{target} failed (#{provider.class})" 53 | end 54 | errors << ex 55 | end 56 | 57 | if errors.present? 58 | raise AggregateError.new("Failed to fetch rate for #{base} -> #{target}", errors) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/exchangerate.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [Exchangerate](https://exchangerate.host/) currency rate provider. 5 | class RateProvider::Exchangerate < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter access_key : String do 11 | option_from_env("EXCHANGERATE_ACCESS_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.exchangerate.host") 15 | end 16 | 17 | def initialize(*, @access_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "access_key": access_key, 26 | } 27 | request("/list", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | 30 | unless result["success"].as_bool 31 | raise ResponseError.new( 32 | result.dig("error", "code"), 33 | result.dig?("error", "info")) 34 | end 35 | 36 | currencies = 37 | result["currencies"].as_h.keys 38 | 39 | currencies 40 | end 41 | end 42 | 43 | # 44 | def exchange_rate?(base : Currency, target : Currency) : Rate? 45 | Log.debug { "Fetching rate for #{base} -> #{target}" } 46 | 47 | params = { 48 | "access_key": access_key, 49 | "source": base.code, 50 | "currencies": target.code, 51 | } 52 | request("/live", params) do |response| 53 | result = JSON.parse(response.body_io).as_h 54 | 55 | unless result["success"].as_bool 56 | raise ResponseError.new( 57 | result.dig("error", "code"), 58 | result.dig?("error", "info")) 59 | end 60 | 61 | rate = 62 | result.dig("quotes", "%s%s" % {base.code, target.code}).to_s.to_big_d 63 | 64 | Rate.new(base, target, rate) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/currency_layer.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [CurrencyLayer](https://currencylayer.com/) currency rate provider. 5 | class RateProvider::CurrencyLayer < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter access_key : String do 11 | option_from_env("CURRENCY_LAYER_ACCESS_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.currencylayer.com") 15 | end 16 | 17 | def initialize(*, @access_key = nil, @host = nil) 18 | end 19 | 20 | # 21 | getter base_currency_codes : Array(String) do 22 | Log.debug { "Fetching supported currencies" } 23 | 24 | params = { 25 | "access_key": access_key, 26 | } 27 | request("/list", params) do |response| 28 | result = JSON.parse(response.body_io).as_h 29 | 30 | unless result["success"].as_bool 31 | raise ResponseError.new( 32 | result.dig("error", "code"), 33 | result.dig?("error", "info")) 34 | end 35 | 36 | currencies = 37 | result["currencies"].as_h.keys 38 | 39 | currencies 40 | end 41 | end 42 | 43 | # 44 | def exchange_rate?(base : Currency, target : Currency) : Rate? 45 | Log.debug { "Fetching rate for #{base} -> #{target}" } 46 | 47 | params = { 48 | "access_key": access_key, 49 | "source": base.code, 50 | "currencies": target.code, 51 | } 52 | request("/live", params) do |response| 53 | result = JSON.parse(response.body_io).as_h 54 | 55 | unless result["success"].as_bool 56 | raise ResponseError.new( 57 | result.dig("error", "code"), 58 | result.dig?("error", "info")) 59 | end 60 | 61 | rate = 62 | result.dig("quotes", "%s%s" % {base.code, target.code}).to_s.to_big_d 63 | 64 | Rate.new(base, target, rate) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/coinlayer.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [Coinlayer](https://coinlayer.com/) currency rate provider. 5 | class RateProvider::Coinlayer < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter access_key : String do 11 | option_from_env("COINLAYER_ACCESS_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://api.coinlayer.com") 15 | end 16 | 17 | def initialize(*, @access_key = nil, @host = nil) 18 | end 19 | 20 | def base_currency_codes : Array(String) 21 | currency_codes[0] 22 | end 23 | 24 | def target_currency_codes : Array(String) 25 | currency_codes[0] + currency_codes[1] 26 | end 27 | 28 | # 29 | protected getter currency_codes : {Array(String), Array(String)} do 30 | Log.debug { "Fetching supported currencies" } 31 | 32 | params = { 33 | "access_key": access_key, 34 | } 35 | request("/list", params) do |response| 36 | result = JSON.parse(response.body_io).as_h 37 | 38 | unless result["success"].as_bool 39 | raise ResponseError.new( 40 | result.dig("error", "code"), 41 | result.dig?("error", "info")) 42 | end 43 | 44 | { 45 | result["crypto"].as_h.keys, 46 | result["fiat"].as_h.keys, 47 | } 48 | end 49 | end 50 | 51 | # 52 | def exchange_rate?(base : Currency, target : Currency) : Rate? 53 | Log.debug { "Fetching rate for #{base} -> #{target}" } 54 | 55 | params = { 56 | "access_key": access_key, 57 | "target": target.code, 58 | "symbols": base.code, 59 | } 60 | request("/live", params) do |response| 61 | result = JSON.parse(response.body_io).as_h 62 | 63 | unless result["success"].as_bool 64 | raise ResponseError.new( 65 | result.dig("error", "code"), 66 | result.dig?("error", "info")) 67 | end 68 | 69 | rate = 70 | result.dig("rates", base.code).to_s.to_big_d 71 | 72 | Rate.new(base, target, rate) 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /src/money/money/constructors.cr: -------------------------------------------------------------------------------- 1 | struct Money 2 | module Constructors 3 | # Creates a new `Money` object of value given in the *unit* of the given 4 | # *currency*. 5 | # 6 | # ``` 7 | # Money.from_amount(23.45, "USD") # => Money(@amount=23.45 @currency="USD") 8 | # Money.from_amount(23.45, "JPY") # => Money(@amount=23.0 @currency="JPY") 9 | # ``` 10 | # 11 | # See also `#initialize`. 12 | def from_amount(amount : Number | String, currency = Money.default_currency) : Money 13 | new( 14 | amount: amount.to_big_d, 15 | currency: currency, 16 | ) 17 | end 18 | 19 | # Creates a new `Money` object of value given in the fractional *unit* of 20 | # the given *currency*. 21 | # 22 | # ``` 23 | # Money.from_fractional(23_45.67, "USD") # => Money(@amount=23.4567 @currency="USD") 24 | # Money.from_fractional(23_45, "USD") # => Money(@amount=23.45 @currency="USD") 25 | # ``` 26 | # 27 | # See also `#initialize`. 28 | def from_fractional(fractional : Number | String, currency = Money.default_currency) : Money 29 | new( 30 | fractional: fractional.to_big_d, 31 | currency: currency, 32 | ) 33 | end 34 | 35 | # Creates a new `Money` object with value `0`. 36 | # 37 | # ``` 38 | # Money.zero # => Money(@amount=0.0) 39 | # Money.zero(:pln) # => Money(@amount=0.0 @currency="PLN") 40 | # ``` 41 | def zero(currency = Money.default_currency) : Money 42 | new(0, currency) 43 | end 44 | 45 | # Creates a new `Money` object of the given value, using the 46 | # American dollar currency. 47 | # 48 | # ``` 49 | # Money.us_dollar(1_00) # => Money(@amount=1.0 @currency="USD") 50 | # ``` 51 | def us_dollar(value) : Money 52 | new(value, "USD") 53 | end 54 | 55 | # Creates a new `Money` object of the given value, using the 56 | # Euro currency. 57 | # 58 | # ``` 59 | # Money.euro(1_00) # => Money(@amount=1.0 @currency="EUR") 60 | # ``` 61 | def euro(value) : Money 62 | new(value, "EUR") 63 | end 64 | 65 | # Creates a new `Money` object of the given value, using the 66 | # Bitcoin cryptocurrency. 67 | # 68 | # ``` 69 | # Money.bitcoin(100) # => Money(@amount=0.000001 @currency="BTC") 70 | # ``` 71 | def bitcoin(value) : Money 72 | new(value, "BTC") 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /src/money/context.cr: -------------------------------------------------------------------------------- 1 | struct Money 2 | # `Context` class holding global settings for `Money` objects. 3 | # Each `Fiber` has its own `Fiber#money_context` instance. 4 | # 5 | # See also `Money.spawn_with_same_context`. 6 | class Context 7 | # :nodoc: 8 | module Delegators 9 | # Alias of `Fiber.current.money_context`. 10 | @[AlwaysInline] 11 | def context : Context 12 | Fiber.current.money_context 13 | end 14 | 15 | # Alias of `Fiber.current.money_context=`. 16 | @[AlwaysInline] 17 | def context=(context : Context) 18 | Fiber.current.money_context = context 19 | end 20 | 21 | delegate \ 22 | :infinite_precision?, :infinite_precision=, 23 | :rounding_mode, :rounding_mode=, 24 | :default_currency, :default_currency=, 25 | :default_exchange, :default_exchange=, 26 | :default_rate_store, :default_rate_store=, 27 | :default_rate_provider, :default_rate_provider=, 28 | to: context 29 | end 30 | 31 | # Use this to control infinite precision cents. 32 | property? infinite_precision : Bool = false 33 | 34 | # Default rounding mode. 35 | property rounding_mode : Number::RoundingMode = :ties_away 36 | 37 | # Default currency for creating new `Money` object. 38 | # Raises `UndefinedCurrencyError` if not set. 39 | property default_currency : Currency { raise UndefinedCurrencyError.new } 40 | 41 | # :ditto: 42 | def default_currency=(currency_code : String | Symbol) 43 | self.default_currency = Currency.find(currency_code) 44 | end 45 | 46 | # :ditto: 47 | def default_currency=(@default_currency : Nil) 48 | end 49 | 50 | # Each `Money` object is associated to a currency exchange object. 51 | # This property allows you to specify the default exchange object. 52 | # The default value for this property is an instance of 53 | # `Currency::Exchange`, which allows one to specify custom exchange rates. 54 | property default_exchange : Currency::Exchange do 55 | Currency::Exchange.new 56 | end 57 | 58 | # Default currency rate store used by `Currency::Exchange` objects. 59 | # It defaults to using an in-memory, concurrency-safe, store instance for 60 | # storing exchange rates. 61 | property default_rate_store : Currency::RateStore do 62 | Currency::RateStore::Memory.new 63 | end 64 | 65 | # Default currency rate provider used by `Currency::Exchange` objects. 66 | property default_rate_provider : Currency::RateProvider do 67 | Currency::RateProvider::Null.new 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /src/money/currency/rate_store/file.cr: -------------------------------------------------------------------------------- 1 | require "./memory" 2 | 3 | class Money::Currency 4 | # Class for storage of exchange rate pairs in a JSON file. 5 | # 6 | # NOTE: Exchange rates are stored in memory and are automatically loaded from 7 | # (upon initialization), and persisted to a JSON file (on every mutable 8 | # transaction commit). 9 | # 10 | # ``` 11 | # store = Money::Currency::RateStore::File.new("path/to/file.json") 12 | # store["USD", "CAD"] = 0.98 13 | # store["USD", "CAD"] # => 0.98 14 | # 15 | # # Iterates rates 16 | # store.each do |rate| 17 | # puts rate 18 | # end 19 | # 20 | # # Save rates 21 | # store.save 22 | # ``` 23 | class RateStore::File < RateStore::Memory 24 | getter path : Path 25 | 26 | def path=(path : Path) 27 | path = path.expand(home: true) 28 | return if @path == path 29 | 30 | @path = path 31 | load 32 | end 33 | 34 | def initialize(path : Path | String, *, ttl : Time::Span? = nil) 35 | @path = Path[path] 36 | super(ttl: ttl) 37 | end 38 | 39 | protected def after_initialize 40 | super 41 | @path = path.expand(home: true) 42 | load 43 | end 44 | 45 | # Wraps block execution in a concurrency-safe transaction. 46 | def transaction(*, mutable : Bool = false, & : -> _) 47 | super do 48 | yield.tap do 49 | save if mutable 50 | end 51 | end 52 | end 53 | 54 | # Loads rates from a JSON file. 55 | def load : Nil 56 | # Intentionally omits `mutable` argument so that the file 57 | # is not saved immediately after (re)loading 58 | transaction do 59 | return unless ::File.exists?(path) 60 | 61 | ::File.open(path) do |file| 62 | rates = 63 | Array(Rate).from_json(file) 64 | 65 | clear_rates 66 | set_rates(rates) 67 | end 68 | end 69 | end 70 | 71 | # Saves rates to a JSON file. 72 | def save : Nil 73 | # Intentionally omits `mutable` argument so that it won't 74 | # trigger an infinite loop from within `transaction` 75 | transaction do 76 | # Create directory if it doesn't exist 77 | ::Dir.mkdir_p(path.dirname) 78 | 79 | {% if flag?(:windows) %} 80 | # Save rates to a JSON file 81 | ::File.open(path, "w") do |file| 82 | rates.to_pretty_json(file) 83 | end 84 | {% else %} 85 | # Save rates to a JSON file 86 | ::File.atomic_write(path) do |file| 87 | rates.to_pretty_json(file) 88 | end 89 | {% end %} 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /src/money/money/allocate.cr: -------------------------------------------------------------------------------- 1 | struct Money 2 | module Allocate 3 | # Splits a given amount in parts. The allocation is based on the parts' proportions. 4 | # The results should always add up to the original amount. 5 | protected def self.generate(amount, parts, *, whole_amounts = true) : Array(BigDecimal) 6 | raise ArgumentError.new("Need at least one part") if parts.empty? 7 | 8 | parts = 9 | if parts.all?(&.zero?) 10 | Array.new(parts.size, 1.to_big_d) 11 | else 12 | Array.new(parts.size) { |idx| parts[idx].to_big_d } 13 | end 14 | 15 | result = [] of BigDecimal 16 | remaining_amount = amount 17 | parts_sum = parts.sum 18 | 19 | until parts.empty? 20 | part = parts.pop 21 | 22 | if parts_sum.positive? 23 | current_split = remaining_amount * part / parts_sum 24 | current_split = current_split.round(:to_zero) if whole_amounts 25 | else 26 | current_split = 0.to_big_d 27 | end 28 | 29 | result.unshift current_split 30 | remaining_amount -= current_split 31 | parts_sum -= part 32 | end 33 | 34 | result 35 | end 36 | 37 | # Splits a given amount in parts without losing pennies. 38 | # 39 | # The left-over pennies will be distributed round-robin amongst the parties. 40 | # This means that parts listed first will likely receive more pennies than 41 | # ones listed later. 42 | # 43 | # Pass `{2, 1, 1}` as input to give twice as much to _part1_ as _part2_ or _part3_ 44 | # which results in **50%** of the cash assigned to _part1_, **25%** to _part2_, 45 | # and **25%** to _part3_. 46 | # 47 | # The results should always add up to the original amount. 48 | # 49 | # ``` 50 | # Money.new(5, "USD").allocate(3, 7).map(&.cents) # => [2, 3] 51 | # Money.new(100, "USD").allocate(1, 1, 1).map(&.cents) # => [34, 33, 33] 52 | # ``` 53 | def allocate(parts : Enumerable(Number)) : Array(Money) 54 | Money::Allocate 55 | .generate(fractional, parts, whole_amounts: !Money.infinite_precision?) 56 | .map { |amount| copy_with(fractional: amount) } 57 | end 58 | 59 | # :ditto: 60 | def allocate(*parts : Number) : Array(Money) 61 | allocate(parts) 62 | end 63 | 64 | # Splits money evenly amongst parties without losing pennies. 65 | # 66 | # ``` 67 | # Money.new(100, "USD").split(2).map(&.cents) # => [50, 50] 68 | # Money.new(100, "USD").split(3).map(&.cents) # => [34, 33, 33] 69 | # ``` 70 | def split(parts : Int) : Array(Money) 71 | unless parts.positive? 72 | raise ArgumentError.new("Need at least one part") 73 | end 74 | allocate(Array.new(parts, 1)) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/core_ext/to_money_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe String do 4 | describe "#to_money?" do 5 | it "returns a Money object" do 6 | "10.00 USD".to_money?.should eq Money.from_amount(10, "USD") 7 | end 8 | 9 | context "when :allow_ambiguous is true (default)" do 10 | it "returns a first matching currency for ambiguous values" do 11 | "$10.00".to_money?.should eq Money.from_amount(10, "USD") 12 | end 13 | end 14 | 15 | context "when :allow_ambiguous is false" do 16 | it "returns nil for ambiguous values" do 17 | "$10.00".to_money?(allow_ambiguous: false).should be_nil 18 | end 19 | end 20 | 21 | it "returns `nil` for invalid values" do 22 | "foo".to_money?.should be_nil 23 | end 24 | end 25 | 26 | describe "#to_money" do 27 | it "returns a Money object" do 28 | "10.00 USD".to_money.should eq Money.from_amount(10, "USD") 29 | end 30 | 31 | context "when :allow_ambiguous is true (default)" do 32 | it "returns a first matching currency for ambiguous values" do 33 | "$10.00".to_money.should eq Money.from_amount(10, "USD") 34 | end 35 | end 36 | 37 | context "when :allow_ambiguous is false" do 38 | it "raises `Parse::Error` for ambiguous values" do 39 | expect_raises(Money::Parse::Error, %(Cannot parse "$10.00")) do 40 | "$10.00".to_money(allow_ambiguous: false) 41 | end 42 | end 43 | end 44 | 45 | it "raises `Parse::Error` for invalid values" do 46 | expect_raises(Money::Parse::Error, %(Cannot parse "foo")) do 47 | "foo".to_money 48 | end 49 | end 50 | end 51 | end 52 | 53 | describe Number do 54 | describe "#to_money?" do 55 | it "returns a Money object with default currency" do 56 | 111.to_money?.should eq Money.from_amount(111) 57 | 111.5.to_money?.should eq Money.from_amount(111.5) 58 | 111.25.to_big_d.to_money?.should eq Money.from_amount(111.25.to_big_d) 59 | end 60 | 61 | it "returns a Money object with given currency" do 62 | 111.to_money?("PLN").should eq Money.from_amount(111, "PLN") 63 | end 64 | 65 | it "returns `nil` for invalid values" do 66 | Float64::INFINITY.to_money?.should be_nil 67 | Float64::NAN.to_money?.should be_nil 68 | end 69 | end 70 | 71 | describe "#to_money" do 72 | it "returns a Money object with default currency" do 73 | 111.to_money.should eq Money.from_amount(111) 74 | 111.5.to_money.should eq Money.from_amount(111.50) 75 | 111.25.to_big_d.to_money.should eq Money.from_amount(111.25.to_big_d) 76 | end 77 | 78 | it "returns a Money object with given currency" do 79 | 111.to_money("PLN").should eq Money.from_amount(111, "PLN") 80 | end 81 | 82 | it "raises ArgumentError for invalid values" do 83 | expect_raises(ArgumentError) { Float64::INFINITY.to_money } 84 | expect_raises(ArgumentError) { Float64::NAN.to_money } 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/context_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Money do 4 | it "extends `Context::Delegators`" do 5 | Money.should be_a Money::Context::Delegators 6 | end 7 | 8 | describe ".context" do 9 | it "returns a `Context` object" do 10 | Money.context.should be_a Money::Context 11 | end 12 | 13 | it "is an alias of `Fiber.current.money_context`" do 14 | Money.context.should be Fiber.current.money_context 15 | end 16 | end 17 | 18 | describe ".context=" do 19 | it "allows setting the context" do 20 | context = Money::Context.new 21 | 22 | Money.context = context 23 | Money.context.should be context 24 | end 25 | end 26 | end 27 | 28 | describe Money::Context do 29 | subject = Money::Context.new 30 | 31 | describe "#infinite_precision?" do 32 | it "is set to `false` by default" do 33 | subject.infinite_precision?.should be_false 34 | end 35 | end 36 | 37 | describe "#rounding_mode" do 38 | it "is set to `:ties_away` by default" do 39 | subject.rounding_mode.should eq Number::RoundingMode::TIES_AWAY 40 | end 41 | end 42 | 43 | describe "#default_currency" do 44 | it "raises UndefinedCurrencyError by default" do 45 | expect_raises(Money::UndefinedCurrencyError) { subject.default_currency } 46 | end 47 | 48 | context "allows setting the default currency" do 49 | context = Money::Context.new 50 | 51 | it "by Currency" do 52 | context.default_currency = Money::Currency.find("PLN") 53 | context.default_currency.should be_a Money::Currency 54 | context.default_currency.should eq Money::Currency.find("PLN") 55 | end 56 | 57 | it "by String" do 58 | context.default_currency = "EUR" 59 | context.default_currency.should be_a Money::Currency 60 | context.default_currency.should eq Money::Currency.find("EUR") 61 | end 62 | 63 | it "by Symbol" do 64 | context.default_currency = :xag 65 | context.default_currency.should be_a Money::Currency 66 | context.default_currency.should eq Money::Currency.find("XAG") 67 | end 68 | 69 | it "by Nil" do 70 | context.default_currency = nil 71 | expect_raises(Money::UndefinedCurrencyError, "No default currency set") do 72 | context.default_currency 73 | end 74 | end 75 | end 76 | end 77 | 78 | describe "#default_exchange" do 79 | it "is set to `Currency::Exchange` by default" do 80 | subject.default_exchange.should be_a Money::Currency::Exchange 81 | end 82 | end 83 | 84 | describe "#default_rate_store" do 85 | it "is set to `Currency::RateStore::Memory` by default" do 86 | subject.default_rate_store.should be_a Money::Currency::RateStore::Memory 87 | end 88 | end 89 | 90 | describe "#default_rate_provider" do 91 | it "is set to `Currency::RateProvider::Null` by default" do 92 | subject.default_rate_provider.should be_a Money::Currency::RateProvider::Null 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /src/money/money/rounding.cr: -------------------------------------------------------------------------------- 1 | struct Money 2 | module Rounding 3 | # Returns the nearest possible amount in cash value (cents). 4 | # 5 | # For example, in Swiss franc (CHF), the smallest possible amount of 6 | # cash value is CHF 0.05. Therefore, for CHF 0.07 this method returns CHF 0.05, 7 | # and for CHF 0.08, CHF 0.10. 8 | # 9 | # See also `#round_to_nearest_cash_value` and `Currency#smallest_denomination`. 10 | protected def nearest_cash_value(rounding_mode : Number::RoundingMode = Money.rounding_mode) : BigDecimal? 11 | return unless smallest_denomination = currency.smallest_denomination 12 | 13 | rounded_value = 14 | (fractional / smallest_denomination).round(rounding_mode) 15 | rounded_value *= smallest_denomination 16 | rounded_value 17 | end 18 | 19 | # Returns a new `Money` instance with the nearest possible amount in cash value 20 | # (cents), or `nil` if the `#currency` has no smallest denomination defined. 21 | # 22 | # For example, in Swiss franc (CHF), the smallest possible amount of 23 | # cash value is CHF 0.05. Therefore, for CHF 0.07 this method returns CHF 0.05, 24 | # and for CHF 0.08, CHF 0.10. 25 | # 26 | # ``` 27 | # Money.new(0.07, "CHF").round_to_nearest_cash_value? # => Money(@amount = 0.05) 28 | # Money.new(0.08, "CHF").round_to_nearest_cash_value? # => Money(@amount = 0.1) 29 | # Money.new(10.0, "XAG").round_to_nearest_cash_value? # nil 30 | # ``` 31 | def round_to_nearest_cash_value?(rounding_mode : Number::RoundingMode = Money.rounding_mode) : Money? 32 | if nearest_cash_value = nearest_cash_value(rounding_mode) 33 | copy_with(fractional: nearest_cash_value) 34 | end 35 | end 36 | 37 | # :ditto: 38 | # 39 | # NOTE: This variant raises `UndefinedSmallestDenominationError` if 40 | # `#round_to_nearest_cash_value?` returns `nil`. 41 | def round_to_nearest_cash_value!(rounding_mode : Number::RoundingMode = Money.rounding_mode) : Money 42 | round_to_nearest_cash_value?(rounding_mode) || 43 | raise UndefinedSmallestDenominationError.new(currency) 44 | end 45 | 46 | # :ditto: 47 | # 48 | # NOTE: This variant returns `self` if `#round_to_nearest_cash_value?` 49 | # returns `nil`. 50 | def round_to_nearest_cash_value(rounding_mode : Number::RoundingMode = Money.rounding_mode) : Money 51 | round_to_nearest_cash_value?(rounding_mode) || self 52 | end 53 | 54 | # Rounds the monetary amount to smallest unit of coinage, using 55 | # rounding *mode* if given, or `Money.rounding_mode` otherwise. 56 | # 57 | # ``` 58 | # Money.new(10.1, "USD").round # => Money(@amount=10.0, @currency="USD") 59 | # Money.new(10.5, "USD").round(mode: :ties_even) # => Money(@amount=10.0, @currency="USD") 60 | # Money.new(10.5, "USD").round(mode: :ties_away) # => Money(@amount=11.0, @currency="USD") 61 | # ``` 62 | def round(precision : Int = 0, mode : Number::RoundingMode = Money.rounding_mode) : Money 63 | copy_with(amount: @amount.round(precision, mode: mode)) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /src/money/registry.cr: -------------------------------------------------------------------------------- 1 | struct Money 2 | module Registry 3 | # Raised when trying to find an unknown object. 4 | class NotFoundError < Error 5 | def initialize(message : String = "Object not found") 6 | super(message) 7 | end 8 | 9 | def initialize(*, key : String) 10 | super("Object not found: #{key}") 11 | end 12 | end 13 | 14 | macro extended 15 | Money::Registry.setup {{ @type }} 16 | Money::Registry.setup_serializable {{ @type }} 17 | end 18 | 19 | # :nodoc: 20 | macro setup(klass) 21 | # All registered objects. 22 | class_getter registry = {} of String => {{ klass }}.class 23 | 24 | macro inherited 25 | {{ %({% base_namespace = #{klass}.id %}).id }} 26 | {% verbatim do %} 27 | {% @type.raise "abstract descendants are not allowed" if @type.abstract? %} 28 | {% 29 | name = @type.name 30 | name = 31 | if name.starts_with?("#{base_namespace}::") 32 | name[base_namespace.size + 2..].underscore 33 | else 34 | @type.raise "class #{@type} must be placed inside #{base_namespace} namespace" 35 | end 36 | %} 37 | {{ base_namespace }}.registry[{{ name.stringify }}] = self 38 | 39 | # Returns the class key. 40 | def self.key : String 41 | {{ name.stringify }} 42 | end 43 | {% end %} 44 | end 45 | 46 | # Returns the `{{ klass }}.class` for the given *name* if found, 47 | # `nil` otherwise. 48 | def self.find?(name : String | Symbol) : {{ klass }}.class | Nil 49 | registry[name.to_s.underscore]? 50 | end 51 | 52 | # Returns the `{{ klass }}.class` for the given *name* if found, 53 | # raises `Money::Registry::NotFoundError` otherwise. 54 | def self.find(name : String | Symbol) : {{ klass }}.class 55 | {% verbatim do %} 56 | find?(name) || 57 | {% if @type.has_constant?(:NotFoundError) %} 58 | raise NotFoundError.new(key: name.to_s) 59 | {% else %} 60 | raise Money::Registry::NotFoundError.new(key: name.to_s) 61 | {% end %} 62 | {% end %} 63 | end 64 | end 65 | 66 | # :nodoc: 67 | macro setup_serializable(klass) 68 | if_defined?(:JSON) do 69 | module Converter 70 | extend Money::Registry::Converter::JSON({{ klass }}) 71 | end 72 | 73 | include JSON::Serializable 74 | 75 | # Alias of `Converter.from_json`. 76 | def self.new(pull : JSON::PullParser) 77 | Converter.from_json(pull) 78 | end 79 | end 80 | 81 | if_defined?(:YAML) do 82 | module Converter 83 | extend Money::Registry::Converter::YAML({{ klass }}) 84 | end 85 | 86 | include YAML::Serializable 87 | 88 | # Alias of `Converter.from_yaml`. 89 | def self.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) 90 | Converter.from_yaml(ctx, node) 91 | end 92 | end 93 | end 94 | end 95 | end 96 | 97 | require "./registry/**" 98 | -------------------------------------------------------------------------------- /src/money/currency/rate_provider/providers/coin_market_cap.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class Money::Currency 4 | # [CoinMarketCap](https://coinmarketcap.com/) currency rate provider. 5 | class RateProvider::CoinMarketCap < RateProvider 6 | include RateProvider::HTTP 7 | 8 | Log = ::Log.for(self) 9 | 10 | getter api_key : String do 11 | option_from_env("COIN_MARKET_CAP_API_KEY") 12 | end 13 | getter host : URI do 14 | URI.parse("https://pro-api.coinmarketcap.com") 15 | end 16 | 17 | getter? use_fiat : Bool = true 18 | getter? use_crypto : Bool = true 19 | 20 | def initialize(*, @api_key = nil, @host = nil, @use_fiat = true, @use_crypto = true) 21 | end 22 | 23 | getter base_currency_codes : Array(String) do 24 | %w[].tap do |codes| 25 | fiat_currency_map.each_key { |key| codes << key } if use_fiat? 26 | crypto_currency_map.each_key { |key| codes << key } if use_crypto? 27 | end 28 | end 29 | 30 | # 31 | protected getter fiat_currency_map : Hash(String, Int32) do 32 | fetch_currency_map "fiat", &.itself 33 | end 34 | 35 | # 36 | protected getter crypto_currency_map : Hash(String, Int32) do 37 | fetch_currency_map "cryptocurrency", &.select(&.as_h["platform"].raw.nil?) 38 | end 39 | 40 | private def fetch_currency_map(type : String, &) : Hash(String, Int32) 41 | Log.debug { "Fetching supported currencies of type #{type.inspect}" } 42 | 43 | path = "/v1/%s/map" % URI.encode_path_segment(type) 44 | params = { 45 | "CMC_PRO_API_KEY": api_key, 46 | } 47 | request(path, params) do |response| 48 | result = JSON.parse(response.body_io).as_h 49 | 50 | currencies = yield result["data"].as_a 51 | currencies.to_h do |item| 52 | {item.as_h["symbol"].as_s, item.as_h["id"].as_i} 53 | end 54 | end 55 | end 56 | 57 | protected def currency_id(currency : Currency) : Int32? 58 | case {use_fiat?, use_crypto?} 59 | when {true, false} then fiat_currency_map[currency.code]? 60 | when {false, true} then crypto_currency_map[currency.code]? 61 | when {true, true} 62 | fiat_currency_map[currency.code]? || 63 | crypto_currency_map[currency.code]? 64 | end 65 | end 66 | 67 | # 68 | def exchange_rate?(base : Currency, target : Currency) : Rate? 69 | return unless base_id = currency_id(base) 70 | return unless target_id = currency_id(target) 71 | 72 | Log.debug &.emit("Fetching rate for #{base} -> #{target}", 73 | base_id: base_id, 74 | target_id: target_id, 75 | ) 76 | 77 | params = { 78 | "CMC_PRO_API_KEY": api_key, 79 | "amount": "1", 80 | "id": base_id.to_s, 81 | "convert_id": target_id.to_s, 82 | } 83 | request("/v2/tools/price-conversion", params) do |response| 84 | result = JSON.parse(response.body_io).as_h 85 | rate = 86 | result.dig("data", "quote", target_id.to_s, "price").to_s.to_big_d 87 | 88 | Rate.new(base, target, rate) 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/currency/rate_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe Money::Currency::Rate do 4 | rate = Money::Currency::Rate.new( 5 | Money::Currency.find("USD"), 6 | Money::Currency.find("CAD"), 7 | 1.1.to_big_d, 8 | Time.parse_utc("2025-05-22", "%F"), 9 | ) 10 | 11 | it "raises ArgumentError if given values are invalid" do 12 | expect_raises(ArgumentError, "Invalid rate: -100.0") do 13 | Money::Currency::Rate.new( 14 | Money::Currency.find("USD"), 15 | Money::Currency.find("EUR"), 16 | -100.to_big_d, 17 | Time.parse_utc("2025-05-22", "%F"), 18 | ) 19 | end 20 | expect_raises(ArgumentError, "Invalid rate: 0.0") do 21 | Money::Currency::Rate.new( 22 | Money::Currency.find("USD"), 23 | Money::Currency.find("EUR"), 24 | 0.to_big_d, 25 | Time.parse_utc("2025-05-22", "%F"), 26 | ) 27 | end 28 | end 29 | 30 | context "JSON serialization" do 31 | it ".from_json" do 32 | Money::Currency::Rate.from_json(rate.to_json).should eq rate 33 | end 34 | 35 | it "#to_json" do 36 | rate.to_pretty_json.should eq <<-JSON 37 | { 38 | "base": "USD", 39 | "target": "CAD", 40 | "value": 1.1, 41 | "updated_at": "2025-05-22T00:00:00Z" 42 | } 43 | JSON 44 | end 45 | end 46 | 47 | context "YAML serialization" do 48 | it ".from_yaml" do 49 | Money::Currency::Rate.from_yaml(rate.to_yaml).should eq rate 50 | end 51 | 52 | it "#to_yaml" do 53 | rate.to_yaml.should eq <<-YAML 54 | --- 55 | base: USD 56 | target: CAD 57 | value: 1.1 58 | updated_at: 2025-05-22\n 59 | YAML 60 | end 61 | end 62 | 63 | it "#<=>" do 64 | rates = [ 65 | Money::Currency::Rate.new( 66 | Money::Currency.find("USD"), 67 | Money::Currency.find("CAD"), 68 | 1.to_big_d, 69 | Time.parse_utc("2025-05-23", "%F"), 70 | ), 71 | Money::Currency::Rate.new( 72 | Money::Currency.find("USD"), 73 | Money::Currency.find("CAD"), 74 | 1.to_big_d, 75 | Time.parse_utc("2025-05-22", "%F"), 76 | ), 77 | Money::Currency::Rate.new( 78 | Money::Currency.find("USD"), 79 | Money::Currency.find("CAD"), 80 | 1.1.to_big_d, 81 | Time.parse_utc("2025-05-22", "%F"), 82 | ), 83 | Money::Currency::Rate.new( 84 | Money::Currency.find("CAD"), 85 | Money::Currency.find("USD"), 86 | 1.to_big_d, 87 | Time.parse_utc("2025-05-22", "%F"), 88 | ), 89 | ] 90 | rates.sort.map(&.to_s(include_updated_at: true)).should eq [ 91 | "CAD -> USD: 1.0 (2025-05-22 00:00:00 UTC)", 92 | "USD -> CAD: 1.0 (2025-05-23 00:00:00 UTC)", 93 | "USD -> CAD: 1.1 (2025-05-22 00:00:00 UTC)", 94 | "USD -> CAD: 1.0 (2025-05-22 00:00:00 UTC)", 95 | ] 96 | end 97 | 98 | it "#to_s" do 99 | rate.to_s.should eq "USD -> CAD: 1.1" 100 | rate.to_s(include_updated_at: true).should eq "USD -> CAD: 1.1 (2025-05-22 00:00:00 UTC)" 101 | end 102 | 103 | it "#base" do 104 | rate.base.should eq "USD" 105 | end 106 | 107 | it "#target" do 108 | rate.target.should eq "CAD" 109 | end 110 | 111 | it "#value" do 112 | rate.value.should be_a BigDecimal 113 | rate.value.should eq 1.1.to_big_d 114 | end 115 | end 116 | --------------------------------------------------------------------------------