commit df2e196cbdbe1a47eadb626b89c391ded5e75642
parent 21966fd2e29e494d9ec2eb8074e441faf5c88168
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sat, 1 Jun 2024 10:44:32 -0700
Extract calculator logic to utils module
Diffstat:
4 files changed, 306 insertions(+), 180 deletions(-)
diff --git a/src/utils/calculators.js b/src/utils/calculators.js
@@ -0,0 +1,156 @@
+import paceUtils from '@/utils/paces';
+import raceUtils from '@/utils/races';
+import unitUtils from '@/utils/units';
+
+/**
+ * Calculate paces from a target
+ * @param {Object} input The input pace
+ * @param {Object} target The pace target
+ * @param {String} defaultUnitSystem The default unit system (imperial or metric)
+ * @returns {Object} The result
+ */
+function calculatePaceResults(input, target, defaultUnitSystem) {
+ const result = {
+ distanceValue: target.distanceValue,
+ distanceUnit: target.distanceUnit,
+ time: target.time,
+ result: target.result,
+ };
+
+ const pace = paceUtils.getPace(unitUtils.convertDistance(input.distanceValue, input.distanceUnit,
+ 'meters'), input.time);
+
+ // Add missing value to result
+ if (target.result === 'time') {
+ // Convert target distance into meters
+ const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, 'meters');
+
+ // Calculate time to travel distance at input pace
+ const time = paceUtils.getTime(pace, d2);
+
+ // Update result
+ result.time = time;
+ } else {
+ // Calculate distance traveled in time at input pace
+ let distance = paceUtils.getDistance(pace, target.time);
+
+ // Convert output distance into default distance unit
+ distance = unitUtils.convertDistance(distance, 'meters',
+ unitUtils.getDefaultDistanceUnit(defaultUnitSystem));
+
+ // Update result
+ result.distanceValue = distance;
+ result.distanceUnit = unitUtils.getDefaultDistanceUnit(defaultUnitSystem);
+ }
+
+ // Return result
+ return result;
+}
+
+/**
+ * Predict race results from a target
+ * @param {Object} input The input race
+ * @param {Object} target The race target
+ * @param {Object} options The race prediction options
+ * @param {String} defaultUnitSystem The default unit system (imperial or metric)
+ * @returns {Object} The result
+ */
+function calculateRaceResults(input, target, options, defaultUnitSystem) {
+ const result = {
+ distanceValue: target.distanceValue,
+ distanceUnit: target.distanceUnit,
+ time: target.time,
+ result: target.result,
+ };
+
+ const d1 = unitUtils.convertDistance(input.distanceValue, input.distanceUnit, 'meters');
+
+ // Add missing value to result
+ if (target.result === 'time') {
+ // Convert target distance into meters
+ const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, 'meters');
+
+ // Get prediction
+ let time;
+ switch (options.model) {
+ default:
+ case 'AverageModel':
+ time = raceUtils.AverageModel.predictTime(d1, input.time, d2,
+ options.riegelExponent);
+ break;
+ case 'PurdyPointsModel':
+ time = raceUtils.PurdyPointsModel.predictTime(d1, input.time, d2);
+ break;
+ case 'VO2MaxModel':
+ time = raceUtils.VO2MaxModel.predictTime(d1, input.time, d2);
+ break;
+ case 'RiegelModel':
+ time = raceUtils.RiegelModel.predictTime(d1, input.time, d2,
+ options.riegelExponent);
+ break;
+ case 'CameronModel':
+ time = raceUtils.CameronModel.predictTime(d1, input.time, d2);
+ break;
+ }
+
+ // Update result
+ result.time = time;
+ } else {
+ // Get prediction
+ let distance;
+ switch (options.model) {
+ default:
+ case 'AverageModel':
+ distance = raceUtils.AverageModel.predictDistance(input.time, d1, target.time,
+ options.riegelExponent);
+ break;
+ case 'PurdyPointsModel':
+ distance = raceUtils.PurdyPointsModel.predictDistance(input.time, d1,
+ target.time);
+ break;
+ case 'VO2MaxModel':
+ distance = raceUtils.VO2MaxModel.predictDistance(input.time, d1, target.time);
+ break;
+ case 'RiegelModel':
+ distance = raceUtils.RiegelModel.predictDistance(input.time, d1, target.time,
+ options.riegelExponent);
+ break;
+ case 'CameronModel':
+ distance = raceUtils.CameronModel.predictDistance(input.time, d1, target.time);
+ break;
+ }
+
+ // Convert output distance into default distance unit
+ distance = unitUtils.convertDistance(distance, 'meters',
+ unitUtils.getDefaultDistanceUnit(defaultUnitSystem));
+
+ // Update result
+ result.distanceValue = distance;
+ result.distanceUnit = unitUtils.getDefaultDistanceUnit(defaultUnitSystem);
+ }
+
+ // Return result
+ return result;
+}
+
+/**
+ * Calculate race statistics from an input race
+ * @param {Object} input The input race
+ * @returns {Object} The race statistics
+ */
+function calculateRaceStats(input) {
+ const d1 = unitUtils.convertDistance(input.distanceValue, input.distanceUnit, 'meters');
+
+ return {
+ purdyPoints: raceUtils.PurdyPointsModel.getPurdyPoints(d1, input.time),
+ vo2Max: raceUtils.VO2MaxModel.getVO2Max(d1, input.time),
+ vo2: raceUtils.VO2MaxModel.getVO2(d1, input.time),
+ vo2MaxPercentage: raceUtils.VO2MaxModel.getVO2Percentage(input.time) * 100,
+ }
+}
+
+export default {
+ calculatePaceResults,
+ calculateRaceResults,
+ calculateRaceStats,
+};
diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue
@@ -24,15 +24,14 @@
</details>
<h2>Equivalent Paces</h2>
- <simple-target-table class="output" :calculate-result="calculatePace"
+ <simple-target-table class="output" :calculate-result="x =>
+ calcUtils.calculatePaceResults(input, x, defaultUnitSystem)"
:targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []"/>
</div>
</template>
<script setup>
-import { computed } from 'vue';
-
-import paceUtils from '@/utils/paces';
+import calcUtils from '@/utils/calculators';
import targetUtils from '@/utils/targets';
import unitUtils from '@/utils/units';
@@ -65,56 +64,6 @@ const selectedTargetSet = useStorage('pace-calculator-target-set', '_pace_target
* The target sets
*/
const targetSets = useStorage('target-sets', targetUtils.defaultTargetSets);
-
-/**
- * The input pace (in seconds per meter)
- */
-const pace = computed(() => {
- const distance = unitUtils.convertDistance(input.value.distanceValue, input.value.distanceUnit,
- 'meters');
- return paceUtils.getPace(distance, input.value.time);
-});
-
-/**
- * Calculate paces from a target
- * @param {Object} target The target
- * @returns {Object} The result
- */
-function calculatePace(target) {
- // Initialize result
- const result = {
- distanceValue: target.distanceValue,
- distanceUnit: target.distanceUnit,
- time: target.time,
- result: target.result,
- };
-
- // Add missing value to result
- if (target.result === 'time') {
- // Convert target distance into meters
- const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, 'meters');
-
- // Calculate time to travel distance at input pace
- const time = paceUtils.getTime(pace.value, d2);
-
- // Update result
- result.time = time;
- } else {
- // Calculate distance traveled in time at input pace
- let distance = paceUtils.getDistance(pace.value, target.time);
-
- // Convert output distance into default distance unit
- distance = unitUtils.convertDistance(distance, 'meters',
- unitUtils.getDefaultDistanceUnit(defaultUnitSystem.value));
-
- // Update result
- result.distanceValue = distance;
- result.distanceUnit = unitUtils.getDefaultDistanceUnit(defaultUnitSystem.value);
- }
-
- // Return result
- return result;
-}
</script>
<style scoped>
diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue
@@ -10,14 +10,15 @@
<h2>Race Statistics</h2>
</summary>
<div>
- Purdy Points: <b>{{ formatUtils.formatNumber(purdyPoints, 0, 1, true) }}</b>
+ Purdy Points: <b>{{ formatUtils.formatNumber(raceStats.purdyPoints, 0, 1, true) }}</b>
</div>
<div>
- V̇O₂: <b>{{ formatUtils.formatNumber(vo2, 0, 1, true) }}</b> ml/kg/min
- (<b>{{ formatUtils.formatNumber(vo2Percentage, 0, 1, true) }}%</b> of max)
+ V̇O₂: <b>{{ formatUtils.formatNumber(raceStats.vo2, 0, 1, true) }}</b> ml/kg/min
+ (<b>{{ formatUtils.formatNumber(raceStats.vo2MaxPercentage, 0, 1, true) }}%</b> of max)
</div>
<div>
- V̇O₂ Max: <b>{{ formatUtils.formatNumber(vo2Max, 0, 1, true) }}</b> ml/kg/min
+ V̇O₂ Max: <b>{{ formatUtils.formatNumber(raceStats.vo2Max, 0, 1, true) }}</b>
+ ml/kg/min
</div>
</details>
@@ -41,16 +42,17 @@
</details>
<h2>Equivalent Race Results</h2>
- <simple-target-table class="output" :calculate-result="predictResult" :default-unit-system="defaultUnitSystem"
- :targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []" show-pace/>
+ <simple-target-table class="output" :default-unit-system="defaultUnitSystem" show-pace
+ :calculate-result="x => calcUtils.calculateRaceResults(input, x, options, defaultUnitSystem)"
+ :targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []"/>
</div>
</template>
<script setup>
import { computed } from 'vue';
+import calcUtils from '@/utils/calculators';
import formatUtils from '@/utils/format';
-import raceUtils from '@/utils/races';
import targetUtils from '@/utils/targets';
import unitUtils from '@/utils/units';
@@ -76,7 +78,7 @@ const input = useStorage('race-calculator-input', {
const defaultUnitSystem = useStorage('default-unit-system', unitUtils.detectDefaultUnitSystem());
/**
-* The race prediction model
+* The race prediction options
*/
const options = useStorage('race-calculator-options', {
model: 'AverageModel',
@@ -94,125 +96,9 @@ const selectedTargetSet = useStorage('race-calculator-target-set', '_race_target
let targetSets = useStorage('target-sets', targetUtils.defaultTargetSets);
/**
- * Predict race results from a target
- * @param {Object} target The target
- * @returns {Object} The result
+ * The statistics for the current input race
*/
-function predictResult(target) {
- // Initialize result
- const result = {
- distanceValue: target.distanceValue,
- distanceUnit: target.distanceUnit,
- time: target.time,
- result: target.result,
- };
-
- // Add missing value to result
- if (target.result === 'time') {
- // Convert target distance into meters
- const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, 'meters');
-
- // Get prediction
- let time;
- switch (options.value.model) {
- default:
- case 'AverageModel':
- time = raceUtils.AverageModel.predictTime(d1.value, input.value.time, d2,
- options.value.riegelExponent);
- break;
- case 'PurdyPointsModel':
- time = raceUtils.PurdyPointsModel.predictTime(d1.value, input.value.time, d2);
- break;
- case 'VO2MaxModel':
- time = raceUtils.VO2MaxModel.predictTime(d1.value, input.value.time, d2);
- break;
- case 'RiegelModel':
- time = raceUtils.RiegelModel.predictTime(d1.value, input.value.time, d2,
- options.value.riegelExponent);
- break;
- case 'CameronModel':
- time = raceUtils.CameronModel.predictTime(d1.value, input.value.time, d2);
- break;
- }
-
- // Update result
- result.time = time;
- } else {
- // Get prediction
- let distance;
- switch (options.value.model) {
- default:
- case 'AverageModel':
- distance = raceUtils.AverageModel.predictDistance(input.value.time, d1.value, target.time,
- options.value.riegelExponent);
- break;
- case 'PurdyPointsModel':
- distance = raceUtils.PurdyPointsModel.predictDistance(input.value.time, d1.value,
- target.time);
- break;
- case 'VO2MaxModel':
- distance = raceUtils.VO2MaxModel.predictDistance(input.value.time, d1.value, target.time);
- break;
- case 'RiegelModel':
- distance = raceUtils.RiegelModel.predictDistance(input.value.time, d1.value, target.time,
- options.value.riegelExponent);
- break;
- case 'CameronModel':
- distance = raceUtils.CameronModel.predictDistance(input.value.time, d1.value, target.time);
- break;
- }
-
- // Convert output distance into default distance unit
- distance = unitUtils.convertDistance(distance, 'meters',
- unitUtils.getDefaultDistanceUnit(defaultUnitSystem.value));
-
- // Update result
- result.distanceValue = distance;
- result.distanceUnit = unitUtils.getDefaultDistanceUnit(defaultUnitSystem.value);
- }
-
- // Return result
- return result;
-}
-
-/**
- * The input distance in meters
- */
-const d1 = computed(() => {
- return unitUtils.convertDistance(input.value.distanceValue, input.value.distanceUnit, 'meters');
-});
-
-/**
- * The Purdy Points for the input race
- */
-const purdyPoints = computed(() => {
- const result = raceUtils.PurdyPointsModel.getPurdyPoints(d1.value, input.value.time);
- return result;
-});
-
-/**
- * The VO2 Max calculated from the input race
- */
-const vo2Max = computed(() => {
- const result = raceUtils.VO2MaxModel.getVO2Max(d1.value, input.value.time);
- return result;
-});
-
-/**
- * The VO2 calculated from the input race
- */
-const vo2 = computed(() => {
- const result = raceUtils.VO2MaxModel.getVO2(d1.value, input.value.time);
- return result;
-});
-
-/**
- * The percentage of VO2 Max calculated from the input race
- */
-const vo2Percentage = computed(() => {
- const result = raceUtils.VO2MaxModel.getVO2Percentage(input.value.time) * 100;
- return result;
-});
+const raceStats = computed(() => calcUtils.calculateRaceStats(input.value));
</script>
<style scoped>
diff --git a/tests/unit/utils/calculators.spec.js b/tests/unit/utils/calculators.spec.js
@@ -0,0 +1,135 @@
+import { test, expect } from 'vitest';
+import calculatorUtils from '@/utils/calculators';
+
+test('should correctly calculate pace times', () => {
+ const input = {
+ distanceValue: 1,
+ distanceUnit: 'kilometers',
+ time: 100,
+ };
+ const target = {
+ distanceValue: 20,
+ distanceUnit: 'meters',
+ result: 'time',
+ };
+
+ const result = calculatorUtils.calculatePaceResults(input, target, {});
+
+ expect(result).to.deep.equal({
+ distanceValue: 20,
+ distanceUnit: 'meters',
+ time: 2,
+ result: 'time',
+ });
+});
+
+test('should correctly calculate pace distances according to default units setting', () => {
+ const input = {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 1200,
+ };
+ const target = {
+ time: 600,
+ result: 'distance',
+ };
+
+ const result1 = calculatorUtils.calculatePaceResults(input, target, 'metric');
+ const result2 = calculatorUtils.calculatePaceResults(input, target, 'imperial');
+
+ expect(result1.distanceValue).to.be.closeTo(1.609, 0.001);
+ expect(result1.distanceUnit).to.equal('kilometers');
+ expect(result2.distanceValue).to.be.closeTo(1.000, 0.001);
+ expect(result2.distanceUnit).to.equal('miles');
+});
+
+test('should correctly predict race times', () => {
+ const input = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ };
+ const target = {
+ distanceValue: 10,
+ distanceUnit: 'kilometers',
+ result: 'time',
+ };
+ const options = {
+ model: 'average',
+ riegelExponent: 1.06,
+ }
+
+ const result = calculatorUtils.calculateRaceResults(input, target, options, 'imperial');
+
+ expect(result.time).to.be.closeTo(2495, 1);
+ expect(result.distanceValue).to.equal(10);
+ expect(result.distanceUnit).to.equal('kilometers');
+ expect(result.result).to.equal('time');
+});
+
+test('should correctly calculate race distances according to default units setting', () => {
+ const input = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ };
+ const target = {
+ time: 2495,
+ result: 'distance',
+ };
+ const options = {
+ model: 'average',
+ riegelExponent: 1.06,
+ }
+
+ const result1 = calculatorUtils.calculateRaceResults(input, target, options, 'metric');
+ const result2 = calculatorUtils.calculateRaceResults(input, target, options, 'imperial');
+
+ expect(result1.distanceValue).to.be.closeTo(10, 0.01);
+ expect(result1.distanceUnit).to.equal('kilometers');
+ expect(result1.time).to.equal(2495);
+ expect(result1.result).to.equal('distance');
+ expect(result2.distanceValue).to.be.closeTo(6.214, 0.01);
+ expect(result2.distanceUnit).to.equal('miles');
+ expect(result2.time).to.equal(2495);
+ expect(result2.result).to.equal('distance');
+});
+
+test('should correctly predict race times according to race options', () => {
+ const input = {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ };
+ const target = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ result: 'time',
+ };
+ const options = {
+ model: 'RiegelModel',
+ riegelExponent: 1.12,
+ }
+
+ const result = calculatorUtils.calculateRaceResults(input, target, options, 'imperial');
+
+ expect(result.time).to.be.closeTo(1031, 1);
+ expect(result.distanceValue).to.equal(5);
+ expect(result.distanceUnit).to.equal('kilometers');
+ expect(result.result).to.equal('time');
+});
+
+test('should correctly calculate race statistics', () => {
+ const input = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ };
+
+ 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);
+});