running-tools

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

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:
Msrc/utils/calculators.js | 57+++++++--------------------------------------------------
Msrc/utils/races.js | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mtests/unit/utils/calculators.spec.js | 4++--
Mtests/unit/utils/races.spec.js | 174++++++++++++++++++++++++++++++++++++++-----------------------------------------
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); }); });