commit 621da008c90a490b8c2e6e1fc163438b329758b3
parent 1ef20d6cdfc1988f50912ceff2da2c710473d884
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sat, 8 Jun 2024 17:07:54 -0700
Refactor race util methods
Diffstat:
4 files changed, 161 insertions(+), 162 deletions(-)
diff --git a/src/utils/calculators.js b/src/utils/calculators.js
@@ -103,54 +103,11 @@ function calculateRaceResults(input, target, options, defaultUnitSystem) {
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;
+ result.time = raceUtils.predictTime(d1, input.time, d2, options.model, options.riegelExponent);
} 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;
- }
+ let distance = raceUtils.predictDistance(input.time, d1, target.time, options.model,
+ options.riegelExponent);
// Convert output distance into default distance unit
distance = unitUtils.convertDistance(distance, 'meters',
@@ -174,10 +131,10 @@ 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,
+ purdyPoints: raceUtils.getPurdyPoints(d1, input.time),
+ vo2Max: raceUtils.getVO2Max(d1, input.time),
+ vo2: raceUtils.getVO2(d1, input.time),
+ vo2MaxPercentage: raceUtils.getVO2Percentage(input.time) * 100,
}
}
diff --git a/src/utils/races.js b/src/utils/races.js
@@ -84,7 +84,7 @@ const PurdyPointsModel = {
*/
getPurdyPoints(d, t) {
// Get variables
- const variables = this.getVariables(d);
+ const variables = PurdyPointsModel.getVariables(d);
// Calculate Purdy Points
const points = variables.a * ((variables.twsec / t) - variables.b);
@@ -102,10 +102,10 @@ const PurdyPointsModel = {
*/
predictTime(d1, t1, d2) {
// Calculate Purdy Points for distance 1
- const points = this.getPurdyPoints(d1, t1);
+ const points = PurdyPointsModel.getPurdyPoints(d1, t1);
// Calculate time for distance 2
- const variables = this.getVariables(d2);
+ const variables = PurdyPointsModel.getVariables(d2);
const seconds = (variables.a * variables.twsec) / (points + (variables.a * variables.b));
// Return predicted time
@@ -161,9 +161,9 @@ const PurdyPointsModel = {
// Initialize estimate
let estimate = (d1 * t2) / t1;
- // Refine estimate
- const method = (x) => this.predictTime(d1, t1, x);
- const derivative = (x) => this.derivative(d1, t1, x) / 500; // Derivative on its own is too slow
+ // 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
@@ -208,7 +208,7 @@ const VO2MaxModel = {
* @returns {Number} The runner's VO2 max
*/
getVO2Max(d, t) {
- const result = this.getVO2(d, t) / this.getVO2Percentage(t);
+ const result = VO2MaxModel.getVO2(d, t) / VO2MaxModel.getVO2Percentage(t);
return result;
},
@@ -237,14 +237,14 @@ const VO2MaxModel = {
*/
predictTime(d1, t1, d2) {
// Calculate input VO2 max
- const inputVO2 = this.getVO2Max(d1, t1);
+ const inputVO2 = VO2MaxModel.getVO2Max(d1, t1);
// Initialize estimate
let estimate = (t1 * d2) / d1;
// Refine estimate
- const method = (x) => this.getVO2Max(d2, x);
- const derivative = (x) => this.VO2MaxTimeDerivative(d2, x);
+ const method = (x) => VO2MaxModel.getVO2Max(d2, x);
+ const derivative = (x) => VO2MaxModel.VO2MaxTimeDerivative(d2, x);
estimate = NewtonsMethod(estimate, inputVO2, method, derivative, 0.0001);
// Return estimate
@@ -272,14 +272,14 @@ const VO2MaxModel = {
*/
predictDistance(t1, d1, t2) {
// Calculate input VO2 max
- const inputVO2 = this.getVO2Max(d1, t1);
+ const inputVO2 = VO2MaxModel.getVO2Max(d1, t1);
// Initialize estimate
let estimate = (d1 * t2) / t1;
// Refine estimate
- const method = (x) => this.getVO2Max(x, t2);
- const derivative = (x) => this.VO2MaxDistanceDerivative(x, t2);
+ const method = (x) => VO2MaxModel.getVO2Max(x, t2);
+ const derivative = (x) => VO2MaxModel.VO2MaxDistanceDerivative(x, t2);
estimate = NewtonsMethod(estimate, inputVO2, method, derivative, 0.0001);
// Return estimate
@@ -332,8 +332,8 @@ const CameronModel = {
let estimate = (d1 * t2) / t1;
// Refine estimate
- const method = (x) => this.predictTime(d1, t1, x);
- const derivative = (x) => this.derivative(d1, t1, x);
+ 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
@@ -408,10 +408,58 @@ const AverageModel = {
},
};
+/**
+ * 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
+ */
+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
+ */
+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 default {
- PurdyPointsModel,
- VO2MaxModel,
- CameronModel,
- RiegelModel,
- AverageModel,
+ predictTime,
+ predictDistance,
+ getPurdyPoints: PurdyPointsModel.getPurdyPoints,
+ getVO2: VO2MaxModel.getVO2,
+ getVO2Percentage: VO2MaxModel.getVO2Percentage,
+ getVO2Max: VO2MaxModel.getVO2Max,
};
diff --git a/tests/unit/utils/calculators.spec.js b/tests/unit/utils/calculators.spec.js
@@ -63,7 +63,7 @@ test('should correctly predict race times', () => {
result: 'time',
};
const options = {
- model: 'average',
+ model: 'AverageModel',
riegelExponent: 1.06,
}
@@ -87,7 +87,7 @@ test('should correctly calculate race distances according to default units setti
result: 'distance',
};
const options = {
- model: 'average',
+ model: 'AverageModel',
riegelExponent: 1.06,
}
diff --git a/tests/unit/utils/races.spec.js b/tests/unit/utils/races.spec.js
@@ -1,172 +1,166 @@
import { describe, test, expect } from 'vitest';
import raceUtils from '@/utils/races';
-describe('PurdyPointsModel', () => {
- describe('getPurdyPoints method', () => {
- test('Result should be approximately correct', () => {
- const result = raceUtils.PurdyPointsModel.getPurdyPoints(5000, 1200);
- expect(result).to.be.closeTo(454, 1);
+describe('predictTime method', () => {
+ describe('PredictTime method', () => {
+ test('Average Model', () => {
+ const riegel = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
+ const cameron = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
+ const purdyPoints = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
+ const vo2Max = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
+ const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
+
+ const result = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
+ expect(result).to.equal(expected);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.predictTime(5000, 1200, 5000, 'AverageModel');
+ expect(result).to.be.closeTo(1200, 0.001);
});
});
- describe('PredictTime method', () => {
+ describe('Purdy Points Model', () => {
test('Predictions should be approximately correct', () => {
- const result = raceUtils.PurdyPointsModel.predictTime(5000, 1200, 10000);
+ const result = raceUtils.predictTime(5000, 1200, 10000, 'PurdyPointsModel');
expect(result).to.be.closeTo(2490, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.PurdyPointsModel.predictTime(5000, 1200, 5000);
+ const result = raceUtils.predictTime(5000, 1200, 5000, 'PurdyPointsModel');
expect(result).to.be.closeTo(1200, 0.001);
});
});
- describe('PredictDistance method', () => {
+ describe('VO2 Max Model', () => {
test('Predictions should be approximately correct', () => {
- const result = raceUtils.PurdyPointsModel.predictDistance(1200, 5000, 2490);
- expect(result).to.be.closeTo(10000, 10);
+ const result = raceUtils.predictTime(5000, 1200, 10000, 'VO2MaxModel');
+ expect(result).to.be.closeTo(2488, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.PurdyPointsModel.predictDistance(1200, 5000, 1200);
- expect(result).to.be.closeTo(5000, 0.001);
- });
- });
-});
-
-describe('VO2MaxModel', () => {
- describe('getVO2 method', () => {
- test('Result should be approximately correct', () => {
- const result = raceUtils.VO2MaxModel.getVO2(5000, 1200);
- expect(result).to.be.closeTo(47.4, 0.1);
+ const result = raceUtils.predictTime(5000, 1200, 5000, 'VO2MaxModel');
+ expect(result).to.be.closeTo(1200, 0.001);
});
});
- describe('getVO2Percentage method', () => {
- test('Result should be approximately correct', () => {
- const result = raceUtils.VO2MaxModel.getVO2Percentage(660);
- expect(result).to.be.closeTo(1, 0.001);
+ describe('Cameron Model', () => {
+ test('Predictions should be approximately correct', () => {
+ const result = raceUtils.predictTime(5000, 1200, 10000, 'CameronModel');
+ expect(result).to.be.closeTo(2500, 1);
});
- });
- describe('getVO2Max method', () => {
- test('Result should be approximately correct', () => {
- const result = raceUtils.VO2MaxModel.getVO2Max(5000, 1200);
- expect(result).to.be.closeTo(49.8, 0.1);
+ test('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.predictTime(5000, 1200, 5000, 'CameronModel');
+ expect(result).to.be.closeTo(1200, 0.001);
});
});
- describe('PredictTime method', () => {
+ describe('Riegel Model', () => {
test('Predictions should be approximately correct', () => {
- const result = raceUtils.VO2MaxModel.predictTime(5000, 1200, 10000);
- expect(result).to.be.closeTo(2488, 1);
+ const result = raceUtils.predictTime(5000, 1200, 10000, 'RiegelModel');
+ expect(result).to.be.closeTo(2502, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.VO2MaxModel.predictTime(5000, 1200, 5000);
+ const result = raceUtils.predictTime(5000, 1200, 5000, 'RiegelModel');
expect(result).to.be.closeTo(1200, 0.001);
});
});
+});
- describe('PredictDistance method', () => {
- test('Predictions should be approximately correct', () => {
- const result = raceUtils.VO2MaxModel.predictDistance(1200, 5000, 2488);
+describe('predictDistance method', () => {
+ describe('Average Model', () => {
+ test('Predictions should be correct', () => {
+ const riegel = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
+ const cameron = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
+ const purdyPoints = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
+ const vo2Max = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
+ const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
+
+ const result = raceUtils.predictDistance(1200, 5000, expected);
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.VO2MaxModel.predictDistance(1200, 5000, 1200);
+ const result = raceUtils.predictDistance(1200, 5000, 1200, 'AverageModel');
expect(result).to.be.closeTo(5000, 0.001);
});
});
-});
-describe('CameronModel', () => {
- describe('PredictTime method', () => {
+ describe('Purdy Points Model', () => {
test('Predictions should be approximately correct', () => {
- const result = raceUtils.CameronModel.predictTime(5000, 1200, 10000);
- expect(result).to.be.closeTo(2500, 1);
+ const result = raceUtils.predictDistance(1200, 5000, 2490, 'PurdyPointsModel');
+ expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.CameronModel.predictTime(5000, 1200, 5000);
- expect(result).to.be.closeTo(1200, 0.001);
+ const result = raceUtils.predictDistance(1200, 5000, 1200, 'PurdyPointsModel');
+ expect(result).to.be.closeTo(5000, 0.001);
});
});
- describe('PredictDistance method', () => {
+ describe('VO2 Max Model', () => {
test('Predictions should be approximately correct', () => {
- const result = raceUtils.CameronModel.predictDistance(1200, 5000, 2500);
+ const result = raceUtils.predictDistance(1200, 5000, 2488, 'VO2MaxModel');
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.CameronModel.predictDistance(1200, 5000, 1200);
+ const result = raceUtils.predictDistance(1200, 5000, 1200, 'VO2MaxModel');
expect(result).to.be.closeTo(5000, 0.001);
});
});
-});
-describe('RiegelModel', () => {
- describe('PredictTime method', () => {
+ describe('Cameron Model', () => {
test('Predictions should be approximately correct', () => {
- const result = raceUtils.RiegelModel.predictTime(5000, 1200, 10000);
- expect(result).to.be.closeTo(2502, 1);
+ const result = raceUtils.predictDistance(1200, 5000, 2500, 'CameronModel');
+ expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.RiegelModel.predictTime(5000, 1200, 5000);
- expect(result).to.be.closeTo(1200, 0.001);
+ const result = raceUtils.predictDistance(1200, 5000, 1200, 'CameronModel');
+ expect(result).to.be.closeTo(5000, 0.001);
});
});
- describe('PredictDistance method', () => {
+ describe('Riegel Model', () => {
test('Predictions should be approximately correct', () => {
- const result = raceUtils.RiegelModel.predictDistance(1200, 5000, 2502);
+ const result = raceUtils.predictDistance(1200, 5000, 2502, 'RiegelModel');
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.RiegelModel.predictDistance(1200, 5000, 1200);
+ const result = raceUtils.predictDistance(1200, 5000, 1200, 'RiegelModel');
expect(result).to.be.closeTo(5000, 0.001);
});
});
});
-describe('AverageModel', () => {
- describe('PredictTime method', () => {
- test('Predictions should be correct', () => {
- const riegel = raceUtils.RiegelModel.predictTime(5000, 1200, 10000);
- const cameron = raceUtils.CameronModel.predictTime(5000, 1200, 10000);
- const purdyPoints = raceUtils.PurdyPointsModel.predictTime(5000, 1200, 10000);
- const vo2Max = raceUtils.VO2MaxModel.predictTime(5000, 1200, 10000);
- const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
-
- const result = raceUtils.AverageModel.predictTime(5000, 1200, 10000);
- expect(result).to.equal(expected);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.AverageModel.predictTime(5000, 1200, 5000);
- expect(result).to.be.closeTo(1200, 0.001);
- });
+describe('getVO2 method', () => {
+ test('Result should be approximately correct', () => {
+ const result = raceUtils.getVO2(5000, 1200);
+ expect(result).to.be.closeTo(47.4, 0.1);
});
+});
- describe('PredictDistance method', () => {
- test('Predictions should be correct', () => {
- const riegel = raceUtils.RiegelModel.predictTime(5000, 1200, 10000);
- const cameron = raceUtils.CameronModel.predictTime(5000, 1200, 10000);
- const purdyPoints = raceUtils.PurdyPointsModel.predictTime(5000, 1200, 10000);
- const vo2Max = raceUtils.VO2MaxModel.predictTime(5000, 1200, 10000);
- const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
+describe('getVO2Percentage method', () => {
+ test('Result should be approximately correct', () => {
+ const result = raceUtils.getVO2Percentage(660);
+ expect(result).to.be.closeTo(1, 0.001);
+ });
+});
- const result = raceUtils.AverageModel.predictDistance(1200, 5000, expected);
- expect(result).to.be.closeTo(10000, 10);
- });
+describe('getVO2Max method', () => {
+ test('Result should be approximately correct', () => {
+ const result = raceUtils.getVO2Max(5000, 1200);
+ expect(result).to.be.closeTo(49.8, 0.1);
+ });
+});
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.AverageModel.predictDistance(1200, 5000, 1200);
- expect(result).to.be.closeTo(5000, 0.001);
- });
+describe('getPurdyPoints method', () => {
+ test('Result should be approximately correct', () => {
+ const result = raceUtils.getPurdyPoints(5000, 1200);
+ expect(result).to.be.closeTo(454, 1);
});
});