From 0a1bc33c7094ac538e92f0eaa7a93a79d37a6207 Mon Sep 17 00:00:00 2001 From: shrpne Date: Mon, 30 Jul 2018 22:05:32 +0300 Subject: [PATCH] initial --- .babelrc | 3 ++ .editorconfig | 13 ++++++ .eslintrc.js | 51 ++++++++++++++++++++++ .gitignore | 10 +++++ .travis.yml | 9 ++++ LICENSE | 21 +++++++++ jest.config.js | 13 ++++++ package.json | 44 +++++++++++++++++++ src/from-exponential.js | 80 +++++++++++++++++++++++++++++++++++ src/index.js | 19 +++++++++ src/reduce-precision.js | 26 ++++++++++++ src/round.js | 13 ++++++ src/strip-zeros.js | 16 +++++++ test/from-exponential.test.js | 49 +++++++++++++++++++++ test/index.test.js | 14 ++++++ test/reduce-precision.test.js | 25 +++++++++++ test/round.test.js | 16 +++++++ test/strip-zeros.test.js | 34 +++++++++++++++ 18 files changed, 456 insertions(+) create mode 100644 .babelrc create mode 100644 .editorconfig create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 jest.config.js create mode 100644 package.json create mode 100644 src/from-exponential.js create mode 100644 src/index.js create mode 100644 src/reduce-precision.js create mode 100644 src/round.js create mode 100644 src/strip-zeros.js create mode 100644 test/from-exponential.test.js create mode 100644 test/index.test.js create mode 100644 test/reduce-precision.test.js create mode 100644 test/round.test.js create mode 100644 test/strip-zeros.test.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..002b4aa --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["env"] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d83452c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{.babelrc,.stylelintrc,.eslintrc*,jest.config,*.json,*.jsb3,*.jsb2,*.bowerrc,*.yaml,*.yml}] +indent_size = 2 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..bed92dc --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,51 @@ +// http://eslint.org/docs/user-guide/configuring + +module.exports = { + root: true, + // parser: 'babel-eslint', + // parserOptions: { + // sourceType: 'module' + // }, + env: { + browser: true, + jest: true, + }, + // https://github.com/standard/standard/blob/master/docs/RULES-en.md + extends: 'airbnb-base', + // required to lint *.vue files + // plugins: [ + // 'html' + // ], + // // add your custom rules here + rules: { + 'indent': ["error", 4], + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + 'object-curly-spacing': 0, + // disable length limit + 'max-len': 0, + // allow `new Buffer()` + 'no-buffer-constructor': 0, + // allow assigning to function parameter + 'no-param-reassign': 0, + 'no-underscore-dangle': 0, + 'no-else-return': 0, + "no-unused-vars": ["warn", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }], + 'no-use-before-define' : 0, + 'object-curly-newline': 0, + 'import/prefer-default-export': 1, + }, + overrides: [ + { + files: ['examples/*'], + rules: { + "import/no-extraneous-dependencies": 0, + 'no-console': 0, + } + } + ] +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11bfe88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ + + +# dependencies +node_modules +package-lock.json + +/coverage/ + +# logs +npm-debug.log diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3e372a2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: + - stable + - lts/* +jobs: + include: + - stage: Produce Coverage + node_js: stable + script: jest --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..55e8076 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2018, Respective Authors all rights reserved. + +The MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..6c85e24 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + moduleNameMapper: { + '~(.*)$': '/$1', + }, + transform: { + '^.+\\.jsx?$': 'babel-jest', + }, + transformIgnorePatterns: [ + 'node_modules/(?!(minterjs-util|other-module)/)', + ], + /** fix jest bug @see https://github.com/facebook/jest/issues/6766 */ + testURL: 'http://localhost' +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..d085a8f --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "pretty-num", + "version": "0.0.1", + "description": "Lightweight module for formatting numbers to a human readable string", + "main": "src/index.js", + "files": [ + "/src/" + ], + "scripts": { + "prepublishOnly": "npm run lint && npm run test", + "lint": "eslint --ext .js ./src ./test", + "lint:fix": "eslint --ext .js ./src ./test --fix", + "test": "jest", + "coverage": "jest --coverage" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/shrpne/pretty-num.git" + }, + "keywords": [ + "format", + "pretty", + "number", + "human", + "readable", + "string", + "text" + ], + "author": "shrpne ", + "license": "MIT", + "bugs": { + "url": "https://github.com/shrpne/pretty-num/issues" + }, + "dependencies": {}, + "devDependencies": { + "babel-core": "^6.26.3", + "babel-jest": "^23.4.0", + "babel-preset-env": "^1.7.0", + "eslint": "^5.2.0", + "eslint-config-airbnb-base": "^13.0.0", + "eslint-plugin-import": "^2.13.0", + "jest": "^23.4.1" + } +} diff --git a/src/from-exponential.js b/src/from-exponential.js new file mode 100644 index 0000000..d9ea5af --- /dev/null +++ b/src/from-exponential.js @@ -0,0 +1,80 @@ +/** + * + * @param {number|string|Array} num - number or array of its parts + * @return {string} + */ + +export default function fromExponential(num) { + const eParts = getExponentialParts(num); + if (!isExponential(eParts)) { + throw new Error('Only exponential numbers can be converted properly'); + } + + const sign = eParts[0][0] === '-' ? '-' : ''; + const digits = eParts[0].replace(/^-/, ''); + const digitsParts = digits.split('.'); + const wholeDigits = digitsParts[0]; + const fractionDigits = digitsParts[1] || ''; + let e = Number(eParts[1]); + + if (e === 0) { + return `${sign + wholeDigits}.${fractionDigits}`; + } else if (e < 0) { + // move dot to the left + const countWholeAfterTransform = wholeDigits.length + e; + if (countWholeAfterTransform > 0) { + // transform whole to fraction + const wholeDigitsAfterTransform = wholeDigits.substr(0, countWholeAfterTransform); + const wholeDigitsTransformedToFracton = wholeDigits.substr(countWholeAfterTransform); + return `${sign + wholeDigitsAfterTransform}.${wholeDigitsTransformedToFracton}${fractionDigits}`; + } else { + // not enough whole digits: prepend with fractional zeros + + // first e goes to dotted zero + let zeros = '0.'; + e += 1; + while (e) { + zeros += '0'; + e += 1; + } + return sign + zeros + wholeDigits + fractionDigits; + } + } else { + // move dot to the right + const countFractionAfterTransform = fractionDigits.length - e; + if (countFractionAfterTransform > 0) { + // transform fraction to whole + // countTransformedFractionToWhole = e + const fractionDigitsAfterTransform = fractionDigits.substr(e); + const fractionDigitsTransformedToWhole = fractionDigits.substr(0, e); + return `${sign + wholeDigits + fractionDigitsTransformedToWhole}.${fractionDigitsAfterTransform}`; + } else { + // not enough fractions: append whole zeros + let zerosCount = -countFractionAfterTransform; + let zeros = ''; + while (zerosCount) { + zeros += '0'; + zerosCount -= 1; + } + return sign + wholeDigits + fractionDigits + zeros; + } + } +} + +/** + * Return two parts array of exponential number + * @param {number|string|Array} num + * @return {string[]} + */ +export function getExponentialParts(num) { + return Array.isArray(num) ? num : String(num).split(/[eE]/); +} + +/** + * + * @param {number|string|Array} num - number or array of its parts + */ +export function isExponential(num) { + const eParts = getExponentialParts(num); + return !Number.isNaN(Number(eParts[1])); +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..875e755 --- /dev/null +++ b/src/index.js @@ -0,0 +1,19 @@ +import stripZeros from './strip-zeros'; +import reducePrecision from './reduce-precision'; +import fromExponential from './from-exponential'; + +/** + * @param {number|string} num + * @param {number} [precision] + * @return {string} + */ +export default function prettyNum(num, precision) { + num = stripZeros(num); + const eParts = String(num).split(/[eE]/); + + if (eParts.length === 2) { + num = fromExponential(num); + } + + return reducePrecision(num, precision); +} diff --git a/src/reduce-precision.js b/src/reduce-precision.js new file mode 100644 index 0000000..24c6b41 --- /dev/null +++ b/src/reduce-precision.js @@ -0,0 +1,26 @@ +/** + * @param {string|number} num + * @param {number} precision + * @return {string} + */ +export default function reducePrecision(num, precision) { + const numString = num.toString(); + + // decimal exponential number + if ((/e-/i).test(numString)) { + return num.toPrecision(precision); + } + // integer exponential number + if ((/e/i).test(numString)) { + return numString; + } + + + const notMeaningfulFraction = numString.match(/\.0*/); + if (notMeaningfulFraction) { + const MeaningfulFractionIndex = notMeaningfulFraction.index + notMeaningfulFraction[0].length; + return numString.substr(0, MeaningfulFractionIndex + precision); + } + + return numString; +} diff --git a/src/round.js b/src/round.js new file mode 100644 index 0000000..6d130bb --- /dev/null +++ b/src/round.js @@ -0,0 +1,13 @@ +/** + * Round `value` to `fractionDigitNumber` digits after dot + * @param {number} value + * @param {number} fractionDigitNumber - number of fraction digits after dot + * @return {number} + */ +export default function round(value, fractionDigitNumber) { + if (fractionDigitNumber < 0) { + throw new Error('Number of fraction digits should be positive'); + } + const tenPower = 10 ** fractionDigitNumber; + return Math.round(value * tenPower) / tenPower; +} diff --git a/src/strip-zeros.js b/src/strip-zeros.js new file mode 100644 index 0000000..b687767 --- /dev/null +++ b/src/strip-zeros.js @@ -0,0 +1,16 @@ +/** + * Strip unnecessary last zeros after dot + * @param num + * @return {string|number} + */ +export default function stripZeros(num) { + if (typeof num === 'string') { + // strip ending zeros + if (num.indexOf('.') !== -1) { + num = num.replace(/\.?0+$/, ''); + } + // strip leading zeros + num = num.replace(/^0+(?!\.)(?!$)/, ''); + } + return num; +} diff --git a/test/from-exponential.test.js b/test/from-exponential.test.js new file mode 100644 index 0000000..35460a1 --- /dev/null +++ b/test/from-exponential.test.js @@ -0,0 +1,49 @@ +import fromExponential from '../src/from-exponential'; + +describe('prettyNum', () => { + test('positive exponential', () => { + expect(fromExponential(0.123e-10)).toEqual('0.0000000000123'); + expect(fromExponential(1.123e-10)).toEqual('0.0000000001123'); + expect(fromExponential(12.123e-10)).toEqual('0.0000000012123'); + expect(fromExponential(123.123e-10)).toEqual('0.0000000123123'); + expect(fromExponential(123.123e+20)).toEqual('12312300000000000000000'); + expect(Number.MAX_VALUE).toEqual(1.7976931348623157e+308); + expect(fromExponential(Number.MAX_VALUE)).toEqual('179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'); + expect(Number.MIN_VALUE).toEqual(5e-324); + expect(fromExponential(Number.MIN_VALUE)).toEqual('0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005'); + }); + + test('negative exponential', () => { + expect(fromExponential(-0.123e-10)).toEqual('-0.0000000000123'); + expect(fromExponential(-1.123e-10)).toEqual('-0.0000000001123'); + expect(fromExponential(-12.123e-10)).toEqual('-0.0000000012123'); + expect(fromExponential(-123.123e-10)).toEqual('-0.0000000123123'); + expect(fromExponential(-123.123e+20)).toEqual('-12312300000000000000000'); + }); + + test('positive exponential-like strings', () => { + expect(fromExponential('0.123e-1')).toEqual('0.0123'); + expect(fromExponential('1.123e-1')).toEqual('0.1123'); + expect(fromExponential('12.123e-1')).toEqual('1.2123'); + expect(fromExponential('123.123e-1')).toEqual('12.3123'); + expect(fromExponential('123.123e+4')).toEqual('1231230'); + expect(fromExponential('123.123e+0')).toEqual('123.123'); + }); + + test('negative exponential-like strings', () => { + expect(fromExponential('-0.123e-1')).toEqual('-0.0123'); + expect(fromExponential('-1.123e-1')).toEqual('-0.1123'); + expect(fromExponential('-12.123e-1')).toEqual('-1.2123'); + expect(fromExponential('-123.123e-1')).toEqual('-12.3123'); + expect(fromExponential('-123.123e+4')).toEqual('-1231230'); + expect(fromExponential('-123.123e+0')).toEqual('-123.123'); + }); + + test('not exponential', () => { + expect(() => fromExponential(0)).toThrow(); + expect(() => fromExponential(0.0012, 3)).toThrow(); + expect(() => fromExponential(123)).toThrow(); + expect(() => fromExponential(123e+14)).toThrow(); // 12300000000000000 is not exponential + expect(() => fromExponential(0.123e-4)).toThrow(); // 0.0000123 is not exponential + }); +}); diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..8042343 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,14 @@ +import prettyNum from '../src/index'; + +describe('prettyNum', () => { + test('exponential', () => { + expect(prettyNum(1.123e-10, 3)).toEqual('0.000000000112'); + expect(prettyNum(12.123e-10, 3)).toEqual('0.00000000121'); + expect(prettyNum(123.123e-10, 3)).toEqual('0.0000000123'); + expect(prettyNum(123.123e+4, 3)).toEqual('1231230'); + }); + + test('exponential', () => { + expect(prettyNum(0.00123456, 3)).toEqual('0.00123'); + }); +}); diff --git a/test/reduce-precision.test.js b/test/reduce-precision.test.js new file mode 100644 index 0000000..b1eb98d --- /dev/null +++ b/test/reduce-precision.test.js @@ -0,0 +1,25 @@ +import reducePrecision from '../src/reduce-precision'; + +describe('reducePrecision', () => { + test('leave correct numbers untouched', () => { + expect(reducePrecision(1234, 3)).toEqual('1234'); + expect(reducePrecision(12.0001, 3)).toEqual('12.0001'); + expect(reducePrecision(12.000123, 3)).toEqual('12.000123'); + expect(reducePrecision(0, 3)).toEqual('0'); + expect(reducePrecision(0.0001, 3)).toEqual('0.0001'); + }); + + test('reduce precision', () => { + expect(reducePrecision(12.123456, 3)).toEqual('12.123'); + expect(reducePrecision(12.000123456, 3)).toEqual('12.000123'); + expect(reducePrecision(0.123456, 3)).toEqual('0.123'); + expect(reducePrecision(0.000123456, 3)).toEqual('0.000123'); + }); + + test('exponential', () => { + expect(reducePrecision(1.123456e-80, 3)).toEqual('1.12e-80'); + expect(reducePrecision(0.123456e-80, 3)).toEqual('1.23e-81'); + expect(reducePrecision(1.123456e+80, 3)).toEqual('1.123456e+80'); + expect(reducePrecision(0.123456e+80, 3)).toEqual('1.23456e+79'); + }); +}); diff --git a/test/round.test.js b/test/round.test.js new file mode 100644 index 0000000..4f6a137 --- /dev/null +++ b/test/round.test.js @@ -0,0 +1,16 @@ +import round from '../src/round'; + +describe('round', () => { + test('3 fraction digits', () => { + expect(round(12, 3)).toEqual(12); + expect(round(12.123456, 3)).toEqual(12.123); + expect(round(12.123e-10, 3)).toEqual(0); + }); + test('0 fraction digits', () => { + expect(round(12.123456, 0)).toEqual(12); + }); + test('negative fraction digits', () => { + expect(() => round(12.123456, -1)).toThrow(); + expect(() => round(12.123456, -5)).toThrow(); + }); +}); diff --git a/test/strip-zeros.test.js b/test/strip-zeros.test.js new file mode 100644 index 0000000..2d3092e --- /dev/null +++ b/test/strip-zeros.test.js @@ -0,0 +1,34 @@ +import stripZeros from '../src/strip-zeros'; + +describe('stripZeros', () => { + test('leave numbers untouched', () => { + expect(stripZeros(12)).toEqual(12); + expect(stripZeros(12.0001)).toEqual(12.0001); + expect(stripZeros(12.00010000)).toEqual(12.0001); + expect(stripZeros(0)).toEqual(0); + expect(stripZeros(0.0001)).toEqual(0.0001); + }); + + test('leave correct strings untouched', () => { + expect(stripZeros('12.0001')).toEqual('12.0001'); + expect(stripZeros('12')).toEqual('12'); + expect(stripZeros('0.0001')).toEqual('0.0001'); + expect(stripZeros('0')).toEqual('0'); + }); + + test('strip zeros after dot', () => { + expect(stripZeros('12.00010000')).toEqual('12.0001'); + expect(stripZeros('12.00000000')).toEqual('12'); + expect(stripZeros('0.00010000')).toEqual('0.0001'); + expect(stripZeros('0.00000000')).toEqual('0'); + }); + + test('strip zeros before dot', () => { + expect(stripZeros('00.00000000')).toEqual('0'); + expect(stripZeros('0012')).toEqual('12'); + }); + + test('strip both', () => { + expect(stripZeros('0012.00010000')).toEqual('12.0001'); + }); +});