├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── babel.config.js
├── composer.json
├── composer.lock
├── jest.config.js
├── lib
├── bundle.min.js
├── format
│ ├── index.d.ts
│ └── index.js
├── index.d.ts
└── index.js
├── package-lock.json
├── package.json
├── php
├── src
│ └── NumberFormatter.php
└── tests
│ └── NumberFormatterTest.php
├── phpunit.xml.bak
├── ts
├── .github
│ └── workflows
│ │ └── ci.yml
└── src
│ ├── format
│ └── index.ts
│ └── index.ts
├── tsconfig.build.json
├── tsconfig.json
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | ts/src/types/global.d.ts
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | plugins: ['@typescript-eslint', 'node', 'prettier'],
5 | parserOptions: {
6 | tsconfigRootDir: __dirname,
7 | project: ['./tsconfig.json'],
8 | },
9 | extends: [
10 | 'eslint:recommended',
11 | 'plugin:node/recommended',
12 | 'plugin:@typescript-eslint/eslint-recommended',
13 | 'plugin:@typescript-eslint/recommended',
14 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
15 | 'plugin:prettier/recommended',
16 | ],
17 | rules: {
18 | 'prettier/prettier': 'warn',
19 | 'node/no-missing-import': 'off',
20 | 'node/no-empty-function': 'off',
21 | 'node/no-unsupported-features/es-syntax': 'off',
22 | 'node/no-missing-require': 'off',
23 | 'node/shebang': 'off',
24 | '@typescript-eslint/no-use-before-define': 'off',
25 | quotes: ['warn', 'single', { avoidEscape: true }],
26 | 'node/no-unpublished-import': 'off',
27 | '@typescript-eslint/no-unsafe-assignment': 'off',
28 | '@typescript-eslint/no-var-requires': 'off',
29 | '@typescript-eslint/ban-ts-comment': 'off',
30 | '@typescript-eslint/no-explicit-any': 'off',
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the repository to show as TypeScript rather than JS in GitHub
2 | *.js linguist-detectable=false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore node_modules in the root directory
2 | node_modules/
3 |
4 | # Ignore Composer dependencies in the root directory
5 | vendor/
6 |
7 | # Ignore environment variable files
8 | .env
9 | .env.local
10 | .env.*.local
11 |
12 | # Ignore log files and temp files
13 | *.log
14 | *.cache
15 | *.tmp
16 |
17 | # Ignore IDE and editor config files
18 | .idea/
19 | .vscode/
20 | nbproject/
21 | *.sublime-project
22 | *.sublime-workspace
23 |
24 | # Ignore OS generated files
25 | .DS_Store
26 | Thumbs.db
27 |
28 | # Ignore PHPUnit result cache
29 | .phpunit.result.cache
30 |
31 | # Ignore build and release directories
32 | ts/lib/
33 | dist/
34 |
35 | # Ignore coverage reports
36 | coverage/
37 | coverage.xml
38 |
39 | # Ignore Composer PHAR
40 | composer.phar
41 |
42 | # Ignore specific files that might contain sensitive information
43 | config.php
44 |
45 | # Ignore PHPUnit configuration
46 | phpunit.xml
47 | phpunit.xml.dist
48 |
49 | # Ignore personal IDE settings
50 | *.iml
51 | *.ipr
52 | *.iws
53 |
54 | # Ignore any local only files
55 | *.local
56 | *.cache
57 |
58 | # Ignore specific directories
59 | /.github/
60 | docs/
61 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "arrowParens": "avoid",
5 | "printWidth": 80
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 ArzDigital
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # reduce-precision
4 |
5 | [](https://snyk.io/test/github/ArzDigitalLabs/reduce-precision?targetFile=package.json)
6 | [](https://travis-ci.org/ArzDigitalLabs/reduce-precision)
7 | [](https://codecov.io/github/ArzDigitalLabs/reduce-precision?branch=master)
8 | [](https://codeclimate.com/github/ArzDigitalLabs/reduce-precision)
9 | [](https://npmjs.org/package/reduce-precision)
10 |
11 | `reduce-precision` is a versatile package for formatting and reducing the precision of numbers, currencies, and percentages. It supports various templates, precision levels, languages, and output formats, making it easy to generate formatted strings for different use cases.
12 |
13 | ## Features
14 |
15 | - Format numbers with customizable precision levels: high, medium, low, or auto
16 | - Support for multiple templates: number, USD, IRT (Iranian Toman), IRR (Iranian Rial), and percent
17 | - Multilingual support: English and Persian (Farsi)
18 | - Output formats: plain text, HTML, and Markdown
19 | - Customizable prefix and postfix markers for HTML and Markdown output
20 | - Intelligent handling of very small and very large numbers
21 | - Automatic thousand separators and decimal points based on the selected language
22 | - TypeScript type definitions included
23 |
24 | ## Installation
25 |
26 | ### Node.js / TypeScript
27 |
28 | You can install `reduce-precision` using npm:
29 |
30 | ```bash
31 | npm install reduce-precision
32 | ```
33 |
34 | [](https://www.npmjs.com/package/reduce-precision)
35 |
36 | ### PHP
37 |
38 | You can install the PHP version of `reduce-precision` via Composer:
39 |
40 | ```bash
41 | composer require arzdigitallabs/reduce-precision
42 | ```
43 |
44 | ## Usage
45 |
46 | ### Node.js / TypeScript
47 |
48 | ```typescript
49 | import { NumberFormatter } from 'reduce-precision';
50 |
51 | const formatter = new NumberFormatter();
52 |
53 | formatter.setLanguage('en', { prefixMarker: 'strong', prefix: 'USD ' });
54 |
55 | console.log(formatter.toHtmlString(123456789));
56 | console.log(formatter.toJson(123456789));
57 | console.log(formatter.toString(123456789));
58 | ```
59 |
60 | ### PHP
61 |
62 | ```php
63 | require 'vendor/autoload.php';
64 |
65 | use NumberFormatter\NumberFormatter;
66 |
67 | $formatter = new NumberFormatter();
68 | echo $formatter->toString(12345.678); // Default format
69 | ```
70 |
71 | ## Options
72 |
73 | The `format` function accepts an optional `options` object with the following properties:
74 |
75 | | Option | Type | Default | Description |
76 | | --------------- | ---------------------------------------------------------- | ---------- | ----------------------------------------------------- |
77 | | `precision` | `'auto'` \| `'high'` \| `'medium'` \| `'low'` | `'high'` | Precision level for formatting |
78 | | `template` | `'number'` \| `'usd'` \| `'irt'` \| `'irr'` \| `'percent'` | `'number'` | Template for formatting |
79 | | `language` | `'en'` \| `'fa'` | `'en'` | Language for formatting (English or Persian) |
80 | | `outputFormat` | `'plain'` \| `'html'` \| `'markdown'` | `'plain'` | Output format |
81 | | `prefixMarker` | `string` | `'i'` | Prefix marker for HTML and Markdown output |
82 | | `postfixMarker` | `string` | `'i'` | Postfix marker for HTML and Markdown output |
83 | | `prefix` | `string` | `''` | Prefix string to be added before the formatted number |
84 | | `postfix` | `string` | `''` | Postfix string to be added after the formatted number |
85 |
86 | ## Examples
87 |
88 | ### TypeScript/Node.js
89 |
90 | ```typescript
91 | import { NumberFormatter } from 'reduce-precision';
92 |
93 | // Create a formatter instance with default options
94 | const formatter = new NumberFormatter();
95 |
96 | // Basic usage
97 | formatter.setLanguage('en');
98 |
99 | // Basic number formatting
100 | formatter.toJson(1234.5678); // Output: { value: '1,234.6', ... }
101 |
102 | // Formatting with medium precision
103 | formatter.setTemplate('number', 'medium').toJson(1234.5678); // Output: { value: '1.23K', ... }
104 |
105 | // Formatting as USD
106 | formatter.setTemplate('usd', 'high').toJson(1234.5678); // Output: { value: '$1,234.6', ... }
107 |
108 | // Formatting as Iranian Rial with Persian numerals
109 | formatter.setLanguage('fa');
110 | formatter.setTemplate('irr', 'medium').toJson(1234.5678); // Output: { value: '۱٫۲۳ هزار ریال', ... }
111 |
112 | // Formatting as a percentage with low precision
113 | formatter.setTemplate('percent', 'low').toJson(0.1234); // Output: { value: '0.12%', ... }
114 |
115 | // Formatting with HTML output and custom markers
116 | formatter
117 | .setLanguage('en', { prefixMarker: 'strong', prefix: 'USD ' })
118 | .toHtmlString(1234.5678);
119 | // Output: USD 1,234.6
120 |
121 | // Formatting with string input for small or big numbers
122 | formatter.setTemplate('usd', 'medium').toJson('0.00000000000000000000005678521');
123 | // Output: { value: '$0.0₂₂5678', ... }
124 | ```
125 |
126 | ### PHP
127 |
128 | ```php
129 | require 'vendor/autoload.php';
130 |
131 | use NumberFormatter\NumberFormatter;
132 |
133 | $formatter = new NumberFormatter();
134 | echo $formatter->toString(12345.678); // Default format
135 |
136 | $formatter->setLanguage('fa');
137 | echo $formatter->toString(12345.678); // Output in Persian
138 |
139 | $formatter->setTemplate('usd', 'high');
140 | echo $formatter->toString(12345.678); // Output in USD format with high precision
141 |
142 | echo $formatter->toHtmlString(12345.678); // HTML formatted output
143 | echo $formatter->toMdString(12345.678); // Markdown formatted output
144 | ```
145 |
146 | ## API
147 |
148 | ### `FormattedObject` Interface (TypeScript/Node.js)
149 |
150 | The `FormattedObject` interface represents the structure of the formatted number object returned by the `format` method.
151 |
152 | ```typescript
153 | interface FormattedObject {
154 | value: string; // The formatted value as a string
155 | prefix: string; // The prefix string
156 | postfix: string; // The postfix string
157 | sign: string; // The sign of the number (either an empty string or '-')
158 | wholeNumber: string; // The whole number part of the value
159 | }
160 | ```
161 |
162 | ### `NumberFormatter` Class (PHP)
163 |
164 | #### `constructor`
165 |
166 | Creates a new instance of the `NumberFormatter` class with optional configuration options.
167 |
168 | #### `setLanguage`
169 |
170 | Sets the language and optional language configuration for the formatter.
171 |
172 | #### `setTemplate`
173 |
174 | Sets the template and precision for the formatter.
175 |
176 | #### `toString`
177 |
178 | Formats the input number as a string.
179 |
180 | #### `toPlainString`
181 |
182 | Formats the input number as a plain text string.
183 |
184 | #### `toHtmlString`
185 |
186 | Formats the input number as an HTML string.
187 |
188 | #### `toMdString`
189 |
190 | Formats the input number as a Markdown string.
191 |
192 | ## Testing
193 |
194 | ### Node.js / TypeScript
195 |
196 | You can run tests using Jest or any other preferred testing framework for TypeScript.
197 |
198 | ### PHP
199 |
200 | You can run tests using PHPUnit:
201 |
202 | ```bash
203 | ./vendor/bin/phpunit tests
204 | ```
205 |
206 | ## Contributing
207 |
208 | Contributions are welcome! If you find a bug or have a feature request, please open an issue on the [GitHub repository](https://github.com/ArzDigitalLabs/reduce-precision). If you'd like to contribute code, please fork the repository and submit a pull request.
209 |
210 | ## License
211 |
212 | This project is licensed under the [MIT License](LICENSE).
213 |
214 | ---
215 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | ["@babel/plugin-transform-modules-umd", {
4 | exactGlobals: true,
5 | }]
6 | ],
7 | presets: [
8 | [
9 | '@babel/preset-env',
10 | {
11 | targets: {
12 | node: 'current',
13 | },
14 | },
15 | ],
16 | ],
17 | };
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "arzdigitallabs/reduce-precision",
3 | "description": "A PHP package for reducing precision of numbers.",
4 | "version": "1.0.1",
5 | "type": "library",
6 | "authors": [
7 | {
8 | "name": "amirhosseinfaghan",
9 | "email": "www.amir9203joyandeh@gmail.com",
10 | "homepage": "https://github.com/amirhosseinfaghan"
11 | }
12 | ],
13 | "require": {
14 | "php": ">=7.4"
15 | },
16 | "autoload": {
17 | "psr-4": {
18 | "ReducePrecision\\": "php/src/"
19 | }
20 | },
21 | "require-dev": {
22 | "phpunit/phpunit": "*",
23 | "squizlabs/php_codesniffer": "^3.6",
24 | "friendsofphp/php-cs-fixer": "^3.0"
25 | },
26 | "scripts": {
27 | "test": "vendor/bin/phpunit --coverage-text",
28 | "lint": "vendor/bin/phpcs --standard=PSR12 src",
29 | "fix": "vendor/bin/php-cs-fixer fix"
30 | },
31 | "repositories": [
32 | {
33 | "type": "git",
34 | "url": "https://github.com/amirhosseinfaghan/reduce-precision.git"
35 | }
36 | ],
37 | "license": "MIT",
38 | "homepage": "https://github.com/amirhosseinfaghan/reduce-precision",
39 | "support": {
40 | "issues": "https://github.com/amirhosseinfaghan/reduce-precision/issues"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: ['**/test/**/*.spec.ts'],
5 | collectCoverageFrom: [
6 | '/src/**/*.ts',
7 | '!/src/types/**/*.ts',
8 | ],
9 | globals: {
10 | 'ts-jest': {
11 | diagnostics: false,
12 | isolatedModules: true,
13 | },
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/lib/bundle.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ReducePrecision=t():e.ReducePrecision=t()}(self,(()=>(()=>{"use strict";var e={898:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e={}){this.languageBaseConfig={prefixMarker:"i",postfixMarker:"i",prefix:"",postfix:""},this.defaultLanguageConfig={en:Object.assign(Object.assign({},this.languageBaseConfig),{thousandSeparator:",",decimalSeparator:"."}),fa:Object.assign(Object.assign({},this.languageBaseConfig),{thousandSeparator:"٫",decimalSeparator:"٬"})},this.options=Object.assign({language:"en",template:"number",precision:"high",outputFormat:"plain"},this.defaultLanguageConfig.en),this.options=Object.assign(Object.assign({},this.options),e)}setLanguage(e,t={}){return this.options.language=e,this.options.prefixMarker=t.prefixMarker||this.defaultLanguageConfig[e].prefixMarker,this.options.postfixMarker=t.postfixMarker||this.defaultLanguageConfig[e].postfixMarker,this.options.prefix=t.prefix||this.defaultLanguageConfig[e].prefix,this.options.postfix=t.postfix||this.defaultLanguageConfig[e].postfix,this.options.thousandSeparator=t.thousandSeparator||this.defaultLanguageConfig[e].thousandSeparator,this.options.decimalSeparator=t.decimalSeparator||this.defaultLanguageConfig[e].decimalSeparator,this}setTemplate(e,t){return this.options.template=e,this.options.precision=t,this}toJson(e){const t=this.format(e);return delete t.value,t}toString(e){return this.format(e).value||""}toPlainString(e){return this.options.outputFormat="plain",this.format(e).value||""}toHtmlString(e){return this.options.outputFormat="html",this.format(e).value||""}toMdString(e){return this.options.outputFormat="markdown",this.format(e).value||""}isENotation(e){return/^[-+]?[0-9]*\.?[0-9]+([eE][-+][0-9]+)$/.test(e)}format(e){let{precision:t,template:r}=this.options;const{language:i,outputFormat:o,prefixMarker:n,postfixMarker:a,prefix:s,postfix:u,thousandSeparator:l,decimalSeparator:p}=this.options;if(!e)return{};(null==r?void 0:r.match(/^(number|usd|irt|irr|percent)$/g))||(r="number"),this.isENotation(e.toString())&&(e=this.convertENotationToRegularNumber(Number(e)));let g=e.toString().replace(/[\u0660-\u0669\u06F0-\u06F9]/g,(function(e){return String(15&e.charCodeAt(0))})).replace(/[^\d.-]/g,"");g=g.replace(/^0+(?=\d)/g,"").replace(/(?<=\.\d*)0+$|(?<=\.\d)0+\b/g,"");const f=Math.abs(Number(g));let c,d,h,m,x=0;if("auto"===t&&(r.match(/^(usd|irt|irr|number)$/g)?t=f>=1e-4&&f<1e11?"high":"medium":"percent"===r&&(t="low")),"medium"===t)if(f>=0&&f<1e-4)c=33,d=4,h=!1,m=!0;else if(f>=1e-4&&f<.001)c=7,d=4,h=!1,m=!1;else if(f>=.001&&f<.01)c=5,d=3,h=!1,m=!1;else if(f>=.001&&f<.1)c=3,d=2,h=!1,m=!1;else if(f>=.1&&f<1)c=1,d=1,h=!1,m=!1;else if(f>=1&&f<10)c=3,d=3,h=!1,m=!1;else if(f>=10&&f<100)c=2,d=2,h=!1,m=!1;else if(f>=100&&f<1e3)c=1,d=1,h=!1,m=!1;else if(f>=1e3){const e=Math.floor(Math.log10(f))%3;c=2-e,d=2-e,h=!0,m=!0}else c=0,d=0,h=!0,m=!0;else if("low"===t)if(f>=0&&f<.01)c=2,d=0,h=!0,m=!1,x=2;else if(f>=.01&&f<.1)c=2,d=1,h=!0,m=!1;else if(f>=.1&&f<1)c=2,d=2,h=!0,m=!1;else if(f>=1&&f<10)c=2,d=2,h=!0,m=!1,x=2;else if(f>=10&&f<100)c=1,d=1,h=!0,m=!1,x=1;else if(f>=100&&f<1e3)c=0,d=0,h=!0,m=!1;else if(f>=1e3){const e=Math.floor(Math.log10(f))%3;c=1-e,d=1-e,h=!0,m=!0}else c=0,d=0,h=!0,m=!0,x=2;else f>=0&&f<1?(c=33,d=4,h=!1,m=!1):f>=1&&f<10?(c=3,d=3,h=!0,m=!1):f>=10&&f<100||f>=100&&f<1e3?(c=2,d=2,h=!0,m=!1):f>=1e3&&f<1e4?(c=1,d=1,h=!0,m=!1):(c=0,d=0,h=!0,m=!1);return this.reducePrecision(g,c,d,h,m,x,r,i,o,n,a,s,u,l,p)}convertENotationToRegularNumber(e){const[t,r]=e.toString().split("e"),i=t.replace(".","").replace("-","").length,o=parseFloat(r),n=Math.max(i-o,1);return e.toFixed(n)}reducePrecision(e,t=30,r=4,i=!1,o=!1,n=0,a="number",s="en",u="plain",l="span",p="span",g="",f="",c=",",d="."){var h,m;if(!e)return{};e=e.toString();const x=a.match(/^(number|percent)$/g)?{"":"",K:" هزار",M:" میلیون",B:" میلیارد",T:" تریلیون",Qd:" کادریلیون",Qt:" کنتیلیون"}:{"":"",K:" هزار ت",M:" میلیون ت",B:" میلیارد ت",T:" همت",Qd:" هزار همت",Qt:" میلیون همت"},b=a.match(/^(number|percent)$/g)?{"":"",K:" هزار",M:" میلیون",B:" میلیارد",T:" تریلیون",Qd:" کادریلیون",Qt:" کنتیلیون"}:{"":"",K:" هزار تومان",M:" میلیون تومان",B:" میلیارد تومان",T:" هزار میلیارد تومان",Qd:" کادریلیون تومان",Qt:" کنتیلیون تومان"};let S=/^(-)?(\d+)\.?([0]*)(\d*)$/g.exec(e);if(!S)return{};const $=S[1]||"";let M=S[2],v=S[3],C=S[4],Q="",N="";if(v.length>=30?(v="0".padEnd(29,"0"),C="1"):v.length+r>t?(r=t-v.length)<1&&(r=1):M.length>21&&(M="0",v="",C=""),o&&M.length>=4){const e=Object.keys(x);let t=M,r=0;for(;+t>999&&rr&&(i?parseInt(C[r])<5?C=C.substring(0,r):(C=(parseInt(C.substring(0,r))+1).toString(),C.length>r&&(v.length>0?v=v.substring(0,v.length-1):(M=(Number(M)+1).toString(),C=C.substring(1)))):C=C.substring(0,r)),o&&""!==v&&""===N&&(v="0"+v.length.toString().replace(/\d/g,(function(e){return["₀","₁","₂","₃","₄","₅","₆","₇","₈","₉"][parseInt(e,10)]})));let j=`${v}${C}`;j=j.substring(0,t),j=j.replace(/^(\d*[1-9])0+$/g,"$1"),"usd"===a?(Q="en"===s?"$":"",N||(N="fa"===s?" دلار":"")):"irr"===a?N||(N="fa"===s?" ر":" R"):"irt"===a?N||(N="fa"===s?" ت":" T"):"percent"===a&&(N+="en"===s?"%":N?" درصد":"٪"),Q=g+Q,N+=f,"html"===u?(Q&&(Q=`<${l}>${Q}${l}>`),N&&(N=`<${p}>${N}${p}>`)):"markdown"===u&&(Q&&(Q=`${l}${Q}${l}`),N&&(N=`${p}${N}${p}`));const k=/\B(?=(\d{3})+(?!\d))/g,B=n?".".padEnd(n+1,"0"):"";let F,T="";F=t<=0||r<=0||!C?`${M.replace(k,",")}${B}`:`${M.replace(k,",")}.${j}`,T=`${$}${Q}${F}${N}`;const O={value:T,prefix:Q,postfix:N,sign:$,wholeNumber:F};return O.value=(null!==(h=null==O?void 0:O.value)&&void 0!==h?h:"").replace(/,/g,c).replace(/\./g,d),"fa"===s&&(O.value=(null!==(m=null==O?void 0:O.value)&&void 0!==m?m:"").replace(/[0-9]/g,(e=>String.fromCharCode(e.charCodeAt(0)+1728))).replace(/(K|M|B|T|Qt|Qd)/g,(function(e){return String(x[e])})),O.fullPostfix=N.replace(/[0-9]/g,(e=>String.fromCharCode(e.charCodeAt(0)+1728))).replace(/(K|M|B|T|Qt|Qd)/g,(function(e){return String(b[e])})),O.postfix=O.postfix.replace(/[0-9]/g,(e=>String.fromCharCode(e.charCodeAt(0)+1728))).replace(/(K|M|B|T|Qt|Qd)/g,(function(e){return String(x[e])})),O.wholeNumber=O.wholeNumber.replace(/[0-9]/g,(e=>String.fromCharCode(e.charCodeAt(0)+1728))).replace(/(K|M|B|T|Qt|Qd)/g,(function(e){return String(x[e])}))),O}}},156:function(e,t,r){var i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.NumberFormatter=void 0;const o=i(r(898));t.NumberFormatter=o.default}},t={};return function r(i){var o=t[i];if(void 0!==o)return o.exports;var n=t[i]={exports:{}};return e[i].call(n.exports,n,n.exports,r),n.exports}(156)})()));
--------------------------------------------------------------------------------
/lib/format/index.d.ts:
--------------------------------------------------------------------------------
1 | type Template = 'number' | 'usd' | 'irt' | 'irr' | 'percent';
2 | type Precision = 'auto' | 'high' | 'medium' | 'low';
3 | type Language = 'en' | 'fa';
4 | type OutputFormat = 'plain' | 'html' | 'markdown';
5 | interface FormattedObject {
6 | value?: string;
7 | prefix: string;
8 | postfix: string;
9 | fullPostfix?: string;
10 | sign: string;
11 | wholeNumber: string;
12 | }
13 | interface LanguageConfig {
14 | prefixMarker?: string;
15 | postfixMarker?: string;
16 | prefix?: string;
17 | postfix?: string;
18 | thousandSeparator?: string;
19 | decimalSeparator?: string;
20 | }
21 | interface Options extends LanguageConfig {
22 | precision?: Precision;
23 | template?: Template;
24 | language?: Language;
25 | outputFormat?: OutputFormat;
26 | }
27 | declare class NumberFormatter {
28 | private readonly languageBaseConfig;
29 | private defaultLanguageConfig;
30 | private options;
31 | constructor(options?: Options);
32 | setLanguage(lang: Language, config?: LanguageConfig): NumberFormatter;
33 | setTemplate(template: Template, precision: Precision): NumberFormatter;
34 | toJson(input: string | number): FormattedObject;
35 | toString(input: string | number): string;
36 | toPlainString(input: string | number): string;
37 | toHtmlString(input: string | number): string;
38 | toMdString(input: string | number): string;
39 | private isENotation;
40 | private format;
41 | private convertENotationToRegularNumber;
42 | private reducePrecision;
43 | }
44 | export default NumberFormatter;
45 |
--------------------------------------------------------------------------------
/lib/format/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | class NumberFormatter {
4 | constructor(options = {}) {
5 | this.languageBaseConfig = {
6 | prefixMarker: 'i',
7 | postfixMarker: 'i',
8 | prefix: '',
9 | postfix: '',
10 | };
11 | this.defaultLanguageConfig = {
12 | en: Object.assign(Object.assign({}, this.languageBaseConfig), { thousandSeparator: ',', decimalSeparator: '.' }),
13 | fa: Object.assign(Object.assign({}, this.languageBaseConfig), { thousandSeparator: '٫', decimalSeparator: '٬' }),
14 | };
15 | this.options = Object.assign({ language: 'en', template: 'number', precision: 'high', outputFormat: 'plain' }, this.defaultLanguageConfig['en']);
16 | this.options = Object.assign(Object.assign({}, this.options), options);
17 | }
18 | setLanguage(lang, config = {}) {
19 | this.options.language = lang;
20 | this.options.prefixMarker =
21 | config.prefixMarker || this.defaultLanguageConfig[lang].prefixMarker;
22 | this.options.postfixMarker =
23 | config.postfixMarker || this.defaultLanguageConfig[lang].postfixMarker;
24 | this.options.prefix =
25 | config.prefix || this.defaultLanguageConfig[lang].prefix;
26 | this.options.postfix =
27 | config.postfix || this.defaultLanguageConfig[lang].postfix;
28 | this.options.thousandSeparator =
29 | config.thousandSeparator ||
30 | this.defaultLanguageConfig[lang].thousandSeparator;
31 | this.options.decimalSeparator =
32 | config.decimalSeparator ||
33 | this.defaultLanguageConfig[lang].decimalSeparator;
34 | return this;
35 | }
36 | setTemplate(template, precision) {
37 | this.options.template = template;
38 | this.options.precision = precision;
39 | return this;
40 | }
41 | toJson(input) {
42 | const formattedObject = this.format(input);
43 | delete formattedObject.value;
44 | return formattedObject;
45 | }
46 | toString(input) {
47 | const formattedObject = this.format(input);
48 | return formattedObject.value || '';
49 | }
50 | toPlainString(input) {
51 | this.options.outputFormat = 'plain';
52 | const formattedObject = this.format(input);
53 | return formattedObject.value || '';
54 | }
55 | toHtmlString(input) {
56 | this.options.outputFormat = 'html';
57 | const formattedObject = this.format(input);
58 | return formattedObject.value || '';
59 | }
60 | toMdString(input) {
61 | this.options.outputFormat = 'markdown';
62 | const formattedObject = this.format(input);
63 | return formattedObject.value || '';
64 | }
65 | // Private methods...
66 | isENotation(input) {
67 | return /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)$/.test(input);
68 | }
69 | format(input) {
70 | let { precision, template } = this.options;
71 | const { language, outputFormat, prefixMarker, postfixMarker, prefix, postfix, thousandSeparator, decimalSeparator, } = this.options;
72 | if (!input)
73 | return {};
74 | if (!(template === null || template === void 0 ? void 0 : template.match(/^(number|usd|irt|irr|percent)$/g)))
75 | template = 'number';
76 |
77 | // Store original input string to preserve format for trailing zeros
78 | const originalInput = input.toString();
79 |
80 | if (this.isENotation(originalInput)) {
81 | input = this.convertENotationToRegularNumber(Number(input));
82 | }
83 |
84 | // Replace each Persian/Arabic numeral in the string with its English counterpart and strip all non-numeric chars
85 | let numberString = input
86 | .toString()
87 | .replace(/[\u0660-\u0669\u06F0-\u06F9]/g, function (match) {
88 | return String(match.charCodeAt(0) & 0xf);
89 | })
90 | .replace(/[^\d.-]/g, '');
91 |
92 | // Stripping leading zeros only, preserve trailing zeros
93 | numberString = numberString
94 | .replace(/^0+(?=\d)/g, '');
95 |
96 | const number = Math.abs(Number(numberString));
97 | let p, d, r, c;
98 | let f = 0;
99 |
100 | // Auto precision selection
101 | if (precision === 'auto') {
102 | if (template.match(/^(usd|irt|irr|number)$/g)) {
103 | if (number >= 0.0001 && number < 100000000000) {
104 | precision = 'high';
105 | }
106 | else {
107 | precision = 'medium';
108 | }
109 | }
110 | else if (template === 'percent') {
111 | precision = 'low';
112 | }
113 | }
114 |
115 | if (precision === 'medium') {
116 | if (number >= 0 && number < 0.0001) {
117 | p = 33;
118 | d = 4;
119 | r = false;
120 | c = true;
121 | }
122 | else if (number >= 0.0001 && number < 0.001) {
123 | p = 7;
124 | d = 4;
125 | r = false;
126 | c = false;
127 | }
128 | else if (number >= 0.001 && number < 0.01) {
129 | p = 5;
130 | d = 3;
131 | r = false;
132 | c = false;
133 | }
134 | else if (number >= 0.001 && number < 0.1) {
135 | p = 3;
136 | d = 2;
137 | r = false;
138 | c = false;
139 | }
140 | else if (number >= 0.1 && number < 1) {
141 | p = 1;
142 | d = 1;
143 | r = false;
144 | c = false;
145 | }
146 | else if (number >= 1 && number < 10) {
147 | p = 3;
148 | d = 3;
149 | r = false;
150 | c = false;
151 | }
152 | else if (number >= 10 && number < 100) {
153 | p = 2;
154 | d = 2;
155 | r = false;
156 | c = false;
157 | }
158 | else if (number >= 100 && number < 1000) {
159 | p = 1;
160 | d = 1;
161 | r = false;
162 | c = false;
163 | }
164 | else if (number >= 1000) {
165 | const x = Math.floor(Math.log10(number)) % 3;
166 | p = 2 - x;
167 | d = 2 - x;
168 | r = true;
169 | c = true;
170 | }
171 | else {
172 | p = 0;
173 | d = 0;
174 | r = true;
175 | c = true;
176 | }
177 | }
178 | else if (precision === 'low') {
179 | if (number >= 0 && number < 0.01) {
180 | p = 2;
181 | d = 0;
182 | r = true;
183 | c = false;
184 | f = 2;
185 | }
186 | else if (number >= 0.01 && number < 0.1) {
187 | p = 2;
188 | d = 1;
189 | r = true;
190 | c = false;
191 | }
192 | else if (number >= 0.1 && number < 1) {
193 | p = 2;
194 | d = 2;
195 | r = true;
196 | c = false;
197 | }
198 | else if (number >= 1 && number < 10) {
199 | p = 2;
200 | d = 2;
201 | r = true;
202 | c = false;
203 | f = 2;
204 | }
205 | else if (number >= 10 && number < 100) {
206 | p = 1;
207 | d = 1;
208 | r = true;
209 | c = false;
210 | f = 1;
211 | }
212 | else if (number >= 100 && number < 1000) {
213 | p = 0;
214 | d = 0;
215 | r = true;
216 | c = false;
217 | }
218 | else if (number >= 1000) {
219 | const x = Math.floor(Math.log10(number)) % 3;
220 | p = 1 - x;
221 | d = 1 - x;
222 | r = true;
223 | c = true;
224 | }
225 | else {
226 | p = 0;
227 | d = 0;
228 | r = true;
229 | c = true;
230 | f = 2;
231 | }
232 | }
233 | else {
234 | // precision === "high"
235 | if (number >= 0 && number < 1) {
236 | p = 33;
237 | d = 4;
238 | r = false;
239 | c = false;
240 | }
241 | else if (number >= 1 && number < 10) {
242 | p = 3;
243 | d = 3;
244 | r = true;
245 | c = false;
246 | }
247 | else if (number >= 10 && number < 100) {
248 | p = 2;
249 | d = 2;
250 | r = true;
251 | c = false;
252 | }
253 | else if (number >= 100 && number < 1000) {
254 | p = 2;
255 | d = 2;
256 | r = true;
257 | c = false;
258 | }
259 | else if (number >= 1000 && number < 10000) {
260 | p = 1;
261 | d = 1;
262 | r = true;
263 | c = false;
264 | }
265 | else {
266 | p = 0;
267 | d = 0;
268 | r = true;
269 | c = false;
270 | }
271 | }
272 |
273 | // For scientific notation, increase precision to ensure correct representation
274 | if (this.isENotation(originalInput)) {
275 | p = Math.max(p, 20);
276 | r = false;
277 | }
278 |
279 | return this.reducePrecision(
280 | numberString,
281 | p,
282 | d,
283 | r,
284 | c,
285 | f,
286 | template,
287 | language,
288 | outputFormat,
289 | prefixMarker,
290 | postfixMarker,
291 | prefix,
292 | postfix,
293 | thousandSeparator,
294 | decimalSeparator,
295 | originalInput
296 | );
297 | }
298 |
299 | convertENotationToRegularNumber(eNotation) {
300 | // For simple cases like 1e3, directly use Number constructor
301 | if (Number.isInteger(eNotation) && eNotation >= 1000) {
302 | return eNotation.toString();
303 | }
304 |
305 | const parts = eNotation.toString().toLowerCase().split('e');
306 | if (parts.length !== 2) return eNotation.toString();
307 |
308 | const coefficient = parseFloat(parts[0]);
309 | const exponent = parseInt(parts[1], 10);
310 |
311 | // Handle negative exponents (very small numbers)
312 | if (exponent < 0) {
313 | const absExponent = Math.abs(exponent);
314 | // Determine precision needed to show all digits
315 | const precision = absExponent +
316 | (parts[0].includes('.') ? parts[0].split('.')[1].length : 0);
317 | return eNotation.toFixed(precision);
318 | }
319 |
320 | // For positive exponents, let JavaScript do the conversion
321 | return eNotation.toString();
322 | }
323 |
324 | reducePrecision(
325 | numberString,
326 | precision = 30,
327 | nonZeroDigits = 4,
328 | round = false,
329 | compress = false,
330 | fixedDecimalZeros = 0,
331 | template = 'number',
332 | language = 'en',
333 | outputFormat = 'plain',
334 | prefixMarker = 'span',
335 | postfixMarker = 'span',
336 | prefix = '',
337 | postfix = '',
338 | thousandSeparator = ',',
339 | decimalSeparator = '.',
340 | originalInput = ''
341 | ) {
342 | var _a, _b;
343 |
344 | if (!numberString) {
345 | return {};
346 | }
347 |
348 | // Handle negative zero
349 | if (numberString === '-0' || numberString === '-0.0') {
350 | numberString = numberString.substring(1); // Remove negative sign for zero
351 | }
352 |
353 | numberString = numberString.toString();
354 |
355 | const maxPrecision = 30;
356 | const maxIntegerDigits = 21;
357 | const scaleUnits = template.match(/^(number|percent)$/g)
358 | ? {
359 | '': '',
360 | K: ' هزار',
361 | M: ' میلیون',
362 | B: ' میلیارد',
363 | T: ' تریلیون',
364 | Qd: ' کادریلیون',
365 | Qt: ' کنتیلیون',
366 | }
367 | : {
368 | '': '',
369 | K: ' هزار ت',
370 | M: ' میلیون ت',
371 | B: ' میلیارد ت',
372 | T: ' همت',
373 | Qd: ' هزار همت',
374 | Qt: ' میلیون همت',
375 | };
376 | const fullScaleUnits = template.match(/^(number|percent)$/g)
377 | ? {
378 | '': '',
379 | K: ' هزار',
380 | M: ' میلیون',
381 | B: ' میلیارد',
382 | T: ' تریلیون',
383 | Qd: ' کادریلیون',
384 | Qt: ' کنتیلیون',
385 | }
386 | : {
387 | '': '',
388 | K: ' هزار تومان',
389 | M: ' میلیون تومان',
390 | B: ' میلیارد تومان',
391 | T: ' هزار میلیارد تومان',
392 | Qd: ' کادریلیون تومان',
393 | Qt: ' کنتیلیون تومان',
394 | };
395 | let parts = /^(-)?(\d+)\.?([0]*)(\d*)$/g.exec(numberString);
396 | if (!parts) {
397 | return {};
398 | }
399 | const sign = parts[1] || '';
400 | let nonFractionalStr = parts[2];
401 | let fractionalZeroStr = parts[3];
402 | let fractionalNonZeroStr = parts[4];
403 | let unitPrefix = '';
404 | let unitPostfix = '';
405 | if (fractionalZeroStr.length >= maxPrecision) {
406 | // Number is smaller than maximum precision
407 | fractionalZeroStr = '0'.padEnd(maxPrecision - 1, '0');
408 | fractionalNonZeroStr = '1';
409 | }
410 | else if (fractionalZeroStr.length + nonZeroDigits > precision) {
411 | // decrease non-zero digits
412 | nonZeroDigits = precision - fractionalZeroStr.length;
413 | if (nonZeroDigits < 1)
414 | nonZeroDigits = 1;
415 | }
416 | else if (nonFractionalStr.length > maxIntegerDigits) {
417 | nonFractionalStr = '0';
418 | fractionalZeroStr = '';
419 | fractionalNonZeroStr = '';
420 | }
421 | // compress large numbers
422 | if (compress && nonFractionalStr.length >= 4) {
423 | const scaleUnitKeys = Object.keys(scaleUnits);
424 | let scaledWholeNumber = nonFractionalStr;
425 | let unitIndex = 0;
426 | while (+scaledWholeNumber > 999 && unitIndex < scaleUnitKeys.length - 1) {
427 | scaledWholeNumber = (+scaledWholeNumber / 1000).toFixed(2);
428 | unitIndex++;
429 | }
430 | unitPostfix = scaleUnitKeys[unitIndex];
431 | parts = /^(-)?(\d+)\.?([0]*)(\d*)$/g.exec(scaledWholeNumber.toString());
432 | if (!parts) {
433 | return {};
434 | }
435 | // sign = parts[1] || "";
436 | nonFractionalStr = parts[2];
437 | fractionalZeroStr = parts[3];
438 | fractionalNonZeroStr = parts[4];
439 | }
440 | // Truncate the fractional part or round it
441 | // if (precision > 0 && nonZeroDigits > 0 && fractionalNonZeroStr.length > nonZeroDigits) {
442 | if (fractionalNonZeroStr.length > nonZeroDigits) {
443 | if (!round) {
444 | fractionalNonZeroStr = fractionalNonZeroStr.substring(0, nonZeroDigits);
445 | }
446 | else {
447 | if (parseInt(fractionalNonZeroStr[nonZeroDigits]) < 5) {
448 | fractionalNonZeroStr = fractionalNonZeroStr.substring(0, nonZeroDigits);
449 | }
450 | else {
451 | fractionalNonZeroStr = (parseInt(fractionalNonZeroStr.substring(0, nonZeroDigits)) + 1).toString();
452 | // If overflow occurs (e.g., 999 + 1 = 1000), adjust the substring length
453 | if (fractionalNonZeroStr.length > nonZeroDigits) {
454 | if (fractionalZeroStr.length > 0) {
455 | fractionalZeroStr = fractionalZeroStr.substring(0, fractionalZeroStr.length - 1);
456 | }
457 | else {
458 | nonFractionalStr = (Number(nonFractionalStr) + 1).toString();
459 | fractionalNonZeroStr = fractionalNonZeroStr.substring(1);
460 | }
461 | }
462 | }
463 | }
464 | }
465 | // Using dex style
466 | if (compress && fractionalZeroStr !== '' && unitPostfix === '') {
467 | fractionalZeroStr =
468 | '0' +
469 | fractionalZeroStr.length.toString().replace(/\d/g, function (match) {
470 | return [
471 | '₀',
472 | '₁',
473 | '₂',
474 | '₃',
475 | '₄',
476 | '₅',
477 | '₆',
478 | '₇',
479 | '₈',
480 | '₉',
481 | ][parseInt(match, 10)];
482 | });
483 | }
484 |
485 | let fractionalPartStr = `${fractionalZeroStr}${fractionalNonZeroStr}`;
486 | // Don't truncate trailing zeros when they're in the original string
487 | if (fractionalPartStr.length > precision && !originalInput.includes('e')) {
488 | fractionalPartStr = fractionalPartStr.substring(0, precision);
489 | }
490 |
491 | // For scientific notation and numbers with trailing zeros, preserve the format
492 | if (originalInput.includes('e') || originalInput.includes('E')) {
493 | // For scientific notation, use the converted string
494 | } else if (originalInput.includes('.')) {
495 | // For regular numbers with decimal point, check for trailing zeros
496 | const originalParts = originalInput.split('.');
497 | if (originalParts.length === 2) {
498 | const originalDecimal = originalParts[1];
499 | // If original has more digits than what we have now, preserve those trailing zeros
500 | if (originalDecimal.length > fractionalPartStr.length && originalDecimal.endsWith('0')) {
501 | // Count trailing zeros in original
502 | let trailingZeros = 0;
503 | for (let i = originalDecimal.length - 1; i >= 0; i--) {
504 | if (originalDecimal[i] === '0') {
505 | trailingZeros++;
506 | } else {
507 | break;
508 | }
509 | }
510 | // Add back trailing zeros if they were in the original
511 | if (trailingZeros > 0) {
512 | fractionalPartStr = fractionalPartStr.padEnd(fractionalPartStr.length + trailingZeros, '0');
513 | }
514 | }
515 | }
516 | }
517 |
518 | // Output Formating, Prefix, Postfix
519 | if (template === 'usd') {
520 | unitPrefix = language === 'en' ? '$' : '';
521 | if (!unitPostfix)
522 | unitPostfix = language === 'fa' ? ' دلار' : '';
523 | }
524 | else if (template === 'irr') {
525 | if (!unitPostfix)
526 | unitPostfix = language === 'fa' ? ' ر' : ' R';
527 | }
528 | else if (template === 'irt') {
529 | if (!unitPostfix)
530 | unitPostfix = language === 'fa' ? ' ت' : ' T';
531 | }
532 | else if (template === 'percent') {
533 | if (language === 'en') {
534 | unitPostfix += '%';
535 | }
536 | else {
537 | unitPostfix += !unitPostfix ? '٪' : ' درصد';
538 | }
539 | }
540 | unitPrefix = prefix + unitPrefix;
541 | unitPostfix += postfix;
542 | if (outputFormat === 'html') {
543 | if (unitPrefix)
544 | unitPrefix = `<${prefixMarker}>${unitPrefix}${prefixMarker}>`;
545 | if (unitPostfix)
546 | unitPostfix = `<${postfixMarker}>${unitPostfix}${postfixMarker}>`;
547 | }
548 | else if (outputFormat === 'markdown') {
549 | if (unitPrefix)
550 | unitPrefix = `${prefixMarker}${unitPrefix}${prefixMarker}`;
551 | if (unitPostfix)
552 | unitPostfix = `${postfixMarker}${unitPostfix}${postfixMarker}`;
553 | }
554 | const thousandSeparatorRegex = /\B(?=(\d{3})+(?!\d))/g;
555 | const fixedDecimalZeroStr = fixedDecimalZeros
556 | ? '.'.padEnd(fixedDecimalZeros + 1, '0')
557 | : '';
558 | let out = '';
559 | let wholeNumberStr;
560 |
561 | // FIXED: Changed condition to correctly handle numbers with trailing zeros
562 | // Old condition: if (precision <= 0 || nonZeroDigits <= 0 || !fractionalNonZeroStr) {
563 | // New condition checks if both fractional parts are empty
564 | if (precision <= 0 || nonZeroDigits <= 0 || (fractionalNonZeroStr === '' && fractionalZeroStr === '')) {
565 | wholeNumberStr = `${nonFractionalStr.replace(thousandSeparatorRegex, ',')}${fixedDecimalZeroStr}`;
566 | }
567 | else {
568 | wholeNumberStr = `${nonFractionalStr.replace(thousandSeparatorRegex, ',')}.${fractionalPartStr}`;
569 | }
570 |
571 | out = `${sign}${unitPrefix}${wholeNumberStr}${unitPostfix}`;
572 | const formattedObject = {
573 | value: out,
574 | prefix: unitPrefix,
575 | postfix: unitPostfix,
576 | sign: sign,
577 | wholeNumber: wholeNumberStr,
578 | };
579 | // replace custom config
580 | formattedObject.value = ((_a = formattedObject === null || formattedObject === void 0 ? void 0 : formattedObject.value) !== null && _a !== void 0 ? _a : '')
581 | .replace(/,/g, thousandSeparator)
582 | .replace(/\./g, decimalSeparator);
583 | // Convert output to Persian numerals if language is "fa"
584 | if (language === 'fa') {
585 | formattedObject.value = ((_b = formattedObject === null || formattedObject === void 0 ? void 0 : formattedObject.value) !== null && _b !== void 0 ? _b : '')
586 | .replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) + 1728))
587 | .replace(/(K|M|B|T|Qt|Qd)/g, function (c) {
588 | return String(scaleUnits[c]);
589 | });
590 | formattedObject.fullPostfix = unitPostfix
591 | .replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) + 1728))
592 | .replace(/(K|M|B|T|Qt|Qd)/g, function (c) {
593 | return String(fullScaleUnits[c]);
594 | });
595 | formattedObject.postfix = formattedObject.postfix
596 | .replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) + 1728))
597 | .replace(/(K|M|B|T|Qt|Qd)/g, function (c) {
598 | return String(scaleUnits[c]);
599 | });
600 | formattedObject.wholeNumber = formattedObject.wholeNumber
601 | .replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) + 1728))
602 | .replace(/(K|M|B|T|Qt|Qd)/g, function (c) {
603 | return String(scaleUnits[c]);
604 | });
605 | }
606 | return formattedObject;
607 | }
608 | }
609 | exports.default = NumberFormatter;
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | import NumberFormatter from './format';
2 | export { NumberFormatter };
3 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | exports.NumberFormatter = void 0;
7 | const format_1 = __importDefault(require("./format"));
8 | exports.NumberFormatter = format_1.default;
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reduce-precision",
3 | "version": "1.0.7",
4 | "description": "",
5 | "main": "./ts/lib/index.js",
6 | "files": [
7 | "ts/lib/**/*"
8 | ],
9 | "scripts": {
10 | "build": "npm run build:tsc && npm run build:webpack",
11 | "build:tsc": "tsc --project tsconfig.json",
12 | "build:webpack": "webpack --config webpack.config.js",
13 | "clean": "rm -rf ./ts/lib/",
14 | "lint": "eslint ./ts/src/ --fix",
15 | "test:watch": "jest --watch",
16 | "test": "jest --coverage",
17 | "typecheck": "tsc --noEmit"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/ArzDigitalLabs/reduce-precision.git"
22 | },
23 | "license": "MIT",
24 | "author": {
25 | "name": "Mohammad Anaraki",
26 | "email": "m.anaraki1376@gmail.com",
27 | "url": "https://github.com/mohammadanaraki"
28 | },
29 | "bugs": {
30 | "url": "https://github.com/ArzDigitalLabs/reduce-precision/issues"
31 | },
32 | "homepage": "https://github.com/ArzDigitalLabs/reduce-precision#readme",
33 | "devDependencies": {
34 | "@babel/core": "^7.24.7",
35 | "@babel/preset-env": "^7.24.7",
36 | "@types/jest": "^27.5.2",
37 | "@types/node": "^12.20.11",
38 | "@typescript-eslint/eslint-plugin": "^4.22.0",
39 | "@typescript-eslint/parser": "^4.22.0",
40 | "babel-loader": "^9.1.3",
41 | "eslint": "^7.25.0",
42 | "eslint-config-prettier": "^8.3.0",
43 | "eslint-plugin-node": "^11.1.0",
44 | "eslint-plugin-prettier": "^3.4.0",
45 | "jest": "^27.2.0",
46 | "lint-staged": "^13.2.1",
47 | "prettier": "^2.2.1",
48 | "ts-jest": "^27.0.5",
49 | "ts-loader": "^9.5.1",
50 | "ts-node": "^10.2.1",
51 | "typescript": "^4.9.5",
52 | "webpack": "^5.92.1",
53 | "webpack-cli": "^5.1.4"
54 | },
55 | "lint-staged": {
56 | "*.ts": "eslint --cache --cache-location .eslintcache --fix"
57 | },
58 | "release": {
59 | "branches": [
60 | "main"
61 | ]
62 | }
63 | }
--------------------------------------------------------------------------------
/php/src/NumberFormatter.php:
--------------------------------------------------------------------------------
1 | options = [
12 | 'language' => 'en',
13 | 'template' => 'number',
14 | 'precision' => 'high',
15 | 'outputFormat' => 'plain',
16 | 'prefixMarker' => 'i',
17 | 'postfixMarker' => 'i',
18 | 'prefix' => '',
19 | 'postfix' => '',
20 | ];
21 | $this->options = array_merge($this->options, $options);
22 | }
23 |
24 | public function setLanguage($lang, $config = [])
25 | {
26 | $this->options['language'] = $lang;
27 | $this->options['prefixMarker'] = $config['prefixMarker'] ?? $this->options['prefixMarker'];
28 | $this->options['postfixMarker'] = $config['postfixMarker'] ?? $this->options['postfixMarker'];
29 | $this->options['prefix'] = $config['prefix'] ?? $this->options['prefix'];
30 | $this->options['postfix'] = $config['postfix'] ?? $this->options['postfix'];
31 | return $this;
32 | }
33 |
34 | public function setTemplate($template, $precision)
35 | {
36 | $this->options['template'] = $template;
37 | $this->options['precision'] = $precision;
38 | return $this;
39 | }
40 |
41 | public function toJson($input)
42 | {
43 | $formattedObject = $this->format($input);
44 | unset($formattedObject['value']);
45 | return $formattedObject;
46 | }
47 |
48 | public function toString($input)
49 | {
50 | $formattedObject = $this->format($input);
51 | return $formattedObject['value'] ?? '';
52 | }
53 |
54 | public function toPlainString($input)
55 | {
56 | $this->options['outputFormat'] = 'plain';
57 | $formattedObject = $this->format($input);
58 | return $formattedObject['value'] ?? '';
59 | }
60 |
61 | public function toHtmlString($input)
62 | {
63 | $this->options['outputFormat'] = 'html';
64 | $formattedObject = $this->format($input);
65 | return $formattedObject['value'] ?? '';
66 | }
67 |
68 | public function toMdString($input)
69 | {
70 | $this->options['outputFormat'] = 'markdown';
71 | $formattedObject = $this->format($input);
72 | return $formattedObject['value'] ?? '';
73 | }
74 |
75 | // FIXED: Updated regex to handle both positive and negative exponents
76 | private function isENotation($input)
77 | {
78 | return preg_match('/^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)$/', $input);
79 | }
80 |
81 | private function format($input)
82 | {
83 | $precision = $this->options['precision'];
84 | $template = $this->options['template'];
85 | $language = $this->options['language'];
86 | $outputFormat = $this->options['outputFormat'];
87 | $prefixMarker = $this->options['prefixMarker'];
88 | $postfixMarker = $this->options['postfixMarker'];
89 | $prefix = $this->options['prefix'];
90 | $postfix = $this->options['postfix'];
91 |
92 | // Check if the input is null or empty but not 0
93 | if ($input === null || $input === '') {
94 | return [];
95 | }
96 |
97 | // Store original input string to preserve format for trailing zeros
98 | $originalInput = (string)$input;
99 |
100 | if (!preg_match('/^(number|usd|irt|irr|percent)$/i', $template)) {
101 | $template = 'number';
102 | }
103 |
104 | if ($this->isENotation((string)$input)) {
105 | $input = $this->convertENotationToRegularNumber((float)$input, $originalInput);
106 | }
107 |
108 | $numberString = (string)$input;
109 | $numberString = preg_replace_callback('/[\x{0660}-\x{0669}\x{06F0}-\x{06F9}]/u', function ($match) {
110 | return mb_chr(ord($match[0]) - 1728);
111 | }, $numberString);
112 | $numberString = preg_replace('/[^\d.-]/', '', $numberString);
113 |
114 | // Stripping leading zeros only, preserve trailing zeros
115 | $numberString = preg_replace('/^0+(?=\d)/', '', $numberString);
116 |
117 | $number = abs((float)$numberString);
118 |
119 | $p = $d = $r = $c = $f = 0;
120 |
121 | // Auto precision selection
122 | if ($precision === 'auto') {
123 | if (preg_match('/^(usd|irt|irr)$/i', $template)) {
124 | if ($number >= 0.0001 && $number < 100000000000) {
125 | $precision = 'high';
126 | } else {
127 | $precision = 'medium';
128 | }
129 | } elseif ($template === 'number') {
130 | $precision = 'medium';
131 | } elseif ($template === 'percent') {
132 | $precision = 'low';
133 | }
134 | }
135 |
136 | if ($precision === 'medium') {
137 | if ($number >= 0 && $number < 0.0001) {
138 | $p = 33;
139 | $d = 4;
140 | $r = false;
141 | $c = true;
142 | } elseif ($number >= 0.0001 && $number < 0.001) {
143 | $p = 7;
144 | $d = 4;
145 | $r = false;
146 | $c = false;
147 | } elseif ($number >= 0.001 && $number < 0.01) {
148 | $p = 5;
149 | $d = 3;
150 | $r = false;
151 | $c = false;
152 | } elseif ($number >= 0.001 && $number < 0.1) {
153 | $p = 3;
154 | $d = 2;
155 | $r = false;
156 | $c = false;
157 | } elseif ($number >= 0.1 && $number < 1) {
158 | $p = 1;
159 | $d = 1;
160 | $r = false;
161 | $c = false;
162 | } elseif ($number >= 1 && $number < 10) {
163 | $p = 3;
164 | $d = 3;
165 | $r = false;
166 | $c = false;
167 | } elseif ($number >= 10 && $number < 100) {
168 | $p = 2;
169 | $d = 2;
170 | $r = false;
171 | $c = false;
172 | } elseif ($number >= 100 && $number < 1000) {
173 | $p = 1;
174 | $d = 1;
175 | $r = false;
176 | $c = false;
177 | } elseif ($number >= 1000) {
178 | $x = floor(log10($number)) % 3;
179 | $p = 2 - $x;
180 | $d = 2 - $x;
181 | $r = true;
182 | $c = true;
183 | } else {
184 | $p = 0;
185 | $d = 0;
186 | $r = true;
187 | $c = true;
188 | }
189 | } elseif ($precision === 'low') {
190 | if ($number >= 0 && $number < 0.01) {
191 | $p = 2;
192 | $d = 0;
193 | $r = true;
194 | $c = false;
195 | $f = 2;
196 | } elseif ($number >= 0.01 && $number < 0.1) {
197 | $p = 2;
198 | $d = 1;
199 | $r = true;
200 | $c = false;
201 | } elseif ($number >= 0.1 && $number < 1) {
202 | $p = 2;
203 | $d = 2;
204 | $r = true;
205 | $c = false;
206 | } elseif ($number >= 1 && $number < 10) {
207 | $p = 2;
208 | $d = 2;
209 | $r = true;
210 | $c = false;
211 | $f = 2;
212 | } elseif ($number >= 10 && $number < 100) {
213 | $p = 1;
214 | $d = 1;
215 | $r = true;
216 | $c = false;
217 | $f = 1;
218 | } elseif ($number >= 100 && $number < 1000) {
219 | $p = 0;
220 | $d = 0;
221 | $r = true;
222 | $c = false;
223 | } elseif ($number >= 1000) {
224 | $x = floor(log10($number)) % 3;
225 | $p = 1 - $x;
226 | $d = 1 - $x;
227 | $r = true;
228 | $c = true;
229 | } else {
230 | $p = 0;
231 | $d = 0;
232 | $r = true;
233 | $c = true;
234 | $f = 2;
235 | }
236 | } else {
237 | // precision === "high"
238 | if ($number >= 0 && $number < 1) {
239 | $p = 33;
240 | $d = 4;
241 | $r = false;
242 | $c = false;
243 | } elseif ($number >= 1 && $number < 10) {
244 | $p = 3;
245 | $d = 3;
246 | $r = true;
247 | $c = false;
248 | } elseif ($number >= 10 && $number < 100) {
249 | $p = 2;
250 | $d = 2;
251 | $r = true;
252 | $c = false;
253 | } elseif ($number >= 100 && $number < 1000) {
254 | $p = 2;
255 | $d = 2;
256 | $r = true;
257 | $c = false;
258 | } elseif ($number >= 1000 && $number < 10000) {
259 | $p = 1;
260 | $d = 1;
261 | $r = true;
262 | $c = false;
263 | } else {
264 | $p = 0;
265 | $d = 0;
266 | $r = true;
267 | $c = false;
268 | }
269 | }
270 |
271 | // For scientific notation, increase precision to ensure correct representation
272 | if ($this->isENotation($originalInput)) {
273 | $p = max($p, 20);
274 | $r = false;
275 | }
276 |
277 | return $this->reducePrecision(
278 | $numberString,
279 | $p,
280 | $d,
281 | $r,
282 | $c,
283 | $f,
284 | $template,
285 | $language,
286 | $outputFormat,
287 | $prefixMarker,
288 | $postfixMarker,
289 | $prefix,
290 | $postfix,
291 | $originalInput
292 | );
293 | }
294 |
295 | private function reducePrecision(
296 | $numberString,
297 | $precision = 30,
298 | $nonZeroDigits = 4,
299 | $round = false,
300 | $compress = false,
301 | $fixedDecimalZeros = 0,
302 | $template = 'number',
303 | $language = 'en',
304 | $outputFormat = 'plain',
305 | $prefixMarker = 'span',
306 | $postfixMarker = 'span',
307 | $prefix = '',
308 | $postfix = '',
309 | $originalInput = ''
310 | ) {
311 | if ($numberString === null || $numberString === '') {
312 | return [];
313 | }
314 |
315 | // FIXED: Handle negative zero
316 | if ($numberString === '-0' || $numberString === '-0.0') {
317 | $numberString = substr($numberString, 1); // Remove negative sign for zero
318 | }
319 |
320 | $maxPrecision = 30;
321 | $maxIntegerDigits = 21;
322 |
323 | $scaleUnits = preg_match('/^(number|percent)$/i', $template)
324 | ? [
325 | '' => '',
326 | 'K' => ' هزار',
327 | 'M' => ' میلیون',
328 | 'B' => ' میلیارد',
329 | 'T' => ' تریلیون',
330 | 'Qd' => ' کادریلیون',
331 | 'Qt' => ' کنتیلیون',
332 | ]
333 | : [
334 | '' => '',
335 | 'K' => ' هزار ت',
336 | 'M' => ' میلیون ت',
337 | 'B' => ' میلیارد ت',
338 | 'T' => ' همت',
339 | 'Qd' => ' هزار همت',
340 | 'Qt' => ' میلیون همت',
341 | ];
342 |
343 | $parts = [];
344 | preg_match('/^(-)?(\d+)\.?([0]*)(\d*)$/u', $numberString, $parts);
345 |
346 | if (empty($parts)) {
347 | return [];
348 | }
349 |
350 | $sign = isset($parts[1]) ? $parts[1] : '';
351 | $nonFractionalStr = $parts[2];
352 | $fractionalZeroStr = $parts[3];
353 | $fractionalNonZeroStr = $parts[4];
354 |
355 | $unitPrefix = '';
356 | $unitPostfix = '';
357 |
358 | if (strlen($fractionalZeroStr) >= $maxPrecision) {
359 | // Number is smaller than maximum precision
360 | $fractionalZeroStr = str_pad('', $maxPrecision - 1, '0');
361 | $fractionalNonZeroStr = '1';
362 | } elseif (strlen($fractionalZeroStr) + $nonZeroDigits > $precision) {
363 | // decrease non-zero digits
364 | $nonZeroDigits = $precision - strlen($fractionalZeroStr);
365 | if ($nonZeroDigits < 1) {
366 | $nonZeroDigits = 1;
367 | }
368 | } elseif (strlen($nonFractionalStr) > $maxIntegerDigits) {
369 | $nonFractionalStr = '0';
370 | $fractionalZeroStr = '';
371 | $fractionalNonZeroStr = '';
372 | }
373 |
374 | // compress large numbers
375 | if ($compress && strlen($nonFractionalStr) >= 4) {
376 | $scaleUnitKeys = array_keys($scaleUnits);
377 | $scaledWholeNumber = $nonFractionalStr;
378 | $unitIndex = 0;
379 | while ((int)$scaledWholeNumber > 999 && $unitIndex < count($scaleUnitKeys) - 1) {
380 | $scaledWholeNumber = number_format((float)$scaledWholeNumber / 1000, 2, '.', '');
381 | $unitIndex++;
382 | }
383 | $unitPostfix = $scaleUnitKeys[$unitIndex];
384 |
385 | if ($language == 'fa') {
386 | $unitPostfix = $scaleUnits[$scaleUnitKeys[$unitIndex]];
387 | }
388 |
389 | preg_match('/^(-)?(\d+)\.?([0]*)(\d*)$/u', $scaledWholeNumber, $parts);
390 | if (empty($parts)) {
391 | return [];
392 | }
393 | $nonFractionalStr = $parts[2];
394 | $fractionalZeroStr = $parts[3];
395 | $fractionalNonZeroStr = $parts[4];
396 | }
397 |
398 | // Truncate the fractional part or round it
399 | if (strlen($fractionalNonZeroStr) > $nonZeroDigits) {
400 | if (!$round) {
401 | $fractionalNonZeroStr = substr($fractionalNonZeroStr, 0, $nonZeroDigits);
402 | } else {
403 | if ((int)$fractionalNonZeroStr[$nonZeroDigits] < 5) {
404 | $fractionalNonZeroStr = substr($fractionalNonZeroStr, 0, $nonZeroDigits);
405 | } else {
406 | $fractionalNonZeroStr = (string)((int)substr($fractionalNonZeroStr, 0, $nonZeroDigits) + 1);
407 | // If overflow occurs (e.g., 999 + 1 = 1000), adjust the substring length
408 | if (strlen($fractionalNonZeroStr) > $nonZeroDigits) {
409 | if (strlen($fractionalZeroStr) > 0) {
410 | $fractionalZeroStr = substr($fractionalZeroStr, 0, -1);
411 | } else {
412 | $nonFractionalStr = (string)((float)$nonFractionalStr + 1);
413 | $fractionalNonZeroStr = substr($fractionalNonZeroStr, 1);
414 | }
415 | }
416 | }
417 | }
418 | }
419 |
420 | // Using dex style
421 | if ($compress && $fractionalZeroStr !== '' && $unitPostfix === '') {
422 | $fractionalZeroStr = '0' . preg_replace_callback('/\d/', function ($match) {
423 | return [
424 | '₀',
425 | '₁',
426 | '₂',
427 | '₃',
428 | '₄',
429 | '₅',
430 | '₆',
431 | '₇',
432 | '₈',
433 | '₉',
434 | ][$match[0]];
435 | }, (string)strlen($fractionalZeroStr));
436 | }
437 |
438 | $fractionalPartStr = $fractionalZeroStr . $fractionalNonZeroStr;
439 |
440 | // FIXED: Don't truncate trailing zeros when they're in the original string
441 | if (strlen($fractionalPartStr) > $precision && !strpos($originalInput, 'e') && !strpos($originalInput, 'E')) {
442 | $fractionalPartStr = substr($fractionalPartStr, 0, $precision);
443 | }
444 |
445 | // FIXED: For numbers with decimal point, check for trailing zeros
446 | if (strpos($originalInput, '.') !== false) {
447 | $originalParts = explode('.', $originalInput);
448 | if (count($originalParts) === 2) {
449 | $originalDecimal = $originalParts[1];
450 | // If original has more digits than what we have now, preserve those trailing zeros
451 | if (strlen($originalDecimal) > strlen($fractionalPartStr) && substr($originalDecimal, -1) === '0') {
452 | // Count trailing zeros in original
453 | $trailingZeros = 0;
454 | for ($i = strlen($originalDecimal) - 1; $i >= 0; $i--) {
455 | if ($originalDecimal[$i] === '0') {
456 | $trailingZeros++;
457 | } else {
458 | break;
459 | }
460 | }
461 | // Add back trailing zeros if they were in the original
462 | if ($trailingZeros > 0) {
463 | $fractionalPartStr = str_pad($fractionalPartStr, strlen($fractionalPartStr) + $trailingZeros, '0');
464 | }
465 | }
466 | }
467 | }
468 |
469 | // Output Formating, Prefix, Postfix
470 | if ($template === 'usd') {
471 | $unitPrefix = $language === 'en' ? '$' : '';
472 | if (!$unitPostfix) {
473 | $unitPostfix = $language === 'fa' ? ' دلار' : '';
474 | }
475 | } elseif ($template === 'irr') {
476 | if (!$unitPostfix) {
477 | $unitPostfix = $language === 'fa' ? ' ر' : ' R';
478 | }
479 | } elseif ($template === 'irt') {
480 | if (!$unitPostfix) {
481 | $unitPostfix = $language === 'fa' ? ' ت' : ' T';
482 | }
483 | } elseif ($template === 'percent') {
484 | if ($language === 'en') {
485 | $unitPostfix .= '%';
486 | } else {
487 | $unitPostfix .= !$unitPostfix ? '٪' : ' درصد';
488 | }
489 | }
490 | $unitPrefix = $prefix . $unitPrefix;
491 | $unitPostfix .= $postfix;
492 |
493 | if ($outputFormat === 'html') {
494 | if ($unitPrefix) {
495 | $unitPrefix = '<' . $prefixMarker . '>' . $unitPrefix . '' . $prefixMarker . '>';
496 | }
497 | if ($unitPostfix) {
498 | $unitPostfix = '<' . $postfixMarker . '>' . $unitPostfix . '' . $postfixMarker . '>';
499 | }
500 | } elseif ($outputFormat === 'markdown') {
501 | if ($unitPrefix) {
502 | $unitPrefix = $prefixMarker . $unitPrefix . $prefixMarker;
503 | }
504 | if ($unitPostfix) {
505 | $unitPostfix = $postfixMarker . $unitPostfix . $postfixMarker;
506 | }
507 | }
508 |
509 | $thousandSeparatorRegex = '/\B(?=(\d{3})+(?!\d))/';
510 |
511 | $fixedDecimalZeroStr = $fixedDecimalZeros
512 | ? str_pad('.', $fixedDecimalZeros + 1, '0')
513 | : '';
514 |
515 | $wholeNumberStr = '';
516 |
517 | // FIXED: Changed condition to correctly handle numbers with trailing zeros
518 | if ($precision <= 0 || $nonZeroDigits <= 0 || ($fractionalNonZeroStr === '' && $fractionalZeroStr === '')) {
519 | $wholeNumberStr = number_format((float)$nonFractionalStr, 0, '', ',') . $fixedDecimalZeroStr;
520 | } else {
521 | $wholeNumberStr = number_format((float)$nonFractionalStr, 0, '', ',') . '.' . $fractionalPartStr;
522 | }
523 |
524 | $out = $sign . $unitPrefix . $wholeNumberStr . $unitPostfix;
525 |
526 | $formattedObject = [
527 | 'value' => $out,
528 | 'prefix' => $unitPrefix,
529 | 'postfix' => $unitPostfix,
530 | 'sign' => $sign,
531 | 'wholeNumber' => $wholeNumberStr,
532 | ];
533 |
534 | // Convert output to Persian numerals if language is "fa"
535 | if ($language === 'fa') {
536 | $formattedObject['value'] = preg_replace_callback('/[0-9]/', function ($match) {
537 | return mb_chr(ord($match[0]) + 1728);
538 | }, $formattedObject['value']);
539 | $formattedObject['postfix'] = preg_replace_callback('/[0-9]/', function ($match) {
540 | return mb_chr(ord($match[0]) + 1728);
541 | }, $formattedObject['postfix']);
542 | $formattedObject['wholeNumber'] = preg_replace_callback('/[0-9]/', function ($match) {
543 | return mb_chr(ord($match[0]) + 1728);
544 | }, $formattedObject['wholeNumber']);
545 | }
546 |
547 | return $formattedObject;
548 | }
549 |
550 | // FIXED: Improved scientific notation conversion
551 | private function convertENotationToRegularNumber($eNotation, $originalInput)
552 | {
553 | // For simple cases like 1e3, directly format as a regular number
554 | if (is_int($eNotation) && $eNotation >= 1000) {
555 | return number_format($eNotation, 0, '.', '');
556 | }
557 |
558 | $parts = explode('e', strtolower((string)$originalInput));
559 | if (count($parts) !== 2) {
560 | return (string)$eNotation;
561 | }
562 |
563 | $coefficient = (float)$parts[0];
564 | $exponent = (int)$parts[1];
565 |
566 | // Handle negative exponents (very small numbers)
567 | if ($exponent < 0) {
568 | $absExponent = abs($exponent);
569 | // Determine precision needed to show all digits
570 | $precision = $absExponent;
571 | if (strpos($parts[0], '.') !== false) {
572 | $precision += strlen(explode('.', $parts[0])[1]);
573 | }
574 | return number_format($eNotation, $precision, '.', '');
575 | }
576 |
577 | // For positive exponents, format to show as a regular number
578 | return number_format($eNotation, 0, '.', '');
579 | }
580 | }
--------------------------------------------------------------------------------
/php/tests/NumberFormatterTest.php:
--------------------------------------------------------------------------------
1 | 'fa',
13 | 'template' => 'usd',
14 | 'precision' => 'auto'
15 | ]);
16 | $this->assertEquals('۴۲۳ میلیون همت', $formatter->toString('423000000000000000000'));
17 | }
18 |
19 | public function testSetLanguage()
20 | {
21 | $formatter = new NumberFormatter();
22 | $formatter->setLanguage('en', ['prefixMarker' => 'span', 'postfixMarker' => 'span', 'prefix' => '', 'postfix' => '']);
23 | // exit(var_dump($formatter->toString('123')));
24 | $this->assertEquals('123', $formatter->toHtmlString('123'));
25 | }
26 |
27 | public function testSetTemplate()
28 | {
29 | $formatter = new NumberFormatter();
30 | $formatter->setTemplate('usd', 'high');
31 | $this->assertEquals('$123', $formatter->toString('123'));
32 | }
33 |
34 | // public function testToJson()
35 | // {
36 | // $formatter = new NumberFormatter();
37 | // $this->assertJsonStringEqualsJsonString( json_decode(['prefix'=> '', 'postfix'=> '', 'sign'=> '', 'wholeNumber'=> '123' ]), $formatter->toJson('123'));
38 | // }
39 |
40 | public function testToPlainString()
41 | {
42 | $formatter = new NumberFormatter();
43 | $this->assertEquals('123', $formatter->toPlainString('123'));
44 | }
45 |
46 | public function testToHtmlString()
47 | {
48 | $formatter = new NumberFormatter();
49 | $this->assertEquals('123', $formatter->toHtmlString('123'));
50 | }
51 |
52 | public function testToMdString()
53 | {
54 | $formatter = new NumberFormatter();
55 | $this->assertEquals('123', $formatter->toMdString('123'));
56 | }
57 |
58 | public function testENotationConversion()
59 | {
60 | $formatter = new NumberFormatter();
61 | $this->assertEquals('1.23', $formatter->toString('1.23e0'));
62 | $this->assertEquals('1.233', $formatter->toString('1.23e3'));
63 | $this->assertEquals('0.00123', $formatter->toString('1.23e-3'));
64 | }
65 |
66 | public function testMediumPrecision()
67 | {
68 | $formatter = new NumberFormatter(['precision' => 'medium']);
69 | $this->assertEquals('0.0001', $formatter->toString('0.0001'));
70 | $this->assertEquals('0.01', $formatter->toString('0.01'));
71 | $this->assertEquals('0.1', $formatter->toString('0.1'));
72 | $this->assertEquals('1', $formatter->toString('1'));
73 | $this->assertEquals('10', $formatter->toString('10'));
74 | }
75 |
76 | public function testLowPrecision()
77 | {
78 | $formatter = new NumberFormatter(['precision' => 'low']);
79 | $this->assertEquals('0.00', $formatter->toString('0.0001'));
80 | $this->assertEquals('0.01', $formatter->toString('0.01'));
81 | $this->assertEquals('0.1', $formatter->toString('0.1'));
82 | $this->assertEquals('1.00', $formatter->toString('1'));
83 | $this->assertEquals('10.0', $formatter->toString('10'));
84 | }
85 |
86 | public function testHighPrecision()
87 | {
88 | $formatter = new NumberFormatter(['precision' => 'high']);
89 | $this->assertEquals('0.0001', $formatter->toString('0.0001'));
90 | $this->assertEquals('0.01', $formatter->toString('0.01'));
91 | $this->assertEquals('0.1', $formatter->toString('0.1'));
92 | $this->assertEquals('1', $formatter->toString('1'));
93 | $this->assertEquals('10', $formatter->toString('10'));
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/phpunit.xml.bak:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests
6 |
7 |
8 |
9 |
10 | ./src
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/ts/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Workflow for Codecov
2 | on: [push, pull_request]
3 | jobs:
4 | run:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v4
9 | with:
10 | fetch-depth: 0
11 | - name: Upload coverage reports to Codecov
12 | uses: codecov/codecov-action@v4.0.1
13 | with:
14 | token: ${{ secrets.CODECOV_TOKEN }}
15 | slug: ArzDigitalLabs/reduce-precision
16 |
--------------------------------------------------------------------------------
/ts/src/format/index.ts:
--------------------------------------------------------------------------------
1 | type Template = 'number' | 'usd' | 'irt' | 'irr' | 'percent';
2 | type Precision = 'auto' | 'high' | 'medium' | 'low';
3 | type Language = 'en' | 'fa';
4 | type OutputFormat = 'plain' | 'html' | 'markdown';
5 |
6 | interface FormattedObject {
7 | value?: string;
8 | prefix: string;
9 | postfix: string;
10 | fullPostfix?: string;
11 | sign: string;
12 | wholeNumber: string;
13 | }
14 |
15 | interface LanguageConfig {
16 | prefixMarker?: string;
17 | postfixMarker?: string;
18 | prefix?: string;
19 | postfix?: string;
20 | thousandSeparator?: string;
21 | decimalSeparator?: string;
22 | }
23 |
24 | interface Options extends LanguageConfig {
25 | precision?: Precision;
26 | template?: Template;
27 | language?: Language;
28 | outputFormat?: OutputFormat;
29 | }
30 |
31 | class NumberFormatter {
32 | private readonly languageBaseConfig: LanguageConfig = {
33 | prefixMarker: 'i',
34 | postfixMarker: 'i',
35 | prefix: '',
36 | postfix: '',
37 | };
38 |
39 | private defaultLanguageConfig: { [key in Language]: LanguageConfig } = {
40 | en: {
41 | ...this.languageBaseConfig,
42 | thousandSeparator: ',',
43 | decimalSeparator: '.',
44 | },
45 | fa: {
46 | ...this.languageBaseConfig,
47 | thousandSeparator: '٫',
48 | decimalSeparator: '٬',
49 | },
50 | };
51 |
52 | private options: Options = {
53 | language: 'en',
54 | template: 'number',
55 | precision: 'high',
56 | outputFormat: 'plain',
57 | ...this.defaultLanguageConfig['en'],
58 | };
59 |
60 | constructor(options: Options = {}) {
61 | this.options = { ...this.options, ...options };
62 | }
63 | setLanguage(lang: Language, config: LanguageConfig = {}): NumberFormatter {
64 | this.options.language = lang;
65 | this.options.prefixMarker =
66 | config.prefixMarker || this.defaultLanguageConfig[lang].prefixMarker;
67 | this.options.postfixMarker =
68 | config.postfixMarker || this.defaultLanguageConfig[lang].postfixMarker;
69 | this.options.prefix =
70 | config.prefix || this.defaultLanguageConfig[lang].prefix;
71 | this.options.postfix =
72 | config.postfix || this.defaultLanguageConfig[lang].postfix;
73 | this.options.thousandSeparator =
74 | config.thousandSeparator ||
75 | this.defaultLanguageConfig[lang].thousandSeparator;
76 | this.options.decimalSeparator =
77 | config.decimalSeparator ||
78 | this.defaultLanguageConfig[lang].decimalSeparator;
79 | return this;
80 | }
81 |
82 | setTemplate(template: Template, precision: Precision): NumberFormatter {
83 | this.options.template = template;
84 | this.options.precision = precision;
85 | return this;
86 | }
87 |
88 | toJson(input: string | number): FormattedObject {
89 | const formattedObject = this.format(input);
90 | delete formattedObject.value;
91 |
92 | return formattedObject;
93 | }
94 |
95 | toString(input: string | number): string {
96 | const formattedObject = this.format(input);
97 | return formattedObject.value || '';
98 | }
99 |
100 | toPlainString(input: string | number): string {
101 | this.options.outputFormat = 'plain';
102 | const formattedObject = this.format(input);
103 | return formattedObject.value || '';
104 | }
105 |
106 | toHtmlString(input: string | number): string {
107 | this.options.outputFormat = 'html';
108 | const formattedObject = this.format(input);
109 | return formattedObject.value || '';
110 | }
111 |
112 | toMdString(input: string | number): string {
113 | this.options.outputFormat = 'markdown';
114 | const formattedObject = this.format(input);
115 | return formattedObject.value || '';
116 | }
117 |
118 | // Private methods...
119 | private isENotation(input: string): boolean {
120 | return /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)$/.test(input);
121 | }
122 |
123 | private format(input: string | number): FormattedObject {
124 | let { precision, template } = this.options;
125 |
126 | const {
127 | language,
128 | outputFormat,
129 | prefixMarker,
130 | postfixMarker,
131 | prefix,
132 | postfix,
133 | thousandSeparator,
134 | decimalSeparator,
135 | } = this.options;
136 |
137 | if (input === undefined || input === null || input === '') {
138 | return {} as FormattedObject;
139 | }
140 |
141 | if (!template?.match(/^(number|usd|irt|irr|percent)$/g))
142 | template = 'number';
143 |
144 | // Store original input string to preserve format for trailing zeros
145 | const originalInput = input.toString();
146 |
147 | if (this.isENotation(originalInput)) {
148 | input = this.convertENotationToRegularNumber(Number(input));
149 | }
150 |
151 | // Replace each Persian/Arabic numeral in the string with its English counterpart and strip all non-numeric chars
152 | let numberString = input
153 | .toString()
154 | .replace(/[\u0660-\u0669\u06F0-\u06F9]/g, function (match: string) {
155 | return String(match.charCodeAt(0) & 0xf);
156 | })
157 | .replace(/[^\d.-]/g, '');
158 |
159 | // Stripping leading zeros only, preserve trailing zeros
160 | numberString = numberString
161 | .replace(/^0+(?=\d)/g, '');
162 |
163 | const number = Math.abs(Number(numberString));
164 | let p, d, r, c;
165 | let f = 0;
166 |
167 | // Auto precision selection
168 | if (precision === 'auto') {
169 | if (template.match(/^(usd|irt|irr|number)$/g)) {
170 | if (number >= 0.0001 && number < 100_000_000_000) {
171 | precision = 'high';
172 | } else {
173 | precision = 'medium';
174 | }
175 | } else if (template === 'percent') {
176 | precision = 'low';
177 | }
178 | }
179 |
180 | if (precision === 'medium') {
181 | if (number >= 0 && number < 0.0001) {
182 | p = 33;
183 | d = 4;
184 | r = false;
185 | c = true;
186 | } else if (number >= 0.0001 && number < 0.001) {
187 | p = 7;
188 | d = 4;
189 | r = false;
190 | c = false;
191 | } else if (number >= 0.001 && number < 0.01) {
192 | p = 5;
193 | d = 3;
194 | r = false;
195 | c = false;
196 | } else if (number >= 0.001 && number < 0.1) {
197 | p = 3;
198 | d = 2;
199 | r = false;
200 | c = false;
201 | } else if (number >= 0.1 && number < 1) {
202 | p = 1;
203 | d = 1;
204 | r = false;
205 | c = false;
206 | } else if (number >= 1 && number < 10) {
207 | p = 3;
208 | d = 3;
209 | r = false;
210 | c = false;
211 | } else if (number >= 10 && number < 100) {
212 | p = 2;
213 | d = 2;
214 | r = false;
215 | c = false;
216 | } else if (number >= 100 && number < 1000) {
217 | p = 1;
218 | d = 1;
219 | r = false;
220 | c = false;
221 | } else if (number >= 1000) {
222 | const x = Math.floor(Math.log10(number)) % 3;
223 | p = 2 - x;
224 | d = 2 - x;
225 | r = true;
226 | c = true;
227 | } else {
228 | p = 0;
229 | d = 0;
230 | r = true;
231 | c = true;
232 | }
233 | } else if (precision === 'low') {
234 | if (number >= 0 && number < 0.01) {
235 | p = 2;
236 | d = 0;
237 | r = true;
238 | c = false;
239 | f = 2;
240 | } else if (number >= 0.01 && number < 0.1) {
241 | p = 2;
242 | d = 1;
243 | r = true;
244 | c = false;
245 | } else if (number >= 0.1 && number < 1) {
246 | p = 2;
247 | d = 2;
248 | r = true;
249 | c = false;
250 | } else if (number >= 1 && number < 10) {
251 | p = 2;
252 | d = 2;
253 | r = true;
254 | c = false;
255 | f = 2;
256 | } else if (number >= 10 && number < 100) {
257 | p = 1;
258 | d = 1;
259 | r = true;
260 | c = false;
261 | f = 1;
262 | } else if (number >= 100 && number < 1000) {
263 | p = 0;
264 | d = 0;
265 | r = true;
266 | c = false;
267 | } else if (number >= 1000) {
268 | const x = Math.floor(Math.log10(number)) % 3;
269 | p = 1 - x;
270 | d = 1 - x;
271 | r = true;
272 | c = true;
273 | } else {
274 | p = 0;
275 | d = 0;
276 | r = true;
277 | c = true;
278 | f = 2;
279 | }
280 | } else {
281 | // precision === "high"
282 | if (number >= 0 && number < 1) {
283 | p = 33;
284 | d = 4;
285 | r = false;
286 | c = false;
287 | } else if (number >= 1 && number < 10) {
288 | p = 3;
289 | d = 3;
290 | r = true;
291 | c = false;
292 | } else if (number >= 10 && number < 100) {
293 | p = 2;
294 | d = 2;
295 | r = true;
296 | c = false;
297 | } else if (number >= 100 && number < 1000) {
298 | p = 2;
299 | d = 2;
300 | r = true;
301 | c = false;
302 | } else if (number >= 1000 && number < 10000) {
303 | p = 1;
304 | d = 1;
305 | r = true;
306 | c = false;
307 | } else {
308 | p = 0;
309 | d = 0;
310 | r = true;
311 | c = false;
312 | }
313 | }
314 |
315 | // For scientific notation, increase precision to ensure correct representation
316 | if (this.isENotation(originalInput)) {
317 | p = Math.max(p, 20);
318 | r = false;
319 | }
320 |
321 | return this.reducePrecision(
322 | numberString,
323 | p,
324 | d,
325 | r,
326 | c,
327 | f,
328 | template,
329 | language,
330 | outputFormat,
331 | prefixMarker,
332 | postfixMarker,
333 | prefix,
334 | postfix,
335 | thousandSeparator,
336 | decimalSeparator,
337 | originalInput
338 | );
339 | }
340 |
341 | private convertENotationToRegularNumber(eNotation: number): string {
342 | // For simple cases like 1e3, directly use Number constructor
343 | if (Number.isInteger(eNotation) && eNotation >= 1000) {
344 | return eNotation.toString();
345 | }
346 |
347 | const parts = eNotation.toString().toLowerCase().split('e');
348 | if (parts.length !== 2) return eNotation.toString();
349 |
350 | const coefficient = parseFloat(parts[0]);
351 | const exponent = parseInt(parts[1], 10);
352 |
353 | // Handle negative exponents (very small numbers)
354 | if (exponent < 0) {
355 | const absExponent = Math.abs(exponent);
356 | // Determine precision needed to show all digits
357 | const precision = absExponent +
358 | (parts[0].includes('.') ? parts[0].split('.')[1].length : 0);
359 | return eNotation.toFixed(precision);
360 | }
361 |
362 | // For positive exponents, let JavaScript do the conversion
363 | return eNotation.toString();
364 | }
365 |
366 | private reducePrecision(
367 | numberString: string,
368 | precision = 30,
369 | nonZeroDigits = 4,
370 | round = false,
371 | compress = false,
372 | fixedDecimalZeros = 0,
373 | template = 'number',
374 | language = 'en',
375 | outputFormat = 'plain',
376 | prefixMarker = 'span',
377 | postfixMarker = 'span',
378 | prefix = '',
379 | postfix = '',
380 | thousandSeparator = ',',
381 | decimalSeparator = '.',
382 | originalInput = ''
383 | ) {
384 | if (numberString === undefined || numberString === null || numberString.trim() === '') {
385 | return {} as FormattedObject;
386 | }
387 |
388 | // Handle negative zero
389 | if (numberString === '-0' || numberString === '-0.0') {
390 | numberString = numberString.substring(1); // Remove negative sign for zero
391 | }
392 |
393 | numberString = numberString.toString();
394 |
395 | const maxPrecision = 30;
396 | const maxIntegerDigits = 21;
397 | const scaleUnits = template.match(/^(number|percent)$/g)
398 | ? {
399 | '': '',
400 | K: ' هزار',
401 | M: ' میلیون',
402 | B: ' میلیارد',
403 | T: ' تریلیون',
404 | Qd: ' کادریلیون',
405 | Qt: ' کنتیلیون',
406 | }
407 | : {
408 | '': '',
409 | K: ' هزار ت',
410 | M: ' میلیون ت',
411 | B: ' میلیارد ت',
412 | T: ' همت',
413 | Qd: ' هزار همت',
414 | Qt: ' میلیون همت',
415 | };
416 |
417 | const fullScaleUnits = template.match(/^(number|percent)$/g)
418 | ? {
419 | '': '',
420 | K: ' هزار',
421 | M: ' میلیون',
422 | B: ' میلیارد',
423 | T: ' تریلیون',
424 | Qd: ' کادریلیون',
425 | Qt: ' کنتیلیون',
426 | }
427 | : {
428 | '': '',
429 | K: ' هزار تومان',
430 | M: ' میلیون تومان',
431 | B: ' میلیارد تومان',
432 | T: ' هزار میلیارد تومان',
433 | Qd: ' کادریلیون تومان',
434 | Qt: ' کنتیلیون تومان',
435 | };
436 |
437 | let parts = /^(-)?(\d+)\.?([0]*)(\d*)$/g.exec(numberString);
438 |
439 | if (!parts) {
440 | return {} as FormattedObject;
441 | }
442 |
443 | const sign = parts[1] || '';
444 | let nonFractionalStr = parts[2];
445 | let fractionalZeroStr = parts[3];
446 | let fractionalNonZeroStr = parts[4];
447 | let unitPrefix = '';
448 | let unitPostfix = '';
449 |
450 | if (fractionalZeroStr.length >= maxPrecision) {
451 | // Number is smaller than maximum precision
452 | fractionalZeroStr = '0'.padEnd(maxPrecision - 1, '0');
453 | fractionalNonZeroStr = '1';
454 | } else if (fractionalZeroStr.length + nonZeroDigits > precision) {
455 | // decrease non-zero digits
456 | nonZeroDigits = precision - fractionalZeroStr.length;
457 | if (nonZeroDigits < 1) nonZeroDigits = 1;
458 | } else if (nonFractionalStr.length > maxIntegerDigits) {
459 | nonFractionalStr = '0';
460 | fractionalZeroStr = '';
461 | fractionalNonZeroStr = '';
462 | }
463 |
464 | // compress large numbers
465 | if (compress && nonFractionalStr.length >= 4) {
466 | const scaleUnitKeys = Object.keys(scaleUnits);
467 | let scaledWholeNumber = nonFractionalStr;
468 | let unitIndex = 0;
469 | while (+scaledWholeNumber > 999 && unitIndex < scaleUnitKeys.length - 1) {
470 | scaledWholeNumber = (+scaledWholeNumber / 1000).toFixed(2);
471 | unitIndex++;
472 | }
473 | unitPostfix = scaleUnitKeys[unitIndex];
474 | parts = /^(-)?(\d+)\.?([0]*)(\d*)$/g.exec(scaledWholeNumber.toString());
475 |
476 | if (!parts) {
477 | return {} as FormattedObject;
478 | }
479 |
480 | // sign = parts[1] || "";
481 | nonFractionalStr = parts[2];
482 | fractionalZeroStr = parts[3];
483 | fractionalNonZeroStr = parts[4];
484 | }
485 |
486 | // Truncate the fractional part or round it
487 | // if (precision > 0 && nonZeroDigits > 0 && fractionalNonZeroStr.length > nonZeroDigits) {
488 | if (fractionalNonZeroStr.length > nonZeroDigits) {
489 | if (!round) {
490 | fractionalNonZeroStr = fractionalNonZeroStr.substring(0, nonZeroDigits);
491 | } else {
492 | if (parseInt(fractionalNonZeroStr[nonZeroDigits]) < 5) {
493 | fractionalNonZeroStr = fractionalNonZeroStr.substring(
494 | 0,
495 | nonZeroDigits
496 | );
497 | } else {
498 | fractionalNonZeroStr = (
499 | parseInt(fractionalNonZeroStr.substring(0, nonZeroDigits)) + 1
500 | ).toString();
501 | // If overflow occurs (e.g., 999 + 1 = 1000), adjust the substring length
502 | if (fractionalNonZeroStr.length > nonZeroDigits) {
503 | if (fractionalZeroStr.length > 0) {
504 | fractionalZeroStr = fractionalZeroStr.substring(
505 | 0,
506 | fractionalZeroStr.length - 1
507 | );
508 | } else {
509 | nonFractionalStr = (Number(nonFractionalStr) + 1).toString();
510 | fractionalNonZeroStr = fractionalNonZeroStr.substring(1);
511 | }
512 | }
513 | }
514 | }
515 | }
516 |
517 | // Using dex style
518 | if (compress && fractionalZeroStr !== '' && unitPostfix === '') {
519 | fractionalZeroStr =
520 | '0' +
521 | fractionalZeroStr.length.toString().replace(/\d/g, function (match) {
522 | return [
523 | '₀',
524 | '₁',
525 | '₂',
526 | '₃',
527 | '₄',
528 | '₅',
529 | '₆',
530 | '₇',
531 | '₈',
532 | '₉',
533 | ][parseInt(match, 10)];
534 | });
535 | }
536 |
537 | // Check if the original input had trailing zeros
538 | let fractionalPartStr = `${fractionalZeroStr}${fractionalNonZeroStr}`;
539 | // Don't truncate trailing zeros when they're in the original string
540 | if (fractionalPartStr.length > precision && !originalInput.includes('e')) {
541 | fractionalPartStr = fractionalPartStr.substring(0, precision);
542 | }
543 |
544 | // For scientific notation and numbers with trailing zeros, preserve the format
545 | if (originalInput.includes('e') || originalInput.includes('E')) {
546 | // For scientific notation, use the converted string
547 | } else if (originalInput.includes('.')) {
548 | // For regular numbers with decimal point, check for trailing zeros
549 | const originalParts = originalInput.split('.');
550 | if (originalParts.length === 2) {
551 | const originalDecimal = originalParts[1];
552 | // If original has more digits than what we have now, preserve those trailing zeros
553 | if (originalDecimal.length > fractionalPartStr.length && originalDecimal.endsWith('0')) {
554 | // Count trailing zeros in original
555 | let trailingZeros = 0;
556 | for (let i = originalDecimal.length - 1; i >= 0; i--) {
557 | if (originalDecimal[i] === '0') {
558 | trailingZeros++;
559 | } else {
560 | break;
561 | }
562 | }
563 | // Add back trailing zeros if they were in the original
564 | if (trailingZeros > 0) {
565 | fractionalPartStr = fractionalPartStr.padEnd(fractionalPartStr.length + trailingZeros, '0');
566 | }
567 | }
568 | }
569 | }
570 |
571 | // Output Formating, Prefix, Postfix
572 | if (template === 'usd') {
573 | unitPrefix = language === 'en' ? '$' : '';
574 | if (!unitPostfix) unitPostfix = language === 'fa' ? ' دلار' : '';
575 | } else if (template === 'irr') {
576 | if (!unitPostfix) unitPostfix = language === 'fa' ? ' ر' : ' R';
577 | } else if (template === 'irt') {
578 | if (!unitPostfix) unitPostfix = language === 'fa' ? ' ت' : ' T';
579 | } else if (template === 'percent') {
580 | if (language === 'en') {
581 | unitPostfix += '%';
582 | } else {
583 | unitPostfix += !unitPostfix ? '٪' : ' درصد';
584 | }
585 | }
586 | unitPrefix = prefix + unitPrefix;
587 | unitPostfix += postfix;
588 | if (outputFormat === 'html') {
589 | if (unitPrefix)
590 | unitPrefix = `<${prefixMarker}>${unitPrefix}${prefixMarker}>`;
591 | if (unitPostfix)
592 | unitPostfix = `<${postfixMarker}>${unitPostfix}${postfixMarker}>`;
593 | } else if (outputFormat === 'markdown') {
594 | if (unitPrefix)
595 | unitPrefix = `${prefixMarker}${unitPrefix}${prefixMarker}`;
596 | if (unitPostfix)
597 | unitPostfix = `${postfixMarker}${unitPostfix}${postfixMarker}`;
598 | }
599 |
600 | const thousandSeparatorRegex = /\B(?=(\d{3})+(?!\d))/g;
601 | const fixedDecimalZeroStr = fixedDecimalZeros
602 | ? '.'.padEnd(fixedDecimalZeros + 1, '0')
603 | : '';
604 | let out = '';
605 | let wholeNumberStr;
606 |
607 | // FIXED: Changed condition to correctly handle numbers with trailing zeros
608 | // Old condition: if (precision <= 0 || nonZeroDigits <= 0 || !fractionalNonZeroStr) {
609 | // New condition checks if both fractional parts are empty
610 | if (precision <= 0 || nonZeroDigits <= 0 || (fractionalNonZeroStr === '' && fractionalZeroStr === '')) {
611 | wholeNumberStr = `${nonFractionalStr.replace(
612 | thousandSeparatorRegex,
613 | ','
614 | )}${fixedDecimalZeroStr}`;
615 | } else {
616 | wholeNumberStr = `${nonFractionalStr.replace(
617 | thousandSeparatorRegex,
618 | ','
619 | )}.${fractionalPartStr}`;
620 | }
621 |
622 | out = `${sign}${unitPrefix}${wholeNumberStr}${unitPostfix}`;
623 |
624 | const formattedObject: FormattedObject = {
625 | value: out,
626 | prefix: unitPrefix,
627 | postfix: unitPostfix,
628 | sign: sign,
629 | wholeNumber: wholeNumberStr,
630 | };
631 |
632 | // replace custom config
633 | formattedObject.value = (formattedObject?.value ?? '')
634 | .replace(/,/g, thousandSeparator)
635 | .replace(/\./g, decimalSeparator);
636 |
637 | // Convert output to Persian numerals if language is "fa"
638 | if (language === 'fa') {
639 | formattedObject.value = (formattedObject?.value ?? '')
640 | .replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) + 1728))
641 | .replace(/(K|M|B|T|Qt|Qd)/g, function (c: string) {
642 | return String(scaleUnits[c as keyof typeof scaleUnits]);
643 | });
644 |
645 | formattedObject.fullPostfix = unitPostfix
646 | .replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) + 1728))
647 | .replace(/(K|M|B|T|Qt|Qd)/g, function (c: string) {
648 | return String(fullScaleUnits[c as keyof typeof fullScaleUnits]);
649 | });
650 |
651 | formattedObject.postfix = formattedObject.postfix
652 | .replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) + 1728))
653 | .replace(/(K|M|B|T|Qt|Qd)/g, function (c: string) {
654 | return String(scaleUnits[c as keyof typeof scaleUnits]);
655 | });
656 |
657 | formattedObject.wholeNumber = formattedObject.wholeNumber
658 | .replace(/[0-9]/g, c => String.fromCharCode(c.charCodeAt(0) + 1728))
659 | .replace(/(K|M|B|T|Qt|Qd)/g, function (c: string) {
660 | return String(scaleUnits[c as keyof typeof scaleUnits]);
661 | });
662 | }
663 |
664 | return formattedObject;
665 | }
666 | }
667 |
668 | export default NumberFormatter;
--------------------------------------------------------------------------------
/ts/src/index.ts:
--------------------------------------------------------------------------------
1 | import NumberFormatter from './format';
2 |
3 | export { NumberFormatter };
4 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": [
4 | "test/**/*.spec.ts",
5 | ]
6 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "outDir": "./ts/lib/",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true
11 | },
12 | "include": ["ts/src/**/*.ts", "ts/test/**/*.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './ts/src/index.ts', // Adjust this to the entry point of your application
5 | output: {
6 | path: path.resolve(__dirname, 'ts/lib'), // The output directory
7 | filename: 'bundle.min.js', // The name of the bundled file
8 | libraryTarget: 'umd', // The format of the bundled file
9 | library: 'ReducePrecision',
10 | },
11 | resolve: {
12 | extensions: ['.ts', '.js'], // Add `.ts` as a resolvable extension
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /\.ts$/,
18 | exclude: /node_modules/,
19 | use: 'ts-loader', // Use ts-loader to handle TypeScript files
20 | },
21 | {
22 | test: /\.js$/,
23 | exclude: /node_modules/,
24 | use: {
25 | loader: 'babel-loader', // Transpile ES6+ code to ES5 for compatibility
26 | options: {
27 | presets: ['@babel/preset-env'],
28 | },
29 | },
30 | },
31 | ],
32 | },
33 | mode: 'production', // Enable optimizations like minification
34 | };
35 |
--------------------------------------------------------------------------------