commit 6b28796f0472785d82c9effcc626d6232d4d54c1
parent 79d6bee6e188ce7e66ecf9bd6923100e5789fbaf
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Fri, 15 Aug 2025 14:03:58 -0700
Create RacePredictionOptions interface
Use RacePredictionOptions type in calculator options types. Also remove
legacy RaceOptionsInput component and several default function argument
values. Migration scripts and end-to-end tests not yet updated.
Diffstat:
14 files changed, 342 insertions(+), 260 deletions(-)
diff --git a/src/components/AdvancedOptionsInput.vue b/src/components/AdvancedOptionsInput.vue
@@ -33,7 +33,8 @@
<div v-if="props.type === Calculators.Race || props.type === Calculators.Workout">
Prediction model:
- <select v-model="(options as RaceOptions).model" aria-label="Prediction model">
+ <select v-model="(options as RaceOptions).predictionOptions.model"
+ aria-label="Prediction model">
<option :value="RacePredictionModels.AverageModel">Average</option>
<option :value="RacePredictionModels.PurdyPointsModel">Purdy Points Model</option>
<option :value="RacePredictionModels.VO2MaxModel">V̇O₂ Max Model</option>
@@ -43,10 +44,10 @@
</div>
<div v-if="props.type === Calculators.Race || props.type === Calculators.Workout"
- v-show="(options as RaceOptions).model == RacePredictionModels.AverageModel ||
- (options as RaceOptions).model == RacePredictionModels.RiegelModel">
+ v-show="(options as RaceOptions).predictionOptions.model == RacePredictionModels.AverageModel
+ || (options as RaceOptions).predictionOptions.model == RacePredictionModels.RiegelModel">
Riegel exponent:
- <decimal-input v-model="(options as RaceOptions).riegelExponent"
+ <decimal-input v-model="(options as RaceOptions).predictionOptions.riegelExponent"
aria-label="Riegel exponent" :min="1" :max="1.3" :digits="2" :step="0.01"/>
(default: 1.06)
</div>
diff --git a/src/components/RaceOptionsInput.vue b/src/components/RaceOptionsInput.vue
@@ -1,39 +0,0 @@
-<template>
- <div>
- Prediction Model:
- <select v-model="model.model" aria-label="Prediction model">
- <option value="AverageModel">Average</option>
- <option value="PurdyPointsModel">Purdy Points Model</option>
- <option value="VO2MaxModel">V̇O₂ Max Model</option>
- <option value="CameronModel">Cameron's Model</option>
- <option value="RiegelModel">Riegel's Model</option>
- </select>
- </div>
- <div>
- Riegel Exponent:
- <decimal-input v-model="model.riegelExponent" aria-label="Riegel exponent" :min="1" :max="1.3"
- :digits="2" :step="0.01"/>
- (default: 1.06)
- </div>
-</template>
-
-<script setup lang="ts">
-import type { RaceOptions } from '@/core/calculators';
-
-import DecimalInput from '@/components/DecimalInput.vue';
-import useObjectModel from '@/composables/useObjectModel';
-
-interface Props {
- /**
- * The component value
- */
- modelValue: RaceOptions,
-}
-
-const props = defineProps<Props>();
-
-// Generate internal ref tied to modelValue prop
-const emit = defineEmits(['update:modelValue']);
-const model = useObjectModel<RaceOptions>(() => props.modelValue,
- (x) => emit('update:modelValue', x));
-</script>
diff --git a/src/core/calculators.ts b/src/core/calculators.ts
@@ -3,6 +3,7 @@
*/
import * as racePrediction from '@/core/racePrediction';
+import type { RacePredictionOptions } from '@/core/racePrediction';
import { TargetTypes, workoutTargetToString } from '@/core/targets';
import type { StandardTarget, WorkoutTarget } from '@/core/targets';
import { DistanceUnits, UnitSystems, convertDistance, formatDistance, formatDuration, formatPace,
@@ -38,8 +39,7 @@ export interface StandardOptions {
selectedTargetSet: string,
}
export interface RaceOptions extends StandardOptions {
- model: racePrediction.RacePredictionModels,
- riegelExponent: number,
+ predictionOptions: RacePredictionOptions,
};
export interface WorkoutOptions extends RaceOptions {
customTargetNames: boolean,
@@ -88,8 +88,10 @@ export const defaultPaceOptions: StandardOptions = {
selectedTargetSet: '_pace_targets',
};
export const defaultRaceOptions: RaceOptions = {
- model: racePrediction.RacePredictionModels.AverageModel,
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: racePrediction.RacePredictionModels.AverageModel,
+ riegelExponent: 1.06,
+ },
selectedTargetSet: '_race_targets',
};
export const defaultSplitOptions: StandardOptions = {
@@ -165,7 +167,7 @@ function calculateStandardResult(input: DistanceTime, target: StandardTarget,
*/
export function calculatePaceResults(input: DistanceTime, target: StandardTarget,
defaultUnitSystem: UnitSystems,
- preciseDurations: boolean = true): TargetResult {
+ preciseDurations: boolean): TargetResult {
return calculateStandardResult(input, target, (d1, t1, d2) => ((t1 / d1) * d2),
(t1, d1, t2) => ((d1 / t1) * t2), defaultUnitSystem, preciseDurations);
@@ -175,18 +177,19 @@ export function calculatePaceResults(input: DistanceTime, target: StandardTarget
* Predict race results from a target
* @param {DistanceTime} input The input race
* @param {StandardTarget} target The race target
- * @param {RaceOptions} options The race prediction options
+ * @param {RacePredictionOptions} racePredictionOptions The race prediction options
* @param {UnitSystems} defaultUnitSystem The default unit system (imperial or metric)
* @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
* @returns {TargetResult} The result
*/
export function calculateRaceResults(input: DistanceTime, target: StandardTarget,
- options: RaceOptions, defaultUnitSystem: UnitSystems,
- preciseDurations: boolean = true): TargetResult {
+ racePredictionOptions: RacePredictionOptions,
+ defaultUnitSystem: UnitSystems, preciseDurations: boolean
+ ): TargetResult {
return calculateStandardResult(input, target,
- (d1, t1, d2) => racePrediction.predictTime(d1, t1, d2, options.model, options.riegelExponent),
- (t1, d1, t2) => racePrediction.predictDistance(t1, d1, t2, options.model, options.riegelExponent),
+ (d1, t1, d2) => racePrediction.predictTime(d1, t1, d2, racePredictionOptions),
+ (t1, d1, t2) => racePrediction.predictDistance(t1, d1, t2, racePredictionOptions),
defaultUnitSystem, preciseDurations);
}
@@ -210,13 +213,15 @@ export function calculateRaceStats(input: DistanceTime): RaceStats {
* Predict workout results from a target
* @param {DistanceTime} input The input race
* @param {WorkoutTarget} target The workout target
- * @param {WorkoutOptions} options The workout options
+ * @param {RacePredictionOptions} racePredictionOptions The race prediction options
+ * @param {Boolean} customTargetNames Whether to use custom target names
* @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
* @returns {TargetResult} The result
*/
export function calculateWorkoutResults(input: DistanceTime, target: WorkoutTarget,
- options: WorkoutOptions,
- preciseDurations: boolean = true): TargetResult {
+ racePredictionOptions: RacePredictionOptions,
+ customTargetNames: boolean, preciseDurations: boolean
+ ): TargetResult {
// Initialize distance and time variables
const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
const t1 = input.time;
@@ -229,18 +234,18 @@ export function calculateWorkoutResults(input: DistanceTime, target: WorkoutTarg
d2 = convertDistance(target.distanceValue, target.distanceUnit, DistanceUnits.Meters);
// Get workout split prediction
- t2 = racePrediction.predictTime(d1, input.time, d2, options.model, options.riegelExponent);
+ t2 = racePrediction.predictTime(d1, input.time, d2, racePredictionOptions);
} else {
t2 = target.time;
// Get workout split prediction
- d2 = racePrediction.predictDistance(t1, d1, t2, options.model, options.riegelExponent);
+ d2 = racePrediction.predictDistance(t1, d1, t2, racePredictionOptions);
}
const t3 = (t2 / d2) * d3;
// Return result
return {
- key: (options.customTargetNames && target.customName) || workoutTargetToString(target),
+ key: (customTargetNames && target.customName) || workoutTargetToString(target),
value: formatDuration(t3, 3, preciseDurations ? 2 : 0, true),
pace: '', // Pace not used in workout calculator
result: ResultType.Value,
diff --git a/src/core/racePrediction.ts b/src/core/racePrediction.ts
@@ -14,6 +14,14 @@ export enum RacePredictionModels {
};
/*
+ * The type for race prediction options
+ */
+export interface RacePredictionOptions {
+ model: RacePredictionModels,
+ riegelExponent: number,
+};
+
+/*
* The type for internal variables used by the Purdy Points race prediction model
*/
interface PurdyPointsVariables {
@@ -438,22 +446,21 @@ const AverageModel = {
* @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
+ * @param {RacePredictionOptions} options The race prediction options
+ * @param {number} The predicted finish time in seconds
*/
export function predictTime(d1: number, t1: number, d2: number,
- model: RacePredictionModels = RacePredictionModels.AverageModel,
- c: number = 1.06): number {
- switch (model) {
+ options: RacePredictionOptions): number {
+ switch (options.model) {
default:
case RacePredictionModels.AverageModel:
- return AverageModel.predictTime(d1, t1, d2, c);
+ return AverageModel.predictTime(d1, t1, d2, options.riegelExponent);
case RacePredictionModels.PurdyPointsModel:
return PurdyPointsModel.predictTime(d1, t1, d2);
case RacePredictionModels.VO2MaxModel:
return VO2MaxModel.predictTime(d1, t1, d2);
case RacePredictionModels.RiegelModel:
- return RiegelModel.predictTime(d1, t1, d2, c);
+ return RiegelModel.predictTime(d1, t1, d2, options.riegelExponent);
case RacePredictionModels.CameronModel:
return CameronModel.predictTime(d1, t1, d2);
}
@@ -464,22 +471,21 @@ export function predictTime(d1: number, t1: number, d2: number,
* @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
+ * @param {RacePredictionOptions} options The race prediction options
+ * @param {number} The predicted finish distance in meters
*/
export function predictDistance(t1: number, d1: number, t2: number,
- model: RacePredictionModels = RacePredictionModels.AverageModel,
- c: number = 1.06) {
- switch (model) {
+ options: RacePredictionOptions): number {
+ switch (options.model) {
default:
case RacePredictionModels.AverageModel:
- return AverageModel.predictDistance(t1, d1, t2, c);
+ return AverageModel.predictDistance(t1, d1, t2, options.riegelExponent);
case RacePredictionModels.PurdyPointsModel:
return PurdyPointsModel.predictDistance(t1, d1, t2);
case RacePredictionModels.VO2MaxModel:
return VO2MaxModel.predictDistance(t1, d1, t2);
case RacePredictionModels.RiegelModel:
- return RiegelModel.predictDistance(t1, d1, t2, c);
+ return RiegelModel.predictDistance(t1, d1, t2, options.riegelExponent);
case RacePredictionModels.CameronModel:
return CameronModel.predictDistance(t1, d1, t2);
}
diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue
@@ -196,13 +196,13 @@ const calculateResult = computed<(x: DistanceTime, y: targetUtils.Target) => Tar
return (x,y) => calculators.calculatePaceResults(x, y, defaultUnitSystem.value, false);
}
case (calculators.Calculators.Race): {
- return (x,y) => calculators.calculateRaceResults(x, y, raceOptions.value,
+ return (x,y) => calculators.calculateRaceResults(x, y, raceOptions.value.predictionOptions,
defaultUnitSystem.value, false);
}
default:
case (calculators.Calculators.Workout): {
return (x,y) => calculators.calculateWorkoutResults(x, y as targetUtils.WorkoutTarget,
- workoutOptions.value, false);
+ workoutOptions.value.predictionOptions, workoutOptions.value.customTargetNames, false);
}
}
});
diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue
@@ -32,7 +32,8 @@
<h2>Equivalent Race Results</h2>
<single-output-table class="output" show-pace
- :calculate-result="x => calculateRaceResults(input, x, options, defaultUnitSystem, true)"
+ :calculate-result="x => calculateRaceResults(input, x, options.predictionOptions,
+ defaultUnitSystem, true)"
:targets="targetSets[options.selectedTargetSet] ?
targetSets[options.selectedTargetSet].targets : []"/>
</div>
diff --git a/src/views/WorkoutCalculator.vue b/src/views/WorkoutCalculator.vue
@@ -15,7 +15,8 @@
<h2>Workout Splits</h2>
<single-output-table class="output"
- :calculate-result="x => calculateWorkoutResults(input, x as WorkoutTarget, options, true)"
+ :calculate-result="x => calculateWorkoutResults(input, x as WorkoutTarget,
+ options.predictionOptions, options.customTargetNames, true)"
:targets="targetSets[options.selectedTargetSet] ?
targetSets[options.selectedTargetSet].targets : []"/>
</div>
diff --git a/tests/unit/components/AdvancedOptionsInput.spec.js b/tests/unit/components/AdvancedOptionsInput.spec.js
@@ -68,8 +68,10 @@ test('should be correctly render race options according to props', () => {
propsData: {
defaultUnitSystem: 'metric',
options: {
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: '_new',
},
type: 'race',
@@ -98,8 +100,10 @@ test('should render riegel exponent field only for supported race prediction mod
propsData: {
defaultUnitSystem: 'metric',
options: {
- model: 'AverageModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: '_new',
},
type: 'race',
@@ -161,8 +165,10 @@ test('should be correctly render workout options according to props', () => {
defaultUnitSystem: 'metric',
options: {
customTargetNames: true,
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -190,8 +196,10 @@ test('should only show batch column label field when applicable', async () => {
defaultUnitSystem: 'metric',
options: {
customTargetNames: true,
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -220,8 +228,10 @@ test('should only show batch column label field when applicable', async () => {
defaultUnitSystem: 'metric',
options: {
customTargetNames: false, // disabled
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -247,8 +257,10 @@ test('should only show batch column label field when applicable', async () => {
defaultUnitSystem: 'metric',
options: {
customTargetNames: true, // enabled
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -275,8 +287,10 @@ test('should only show batch column label field when applicable', async () => {
},
defaultUnitSystem: 'metric',
options: {
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -295,8 +309,10 @@ test('should pass correct props to TargetSetSelector', async () => {
defaultUnitSystem: 'metric',
options: {
customTargetNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: 'B',
},
targetSets: {
@@ -359,8 +375,10 @@ test('should emit input events when options are modified', async () => {
defaultUnitSystem: 'metric',
options: {
customTargetNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -416,26 +434,34 @@ test('should emit input events when options are modified', async () => {
expect(wrapper.emitted()['update:options']).to.deep.equal([
[{
customTargetNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: 'B',
}],
[{
customTargetNames: true,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: 'B',
}],
[{
customTargetNames: true,
- model: 'CameronModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: 'B',
}],
[{
customTargetNames: true,
- model: 'CameronModel',
- riegelExponent: 1.3,
+ predictionOptions: {
+ model: 'CameronModel',
+ riegelExponent: 1.3,
+ },
selectedTargetSet: 'B',
}],
]);
diff --git a/tests/unit/components/RaceOptionsInput.spec.js b/tests/unit/components/RaceOptionsInput.spec.js
@@ -1,53 +0,0 @@
-import { test, expect } from 'vitest';
-import { shallowMount } from '@vue/test-utils';
-import RaceOptionsInput from '@/components/RaceOptionsInput.vue';
-
-test('should be initialized to modelValue', () => {
- // Initialize component
- const wrapper = shallowMount(RaceOptionsInput, {
- propsData: {
- modelValue: {
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
- }
- },
- });
-
- // Assert input fields are correct
- expect(wrapper.find('select').element.value).to.equal('PurdyPointsModel');
- expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1.2);
-});
-
-test('should emit event when inputs are modified', async () => {
- // Initialize component
- const wrapper = shallowMount(RaceOptionsInput, {
- propsData: {
- modelValue: {
- model: 'AverageModel',
- riegelExponent: 1.06,
- },
- },
- });
-
- // Update model
- await wrapper.find('select').setValue('CameronModel');
- expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
- [{
- model: 'CameronModel',
- riegelExponent: 1.06,
- }],
- ]);
-
- // Update Riegel exponent
- await wrapper.findComponent({ name: 'decimal-input' }).setValue(1.3);
- expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
- [{
- model: 'CameronModel',
- riegelExponent: 1.06,
- }],
- [{
- model: 'CameronModel',
- riegelExponent: 1.3,
- }],
- ]);
-});
diff --git a/tests/unit/core/calculators.spec.js b/tests/unit/core/calculators.spec.js
@@ -14,7 +14,7 @@ describe('calculatePaceResults method', () => {
type: 'distance',
};
- const result = calculators.calculatePaceResults(input, target, 'metric');
+ const result = calculators.calculatePaceResults(input, target, 'metric', true);
expect(result).to.deep.equal({
key: '20 m',
@@ -36,8 +36,8 @@ describe('calculatePaceResults method', () => {
type: 'time',
};
- const result1 = calculators.calculatePaceResults(input, target, 'metric');
- const result2 = calculators.calculatePaceResults(input, target, 'imperial');
+ const result1 = calculators.calculatePaceResults(input, target, 'metric', true);
+ const result2 = calculators.calculatePaceResults(input, target, 'imperial', true);
expect(result1.key).to.equal('1.61 km');
expect(result1.value).to.equal('10:00');
@@ -65,12 +65,13 @@ describe('calculateRaceResults method', () => {
distanceUnit: 'kilometers',
type: 'distance',
};
- const options = {
+ const racePredictionOptions = {
model: 'AverageModel',
riegelExponent: 1.06,
}
- const result = calculators.calculateRaceResults(input, target, options, 'imperial');
+ const result = calculators.calculateRaceResults(input, target, racePredictionOptions,
+ 'imperial', true);
expect(result.key).to.equal('10 km');
expect(result.value).to.equal('41:34.80');
@@ -89,13 +90,15 @@ describe('calculateRaceResults method', () => {
time: 2495,
type: 'time',
};
- const options = {
+ const racePredictionOptions = {
model: 'AverageModel',
riegelExponent: 1.06,
- }
+ };
- const result1 = calculators.calculateRaceResults(input, target, options, 'metric');
- const result2 = calculators.calculateRaceResults(input, target, options, 'imperial');
+ const result1 = calculators.calculateRaceResults(input, target, racePredictionOptions,
+ 'metric', true);
+ const result2 = calculators.calculateRaceResults(input, target, racePredictionOptions,
+ 'imperial', true);
expect(result1.key).to.equal('10.00 km');
expect(result1.value).to.equal('41:35');
@@ -121,12 +124,13 @@ describe('calculateRaceResults method', () => {
distanceUnit: 'kilometers',
type: 'distance',
};
- const options = {
+ const racePredictionOptions = {
model: 'RiegelModel',
riegelExponent: 1.12,
}
- const result = calculators.calculateRaceResults(input, target, options, 'imperial');
+ const result = calculators.calculateRaceResults(input, target, racePredictionOptions,
+ 'imperial', true);
expect(result.key).to.equal('5 km');
expect(result.value).to.equal('17:11.78');
@@ -167,13 +171,13 @@ describe('calculateWorkoutResults method', () => {
splitUnit: 'meters',
type: 'distance',
};
- const options = {
- customTargetNames: false,
+ const racePredictionOptions = {
model: 'RiegelModel',
riegelExponent: 1.12,
}
- const result = calculators.calculateWorkoutResults(input, target, options);
+ const result = calculators.calculateWorkoutResults(input, target, racePredictionOptions,
+ false, true);
expect(result.key).to.equal('1000 m @ 5 km');
expect(result.value).to.equal('3:26.36');
@@ -204,21 +208,19 @@ describe('calculateWorkoutResults method', () => {
type: 'distance',
customName: 'my custom name',
};
- const options_a = {
- customTargetNames: false,
- model: 'RiegelModel',
- riegelExponent: 1.12,
- };
- const options_b = {
- customTargetNames: true,
+ const racePredictionOptions = {
model: 'RiegelModel',
riegelExponent: 1.12,
};
- const result1a = calculators.calculateWorkoutResults(input, target_1, options_a);
- const result1b = calculators.calculateWorkoutResults(input, target_1, options_b);
- const result2a = calculators.calculateWorkoutResults(input, target_2, options_a);
- const result2b = calculators.calculateWorkoutResults(input, target_2, options_b);
+ const result1a = calculators.calculateWorkoutResults(input, target_1, racePredictionOptions,
+ false, true);
+ const result1b = calculators.calculateWorkoutResults(input, target_1, racePredictionOptions,
+ true, true);
+ const result2a = calculators.calculateWorkoutResults(input, target_2, racePredictionOptions,
+ false, true);
+ const result2b = calculators.calculateWorkoutResults(input, target_2, racePredictionOptions,
+ true, true);
expect(result1a.key).to.equal('1000 m @ 5 km');
expect(result1b.key).to.equal('1000 m @ 5 km');
@@ -238,13 +240,13 @@ describe('calculateWorkoutResults method', () => {
splitUnit: 'miles',
type: 'time',
};
- const options = {
- customTargetNames: false,
+ const racePredictionOptions = {
model: 'AverageModel',
riegelExponent: 1.06,
}
- const result = calculators.calculateWorkoutResults(input, target, options);
+ const result = calculators.calculateWorkoutResults(input, target, racePredictionOptions, false,
+ true);
expect(result.key).to.equal('1 mi @ 41:35');
expect(result.value).to.equal('6:41.50');
diff --git a/tests/unit/core/racePrediction.spec.js b/tests/unit/core/racePrediction.spec.js
@@ -4,66 +4,108 @@ import * as racePrediction from '@/core/racePrediction';
describe('predictTime method', () => {
describe('PredictTime method', () => {
test('Average Model', () => {
- const riegel = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const cameron = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const vo2Max = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const riegel = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'RiegelModel',
+ riegelExponent: 1.06,
+ });
+ const cameron = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ });
+ const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.06,
+ });
+ const vo2Max = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'VO2MaxModel',
+ riegelExponent: 1.06,
+ });
const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
- const result = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.equal(expected);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'AverageModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
describe('Purdy Points Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictTime(5000, 1200, 10000, 'PurdyPointsModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(2490, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'PurdyPointsModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
describe('VO2 Max Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictTime(5000, 1200, 10000, 'VO2MaxModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'VO2MaxModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(2488, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'VO2MaxModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'VO2MaxModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
describe('Cameron Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictTime(5000, 1200, 10000, 'CameronModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(2500, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'CameronModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
describe('Riegel Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictTime(5000, 1200, 10000, 'RiegelModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'RiegelModel',
+ RiegelModel: 1.06,
+ });
expect(result).to.be.closeTo(2502, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'RiegelModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'RiegelModel',
+ RiegelModel: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
@@ -72,66 +114,108 @@ describe('predictTime method', () => {
describe('predictDistance method', () => {
describe('Average Model', () => {
test('Predictions should be correct', () => {
- const riegel = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const cameron = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const vo2Max = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const riegel = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'RiegelModel',
+ riegel: 1.06,
+ });
+ const cameron = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'CameronModel',
+ riegel: 1.06,
+ });
+ const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'PurdyPointsModel',
+ riegel: 1.06,
+ });
+ const vo2Max = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'VO2MaxModel',
+ riegel: 1.06,
+ });
const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
- const result = racePrediction.predictDistance(1200, 5000, expected);
+ const result = racePrediction.predictDistance(1200, 5000, expected, {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'AverageModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'AverageModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
describe('Purdy Points Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictDistance(1200, 5000, 2490, 'PurdyPointsModel');
+ const result = racePrediction.predictDistance(1200, 5000, 2490, {
+ model: 'PurdyPointsModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'PurdyPointsModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'PurdyPointsModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
describe('VO2 Max Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictDistance(1200, 5000, 2488, 'VO2MaxModel');
+ const result = racePrediction.predictDistance(1200, 5000, 2488, {
+ model: 'VO2MaxModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'VO2MaxModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'VO2MaxModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
describe('Cameron Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictDistance(1200, 5000, 2500, 'CameronModel');
+ const result = racePrediction.predictDistance(1200, 5000, 2500, {
+ model: 'CameronModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'CameronModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'CameronModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
describe('Riegel Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictDistance(1200, 5000, 2502, 'RiegelModel');
+ const result = racePrediction.predictDistance(1200, 5000, 2502, {
+ model: 'RiegelModel',
+ RiegelModel: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'RiegelModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'RiegelModel',
+ RiegelModel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
diff --git a/tests/unit/views/BatchCalculator.spec.js b/tests/unit/views/BatchCalculator.spec.js
@@ -210,14 +210,18 @@ test('should load calculator options from localStorage', async () => {
selectedTargetSet: 'A',
}));
localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: 'C',
}));
localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({
customTargetNames: true,
- model: 'RiegelModel',
- riegelExponent: 1.1,
+ predictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1.1,
+ },
selectedTargetSet: 'E',
}));
@@ -235,8 +239,10 @@ test('should load calculator options from localStorage', async () => {
// Assert race calculator options are loaded
await wrapper.find('select[aria-label="Calculator"]').setValue('race');
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: 'C',
});
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
@@ -246,8 +252,10 @@ test('should load calculator options from localStorage', async () => {
await wrapper.find('select[aria-label="Calculator"]').setValue('workout');
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
customTargetNames: true,
- model: 'RiegelModel',
- riegelExponent: 1.1,
+ predictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1.1,
+ },
selectedTargetSet: 'E',
});
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
@@ -301,8 +309,10 @@ test('should save calculator options to localStorage when modified', async () =>
}
}));
localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: 'D',
}));
localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify({
@@ -316,8 +326,10 @@ test('should save calculator options to localStorage when modified', async () =>
}));
localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({
customWorkoutNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: 'F',
}));
@@ -335,8 +347,10 @@ test('should save calculator options to localStorage when modified', async () =>
// Update race calculator options
await wrapper.find('select[aria-label="Calculator"]').setValue('race');
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: 'C',
}, 'options');
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
@@ -346,8 +360,10 @@ test('should save calculator options to localStorage when modified', async () =>
await wrapper.find('select[aria-label="Calculator"]').setValue('workout');
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
customTargetNames: true,
- model: 'RiegelModel',
- riegelExponent: 1.1,
+ predictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1.1,
+ },
selectedTargetSet: 'E',
}, 'options');
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
@@ -358,14 +374,18 @@ test('should save calculator options to localStorage when modified', async () =>
selectedTargetSet: 'A',
}));
expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(JSON.stringify({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: 'C',
}));
expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({
customTargetNames: true,
- model: 'RiegelModel',
- riegelExponent: 1.1,
+ predictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1.1,
+ },
selectedTargetSet: 'E',
}));
});
@@ -451,8 +471,10 @@ test('should pass correct input props to DoubleOutputTable', async () => {
// Enable target name customization
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
customTargetNames: true,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: '_workout_targets',
}, 'options');
@@ -517,8 +539,10 @@ test('should correctly set AdvancedOptionsInput props', async () => {
await wrapper.find('select[aria-label="Calculator"]').setValue('race');
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.type).to.equal('race');
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: '_race_targets',
});
@@ -527,8 +551,10 @@ test('should correctly set AdvancedOptionsInput props', async () => {
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.type).to.equal('workout');
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
customTargetNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: '_workout_targets',
});
});
@@ -537,13 +563,17 @@ test('should correctly calculate outputs', async () => {
// Initialize localStorage
localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({
selectedTargetSet: '_race_targets',
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
}));
localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({
selectedTargetSet: '_workout_targets',
- model: 'RiegelModel',
- riegelExponent: 1.1,
+ predictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1.1,
+ },
}));
localStorage.setItem('running-tools.default-unit-system', '"imperial"');
diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js
@@ -87,8 +87,10 @@ test('should correctly handle null target set', async () => {
// Switch to invalid target set
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: 'does_not_exist',
}, 'options');
@@ -97,8 +99,10 @@ test('should correctly handle null target set', async () => {
// Switch to valid target set
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- model: 'AverageModel',
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
selectedTargetSet: '_race_targets',
}, 'options');
@@ -144,8 +148,10 @@ test('should correctly calculate results according to model options', async () =
// Switch model
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- model: 'RiegelModel', // changed from the Riegel Model
- riegelExponent: 1.06,
+ predictionOptions: {
+ model: 'RiegelModel', // changed from the Riegel Model
+ riegelExponent: 1.06,
+ },
selectedTargetSet: '_race_targets',
}, 'options');
@@ -162,8 +168,10 @@ test('should correctly calculate results according to model options', async () =
// Update Riegel Exponent
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- model: 'RiegelModel',
- riegelExponent: 1, // changed from 1.06
+ predictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1, // changed from 1.06
+ },
selectedTargetSet: '_race_targets',
}, 'options');
@@ -252,8 +260,10 @@ test('should load options from localStorage', async () => {
'B': targetSet2,
}));
localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: 'B',
}));
@@ -262,8 +272,10 @@ test('should load options from localStorage', async () => {
// Assert data loaded
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ predictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
selectedTargetSet: 'B',
});
expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets)
@@ -276,15 +288,19 @@ test('should save options to localStorage when modified', async () => {
// Update options
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- model: 'CameronModel',
- riegelExponent: 1.30,
+ predictionOptions: {
+ model: 'CameronModel',
+ riegelExponent: 1.30,
+ },
selectedTargetSet: 'B',
}, 'options');
// Assert data saved to localStorage
expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(JSON.stringify({
- model: 'CameronModel',
- riegelExponent: 1.3,
+ predictionOptions: {
+ model: 'CameronModel',
+ riegelExponent: 1.3,
+ },
selectedTargetSet: 'B',
}));
});
diff --git a/tests/unit/views/WorkoutCalculator.spec.js b/tests/unit/views/WorkoutCalculator.spec.js
@@ -75,8 +75,10 @@ test('should correctly calculate results according to advanced model options', a
// Update model and Riegel Exponent
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
customTargetNames: false,
- model: 'RiegelModel',
- riegelExponent: 1.10,
+ predictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1.10,
+ },
selectedTargetSet: '_workout_targets',
}, 'options');