running-tools

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

commit b9cc28896bcd4de991568fcddc67f35859338766
parent cedfddc830eee593b31732900a421953551b70d2
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sat, 31 May 2025 12:44:50 -0700

Implement target name customization option

Diffstat:
Msrc/utils/calculators.js | 4++--
Msrc/views/BatchCalculator.vue | 10+++++++++-
Msrc/views/WorkoutCalculator.vue | 10+++++++++-
Mtests/unit/utils/calculators.spec.js | 450++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mtests/unit/views/WorkoutCalculator.spec.js | 1+
5 files changed, 259 insertions(+), 216 deletions(-)

diff --git a/src/utils/calculators.js b/src/utils/calculators.js @@ -141,7 +141,7 @@ export function calculateRaceStats(input) { * Predict workout results from a target * @param {Object} input The input race * @param {Object} target The workout target - * @param {Object} options The race prediction options + * @param {Object} options The workout options * @param {Boolean} preciseDurations Whether to return precise, unrounded, durations * @returns {Object} The result */ @@ -169,7 +169,7 @@ export function calculateWorkoutResults(input, target, options, preciseDurations // Return result return { - key: target.customName || workoutTargetToString(target), + key: (options.customTargetNames && target.customName) || workoutTargetToString(target), value: formatDuration(t3, 3, preciseDurations ? 2 : 0, true), pace: '', // Pace not used in workout calculator result: 'value', diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue @@ -38,9 +38,16 @@ Target Set: <target-set-selector v-model:selectedTargetSet="selectedTargetSet" :setType="options.calculator === 'workout' ? 'workout' : 'standard'" - :customWorkoutNames="true" v-model:targetSets="targetSets" + :customWorkoutNames="advancedOptions.customTargetNames" v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/> </div> + <div v-if="options.calculator === 'workout'"> + Target Name Customization: + <select v-model="advancedOptions.customTargetNames" aria-label="Target name customization"> + <option :value="false">Disabled</option> + <option :value="true">Enabled</option> + </select> + </div> <race-options v-if="options.calculator !== 'pace'" v-model="advancedOptions"/> </details> @@ -118,6 +125,7 @@ const raceOptions = useStorage('race-calculator-options', { riegelExponent: 1.06, }); const workoutOptions = useStorage('workout-calculator-options', { + customTargetNames: false, model: 'AverageModel', riegelExponent: 1.06, }); diff --git a/src/views/WorkoutCalculator.vue b/src/views/WorkoutCalculator.vue @@ -19,9 +19,16 @@ <div> Target Set: <target-set-selector v-model:selectedTargetSet="selectedTargetSet" setType="workout" - :customWorkoutNames="true" v-model:targetSets="targetSets" + :customWorkoutNames="options.customTargetNames" v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/> </div> + <div> + Target Name Customization: + <select v-model="options.customTargetNames" aria-label="Target name customization"> + <option :value="false">Disabled</option> + <option :value="true">Enabled</option> + </select> + </div> <race-options v-model="options"/> </details> @@ -62,6 +69,7 @@ const defaultUnitSystem = useStorage('default-unit-system', detectDefaultUnitSys * The race prediction options */ const options = useStorage('workout-calculator-options', { + customTargetNames: false, model: 'AverageModel', riegelExponent: 1.06, }); diff --git a/tests/unit/utils/calculators.spec.js b/tests/unit/utils/calculators.spec.js @@ -1,229 +1,255 @@ -import { test, expect } from 'vitest'; +import { describe, test, expect } from 'vitest'; import * as calculatorUtils from '@/utils/calculators'; -test('should correctly calculate pace times', () => { - const input = { - distanceValue: 1, - distanceUnit: 'kilometers', - time: 100, - }; - const target = { - distanceValue: 20, - distanceUnit: 'meters', - type: 'distance', - }; - - const result = calculatorUtils.calculatePaceResults(input, target, 'metric'); - - expect(result).to.deep.equal({ - key: '20 m', - value: '0:02.00', - pace: '1:40 / km', - result: 'value', - sort: 2, +describe('calculatePaceResults method', () => { + test('should correctly calculate pace times', () => { + const input = { + distanceValue: 1, + distanceUnit: 'kilometers', + time: 100, + }; + const target = { + distanceValue: 20, + distanceUnit: 'meters', + type: 'distance', + }; + + const result = calculatorUtils.calculatePaceResults(input, target, 'metric'); + + expect(result).to.deep.equal({ + key: '20 m', + value: '0:02.00', + pace: '1:40 / km', + result: 'value', + sort: 2, + }); }); -}); -test('should correctly calculate pace distances according to default units setting', () => { - const input = { - distanceValue: 2, - distanceUnit: 'miles', - time: 1200, - }; - const target = { - time: 600, - type: 'time', - }; - - const result1 = calculatorUtils.calculatePaceResults(input, target, 'metric'); - const result2 = calculatorUtils.calculatePaceResults(input, target, 'imperial'); - - expect(result1.key).to.equal('1.61 km'); - expect(result1.value).to.equal('10:00'); - expect(result1.pace).to.equal('6:13 / km'); - expect(result1.result).to.equal('key'); - expect(result1.sort).to.be.closeTo(600, 0.01); - - expect(result2.key).to.equal('1.00 mi'); - expect(result2.value).to.equal('10:00'); - expect(result2.pace).to.equal('10:00 / mi'); - expect(result2.result).to.equal('key'); - expect(result2.sort).to.be.closeTo(600, 0.01); + test('should correctly calculate pace distances according to default units setting', () => { + const input = { + distanceValue: 2, + distanceUnit: 'miles', + time: 1200, + }; + const target = { + time: 600, + type: 'time', + }; + + const result1 = calculatorUtils.calculatePaceResults(input, target, 'metric'); + const result2 = calculatorUtils.calculatePaceResults(input, target, 'imperial'); + + expect(result1.key).to.equal('1.61 km'); + expect(result1.value).to.equal('10:00'); + expect(result1.pace).to.equal('6:13 / km'); + expect(result1.result).to.equal('key'); + expect(result1.sort).to.be.closeTo(600, 0.01); + + expect(result2.key).to.equal('1.00 mi'); + expect(result2.value).to.equal('10:00'); + expect(result2.pace).to.equal('10:00 / mi'); + expect(result2.result).to.equal('key'); + expect(result2.sort).to.be.closeTo(600, 0.01); + }); }); -test('should correctly predict race times', () => { - const input = { - distanceValue: 5, - distanceUnit: 'kilometers', - time: 1200, - }; - const target = { - distanceValue: 10, - distanceUnit: 'kilometers', - type: 'distance', - }; - const options = { - model: 'AverageModel', - riegelExponent: 1.06, - } - - const result = calculatorUtils.calculateRaceResults(input, target, options, 'imperial'); - - expect(result.key).to.equal('10 km'); - expect(result.value).to.equal('41:34.80'); - expect(result.pace).to.equal('6:42 / mi'); - expect(result.result).to.equal('value'); - expect(result.sort).to.be.closeTo(2494.80, 0.01); -}); +describe('calculateRaceResults method', () => { + test('should correctly predict race times', () => { + const input = { + distanceValue: 5, + distanceUnit: 'kilometers', + time: 1200, + }; + const target = { + distanceValue: 10, + distanceUnit: 'kilometers', + type: 'distance', + }; + const options = { + model: 'AverageModel', + riegelExponent: 1.06, + } + + const result = calculatorUtils.calculateRaceResults(input, target, options, 'imperial'); + + expect(result.key).to.equal('10 km'); + expect(result.value).to.equal('41:34.80'); + expect(result.pace).to.equal('6:42 / mi'); + expect(result.result).to.equal('value'); + expect(result.sort).to.be.closeTo(2494.80, 0.01); + }); -test('should correctly calculate race distances according to default units setting', () => { - const input = { - distanceValue: 5, - distanceUnit: 'kilometers', - time: 1200, - }; - const target = { - time: 2495, - type: 'time', - }; - const options = { - model: 'AverageModel', - riegelExponent: 1.06, - } - - const result1 = calculatorUtils.calculateRaceResults(input, target, options, 'metric'); - const result2 = calculatorUtils.calculateRaceResults(input, target, options, 'imperial'); - - expect(result1.key).to.equal('10.00 km'); - expect(result1.value).to.equal('41:35'); - expect(result1.pace).to.equal('4:09 / km'); - expect(result1.result).to.equal('key'); - expect(result1.sort).to.equal(2495); - - expect(result2.key).to.equal('6.21 mi'); - expect(result2.value).to.equal('41:35'); - expect(result2.pace).to.equal('6:41 / mi'); - expect(result2.result).to.equal('key'); - expect(result2.sort).to.equal(2495); -}); + test('should correctly calculate race distances according to default units setting', () => { + const input = { + distanceValue: 5, + distanceUnit: 'kilometers', + time: 1200, + }; + const target = { + time: 2495, + type: 'time', + }; + const options = { + model: 'AverageModel', + riegelExponent: 1.06, + } + + const result1 = calculatorUtils.calculateRaceResults(input, target, options, 'metric'); + const result2 = calculatorUtils.calculateRaceResults(input, target, options, 'imperial'); + + expect(result1.key).to.equal('10.00 km'); + expect(result1.value).to.equal('41:35'); + expect(result1.pace).to.equal('4:09 / km'); + expect(result1.result).to.equal('key'); + expect(result1.sort).to.equal(2495); + + expect(result2.key).to.equal('6.21 mi'); + expect(result2.value).to.equal('41:35'); + expect(result2.pace).to.equal('6:41 / mi'); + expect(result2.result).to.equal('key'); + expect(result2.sort).to.equal(2495); + }); -test('should correctly predict race times according to race options', () => { - const input = { - distanceValue: 2, - distanceUnit: 'miles', - time: 630, - }; - const target = { - distanceValue: 5, - distanceUnit: 'kilometers', - type: 'distance', - }; - const options = { - model: 'RiegelModel', - riegelExponent: 1.12, - } - - const result = calculatorUtils.calculateRaceResults(input, target, options, 'imperial'); - - expect(result.key).to.equal('5 km'); - expect(result.value).to.equal('17:11.77'); - expect(result.pace).to.equal('5:32 / mi'); - expect(result.result).to.equal('value'); - expect(result.sort).to.be.closeTo(1031.77, 0.01); + test('should correctly predict race times according to race options', () => { + const input = { + distanceValue: 2, + distanceUnit: 'miles', + time: 630, + }; + const target = { + distanceValue: 5, + distanceUnit: 'kilometers', + type: 'distance', + }; + const options = { + model: 'RiegelModel', + riegelExponent: 1.12, + } + + const result = calculatorUtils.calculateRaceResults(input, target, options, 'imperial'); + + expect(result.key).to.equal('5 km'); + expect(result.value).to.equal('17:11.77'); + expect(result.pace).to.equal('5:32 / mi'); + expect(result.result).to.equal('value'); + expect(result.sort).to.be.closeTo(1031.77, 0.01); + }); }); -test('should correctly calculate race statistics', () => { - const input = { - distanceValue: 5, - distanceUnit: 'kilometers', - time: 1200, - }; +describe('calculateRaceStats method', () => { + test('should correctly calculate race statistics', () => { + const input = { + distanceValue: 5, + distanceUnit: 'kilometers', + time: 1200, + }; - const results = calculatorUtils.calculateRaceStats(input); + const results = calculatorUtils.calculateRaceStats(input); - expect(results.purdyPoints).to.be.closeTo(454.5, 0.1); - expect(results.vo2).to.be.closeTo(47.4, 0.1); - expect(results.vo2MaxPercentage).to.be.closeTo(95.3, 0.1); - expect(results.vo2Max).to.be.closeTo(49.8, 0.1); + expect(results.purdyPoints).to.be.closeTo(454.5, 0.1); + expect(results.vo2).to.be.closeTo(47.4, 0.1); + expect(results.vo2MaxPercentage).to.be.closeTo(95.3, 0.1); + expect(results.vo2Max).to.be.closeTo(49.8, 0.1); + }); }); -test('should correctly calculate distance-based workouts according to race options', () => { - const input = { - distanceValue: 2, - distanceUnit: 'miles', - time: 630, - }; - const target = { - distanceValue: 5, - distanceUnit: 'kilometers', // 5k split is ~17:11.77 - splitValue: 1000, - splitUnit: 'meters', - type: 'distance', - }; - const options = { - model: 'RiegelModel', - riegelExponent: 1.12, - } - - const result = calculatorUtils.calculateWorkoutResults(input, target, options); - - expect(result.key).to.equal('1000 m @ 5 km'); - expect(result.value).to.equal('3:26.35'); - expect(result.pace).to.equal(''); - expect(result.result).to.equal('value'); - expect(result.sort).to.be.closeTo(206.35, 0.01); -}); +describe('calculateWorkoutResults method', () => { + test('should correctly calculate distance-based workouts according to race options', () => { + const input = { + distanceValue: 2, + distanceUnit: 'miles', + time: 630, + }; + const target = { + distanceValue: 5, + distanceUnit: 'kilometers', // 5k split is ~17:11.77 + splitValue: 1000, + splitUnit: 'meters', + type: 'distance', + }; + const options = { + customTargetNames: false, + model: 'RiegelModel', + riegelExponent: 1.12, + } + + const result = calculatorUtils.calculateWorkoutResults(input, target, options); + + expect(result.key).to.equal('1000 m @ 5 km'); + expect(result.value).to.equal('3:26.35'); + expect(result.pace).to.equal(''); + expect(result.result).to.equal('value'); + expect(result.sort).to.be.closeTo(206.35, 0.01); + }); -test('should correctly calculate distance-based workouts with custom names', () => { - const input = { - distanceValue: 2, - distanceUnit: 'miles', - time: 630, - }; - const target = { - distanceValue: 5, - distanceUnit: 'kilometers', // 5k split is ~17:11.77 - splitValue: 1000, - splitUnit: 'meters', - type: 'distance', - customName: 'my custom name', - }; - const options = { - model: 'RiegelModel', - riegelExponent: 1.12, - } - - const result = calculatorUtils.calculateWorkoutResults(input, target, options); - - expect(result.key).to.equal('my custom name'); - expect(result.value).to.equal('3:26.35'); - expect(result.pace).to.equal(''); - expect(result.result).to.equal('value'); - expect(result.sort).to.be.closeTo(206.35, 0.01); -}); + test('should correctly calculate distance-based workouts according to custom names', () => { + const input = { + distanceValue: 2, + distanceUnit: 'miles', + time: 630, + }; + const target_1 = { + distanceValue: 5, + distanceUnit: 'kilometers', // 5k split is ~17:11.77 + splitValue: 1000, + splitUnit: 'meters', + type: 'distance', + // no custom name + }; + const target_2 = { + distanceValue: 5, + distanceUnit: 'kilometers', // 5k split is ~17:11.77 + splitValue: 1000, + splitUnit: 'meters', + type: 'distance', + customName: 'my custom name', + }; + const options_a = { + customTargetNames: false, + model: 'RiegelModel', + riegelExponent: 1.12, + }; + const options_b = { + customTargetNames: true, + model: 'RiegelModel', + riegelExponent: 1.12, + }; + + const result1a = calculatorUtils.calculateWorkoutResults(input, target_1, options_a); + const result1b = calculatorUtils.calculateWorkoutResults(input, target_1, options_b); + const result2a = calculatorUtils.calculateWorkoutResults(input, target_2, options_a); + const result2b = calculatorUtils.calculateWorkoutResults(input, target_2, options_b); + + expect(result1a.key).to.equal('1000 m @ 5 km'); + expect(result1b.key).to.equal('1000 m @ 5 km'); + expect(result2a.key).to.equal('1000 m @ 5 km'); + expect(result2b.key).to.equal('my custom name'); + }); -test('should correctly calculate time-based workouts', () => { - const input = { - distanceValue: 5, - distanceUnit: 'kilometers', - time: 1200, - }; - const target = { - time: 2495, // ~10k split is 41:35 - splitValue: 1, - splitUnit: 'miles', - type: 'time', - }; - const options = { - model: 'AverageModel', - riegelExponent: 1.06, - } - - const result = calculatorUtils.calculateWorkoutResults(input, target, options); - - expect(result.key).to.equal('1 mi @ 41:35'); - expect(result.value).to.equal('6:41.50'); - expect(result.pace).to.equal(''); - expect(result.result).to.equal('value'); - expect(result.sort).to.be.closeTo(401.50, 0.01); + test('should correctly calculate time-based workouts', () => { + const input = { + distanceValue: 5, + distanceUnit: 'kilometers', + time: 1200, + }; + const target = { + time: 2495, // ~10k split is 41:35 + splitValue: 1, + splitUnit: 'miles', + type: 'time', + }; + const options = { + customTargetNames: false, + model: 'AverageModel', + riegelExponent: 1.06, + } + + const result = calculatorUtils.calculateWorkoutResults(input, target, options); + + expect(result.key).to.equal('1 mi @ 41:35'); + expect(result.value).to.equal('6:41.50'); + expect(result.pace).to.equal(''); + expect(result.result).to.equal('value'); + expect(result.sort).to.be.closeTo(401.50, 0.01); + }); }); diff --git a/tests/unit/views/WorkoutCalculator.spec.js b/tests/unit/views/WorkoutCalculator.spec.js @@ -73,6 +73,7 @@ test('should correctly calculate results according to advanced model options', a // Calculate result const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult; let result = calculateResult({ + customName: 'foo', splitValue: 1, splitUnit: 'kilometers', type: 'distance', distanceValue: 10, distanceUnit: 'kilometers', });