├── .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 | [![Known Vulnerabilities](https://snyk.io/test/github/ArzDigitalLabs/reduce-precision/badge.svg?targetFile=package.json)](https://snyk.io/test/github/ArzDigitalLabs/reduce-precision?targetFile=package.json) 6 | [![Build Status](https://travis-ci.org/ArzDigitalLabs/reduce-precision.svg?branch=master)](https://travis-ci.org/ArzDigitalLabs/reduce-precision) 7 | [![codecov.io Code Coverage](https://img.shields.io/codecov/c/github/ArzDigitalLabs/reduce-precision.svg?maxAge=2592000)](https://codecov.io/github/ArzDigitalLabs/reduce-precision?branch=master) 8 | [![Code Climate](https://codeclimate.com/github/ArzDigitalLabs/reduce-precision/badges/gpa.svg)](https://codeclimate.com/github/ArzDigitalLabs/reduce-precision) 9 | [![NPM Version](https://badge.fury.io/js/reduce-precision.svg?style=flat)](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 | [![NPM Download Stats](https://nodei.co/npm/reduce-precision.png?downloads=true)](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}`),N&&(N=`<${p}>${N}`)):"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}`; 545 | if (unitPostfix) 546 | unitPostfix = `<${postfixMarker}>${unitPostfix}`; 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 . ''; 496 | } 497 | if ($unitPostfix) { 498 | $unitPostfix = '<' . $postfixMarker . '>' . $unitPostfix . ''; 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}`; 591 | if (unitPostfix) 592 | unitPostfix = `<${postfixMarker}>${unitPostfix}`; 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 | --------------------------------------------------------------------------------