running-tools

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

commit 6d05ae8af95954bd2ec4af0fd7c5966e87a0997e
parent 423edb9a929979a01cd28a35fe9ca023b59d5fce
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sat, 21 Jun 2025 15:55:22 -0700

Convert most utils to TypeScript

Diffstat:
Dsrc/utils/format.js | 99-------------------------------------------------------------------------------
Asrc/utils/format.ts | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/utils/misc.js | 18------------------
Asrc/utils/misc.ts | 18++++++++++++++++++
Dsrc/utils/paces.js | 21---------------------
Asrc/utils/paces.ts | 21+++++++++++++++++++++
Dsrc/utils/races.js | 461-------------------------------------------------------------------------------
Asrc/utils/races.ts | 484+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/utils/storage.js | 36------------------------------------
Asrc/utils/storage.ts | 37+++++++++++++++++++++++++++++++++++++
Dsrc/utils/targets.js | 129-------------------------------------------------------------------------------
Asrc/utils/targets.ts | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/utils/units.js | 199-------------------------------------------------------------------------------
Asrc/utils/units.ts | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 1114 insertions(+), 963 deletions(-)

diff --git a/src/utils/format.js b/src/utils/format.js @@ -1,99 +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, 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 - */ -export 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; -} diff --git a/src/utils/format.ts b/src/utils/format.ts @@ -0,0 +1,102 @@ +/** + * 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/misc.js b/src/utils/misc.js @@ -1,18 +0,0 @@ -/** - * Create a deep copy of an object - * @param {Object} value The object to copy - * @returns {Object} The copied object - */ -export function deepCopy(value) { - return JSON.parse(JSON.stringify(value)); -} - -/** - * Test whether two objects are deeply equal - * @param {Object} value1 The first object - * @param {Object} value2 The second object - * @returns {Boolean} Whether the two objects are equal - */ -export function deepEqual(value1, value2) { - return JSON.stringify(value1) === JSON.stringify(value2); -} diff --git a/src/utils/misc.ts b/src/utils/misc.ts @@ -0,0 +1,18 @@ +/** + * Create a deep copy of an object + * @param {object} value The object to copy + * @returns {object} The copied object + */ +export function deepCopy(value: object): object { + return JSON.parse(JSON.stringify(value)); +} + +/** + * Test whether two objects are deeply equal + * @param {object} value1 The first object + * @param {object} value2 The second object + * @returns {boolean} Whether the two objects are equal + */ +export function deepEqual(value1: object, value2: object): boolean { + return JSON.stringify(value1) === JSON.stringify(value2); +} diff --git a/src/utils/paces.js b/src/utils/paces.js @@ -1,21 +0,0 @@ -/** - * Calculate time from a distance and input pace - * @param {Number} d1 The input pace distance (in any unit) - * @param {Number} t1 The input pace time (in seconds) - * @param {Number} d2 The output distance (in the same unit as d1) - * @returns {Number} The output time (in seconds) - */ -export function calculateTime(d1, t1, d2) { - return (t1 / d1) * d2 -} - -/** - * Calculate distance from a time and input pace - * @param {Number} t1 The input pace time (in seconds) - * @param {Number} d1 The input pace distance (in any unit) - * @param {Number} t2 The output time (in seconds) - * @returns {Number} The output distance (in the same unit as d1) - */ -export function calculateDistance(t1, d1, t2) { - return (d1 / t1) * t2 -} diff --git a/src/utils/paces.ts b/src/utils/paces.ts @@ -0,0 +1,21 @@ +/** + * Calculate time from a distance and input pace + * @param {number} d1 The input pace distance (in any unit) + * @param {number} t1 The input pace time (in seconds) + * @param {number} d2 The output distance (in the same unit as d1) + * @returns {number} The output time (in seconds) + */ +export function calculateTime(d1: number, t1: number, d2: number): number { + return (t1 / d1) * d2 +} + +/** + * Calculate distance from a time and input pace + * @param {number} t1 The input pace time (in seconds) + * @param {number} d1 The input pace distance (in any unit) + * @param {number} t2 The output time (in seconds) + * @returns {number} The output distance (in the same unit as d1) + */ +export function calculateDistance(t1: number, d1: number, t2: number): number { + return (d1 / t1) * t2 +} diff --git a/src/utils/races.js b/src/utils/races.js @@ -1,461 +0,0 @@ -/** - * Estimate the point at which a function returns a target value using Newton's Method - * @param {Number} initialEstimate The initial estimate - * @param {Number} target The target function output - * @param {Function} method The function - * @param {Function} derivative The function derivative - * @param {Number} precision The acceptable precision - * @returns {Number} The refined estimate - */ -function NewtonsMethod(initialEstimate, target, method, derivative, precision) { - // Initialize estimate - let estimate = initialEstimate; - let estimateValue; - - for (let i = 0; i < 500; i += 1) { - // Evaluate function at estimate - estimateValue = method(estimate); - - // Check if estimate is close enough (usually occurs way before i = 500) - if (Math.abs(target - estimateValue) < precision) { - break; - } - - // Refine estimate - estimate -= (estimateValue - target) / derivative(estimate); - } - - // Return refined estimate - return estimate; -} - -/* - * Methods that implement the Purdy Points race prediction model - * https://www.cs.uml.edu/~phoffman/xcinfo3.html - */ -const PurdyPointsModel = { - /** - * Calculate the Purdy Point variables for a distance - * @param {Number} d The distance in meters - * @returns {Object} The Purdy Point variables - */ - getVariables(d) { - // Declare constants - const c1 = 11.15895; - const c2 = 4.304605; - const c3 = 0.5234627; - const c4 = 4.031560; - const c5 = 2.316157; - const r1 = 3.796158e-2; - const r2 = 1.646772e-3; - const r3 = 4.107670e-4; - const r4 = 7.068099e-6; - const r5 = 5.220990e-9; - - // Calculate world record velocity from running curve - const v = (-c1 * Math.exp(-r1 * d)) - + (c2 * Math.exp(-r2 * d)) - + (c3 * Math.exp(-r3 * d)) - + (c4 * Math.exp(-r4 * d)) - + (c5 * Math.exp(-r5 * d)); - - // Calculate world record time - const twsec = d / v; - - // Calculate constants - const k = 0.0654 - (0.00258 * v); - const a = 85 / k; - const b = 1 - (1035 / a); - - // Return Purdy Point variables - return { - twsec, - a, - b, - }; - }, - - /** - * Get the Purdy Points for a race - * @param {Number} d The distance of the race in meters - * @param {Number} t The finish time of the race in seconds - * @returns {Number} The Purdy Points for the race - */ - getPurdyPoints(d, t) { - // Get variables - const variables = PurdyPointsModel.getVariables(d); - - // Calculate Purdy Points - const points = variables.a * ((variables.twsec / t) - variables.b); - - // Return Purdy Points - return points; - }, - - /** - * Predict a race time using the Purdy Points Model - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d2 The distance of the output race in meters - * @returns {Number} The predicted time for the output race in seconds - */ - predictTime(d1, t1, d2) { - // Calculate Purdy Points for distance 1 - const points = PurdyPointsModel.getPurdyPoints(d1, t1); - - // Calculate time for distance 2 - const variables = PurdyPointsModel.getVariables(d2); - const seconds = (variables.a * variables.twsec) / (points + (variables.a * variables.b)); - - // Return predicted time - return seconds; - }, - - /** - * Calculate the derivative with respect to distance of the Purdy Points curve at a specific point - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d2 The distance of the output race in meters - * @return {Number} The derivative with respect to distance - */ - derivative(d1, t1, d2) { - const result = (85 * d2) / (((2316157 * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 - + (100789 * Math.exp(-(7068099 * d2) / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 - * d2) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - - (223179 * Math.exp(-(1898079 * d2) / 50000000)) / 20000) * (327 / 5000 - (129 * ((2316157 - * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2) - / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000 - + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 - * d2) / 50000000)) / 20000)) / 50000) * ((85 * (1 - (207 * (327 / 5000 - (129 * ((2316157 - * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2) - / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000 - + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 - * d2) / 50000000)) / 20000)) / 50000)) / 17)) / (327 / 5000 - (129 * ((2316157 - * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2) - / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000 - + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 - * d2) / 50000000)) / 20000)) / 50000) + (85 * (d1 / (((2316157 * Math.exp(-(522099 * d1) - / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 - + (5234627 * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 - * d1) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000) * t1) - + (207 * (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000)) - / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627 - * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1) - / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000)) - / 17 - 1)) / (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000)) - / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627 - * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1) - / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000))); - return result; - }, - - /** - * Predict a race distance using the Purdy Points Model - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t2 The finish time of the output race in seconds - * @returns {Number} The predicted distance for the output race in meters - */ - predictDistance(t1, d1, t2) { - // Initialize estimate - let estimate = (d1 * t2) / t1; - - // Refine estimate (derivative on its own is too slow) - const method = (x) => PurdyPointsModel.predictTime(d1, t1, x); - const derivative = (x) => PurdyPointsModel.derivative(d1, t1, x) / 500; - estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01); - - // Return estimate - return estimate; - }, -}; - -/* - * Methods that implement the VO2 Max race prediction model - * http://run-down.com/statistics/calcs_explained.php - * https://vdoto2.com/Calculator - */ -const VO2MaxModel = { - /** - * Calculate the VO2 of a runner during a race - * @param {Number} d The race distance in meters - * @param {Number} t The finish time in seconds - * @returns {Number} The VO2 - */ - getVO2(d, t) { - const minutes = t / 60; - const v = d / minutes; - const result = -4.6 + (0.182258 * v) + (0.000104 * (v ** 2)); - return result; - }, - - /** - * Calculate the percentage of VO2 max a runner is at during a race - * @param {Number} t The race time in seconds - * @returns {Number} The percentage of VO2 max - */ - getVO2Percentage(t) { - const minutes = t / 60; - const result = 0.8 + (0.189439 * Math.exp(-0.012778 * minutes)) + (0.298956 * Math.exp(-0.193261 - * minutes)); - return result; - }, - - /** - * Calculate a runner's VO2 max from a race result - * @param {Number} d The race distance in meters - * @param {Number} t The finish time in seconds - * @returns {Number} The runner's VO2 max - */ - getVO2Max(d, t) { - const result = VO2MaxModel.getVO2(d, t) / VO2MaxModel.getVO2Percentage(t); - return result; - }, - - /** - * Calculate the derivative with respect to time of the VO2 max curve at a specific point - * @param {Number} d The race distance in meters - * @param {Number} t The finish time in seconds - * @return {Number} The derivative with respect to time - */ - VO2MaxTimeDerivative(d, t) { - const result = (-(273 * d) / (25 * (t ** 2)) - (468 * (d ** 2)) / (625 * (t ** 3))) / ((189 - * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5) - - (((273 * d) / (25 * t) + (234 * (d ** 2)) / (625 * (t ** 2)) - 23 / 5) * (-(63 - * Math.exp(-(2 * t) / 9375)) / 1562500 - (57707 * Math.exp(-(193 * t) / 60000)) / 60000000)) - / (((189 * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 - + 4 / 5) ** 2); - return result; - }, - - /** - * Predict a race time using the VO2 Max Model - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d2 The distance of the output race in meters - * @returns {Number} The predicted time for the output race in seconds - */ - predictTime(d1, t1, d2) { - // Calculate input VO2 max - const inputVO2Max = VO2MaxModel.getVO2Max(d1, t1); - - // Initialize estimate - let estimate = (t1 * d2) / d1; - - // Refine estimate - const method = (x) => VO2MaxModel.getVO2Max(d2, x); - const derivative = (x) => VO2MaxModel.VO2MaxTimeDerivative(d2, x); - estimate = NewtonsMethod(estimate, inputVO2Max, method, derivative, 0.0001); - - // Return estimate - return estimate; - }, - - /** - * Calculate the derivative with respect to distance of the VO2 max curve at a specific point - * @param {Number} d The race distance in meters - * @param {Number} t The finish time in seconds - * @return {Number} The derivative with respect to distance - */ - VO2MaxDistanceDerivative(d, t) { - const result = ((468 * d) / (625 * (t ** 2)) + 273 / (25 * t)) / ((189 * Math.exp(-(2 * t) - / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5); - return result; - }, - - /** - * Predict a race distance using the VO2 Max Model - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t2 The finish time of the output race in seconds - * @returns {Number} The predicted distance for the output race in meters - */ - predictDistance(t1, d1, t2) { - // Calculate input VO2 max - const inputVO2 = VO2MaxModel.getVO2Max(d1, t1); - - // Initialize estimate - let estimate = (d1 * t2) / t1; - - // Refine estimate - const method = (x) => VO2MaxModel.getVO2Max(x, t2); - const derivative = (x) => VO2MaxModel.VO2MaxDistanceDerivative(x, t2); - estimate = NewtonsMethod(estimate, inputVO2, method, derivative, 0.0001); - - // Return estimate - return estimate; - }, -}; - -/* - * Methods that implement Dave Cameron's race prediction model - * https://www.cs.uml.edu/~phoffman/cammod.html - */ -const CameronModel = { - /** - * Predict a race time using Dave Cameron's Model - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d2 The distance of the output race in meters - * @returns {Number} The predicted time for the output race in seconds - */ - predictTime(d1, t1, d2) { - const a = 13.49681 - (0.000030363 * d1) + (835.7114 / (d1 ** 0.7905)); - const b = 13.49681 - (0.000030363 * d2) + (835.7114 / (d2 ** 0.7905)); - return (t1 / d1) * (a / b) * d2; - }, - - /** - * Calculate the derivative with respect to distance of the Cameron curve at a specific point - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d2 The distance of the output race in meters - * @return {Number} The derivative with respect to distance - */ - derivative(d1, t1, d2) { - const result = -(100 * (30363 * (d1 ** (3581 / 2000)) - 13496810000 * (d1 ** (1581 / 2000)) - - 835711400000) * t1 * (134968100 * (d2 ** (3581 / 2000)) + 14963412617 * d2)) / ((d1 ** (3581 - / 2000)) * (d2 ** (419 / 2000)) * ((30363 * (d2 ** (3581 / 2000)) - 13496810000 * (d2 ** (1581 - / 2000)) - 835711400000) ** 2)); - return result; - }, - - /** - * Predict a race distance using Dave Cameron's Model - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t2 The finish time of the output race in seconds - * @returns {Number} The predicted distance for the output race in meters - */ - predictDistance(t1, d1, t2) { - // Initialize estimate - let estimate = (d1 * t2) / t1; - - // Refine estimate - const method = (x) => CameronModel.predictTime(d1, t1, x); - const derivative = (x) => CameronModel.derivative(d1, t1, x); - estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01); - - // Return estimate - return estimate; - }, -}; - -/* - * Methods that implement Pete Riegel's race prediction model - * https://en.wikipedia.org/wiki/Peter_Riegel - */ -const RiegelModel = { - /** - * Predict a race time using Pete Riegel's Model - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d2 The distance of the output race in meters - * @param {Number} c The value of the exponent in the equation - * @returns {Number} The predicted time for the output race in seconds - */ - predictTime(d1, t1, d2, c = 1.06) { - return t1 * ((d2 / d1) ** c); - }, - - /** - * Predict a race distance using Pete Riegel's Model - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t2 The finish time of the output race in seconds - * @param {Number} c The value of the exponent in the equation - * @returns {Number} The predicted distance for the output race in meters - */ - predictDistance(t1, d1, t2, c = 1.06) { - return d1 * ((t2 / t1) ** (1 / c)); - }, -}; - -/* - * Methods that average the results of different race prediction models - */ -const AverageModel = { - /** - * Predict a race time by averaging the results of different models - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d2 The distance of the output race in meters - * @param {Number} c The value of the exponent in Pete Riegel's Model - * @returns {Number} The predicted time for the output race in seconds - */ - predictTime(d1, t1, d2, c = 1.06) { - const purdy = PurdyPointsModel.predictTime(d1, t1, d2); - const vo2max = VO2MaxModel.predictTime(d1, t1, d2); - const cameron = CameronModel.predictTime(d1, t1, d2); - const riegel = RiegelModel.predictTime(d1, t1, d2, c); - return (purdy + vo2max + cameron + riegel) / 4; - }, - - /** - * Predict a race distance by averaging the results of different models - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t2 The finish time of the output race in seconds - * @param {Number} c The value of the exponent in Pete Riegel's Model - * @returns {Number} The predicted distance for the output race in meters - */ - predictDistance(t1, d1, t2, c = 1.06) { - const purdy = PurdyPointsModel.predictDistance(t1, d1, t2); - const vo2max = VO2MaxModel.predictDistance(t1, d1, t2); - const cameron = CameronModel.predictDistance(t1, d1, t2); - const riegel = RiegelModel.predictDistance(t1, d1, t2, c); - return (purdy + vo2max + cameron + riegel) / 4; - }, -}; - -/** - * Predict a race time - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d2 The distance of the output race in meters - * @param {String} model The race prediction model to use - * @param {Number} c The value of the exponent in Pete Riegel's Model - */ -export function predictTime(d1, t1, d2, model='AverageModel', c=1.06) { - switch (model) { - case 'AverageModel': - return AverageModel.predictTime(d1, t1, d2, c); - case 'PurdyPointsModel': - return PurdyPointsModel.predictTime(d1, t1, d2); - case 'VO2MaxModel': - return VO2MaxModel.predictTime(d1, t1, d2); - case 'RiegelModel': - return RiegelModel.predictTime(d1, t1, d2, c); - case 'CameronModel': - return CameronModel.predictTime(d1, t1, d2); - } -} - -/** - * Predict a race distance - * @param {Number} t1 The finish time of the input race in seconds - * @param {Number} d1 The distance of the input race in meters - * @param {Number} t2 The finish time of the output race in seconds - * @param {String} model The race prediction model to use - * @param {Number} c The value of the exponent in Pete Riegel's Model - */ -export function predictDistance(t1, d1, t2, model='AverageModel', c=1.06) { - switch (model) { - default: - case 'AverageModel': - return AverageModel.predictDistance(t1, d1, t2, c); - case 'PurdyPointsModel': - return PurdyPointsModel.predictDistance(t1, d1, t2); - case 'VO2MaxModel': - return VO2MaxModel.predictDistance(t1, d1, t2); - case 'RiegelModel': - return RiegelModel.predictDistance(t1, d1, t2, c); - case 'CameronModel': - return CameronModel.predictDistance(t1, d1, t2); - } -} - -export const getPurdyPoints = PurdyPointsModel.getPurdyPoints; -export const getVO2 = VO2MaxModel.getVO2; -export const getVO2Percentage = VO2MaxModel.getVO2Percentage; -export const getVO2Max = VO2MaxModel.getVO2Max; diff --git a/src/utils/races.ts b/src/utils/races.ts @@ -0,0 +1,484 @@ +export enum RacePredictionModel { + AverageModel = 'AverageModel', + PurdyPointsModel = 'PurdyPointsModel', + VO2MaxModel = 'VO2MaxModel', + RiegelModel = 'RiegelModel', + CameronModel = 'CameronModel', +} + +/** + * Estimate the point at which a function returns a target value using Newton's Method + * @param {number} initialEstimate The initial estimate + * @param {number} target The target function output + * @param {Function} method The function + * @param {Function} derivative The function derivative + * @param {number} precision The acceptable precision + * @returns {number} The refined estimate + */ +function NewtonsMethod(initialEstimate: number, target: number, method: (x: number) => number, + derivative: (x: number) => number, precision: number): number { + // Initialize estimate + let estimate = initialEstimate; + let estimateValue; + + for (let i = 0; i < 500; i += 1) { + // Evaluate function at estimate + estimateValue = method(estimate); + + // Check if estimate is close enough (usually occurs way before i = 500) + if (Math.abs(target - estimateValue) < precision) { + break; + } + + // Refine estimate + estimate -= (estimateValue - target) / derivative(estimate); + } + + // Return refined estimate + return estimate; +} + +/* + * The internal variables used by the Purdy Points race prediction model + */ +interface PurdyPointsVariables { + twsec: number, + a: number, + b: number, +} + +/* + * Methods that implement the Purdy Points race prediction model + * https://www.cs.uml.edu/~phoffman/xcinfo3.html + */ +const PurdyPointsModel = { + /** + * Calculate the Purdy Point variables for a distance + * @param {number} d The distance in meters + * @returns {PurdyPointsVariables} The Purdy Point variables + */ + getVariables(d: number): PurdyPointsVariables { + // Declare constants + const c1 = 11.15895; + const c2 = 4.304605; + const c3 = 0.5234627; + const c4 = 4.031560; + const c5 = 2.316157; + const r1 = 3.796158e-2; + const r2 = 1.646772e-3; + const r3 = 4.107670e-4; + const r4 = 7.068099e-6; + const r5 = 5.220990e-9; + + // Calculate world record velocity from running curve + const v = (-c1 * Math.exp(-r1 * d)) + + (c2 * Math.exp(-r2 * d)) + + (c3 * Math.exp(-r3 * d)) + + (c4 * Math.exp(-r4 * d)) + + (c5 * Math.exp(-r5 * d)); + + // Calculate world record time + const twsec = d / v; + + // Calculate constants + const k = 0.0654 - (0.00258 * v); + const a = 85 / k; + const b = 1 - (1035 / a); + + // Return Purdy Point variables + return { + twsec, + a, + b, + }; + }, + + /** + * Get the Purdy Points for a race + * @param {number} d The distance of the race in meters + * @param {number} t The finish time of the race in seconds + * @returns {number} The Purdy Points for the race + */ + getPurdyPoints(d: number, t: number): number { + // Get variables + const variables = PurdyPointsModel.getVariables(d); + + // Calculate Purdy Points + const points = variables.a * ((variables.twsec / t) - variables.b); + + // Return Purdy Points + return points; + }, + + /** + * Predict a race time using the Purdy Points Model + * @param {number} d1 The distance of the input race in meters + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d2 The distance of the output race in meters + * @returns {number} The predicted time for the output race in seconds + */ + predictTime(d1: number, t1: number, d2: number): number { + // Calculate Purdy Points for distance 1 + const points = PurdyPointsModel.getPurdyPoints(d1, t1); + + // Calculate time for distance 2 + const variables = PurdyPointsModel.getVariables(d2); + const seconds = (variables.a * variables.twsec) / (points + (variables.a * variables.b)); + + // Return predicted time + return seconds; + }, + + /** + * Calculate the derivative with respect to distance of the Purdy Points curve at a specific point + * @param {number} d1 The distance of the input race in meters + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d2 The distance of the output race in meters + * @return {number} The derivative with respect to distance + */ + derivative(d1: number, t1: number, d2: number): number { + const result = (85 * d2) / (((2316157 * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + + (100789 * Math.exp(-(7068099 * d2) / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 + * d2) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 + - (223179 * Math.exp(-(1898079 * d2) / 50000000)) / 20000) * (327 / 5000 - (129 * ((2316157 + * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2) + / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000 + + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 + * d2) / 50000000)) / 20000)) / 50000) * ((85 * (1 - (207 * (327 / 5000 - (129 * ((2316157 + * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2) + / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000 + + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 + * d2) / 50000000)) / 20000)) / 50000)) / 17)) / (327 / 5000 - (129 * ((2316157 + * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2) + / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000 + + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 + * d2) / 50000000)) / 20000)) / 50000) + (85 * (d1 / (((2316157 * Math.exp(-(522099 * d1) + / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + + (5234627 * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 + * d1) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000) * t1) + + (207 * (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000)) + / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627 + * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1) + / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000)) + / 17 - 1)) / (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000)) + / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627 + * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1) + / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000))); + return result; + }, + + /** + * Predict a race distance using the Purdy Points Model + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d1 The distance of the input race in meters + * @param {number} t2 The finish time of the output race in seconds + * @returns {number} The predicted distance for the output race in meters + */ + predictDistance(t1: number, d1: number, t2: number): number { + // Initialize estimate + let estimate = (d1 * t2) / t1; + + // Refine estimate (derivative on its own is too slow) + const method = (x: number) => PurdyPointsModel.predictTime(d1, t1, x); + const derivative = (x: number) => PurdyPointsModel.derivative(d1, t1, x) / 500; + estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01); + + // Return estimate + return estimate; + }, +}; + +/* + * Methods that implement the VO2 Max race prediction model + * http://run-down.com/statistics/calcs_explained.php + * https://vdoto2.com/Calculator + */ +const VO2MaxModel = { + /** + * Calculate the VO2 of a runner during a race + * @param {number} d The race distance in meters + * @param {number} t The finish time in seconds + * @returns {number} The VO2 + */ + getVO2(d: number, t: number): number { + const minutes = t / 60; + const v = d / minutes; + const result = -4.6 + (0.182258 * v) + (0.000104 * (v ** 2)); + return result; + }, + + /** + * Calculate the percentage of VO2 max a runner is at during a race + * @param {number} t The race time in seconds + * @returns {number} The percentage of VO2 max + */ + getVO2Percentage(t: number): number { + const minutes = t / 60; + const result = 0.8 + (0.189439 * Math.exp(-0.012778 * minutes)) + (0.298956 * Math.exp(-0.193261 + * minutes)); + return result; + }, + + /** + * Calculate a runner's VO2 max from a race result + * @param {number} d The race distance in meters + * @param {number} t The finish time in seconds + * @returns {number} The runner's VO2 max + */ + getVO2Max(d: number, t: number): number { + const result = VO2MaxModel.getVO2(d, t) / VO2MaxModel.getVO2Percentage(t); + return result; + }, + + /** + * Calculate the derivative with respect to time of the VO2 max curve at a specific point + * @param {number} d The race distance in meters + * @param {number} t The finish time in seconds + * @return {number} The derivative with respect to time + */ + VO2MaxTimeDerivative(d: number, t: number): number { + const result = (-(273 * d) / (25 * (t ** 2)) - (468 * (d ** 2)) / (625 * (t ** 3))) / ((189 + * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5) + - (((273 * d) / (25 * t) + (234 * (d ** 2)) / (625 * (t ** 2)) - 23 / 5) * (-(63 + * Math.exp(-(2 * t) / 9375)) / 1562500 - (57707 * Math.exp(-(193 * t) / 60000)) / 60000000)) + / (((189 * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + + 4 / 5) ** 2); + return result; + }, + + /** + * Predict a race time using the VO2 Max Model + * @param {number} d1 The distance of the input race in meters + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d2 The distance of the output race in meters + * @returns {number} The predicted time for the output race in seconds + */ + predictTime(d1: number, t1: number, d2: number): number { + // Calculate input VO2 max + const inputVO2Max = VO2MaxModel.getVO2Max(d1, t1); + + // Initialize estimate + let estimate = (t1 * d2) / d1; + + // Refine estimate + const method = (x: number) => VO2MaxModel.getVO2Max(d2, x); + const derivative = (x: number) => VO2MaxModel.VO2MaxTimeDerivative(d2, x); + estimate = NewtonsMethod(estimate, inputVO2Max, method, derivative, 0.0001); + + // Return estimate + return estimate; + }, + + /** + * Calculate the derivative with respect to distance of the VO2 max curve at a specific point + * @param {number} d The race distance in meters + * @param {number} t The finish time in seconds + * @return {number} The derivative with respect to distance + */ + VO2MaxDistanceDerivative(d: number, t: number): number { + const result = ((468 * d) / (625 * (t ** 2)) + 273 / (25 * t)) / ((189 * Math.exp(-(2 * t) + / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5); + return result; + }, + + /** + * Predict a race distance using the VO2 Max Model + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d1 The distance of the input race in meters + * @param {number} t2 The finish time of the output race in seconds + * @returns {number} The predicted distance for the output race in meters + */ + predictDistance(t1: number, d1: number, t2: number): number { + // Calculate input VO2 max + const inputVO2 = VO2MaxModel.getVO2Max(d1, t1); + + // Initialize estimate + let estimate = (d1 * t2) / t1; + + // Refine estimate + const method = (x: number) => VO2MaxModel.getVO2Max(x, t2); + const derivative = (x: number) => VO2MaxModel.VO2MaxDistanceDerivative(x, t2); + estimate = NewtonsMethod(estimate, inputVO2, method, derivative, 0.0001); + + // Return estimate + return estimate; + }, +}; + +/* + * Methods that implement Dave Cameron's race prediction model + * https://www.cs.uml.edu/~phoffman/cammod.html + */ +const CameronModel = { + /** + * Predict a race time using Dave Cameron's Model + * @param {number} d1 The distance of the input race in meters + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d2 The distance of the output race in meters + * @returns {number} The predicted time for the output race in seconds + */ + predictTime(d1: number, t1: number, d2: number): number { + const a = 13.49681 - (0.000030363 * d1) + (835.7114 / (d1 ** 0.7905)); + const b = 13.49681 - (0.000030363 * d2) + (835.7114 / (d2 ** 0.7905)); + return (t1 / d1) * (a / b) * d2; + }, + + /** + * Calculate the derivative with respect to distance of the Cameron curve at a specific point + * @param {number} d1 The distance of the input race in meters + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d2 The distance of the output race in meters + * @return {number} The derivative with respect to distance + */ + derivative(d1: number, t1: number, d2: number): number { + const result = -(100 * (30363 * (d1 ** (3581 / 2000)) - 13496810000 * (d1 ** (1581 / 2000)) + - 835711400000) * t1 * (134968100 * (d2 ** (3581 / 2000)) + 14963412617 * d2)) / ((d1 ** (3581 + / 2000)) * (d2 ** (419 / 2000)) * ((30363 * (d2 ** (3581 / 2000)) - 13496810000 * (d2 ** (1581 + / 2000)) - 835711400000) ** 2)); + return result; + }, + + /** + * Predict a race distance using Dave Cameron's Model + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d1 The distance of the input race in meters + * @param {number} t2 The finish time of the output race in seconds + * @returns {number} The predicted distance for the output race in meters + */ + predictDistance(t1: number, d1: number, t2: number): number { + // Initialize estimate + let estimate = (d1 * t2) / t1; + + // Refine estimate + const method = (x: number) => CameronModel.predictTime(d1, t1, x); + const derivative = (x: number) => CameronModel.derivative(d1, t1, x); + estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01); + + // Return estimate + return estimate; + }, +}; + +/* + * Methods that implement Pete Riegel's race prediction model + * https://en.wikipedia.org/wiki/Peter_Riegel + */ +const RiegelModel = { + /** + * Predict a race time using Pete Riegel's Model + * @param {number} d1 The distance of the input race in meters + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d2 The distance of the output race in meters + * @param {number} c The value of the exponent in the equation + * @returns {number} The predicted time for the output race in seconds + */ + predictTime(d1: number, t1: number, d2: number, c: number = 1.06): number { + return t1 * ((d2 / d1) ** c); + }, + + /** + * Predict a race distance using Pete Riegel's Model + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d1 The distance of the input race in meters + * @param {number} t2 The finish time of the output race in seconds + * @param {number} c The value of the exponent in the equation + * @returns {number} The predicted distance for the output race in meters + */ + predictDistance(t1: number, d1: number, t2: number, c: number = 1.06) { + return d1 * ((t2 / t1) ** (1 / c)); + }, +}; + +/* + * Methods that average the results of different race prediction models + */ +const AverageModel = { + /** + * Predict a race time by averaging the results of different models + * @param {number} d1 The distance of the input race in meters + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d2 The distance of the output race in meters + * @param {number} c The value of the exponent in Pete Riegel's Model + * @returns {number} The predicted time for the output race in seconds + */ + predictTime(d1: number, t1: number, d2: number, c: number = 1.06): number { + const purdy = PurdyPointsModel.predictTime(d1, t1, d2); + const vo2max = VO2MaxModel.predictTime(d1, t1, d2); + const cameron = CameronModel.predictTime(d1, t1, d2); + const riegel = RiegelModel.predictTime(d1, t1, d2, c); + return (purdy + vo2max + cameron + riegel) / 4; + }, + + /** + * Predict a race distance by averaging the results of different models + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d1 The distance of the input race in meters + * @param {number} t2 The finish time of the output race in seconds + * @param {number} c The value of the exponent in Pete Riegel's Model + * @returns {number} The predicted distance for the output race in meters + */ + predictDistance(t1: number, d1: number, t2: number, c: number = 1.06) { + const purdy = PurdyPointsModel.predictDistance(t1, d1, t2); + const vo2max = VO2MaxModel.predictDistance(t1, d1, t2); + const cameron = CameronModel.predictDistance(t1, d1, t2); + const riegel = RiegelModel.predictDistance(t1, d1, t2, c); + return (purdy + vo2max + cameron + riegel) / 4; + }, +}; + +/** + * Predict a race time + * @param {number} d1 The distance of the input race in meters + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d2 The distance of the output race in meters + * @param {string} model The race prediction model to use + * @param {number} c The value of the exponent in Pete Riegel's Model + */ +export function predictTime(d1: number, t1: number, d2: number, + model: RacePredictionModel = RacePredictionModel.AverageModel, + c: number = 1.06): number { + switch (model) { + default: + case RacePredictionModel.AverageModel: + return AverageModel.predictTime(d1, t1, d2, c); + case RacePredictionModel.PurdyPointsModel: + return PurdyPointsModel.predictTime(d1, t1, d2); + case RacePredictionModel.VO2MaxModel: + return VO2MaxModel.predictTime(d1, t1, d2); + case RacePredictionModel.RiegelModel: + return RiegelModel.predictTime(d1, t1, d2, c); + case RacePredictionModel.CameronModel: + return CameronModel.predictTime(d1, t1, d2); + } +} + +/** + * Predict a race distance + * @param {number} t1 The finish time of the input race in seconds + * @param {number} d1 The distance of the input race in meters + * @param {number} t2 The finish time of the output race in seconds + * @param {string} model The race prediction model to use + * @param {number} c The value of the exponent in Pete Riegel's Model + */ +export function predictDistance(t1: number, d1: number, t2: number, + model: RacePredictionModel = RacePredictionModel.AverageModel, + c: number = 1.06) { + switch (model) { + default: + case RacePredictionModel.AverageModel: + return AverageModel.predictDistance(t1, d1, t2, c); + case RacePredictionModel.PurdyPointsModel: + return PurdyPointsModel.predictDistance(t1, d1, t2); + case RacePredictionModel.VO2MaxModel: + return VO2MaxModel.predictDistance(t1, d1, t2); + case RacePredictionModel.RiegelModel: + return RiegelModel.predictDistance(t1, d1, t2, c); + case RacePredictionModel.CameronModel: + return CameronModel.predictDistance(t1, d1, t2); + } +} + +export const getPurdyPoints = PurdyPointsModel.getPurdyPoints; +export const getVO2 = VO2MaxModel.getVO2; +export const getVO2Percentage = VO2MaxModel.getVO2Percentage; +export const getVO2Max = VO2MaxModel.getVO2Max; diff --git a/src/utils/storage.js b/src/utils/storage.js @@ -1,36 +0,0 @@ -// The global localStorage prefix -const prefix = 'running-tools'; - -/** - * Read an object from a localStorage item - * @param {String} key The localStorage item's key - * @returns {Object} The object - */ -export function get(key) { - try { - return JSON.parse(localStorage.getItem(`${prefix}.${key}`)); - } catch { - return null; - } -} - -/** - * Write an object to a localStorage item - * @param {String} key The localStorage item's key - * @param {Object} value The object to write - */ -export function set(key, value) { - localStorage.setItem(`${prefix}.${key}`, JSON.stringify(value)); -} - -/** - * Migrate outdated localStorage options - */ -export function migrate() { - // Add customTargetNames property to workout options (>1.4.1) - let workoutOptions = get('workout-calculator-options'); - if (workoutOptions !== null && workoutOptions.customTargetNames === undefined) { - workoutOptions.customTargetNames = false; - set('workout-calculator-options', workoutOptions); - } -} diff --git a/src/utils/storage.ts b/src/utils/storage.ts @@ -0,0 +1,37 @@ +// The global localStorage prefix +const prefix = 'running-tools'; + +/** + * Read an object from a localStorage item + * @param {string} key The localStorage item's key + * @returns {object} The object + */ +export function get(key: string): object | null { + try { + return JSON.parse(localStorage.getItem(`${prefix}.${key}`) || ''); + } catch { + return null; + } +} + +/** + * Write an object to a localStorage item + * @param {string} key The localStorage item's key + * @param {object} value The object to write + */ +export function set(key: string, value: object) { + localStorage.setItem(`${prefix}.${key}`, JSON.stringify(value)); +} + +/** + * Migrate outdated localStorage options + */ +export function migrate() { + // Add customTargetNames property to workout options (>1.4.1) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const workoutOptions = get('workout-calculator-options') as any; // TODO: update types + if (workoutOptions !== null && workoutOptions.customTargetNames === undefined) { + workoutOptions.customTargetNames = false; + set('workout-calculator-options', workoutOptions); + } +} diff --git a/src/utils/targets.js b/src/utils/targets.js @@ -1,129 +0,0 @@ -import { formatDuration, formatNumber } from '@/utils/format'; -import { DISTANCE_UNITS, convertDistance } from '@/utils/units'; - -/** - * Sort an array of targets - * @param {Array} targets The array of targets - * @returns {Array} The sorted targets - */ -export function sort(targets) { - return [ - ...targets.filter((item) => item.type === 'distance') - .sort((a, b) => convertDistance(a.distanceValue, a.distanceUnit, 'meters') - - convertDistance(b.distanceValue, b.distanceUnit, 'meters')), - - ...targets.filter((item) => item.type === 'time') - .sort((a, b) => a.time - b.time), - ]; -} - -/** - * Generate a string description of a workout target - * @param {Object} target The workout target - * @return {String} The string description - */ -export function workoutTargetToString(target) { - let result = formatNumber(target.splitValue, 0, 2, false) + ' ' + - DISTANCE_UNITS[target.splitUnit].symbol; - if (target.type === '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) + ' ' + - DISTANCE_UNITS[target.distanceUnit].symbol; - } - return result; -} - -export const defaultTargetSets = { - '_pace_targets': { - name: 'Common Pace Targets', - targets: sort([ - { type: 'distance', distanceValue: 100, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 200, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 300, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 600, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1000, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1200, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' }, - - { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 3, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 4, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' }, - - { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 3, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 6, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 8, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 10, distanceUnit: 'miles' }, - - { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' }, - { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' }, - - { type: 'time', time: 600 }, - { type: 'time', time: 1800 }, - { type: 'time', time: 3600 }, - ]), - }, - '_race_targets': { - name: 'Common Race Targets', - targets: sort([ - { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 3000, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - - { type: 'distance', distanceValue: 3, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 15, distanceUnit: 'kilometers' }, - - { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' }, - { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' }, - ]), - }, - '_split_targets': { - name: '5K Mile Splits', - targets: [ - { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, - ], - }, - '_workout_targets': { - name: 'Common Workout Targets', - targets: [ - { - splitValue: 400, splitUnit: 'meters', - type: 'distance', distanceValue: 1, distanceUnit: 'miles', - }, - { - splitValue: 800, splitUnit: 'meters', - type: 'distance', distanceValue: 5, distanceUnit: 'kilometers', - }, - { - splitValue: 1600, splitUnit: 'meters', - type: 'time', time: 3600, - }, - { - splitValue: 1, splitUnit: 'miles', - type: 'distance', distanceValue: 1, distanceUnit: 'marathons', - }, - ], - }, -}; diff --git a/src/utils/targets.ts b/src/utils/targets.ts @@ -0,0 +1,224 @@ +import { formatDuration, formatNumber } from '@/utils/format'; +import { DISTANCE_UNITS, DISTANCE_UNIT_KEYS, convertDistance } from '@/utils/units'; + +/* + * Enumeration for the two basic types of targets: those defined by distance vs time + */ +export enum TargetType { + Distance = 'distance', + Time = 'time', +}; + +/** + * Type for basic distance-defined targets + */ +interface DistanceTarget { + type: TargetType.Distance, + distanceValue: number, + distanceUnit: DISTANCE_UNIT_KEYS, +}; + +/** + * Type for basic time-defined targets + */ +interface TimeTarget { + type: TargetType.Time, + time: number, +}; + +/** + * Type for pace and race calculator targets + */ +export type StandardTarget = DistanceTarget | TimeTarget; + +/* + * Type for pace and race calculator target sets + */ +export interface StandardTargetSet { + name: string, + targets: Array<StandardTarget>, +} + +/* + * Type for split calculator targets + */ +export type SplitTarget = DistanceTarget & { + splitTime?: number +}; + +/* + * Type for split calculator target sets + */ +export interface SplitTargetSet { + name: string, + targets: Array<SplitTarget>, +} + +/* + * Type for workout calculator targets + */ +export type WorkoutTarget = StandardTarget & { + splitValue: number, + splitUnit: DISTANCE_UNIT_KEYS, +}; + +/* + * Type for workout calculator target sets + */ +export interface WorkoutTargetSet { + name: string, + targets: Array<WorkoutTarget>, +} + +/* + * Type for generic targets + */ +export type Target = StandardTarget | SplitTarget | WorkoutTarget; + +/** + * Sort an array of targets + * @param {Array<Target>} targets The array of targets + * @returns {Array<Target>} The sorted targets + */ +export function sort(targets: Array<Target>): Array<Target> { + return [ + ...targets.filter((item) => item.type === TargetType.Distance) + .sort((a, b) => convertDistance(a.distanceValue, a.distanceUnit, DISTANCE_UNIT_KEYS.meters) + - convertDistance(b.distanceValue, b.distanceUnit, DISTANCE_UNIT_KEYS.meters)), + + ...targets.filter((item) => item.type === TargetType.Time) + .sort((a, b) => a.time - b.time), + ]; +} + +/** + * Generate a string description of a workout target + * @param {WorkoutTarget} target The workout target + * @return {String} The string description + */ +export function workoutTargetToString(target: WorkoutTarget): string { + let result = formatNumber(target.splitValue, 0, 2, false) + ' ' + + DISTANCE_UNITS[target.splitUnit].symbol; + if (target.type === TargetType.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) + ' ' + + DISTANCE_UNITS[target.distanceUnit].symbol; + } + return result; +} + +/** + * A set of common pace calculator targets + */ +const common_pace_targets: StandardTargetSet = { + name: 'Common Pace Targets', + targets: sort([ + { type: TargetType.Distance, distanceValue: 100, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 200, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 300, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 400, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 600, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 800, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 1000, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 1200, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 1500, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 1600, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 3200, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + + { type: TargetType.Distance, distanceValue: 2, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 3, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 4, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 5, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 6, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 8, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 10, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + + { type: TargetType.Distance, distanceValue: 1, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 2, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 3, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 5, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 6, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 8, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 10, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + + { type: TargetType.Distance, distanceValue: 0.5, distanceUnit: DISTANCE_UNIT_KEYS.marathons }, + { type: TargetType.Distance, distanceValue: 1, distanceUnit: DISTANCE_UNIT_KEYS.marathons }, + + { type: TargetType.Time, time: 600 }, + { type: TargetType.Time, time: 1800 }, + { type: TargetType.Time, time: 3600 }, + ]), +}; + +/** + * A set of common race calculator targets + */ +const common_race_targets: StandardTargetSet = { + name: 'Common Race Targets', + targets: sort([ + { type: TargetType.Distance, distanceValue: 400, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 800, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 1500, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 1600, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 1, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 3000, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 3200, distanceUnit: DISTANCE_UNIT_KEYS.meters }, + { type: TargetType.Distance, distanceValue: 2, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + + { type: TargetType.Distance, distanceValue: 3, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 5, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 6, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 8, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 10, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + { type: TargetType.Distance, distanceValue: 15, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + + { type: TargetType.Distance, distanceValue: 0.5, distanceUnit: DISTANCE_UNIT_KEYS.marathons }, + { type: TargetType.Distance, distanceValue: 1, distanceUnit: DISTANCE_UNIT_KEYS.marathons }, + ]), +}; + + +/** + * A set of targets for 5K mile splits + */ +const five_k_mile_splits: SplitTargetSet = { + name: '5K Mile Splits', + targets: [ + { type: TargetType.Distance, distanceValue: 1, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 2, distanceUnit: DISTANCE_UNIT_KEYS.miles }, + { type: TargetType.Distance, distanceValue: 5, distanceUnit: DISTANCE_UNIT_KEYS.kilometers }, + ], +}; + +/** + * A set of common workout calculator targets + */ +const common_workout_targets: WorkoutTargetSet = { + name: 'Common Workout Targets', + targets: [ + { + splitValue: 400, splitUnit: DISTANCE_UNIT_KEYS.meters, + type: TargetType.Distance, distanceValue: 1, distanceUnit: DISTANCE_UNIT_KEYS.miles, + }, + { + splitValue: 800, splitUnit: DISTANCE_UNIT_KEYS.meters, + type: TargetType.Distance, distanceValue: 5, distanceUnit: DISTANCE_UNIT_KEYS.kilometers, + }, + { + splitValue: 1600, splitUnit: DISTANCE_UNIT_KEYS.meters, + type: TargetType.Time, time: 3600, + }, + { + splitValue: 1, splitUnit: DISTANCE_UNIT_KEYS.miles, + type: TargetType.Distance, distanceValue: 1, distanceUnit: DISTANCE_UNIT_KEYS.marathons, + }, + ], +}; + +export const defaultTargetSets = { + _pace_targets: common_pace_targets, + _race_targets: common_race_targets, + _split_targets: five_k_mile_splits, + _workout_targets: common_workout_targets, +}; diff --git a/src/utils/units.js b/src/utils/units.js @@ -1,199 +0,0 @@ -/** - * The time units - */ -export const TIME_UNITS = { - seconds: { - name: 'Seconds', - symbol: 's', - value: 1, - }, - minutes: { - name: 'Minutes', - symbol: 'min', - value: 60, - }, - hours: { - name: 'Hours', - symbol: 'hr', - value: 3600, - }, -}; - -/** - * The distance units - */ -export const DISTANCE_UNITS = { - meters: { - name: 'Meters', - symbol: 'm', - value: 1, - }, - yards: { - name: 'Yards', - symbol: 'yd', - value: 0.9144, - }, - kilometers: { - name: 'Kilometers', - symbol: 'km', - value: 1000, - }, - miles: { - name: 'Miles', - symbol: 'mi', - value: 1609.3499, - }, - marathons: { - name: 'Marathons', - symbol: 'Mar', - value: 42195, - }, -}; - -/** - * The speed units - */ -export const SPEED_UNITS = { - meters_per_second: { - name: 'Meters per Second', - symbol: 'm/s', - value: 1, - }, - kilometers_per_hour: { - name: 'Kilometers per Hour', - symbol: 'kph', - value: DISTANCE_UNITS.kilometers.value / TIME_UNITS.hours.value, - }, - miles_per_hour: { - name: 'Miles per Hour', - symbol: 'mph', - value: DISTANCE_UNITS.miles.value / TIME_UNITS.hours.value, - }, -}; - -/** - * The value of each pace unit in seconds per meter - */ -export const PACE_UNITS = { - seconds_per_meter: { - name: 'Seconds per Meter', - symbol: 's/m', - value: 1, - }, - seconds_per_kilometer: { - name: 'Time per Kilometer', - symbol: '/ km', - value: TIME_UNITS.seconds.value / DISTANCE_UNITS.kilometers.value, - }, - seconds_per_mile: { - name: 'Time per Mile', - symbol: '/ mi', - value: TIME_UNITS.seconds.value / DISTANCE_UNITS.miles.value, - }, -}; - -/** - * Convert between time units - * @param {Number} inputValue The input value - * @param {String} inputUnit The unit of the input - * @param {String} outputUnit The unit of the output - * @returns {Number} The output - */ -export function convertTime(inputValue, inputUnit, outputUnit) { - return (inputValue * TIME_UNITS[inputUnit].value) / TIME_UNITS[outputUnit].value; -} - -/** - * Convert between distance units - * @param {Number} inputValue The input value - * @param {String} inputUnit The unit of the input - * @param {String} outputUnit The unit of the output - * @returns {Number} The output - */ -export function convertDistance(inputValue, inputUnit, outputUnit) { - return (inputValue * DISTANCE_UNITS[inputUnit].value) / DISTANCE_UNITS[outputUnit].value; -} - -/** - * Convert between speed units - * @param {Number} inputValue The input value - * @param {String} inputUnit The unit of the input - * @param {String} outputUnit The unit of the output - * @returns {Number} The output - */ -export function convertSpeed(inputValue, inputUnit, outputUnit) { - return (inputValue * SPEED_UNITS[inputUnit].value) / SPEED_UNITS[outputUnit].value; -} - -/** - * Convert between pace units - * @param {Number} inputValue The input value - * @param {String} inputUnit The unit of the input - * @param {String} outputUnit The unit of the output - * @returns {Number} The output - */ -export function convertPace(inputValue, inputUnit, outputUnit) { - return (inputValue * PACE_UNITS[inputUnit].value) / PACE_UNITS[outputUnit].value; -} - -/** - * Convert between speed and/or pace units - * @param {Number} inputValue The input value - * @param {String} inputUnit The unit of the input - * @param {String} outputUnit The unit of the output - * @returns {Number} The output - */ -export function convertSpeedPace(inputValue, inputUnit, outputUnit) { - // Calculate input speed - let speed; - if (inputUnit in PACE_UNITS) { - speed = 1 / (inputValue * PACE_UNITS[inputUnit].value); - } else { - speed = inputValue * SPEED_UNITS[inputUnit].value; - } - - // Calculate output - if (outputUnit in PACE_UNITS) { - return (1 / speed) / PACE_UNITS[outputUnit].value; - } - return speed / SPEED_UNITS[outputUnit].value; -} - -/** - * Detect the user's default unit system - * @returns {String} The default unit system - */ -export function detectDefaultUnitSystem() { - const language = (navigator.language || navigator.userLanguage).toLowerCase(); - if (language.endsWith('-us') || language.endsWith('-mm')) { - return 'imperial'; - } - return 'metric'; -} - -/** - * Get the default distance unit in a unit system - * @param {String} unitSystem The unit system - * @returns {String} The default distance unit - */ -export function getDefaultDistanceUnit(unitSystem) { - return unitSystem === 'metric' ? 'kilometers' : 'miles'; -} - -/** - * Get the default speed unit in a unit system - * @param {String} unitSystem The unit system - * @returns {String} The default speed unit - */ -export function getDefaultSpeedUnit(unitSystem) { - return unitSystem === 'metric' ? 'kilometers_per_hour' : 'miles_per_hour'; -} - -/** - * Get the default pace unit in a unit system - * @param {String} unitSystem The unit system - * @returns {String} The default pace unit - */ -export function getDefaultPaceUnit(unitSystem) { - return unitSystem === 'metric' ? 'seconds_per_kilometer' : 'seconds_per_mile'; -} diff --git a/src/utils/units.ts b/src/utils/units.ts @@ -0,0 +1,228 @@ +export const enum TIME_UNIT_KEYS { + seconds = 'seconds', + minutes = 'minutes', + hours = 'hours', +} +export const enum DISTANCE_UNIT_KEYS { + meters = 'meters', + yards = 'yards', + kilometers = 'kilometers', + miles = 'miles', + marathons = 'marathons', +} +export const enum SPEED_UNIT_KEYS { + meters_per_second = 'meters_per_second', + kilometers_per_hour = 'kilometers_per_hour', + miles_per_hour = 'miles_per_hour', +} +export const enum PACE_UNIT_KEYS { + seconds_per_meter = 'seconds_per_meter', + time_per_kilometer = 'seconds_per_kilometer', + time_per_mile = 'seconds_per_mile', +} + +/** + * The time units + */ +export const TIME_UNITS = { + seconds: { + name: 'Seconds', + symbol: 's', + value: 1, + }, + minutes: { + name: 'Minutes', + symbol: 'min', + value: 60, + }, + hours: { + name: 'Hours', + symbol: 'hr', + value: 3600, + }, +}; + +/** + * The distance units + */ +export const DISTANCE_UNITS = { + meters: { + name: 'Meters', + symbol: 'm', + value: 1, + }, + yards: { + name: 'Yards', + symbol: 'yd', + value: 0.9144, + }, + kilometers: { + name: 'Kilometers', + symbol: 'km', + value: 1000, + }, + miles: { + name: 'Miles', + symbol: 'mi', + value: 1609.3499, + }, + marathons: { + name: 'Marathons', + symbol: 'Mar', + value: 42195, + }, +}; + +/** + * The speed units + */ +export const SPEED_UNITS = { + meters_per_second: { + name: 'Meters per Second', + symbol: 'm/s', + value: 1, + }, + kilometers_per_hour: { + name: 'Kilometers per Hour', + symbol: 'kph', + value: DISTANCE_UNITS.kilometers.value / TIME_UNITS.hours.value, + }, + miles_per_hour: { + name: 'Miles per Hour', + symbol: 'mph', + value: DISTANCE_UNITS.miles.value / TIME_UNITS.hours.value, + }, +}; + +/** + * The value of each pace unit in seconds per meter + */ +export const PACE_UNITS = { + seconds_per_meter: { + name: 'Seconds per Meter', + symbol: 's/m', + value: 1, + }, + seconds_per_kilometer: { + name: 'Time per Kilometer', + symbol: '/ km', + value: TIME_UNITS.seconds.value / DISTANCE_UNITS.kilometers.value, + }, + seconds_per_mile: { + name: 'Time per Mile', + symbol: '/ mi', + value: TIME_UNITS.seconds.value / DISTANCE_UNITS.miles.value, + }, +}; + +/** + * Convert between time units + * @param {number} inputValue The input value + * @param {string} inputUnit The unit of the input + * @param {string} outputUnit The unit of the output + * @returns {number} The output + */ +export function convertTime(inputValue: number, inputUnit: TIME_UNIT_KEYS, + outputUnit: TIME_UNIT_KEYS): number { + return (inputValue * TIME_UNITS[inputUnit].value) / TIME_UNITS[outputUnit].value; +} + +/** + * Convert between distance units + * @param {number} inputValue The input value + * @param {string} inputUnit The unit of the input + * @param {string} outputUnit The unit of the output + * @returns {number} The output + */ +export function convertDistance(inputValue: number, inputUnit: DISTANCE_UNIT_KEYS, + outputUnit: DISTANCE_UNIT_KEYS): number { + return (inputValue * DISTANCE_UNITS[inputUnit].value) / DISTANCE_UNITS[outputUnit].value; +} + +/** + * Convert between speed units + * @param {number} inputValue The input value + * @param {string} inputUnit The unit of the input + * @param {string} outputUnit The unit of the output + * @returns {number} The output + */ +export function convertSpeed(inputValue: number, inputUnit: SPEED_UNIT_KEYS, + outputUnit: SPEED_UNIT_KEYS): number { + return (inputValue * SPEED_UNITS[inputUnit].value) / SPEED_UNITS[outputUnit].value; +} + +/** + * Convert between pace units + * @param {number} inputValue The input value + * @param {string} inputUnit The unit of the input + * @param {string} outputUnit The unit of the output + * @returns {number} The output + */ +export function convertPace(inputValue: number, inputUnit: PACE_UNIT_KEYS, + outputUnit: PACE_UNIT_KEYS): number { + return (inputValue * PACE_UNITS[inputUnit].value) / PACE_UNITS[outputUnit].value; +} + +/** + * Convert between speed and/or pace units + * @param {number} inputValue The input value + * @param {string} inputUnit The unit of the input + * @param {string} outputUnit The unit of the output + * @returns {number} The output + */ +export function convertSpeedPace(inputValue: number, inputUnit: SPEED_UNIT_KEYS | PACE_UNIT_KEYS, + outputUnit: SPEED_UNIT_KEYS | PACE_UNIT_KEYS): number { + // Calculate input speed + let speed; + if (inputUnit in PACE_UNITS) { + speed = 1 / (inputValue * PACE_UNITS[inputUnit as PACE_UNIT_KEYS].value); + } else { + speed = inputValue * SPEED_UNITS[inputUnit as SPEED_UNIT_KEYS].value; + } + + // Calculate output + if (outputUnit in PACE_UNITS) { + return (1 / speed) / PACE_UNITS[outputUnit as PACE_UNIT_KEYS].value; + } + return speed / SPEED_UNITS[outputUnit as SPEED_UNIT_KEYS].value; +} + +/** + * Detect the user's default unit system + * @returns {string} The default unit system + */ +export function detectDefaultUnitSystem(): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const language = (navigator.language || (navigator as any).userLanguage).toLowerCase(); + if (language.endsWith('-us') || language.endsWith('-mm')) { + return 'imperial'; + } + return 'metric'; +} + +/** + * Get the default distance unit in a unit system + * @param {string} unitSystem The unit system + * @returns {string} The default distance unit + */ +export function getDefaultDistanceUnit(unitSystem: string): string { + return unitSystem === 'metric' ? 'kilometers' : 'miles'; +} + +/** + * Get the default speed unit in a unit system + * @param {string} unitSystem The unit system + * @returns {string} The default speed unit + */ +export function getDefaultSpeedUnit(unitSystem: string): string { + return unitSystem === 'metric' ? 'kilometers_per_hour' : 'miles_per_hour'; +} + +/** + * Get the default pace unit in a unit system + * @param {string} unitSystem The unit system + * @returns {string} The default pace unit + */ +export function getDefaultPaceUnit(unitSystem: string): string { + return unitSystem === 'metric' ? 'seconds_per_kilometer' : 'seconds_per_mile'; +}