running-tools

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

commit 8bf59b1ddfc25984b5d5074009945848aa018092
parent bf518dc569a9970f68d021e03d94233fbb3e1eaa
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sat, 12 Jul 2025 11:40:32 -0700

Implement formatDistance and formatPace functions

Also moved existing formatNumber and formatDuration functions to unit
utils.

Diffstat:
Msrc/components/DecimalInput.vue | 2+-
Msrc/components/DoubleOutputTable.vue | 6++----
Msrc/components/SplitOutputTable.vue | 51+++++++++++++++++++++++----------------------------
Msrc/utils/calculators.ts | 15++++-----------
Dsrc/utils/format.ts | 102-------------------------------------------------------------------------------
Msrc/utils/targets.ts | 12++++++------
Msrc/utils/units.ts | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/views/RaceCalculator.vue | 3+--
Msrc/views/UnitCalculator.vue | 5++---
Dtests/unit/utils/format.spec.js | 193-------------------------------------------------------------------------------
Mtests/unit/utils/units.spec.js | 377+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 543 insertions(+), 350 deletions(-)

diff --git a/src/components/DecimalInput.vue b/src/components/DecimalInput.vue @@ -4,7 +4,7 @@ <script setup lang="ts"> import { ref, watch } from 'vue'; -import { formatNumber } from '@/utils/format'; +import { formatNumber } from '@/utils/units'; /** * The component value diff --git a/src/components/DoubleOutputTable.vue b/src/components/DoubleOutputTable.vue @@ -31,9 +31,8 @@ import { computed } from 'vue'; import { ResultType } from '@/utils/calculators'; import type { TargetResult } from '@/utils/calculators'; -import { formatDuration, formatNumber } from '@/utils/format'; import type { Target } from '@/utils/targets'; -import { DistanceUnitData } from '@/utils/units'; +import { formatDistance, formatDuration } from '@/utils/units'; import type { Distance, DistanceTime } from '@/utils/units'; interface Props { @@ -66,8 +65,7 @@ const props = defineProps<Props>(); const results = computed(() => { // Calculate results const results: Array<Array<string>> = [[ - formatNumber(props.inputDistance.distanceValue, 0, 2, false) + ' ' - + DistanceUnitData[props.inputDistance.distanceUnit].symbol + formatDistance(props.inputDistance, false), ]]; props.inputTimes.forEach((input, y) => { diff --git a/src/components/SplitOutputTable.vue b/src/components/SplitOutputTable.vue @@ -18,12 +18,11 @@ <tbody> <tr v-for="(item, index) in results" :key="index"> <td> - {{ formatNumber(item.distanceValue, 0, 2, false) }} - {{ DistanceUnitData[item.distanceUnit].symbol }} + {{ formatDistance(model[index] as Distance, false) }} </td> <td> - {{ formatDuration(item.time, 3, 2, true) }} + {{ formatDuration(item.total.time, 3, 2, true) }} </td> <td> @@ -31,9 +30,7 @@ </td> <td> - {{ formatDuration(item.pace, 3, 0, true) }} - / {{ DistanceUnitData[getDefaultDistanceUnit(defaultUnitSystem)] - .symbol }} + {{ formatPace(item.split, getDefaultPaceUnit(defaultUnitSystem)) }} </td> </tr> @@ -49,21 +46,17 @@ <script setup lang="ts"> import { computed } from 'vue'; -import { formatDuration, formatNumber } from '@/utils/format'; import type { SplitTarget } from '@/utils/targets'; -import { DistanceUnits, DistanceUnitData, UnitSystems, convertDistance, - getDefaultDistanceUnit } from '@/utils/units'; +import { DistanceUnits, UnitSystems, convertDistance, formatDistance, formatDuration, + formatPace, getDefaultPaceUnit } from '@/utils/units'; +import type { Distance, DistanceTime } from '@/utils/units'; import TimeInput from '@/components/TimeInput.vue'; import useObjectModel from '@/composables/useObjectModel'; interface SplitTargetResult { - distance: number, - distanceValue: number, - distanceUnit: DistanceUnits, - time: number, - splitTime: number, - pace: number, + split: DistanceTime, + total: DistanceTime, }; interface Props { @@ -95,27 +88,29 @@ const results = computed(() => { for (let i = 0; i < model.value.length; i += 1) { // Calculate split and total times const splitTime = model.value[i].splitTime || 0; - const totalTime = i === 0 ? splitTime : results[i - 1].time + splitTime; + const totalTime = i === 0 ? splitTime : results[i - 1].total.time + splitTime; // Calculate split and total distances const totalDistance = convertDistance( model.value[i].distanceValue, - model.value[i].distanceUnit, DistanceUnits.Meters, + model.value[i].distanceUnit, + DistanceUnits.Meters, ); - const splitDistance = i === 0 ? totalDistance : totalDistance - results[i - 1].distance; - - // Calculate pace - const pace = splitTime / convertDistance(splitDistance, DistanceUnits.Meters, - getDefaultDistanceUnit(props.defaultUnitSystem)); + const splitDistance = i === 0 ? totalDistance : + totalDistance - results[i - 1].total.distanceValue; // Add row to results array results.push({ - distance: totalDistance, - distanceValue: model.value[i].distanceValue, - distanceUnit: model.value[i].distanceUnit, - time: totalTime, - splitTime, - pace, + split: { + distanceValue: splitDistance, + distanceUnit: DistanceUnits.Meters, + time: splitTime, + }, + total: { + distanceValue: totalDistance, + distanceUnit: DistanceUnits.Meters, + time: totalTime, + }, }); } diff --git a/src/utils/calculators.ts b/src/utils/calculators.ts @@ -1,10 +1,9 @@ -import { formatDuration, formatNumber } from '@/utils/format'; import * as paceUtils from '@/utils/paces'; import * as raceUtils from '@/utils/races'; import { TargetTypes, workoutTargetToString } from '@/utils/targets'; import type { StandardTarget, WorkoutTarget } from '@/utils/targets'; -import { DistanceUnits, DistanceUnitData, UnitSystems, convertDistance, - getDefaultDistanceUnit } from '@/utils/units'; +import { DistanceUnits, UnitSystems, convertDistance, formatDistance, formatDuration, formatPace, + getDefaultDistanceUnit, getDefaultPaceUnit } from '@/utils/units'; import type { DistanceTime } from '@/utils/units'; /* @@ -99,21 +98,15 @@ function calculateStandardResult(input: DistanceTime, target: StandardTarget, distanceUnit = units; } - // Calculate numerical pace - const pace = time / convertDistance(distanceValue, distanceUnit, - getDefaultDistanceUnit(defaultUnitSystem)); - return { // Convert distance to key string - key: formatNumber(distanceValue, 0, 2, target.type === TargetTypes.Time) + ' ' + - DistanceUnitData[distanceUnit].symbol, + key: formatDistance({ distanceValue, distanceUnit }, target.type === TargetTypes.Time), // Convert time to time string value: formatDuration(time, 3, preciseDurations ? 2 : 0, target.type === TargetTypes.Distance), // Convert pace to pace string - pace: formatDuration(pace, 3, 0, true) + ' / ' - + DistanceUnitData[getDefaultDistanceUnit(defaultUnitSystem)].symbol, + pace: formatPace({ time, distanceValue, distanceUnit }, getDefaultPaceUnit(defaultUnitSystem)), // Convert dist/time result to key/value result: target.type === TargetTypes.Distance ? ResultType.Value : ResultType.Key, diff --git a/src/utils/format.ts b/src/utils/format.ts @@ -1,102 +0,0 @@ -/** - * 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 - */ -export function formatNumber(value: number, minPadding: number = 0, maxDigits: number = 2, - extraDigits: boolean = true): string { - - // 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 - */ -export function formatDuration(value: number, minPadding: number = 6, maxDigits: number = 2, - extraDigits: boolean = true): string { - // 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; -} diff --git a/src/utils/targets.ts b/src/utils/targets.ts @@ -1,5 +1,5 @@ -import { formatDuration, formatNumber } from '@/utils/format'; -import { DistanceUnits, DistanceUnitData, convertDistance } from '@/utils/units'; +import { DistanceUnits, convertDistance, formatDistance, formatDuration } from '@/utils/units'; +import type { Distance } from '@/utils/units'; /* * The two basic types of targets: those defined by distance and those defined by time @@ -89,13 +89,13 @@ export function sort(targets: Array<Target>): Array<Target> { * @return {string} The string description */ export function workoutTargetToString(target: WorkoutTarget): string { - let result = formatNumber(target.splitValue, 0, 2, false) + ' ' + - DistanceUnitData[target.splitUnit].symbol; + let result = formatDistance({ distanceValue: target.splitValue, distanceUnit: target.splitUnit }, + false); + if (target.type === TargetTypes.Time) { result += ' @ ' + formatDuration(target.time, 3, 2, false); } else if (target.distanceValue != target.splitValue || target.distanceUnit != target.splitUnit) { - result += ' @ ' + formatNumber(target.distanceValue, 0, 2, false) + ' ' + - DistanceUnitData[target.distanceUnit].symbol; + result += ' @ ' + formatDistance(target as Distance, false); } return result; } diff --git a/src/utils/units.ts b/src/utils/units.ts @@ -237,6 +237,133 @@ export function detectDefaultUnitSystem(): UnitSystems { } /** + * 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 number + */ +export function formatNumber(value: number, minPadding: number = 0, maxDigits: number = 2, + extraDigits: boolean = true): string { + + // 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 distance as a string + * @param {Distance} input The distance + * @param {boolean} extraDigits Whether to show extra zeros after the decimal point + * @returns {string} The formatted distance + */ +export function formatDistance(input: Distance, extraDigits: boolean) { + return formatNumber(input.distanceValue, 0, 2, extraDigits) + ' ' + + DistanceUnitData[input.distanceUnit].symbol; +} + +/** + * 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 duration + */ +export function formatDuration(value: number, minPadding: number = 6, maxDigits: number = 2, + extraDigits: boolean = true): string { + // 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; +} + +/** + * Calculate the pace of a distance/time pair and format it as a string + * @param {DistanceTime} input The input distance/time pair + * @param {PaceUnits} unit The desired pace unit + * @returns {string} The formatted pace + */ +export function formatPace(input: DistanceTime, unit: PaceUnits) { + const dist = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters); + const pace = convertPace(input.time / dist, PaceUnits.SecondsPerMeter, unit) + const result = formatDuration(pace, 3, 0, true) + ' ' + PaceUnitData[unit].symbol; + return result; +} + +/** * Get the default distance unit in a unit system * @param {UnitSystems} unitSystem The unit system * @returns {DistanceUnits} The default distance unit diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -43,11 +43,10 @@ import { computed } from 'vue'; import { Calculators, calculateRaceResults, calculateRaceStats } from '@/utils/calculators'; import type { RaceOptions, RaceStats } from '@/utils/calculators'; -import { formatNumber } from '@/utils/format'; import { RacePredictionModel } from '@/utils/races'; import { defaultTargetSets } from '@/utils/targets'; import type { StandardTargetSets } from '@/utils/targets'; -import { DistanceUnits, UnitSystems, detectDefaultUnitSystem } from '@/utils/units'; +import { DistanceUnits, UnitSystems, detectDefaultUnitSystem, formatNumber } from '@/utils/units'; import type { DistanceTime } from '@/utils/units'; import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue'; diff --git a/src/views/UnitCalculator.vue b/src/views/UnitCalculator.vue @@ -20,10 +20,10 @@ <span class="equals"> = </span> <span v-if="isTimeUnit(input.outputUnit)" class="output-value" aria-label="Output value"> - {{ formatDuration(outputValue, 6, 3, true) }} + {{ unitUtils.formatDuration(outputValue, 6, 3, true) }} </span> <span v-else class="output-value" aria-label="Output value"> - {{ formatNumber(outputValue, 0, 3, true) }} + {{ unitUtils.formatNumber(outputValue, 0, 3, true) }} </span> <select v-model="input.outputUnit" class="output-units" aria-label="Output units"> @@ -37,7 +37,6 @@ <script setup lang="ts"> import { computed } from 'vue'; -import { formatDuration, formatNumber } from '@/utils/format'; import * as unitUtils from '@/utils/units'; import DecimalInput from '@/components/DecimalInput.vue'; diff --git a/tests/unit/utils/format.spec.js b/tests/unit/utils/format.spec.js @@ -1,193 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import * as formatUtils from '@/utils/format'; - -describe('formatNumber method', () => { - test('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'); - }); - - test('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'); - }); - - test('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'); - }); - - test('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'); - }); - - test('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'); - }); - - test('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'); - }); - - test('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'); - }); - - test('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', () => { - test('should correctly divide durations into parts', () => { - const result = formatUtils.formatDuration(3600 + 120 + 3 + 0.4); - expect(result).to.equal('01:02:03.40'); - }); - - test('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'); - }); - - test('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'); - }); - - test('should correctly format duration when padding is 0', () => { - const result = formatUtils.formatDuration(0.4, 0); - expect(result).to.equal('0.40'); - }); - - test('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'); - }); - - test('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'); - }); - - test('should correctly format NaN', () => { - const result = formatUtils.formatDuration(NaN); - expect(result).to.equal('NaN'); - }); - - test('should correctly format +/- Infinity', () => { - let result = formatUtils.formatDuration(Infinity); - expect(result).to.equal('Infinity'); - - result = formatUtils.formatDuration(-Infinity); - expect(result).to.equal('-Infinity'); - }); - - test('should correctly format 0 when padding is 0', () => { - const result = formatUtils.formatDuration(0, 0); - expect(result).to.equal('0.00'); - }); - - test('should correctly format negative durations', () => { - const result = formatUtils.formatDuration(-3600 - 120 - 3 - 0.4); - expect(result).to.equal('-01:02:03.40'); - }); - - test('should correctly format 59.9999', () => { - const result = formatUtils.formatDuration(59.9999); - expect(result).to.equal('00:01:00.00'); - }); - - test('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 @@ -60,3 +60,380 @@ describe('convertSpeedPace method', () => { expect(result).to.equal(1); }); }); + +describe('formatNumber method', () => { + test('should correctly format number when padding is not 0', () => { + let result = units.formatNumber(12.3, 3, 0); + expect(result).to.equal('012'); + + result = units.formatNumber(12.3, 3, 2); + expect(result).to.equal('012.30'); + + result = units.formatNumber(123, 2, 0); + expect(result).to.equal('123'); + + result = units.formatNumber(-12, 3, 0); + expect(result).to.equal('-012'); + }); + + test('should correctly format number when extraDigits is true', () => { + let result = units.formatNumber(1234, 0, 2); + expect(result).to.equal('1234.00'); + + result = units.formatNumber(1234.5, 0, 2); + expect(result).to.equal('1234.50'); + + result = units.formatNumber(1234.56, 0, 2); + expect(result).to.equal('1234.56'); + + result = units.formatNumber(1234.567, 0, 2); + expect(result).to.equal('1234.57'); + + result = units.formatNumber(1234.56, 0, 0); + expect(result).to.equal('1235'); + }); + + test('should correctly format number when extraDigits is false', () => { + let result = units.formatNumber(1234, 0, 2, false); + expect(result).to.equal('1234'); + + result = units.formatNumber(1234.5, 0, 2, false); + expect(result).to.equal('1234.5'); + + result = units.formatNumber(1234.56, 0, 2, false); + expect(result).to.equal('1234.56'); + + result = units.formatNumber(1234.567, 0, 2, false); + expect(result).to.equal('1234.57'); + + result = units.formatNumber(1234.56, 0, 0, false); + expect(result).to.equal('1235'); + }); + + test('should correctly format undefined', () => { + let result = units.formatNumber(undefined, 0, 2); + expect(result).to.equal('NaN'); + + result = units.formatNumber(undefined, 0, 2, false); + expect(result).to.equal('NaN'); + + result = units.formatNumber(undefined, 5, 2); + expect(result).to.equal('NaN'); + }); + + test('should correctly format NaN', () => { + let result = units.formatNumber(NaN, 0, 0); + expect(result).to.equal('NaN'); + + result = units.formatNumber(NaN, 0, 2, false); + expect(result).to.equal('NaN'); + + result = units.formatNumber(NaN, 5, 2); + expect(result).to.equal('NaN'); + }); + + test('should correctly format +/- Infinity', () => { + let result = units.formatNumber(Infinity); + expect(result).to.equal('Infinity'); + + result = units.formatNumber(Infinity, 10, 2); + expect(result).to.equal('Infinity'); + + result = units.formatNumber(-Infinity); + expect(result).to.equal('-Infinity'); + }); + + test('should correctly format numbers smaller than 1', () => { + let result = units.formatNumber(0.123, 0, 0); + expect(result).to.equal('0'); + + result = units.formatNumber(0.123, 0, 2); + expect(result).to.equal('0.12'); + }); + + test('should correctly format negative numbers', () => { + let result = units.formatNumber(-12, 0, 2, false); + expect(result).to.equal('-12'); + + result = units.formatNumber(-12, 0, 2); + expect(result).to.equal('-12.00'); + + result = units.formatNumber(-12.34, 0, 2); + expect(result).to.equal('-12.34'); + + result = units.formatNumber(-12.34, 3, 2); + expect(result).to.equal('-012.34'); + + result = units.formatNumber(-0.12, 0, 2); + expect(result).to.equal('-0.12'); + }); +}); + +describe('formatDistance method', () => { + test('should correctly format distances with a variety of units', () => { + let result = units.formatDistance({ + distanceValue: 1, + distanceUnit: units.DistanceUnits.Yards, + }, false); + expect(result).to.equal('1 yd'); + + result = units.formatDistance({ + distanceValue: 2, + distanceUnit: units.DistanceUnits.Meters, + }, false); + expect(result).to.equal('2 m'); + + result = units.formatDistance({ + distanceValue: 3, + distanceUnit: units.DistanceUnits.Kilometers, + }, false); + expect(result).to.equal('3 km'); + + result = units.formatDistance({ + distanceValue: 4, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('4 mi'); + + result = units.formatDistance({ + distanceValue: 5, + distanceUnit: units.DistanceUnits.Marathons, + }, false); + expect(result).to.equal('5 Mar'); + }); + + test('should correctly format distance when extraDigits is true', () => { + let result = units.formatDistance({ + distanceValue: 1234, + distanceUnit: units.DistanceUnits.Miles, + }, true); + expect(result).to.equal('1234.00 mi'); + + result = units.formatDistance({ + distanceValue: 1234.5, + distanceUnit: units.DistanceUnits.Miles, + }, true); + expect(result).to.equal('1234.50 mi'); + + result = units.formatDistance({ + distanceValue: 1234.56, + distanceUnit: units.DistanceUnits.Miles, + }, true); + expect(result).to.equal('1234.56 mi'); + + result = units.formatDistance({ + distanceValue: 1234.567, + distanceUnit: units.DistanceUnits.Miles, + }, true); + expect(result).to.equal('1234.57 mi'); + }); + + test('should correctly format distance when extraDigits is false', () => { + let result = units.formatDistance({ + distanceValue: 1234, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('1234 mi'); + + result = units.formatDistance({ + distanceValue: 1234.5, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('1234.5 mi'); + + result = units.formatDistance({ + distanceValue: 1234.56, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('1234.56 mi'); + + result = units.formatDistance({ + distanceValue: 1234.567, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('1234.57 mi'); + }); + + test('should correctly format distances smaller than 1', () => { + let result = units.formatDistance({ + distanceValue: 0, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('0 mi'); + + result = units.formatDistance({ + distanceValue: 0, + distanceUnit: units.DistanceUnits.Miles, + }, true); + expect(result).to.equal('0.00 mi'); + + result = units.formatDistance({ + distanceValue: 0.1, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('0.1 mi'); + + result = units.formatDistance({ + distanceValue: 0.1, + distanceUnit: units.DistanceUnits.Miles, + }, true); + expect(result).to.equal('0.10 mi'); + + result = units.formatDistance({ + distanceValue: 0.12, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('0.12 mi'); + }); + + test('should correctly format negative distances', () => { + let result = units.formatDistance({ + distanceValue: -1234, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('-1234 mi'); + + result = units.formatDistance({ + distanceValue: -1234, + distanceUnit: units.DistanceUnits.Miles, + }, true); + expect(result).to.equal('-1234.00 mi'); + + result = units.formatDistance({ + distanceValue: -1234.56, + distanceUnit: units.DistanceUnits.Miles, + }, false); + expect(result).to.equal('-1234.56 mi'); + }); +}); + +describe('formatDuration method', () => { + test('should correctly divide durations into parts', () => { + const result = units.formatDuration(3600 + 120 + 3 + 0.4); + expect(result).to.equal('01:02:03.40'); + }); + + test('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'); + }); + + test('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'); + }); + + test('should correctly format duration when padding is 0', () => { + const result = units.formatDuration(0.4, 0); + expect(result).to.equal('0.40'); + }); + + test('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'); + }); + + test('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'); + }); + + test('should correctly format NaN', () => { + const result = units.formatDuration(NaN); + expect(result).to.equal('NaN'); + }); + + test('should correctly format +/- Infinity', () => { + let result = units.formatDuration(Infinity); + expect(result).to.equal('Infinity'); + + result = units.formatDuration(-Infinity); + expect(result).to.equal('-Infinity'); + }); + + test('should correctly format 0 when padding is 0', () => { + const result = units.formatDuration(0, 0); + expect(result).to.equal('0.00'); + }); + + test('should correctly format negative durations', () => { + const result = units.formatDuration(-3600 - 120 - 3 - 0.4); + expect(result).to.equal('-01:02:03.40'); + }); + + test('should correctly format 59.9999', () => { + const result = units.formatDuration(59.9999); + expect(result).to.equal('00:01:00.00'); + }); + + test('should correctly format duration when extraDigits is false', () => { + let result = units.formatDuration(83, 0, 2, false); + expect(result).to.equal('1:23'); + + result = units.formatDuration(83.4, 0, 2, false); + expect(result).to.equal('1:23.4'); + + result = units.formatDuration(83.45, 0, 2, false); + expect(result).to.equal('1:23.45'); + + result = units.formatDuration(83.456, 0, 2, false); + expect(result).to.equal('1:23.46'); + + result = units.formatDuration(83.45, 0, 0, false); + expect(result).to.equal('1:23'); + }); +}); + +describe('formatPace method', () => { + test('should correctly format paces in a variety of units', () => { + let result = units.formatPace({ + distanceValue: 1, + distanceUnit: units.DistanceUnits.Meters, + time: 600, + }, units.PaceUnits.SecondsPerMeter); + expect(result).to.equal('10:00 s/m'); + + result = units.formatPace({ + distanceValue: 2, + distanceUnit: units.DistanceUnits.Kilometers, + time: 600, + }, units.PaceUnits.TimePerKilometer); + expect(result).to.equal('5:00 / km'); + + result = units.formatPace({ + distanceValue: 3, + distanceUnit: units.DistanceUnits.Miles, + time: 600, + }, units.PaceUnits.TimePerMile); + expect(result).to.equal('3:20 / mi'); + }); + + test('should correctly format paces that require distance conversion', () => { + let result = units.formatPace({ + distanceValue: 100, + distanceUnit: units.DistanceUnits.Meters, + time: 600, + }, units.PaceUnits.TimePerKilometer); + expect(result).to.equal('1:40:00 / km'); + + result = units.formatPace({ + distanceValue: 2, + distanceUnit: units.DistanceUnits.Kilometers, + time: 600, + }, units.PaceUnits.TimePerMile); + expect(result).to.equal('8:03 / mi'); + + result = units.formatPace({ + distanceValue: 0.03, + distanceUnit: units.DistanceUnits.Miles, + time: 600, + }, units.PaceUnits.SecondsPerMeter); + expect(result).to.equal('0:12 s/m'); + }); +});