├── .editorconfig ├── .github ├── funding.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── license ├── package.json ├── readme.md ├── src ├── cashify.ts ├── convert.ts ├── index.ts ├── lib │ ├── get-rate.ts │ └── options.ts └── utils │ ├── has-key.ts │ └── parser.ts ├── test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: xxczaki 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2-beta 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm run build --if-present 29 | - run: npm test -- --colors 30 | 31 | - name: Coveralls 32 | uses: coverallsapp/github-action@master 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Lockfiles 2 | 3 | package-lock.json 4 | yarn.lock 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | # next.js build output 66 | .next 67 | 68 | # Transpilation output 69 | dist 70 | 71 | .DS_store 72 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "13" 4 | - "12" 5 | - "10" 6 | after_success: 7 | - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls' 8 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Antoni Kepinski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cashify", 3 | "version": "3.0.1", 4 | "description": "Lightweight currency conversion library, successor of money.js", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist/**/*" 10 | ], 11 | "author": "Antoni Kępiński (https://www.kepinski.ch)", 12 | "bugs": { 13 | "url": "https://github.com/xxczaki/cashify/issues" 14 | }, 15 | "scripts": { 16 | "prebuild": "del-cli dist", 17 | "build": "tsc", 18 | "test": "xo && c8 --reporter=lcov ava", 19 | "prepublishOnly": "npm run build" 20 | }, 21 | "engines": { 22 | "node": ">=14" 23 | }, 24 | "license": "MIT", 25 | "repository": "xxczaki/cashify", 26 | "homepage": "https://github.com/xxczaki/cashify", 27 | "funding": { 28 | "type": "opencollective", 29 | "url": "https://opencollective.com/cashify" 30 | }, 31 | "keywords": [ 32 | "cashify", 33 | "cash", 34 | "moneyjs", 35 | "money.js", 36 | "money", 37 | "conversion", 38 | "exchange", 39 | "currency-exchange", 40 | "exchange-rates", 41 | "open-exchange-rates", 42 | "fixer", 43 | "currencies", 44 | "convert-currency-rates", 45 | "replacement", 46 | "convert-currencies", 47 | "typescript", 48 | "money-conversion" 49 | ], 50 | "devDependencies": { 51 | "@sindresorhus/tsconfig": "^2.0.0", 52 | "@types/node": "^16.11.6", 53 | "ava": "^3.15.0", 54 | "big.js": "^6.1.1", 55 | "c8": "^7.10.0", 56 | "coveralls": "^3.1.1", 57 | "del-cli": "^4.0.1", 58 | "ts-node": "^10.4.0", 59 | "typescript": "^4.4.4", 60 | "xo": "^0.46.4" 61 | }, 62 | "sideEffects": false, 63 | "ava": { 64 | "extensions": { 65 | "ts": "module" 66 | }, 67 | "nonSemVerExperiments": { 68 | "configurableModuleFormat": true 69 | }, 70 | "nodeArguments": [ 71 | "--loader=ts-node/esm" 72 | ] 73 | }, 74 | "xo": { 75 | "rules": { 76 | "@typescript-eslint/naming-convention": "off" 77 | } 78 | }, 79 | "dependencies": { 80 | "@types/big.js": "^6.1.2" 81 | }, 82 | "peerDependencies": { 83 | "big.js": ">=6.1.1" 84 | }, 85 | "peerDependenciesMeta": { 86 | "big.js": { 87 | "optional": true 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Cashify 💸 2 | 3 | > Lightweight currency conversion library, successor of money.js 4 | 5 | [![Build Status](https://github.com/xxczaki/cashify/workflows/CI/badge.svg)](https://github.com/xxczaki/cashify/actions?query=workflow%3ACI) 6 | [![Coverage Status](https://coveralls.io/repos/github/xxczaki/cashify/badge.svg?branch=master)](https://coveralls.io/github/xxczaki/cashify?branch=master) 7 | [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) 8 | [![install size](https://packagephobia.now.sh/badge?p=cashify)](https://packagephobia.now.sh/result?p=cashify) 9 | ![minified size](https://img.shields.io/bundlephobia/minzip/cashify) 10 | [![Mentioned in Awesome Node.js](https://awesome.re/mentioned-badge.svg)](https://github.com/sindresorhus/awesome-nodejs) 11 | 12 | - [Motivation](#motivation) 13 | - [Highlights](#highlights) 14 | - [Install](#install) 15 | - [Usage](#usage) 16 | - [With constructor](#with-constructor) 17 | - [Without constructor](#without-constructor) 18 | - [Parsing](#parsing) 19 | - [Integration with big.js](#integration-bigjs) 20 | - [Integration with currency.js](#integration-currencyjs) 21 | - [API](#api) 22 | - [Cashify({base, rates})](#cashifybase-rates) 23 | - [base](#base) 24 | - [rates](#rates) 25 | - [BigJs](#bigjs) 26 | - [convert(amount, {from, to, base, rates})](#convertamount-from-to-base-rates-with-and-without-constructor) 27 | - [amount](#amount) 28 | - [from](#from) 29 | - [to](#to) 30 | - [base](#base-1) 31 | - [rates](#rates-1) 32 | - [BigJs](#bigjs-1) 33 | - [parse(expression)](#parseexpression) 34 | - [expression](#expression) 35 | - [Migrating from money.js](#migrating-from-moneyjs) 36 | - [Floating point issues](#floating-point-issues) 37 | - [Related projects](#related-projects) 38 | - [License](#license) 39 | 40 | --- 41 | 42 | ## Motivation 43 | 44 | This package was created, because the popular [money.js](http://openexchangerates.github.io/money.js/) library: 45 | * is not maintained (last commit was ~5 years ago) 46 | * has over 20 open issues 47 | * does not support TypeScript 48 | * has implicit globals 49 | * does not have any unit tests 50 | * [has floating point issues](#floating-point-issues) 51 | 52 | ## Highlights 53 | 54 | - Simple API 55 | - 0 dependencies 56 | - Actively maintained 57 | - Well tested and documented 58 | - [Easy migration from money.js](#migrating-from-moneyjs) 59 | - Written in TypeScript 60 | - ESM-only 61 | 62 | ## Install 63 | 64 | ``` 65 | $ npm install cashify 66 | ``` 67 | 68 | **Please note that starting with version `3.0.0` this package is ESM-only and thus requires Node.js v14 or higher.** 69 | 70 | ## Usage 71 | 72 | ### With constructor 73 | 74 | ```js 75 | import {Cashify} from 'cashify'; 76 | 77 | const rates = { 78 | GBP: 0.92, 79 | EUR: 1.00, 80 | USD: 1.12 81 | }; 82 | 83 | const cashify = new Cashify({base: 'EUR', rates}); 84 | 85 | const result = cashify.convert(10, {from: 'EUR', to: 'GBP'}); 86 | 87 | console.log(result); //=> 9.2 88 | ``` 89 | 90 | ### Without constructor 91 | 92 | Using the `Cashify` constructor is not required. Instead, you can just use the `convert` function: 93 | 94 | ```js 95 | import {convert} from 'cashify'; 96 | 97 | const rates = { 98 | GBP: 0.92, 99 | EUR: 1.00, 100 | USD: 1.12 101 | }; 102 | 103 | const result = convert(10, {from: 'EUR', to: 'GBP', base: 'EUR', rates}); 104 | 105 | console.log(result); //=> 9.2 106 | ``` 107 | 108 | ### Parsing 109 | 110 | Cashify supports parsing, so you can pass a `string` to the `amount` argument and the `from` and/or `to` currency will be automatically detected: 111 | 112 | ```js 113 | import {Cashify} from 'cashify'; 114 | 115 | const rates = { 116 | GBP: 0.92, 117 | EUR: 1.00, 118 | USD: 1.12 119 | }; 120 | 121 | const cashify = new Cashify({base: 'EUR', rates}); 122 | 123 | // Basic parsing 124 | cashify.convert('€10 EUR', {to: 'GBP'}); 125 | 126 | // Full parsing 127 | cashify.convert('10 EUR to GBP'); 128 | ``` 129 | 130 | Alternatively, if you just want to parse a `string` without conversion you can use the [`parse`](#parseexpression) function which returns an `object` with parsing results: 131 | 132 | ```js 133 | import {parse} from 'cashify'; 134 | 135 | parse('10 EUR to GBP'); //=> {amount: 10, from: 'EUR', to: 'GBP'} 136 | ``` 137 | 138 | **Note:** If you want to use full parsing, you need to pass a `string` in a specific format: 139 | 140 | ``` 141 | 10 usd to pln 142 | 12.5 GBP in EUR 143 | 3.1415 eur as chf 144 | ``` 145 | 146 | You can use `to`, `in` or `as` to separate the expression (case insensitive). Used currencies name case doesn't matter, as cashify will automatically convert them to upper case. 147 | 148 | 149 | 150 | ### Integration with [big.js](https://github.com/MikeMcl/big.js/) 151 | 152 | [big.js](https://github.com/scurker/currency.js/) is a small JavaScript library for arbitrary-precision decimal arithmetic. You can use it with cashify to make sure you won't run into floating point issues: 153 | 154 | ```js 155 | import {Cashify} from 'cashify'; 156 | import Big from 'big.js'; 157 | 158 | const rates = { 159 | EUR: 0.8235, 160 | USD: 1 161 | }; 162 | 163 | const cashify = new Cashify({base: 'USD', rates}); 164 | 165 | const result = cashify.convert(1, { 166 | from: 'USD', 167 | to: 'EUR', 168 | BigJs: Big 169 | }); 170 | 171 | console.log(result); //=> 8.235 (without big.js you would get something like 0.8234999999999999) 172 | ``` 173 | 174 | 175 | 176 | ### Integration with [currency.js](https://github.com/scurker/currency.js/) 177 | 178 | [currency.js](https://github.com/scurker/currency.js/) is a small and lightweight library for working with currency values. It integrates well with cashify. In the following example we are using it to format the conversion result: 179 | 180 | ```js 181 | import {Cashify} from 'cashify'; 182 | import currency from 'currency.js'; 183 | 184 | const rates = { 185 | GBP: 0.92, 186 | EUR: 1.00, 187 | USD: 1.12 188 | }; 189 | 190 | const cashify = new Cashify({base: 'EUR', rates}); 191 | 192 | const converted = cashify.convert(8635619, {from: 'EUR', to: 'GBP'}); // => 7944769.48 193 | 194 | // Format the conversion result 195 | currency(converted, {symbol: '€', formatWithSymbol: true}).format(); // => €7,944,769.48 196 | ``` 197 | 198 | ## API 199 | 200 | ### Cashify({base, rates, BigJs}) 201 | 202 | Constructor. 203 | 204 | ##### base 205 | 206 | Type: `string` 207 | 208 | The base currency. 209 | 210 | ##### rates 211 | 212 | Type: `object` 213 | 214 | An object containing currency rates (for example from an API, such as Open Exchange Rates). 215 | 216 | ##### BigJs 217 | 218 | Type: [big.js](https://github.com/MikeMcl/big.js/) constructor 219 | 220 | See [integration with big.js](#integration-bigjs). 221 | 222 | ### convert(amount, {from, to, base, rates}) *`with and without constructor`* 223 | 224 | Returns conversion result (`number`). 225 | 226 | ##### amount 227 | 228 | Type: `number` or `string` 229 | 230 | Amount of money you want to convert. You can either use a `number` or a `string`. If you choose the second option, you can take advantage of [parsing](#parsing) and not specify `from` and/or `to` argument(s). 231 | 232 | ##### from 233 | 234 | Type: `string` 235 | 236 | Currency from which you want to convert. You might not need to specify it if you are using [parsing](#parsing). 237 | 238 | ##### to 239 | 240 | Type: `string` 241 | 242 | Currency to which you want to convert. You might not need to specify it if you are using [parsing](#parsing). 243 | 244 | ##### base 245 | 246 | Type: `string` 247 | 248 | The base currency. 249 | 250 | ##### rates 251 | 252 | Type: `object` 253 | 254 | An object containing currency rates (for example from an API, such as Open Exchange Rates). 255 | 256 | ##### BigJs 257 | 258 | Type: [big.js](https://github.com/MikeMcl/big.js/) constructor 259 | 260 | See [integration with big.js](#integration-bigjs). 261 | 262 | ### parse(expression) 263 | 264 | Returns an `object`, which contains parsing results: 265 | 266 | ``` 267 | { 268 | amount: number; 269 | from: string | undefined; 270 | to: string | undefined; 271 | } 272 | ``` 273 | 274 | ##### expression 275 | 276 | Type: `string` 277 | 278 | Expression you want to parse, ex. `10 usd to pln` or `€1.23 eur` 279 | 280 | ## Migrating from money.js 281 | 282 | With `Cashify` constructor: 283 | 284 | ```diff 285 | - import fx from 'money'; 286 | + import {Cashify} from 'cashify'; 287 | 288 | - fx.base = 'EUR'; 289 | - fx.rates = { 290 | - GBP: 0.92, 291 | - EUR: 1.00, 292 | - USD: 1.12 293 | - }; 294 | 295 | + const rates = { 296 | + GBP: 0.92, 297 | + EUR: 1.00, 298 | + USD: 1.12 299 | + }; 300 | 301 | + const cashify = new Cashify({base: 'EUR', rates}); 302 | 303 | - fx.convert(10, {from: 'GBP', to: 'EUR'}); 304 | + cashify.convert(10, {from: 'GBP', to: 'EUR'}); 305 | ``` 306 | 307 | With `convert` function: 308 | 309 | ```diff 310 | - import fx from 'money'; 311 | + import {convert} from 'cashify'; 312 | 313 | - fx.base = 'EUR'; 314 | - fx.rates = { 315 | - GBP: 0.92, 316 | - EUR: 1.00, 317 | - USD: 1.12 318 | - }; 319 | 320 | + const rates = { 321 | + GBP: 0.92, 322 | + EUR: 1.00, 323 | + USD: 1.12 324 | + }; 325 | 326 | - fx.convert(10, {from: 'GBP', to: 'EUR'}); 327 | + convert(10, {from: 'GBP', to: 'EUR', base: 'EUR', rates}); 328 | ``` 329 | 330 | ## Floating point issues 331 | 332 | When working with currencies, decimals only need to be precise up to the smallest cent value while avoiding common floating point errors when performing basic arithmetic. 333 | 334 | Let's take a look at the following example: 335 | 336 | ```js 337 | import fx from 'money'; 338 | import {Cashify} from 'cashify'; 339 | 340 | const rates = { 341 | GBP: 0.92, 342 | USD: 1.12 343 | }; 344 | 345 | fx.rates = rates; 346 | fx.base = 'EUR'; 347 | 348 | const cashify = new Cashify({base: 'EUR', rates}); 349 | 350 | fx.convert(10, {from: 'EUR', to: 'GBP'}); //=> 9.200000000000001 351 | cashify.convert(10, {from: 'EUR', to: 'GBP'}); //=> 9.2 352 | ``` 353 | 354 | As you can see, money.js doesn't handle currencies correctly and therefore a floating point issues are occuring. Even though there's just a minor discrepancy between the results, if you're converting large amounts, that can add up. 355 | 356 | Cashify solves this problem the same way as [currency.js](https://github.com/scurker/currency.js/) - by working with integers behind the scenes. **This should be okay for most reasonable values of currencies**; if you want to avoid all floating point issues, see [integration with big.js](). 357 | 358 | ## Related projects 359 | 360 | * [nestjs-cashify](https://github.com/vahidvdn/nestjs-cashify) - Node.js Cashify module for Nest.js. 361 | * [cashify-rs](https://github.com/xxczaki/cashify-rs) - Cashify port for Rust. 362 | 363 | ## License 364 | 365 | MIT © [Antoni Kępiński](https://www.kepinski.ch) 366 | -------------------------------------------------------------------------------- /src/cashify.ts: -------------------------------------------------------------------------------- 1 | import {Options} from './lib/options.js'; 2 | import convert from './convert.js'; 3 | 4 | export default class Cashify { 5 | /** 6 | * @constructor 7 | * @param {Object} [options] Conversion options. 8 | */ 9 | constructor(public readonly options: Partial) {} 10 | 11 | /** 12 | * Function, which converts currencies based on provided rates. 13 | * 14 | * @param {number | string} amount - Amount of money you want to convert. 15 | * @param {Object} [options] - Conversion options. 16 | * @return {number} Conversion result. 17 | * 18 | * @example 19 | * const rates = { 20 | * GBP: 0.92, 21 | * EUR: 1.00, 22 | * USD: 1.12 23 | * }; 24 | * 25 | * const cashify = new Cashify({base: 'EUR', rates}); 26 | * 27 | * cashify.convert(10, {from: 'EUR', to: 'GBP'}); //=> 9.2 28 | */ 29 | convert(amount: number | string, options?: Partial): number { 30 | return convert(amount, {...this.options, ...options} as Options); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/convert.ts: -------------------------------------------------------------------------------- 1 | import getRate from './lib/get-rate.js'; 2 | import {Options} from './lib/options.js'; 3 | import parse from './utils/parser.js'; 4 | 5 | /** 6 | * Function, which converts currencies based on provided rates. 7 | * 8 | * @param {number | string} amount - Amount of money you want to convert. 9 | * @param {Object} options - Conversion options. 10 | * @param {new (value: BigSource) => Big} fn - Optional, Big.js constructor - useful to avoid floating point errors. 11 | * @return {number} Conversion result. 12 | * 13 | * @example 14 | * const rates = { 15 | * GBP: 0.92, 16 | * EUR: 1.00, 17 | * USD: 1.12 18 | * }; 19 | * 20 | * convert(10, {from: 'EUR', to: 'GBP', base: 'EUR', rates}); //=> 9.2 21 | */ 22 | export default function convert( 23 | amount: number | string, 24 | {from, to, base, rates, BigJs}: Options, 25 | ): number { 26 | // If provided `amount` is a string, use parsing 27 | if (typeof amount === 'string') { 28 | const data = parse(amount); 29 | 30 | amount = data.amount; 31 | from = data.from ?? from; 32 | to = data.to ?? to; 33 | } 34 | 35 | if (BigJs) { 36 | return new BigJs(amount).times(getRate(base, rates, from, to)).toNumber(); 37 | } 38 | 39 | return (amount * 100) * getRate(base, rates, from, to) / 100; 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Cashify from './cashify.js'; 2 | import convert from './convert.js'; 3 | import parse from './utils/parser.js'; 4 | 5 | export {Cashify, convert, parse}; 6 | -------------------------------------------------------------------------------- /src/lib/get-rate.ts: -------------------------------------------------------------------------------- 1 | import hasKey from '../utils/has-key.js'; 2 | import {Rates} from './options.js'; 3 | 4 | /** 5 | * Get the conversion rate. 6 | * @param base Base currency. 7 | * @param rates Object containing currency rates (for example from an API, such as Open Exchange Rates). 8 | * @param from Currency from which you want to convert. 9 | * @param to Currency to which you want to convert. 10 | * @return Conversion result. 11 | */ 12 | export default function getRate(base: string, rates: Rates, from: string | undefined, to: string | undefined): number { 13 | if (from && to) { 14 | // If `from` equals `to`, return 100% directly 15 | if (from === to) { 16 | return 1; 17 | } 18 | 19 | // If `from` equals `base`, return the basic exchange rate for the `to` currency 20 | if (from === base && hasKey(rates, to)) { 21 | return rates[to]!; 22 | } 23 | 24 | // If `to` equals `base`, return the basic inverse rate of the `from` currency 25 | if (to === base && hasKey(rates, from)) { 26 | return 1 / rates[from]!; 27 | } 28 | 29 | // Otherwise, return the `to` rate multipled by the inverse of the `from` rate to get the relative exchange rate between the two currencies. 30 | if (hasKey(rates, from) && hasKey(rates, to)) { 31 | return rates[to]! * (1 / rates[from]!); 32 | } 33 | 34 | throw new Error('The `rates` object does not contain either the `from` or `to` currency.'); 35 | } else { 36 | throw new Error('Please specify the `from` and/or `to` currency, or use parsing.'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/options.ts: -------------------------------------------------------------------------------- 1 | import type {BigSource, Big} from 'big.js'; 2 | 3 | export type Rates = Record; 4 | 5 | export interface Options { 6 | /** 7 | * Currency from which you want to convert. 8 | */ 9 | from?: string; 10 | 11 | /** 12 | * Currency to which you want to convert. 13 | */ 14 | to?: string; 15 | 16 | /** 17 | * Base currency. 18 | */ 19 | base: string; 20 | 21 | /** 22 | * Object containing currency rates (for example from an API, such as Open Exchange Rates). 23 | */ 24 | rates: Rates; 25 | 26 | /** 27 | * Optional, Big.js constructor - useful to avoid floating point errors. 28 | */ 29 | BigJs?: new (value: BigSource) => Big; 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/has-key.ts: -------------------------------------------------------------------------------- 1 | import {Rates} from '../lib/options.js'; 2 | 3 | /** 4 | * Check if an object contains a key. 5 | * @param obj The object to check. 6 | * @param key The key to check for. 7 | */ 8 | export default function hasKey(object: Rates, key: string | number | symbol): key is keyof T { 9 | return Object.prototype.hasOwnProperty.call(object, key); 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/parser.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | amount: number; 3 | from: string | undefined; 4 | to: string | undefined; 5 | } 6 | 7 | /** 8 | * Expression parser 9 | * 10 | * @param {string} expression - Expression you want to parse, ex. `10 usd to pln` or `€1.23 eur` 11 | * @return {Object} Object with parsing results 12 | * 13 | * @example 14 | * parse('10 EUR to GBP'); //=> {amount: 10, from: 'EUR', to: 'GBP'} 15 | */ 16 | export default function parse(expression: string): Options { 17 | const amount = Number(expression.replace(/[^\d-.]/g, '')); 18 | let from; 19 | let to; 20 | 21 | // Search for separating keyword (case insensitive) to split the expression into 2 parts 22 | if (/to|in|as/i.test(expression)) { 23 | const firstPart = expression.slice(0, expression.search(/to|in|as/i)).toUpperCase().trim(); 24 | 25 | from = firstPart.replace(/[^A-Za-z]/g, ''); 26 | to = expression.slice(expression.search(/to|in|as/i) + 2).toUpperCase().trim(); 27 | } else { 28 | from = expression.replace(/[^A-Za-z]/g, ''); 29 | } 30 | 31 | if (Number.isNaN(amount) || expression.trim().length === 0) { 32 | throw new Error('Could not parse the expression. Make sure it includes at least a valid amount.'); 33 | } 34 | 35 | return { 36 | amount, 37 | from: from.toUpperCase() || undefined, 38 | to, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Big from 'big.js'; 3 | import {Cashify, convert, parse} from './src/index.js'; 4 | 5 | const rates = { 6 | GBP: 0.92, 7 | EUR: 1, 8 | USD: 1.12, 9 | }; 10 | 11 | test('exports a constructor', t => { 12 | const cashify = new Cashify({base: 'EUR', rates}); 13 | 14 | t.is(cashify.convert(12, {from: 'USD', to: 'GBP'}), 9.857_142_857_142_856); 15 | }); 16 | 17 | test('exports a `parse` function', t => { 18 | t.deepEqual(parse('10 eur to pln'), { 19 | amount: 10, 20 | from: 'EUR', 21 | to: 'PLN', 22 | }); 23 | }); 24 | 25 | test('basic conversion', t => { 26 | t.is(convert(12, {from: 'USD', to: 'GBP', base: 'EUR', rates}), 9.857_142_857_142_856); 27 | }); 28 | 29 | test('`from` equals `base`', t => { 30 | t.is(convert(10, {from: 'EUR', to: 'GBP', base: 'EUR', rates}), 9.2); 31 | }); 32 | 33 | test('`to` equals `base`', t => { 34 | t.is(convert(10, {from: 'GBP', to: 'EUR', base: 'EUR', rates}), 10.869_565_217_391_303); 35 | }); 36 | 37 | test('`from` equals `to`', t => { 38 | t.is(convert(10, {from: 'USD', to: 'USD', base: 'EUR', rates}), 10); 39 | }); 40 | 41 | test('`from` equals `to`, but `base` is different', t => { 42 | t.is(convert(10, {from: 'EUR', to: 'EUR', base: 'USD', rates}), 10); 43 | }); 44 | 45 | test('accepts `amount` of type `string`', t => { 46 | t.is(convert('12', {from: 'USD', to: 'GBP', base: 'EUR', rates}), 9.857_142_857_142_856); 47 | }); 48 | 49 | test('edge case: accepts `amount` of type `string`, equal to 0', t => { 50 | t.is(convert('0', {from: 'USD', to: 'GBP', base: 'EUR', rates}), 0); 51 | }); 52 | 53 | test('`amount` equals 0', t => { 54 | t.is(convert(0, {from: 'USD', to: 'GBP', base: 'EUR', rates}), 0); 55 | }); 56 | 57 | test('basic parsing (integer)', t => { 58 | t.is(convert('$12 USD', {to: 'GBP', base: 'EUR', rates}), 9.857_142_857_142_856); 59 | }); 60 | 61 | test('basic parsing (float)', t => { 62 | t.is(convert('1.23 GBP', {to: 'EUR', base: 'USD', rates}), 1.336_956_521_739_130_4); 63 | }); 64 | 65 | test('parsing without the `from` currency (integer)', t => { 66 | t.is(convert('12 to GBP', {from: 'USD', base: 'EUR', rates}), 9.857_142_857_142_856); 67 | }); 68 | 69 | test('full parsing (integer)', t => { 70 | t.is(convert('$12 USD TO GBP', {base: 'EUR', rates}), 9.857_142_857_142_856); 71 | t.is(convert('$12 USD IN GBP', {base: 'EUR', rates}), 9.857_142_857_142_856); 72 | t.is(convert('$12 USD AS GBP', {base: 'EUR', rates}), 9.857_142_857_142_856); 73 | }); 74 | 75 | test('full parsing (float)', t => { 76 | t.is(convert('1.23 gbp to eur', {base: 'USD', rates}), 1.336_956_521_739_130_4); 77 | t.is(convert('1.23 gbp in eur', {base: 'USD', rates}), 1.336_956_521_739_130_4); 78 | t.is(convert('1.23 gbp as eur', {base: 'USD', rates}), 1.336_956_521_739_130_4); 79 | }); 80 | 81 | test('`from` is not defined', t => { 82 | const error = t.throws(() => { 83 | convert(10, {to: 'EUR', base: 'USD', rates}); 84 | }, {instanceOf: Error}); 85 | 86 | t.is(error.message, 'Please specify the `from` and/or `to` currency, or use parsing.'); 87 | }); 88 | 89 | test('`rates` without the `base` currency', t => { 90 | const rates = { 91 | GBP: 0.92, 92 | USD: 1.12, 93 | }; 94 | 95 | t.is(convert(10, {from: 'EUR', to: 'GBP', base: 'EUR', rates}), 9.2); 96 | }); 97 | 98 | test('`rates` without either the `from` or `to` currency', t => { 99 | const error = t.throws(() => { 100 | convert(10, {from: 'CHF', to: 'EUR', base: 'EUR', rates}); 101 | }, {instanceOf: Error}); 102 | 103 | t.is(error.message, 'The `rates` object does not contain either the `from` or `to` currency.'); 104 | }); 105 | 106 | test('parsing without a correct amount', t => { 107 | const error = t.throws(() => { 108 | convert('', {base: 'EUR', rates}); 109 | }, {instanceOf: Error}); 110 | 111 | t.is(error.message, 'Could not parse the expression. Make sure it includes at least a valid amount.'); 112 | }); 113 | 114 | test('avoiding floating point issues with Big.js', t => { 115 | const rates = { 116 | USD: 1, 117 | EUR: 0.8235, 118 | }; 119 | 120 | t.is(convert(1, {from: 'USD', to: 'EUR', base: 'USD', rates, BigJs: Big}), 0.8235); 121 | }); 122 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2020", // Node.js 14 6 | "moduleResolution": "node", 7 | "lib": [ 8 | "es2020" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------