├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ ├── backup.yml
│ └── build.yml
├── .gitignore
├── .node-version
├── .npmrc
├── .prettierrc
├── license
├── logo.png
├── package-lock.json
├── package.json
├── readme.md
├── sh
├── build.ts
└── coverage.ts
├── src
├── bug-reports.test.ts
├── cjs
│ └── index.cjs
├── formats
│ ├── ordinal.test.ts
│ ├── ordinal.ts
│ ├── percentage.test.ts
│ └── percentage.ts
├── helpers
│ ├── convert-to-number.test.ts
│ ├── convert-to-number.ts
│ ├── digits-format.test.ts
│ ├── digits-format.ts
│ ├── fraction-digits.test.ts
│ ├── fraction-digits.ts
│ ├── from-parts.test.ts
│ ├── from-parts.ts
│ ├── integer-digits-on-format.ts
│ ├── regex.test.ts
│ ├── regex.ts
│ ├── resolved-options.test.ts
│ ├── resolved-options.ts
│ ├── value.test.ts
│ └── value.ts
├── index.test.ts
├── index.ts
├── numberfmt.d.ts
├── options
│ ├── compact-display.test.ts
│ ├── compact-display.ts
│ ├── currency-display.test.ts
│ ├── currency-display.ts
│ ├── currency-sign.test.ts
│ ├── currency-sign.ts
│ ├── currency.test.ts
│ ├── currency.ts
│ ├── maximum-fraction-digits.test.ts
│ ├── maximum-fraction-digits.ts
│ ├── minimum-fraction-digits.test.ts
│ ├── minimum-fraction-digits.ts
│ ├── minimum-integer-digits.test.ts
│ ├── minimum-integer-digits.ts
│ ├── notation.test.ts
│ ├── notation.ts
│ ├── sign-display.test.ts
│ ├── sign-display.ts
│ ├── style.test.ts
│ ├── style.ts
│ ├── unit-display.test.ts
│ ├── unit-display.ts
│ ├── unit.test.ts
│ └── unit.ts
└── tests
│ ├── compact.test.ts
│ ├── compound.test.ts
│ ├── currency.test.ts
│ ├── digital.test.ts
│ ├── exponential.test.ts
│ ├── length.test.ts
│ ├── mass.test.ts
│ ├── numeric.test.ts
│ ├── ordinal.test.ts
│ ├── partial-application.test.ts
│ ├── percentage.test.ts
│ └── sign.test.ts
├── tsconfig.build.json
├── tsconfig.json
└── vite.config.mts
/.eslintignore:
--------------------------------------------------------------------------------
1 | __data__
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb-base",
4 | "airbnb-typescript/base",
5 | "prettier",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:import/typescript"
8 | ],
9 | "parser": "@typescript-eslint/parser",
10 | "plugins": [
11 | "@typescript-eslint"
12 | ],
13 | "parserOptions": {
14 | "ecmaVersion": 9,
15 | "project": "./tsconfig.json"
16 | },
17 | "env": {
18 | "node": true
19 | },
20 | "rules": {
21 | "@typescript-eslint/comma-dangle": "off",
22 | "import/prefer-default-export": "off"
23 | },
24 | "overrides": [
25 | {
26 | "files": [
27 | "**/*.ts"
28 | ],
29 | "rules": {
30 | "no-undef": "off"
31 | }
32 | }
33 | ]
34 | }
--------------------------------------------------------------------------------
/.github/workflows/backup.yml:
--------------------------------------------------------------------------------
1 | name: backup
2 |
3 | on: [push, delete]
4 |
5 | jobs:
6 | backup:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@main
10 | with:
11 | fetch-depth: "0"
12 | - uses: ruicsh/backup-action@main
13 | with:
14 | bitbucket_app_user: ${{ secrets.BACKUP_APP_USER }}
15 | bitbucket_app_password: ${{ secrets.BACKUP_APP_PASSWORD }}
16 | target_repo: tuplo/numberfmt
17 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | node-version: [16.x, 18.x, 20.x]
13 | steps:
14 | - uses: actions/checkout@main
15 | with:
16 | fetch-depth: "0"
17 | - uses: actions/setup-node@main
18 | with:
19 | node-version: 20
20 | - uses: actions/cache@main
21 | with:
22 | path: node_modules
23 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
24 | - run: |
25 | npm install --frozen-lockfile --legacy-peer-deps --no-audit
26 | npm run lint
27 | npm run test:ci
28 |
29 | test-coverage:
30 | needs: test
31 | name: test-coverage
32 | runs-on: ubuntu-latest
33 | steps:
34 | - uses: actions/checkout@main
35 | with:
36 | fetch-depth: "0"
37 | - uses: actions/setup-node@main
38 | with:
39 | node-version: 20
40 | - uses: actions/cache@main
41 | with:
42 | path: node_modules
43 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
44 | - run: npm install --frozen-lockfile --legacy-peer-deps --no-audit
45 | - uses: paambaati/codeclimate-action@v2.7.2
46 | env:
47 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
48 | with:
49 | coverageCommand: npm run coverage
50 | debug: true
51 |
52 | publish-to-npm:
53 | needs: test
54 | runs-on: ubuntu-latest
55 | steps:
56 | - uses: actions/checkout@main
57 | with:
58 | fetch-depth: "0"
59 | - uses: actions/setup-node@main
60 | with:
61 | node-version: 20
62 | registry-url: https://registry.npmjs.org/
63 | - run: |
64 | npm install --frozen-lockfile --legacy-peer-deps --no-audit
65 | npm run build
66 | - name: Semantic Release
67 | uses: cycjimmy/semantic-release-action@main
68 | with:
69 | branch: main
70 | env:
71 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
72 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
73 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.nyc_output
2 | /coverage
3 | /node_modules
4 | /dist
5 | /coverage
6 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 20.14.0
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save=true
2 | save-exact=true
3 | access=public
4 | fund=false
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "printWidth": 80,
4 | "useTabs": true
5 | }
6 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tuplo
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 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tuplo/numberfmt/cb2fb303a89ee8013476b0710a6286fb49b767ee/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tuplo/numberfmt",
3 | "description": "Native numeric formatting using a text pattern",
4 | "version": "0.0.0-development",
5 | "repository": "git@github.com:tuplo/numberfmt.git",
6 | "author": "Rui Costa",
7 | "license": "MIT",
8 | "keywords": [
9 | "numbers",
10 | "formatting",
11 | "Intl.NumberFormat"
12 | ],
13 | "types": "dist/index.d.ts",
14 | "module": "./dist/index.mjs",
15 | "main": "./dist/index.cjs",
16 | "exports": {
17 | ".": [
18 | {
19 | "import": {
20 | "types": "./dist/index.d.ts",
21 | "default": "./dist/index.mjs"
22 | },
23 | "require": {
24 | "types": "./dist/index.d.ts",
25 | "default": "./dist/index.cjs"
26 | },
27 | "default": "./dist/index.mjs"
28 | },
29 | "./dist/index.mjs"
30 | ]
31 | },
32 | "files": [
33 | "dist/index.mjs",
34 | "dist/index.cjs",
35 | "dist/index.d.ts",
36 | "dist/numberfmt.d.ts"
37 | ],
38 | "devDependencies": {
39 | "@tuplo/shell": "1.2.2",
40 | "@types/node": "20.14.2",
41 | "@typescript-eslint/eslint-plugin": "7.13.0",
42 | "@typescript-eslint/parser": "7.13.0",
43 | "@vitest/coverage-v8": "1.6.0",
44 | "esbuild": "0.21.5",
45 | "eslint": "8.57.0",
46 | "eslint-config-airbnb-base": "15.0.0",
47 | "eslint-config-airbnb-typescript": "18.0.0",
48 | "eslint-config-prettier": "9.1.0",
49 | "eslint-plugin-import": "2.29.1",
50 | "npm-check-updates": "16.14.20",
51 | "nyc": "17.0.0",
52 | "prettier": "3.3.2",
53 | "tsx": "4.15.4",
54 | "typescript": "5.4.5",
55 | "vitest": "1.6.0"
56 | },
57 | "scripts": {
58 | "build": "tsx sh/build.ts",
59 | "coverage": "tsx sh/coverage.ts",
60 | "format": "prettier --write src sh",
61 | "lint:ts": "tsc --noEmit",
62 | "lint": "eslint src",
63 | "test:ci": "LANG=en-GB vitest run",
64 | "test": "LANG=en-GB vitest --watch",
65 | "upgrade": "npm-check-updates -u -x eslint && npm install"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
numberfmt
6 |
7 |
8 | Number formatting using a text pattern and native Intl.NumberFormat()
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Why
21 |
22 | JS provides powerful number formatting with the standard built-in object `Intl.NumberFormat`, but we find its API a little verbose and hard to grasp its full potential. We took inspiration from older libraries like numbro.js and numeral.js and built a string based pattern for interacting with `Intl.NumberFormat`. Tiny footprint, no dependencies, works on the browser or nodejs.
23 |
24 | ## Usage
25 |
26 | ```typescript
27 | import nf from '@tuplo/numberfmt';
28 |
29 | // numeric
30 | nf(123_456, '0,0.00'); // → 123,456.00
31 |
32 | // currency
33 | nf(123_456, '0,0GBP'); // → £123,456
34 |
35 | // digital units
36 | nf(123_456, '0b'); // → 120.56kb
37 |
38 | // with locale
39 | nf(123_456, '0,0.00', { locale: 'ar-EG' }); // → ١٢٣٬٤٥٦٫٧٩
40 |
41 | // functional programming, partial application
42 | const nfp = nf.partial('0,0.00');
43 | nfp(123_456); // → 123,456.00
44 | ```
45 |
46 | ## Install
47 |
48 | ```bash
49 | $ npm install @tuplo/numberfmt
50 |
51 | # or with yarn
52 | $ yarn add @tuplo/numberfmt
53 | ```
54 |
55 | ## Options
56 |
57 | An optional set of options can be provided
58 |
59 | ```typescript
60 | nf(123_456, '0,0.00', {
61 | locale: 'ar-EG',
62 | numberingSystem: 'arab'
63 | }); // → ١٢٣٬٤٥٦٫٧٩
64 | ```
65 |
66 | ### locale
67 |
68 | > `string` | optional | defaults to system locale
69 |
70 | The BCP 47 language tag for the locale actually used.
71 |
72 |
73 | ### numberingSystem
74 |
75 | > `string` | optional | derives from locale
76 |
77 | Examples: `arab`, `fullwide`, `hant`, `latn`.
78 |
79 | ## Reference
80 |
81 | | Character | Meaning | |
82 | | --------- | -------------------------------- | ---------------- |
83 | | 0 | digit | 1 |
84 | | . | decimal separator | 1.2 |
85 | | , | thousands group separator | 1,234 |
86 | | [] | optional | |
87 | | - | negative sign | -1 |
88 | | () | negative sign (accounting) | (123) |
89 | | % | percentage | 95% |
90 | | o | ordinals | 1st |
91 | | a | compact notation (short display) | 1K |
92 | | A | compact notation (long display) | 1 thousand |
93 | | e | exponential (scientific) | 1E4 |
94 | | E | exponential (engineering) | 100E3 |
95 | | b | bits | 1.23Mb |
96 | | B | bytes | 1.23TB |
97 | | m | length | 1.23km |
98 | | k | mass | 1.23kg |
99 | | USD | currency symbol | US$1,000 |
100 | | s | currency narrow symbol | $1,000 |
101 | | c | currency code | USD 1,000 |
102 | | n | currency name | 1,000 US dollars |
103 |
104 | ### Numbers
105 |
106 | | Value | Format | Result | |
107 | | ------- | ------- | -------- | --------------------------------- |
108 | | 123456 | 0,0 | 123,456 | thousands separator |
109 | | 1234.5 | 0,0.0 | 1,234.5 | decimal separator |
110 | | 1234.5 | 0,0.00 | 1,234.50 | fixed number of decimal digits |
111 | | 123 | 0[.]0 | 123 | optional decimal separator |
112 | | 123.4 | 0[.]0 | 123.4 | |
113 | | 123.4 | 0[.]00 | 123.40 | |
114 | | 123.45 | 0.00[0] | 123.45 | fixed and optional decimal digits |
115 | | 123.456 | 0.00[0] | 123.456 | |
116 | | -1 | -0 | -1 | negative sign |
117 | | 1 | (0) | 1 | negative sign (accounting) |
118 | | -1 | (0) | (1) | |
119 |
120 | ### Percentage
121 |
122 | | Value | Format | Result |
123 | | ------- | ------ | ------ |
124 | | 1 | 0% | 100% |
125 | | -0.12 | 0% | -12% |
126 | | 0.12345 | 0.00% | 12.34% |
127 |
128 | ### Ordinals
129 |
130 | | Value | Format | Result |
131 | | ----- | ------ | ------- |
132 | | 0 | 0o | 0th |
133 | | 1 | 0o | 1st |
134 | | 2 | 0o | 2nd |
135 | | 3 | 0o | 3rd |
136 | | 4 | 0o | 4th |
137 | | 1234 | 0,0o | 1,234th |
138 |
139 | ### Compact notation
140 |
141 | | Value | Format | Result | |
142 | | ------------- | ------ | ----------- | ----------------------------- |
143 | | 123 | 0a | 123 | short display |
144 | | 1234 | 0a | 1K | |
145 | | 12345 | 0a | 12K | |
146 | | 123456 | 0a | 123K | |
147 | | 1234567 | 0a | 1M | |
148 | | 1234567890 | 0a | 1B | |
149 | | 1234567890123 | 0a | 1T | |
150 | | 1234 | 0A | 1 thousand | long display |
151 | | 1234567 | 0A | 1 million | |
152 | | 1234567890 | 0A | 1 billion | |
153 | | 1234567890123 | 0A | 1 trillion | |
154 | | 1234567 | 0.0a | 1.2M | combined with numeric formats |
155 | | 1234567 | 0.0A | 1.2 million | |
156 |
157 | ### Exponential
158 |
159 | | Value | Format | Result | |
160 | | -------- | ------ | ------ | ----------------------------------------------------- |
161 | | 1 | 0e | 1E0 | scientific (order-of-magnitude) |
162 | | 10 | 0e | 1E1 | |
163 | | 100 | 0e | 1E2 | |
164 | | 1000 | 0e | 1E3 | |
165 | | 10000 | 0e | 1E4 | |
166 | | 1 | 0E | 1E0 | engineering (exponent of ten when divisible by three) |
167 | | 10 | 0E | 10E0 | |
168 | | 100 | 0E | 100E0 | |
169 | | 1000 | 0E | 1E3 | |
170 | | 10000 | 0E | 10E3 | |
171 | | 100000 | 0E | 100E3 | |
172 | | 12345678 | 0.0e | 1.2E7 | combined with numeric formats |
173 | | 12345678 | 0.0E | 12.3E6 | |
174 |
175 | ### Digital
176 |
177 | | Value | Format | Result | |
178 | | ------------- | ------ | ------ | ----------------- |
179 | | 1 | 0b | 1bit | bits (narrow) |
180 | | 1024 | 0b | 1kb | |
181 | | 1048576 | 0b | 1Mb | |
182 | | 1073741824 | 0b | 1Gb | |
183 | | 1099511627776 | 0b | 1Tb | |
184 | | 1 | 0B | 1B | bytes (narrow) |
185 | | 1024 | 0B | 1kB | |
186 | | 1048576 | 0B | 1MB | |
187 | | 1073741824 | 0B | 1GB | |
188 | | 1099511627776 | 0B | 1TB | |
189 | | 1 | 0 b | 1 bit | bits (short) |
190 | | 1024 | 0 b | 1 kb | |
191 | | 1 | 0 B | 1 byte | bytes (short) |
192 | | 1024 | 0 B | 1 kB | |
193 | | 1524 | 0b | 1.49kb | 2 fraction digits |
194 | | 1524 | 0B | 1.49kB | |
195 |
196 | ### Length
197 |
198 | | Value | Format | Result | |
199 | | ----- | ------ | ------ | --------------------------------------------------- |
200 | | 0.001 | 0m | 1mm | value in meters, formatted to closest unit (narrow) |
201 | | 0.01 | 0m | 1cm | |
202 | | 1 | 0m | 1m | |
203 | | 1000 | 0m | 1km | |
204 | | 1200 | 0m | 1.2km | |
205 | | 0.001 | 0 m | 1 mm | (short) |
206 | | 0.01 | 0 m | 1 cm | |
207 | | 1 | 0 m | 1 m | |
208 | | 1000 | 0 m | 1 km | |
209 | | 1200 | 0 m | 1.2 km | |
210 |
211 | ### Mass
212 |
213 | | Value | Format | Result | |
214 | | ----- | ------ | ------- | ------ |
215 | | 1 | 0k | 1kg | narrow |
216 | | 0.001 | 0k | 1g | |
217 | | 1.23 | 0k | 1.23kg | |
218 | | 1 | 0 k | 1 kg | short |
219 | | 0.001 | 0 k | 1 g | |
220 | | 1.23 | 0 k | 1.23 kg | |
221 |
222 | ### Currency
223 |
224 | | Value | Format | Result | |
225 | | ----- | --------- | -------------------- | ----------------- |
226 | | 123 | GBP | £123 | symbol |
227 | | 1234 | 0,0GBP | £1,234 | |
228 | | 1234 | 0,0.00GBP | £1,234.00 | |
229 | | 123 | EUR | €123 | |
230 | | 123 | JPY | JP¥123 | |
231 | | 123 | USD | US$123 | |
232 | | 123 | CAD | CA$123 | |
233 | | 1000 | 0,0GBPs | £1,000 | narrow symbol |
234 | | 1000 | 0,0EURs | €1,000 | |
235 | | 1000 | 0,0USDs | $1,000 | |
236 | | 1000 | 0,0CADs | $1,000 | |
237 | | 1000 | 0,0JPYs | ¥1,000 | |
238 | | 1000 | 0,0GBPc | GBP 1,000 | ISO currency code |
239 | | 1000 | 0,0EURc | EUR 1,000 | |
240 | | 1000 | 0,0USDc | USD 1,000 | |
241 | | 1000 | 0,0JPYc | JPY 1,000 | |
242 | | 1000 | 0,0GBPn | 1,000 British pounds | currency name |
243 | | 1000 | 0,0EURn | 1,000 euros | |
244 | | 1000 | 0,0USDn | 1,000 US dollars | |
245 | | 1000 | 0,0JPYn | 1,000 Japanese yen | |
246 |
247 | ## License
248 |
249 | MIT
--------------------------------------------------------------------------------
/sh/build.ts:
--------------------------------------------------------------------------------
1 | import { $ } from "@tuplo/shell";
2 |
3 | async function main() {
4 | await $`rm -rf dist`;
5 | await $`tsc --build tsconfig.build.json`;
6 |
7 | const flags = ["--bundle", "--platform=node", "--minify"];
8 |
9 | await $`esbuild src/cjs/index.cjs --outfile=dist/index.cjs ${flags}`;
10 | await $`esbuild src/index.ts --format=esm --outfile=dist/index.mjs ${flags}`;
11 |
12 | await $`rm dist/index.js`;
13 | await $`rm -rf dist/formats dist/helpers dist/options`;
14 | await $`cp src/numberfmt.d.ts dist/`;
15 | }
16 |
17 | main();
18 |
--------------------------------------------------------------------------------
/sh/coverage.ts:
--------------------------------------------------------------------------------
1 | import { $ } from "@tuplo/shell";
2 |
3 | async function main() {
4 | await $`rm -rf ./node_modules/.cache`;
5 | await $`rm -rf coverage/`;
6 | await $`rm -rf .nyc_output/`;
7 |
8 | const flags = ["--coverage true"].flatMap((f) => f.split(" "));
9 | await $`NODE_ENV=test LOG_LEVEL=silent nyc yarn run test:ci ${flags}`;
10 | }
11 |
12 | main();
13 |
--------------------------------------------------------------------------------
/src/bug-reports.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "./index";
2 |
3 | describe("bug reports", () => {
4 | it("shows mismatched values between nodejs and browser", () => {
5 | const value = 47_939;
6 | const actual = nf(value, "0.0a");
7 |
8 | // const expected = '48K'; // chrome, firefox
9 | const expected = "47.9K"; // nodejs
10 | expect(actual).toBe(expected);
11 | });
12 |
13 | it("respect number of fraction digits", () => {
14 | const value = 123.4;
15 | const actual = nf(value, "0.00[0]");
16 |
17 | const expected = "123.40";
18 | expect(actual).toBe(expected);
19 | });
20 |
21 | it("respect number of fraction digits using short notation", () => {
22 | const actual = nf(6_500, "0.0a");
23 |
24 | const expected = "6.5K";
25 | expect(actual).toBe(expected);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/cjs/index.cjs:
--------------------------------------------------------------------------------
1 | import numberfmt from "../index";
2 |
3 | module.exports = numberfmt;
4 |
--------------------------------------------------------------------------------
/src/formats/ordinal.test.ts:
--------------------------------------------------------------------------------
1 | import { formatOrdinal } from "./ordinal";
2 |
3 | describe("formats ordinals", () => {
4 | it.each([
5 | ["0o", 1, "1", "en-GB", "1st"],
6 | ["0o", 2, "2", "en-GB", "2nd"],
7 | ])("ordinals: %s", (format, value, formatted, locale, expected) => {
8 | const result = formatOrdinal({ value, formatted, locale, format });
9 | expect(result).toBe(expected);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/formats/ordinal.ts:
--------------------------------------------------------------------------------
1 | import { rgSpaceBetween } from "../helpers/regex";
2 |
3 | function getSuffixes(locale: string) {
4 | switch (locale) {
5 | case "en-GB":
6 | default:
7 | return new Map([
8 | ["one", "st"],
9 | ["two", "nd"],
10 | ["few", "rd"],
11 | ["other", "th"],
12 | ]);
13 | }
14 | }
15 |
16 | interface IFormatOrdinalParams {
17 | value: number;
18 | formatted: string;
19 | locale: string;
20 | format: string;
21 | }
22 |
23 | export function formatOrdinal(params: IFormatOrdinalParams) {
24 | const { value, formatted, locale, format } = params;
25 | const pluralRules = new Intl.PluralRules(locale, {
26 | type: "ordinal",
27 | });
28 |
29 | const rule = pluralRules.select(value);
30 | const suffix = getSuffixes(locale).get(rule);
31 |
32 | return rgSpaceBetween.test(format)
33 | ? `${formatted} ${suffix}`
34 | : `${formatted}${suffix}`;
35 | }
36 |
--------------------------------------------------------------------------------
/src/formats/percentage.test.ts:
--------------------------------------------------------------------------------
1 | import { formatPercentage } from "./percentage";
2 |
3 | describe("percentage", () => {
4 | it.each([
5 | ["0%", "100%", 1, undefined],
6 | ["0 %", "100 %", 1, undefined],
7 | ["0.0%", "1.0%", 0.01, 1],
8 | ["0.0 %", "1.0 %", 0.01, 1],
9 | ["0.00 %", "1.20 %", 0.012, 2],
10 | ["0.00 %", "1.23 %", 0.0123, 2],
11 | ["0,0.0 %", "2,000 %", 20, undefined],
12 | ["0,0.0 %", "2,000,000 %", 20_000, undefined],
13 | ["0,0.0 %", "2,000,000.00 %", 20_000, 2],
14 | ])(
15 | "percentage: %s = %s",
16 | (format, expected, value, minimumFractionDigits) => {
17 | const nf = new Intl.NumberFormat("en-GB", {
18 | style: "percent",
19 | minimumFractionDigits,
20 | });
21 | const params = { format, value, numberFormatter: nf };
22 | const result = formatPercentage(params);
23 | expect(result).toBe(expected);
24 | }
25 | );
26 | });
27 |
--------------------------------------------------------------------------------
/src/formats/percentage.ts:
--------------------------------------------------------------------------------
1 | import { rgSpaceBetween } from "../helpers/regex";
2 | import { fromParts } from "../helpers/from-parts";
3 |
4 | interface IFormatPercentageParams {
5 | format: string;
6 | numberFormatter: Intl.NumberFormat;
7 | value: number;
8 | }
9 |
10 | export function formatPercentage(params: IFormatPercentageParams) {
11 | const { value, numberFormatter, format } = params;
12 | const { minusSign, integer, group, decimal, fraction, percentSign } =
13 | fromParts(numberFormatter.formatToParts(value));
14 |
15 | return [
16 | minusSign,
17 | (integer as string[]).join((group as string) || ""),
18 | decimal,
19 | fraction,
20 | rgSpaceBetween.test(format) && " ",
21 | percentSign,
22 | ]
23 | .filter(Boolean)
24 | .join("");
25 | }
26 |
--------------------------------------------------------------------------------
/src/helpers/convert-to-number.test.ts:
--------------------------------------------------------------------------------
1 | import { convertToNumber } from "./convert-to-number";
2 |
3 | describe("convert to number", () => {
4 | it.each([
5 | [undefined, null],
6 | [null, null],
7 | [{}, null],
8 | [[], null],
9 | [[1_234], 1_234],
10 | [[1_234, 5_678], null],
11 | ["abc", null],
12 | ["1234", 1_234],
13 | [1_234, 1_234],
14 | ])("converts from user input to number", (userInput, expected) => {
15 | const result = convertToNumber(userInput);
16 | expect(result).toBe(expected);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/helpers/convert-to-number.ts:
--------------------------------------------------------------------------------
1 | export function convertToNumber(userInput: unknown) {
2 | if (userInput === null) return null;
3 | if (Array.isArray(userInput) && userInput.length === 0) return null;
4 | const userValue = Number(userInput);
5 | if (Number.isNaN(userValue)) return null;
6 |
7 | return Number(userInput);
8 | }
9 |
--------------------------------------------------------------------------------
/src/helpers/digits-format.test.ts:
--------------------------------------------------------------------------------
1 | import { getDigitsFormat } from "./digits-format";
2 |
3 | describe("digits format", () => {
4 | it.each([
5 | [undefined, "0,000"],
6 | ["0", "0"],
7 | ["0,0", "0,000"],
8 | ["0,000[.]00", "0,000[.]00"],
9 | ])("digits format: %s = %s", (format, expected) => {
10 | const result = getDigitsFormat(format);
11 | expect(result).toBe(expected);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/helpers/digits-format.ts:
--------------------------------------------------------------------------------
1 | import { rgDigitsFormat } from "./regex";
2 |
3 | export function getDigitsFormat(format?: string) {
4 | const [digitsFormat] = rgDigitsFormat.exec(format || "") || ["0,000"];
5 |
6 | if (digitsFormat === "0,0") return "0,000";
7 |
8 | return digitsFormat;
9 | }
10 |
--------------------------------------------------------------------------------
/src/helpers/fraction-digits.test.ts:
--------------------------------------------------------------------------------
1 | import { getFractionDigits } from "./fraction-digits";
2 |
3 | describe("fraction digits", () => {
4 | it.each([
5 | ["0,0", [0, 0]],
6 | ["0.0", [1, 0]],
7 | ["0.000", [3, 0]],
8 | ["0,000.00", [2, 0]],
9 | ["0[.]0000", [4, 0]],
10 | ["0.00[0]", [2, 1]],
11 | ["0.0[00]", [1, 2]],
12 | ])("gets fraction digits on format: %s", (format, expected) => {
13 | const actual = getFractionDigits(format);
14 | expect(actual).toStrictEqual(expected);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/helpers/fraction-digits.ts:
--------------------------------------------------------------------------------
1 | import { rgFractionDigits, rgRequiredDigits, rgOptionalDigits } from "./regex";
2 |
3 | export function getFractionDigits(format: string) {
4 | const [digits] = rgFractionDigits.exec(format) || [""];
5 | const [required] = rgRequiredDigits.exec(digits) || [""];
6 | const [, optional] = rgOptionalDigits.exec(digits) || ["", ""];
7 |
8 | return [required.length, optional.length];
9 | }
10 |
--------------------------------------------------------------------------------
/src/helpers/from-parts.test.ts:
--------------------------------------------------------------------------------
1 | import { fromParts } from "./from-parts";
2 |
3 | describe("from parts", () => {
4 | it.each([
5 | [
6 | "percentage",
7 | [
8 | { type: "integer", value: "100" },
9 | { type: "percentSign", value: "%" },
10 | ],
11 | { integer: ["100"], percentSign: "%" },
12 | ],
13 | [
14 | "grouped integers",
15 | [
16 | { type: "integer", value: "2" },
17 | { type: "group", value: "," },
18 | { type: "integer", value: "000" },
19 | ],
20 | { integer: ["2", "000"], group: "," },
21 | ],
22 | ])("converts from parts to object: %s", (_, parts, expected) => {
23 | const result = fromParts(parts as Intl.NumberFormatPart[]);
24 | expect(result).toStrictEqual(expected);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/helpers/from-parts.ts:
--------------------------------------------------------------------------------
1 | type INumberParts = Record;
2 |
3 | export function fromParts(parts: Intl.NumberFormatPart[]) {
4 | return parts.reduce((acc, part) => {
5 | const { type, value } = part;
6 | if (type === "integer") {
7 | acc[type] = acc[type] || [];
8 | (acc[type] as string[]).push(value);
9 | } else {
10 | acc[type] = value;
11 | }
12 | return acc;
13 | }, {} as INumberParts);
14 | }
15 |
--------------------------------------------------------------------------------
/src/helpers/integer-digits-on-format.ts:
--------------------------------------------------------------------------------
1 | import { rgIntegerDigitsOnFormat } from "./regex";
2 |
3 | export function getIntegerDigitsOnFormat(format: string) {
4 | const [integersInFormat] = rgIntegerDigitsOnFormat.exec(format) || [""];
5 | return integersInFormat.length;
6 | }
7 |
--------------------------------------------------------------------------------
/src/helpers/regex.test.ts:
--------------------------------------------------------------------------------
1 | import { rgDigitsFormat, rgIsCompact, rgCurrencyFormat } from "./regex";
2 |
3 | describe("operational regular expressions", () => {
4 | it.each([
5 | ["0.0", "0.0"],
6 | ["0,000.0", "0,000.0"],
7 | ["0,000[.]0", "0,000[.]0"],
8 | ["0,0[.]0[0]", "0,0[.]0[0]"],
9 | ["0.0a", "0.0"],
10 | ])("extracts digits format: %s", (format, expected) => {
11 | const [, result] = format.match(rgDigitsFormat) || ["", ""];
12 | expect(result).toBe(expected);
13 | });
14 |
15 | it.each([
16 | ["0.0", false],
17 | ["0.000a", true],
18 | ["0.000A", true],
19 | ])("compact display: %s", (format, expected) => {
20 | const result = rgIsCompact.test(format);
21 | expect(result).toBe(expected);
22 | });
23 |
24 | it.each([
25 | ["GBP", "GBP"],
26 | ["GBPa", "GBP"],
27 | ["0.0GBPa", "GBP"],
28 | ["CADn", "CADn"],
29 | ["USDs", "USDs"],
30 | ["EURn", "EURn"],
31 | ])("extracts currency format: %s", (format, expected) => {
32 | const [, result] = format.match(rgCurrencyFormat) || ["", ""];
33 | expect(result).toBe(expected);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/helpers/regex.ts:
--------------------------------------------------------------------------------
1 | export const rgCurrency = /([A-Z]{3})[scn]?/;
2 | export const rgCurrencyDisplay = /[A-Z]{3}([scn]?)/;
3 | export const rgCurrencyFormat = /([A-Z]{3}[scn]?)/;
4 | export const rgDigitsFormat = /([0,.[\]]+)/;
5 | export const rgFractionDigits = /\.\]?([0-9[\]]*)/;
6 | export const rgFractionDigitsAreaOptional = /\[\.\]/;
7 | export const rgHasParentheses = /^\([^)]*\)/;
8 | export const rgIntegerDigits = /^([0-9]+)/;
9 | export const rgIntegerDigitsOnFormat = /^([0]+)/;
10 | export const rgIsCompact = /[aA]\)?$/;
11 | export const rgIsEngineering = /E\)?$/;
12 | export const rgIsLongCompactFormat = /A\)?$/;
13 | export const rgIsScientific = /e\)?$/;
14 | export const rgIsShortCompactFormat = /a\)?$/;
15 | export const rgNotationFormat = /[aAeE]\)?$/;
16 | export const rgOptionalDigits = /\[([0]+)\]/;
17 | export const rgOrdinalFormat = /o\)?$/;
18 | export const rgRequiredDigits = /([0]+)/;
19 | export const rgSpaceBetween = /[0\]]\s[o%bBmk]/;
20 | export const rgStartsWithPlus = /^\+/;
21 | export const rgZerosOnTheLeft = /^([0]+)/;
22 |
23 | // units systems
24 | export const rgBitSystem = /b$/;
25 | export const rgByteSystem = /B$/;
26 | export const rgDigitalSystem = /[bB]$/;
27 | export const rgKiloSystem = /k\)?$/;
28 | export const rgMetricSystem = /m\)?$/;
29 | export const rgUnitSystem = /[bBmik]\)?$/;
30 |
--------------------------------------------------------------------------------
/src/helpers/resolved-options.test.ts:
--------------------------------------------------------------------------------
1 | import { getResolvedOptions } from "./resolved-options";
2 |
3 | describe("getResolvedOptions", () => {
4 | it("should return resolved options", () => {
5 | const actual = getResolvedOptions();
6 | const expected = {
7 | locale: "en-GB",
8 | numberingSystem: "latn",
9 | };
10 | expect(actual).toStrictEqual(expected);
11 | });
12 |
13 | it("bases on user's locale", () => {
14 | const userOptions = { locale: "ar-EG" };
15 | const actual = getResolvedOptions(userOptions);
16 | const expected = {
17 | locale: "ar-EG",
18 | numberingSystem: "arab",
19 | };
20 | expect(actual).toStrictEqual(expected);
21 | });
22 |
23 | it("user options overrides system defaults", () => {
24 | const userOptions = {
25 | numberingSystem: "arab",
26 | locale: "id-ID",
27 | };
28 | const actual = getResolvedOptions(userOptions);
29 | expect(actual).toStrictEqual(userOptions);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/helpers/resolved-options.ts:
--------------------------------------------------------------------------------
1 | import type { INumberTimeResolvedOptions } from "src/numberfmt.d";
2 |
3 | export function getResolvedOptions(
4 | userOptions?: Partial
5 | ) {
6 | const { locale: userLocale } = userOptions || {};
7 | const { locale, numberingSystem } = {
8 | ...Intl.DateTimeFormat(userLocale).resolvedOptions(),
9 | ...userOptions,
10 | };
11 |
12 | return { locale, numberingSystem };
13 | }
14 |
--------------------------------------------------------------------------------
/src/helpers/value.test.ts:
--------------------------------------------------------------------------------
1 | import { getCompactPowersOfTwo, getMetricValue, getValue } from "./value";
2 |
3 | describe("calculates value based on format", () => {
4 | it.each([
5 | ["0", 1, 1],
6 | ["0.0", 1024, 1024],
7 | ["0b", 1, 1],
8 | ["0b", 1, 1024],
9 | ["0b", 1, 1048576],
10 | ["0b", 1, 1073741824],
11 | ])("value: %s = %s", (format, expected, value) => {
12 | const result = getValue(value, format);
13 | expect(result).toBe(expected);
14 | });
15 |
16 | it.each([
17 | ["meter", 1, 1],
18 | ["meter", 10, 10],
19 | ["meter", 999, 999],
20 | ["millimeter", 0.001, 1],
21 | ["centimeter", 0.01, 1],
22 | ["centimeter", 0.09, 9],
23 | ["centimeter", 0.095, 9.5],
24 | ["kilometer", 1000, 1],
25 | ["kilometer", 1200, 1.2],
26 | ])("get metric value: %s - %s", (_, value, expected) => {
27 | const result = getMetricValue(value);
28 | expect(result).toBe(expected);
29 | });
30 |
31 | it.each([
32 | [0, 0],
33 | [1, 1],
34 | [1024, 1],
35 | [1500, 1.46484375],
36 | [1048576, 1],
37 | [1278541824, 1.19073486328125],
38 | ])("get compact powers of two: %s", (value, expected) => {
39 | const result = getCompactPowersOfTwo(value);
40 | expect(result).toBe(expected);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/helpers/value.ts:
--------------------------------------------------------------------------------
1 | import { rgDigitalSystem, rgMetricSystem, rgKiloSystem } from "./regex";
2 |
3 | export function getCompactPowersOfTwo(value: number): number {
4 | let n = value;
5 | while (n >= 1024) {
6 | n /= 1024;
7 | }
8 | return n;
9 | }
10 |
11 | export function getMetricValue(value: number): number {
12 | const absValue = Math.abs(value);
13 |
14 | if (absValue > 999.9999) return value / 1000;
15 | if (absValue >= 0.01 && absValue <= 0.0999) return value * 100;
16 | if (absValue >= 0.001 && absValue <= 0.00999) return value * 1000;
17 |
18 | return value;
19 | }
20 |
21 | export function getKiloValue(value: number) {
22 | const absValue = Math.abs(value);
23 |
24 | if (absValue >= 0.001 && absValue <= 0.00999) return value * 1000;
25 |
26 | return value;
27 | }
28 |
29 | export function getValue(value: number, format: string) {
30 | if (rgDigitalSystem.test(format)) return getCompactPowersOfTwo(value);
31 | if (rgMetricSystem.test(format)) return getMetricValue(value);
32 | if (rgKiloSystem.test(format)) return getKiloValue(value);
33 |
34 | return value;
35 | }
36 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import nf from ".";
2 |
3 | describe("numberfmt", () => {
4 | it("has default format", () => {
5 | const result = nf(123_456);
6 | const expected = "123,456";
7 | expect(result).toBe(expected);
8 | });
9 |
10 | it.each([
11 | [undefined, ""],
12 | [null, ""],
13 | [{}, ""],
14 | [[], ""],
15 | [[1_234], "1,234"],
16 | [[1_234, 5_678], ""],
17 | ["abc", ""],
18 | ["1234", "1,234"],
19 | [1_234, "1,234"],
20 | ])("handles non numeric values: %s", (value, expected) => {
21 | const result = nf(value, "0,0");
22 | expect(result).toBe(expected);
23 | });
24 |
25 | it.each([
26 | ["en-GB", "123,456.79"],
27 | ["id-ID", "123.456,79"],
28 | ["ar-EG", "١٢٣٬٤٥٦٫٧٩"],
29 | ])("accepts a user locale: %s", (locale, expected) => {
30 | const result = nf(123_456.789, "0,0.00", { locale });
31 | expect(result).toBe(expected);
32 | });
33 |
34 | it("accepts a user locale (currencies): %s", () => {
35 | const result = nf(123_456.789, "0,0.00IDRs", { locale: "id-ID" });
36 | const expected = "Rp 123.456,79";
37 | expect(result).toBe(expected);
38 | });
39 |
40 | it("formats number with partial", () => {
41 | const value = 123_456.789;
42 | const fn = nf.partial("0,0.00");
43 | const actual = fn(value);
44 |
45 | const expected = nf(value, "0,0.00");
46 | expect(actual).toBe(expected);
47 | });
48 |
49 | it("setting locale with partial", () => {
50 | const value = 1234.5;
51 | const fn = nf.partial("0.0", { locale: "fr" });
52 | const actual = fn(value);
53 |
54 | const expected = nf(value, "0.0", { locale: "fr" });
55 | expect(actual).toBe(expected);
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { convertToNumber } from "./helpers/convert-to-number";
2 | import { getCompactDisplay } from "./options/compact-display";
3 | import { getCurrency } from "./options/currency";
4 | import { getCurrencyDisplay } from "./options/currency-display";
5 | import { getCurrencySign } from "./options/currency-sign";
6 | import { getMaximumFractionDigits } from "./options/maximum-fraction-digits";
7 | import { getMinimumFractionDigits } from "./options/minimum-fraction-digits";
8 | import { getMinimumIntegerDigits } from "./options/minimum-integer-digits";
9 | import { getNotation } from "./options/notation";
10 | import { getSignDisplay } from "./options/sign-display";
11 | import { getStyle } from "./options/style";
12 | import { getUnit } from "./options/unit";
13 | import { getUnitDisplay } from "./options/unit-display";
14 |
15 | import { formatOrdinal } from "./formats/ordinal";
16 | import { formatPercentage } from "./formats/percentage";
17 |
18 | import { getIntegerDigitsOnFormat } from "./helpers/integer-digits-on-format";
19 | import { getDigitsFormat } from "./helpers/digits-format";
20 | import {
21 | rgZerosOnTheLeft,
22 | rgOrdinalFormat,
23 | rgHasParentheses,
24 | } from "./helpers/regex";
25 | import { getValue } from "./helpers/value";
26 | import { getResolvedOptions } from "./helpers/resolved-options";
27 | import type { INumberTimeResolvedOptions } from "./numberfmt.d";
28 |
29 | function numberfmt(
30 | userInput: unknown,
31 | userFormat?: string,
32 | userOptions?: Partial
33 | ) {
34 | const userValue = convertToNumber(userInput);
35 | if (userValue === null) return "";
36 |
37 | // default format
38 | const format = userFormat || "0,0";
39 |
40 | const { locale, numberingSystem } = getResolvedOptions(userOptions);
41 | const digitsFormat = getDigitsFormat(format);
42 |
43 | // convert user value to computed values (bits, bytes, metric)
44 | const value = getValue(userValue, format);
45 |
46 | const options = {
47 | compactDisplay: getCompactDisplay(format),
48 | currency: getCurrency(format),
49 | currencyDisplay: getCurrencyDisplay(format),
50 | currencySign: getCurrencySign(format),
51 | maximumFractionDigits: getMaximumFractionDigits(value, format),
52 | minimumFractionDigits: getMinimumFractionDigits(value, format),
53 | minimumIntegerDigits: getMinimumIntegerDigits(value, format),
54 | notation: getNotation(format),
55 | numberingSystem,
56 | signDisplay: getSignDisplay(format),
57 | style: getStyle(format),
58 | useGrouping: digitsFormat.includes(","),
59 | unit: getUnit(userValue, format),
60 | unitDisplay: getUnitDisplay(format),
61 | };
62 |
63 | const nf = new Intl.NumberFormat(locale, options);
64 | let n = nf.format(value);
65 |
66 | // remove zeros from '0.12'
67 | if (getIntegerDigitsOnFormat(digitsFormat) === 0) {
68 | n = n.replace(rgZerosOnTheLeft, "");
69 | }
70 | // percentage with spaces before symbol
71 | if (format.includes("%")) {
72 | n = formatPercentage({ value, numberFormatter: nf, format });
73 | }
74 | // ordinals
75 | if (rgOrdinalFormat.test(format)) {
76 | n = formatOrdinal({ value, formatted: n, format, locale });
77 | }
78 | // add parentheses when not currency
79 | if (rgHasParentheses.test(format) && value < 0) {
80 | n = `(${n})`;
81 | }
82 |
83 | return n;
84 | }
85 |
86 | // Partial application for functional programming
87 | numberfmt.partial =
88 | (format: string, options?: Partial) =>
89 | (userValue: number) =>
90 | numberfmt(userValue, format, options);
91 |
92 | export default numberfmt;
93 |
--------------------------------------------------------------------------------
/src/numberfmt.d.ts:
--------------------------------------------------------------------------------
1 | export interface INumberTimeResolvedOptions {
2 | locale: string;
3 | numberingSystem: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/options/compact-display.test.ts:
--------------------------------------------------------------------------------
1 | import { getCompactDisplay } from "./compact-display";
2 |
3 | describe("compact display", () => {
4 | it.each([
5 | ["0.000", undefined],
6 | ["0.0a", "short"],
7 | ["0.0A", "long"],
8 | ])("compact display: %s", (format, expected) => {
9 | const result = getCompactDisplay(format);
10 | expect(result).toBe(expected);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/options/compact-display.ts:
--------------------------------------------------------------------------------
1 | import {
2 | rgIsShortCompactFormat,
3 | rgIsLongCompactFormat,
4 | } from "../helpers/regex";
5 |
6 | type CompactDisplay = "short" | "long" | undefined;
7 |
8 | export function getCompactDisplay(format: string): CompactDisplay {
9 | if (rgIsShortCompactFormat.test(format)) return "short";
10 | if (rgIsLongCompactFormat.test(format)) return "long";
11 | return undefined;
12 | }
13 |
--------------------------------------------------------------------------------
/src/options/currency-display.test.ts:
--------------------------------------------------------------------------------
1 | import { getCurrencyDisplay } from "./currency-display";
2 |
3 | describe("currency display", () => {
4 | it.each([
5 | ["0GBP", "symbol"],
6 | ["0GBPs", "narrowSymbol"],
7 | ["0GBPc", "code"],
8 | ["0GBPn", "name"],
9 | ])("compact display - %s", (format, expected) => {
10 | const result = getCurrencyDisplay(format);
11 | expect(result).toBe(expected);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/options/currency-display.ts:
--------------------------------------------------------------------------------
1 | import { rgCurrencyDisplay } from "../helpers/regex";
2 |
3 | type CurrencyDisplay = "symbol" | "narrowSymbol" | "code" | "name";
4 |
5 | export function getCurrencyDisplay(format: string): CurrencyDisplay {
6 | const [, display] = format.match(rgCurrencyDisplay) || [""];
7 | switch (display) {
8 | case "s":
9 | return "narrowSymbol";
10 | case "c":
11 | return "code";
12 | case "n":
13 | return "name";
14 | default:
15 | return "symbol";
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/options/currency-sign.test.ts:
--------------------------------------------------------------------------------
1 | import { getCurrencySign } from "./currency-sign";
2 |
3 | describe("currency sign", () => {
4 | it.each([
5 | ["0.0", "standard"],
6 | ["(0.0)", "accounting"],
7 | ])("currency sign: %s = %s", (format, expected) => {
8 | const result = getCurrencySign(format);
9 | expect(result).toBe(expected);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/options/currency-sign.ts:
--------------------------------------------------------------------------------
1 | import { rgHasParentheses } from "../helpers/regex";
2 |
3 | type CurrencySign = "standard" | "accounting";
4 |
5 | export function getCurrencySign(format: string): CurrencySign {
6 | return rgHasParentheses.test(format) ? "accounting" : "standard";
7 | }
8 |
--------------------------------------------------------------------------------
/src/options/currency.test.ts:
--------------------------------------------------------------------------------
1 | import { getCurrency } from "./currency";
2 |
3 | describe("currency", () => {
4 | it.each([
5 | ["0.0", undefined],
6 | ["0.0gbp", undefined],
7 | ["0.0£", undefined],
8 | ["0.0A", undefined],
9 | ["0.0GBP", "GBP"],
10 | ["0.0[0]GBP", "GBP"],
11 | ["0,000.0[0]GBP", "GBP"],
12 | ["0GBPa", "GBP"],
13 | ["0GBPs", "GBP"],
14 | ["0GBPn", "GBP"],
15 | ["0GBPn", "GBP"],
16 | ["0EUR", "EUR"],
17 | ])("currency: %s = %s", (format, expected) => {
18 | const result = getCurrency(format);
19 | expect(result).toBe(expected);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/options/currency.ts:
--------------------------------------------------------------------------------
1 | import { rgCurrency } from "../helpers/regex";
2 |
3 | export function getCurrency(format: string) {
4 | const [, currency] = format.match(rgCurrency) || [""];
5 | return currency || undefined;
6 | }
7 |
--------------------------------------------------------------------------------
/src/options/maximum-fraction-digits.test.ts:
--------------------------------------------------------------------------------
1 | import { getMaximumFractionDigits } from "./maximum-fraction-digits";
2 |
3 | describe("maximum fraction digits", () => {
4 | it.each([
5 | [".0", 1, 1],
6 | ["0.0", 1, 1],
7 | [".00", 1, 2],
8 | ["0.00", 1, 2],
9 | ["0[.]000", 1, 3],
10 | ["0[.]00", 1.001, 2],
11 | ["0[.]00[0]", 1.23, 3],
12 | ["0[.]00[0]", 1.234, 3],
13 | ])("gets maximum fraction digits - %s", (format, value, expected) => {
14 | const result = getMaximumFractionDigits(value, format);
15 | expect(result).toBe(expected);
16 | });
17 |
18 | it.each([
19 | ["0b", 1_200, 2],
20 | ["0B", 1_200, 2],
21 | ["0m", 1_200, 2],
22 | ])("fraction digits on unit systems: %s", (format, value, expected) => {
23 | const result = getMaximumFractionDigits(value, format);
24 | expect(result).toBe(expected);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/options/maximum-fraction-digits.ts:
--------------------------------------------------------------------------------
1 | import { getDigitsFormat } from "../helpers/digits-format";
2 | import { getFractionDigits } from "../helpers/fraction-digits";
3 | import { rgUnitSystem } from "../helpers/regex";
4 |
5 | export function getMaximumFractionDigits(
6 | value: number,
7 | format: string
8 | ): number {
9 | const digitsFormat = getDigitsFormat(format);
10 | const [required, optional] = getFractionDigits(digitsFormat);
11 |
12 | if (rgUnitSystem.test(format)) {
13 | return Math.max(required + optional, 2);
14 | }
15 |
16 | return required + optional;
17 | }
18 |
--------------------------------------------------------------------------------
/src/options/minimum-fraction-digits.test.ts:
--------------------------------------------------------------------------------
1 | import { getMinimumFractionDigits } from "./minimum-fraction-digits";
2 |
3 | describe("minimum fraction digits", () => {
4 | it.each([
5 | [".0", 1, 1],
6 | ["0.0", 1, 1],
7 | [".00", 1, 2],
8 | ["0.00", 1, 2],
9 | ["0[.]000", 1, 0],
10 | ["0[.]000", 1.2, 3],
11 | ["0[.]000", 1.23, 3],
12 | ["0[.]00", 1.001, 2],
13 | ["0.00[0]", 1.23, 2],
14 | ["0.00[0]", 1.234, 2],
15 | ["0[.]0[0]", 1, 1],
16 | ["0[.]0[0]", 1.2, 1],
17 | ["0[.]0[0]", 1.23, 1],
18 | ["0[.]00[0]", 1.234, 2],
19 | ])("gets minimum fraction digits: %s", (format, value, expected) => {
20 | const actual = getMinimumFractionDigits(value, format);
21 | expect(actual).toBe(expected);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/options/minimum-fraction-digits.ts:
--------------------------------------------------------------------------------
1 | import { getDigitsFormat } from "../helpers/digits-format";
2 | import { getFractionDigits } from "../helpers/fraction-digits";
3 | import {
4 | rgFractionDigitsAreaOptional,
5 | rgOptionalDigits,
6 | } from "../helpers/regex";
7 |
8 | function handleOptionalDigits(value: number, digits: [number, number]) {
9 | const [required] = digits;
10 |
11 | return required;
12 | }
13 |
14 | export function getMinimumFractionDigits(value: number, format: string) {
15 | const digitsFormat = getDigitsFormat(format);
16 | const [required, optional] = getFractionDigits(digitsFormat);
17 |
18 | if (rgFractionDigitsAreaOptional.test(digitsFormat)) {
19 | const strNumber = value.toString();
20 | if (rgOptionalDigits.test(digitsFormat)) {
21 | return handleOptionalDigits(value, [required, optional]);
22 | }
23 |
24 | return strNumber.indexOf(".") > -1 ? required : 0;
25 | }
26 |
27 | return rgOptionalDigits.test(digitsFormat) ? required : required + optional;
28 | }
29 |
--------------------------------------------------------------------------------
/src/options/minimum-integer-digits.test.ts:
--------------------------------------------------------------------------------
1 | import { getMinimumIntegerDigits } from "./minimum-integer-digits";
2 |
3 | describe("integer digits configuration", () => {
4 | it.each([
5 | ["0", 1, 1],
6 | ["0.0", 1, 1],
7 | ["00", 2, 1],
8 | ["00000", 5, 1],
9 | ["000", 5, 10_000],
10 | ["0,0", undefined, 1],
11 | [".00", undefined, 0.23],
12 | [".00", 4, 1_000.23],
13 | ["0a", undefined, 1],
14 | ["0A", undefined, 1],
15 | ["0e", undefined, 1_000],
16 | ["0E", undefined, 1_000],
17 | ])("gets minimum integer digits: %s = %s", (format, expected, value) => {
18 | const result = getMinimumIntegerDigits(value, format);
19 | expect(result).toBe(expected);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/options/minimum-integer-digits.ts:
--------------------------------------------------------------------------------
1 | import {
2 | rgIntegerDigits,
3 | rgNotationFormat,
4 | rgZerosOnTheLeft,
5 | } from "../helpers/regex";
6 | import { getIntegerDigitsOnFormat } from "../helpers/integer-digits-on-format";
7 | import { getDigitsFormat } from "../helpers/digits-format";
8 |
9 | export function getMinimumIntegerDigits(value: number, format: string) {
10 | const digitsFormat = getDigitsFormat(format);
11 | if (digitsFormat === "0,000") return undefined;
12 | if (rgNotationFormat.test(format || "")) return undefined;
13 |
14 | const integersInFormat = getIntegerDigitsOnFormat(digitsFormat);
15 | const strValue = value.toString();
16 | const [integersInValue] = rgIntegerDigits.exec(strValue) || [""];
17 | const [zerosInLeft] = rgZerosOnTheLeft.exec(strValue) || [""];
18 |
19 | return (
20 | Math.max(integersInFormat, integersInValue.length - zerosInLeft.length) ||
21 | undefined
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/options/notation.test.ts:
--------------------------------------------------------------------------------
1 | import { getNotation } from "./notation";
2 |
3 | describe("notation", () => {
4 | it.each([
5 | ["0.0", "standard"],
6 | ["0.0", "standard"],
7 | ["0.0a", "compact"],
8 | ["0.0A", "compact"],
9 | ["(0.0A)", "compact"],
10 | ["0.0e", "scientific"],
11 | ["(0.0e)", "scientific"],
12 | ["0.0E", "engineering"],
13 | ["(0.0E)", "engineering"],
14 | ])("notation: %s = %s", (format, expected) => {
15 | const result = getNotation(format);
16 | expect(result).toBe(expected);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/options/notation.ts:
--------------------------------------------------------------------------------
1 | import { rgIsCompact, rgIsScientific, rgIsEngineering } from "../helpers/regex";
2 |
3 | type INotation = "standard" | "scientific" | "engineering" | "compact";
4 |
5 | export function getNotation(format: string): INotation {
6 | if (rgIsCompact.test(format)) return "compact";
7 | if (rgIsScientific.test(format)) return "scientific";
8 | if (rgIsEngineering.test(format)) return "engineering";
9 |
10 | return "standard";
11 | }
12 |
--------------------------------------------------------------------------------
/src/options/sign-display.test.ts:
--------------------------------------------------------------------------------
1 | import { getSignDisplay } from "./sign-display";
2 |
3 | describe("format sign display", () => {
4 | it.each([
5 | ["0,0", "auto"],
6 | ["+0,0", "exceptZero"],
7 | ["-0,0", "auto"],
8 | ["(0)", "never"],
9 | ["0,0.0", "auto"],
10 | ["+0,0.0", "exceptZero"],
11 | ["-0,0.0", "auto"],
12 | ["(0,0.0)", "never"],
13 | ])("sign display: %s = %s", (format, expected) => {
14 | const result = getSignDisplay(format);
15 | expect(result).toBe(expected);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/options/sign-display.ts:
--------------------------------------------------------------------------------
1 | import { rgHasParentheses, rgStartsWithPlus } from "../helpers/regex";
2 |
3 | type ISignDisplay = "auto" | "never" | "exceptZero";
4 |
5 | export function getSignDisplay(format: string): ISignDisplay | undefined {
6 | if (rgHasParentheses.test(format)) return "never";
7 | if (rgStartsWithPlus.test(format)) return "exceptZero";
8 | return "auto";
9 | }
10 |
--------------------------------------------------------------------------------
/src/options/style.test.ts:
--------------------------------------------------------------------------------
1 | import { getStyle } from "./style";
2 |
3 | describe("get style", () => {
4 | it.each([
5 | ["0.0", "decimal"],
6 | ["0.0GBP", "currency"],
7 | ["0.0GBPs", "currency"],
8 | ["0.0GBPc", "currency"],
9 | ["0.0GBPn", "currency"],
10 | ["0.0%", "percent"],
11 | ])("style: $s = $s", (format, expected) => {
12 | const result = getStyle(format);
13 | expect(result).toBe(expected);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/options/style.ts:
--------------------------------------------------------------------------------
1 | import { rgCurrencyFormat, rgUnitSystem } from "../helpers/regex";
2 |
3 | type IStyle = "decimal" | "currency" | "percent" | "unit";
4 |
5 | export function getStyle(format: string): IStyle {
6 | if (rgCurrencyFormat.test(format)) return "currency";
7 | if (rgUnitSystem.test(format)) return "unit";
8 | if (format.includes("%")) return "percent";
9 |
10 | return "decimal";
11 | }
12 |
--------------------------------------------------------------------------------
/src/options/unit-display.test.ts:
--------------------------------------------------------------------------------
1 | import { getUnitDisplay } from "./unit-display";
2 |
3 | describe("unit display", () => {
4 | it.each([
5 | ["0.0", "short"],
6 | ["0b", "narrow"],
7 | ["0 b", "short"],
8 | ["0B", "narrow"],
9 | ["0 B", "short"],
10 | ["0 k", "short"],
11 | ["0k", "narrow"],
12 | ])("unit display: % = %s", (format, expected) => {
13 | const result = getUnitDisplay(format);
14 | expect(result).toBe(expected);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/options/unit-display.ts:
--------------------------------------------------------------------------------
1 | import { rgUnitSystem, rgSpaceBetween } from "../helpers/regex";
2 |
3 | type IUnitDisplay = "short" | "narrow" | "long";
4 |
5 | export function getUnitDisplay(format: string): IUnitDisplay {
6 | if (rgUnitSystem.test(format) && !rgSpaceBetween.test(format)) {
7 | return "narrow";
8 | }
9 |
10 | return "short";
11 | }
12 |
--------------------------------------------------------------------------------
/src/options/unit.test.ts:
--------------------------------------------------------------------------------
1 | import { getUnitFromPowerOfTwo, getMetricUnit, getUnit } from "./unit";
2 |
3 | describe("get unit", () => {
4 | it.each([
5 | ["bit", 1],
6 | ["kilobit", 1_024],
7 | ["kilobit", 1_024 + 100],
8 | ["kilobit", 1_024 * 1_024 - 100],
9 | ["megabit", 1_024 * 1_024],
10 | ["gigabit", 1_024 * 1_024 * 1_024],
11 | ["terabit", 1_024 * 1_024 * 1_024 * 1_024],
12 | ])("unit for power of twos (bits): %s - %s", (expected, value) => {
13 | const result = getUnitFromPowerOfTwo(value, [
14 | "bit",
15 | "kilobit",
16 | "megabit",
17 | "gigabit",
18 | "terabit",
19 | ]);
20 | expect(result).toBe(expected);
21 | });
22 |
23 | it.each([
24 | ["byte", 1],
25 | ["kilobyte", 1_024],
26 | ["kilobyte", 1_024 + 100],
27 | ["kilobyte", 1_024 * 1_024 - 100],
28 | ["megabyte", 1_024 * 1_024],
29 | ["gigabyte", 1_024 * 1_024 * 1_024],
30 | ["terabyte", 1_024 * 1_024 * 1_024 * 1_024],
31 | ["petabyte", 1_024 * 1_024 * 1_024 * 1_024 * 1_024],
32 | ])("unit for power of twos (bits): %s - %s", (expected, value) => {
33 | const result = getUnitFromPowerOfTwo(value, [
34 | "byte",
35 | "kilobyte",
36 | "megabyte",
37 | "gigabyte",
38 | "terabyte",
39 | "petabyte",
40 | ]);
41 | expect(result).toBe(expected);
42 | });
43 |
44 | it.each([
45 | ["millimeter", 0.001],
46 | ["centimeter", 0.01],
47 | ["meter", 1],
48 | ["kilometer", 1_000],
49 | ])("metric value: %s - %s", (expected, value) => {
50 | const result = getMetricUnit(value);
51 | expect(result).toBe(expected);
52 | });
53 |
54 | it.each([
55 | ["0b", "bit", 1],
56 | ["0b", "kilobit", 1_024],
57 | ["0B", "byte", 1],
58 | ["0B", "kilobyte", 1_024],
59 | ])("unit: %s = %s", (format, expected, value) => {
60 | const result = getUnit(value, format);
61 | expect(result).toBe(expected);
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/src/options/unit.ts:
--------------------------------------------------------------------------------
1 | import {
2 | rgUnitSystem,
3 | rgBitSystem,
4 | rgByteSystem,
5 | rgMetricSystem,
6 | rgKiloSystem,
7 | } from "../helpers/regex";
8 |
9 | type IBit = "bit" | "kilobit" | "megabit" | "gigabit" | "terabit";
10 |
11 | type IByte =
12 | | "byte"
13 | | "kilobyte"
14 | | "megabyte"
15 | | "gigabyte"
16 | | "terabyte"
17 | | "petabyte";
18 |
19 | type IMetric = "millimeter" | "centimeter" | "meter" | "kilometer";
20 |
21 | type IKilo = "gram" | "kilogram";
22 |
23 | type IUnit = IBit | IByte | IMetric | IKilo;
24 |
25 | export function getUnitFromPowerOfTwo(
26 | value: number,
27 | units: (IBit | IByte)[]
28 | ): IBit | IByte {
29 | const idx = Math.floor(Math.log(value) / Math.log(1024));
30 | return units[idx];
31 | }
32 |
33 | export function getMetricUnit(value: number): IMetric {
34 | const absValue = Math.abs(value);
35 |
36 | if (absValue > 999.9999) return "kilometer";
37 | if (absValue >= 0.01 && absValue <= 0.0999) return "centimeter";
38 | if (absValue >= 0.001 && absValue <= 0.00999) return "millimeter";
39 |
40 | return "meter";
41 | }
42 |
43 | export function getKiloUnit(value: number): IKilo {
44 | const absValue = Math.abs(value);
45 |
46 | if (absValue >= 0.001 && absValue <= 0.00999) return "gram";
47 |
48 | return "kilogram";
49 | }
50 |
51 | export function getUnit(value: number, format: string): IUnit | undefined {
52 | if (!rgUnitSystem.test(format)) return undefined;
53 |
54 | if (rgMetricSystem.test(format)) return getMetricUnit(value);
55 | if (rgKiloSystem.test(format)) return getKiloUnit(value);
56 |
57 | if (rgBitSystem.test(format))
58 | return getUnitFromPowerOfTwo(value, [
59 | "bit",
60 | "kilobit",
61 | "megabit",
62 | "gigabit",
63 | "terabit",
64 | ]);
65 |
66 | if (rgByteSystem.test(format))
67 | return getUnitFromPowerOfTwo(value, [
68 | "byte",
69 | "kilobyte",
70 | "megabyte",
71 | "gigabyte",
72 | "terabyte",
73 | "petabyte",
74 | ]);
75 |
76 | return undefined;
77 | }
78 |
--------------------------------------------------------------------------------
/src/tests/compact.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("compact notation", () => {
4 | it.each([
5 | ["0a", "123", 123],
6 | ["0a", "1K", 1_234],
7 | ["0a", "12K", 12_345],
8 | ["0a", "123K", 123_456],
9 | ["0a", "1M", 1_234_567],
10 | ["0a", "12M", 12_345_678],
11 | ["0a", "123M", 123_456_789],
12 | ["0a", "1B", 1_234_567_890],
13 | ["0a", "12B", 12_345_678_901],
14 | ["0a", "123B", 123_456_789_012],
15 | ["0a", "1T", 1_234_567_890_123],
16 | ["0a", "12T", 12_345_678_901_234],
17 | ["0a", "123T", 123_456_789_012_345],
18 | ["0.0a", "1.2M", 1_234_567],
19 | ["0.00a", "1.23M", 1_234_567],
20 | ["0.000a", "1.235M", 1_234_567],
21 | ["0.000a", "1.235M", 1_234_567],
22 | ])("short format: %s, %s", (format, expected, value) => {
23 | const result = nf(value, format);
24 | expect(result).toBe(expected);
25 | });
26 |
27 | it.each([
28 | ["0A", "1", 1],
29 | ["0A", "123", 123],
30 | ["0A", "1 thousand", 1_234],
31 | ["0A", "12 thousand", 12_345],
32 | ["0A", "123 thousand", 123_456],
33 | ["0A", "1 million", 1_234_567],
34 | ["0A", "12 million", 12_345_678],
35 | ["0A", "123 million", 123_456_789],
36 | ["0A", "1 billion", 1_234_567_890],
37 | ["0A", "12 billion", 12_345_678_901],
38 | ["0A", "123 billion", 123_456_789_012],
39 | ["0A", "1 trillion", 1_234_567_890_123],
40 | ["0A", "12 trillion", 12_345_678_901_234],
41 | ["0A", "123 trillion", 123_456_789_012_345],
42 | ["0.0A", "1.2 million", 1_234_567],
43 | ["0.00A", "1.23 million", 1_234_567],
44 | ])("long format: %s, %s", (format, expected, value) => {
45 | const result = nf(value, format);
46 | expect(result).toBe(expected);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/tests/compound.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("compound format", () => {
4 | it.each([
5 | ["(0m)", "(1.23km)", -1_234],
6 | ["(0.000m)", "(1.234km)", -1_234],
7 | ])("compounds: %s = %s", (format, expected, value) => {
8 | const result = nf(value, format);
9 | expect(result).toBe(expected);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/tests/currency.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("currency", () => {
4 | it.each([
5 | ["0,0GBP", "£1,000", 1_000],
6 | ["GBP", "£1,000", 1_000],
7 | ["0,0USD", "US$1,000", 1_000],
8 | ["0,0EUR", "€1,000", 1_000],
9 | ["0,0JPY", "JP¥1,000", 1_000],
10 | ["0,0CAD", "CA$1,000", 1_000],
11 | ["0,0GBP", "£123,456", 123_456],
12 | ])("currency (symbol): %s = %s", (format, expected, value) => {
13 | const result = nf(value, format);
14 | expect(result).toBe(expected);
15 | });
16 |
17 | it.each([
18 | ["0,0GBPs", "£1,000", 1_000],
19 | ["0,0USDs", "$1,000", 1_000],
20 | ["0,0EURs", "€1,000", 1_000],
21 | ["0,0JPYs", "¥1,000", 1_000],
22 | ["0,0CADs", "$1,000", 1_000],
23 | ])("currency (narrow symbol): %s = %s", (format, expected, value) => {
24 | const result = nf(value, format);
25 | expect(result).toBe(expected);
26 | });
27 |
28 | it.each([
29 | ["0,0GBPc", "GBP 1,000", 1_000],
30 | ["0,0USDc", "USD 1,000", 1_000],
31 | ["0,0EURc", "EUR 1,000", 1_000],
32 | ["0,0JPYc", "JPY 1,000", 1_000],
33 | ["0,0CADc", "CAD 1,000", 1_000],
34 | ])("currency (code): %s = %s", (format, expected, value) => {
35 | const result = nf(value, format);
36 | expect(result).toBe(expected);
37 | });
38 |
39 | it.each([
40 | ["0,0GBPn", "1 British pound", 1],
41 | ["0,0USDn", "1 US dollar", 1],
42 | ["0,0EURn", "1 euro", 1],
43 | ["0,0JPYn", "1 Japanese yen", 1],
44 | ["0,0CADn", "1 Canadian dollar", 1],
45 | ["0,0GBPn", "1,000 British pounds", 1_000],
46 | ["0,0USDn", "1,000 US dollars", 1_000],
47 | ["0,0EURn", "1,000 euros", 1_000],
48 | ["0,0JPYn", "1,000 Japanese yen", 1_000],
49 | ["0,0CADn", "1,000 Canadian dollars", 1_000],
50 | ])("currency (name): %s = %s", (format, expected, value) => {
51 | const result = nf(value, format);
52 | expect(result).toBe(expected);
53 | });
54 |
55 | it.each([
56 | ["0,0GBPa", "£1M", 1_234_567],
57 | ["0,0GBPsa", "£1M", 1_234_567],
58 | ["0,0GBPca", "GBP 1M", 1_234_567],
59 | ["0,0GBPna", "1M British pounds", 1_234_567],
60 | ])("currency: %s = %s", (format, expected, value) => {
61 | const result = nf(value, format);
62 | expect(result).toBe(expected);
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/src/tests/digital.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("digital", () => {
4 | it.each([
5 | ["0b", "1bit", 1],
6 | ["0b", "1kb", 1_024],
7 | ["0b", "1.49kb", 1_024 + 500],
8 | ["0b", "1.02kb", 1_024 + 20],
9 | ["0b", "23.39kb", 1_024 * 23 + 400],
10 | ["0b", "1Mb", 1_024 * 1_024],
11 | ["0b", "1Gb", 1_024 * 1_024 * 1_024],
12 | ["0b", "1Tb", 1_024 * 1_024 * 1_024 * 1_024],
13 | ["0 b", "1 bit", 1],
14 | ["0 b", "1 kb", 1_024],
15 | ["0 b", "1.49 kb", 1_024 + 500],
16 | ["0 b", "1.02 kb", 1_024 + 20],
17 | ["0 b", "23.39 kb", 1_024 * 23 + 400],
18 | ["0 b", "1 Mb", 1_024 * 1_024],
19 | ["0 b", "1 Gb", 1_024 * 1_024 * 1_024],
20 | ["0 b", "1 Tb", 1_024 * 1_024 * 1_024 * 1_024],
21 | ["0b", "120.56kb", 123456],
22 | ])("bits: %s = %s", (format, expected, value) => {
23 | const result = nf(value, format);
24 | expect(result).toBe(expected);
25 | });
26 |
27 | it.each([
28 | ["0B", "1B", 1],
29 | ["0B", "1kB", 1_024],
30 | ["0B", "1.49kB", 1_024 + 500],
31 | ["0B", "1.02kB", 1_024 + 20],
32 | ["0B", "23.39kB", 1_024 * 23 + 400],
33 | ["0B", "1MB", 1_024 * 1_024],
34 | ["0B", "1GB", 1_024 * 1_024 * 1_024],
35 | ["0B", "1TB", 1_024 * 1_024 * 1_024 * 1_024],
36 | ["0 B", "1 byte", 1],
37 | ["0 B", "1 kB", 1_024],
38 | ["0 B", "1.49 kB", 1_024 + 500],
39 | ["0 B", "1.02 kB", 1_024 + 20],
40 | ["0 B", "23.39 kB", 1_024 * 23 + 400],
41 | ["0 B", "1 MB", 1_024 * 1_024],
42 | ["0 B", "1 GB", 1_024 * 1_024 * 1_024],
43 | ["0 B", "1 TB", 1_024 * 1_024 * 1_024 * 1_024],
44 | ])("bytes: %s = %s", (format, expected, value) => {
45 | const result = nf(value, format);
46 | expect(result).toBe(expected);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/tests/exponential.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("exponential", () => {
4 | it.each([
5 | ["0e", "1E0", 1],
6 | ["0e", "1E1", 10],
7 | ["0e", "1E2", 100],
8 | ["0e", "1E3", 1_000],
9 | ["0e", "1E4", 10_000],
10 | ["0e", "1E6", 1_000_000],
11 | ["0.0e", "1.2E7", 12_345_678],
12 | ])("exponential (scientific): %s = %s", (format, expected, value) => {
13 | const result = nf(value, format);
14 | expect(result).toBe(expected);
15 | });
16 |
17 | it.each([
18 | ["0E", "1E0", 1],
19 | ["0E", "10E0", 10],
20 | ["0E", "100E0", 100],
21 | ["0E", "1E3", 1_000],
22 | ["0E", "10E3", 10_000],
23 | ["0E", "100E3", 100_000],
24 | ["0E", "1E6", 1_000_000],
25 | ["0.0E", "12.3E6", 12_345_678],
26 | ])("exponential (engineering): %s = %s", (format, expected, value) => {
27 | const result = nf(value, format);
28 | expect(result).toBe(expected);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/tests/length.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("length", () => {
4 | it.each([
5 | ["0m", "1mm", 0.001],
6 | ["0m", "1cm", 0.01],
7 | ["0m", "1m", 1],
8 | ["0m", "1km", 1_000],
9 | ["0m", "1.2km", 1_200],
10 | ])("metric (narrow): %s = %s", (format, expected, value) => {
11 | const result = nf(value, format);
12 | expect(result).toBe(expected);
13 | });
14 |
15 | it.each([
16 | ["0 m", "1 mm", 0.001],
17 | ["0 m", "1 cm", 0.01],
18 | ["0 m", "1 m", 1],
19 | ["0 m", "1 km", 1_000],
20 | ])("metric (short): %s = %s", (format, expected, value) => {
21 | const result = nf(value, format);
22 | expect(result).toBe(expected);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/tests/mass.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("mass", () => {
4 | it.each([
5 | ["0k", "1kg", 1],
6 | ["0k", "1.23kg", 1.23],
7 | ["0k", "1g", 0.001],
8 | ["0 k", "1 kg", 1],
9 | ["0 k", "1 g", 0.001],
10 | ])("mass: %s = %s", (format, expected, value) => {
11 | const result = nf(value, format);
12 | expect(result).toBe(expected);
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/tests/numeric.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("numeric formatting", () => {
4 | it.each([
5 | ["0.0", 1, "1.0"],
6 | ["0.00", 1.23, "1.23"],
7 | ["0.00", 1.2345, "1.23"],
8 | ["0.00", 1.2, "1.20"],
9 | ["0.00000", 0.23, "0.23000"],
10 | ["0[.]00", 1.23, "1.23"],
11 | ["0[.]000", 1.23, "1.230"],
12 | ["0[.]0000", 1, "1"],
13 | ["0.00", -1.234, "-1.23"],
14 | ["0[.]00", -1, "-1"],
15 | ["0[.]00", -1.234, "-1.23"],
16 | ["0[.]00", 10_000.1, "10000.10"],
17 | ["0[.]00", 10_000.123, "10000.12"],
18 | ["0[.]00", 10_000.456, "10000.46"],
19 | ["0[.]00", 10_000.001, "10000.00"],
20 | ["0[.]00[0]", 10_000.45, "10000.45"],
21 | ["0[.]00[0]", 10_000.451, "10000.451"],
22 | ["0[.]00[0]", 10_000.456, "10000.456"],
23 | ["0[.]00[00]", 10_000.234, "10000.234"],
24 | ["0[.]00[00]", 10_000.2345, "10000.2345"],
25 | ["0,0.00", 123_456, "123,456.00"],
26 | ])("formats fraction digits: %s", (format, value, expected) => {
27 | const result = nf(value, format);
28 | expect(result).toBe(expected);
29 | });
30 |
31 | it.each([
32 | ["0", 1_000, "1000"],
33 | ["0.0", 1, "1.0"],
34 | ["0,0", 1_000, "1,000"],
35 | ["000", 1, "001"],
36 | ["000", 10_000, "10000"],
37 | [".00", 0.23, ".23"],
38 | [".00", 1.23, "1.23"],
39 | [".00", 1_000.23, "1000.23"],
40 | [".0", 0, ".0"],
41 | ])("formats integer digits: %s", (format, value, expected) => {
42 | const result = nf(value, format);
43 | expect(result).toBe(expected);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/src/tests/ordinal.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("ordinals", () => {
4 | it.each([
5 | ["0o", "0th", 0],
6 | ["0o", "1st", 1],
7 | ["0o", "2nd", 2],
8 | ["0o", "3rd", 3],
9 | ["0o", "4th", 4],
10 | ["0,0.000o", "1,234.560th", 1_234.56],
11 | ["0 o", "0 th", 0],
12 | ["0 o", "1 st", 1],
13 | ["0 o", "2 nd", 2],
14 | ["0 o", "3 rd", 3],
15 | ["0 o", "4 th", 4],
16 | ["0,0.000 o", "1,234.560 th", 1_234.56],
17 | ])("ordinals: %s = %s", (format, expected, value) => {
18 | const result = nf(value, format);
19 | expect(result).toBe(expected);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/tests/partial-application.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "../index";
2 |
3 | describe("partial applications", () => {
4 | it.each([
5 | [2, "0.000", "2.000"],
6 | [3_000, "0,000", "3,000"],
7 | [5_000, "0B", "4.88kB"],
8 | ])("partially applies format, then values: %s", (value, format, expected) => {
9 | const fmt = nf.partial(format);
10 | const result = fmt(value);
11 | expect(result).toBe(expected);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/tests/percentage.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("percentage", () => {
4 | it.each([
5 | ["0%", "100%", 1],
6 | ["0.000%", "97.488%", 0.974878234],
7 | ["0%", "-43%", -0.43],
8 | ["(0.000%)", "43.000%", 0.43],
9 | ["0 %", "100 %", 1],
10 | ["(0.000 %)", "43.000 %", 0.43],
11 | ])("percentage: %s = %s", (format, expected, value) => {
12 | const result = nf(value, format);
13 | expect(result).toBe(expected);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/tests/sign.test.ts:
--------------------------------------------------------------------------------
1 | import nf from "..";
2 |
3 | describe("signal display", () => {
4 | it.each([
5 | ["+0", 0, "0"],
6 | ["+0", 1, "+1"],
7 | ["+0", -1, "-1"],
8 | ["-0", 0, "0"],
9 | ["-0", 1, "1"],
10 | ["-0", -1, "-1"],
11 | ["(0)", 0, "0"],
12 | ["(0)", 1, "1"],
13 | ["(0)", -1, "(1)"],
14 | ["+0,0", 1_000, "+1,000"],
15 | ["+0,0", -1_000, "-1,000"],
16 | ["-0,0", 1_000, "1,000"],
17 | ["-0,0", -1_000, "-1,000"],
18 | ["(0,0)", 1_000, "1,000"],
19 | ["(0,0)", -1_000, "(1,000)"],
20 | ["+0.00", 1.23, "+1.23"],
21 | ["+0.00", -1.23, "-1.23"],
22 | ["-0.00", 1.23, "1.23"],
23 | ["-0.00", -1.23, "-1.23"],
24 | ["(0.00)", 1.23, "1.23"],
25 | ["(0.00)", -1.23, "(1.23)"],
26 | ])("sign display: %s", (format, value, expected) => {
27 | const result = nf(value, format);
28 | expect(result).toBe(expected);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "exclude": ["node_modules/**/*", "**/*.test.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "declaration": true,
5 | "esModuleInterop": true,
6 | "lib": ["DOM"],
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "outDir": "./dist",
10 | "removeComments": true,
11 | "skipLibCheck": true,
12 | "strict": true,
13 | "target": "es6",
14 | "types": ["vitest/globals"]
15 | },
16 | "include": ["src/**/*"],
17 | "exclude": ["node_modules/**/*"]
18 | }
19 |
--------------------------------------------------------------------------------
/vite.config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 | import path from "node:path";
3 |
4 | export default defineConfig({
5 | test: {
6 | globals: true,
7 | coverage: {
8 | reporter: ["lcov"],
9 | },
10 | },
11 | resolve: {
12 | alias: {
13 | src: path.resolve(__dirname, "./src/"),
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------