├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── check.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── millify ├── lib ├── index.ts ├── millify.ts ├── options.ts └── utils.ts ├── package-lock.json ├── package.json ├── test.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | lint-test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: '16' 16 | - run: npm install --production=false 17 | - run: npx eslint . --ext .ts 18 | - run: npm test 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | registry-url: https://registry.npmjs.org 20 | 21 | - run: npm install --production=false 22 | 23 | - run: npm publish 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Vim 30 | *.swp 31 | 32 | # TypeScript compiled files 33 | dist/ 34 | 35 | # MacOS 36 | .DS_Store 37 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | .github 3 | tsconfig.json 4 | tslint.json 5 | test.js 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: true 2 | trailingComma: "all" 3 | singleQuote: false 4 | printWidth: 80 5 | tabWidth: 2 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [6.1.0] - 2023-03-11 8 | - Defaults to browser locales from `navigator.languages` 9 | 10 | ## [6.0.2] - 2023-03-11 11 | - Update readme 12 | 13 | ## [6.0.1] - 2023-03-11 14 | - Fix publish 15 | 16 | ## [6.0.0] - 2023-03-11 17 | - Dropped `decimalSeparator` option. (**BREAKING CHANGE**) 18 | - Added `locales` option to format number into different languages. 19 | 20 | ## [5.0.1] - 2022-09-11 21 | ### Fixed 22 | - Ensured `MillifyOptions` interface is published to npm 23 | 24 | ## [5.0.0] - 2022-07-23 25 | ### Changed 26 | - Graceful fallback of invalid values instead of throwing error. (**BREAKING CHANGE**) 27 | 28 | ## [4.0.1] - 2022-07-23 29 | ### Fixed 30 | - Error converting a nullish value to string (undefined, null) 31 | 32 | ## [4.0.0] - 2021-05-19 33 | ### Fixed 34 | - Bug causing "1000T" instead of "1M" (#25) 35 | 36 | ### Changed 37 | - Returns the original number if no unit is available. 38 | - Default precision to 1 instead of 2. (**BREAKING CHANGE**) 39 | - `Options` interface name to `MillifyOptions`. (**BREAKING CHANGE**) 40 | 41 | ### Removed 42 | - `base` from options. We only intend to support the common grouping base (1000). (**BREAKING CHANGE**) 43 | 44 | ## [3.5.2] - 2021-04-07 45 | ### Changed 46 | - Update `yargs` dependency version. 47 | 48 | ## [3.5.1] - 2021-04-07 49 | ### Changed 50 | - Update README code example. 51 | 52 | ## [3.5.0] - 2020-12-31 53 | ### Fixed 54 | - Revert to default export in millify.js. 55 | 56 | ## [3.4.0] - 2020-12-27 57 | ### Changed 58 | - Replace TSLint with ESLint. 59 | - Use a single named export `millify` instead of default export. 60 | 61 | ## [3.3.0] - 2020-07-25 62 | ### Changed 63 | - Outputs an empty suffix if units array is not sufficient length. 64 | - Renamed `lowerCase` option to `lowercase` (no breaking change). 65 | 66 | ## [3.2.1] - 2020-05-12 67 | ### Changed 68 | - Update dependencies. 69 | 70 | ## [3.2.0] - 2020-05-12 71 | ### Fixed 72 | - Catch and log errors in the command line. 73 | 74 | ## [3.1.5] - 2020-05-12 75 | 76 | ### Changed 77 | - Change workflow to publish on tag push events. 78 | 79 | ## [3.1.5] - 2020-05-12 80 | 81 | ### Changed 82 | - Reduce size of packaged tarball by removing source files. 83 | 84 | ## [3.1.4] - 2020-05-12 85 | 86 | ### Fixed 87 | - Prevent infinite loop but caused by `null` values (thanks @dbankier). 88 | 89 | ## [3.1.3] - 2019-11-22 90 | 91 | ### Changed 92 | - Make `options` parameter optional. 93 | 94 | ## [3.1.2] - 2019-09-01 95 | 96 | ### Changed 97 | - Logic to decipher unit index. 98 | 99 | ## [3.1.1] - 2019-09-01 100 | 101 | ### Changed 102 | - Convert project to Typescript. 103 | - Updated dependencies. 104 | 105 | ## [3.1.0] - 2017-05-15 106 | 107 | ### Changed 108 | - Completely rewrite logic and refactor library core. 109 | 110 | ### Added 111 | - Ability to run package from CLI. 112 | - Option `units` to override default suffixes. 113 | - Option `space` to add a space between digit and suffix. 114 | - Option `decimalSeparator` to override default decimal separator. 115 | - Git hooks for code linting. 116 | - New tests. 117 | - Changelog. 118 | 119 | ### Removed 120 | - Dependency on `round-to` package. 121 | 122 | ## [3.0.0] - 2017-04-17 123 | 124 | ### Added 125 | - ESLint and Prettier for code lint/formatting. 126 | - Option `lowerCase` to output result in lower case. 127 | - Greater test coverage. 128 | 129 | ### Changed 130 | - Second function parameter to be an object for options. (**BREAKING CHANGE**) 131 | - Renamed `decimal` option to `precision`. (**BREAKING CHANGE**) 132 | - Replaced mocha test library with ava. 133 | - Created `src` directory for library code. 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yosh 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Millify 2 | 3 | Converts long `numbers` into pretty, human-readable `strings`. 4 | 5 | Before :unamused: | After :tada: 6 | --- | --- 7 | `2000` | `'2K'` 8 | `10000` | `'10k'` 9 | `42500` | `'42.5 kg'` 10 | `1250000` | `'1.25 MB'` 11 | `2700000000` | `'2.7 bil'` 12 | 13 | 14 | ## Install 15 | 16 | Get it on [npm](https://www.npmjs.com/package/millify): 17 | 18 | ```bash 19 | npm install millify 20 | ``` 21 | ## Usage 22 | 23 | ### Command line 24 | 25 | ```bash 26 | $ millify 12345 27 | 12.3K 28 | ``` 29 | 30 | See `millify --help` for options. 31 | 32 | ### Programmatically 33 | 34 | #### `millify(value: number, options: MillifyOptions)` 35 | 36 | ```js 37 | import millify from "millify"; 38 | 39 | // For CommonJS: `const { millify } = require("millify");` 40 | 41 | millify(2500); // 2.5K 42 | 43 | millify(1024000, { 44 | precision: 3, 45 | lowercase: true 46 | }); 47 | // 1.024m 48 | 49 | millify(39500, { 50 | precision: 2, 51 | locales: "de-DE" 52 | }); 53 | // 3,95K 54 | 55 | millify(1440000, { 56 | units: ["B", "KB", "MB", "GB", "TB"], 57 | space: true, 58 | }); 59 | // 1.44 MB 60 | ``` 61 | 62 | ## Options 63 | 64 | Name | Type | Default | Description 65 | --- | --- | --- | --- 66 | `precision` | `number` | `1` | Number of decimal places to use 67 | `locales` | `string \| Array` | browser language | Formats the number in different languages 68 | `lowercase` | `boolean` | `false` | Use lowercase abbreviations 69 | `space` | `boolean` | `false` | Add a space between number and abbreviation 70 | `units` | `Array` | `['', 'K', 'M', 'B', 'T', 'P', 'E']` | Unit abbreviations 71 | -------------------------------------------------------------------------------- /bin/millify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { millify } = require("../dist/millify"); 4 | 5 | require("yargs").command( 6 | "$0 [options]", 7 | "Convert long numbers to pretty, human-readable strings", 8 | (yargs) => { 9 | return yargs 10 | .positional("number", { 11 | describe: "Value to convert", 12 | type: "number", 13 | }) 14 | .option("precision", { 15 | describe: "Number of decimal places", 16 | alias: "p", 17 | type: "number", 18 | default: 1, 19 | }) 20 | .option("decimal", { 21 | describe: "Decimal separator (mark)", 22 | alias: "d", 23 | type: "string", 24 | default: ".", 25 | }) 26 | .option("lowercase", { 27 | describe: "Lowercase attributes", 28 | alias: "l", 29 | type: "boolean", 30 | default: false, 31 | }) 32 | .option("space", { 33 | describe: "Space after value", 34 | alias: "s", 35 | type: "boolean", 36 | default: false, 37 | }) 38 | .option("units", { 39 | describe: "Unit abbreviatons", 40 | alias: "u", 41 | type: "string", 42 | }) 43 | .example("$0 1000", "Run with default options") 44 | .example("$0 15025 --precision 2", "Set precision value") 45 | .example("$0 1000 --lowercase --space", "Lowercase and space set to true") 46 | .example('$0 1000 --decimal=","', "Commas as decimal separator") 47 | .example("$0 1000 -u B -u KB -u MB -u GB", "Specify units"); 48 | }, 49 | (argv) => { 50 | const options = { ...argv, decimalSeparator: argv.d }; 51 | try { 52 | // eslint-disable-next-line no-console 53 | console.log(millify(argv.number, options)); 54 | } catch (e) { 55 | console.error("ERR!", e.message); 56 | } 57 | } 58 | ).argv; 59 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import millify from "./millify"; 2 | 3 | export { millify }; 4 | 5 | export default millify; 6 | -------------------------------------------------------------------------------- /lib/millify.ts: -------------------------------------------------------------------------------- 1 | import { defaultOptions, MillifyOptions } from "./options"; 2 | import { getFractionDigits, getLocales, parseValue, roundTo } from "./utils"; 3 | 4 | // Most commonly used digit grouping base. 5 | const DIGIT_GROUPING_BASE = 1000; 6 | 7 | /** 8 | * Generator that divides a number until a decimal value is found. 9 | * 10 | * The denominator is defined by the numerical digit grouping base, 11 | * or interval. The most commonly-used digit group interval is 1000. 12 | * 13 | * e.g. 1,000,000 is grouped in multiples of 1000. 14 | */ 15 | function* divider(value: number): IterableIterator { 16 | // Create a mutable copy of the base. 17 | let denominator = DIGIT_GROUPING_BASE; 18 | 19 | while (true) { 20 | const result = value / denominator; 21 | if (result < 1) { 22 | // End of operation. We can't divide the value any further. 23 | return; 24 | } 25 | 26 | yield result; 27 | 28 | // The denominator is increased every iteration by multiplying 29 | // the base by itself, until a decimal value remains. 30 | denominator *= DIGIT_GROUPING_BASE; 31 | } 32 | } 33 | 34 | /** 35 | * millify converts long numbers to human-readable strings. 36 | */ 37 | function millify(value: number, options?: Partial): string { 38 | // Override default options with options supplied by user. 39 | const opts: MillifyOptions = options 40 | ? { ...defaultOptions, ...options } 41 | : defaultOptions; 42 | 43 | if (!Array.isArray(opts.units) || !opts.units.length) { 44 | throw new Error("Option `units` must be a non-empty array"); 45 | } 46 | 47 | // If the input value is invalid, then return the value in string form. 48 | // Originally this threw an error, but was changed to return a graceful fallback. 49 | let val: number; 50 | try { 51 | val = parseValue(value); 52 | } catch (e) { 53 | if (e instanceof Error) { 54 | console.warn(`WARN: ${e.message} (millify)`); 55 | } 56 | // Invalid values will be converted to string as per `String()`. 57 | return String(value); 58 | } 59 | 60 | // Add a minus sign (-) prefix if it's a negative number. 61 | const prefix = val < 0 ? "-" : ""; 62 | 63 | // Work only with positive values for simplicity's sake. 64 | val = Math.abs(val); 65 | 66 | // Keep dividing the input value by the digit grouping base 67 | // until the decimal and the unit index is deciphered. 68 | let unitIndex = 0; 69 | for (const result of divider(val)) { 70 | val = result; 71 | unitIndex += 1; 72 | } 73 | 74 | // Return the original number if the number is too large to have 75 | // a corresponding unit. Returning anything else is ambiguous. 76 | const unitIndexOutOfRange = unitIndex >= opts.units.length; 77 | if (unitIndexOutOfRange) { 78 | // At this point we don't know what to do with the input value, 79 | // so we return it as is, without localizing the string. 80 | return value.toString(); 81 | } 82 | 83 | // Round decimal up to desired precision. 84 | let rounded = roundTo(val, opts.precision); 85 | 86 | // Fixes an edge case bug that outputs certain numbers as 1000K instead of 1M. 87 | // The rounded value needs another iteration in the divider cycle. 88 | for (const result of divider(rounded)) { 89 | rounded = result; 90 | unitIndex += 1; 91 | } 92 | 93 | // Calculate the unit suffix and make it lowercase (if needed). 94 | const unit = opts.units[unitIndex] ?? ""; 95 | const suffix = opts.lowercase ? unit.toLowerCase() : unit; 96 | 97 | // Add a space between number and abbreviation. 98 | const space = opts.space ? " " : ""; 99 | 100 | // Format the number according to the desired locale. 101 | const formatted = rounded.toLocaleString(opts.locales ?? getLocales(), { 102 | // toLocaleString needs the explicit fraction digits. 103 | minimumFractionDigits: getFractionDigits(rounded), 104 | }); 105 | 106 | return `${prefix}${formatted}${space}${suffix}`; 107 | } 108 | 109 | export { millify }; 110 | 111 | export default millify; 112 | -------------------------------------------------------------------------------- /lib/options.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Options used to configure Millify. 3 | */ 4 | export interface MillifyOptions { 5 | /** 6 | * The number of significant figures. 7 | */ 8 | precision: number; 9 | /** 10 | * The active browser or server location. A string with a BCP 47 language 11 | * tag, or an array of such strings, e.g. "en-US". 12 | */ 13 | locales?: string | string[]; 14 | /** 15 | * Convert units to lower case. 16 | */ 17 | lowercase: boolean; 18 | /** 19 | * Add a space between the number and the unit. 20 | */ 21 | space: boolean; 22 | /** 23 | * A list of units to use. 24 | */ 25 | units: string[]; 26 | } 27 | 28 | /** 29 | * Default options for Millify. 30 | */ 31 | export const defaultOptions: MillifyOptions = { 32 | lowercase: false, 33 | precision: 1, 34 | space: false, 35 | units: [ 36 | "", 37 | "K", // Thousand 38 | "M", // Million 39 | "B", // Billion 40 | "T", // Trillion 41 | "P", // Quadrillion 42 | "E", // Quintillion 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * parseValue ensures the value is a number and within accepted range. 3 | */ 4 | export function parseValue(value: number): number { 5 | const val: number = parseFloat(value?.toString()); 6 | 7 | if (isNaN(val)) { 8 | throw new Error(`Input value is not a number`); 9 | } 10 | if (val > Number.MAX_SAFE_INTEGER || val < Number.MIN_SAFE_INTEGER) { 11 | throw new RangeError("Input value is outside of safe integer range"); 12 | } 13 | return val; 14 | } 15 | 16 | /** 17 | * Rounds a number [value] up to a specified [precision]. 18 | */ 19 | export function roundTo(value: number, precision: number): number { 20 | if (!Number.isFinite(value)) { 21 | throw new Error("Input value is not a finite number"); 22 | } 23 | if (!Number.isInteger(precision) || precision < 0) { 24 | throw new Error("Precision is not a positive integer"); 25 | } 26 | if (Number.isInteger(value)) { 27 | return value; 28 | } 29 | return parseFloat(value.toFixed(precision)); 30 | } 31 | 32 | /** 33 | * Returns the number of digits after the decimal. 34 | */ 35 | export function getFractionDigits(num: number): number { 36 | if (Number.isInteger(num)) { 37 | return 0; 38 | } 39 | const decimalPart = num.toString().split(".")[1]; 40 | return decimalPart?.length ?? 0; 41 | } 42 | 43 | /** 44 | * Returns the default browser locales. 45 | */ 46 | export function getLocales(): string[] { 47 | if (typeof navigator === "undefined") { 48 | return []; 49 | } 50 | return Array.from(navigator.languages ?? []); 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "millify", 3 | "version": "6.1.0", 4 | "description": "Converts long numbers to pretty, human-readable strings", 5 | "main": "dist/millify.js", 6 | "types": "dist/millify.d.ts", 7 | "bin": { 8 | "millify": "bin/millify" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "test": "npm run build && ava --verbose", 13 | "lint": "eslint . --ext .ts --fix", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/izolate/millify.git" 19 | }, 20 | "keywords": [ 21 | "big", 22 | "large", 23 | "numbers", 24 | "short", 25 | "pretty", 26 | "human", 27 | "format", 28 | "readable", 29 | "simplify", 30 | "beautify", 31 | "thousand", 32 | "million", 33 | "billion", 34 | "trillion", 35 | "millify" 36 | ], 37 | "author": "izolate (http://izolate.net/)", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/izolate/millify/issues" 41 | }, 42 | "homepage": "https://github.com/izolate/millify#readme", 43 | "devDependencies": { 44 | "@typescript-eslint/eslint-plugin": "^4.24.0", 45 | "@typescript-eslint/parser": "^4.24.0", 46 | "ava": "^4.3.1", 47 | "eslint": "^7.26.0", 48 | "eslint-config-prettier": "^8.3.0", 49 | "eslint-plugin-prettier": "^3.4.0", 50 | "prettier": "^2.3.0", 51 | "ts-node": "^9.1.1", 52 | "typescript": "^4.2.4" 53 | }, 54 | "dependencies": { 55 | "yargs": "^17.0.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const { millify } = require("./dist/millify"); 2 | const test = require("ava"); 3 | 4 | test("returns a string", (t) => { 5 | t.is(typeof millify(100), "string"); 6 | }); 7 | 8 | test("uses correct suffixes with default options", (t) => { 9 | const tests = new Map([ 10 | [100, "100"], 11 | [1000, "1K"], 12 | [1000000, "1M"], 13 | [1000000000, "1B"], 14 | [1000000000000, "1T"], 15 | ]); 16 | 17 | for (const [value, expected] of tests.entries()) { 18 | t.is(millify(value), expected); 19 | } 20 | }); 21 | 22 | test("rounds up to the nearest group", (t) => { 23 | const tests = new Map([ 24 | [999999, "1M"], // Not 1000K 25 | [999999999, "1B"], // Not 1000M 26 | [999999999999, "1T"], // Not 1000B 27 | [999000000000, "999B"], 28 | ]); 29 | 30 | for (const [value, expected] of tests.entries()) { 31 | t.is(millify(value, { precision: 1 }), expected); 32 | } 33 | }); 34 | 35 | test("handles negative numbers like positive ones", (t) => { 36 | const tests = new Map([ 37 | [-100, "-100"], 38 | [-1000, "-1K"], 39 | [-1000000, "-1M"], 40 | [-1000000000, "-1B"], 41 | [-1000000000000, "-1T"], 42 | ]); 43 | 44 | for (const [value, expected] of tests.entries()) { 45 | t.is(millify(value), expected); 46 | } 47 | }); 48 | 49 | test("uses lowercase suffixes", (t) => { 50 | const options = { lowercase: true }; 51 | const tests = new Map([ 52 | [1000, "1k"], 53 | [1000000, "1m"], 54 | [1000000000, "1b"], 55 | [1000000000000, "1t"], 56 | ]); 57 | 58 | for (const [value, expected] of tests.entries()) { 59 | t.is(millify(value, options), expected); 60 | } 61 | }); 62 | 63 | test("precision adjusts according to options", (t) => { 64 | const value = 12345.6789; 65 | const expected = [ 66 | "12K", 67 | "12.3K", 68 | "12.35K", 69 | "12.346K", 70 | "12.3457K", 71 | "12.34568K", 72 | "12.345679K", 73 | "12.3456789K", 74 | ]; 75 | 76 | expected.forEach((exp, precision) => 77 | t.is(exp, millify(value, { precision })), 78 | ); 79 | }); 80 | 81 | test("formats to different languages", (t) => { 82 | const tests = new Map([ 83 | ["en-US", "1.2"], 84 | ["de-DE", "1,2"], 85 | ["ar-SA", "١٫٢"], 86 | ]); 87 | for (const [locales, expected] of tests.entries()) { 88 | t.is(millify(1.2, { locales }), expected); 89 | } 90 | }); 91 | 92 | test("allows a space between decimal and unit", (t) => { 93 | const result = millify(55500, { space: true }); 94 | const expected = "55.5 K"; 95 | t.is(expected, result); 96 | }); 97 | 98 | test("allows custom units", (t) => { 99 | const options = { units: ["mg", "g", "kg", "tonne"], space: true }; 100 | 101 | const tests = new Map([ 102 | [Math.pow(10, 0), "1 mg"], 103 | [Math.pow(10, 3), "1 g"], 104 | [Math.pow(10, 6), "1 kg"], 105 | [Math.pow(10, 9), "1 tonne"], 106 | ]); 107 | 108 | for (const [value, expected] of tests.entries()) { 109 | t.is(millify(value, options), expected); 110 | } 111 | 112 | // It should return the original number if there is no unit available 113 | const largeVal = Math.pow(10, 12); 114 | t.is(millify(largeVal, options), largeVal.toString()); 115 | }); 116 | 117 | test("graceful fallback if value is invalid", (t) => { 118 | const invalidValues = [ 119 | Number.MAX_SAFE_INTEGER + 1, 120 | Number.MIN_SAFE_INTEGER - 1, 121 | undefined, 122 | null, 123 | Symbol("foo"), 124 | new Set([1, 2, 3]), 125 | new Map(), 126 | { foo: 1 }, 127 | ]; 128 | for (const value of invalidValues) { 129 | t.is(String(value), millify(value)); 130 | } 131 | }); 132 | 133 | test("throws error if precision is invalid", (t) => { 134 | t.throws(() => millify(10000, { precision: Infinity })); 135 | t.throws(() => millify(10000, { precision: Math.PI })); 136 | }); 137 | 138 | test("throws error if units is invalid", (t) => { 139 | t.throws(() => millify(1000, { units: [] })); 140 | t.throws(() => millify(1000, { units: {} })); 141 | }); 142 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------