running-tools

A collection of tools for runners and their coaches
git clone https://git.ashermorgan.net/running-tools/
Log | Files | Refs | README

commit 2cfdf69b011235bd699364f996ee967b5a15a60e
parent 379e7974ad448b3ad2d1b8ab41bed2566a78ecbc
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Wed, 17 Nov 2021 18:00:28 -0800

Implement formatNumber method

Diffstat:
Msrc/components/DecimalInput.vue | 5+++--
Msrc/components/SimpleTargetTable.vue | 14++++++++++----
Asrc/utils/format.js | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/utils/units.js | 58----------------------------------------------------------
Msrc/views/RaceCalculator.vue | 14++++++++++----
Msrc/views/SplitCalculator.vue | 14++++++++++----
Msrc/views/UnitCalculator.vue | 12+++++++++---
Atests/unit/utils/format.spec.js | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/unit/utils/units.spec.js | 66------------------------------------------------------------------
9 files changed, 341 insertions(+), 141 deletions(-)

diff --git a/src/components/DecimalInput.vue b/src/components/DecimalInput.vue @@ -8,6 +8,8 @@ </template> <script> +import formatUtils from '@/utils/format'; + export default { name: 'DecimalInput', @@ -216,8 +218,7 @@ export default { * @returns {String} The formated string */ format(value) { - const result = value.toFixed(this.digits); - return result.padStart(this.padding + this.digits + 1, '0'); + return formatUtils.formatNumber(value, this.padding, this.digits, true); }, }, }; diff --git a/src/components/SimpleTargetTable.vue b/src/components/SimpleTargetTable.vue @@ -20,16 +20,16 @@ <tbody> <tr v-for="(item, index) in results" :key="index"> <td :class="item.result === 'distance' ? 'result' : ''"> - {{ item.distanceValue.toFixed(2) }} + {{ formatNumber(item.distanceValue, 0, 2, true) }} {{ distanceUnits[item.distanceUnit].symbol }} </td> <td :colspan="showPace ? 1 : 2" :class="item.result === 'time' ? 'result' : ''"> - {{ formatDuration(item.time, 0, 2) }} + {{ formatDuration(item.time, 0, 2, true) }} </td> <td v-if="showPace" colspan="2"> - {{ formatDuration(getPace(item), 3, 0) }} + {{ formatDuration(getPace(item), 3, 0, true) }} / {{ distanceUnits[getDefaultDistanceUnit()].symbol }} </td> </tr> @@ -55,6 +55,7 @@ import { EditIcon, } from 'vue-feather-icons'; +import formatUtils from '@/utils/format'; import storage from '@/utils/localStorage'; import targetUtils from '@/utils/targets'; import unitUtils from '@/utils/units'; @@ -120,7 +121,12 @@ export default { /** * The formatDuration method */ - formatDuration: unitUtils.formatDuration, + formatDuration: formatUtils.formatDuration, + + /** + * The formatNumber method + */ + formatNumber: formatUtils.formatNumber, /** * The getDefaultDistanceUnit method diff --git a/src/utils/format.js b/src/utils/format.js @@ -0,0 +1,104 @@ +/** + * Format a number as a string + * @param {Number} value The number + * @param {Number} minPadding The minimum number of digits to show before the decimal point + * @param {Number} maxDigits The maximum number of digits to show after the decimal point + * @param {Boolean} extraDigits Whether to show extra zeros after the decimal point + * @returns {String} The formatted value + */ +function formatNumber(value, minPadding = 0, maxDigits = 2, extraDigits = true) { + // Initialize result + let result = ''; + + // Remove sign + const negative = value < 0; + const fixedValue = Math.abs(value); + + // Address edge cases + if (Number.isNaN(fixedValue)) { + return 'NaN'; + } + if (fixedValue === Infinity) { + return negative ? '-Infinity' : 'Infinity'; + } + + // Convert number to string + if (extraDigits) { + result = fixedValue.toFixed(maxDigits); + } else { + const power = 10 ** maxDigits; + result = (Math.round((fixedValue + Number.EPSILON) * power) / power).toString(); + } + + // Add padding + const currentPadding = result.split('.')[0].length; + result = result.padStart(result.length - currentPadding + minPadding, '0'); + + // Add negative sign + if (negative) { + result = `-${result}`; + } + + // Return result + return result; +} + +/** + * Format a duration as a string + * @param {Number} value The duration (in seconds) + * @param {Number} minPadding The minimum number of digits to show before the decimal point + * @param {Number} maxDigits The maximum number of digits to show after the decimal point + * @param {Boolean} extraDigits Whether to show extra zeros after the decimal point + * @returns {String} The formatted value + */ +function formatDuration(value, minPadding = 6, maxDigits = 2, extraDigits = true) { + // Check if value is NaN + if (Number.isNaN(value)) { + return 'NaN'; + } + + // Initialize result + let result = ''; + + // Check value sign + if (value < 0) { + result += '-'; + } + + // Check if value is valid + if (Math.abs(value) === Infinity) { + return `${result}Infinity`; + } + + // Validate padding + let fixedPadding = Math.min(minPadding, 6); + + // Prevent rounding errors + const fixedValue = parseFloat(Math.abs(value).toFixed(maxDigits)); + + // Calculate parts + const hours = Math.floor(fixedValue / 3600); + const minutes = Math.floor((fixedValue % 3600) / 60); + const seconds = fixedValue % 60; + + // Format parts + if (hours !== 0 || fixedPadding >= 5) { + result += hours.toString().padStart(fixedPadding - 4, '0'); + result += ':'; + fixedPadding = 4; + } + if (minutes !== 0 || fixedPadding >= 3) { + result += minutes.toString().padStart(fixedPadding - 2, '0'); + result += ':'; + fixedPadding = 2; + } + result += formatNumber(seconds, fixedPadding, maxDigits, extraDigits); + + // Return result + return result; +} + +export default { + formatNumber, + formatDuration, +}; diff --git a/src/utils/units.js b/src/utils/units.js @@ -160,62 +160,6 @@ function convertSpeedPace(inputValue, inputUnit, outputUnit) { } /** - * Format a duration as a string - * @param {Number} value The duration (in seconds) - * @param {Number} padding The number of digits to show before the decimal point - * @param {Number} digits The number of digits to show after the decimal point - * @returns {String} The formatted value - */ -function formatDuration(value, padding = 6, digits = 2) { - // Check if value is NaN - if (Number.isNaN(value)) { - return 'NaN'; - } - - // Initialize result - let result = ''; - - // Check value sign - if (value < 0) { - result += '-'; - } - - // Check if value is valid - if (Math.abs(value) === Infinity) { - return `${result}Infinity`; - } - - // Validate padding - let fixedPadding = Math.min(padding, 6); - - // Prevent rounding errors - const fixedValue = parseFloat(Math.abs(value).toFixed(digits)); - - // Calculate parts - const hours = Math.floor(fixedValue / 3600); - const minutes = Math.floor((fixedValue % 3600) / 60); - const seconds = fixedValue % 60; - - // Format parts - if (hours !== 0 || fixedPadding >= 5) { - result += hours.toString().padStart(fixedPadding - 4, '0'); - result += ':'; - fixedPadding = 4; - } - if (minutes !== 0 || fixedPadding >= 3) { - result += minutes.toString().padStart(fixedPadding - 2, '0'); - result += ':'; - fixedPadding = 2; - } - if (digits === 0) { - result += seconds.toFixed(digits).padStart(fixedPadding, '0'); - } else { - result += seconds.toFixed(digits).padStart(fixedPadding + digits + 1, '0'); - } - return result; -} - -/** * Get the default unit system * @returns {String} The default unit system */ @@ -263,8 +207,6 @@ export default { convertPace, convertSpeedPace, - formatDuration, - getDefaultUnitSystem, getDefaultDistanceUnit, getDefaultSpeedUnit, diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -41,14 +41,14 @@ (default: 1.06) </div> <div> - Purdy Points: <b>{{ purdyPoints.toFixed(1) }}</b> + Purdy Points: <b>{{ formatNumber(purdyPoints, 0, 1, true) }}</b> </div> <div> - V&#775;O&#8322;: <b>{{ vo2.toFixed(1) }}</b> ml/kg/min - (<b>{{ vo2Percentage.toFixed(1) }}%</b> of max) + V&#775;O&#8322;: <b>{{ formatNumber(vo2, 0, 1, true) }}</b> ml/kg/min + (<b>{{ formatNumber(vo2Percentage, 0, 1, true) }}%</b> of max) </div> <div> - V&#775;O&#8322; Max: <b>{{ vo2Max.toFixed(1) }}</b> ml/kg/min + V&#775;O&#8322; Max: <b>{{ formatNumber(vo2Max, 0, 1, true) }}</b> ml/kg/min </div> </div> @@ -60,6 +60,7 @@ </template> <script> +import formatUtils from '@/utils/format'; import raceUtils from '@/utils/races'; import storage from '@/utils/localStorage'; import unitUtils from '@/utils/units'; @@ -115,6 +116,11 @@ export default { distanceUnits: unitUtils.DISTANCE_UNITS, /** + * The formatNumber method + */ + formatNumber: formatUtils.formatNumber, + + /** * The default output targets */ defaultTargets: [ diff --git a/src/views/SplitCalculator.vue b/src/views/SplitCalculator.vue @@ -23,12 +23,12 @@ <tbody> <tr v-for="(item, index) in results" :key="index"> <td> - {{ item.distanceValue.toFixed(2) }} + {{ formatNumber(item.distanceValue, 0, 2, true) }} {{ distanceUnits[item.distanceUnit].symbol }} </td> <td> - {{ formatDuration(item.totalTime, 3, 2) }} + {{ formatDuration(item.totalTime, 3, 2, true) }} </td> <td> @@ -36,7 +36,7 @@ </td> <td colspan="2"> - {{ formatDuration(item.pace, 3, 0) }} + {{ formatDuration(item.pace, 3, 0, true) }} / {{ distanceUnits[getDefaultDistanceUnit()].symbol }} </td> </tr> @@ -63,6 +63,7 @@ import { EditIcon, } from 'vue-feather-icons'; +import formatUtils from '@/utils/format'; import storage from '@/utils/localStorage'; import targetUtils from '@/utils/targets'; import unitUtils from '@/utils/units'; @@ -103,7 +104,12 @@ export default { /** * The formatDuration method */ - formatDuration: unitUtils.formatDuration, + formatDuration: formatUtils.formatDuration, + + /** + * The formatNumber method + */ + formatNumber: formatUtils.formatNumber, /** * The getDefaultDistanceUnit method diff --git a/src/views/UnitCalculator.vue b/src/views/UnitCalculator.vue @@ -20,10 +20,10 @@ <span class="equals"> = </span> <span v-if="getUnitType(outputUnit) === 'time'" class="output-value"> - {{ formatDuration(outputValue, 6, 3) }} + {{ formatDuration(outputValue, 6, 3, true) }} </span> <span v-else class="output-value"> - {{ outputValue.toFixed(3) }} + {{ formatNumber(outputValue, 0, 3, true) }} </span> <select v-model="outputUnit" class="output-units" aria-label="output units"> @@ -35,6 +35,7 @@ </template> <script> +import formatUtils from '@/utils/format'; import storage from '@/utils/localStorage'; import unitUtils from '@/utils/units'; @@ -74,7 +75,12 @@ export default { /** * The formatDuration method */ - formatDuration: unitUtils.formatDuration, + formatDuration: formatUtils.formatDuration, + + /** + * The formatNumber method + */ + formatNumber: formatUtils.formatNumber, }; }, diff --git a/tests/unit/utils/format.spec.js b/tests/unit/utils/format.spec.js @@ -0,0 +1,195 @@ +import { expect } from 'chai'; +import formatUtils from '@/utils/format'; + +describe('utils/format.js', () => { + describe('formatNumber method', () => { + it('should correctly format number when padding is not 0', () => { + let result = formatUtils.formatNumber(12.3, 3, 0); + expect(result).to.equal('012'); + + result = formatUtils.formatNumber(12.3, 3, 2); + expect(result).to.equal('012.30'); + + result = formatUtils.formatNumber(123, 2, 0); + expect(result).to.equal('123'); + + result = formatUtils.formatNumber(-12, 3, 0); + expect(result).to.equal('-012'); + }); + + it('should correctly format number when extraDigits is true', () => { + let result = formatUtils.formatNumber(1234, 0, 2); + expect(result).to.equal('1234.00'); + + result = formatUtils.formatNumber(1234.5, 0, 2); + expect(result).to.equal('1234.50'); + + result = formatUtils.formatNumber(1234.56, 0, 2); + expect(result).to.equal('1234.56'); + + result = formatUtils.formatNumber(1234.567, 0, 2); + expect(result).to.equal('1234.57'); + + result = formatUtils.formatNumber(1234.56, 0, 0); + expect(result).to.equal('1235'); + }); + + it('should correctly format number when extraDigits is false', () => { + let result = formatUtils.formatNumber(1234, 0, 2, false); + expect(result).to.equal('1234'); + + result = formatUtils.formatNumber(1234.5, 0, 2, false); + expect(result).to.equal('1234.5'); + + result = formatUtils.formatNumber(1234.56, 0, 2, false); + expect(result).to.equal('1234.56'); + + result = formatUtils.formatNumber(1234.567, 0, 2, false); + expect(result).to.equal('1234.57'); + + result = formatUtils.formatNumber(1234.56, 0, 0, false); + expect(result).to.equal('1235'); + }); + + it('should correctly format undefined', () => { + let result = formatUtils.formatNumber(undefined, 0, 2); + expect(result).to.equal('NaN'); + + result = formatUtils.formatNumber(undefined, 0, 2, false); + expect(result).to.equal('NaN'); + + result = formatUtils.formatNumber(undefined, 5, 2); + expect(result).to.equal('NaN'); + }); + + it('should correctly format NaN', () => { + let result = formatUtils.formatNumber(NaN, 0, 0); + expect(result).to.equal('NaN'); + + result = formatUtils.formatNumber(NaN, 0, 2, false); + expect(result).to.equal('NaN'); + + result = formatUtils.formatNumber(NaN, 5, 2); + expect(result).to.equal('NaN'); + }); + + it('should correctly format +/- Infinity', () => { + let result = formatUtils.formatNumber(Infinity); + expect(result).to.equal('Infinity'); + + result = formatUtils.formatNumber(Infinity, 10, 2); + expect(result).to.equal('Infinity'); + + result = formatUtils.formatNumber(-Infinity); + expect(result).to.equal('-Infinity'); + }); + + it('should correctly format numbers less than 1', () => { + let result = formatUtils.formatNumber(0.123, 0, 0); + expect(result).to.equal('0'); + + result = formatUtils.formatNumber(0.123, 0, 2); + expect(result).to.equal('0.12'); + }); + + it('should correctly format negative numbers', () => { + let result = formatUtils.formatNumber(-12, 0, 2, false); + expect(result).to.equal('-12'); + + result = formatUtils.formatNumber(-12, 0, 2); + expect(result).to.equal('-12.00'); + + result = formatUtils.formatNumber(-12.34, 0, 2); + expect(result).to.equal('-12.34'); + + result = formatUtils.formatNumber(-12.34, 3, 2); + expect(result).to.equal('-012.34'); + + result = formatUtils.formatNumber(-0.12, 0, 2); + expect(result).to.equal('-0.12'); + }); + }); + + describe('formatDuration method', () => { + it('should correctly divide durations into parts', () => { + const result = formatUtils.formatDuration(3600 + 120 + 3 + 0.4); + expect(result).to.equal('01:02:03.40'); + }); + + it('should correctly format duration when padding is 7', () => { + const result = formatUtils.formatDuration(3600 + 120 + 3 + 0.4, 7); + expect(result).to.equal('01:02:03.40'); + }); + + it('should correctly format duration when padding is 3', () => { + let result = formatUtils.formatDuration(3600 + 120 + 3 + 0.4, 3); + expect(result).to.equal('1:02:03.40'); + + result = formatUtils.formatDuration(120 + 3 + 0.4, 3); + expect(result).to.equal('2:03.40'); + + result = formatUtils.formatDuration(3 + 0.4, 3); + expect(result).to.equal('0:03.40'); + }); + + it('should correctly format duration when padding is 0', () => { + const result = formatUtils.formatDuration(0.4, 0); + expect(result).to.equal('0.40'); + }); + + it('should correctly format duration when digits is 3', () => { + const result = formatUtils.formatDuration(3600 + 120 + 3 + 0.4567, 0, 3); + expect(result).to.equal('1:02:03.457'); + }); + + it('should correctly format duration when digits is 0', () => { + const result = formatUtils.formatDuration(3600 + 120 + 3 + 0.456, 0, 0); + expect(result).to.equal('1:02:03'); + }); + + it('should correctly format NaN', () => { + const result = formatUtils.formatDuration(NaN); + expect(result).to.equal('NaN'); + }); + + it('should correctly format +/- Infinity', () => { + let result = formatUtils.formatDuration(Infinity); + expect(result).to.equal('Infinity'); + + result = formatUtils.formatDuration(-Infinity); + expect(result).to.equal('-Infinity'); + }); + + it('should correctly format 0 when padding is 0', () => { + const result = formatUtils.formatDuration(0, 0); + expect(result).to.equal('0.00'); + }); + + it('should correctly format negative durations', () => { + const result = formatUtils.formatDuration(-3600 - 120 - 3 - 0.4); + expect(result).to.equal('-01:02:03.40'); + }); + + it('should correctly format 59.9999', () => { + const result = formatUtils.formatDuration(59.9999); + expect(result).to.equal('00:01:00.00'); + }); + + it('should correctly format duration when extraDigits is false', () => { + let result = formatUtils.formatDuration(83, 0, 2, false); + expect(result).to.equal('1:23'); + + result = formatUtils.formatDuration(83.4, 0, 2, false); + expect(result).to.equal('1:23.4'); + + result = formatUtils.formatDuration(83.45, 0, 2, false); + expect(result).to.equal('1:23.45'); + + result = formatUtils.formatDuration(83.456, 0, 2, false); + expect(result).to.equal('1:23.46'); + + result = formatUtils.formatDuration(83.45, 0, 0, false); + expect(result).to.equal('1:23'); + }); + }); +}); diff --git a/tests/unit/utils/units.spec.js b/tests/unit/utils/units.spec.js @@ -61,70 +61,4 @@ describe('utils/units.js', () => { expect(result).to.equal(1); }); }); - - describe('formatDuration method', () => { - it('should correctly divide durations into parts', () => { - const result = units.formatDuration(3600 + 120 + 3 + 0.4); - expect(result).to.equal('01:02:03.40'); - }); - - it('should correctly format duration when padding is 7', () => { - const result = units.formatDuration(3600 + 120 + 3 + 0.4, 7); - expect(result).to.equal('01:02:03.40'); - }); - - it('should correctly format duration when padding is 3', () => { - let result = units.formatDuration(3600 + 120 + 3 + 0.4, 3); - expect(result).to.equal('1:02:03.40'); - - result = units.formatDuration(120 + 3 + 0.4, 3); - expect(result).to.equal('2:03.40'); - - result = units.formatDuration(3 + 0.4, 3); - expect(result).to.equal('0:03.40'); - }); - - it('should correctly format duration when padding is 0', () => { - const result = units.formatDuration(0.4, 0); - expect(result).to.equal('0.40'); - }); - - it('should correctly format duration when digits is 3', () => { - const result = units.formatDuration(3600 + 120 + 3 + 0.4567, 0, 3); - expect(result).to.equal('1:02:03.457'); - }); - - it('should correctly format duration when digits is 0', () => { - const result = units.formatDuration(3600 + 120 + 3 + 0.456, 0, 0); - expect(result).to.equal('1:02:03'); - }); - - it('should correctly format NaN', () => { - const result = units.formatDuration(NaN); - expect(result).to.equal('NaN'); - }); - - it('should correctly format +/- Infinity', () => { - let result = units.formatDuration(Infinity); - expect(result).to.equal('Infinity'); - - result = units.formatDuration(-Infinity); - expect(result).to.equal('-Infinity'); - }); - - it('should correctly format 0 when padding is 0', () => { - const result = units.formatDuration(0, 0); - expect(result).to.equal('0.00'); - }); - - it('should correctly format negative durations', () => { - const result = units.formatDuration(-3600 - 120 - 3 - 0.4); - expect(result).to.equal('-01:02:03.40'); - }); - - it('should correctly format 59.9999', () => { - const result = units.formatDuration(59.9999); - expect(result).to.equal('00:01:00.00'); - }); - }); });