commit 7edfe58ae307c4120d55f40b2105d319e12e42ee
parent 827f219882e570c534cfde9028cab19c2b8e7e7f
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sun, 5 Sep 2021 08:30:44 -0700
Merge pull request #2 from ashermorgan/feature/time-based-targets
Add time based targets
Diffstat:
10 files changed, 990 insertions(+), 580 deletions(-)
diff --git a/src/components/TargetTable.vue b/src/components/TargetTable.vue
@@ -0,0 +1,312 @@
+<template>
+ <div class="time-table">
+ <table class="results" v-show="!inEditMode">
+ <thead>
+ <tr>
+ <th colspan="2">Distance</th>
+
+ <th>Time</th>
+
+ <th>
+ <button class="icon" title="Edit Targets" @click="inEditMode=true" v-blur>
+ <img alt="" src="@/assets/edit.svg">
+ </button>
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr v-for="(item, index) in results" :key="index">
+ <td :class="item.result === 'distance' ? 'result' : ''">
+ {{ item.distanceValue.toFixed(2) }}
+ {{ distanceSymbols[item.distanceUnit] }}
+ </td>
+
+ <td>in</td>
+
+ <td colspan="2" :class="item.result === 'time' ? 'result' : ''">
+ {{ formatDuration(item.time, 0, 2) }}
+ </td>
+ </tr>
+
+ <tr v-if="results.length === 0" class="empty-message">
+ <td colspan="4">
+ There aren't any targets yet,<br>
+ click
+ <img alt="Edit Targets" src="@/assets/edit.svg">
+ to edit the list of targets
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table class="targets" v-show="inEditMode">
+ <thead>
+ <tr>
+ <th>Edit Targets</th>
+
+ <th>
+ <button class="icon" title="Reset Targets" @click="resetTargets" v-blur>
+ <img alt="" src="@/assets/rotate-ccw.svg">
+ </button>
+ <button class="icon" title="Close" @click="inEditMode=false" v-blur>
+ <img alt="" src="@/assets/x.svg">
+ </button>
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr v-for="(item, index) in targets" :key="index">
+ <td v-if="item.result === 'time'">
+ <decimal-input v-model="item.distanceValue" aria-label="Distance Value"
+ :min="0" :digits="2"/>
+ <select v-model="item.distanceUnit" aria-label="Distance Unit">
+ <option v-for="(value, key) in distanceUnits" :key="key" :value="key">
+ {{ value }}
+ </option>
+ </select>
+ </td>
+
+ <td v-else>
+ <time-input v-model="item.time" aria-label="Time"/>
+ </td>
+
+ <td>
+ <button class="icon" title="Remove Target" @click="targets.splice(index, 1)" v-blur>
+ <img alt="" src="@/assets/trash-2.svg">
+ </button>
+ </td>
+ </tr>
+
+ <tr v-if="targets.length === 0" class="empty-message">
+ <td colspan="2">
+ There aren't any targets yet
+ </td>
+ </tr>
+ </tbody>
+
+ <tfoot>
+ <tr>
+ <td colspan="2">
+ <button title="Add Distance Target" @click="targets.push({ result: 'time',
+ distanceValue: 1, distanceUnit: 'miles' })" v-blur>
+ Add distance target
+ </button>
+ <button title="Add Time Target" @click="targets.push({ result: 'distance',
+ time: 600 })" v-blur>
+ Add time target
+ </button>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+</template>
+
+<script>
+import unitUtils from '@/utils/units';
+import storage from '@/utils/localStorage';
+
+import DecimalInput from '@/components/DecimalInput.vue';
+import TimeInput from '@/components/TimeInput.vue';
+
+import blur from '@/directives/blur';
+
+export default {
+ name: 'TimeTable',
+
+ components: {
+ DecimalInput,
+ TimeInput,
+ },
+
+ directives: {
+ blur,
+ },
+
+ props: {
+ /**
+ * The method that generates the time table rows
+ */
+ calculateResult: {
+ type: Function,
+ required: true,
+ },
+
+ /**
+ * The default time table targets
+ */
+ defaultTargets: {
+ type: Array,
+ default: () => [],
+ },
+
+ /**
+ * The localStorage key for the list of targets
+ */
+ storageKey: {
+ type: String,
+ default: null,
+ },
+ },
+
+ data() {
+ return {
+ /**
+ * The names of the distance units
+ */
+ distanceUnits: unitUtils.DISTANCE_UNIT_NAMES,
+
+ /**
+ * The symbols of the distance units
+ */
+ distanceSymbols: unitUtils.DISTANCE_UNIT_SYMBOLS,
+
+ /**
+ * The formatDuration method
+ */
+ formatDuration: unitUtils.formatDuration,
+
+ /**
+ * Whether the table is in edit mode
+ */
+ inEditMode: false,
+
+ /**
+ * The time table targets
+ */
+ targets: storage.get(this.storageKey, this.defaultTargets),
+ };
+ },
+
+ computed: {
+ /**
+ * The time table results
+ */
+ results() {
+ // Calculate results
+ const result = [];
+ this.targets.forEach((row) => {
+ // Add result
+ result.push(this.calculateResult(row));
+ });
+
+ // Sort results by time
+ result.sort((a, b) => a.time - b.time);
+
+ // Return results
+ return result;
+ },
+ },
+
+ watch: {
+ /**
+ * Sort targets
+ */
+ inEditMode() {
+ this.sortTargets();
+ },
+
+ /**
+ * Save targets
+ */
+ targets: {
+ handler(newValue) {
+ if (this.storageKey !== null) {
+ storage.set(this.storageKey, newValue);
+ }
+ },
+ deep: true,
+ },
+ },
+
+ methods: {
+ /**
+ * Restore the default targets
+ */
+ resetTargets() {
+ // Clone default targets array
+ this.targets = JSON.parse(JSON.stringify(this.defaultTargets));
+
+ // Sort targets
+ this.sortTargets();
+ },
+
+ /**
+ * Sort the targets by distance
+ */
+ sortTargets() {
+ this.targets = [
+ ...this.targets.filter((item) => item.result === 'time')
+ .sort((a, b) => unitUtils.convertDistance(a.distanceValue, a.distanceUnit, 'meters')
+ - unitUtils.convertDistance(b.distanceValue, b.distanceUnit, 'meters')),
+
+ ...this.targets.filter((item) => item.result === 'distance')
+ .sort((a, b) => a.time - b.time),
+ ];
+ },
+ },
+
+ /**
+ * Close edit targets table
+ */
+ deactivated() {
+ this.inEditMode = false;
+ },
+};
+</script>
+
+<style scoped>
+/* time table */
+.results th:last-child {
+ text-align: right;
+}
+.results .result {
+ font-weight: bold;
+}
+
+/* edit targets table */
+.targets th:last-child, .targets td:last-child {
+ text-align: right;
+}
+.targets td select {
+ margin-left: 0.2em;
+ width: 8em;
+}
+.targets tfoot td {
+ text-align: center !important;
+ padding: 0.5em 0.2em;
+}
+.targets tfoot button {
+ margin: 0.5em;
+}
+
+/* general table styles */
+table {
+ border-collapse: collapse;
+ width: 100%;
+ text-align: left;
+}
+table th, table td {
+ padding: 0.2em;
+}
+table button.icon {
+ height: 2em;
+ width: 2em;
+}
+
+/* empty table message */
+.empty-message td {
+ text-align: center !important;
+}
+.empty-message img {
+ height: 1em;
+ width: 1em;
+}
+@media (prefers-color-scheme: dark) {
+ .empty-message img {
+ filter: invert(90%);
+ }
+}
+</style>
diff --git a/src/components/TimeTable.vue b/src/components/TimeTable.vue
@@ -1,292 +0,0 @@
-<template>
- <div class="time-table">
- <table class="results" v-show="!inEditMode">
- <thead>
- <tr>
- <th colspan="2">Distance</th>
-
- <th>Time</th>
-
- <th>
- <button class="icon" title="Edit Targets" @click="inEditMode=true" v-blur>
- <img alt="" src="@/assets/edit.svg">
- </button>
- </th>
- </tr>
- </thead>
-
- <tbody>
- <tr v-for="(item, index) in results" :key="index">
- <td>
- {{ item.distanceValue.toFixed(2) }}
- {{ distanceSymbols[item.distanceUnit] }}
- </td>
-
- <td>in</td>
-
- <td colspan="2">
- {{ formatDuration(item.time, 0, 2) }}
- </td>
- </tr>
-
- <tr v-if="results.length === 0" class="empty-message">
- <td colspan="4">
- There aren't any targets,<br>
- click
- <img alt="Edit Targets" src="@/assets/edit.svg">
- to add one
- </td>
- </tr>
- </tbody>
- </table>
-
- <table class="targets" v-show="inEditMode">
- <thead>
- <tr>
- <th>Edit Targets</th>
-
- <th>
- <button class="icon" title="Reset Targets" @click="resetTargets" v-blur>
- <img alt="" src="@/assets/rotate-ccw.svg">
- </button>
- <button class="icon" title="Close" @click="inEditMode=false" v-blur>
- <img alt="" src="@/assets/x.svg">
- </button>
- </th>
- </tr>
- </thead>
-
- <tbody>
- <tr v-for="(item, index) in targets" :key="index">
- <td>
- <decimal-input v-model="item.distanceValue" aria-label="Distance Value"
- :min="0" :digits="2"/>
- <select v-model="item.distanceUnit" aria-label="Distance Unit">
- <option v-for="(value, key) in distanceUnits" :key="key" :value="key">
- {{ value }}
- </option>
- </select>
- </td>
-
- <td>
- <button class="icon" title="Remove Target" @click="targets.splice(index, 1)" v-blur>
- <img alt="" src="@/assets/trash-2.svg">
- </button>
- </td>
- </tr>
-
- <tr v-if="targets.length === 0" class="empty-message">
- <td colspan="2">
- There aren't any targets,<br>
- click
- <img alt="Add Target" src="@/assets/plus-circle.svg">
- to add one
- </td>
- </tr>
- </tbody>
-
- <tfoot>
- <tr>
- <td colspan="2">
- <button class="icon" title="Add Target" @click="targets.push({distanceValue: 1,
- distanceUnit: 'miles'})" v-blur>
- <img alt="" src="@/assets/plus-circle.svg">
- </button>
- </td>
- </tr>
- </tfoot>
- </table>
- </div>
-</template>
-
-<script>
-import unitUtils from '@/utils/units';
-import storage from '@/utils/localStorage';
-
-import DecimalInput from '@/components/DecimalInput.vue';
-
-import blur from '@/directives/blur';
-
-export default {
- name: 'TimeTable',
-
- components: {
- DecimalInput,
- },
-
- directives: {
- blur,
- },
-
- props: {
- /**
- * The method that generates the time table rows
- */
- calculateResult: {
- type: Function,
- required: true,
- },
-
- /**
- * The default time table targets
- */
- defaultTargets: {
- type: Array,
- default: () => [],
- },
-
- /**
- * The localStorage key for the list of targets
- */
- storageKey: {
- type: String,
- default: null,
- },
- },
-
- data() {
- return {
- /**
- * The names of the distance units
- */
- distanceUnits: unitUtils.DISTANCE_UNIT_NAMES,
-
- /**
- * The symbols of the distance units
- */
- distanceSymbols: unitUtils.DISTANCE_UNIT_SYMBOLS,
-
- /**
- * The formatDuration method
- */
- formatDuration: unitUtils.formatDuration,
-
- /**
- * Whether the table is in edit mode
- */
- inEditMode: false,
-
- /**
- * The time table targets
- */
- targets: storage.get(this.storageKey, this.defaultTargets),
- };
- },
-
- computed: {
- /**
- * The time table results
- */
- results() {
- // Calculate results
- const result = [];
- this.targets.forEach((row) => {
- // Add result
- result.push(this.calculateResult(row));
- });
-
- // Sort results by time
- result.sort((a, b) => a.time - b.time);
-
- // Return results
- return result;
- },
- },
-
- watch: {
- /**
- * Sort targets
- */
- inEditMode() {
- this.sortTargets();
- },
-
- /**
- * Save targets
- */
- targets: {
- handler(newValue) {
- if (this.storageKey !== null) {
- storage.set(this.storageKey, newValue);
- }
- },
- deep: true,
- },
- },
-
- methods: {
- /**
- * Restore the default targets
- */
- resetTargets() {
- // Clone default targets array
- this.targets = JSON.parse(JSON.stringify(this.defaultTargets));
-
- // Sort targets
- this.sortTargets();
- },
-
- /**
- * Sort the targets by distance
- */
- sortTargets() {
- this.targets.sort((a, b) => unitUtils.convertDistance(a.distanceValue, a.distanceUnit,
- 'meters') - unitUtils.convertDistance(b.distanceValue, b.distanceUnit, 'meters'));
- },
- },
-
- /**
- * Close edit targets table
- */
- deactivated() {
- this.inEditMode = false;
- },
-};
-</script>
-
-<style scoped>
-/* time table */
-.results th:last-child {
- text-align: right;
-}
-
-/* edit targets table */
-.targets th:last-child, .targets td:last-child {
- text-align: right;
-}
-.targets td select {
- margin-left: 0.2em;
-}
-.targets tfoot td {
- text-align: center !important;
- padding: 0.5em 0.2em;
-}
-
-/* general table styles */
-table {
- border-collapse: collapse;
- width: 100%;
- text-align: left;
-}
-table th, table td {
- padding: 0.2em;
-}
-table button.icon {
- height: 2em;
- width: 2em;
-}
-
-/* empty table message */
-.empty-message td {
- text-align: center !important;
-}
-.empty-message img {
- height: 1em;
- width: 1em;
-}
-@media (prefers-color-scheme: dark) {
- .empty-message img {
- filter: invert(90%);
- }
-}
-</style>
diff --git a/src/utils/races.js b/src/utils/races.js
@@ -1,159 +1,362 @@
/**
- * Predict a race time using the Purdy Points Model
- * https://www.cs.uml.edu/~phoffman/xcinfo3.html
- * @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
- * @returns {Number} The predicted time for the output race in seconds
+ * Estimate the point at which a function returns a target value using Newton's Method
+ * @param {Number} initialEstimate The initial estimate
+ * @param {Number} target The target function output
+ * @param {Function} method The function
+ * @param {Function} derivative The function derivative
+ * @param {Number} precision The acceptable precision
+ * @param {Number} iterations The maximum number of iterations
+ * @returns {Number} The refined estimate
*/
-function PurdyPointsModel(d1, t1, d2) {
- // Declare constants
- const c1 = 11.15895;
- const c2 = 4.304605;
- const c3 = 0.5234627;
- const c4 = 4.031560;
- const c5 = 2.316157;
- const r1 = 3.796158e-2;
- const r2 = 1.646772e-3;
- const r3 = 4.107670e-4;
- const r4 = 7.068099e-6;
- const r5 = 5.220990e-9;
-
- // Calculate world record velocity from running curve
- const v1 = (-c1 * Math.exp(-r1 * d1))
- + (c2 * Math.exp(-r2 * d1))
- + (c3 * Math.exp(-r3 * d1))
- + (c4 * Math.exp(-r4 * d1))
- + (c5 * Math.exp(-r5 * d1));
- const v2 = (-c1 * Math.exp(-r1 * d2))
- + (c2 * Math.exp(-r2 * d2))
- + (c3 * Math.exp(-r3 * d2))
- + (c4 * Math.exp(-r4 * d2))
- + (c5 * Math.exp(-r5 * d2));
-
- // Calculate world record time
- const twsec1 = d1 / v1;
- const twsec2 = d2 / v2;
-
- // Calculate constants
- const k1 = 0.0654 - (0.00258 * v1);
- const k2 = 0.0654 - (0.00258 * v2);
- const a1 = 85 / k1;
- const a2 = 85 / k2;
- const b1 = 1 - (1035 / a1);
- const b2 = 1 - (1035 / a2);
-
- // Calculate Purdy Points for distance 1
- const points = a1 * ((twsec1 / t1) - b1);
-
- // Calculate time for distance 2
- const seconds = (a2 * twsec2) / (points + (a2 * b2));
-
- // Return predicted time
- return seconds;
-}
+function NewtonsMethod(initialEstimate, target, method, derivative, precision, iterations = 500) {
+ // Initialize estimate
+ let estimate = initialEstimate;
+ let estimateValue;
-/**
- * Calculate a runner's VO2 max from their performance in a race
- * @param {Number} d The race distance in meters
- * @param {Number} t The finish time in minutes
- * @returns {Number} The runner's VO2 max
- */
-function VO2Max(d, t) {
- const v = d / t;
- const result = (-4.6 + (0.182 * v) + (0.000104 * (v ** 2)))
- / (0.8 + (0.189 * Math.exp(-0.0128 * t)) + (0.299 * Math.exp(-0.193 * t)));
- return result;
+ for (let i = 0; i < iterations; i += 1) {
+ // Evaluate function at estimate
+ estimateValue = method(estimate);
+
+ // Check if estimate is close enough
+ if (Math.abs(target - estimateValue) < precision) {
+ break;
+ }
+
+ // Refine estimate
+ estimate -= (estimateValue - target) / derivative(estimate);
+ }
+
+ // Return refined estimate
+ return estimate;
}
-/**
- * Calculate the derivative with respect to time of the VO2 max curve at a specific point
- * @param {Number} d The race distance in meters
- * @param {Number} t The finish time in minutes
- * @return {Number} The derivative
+/*
+ * Methods that implement the Purdy Points race prediction model
+ * https://www.cs.uml.edu/~phoffman/xcinfo3.html
*/
-function VO2MaxDerivative(d, t) {
- const result = ((-575000 * (t ** 2)) + (22750 * d * t) + (13 * (d ** 2))) / (125
- * (t ** 2) * (189 * Math.exp((-8 * t) / 625) + (299 * Math.exp((-193 * t) / 1000) + 800)));
- return result;
-}
+const PurdyPointsModel = {
+ /**
+ * Predict a race time using the Purdy Points Model
+ * @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
+ * @returns {Number} The predicted time for the output race in seconds
+ */
+ predictTime(d1, t1, d2) {
+ // Declare constants
+ const c1 = 11.15895;
+ const c2 = 4.304605;
+ const c3 = 0.5234627;
+ const c4 = 4.031560;
+ const c5 = 2.316157;
+ const r1 = 3.796158e-2;
+ const r2 = 1.646772e-3;
+ const r3 = 4.107670e-4;
+ const r4 = 7.068099e-6;
+ const r5 = 5.220990e-9;
-/**
- * Predict a race time using the VO2 Max Model
+ // Calculate world record velocity from running curve
+ const v1 = (-c1 * Math.exp(-r1 * d1))
+ + (c2 * Math.exp(-r2 * d1))
+ + (c3 * Math.exp(-r3 * d1))
+ + (c4 * Math.exp(-r4 * d1))
+ + (c5 * Math.exp(-r5 * d1));
+ const v2 = (-c1 * Math.exp(-r1 * d2))
+ + (c2 * Math.exp(-r2 * d2))
+ + (c3 * Math.exp(-r3 * d2))
+ + (c4 * Math.exp(-r4 * d2))
+ + (c5 * Math.exp(-r5 * d2));
+
+ // Calculate world record time
+ const twsec1 = d1 / v1;
+ const twsec2 = d2 / v2;
+
+ // Calculate constants
+ const k1 = 0.0654 - (0.00258 * v1);
+ const k2 = 0.0654 - (0.00258 * v2);
+ const a1 = 85 / k1;
+ const a2 = 85 / k2;
+ const b1 = 1 - (1035 / a1);
+ const b2 = 1 - (1035 / a2);
+
+ // Calculate Purdy Points for distance 1
+ const points = a1 * ((twsec1 / t1) - b1);
+
+ // Calculate time for distance 2
+ const seconds = (a2 * twsec2) / (points + (a2 * b2));
+
+ // Return predicted time
+ return seconds;
+ },
+
+ /**
+ * Calculate the derivative with respect to distance of the Purdy Points curve at a specific point
+ * @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
+ * @return {Number} The derivative with respect to distance
+ */
+ derivative(d1, t1, d2) {
+ const result = (85 * d2) / (((2316157 * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000
+ + (100789 * Math.exp(-(7068099 * d2) / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767
+ * d2) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000
+ - (223179 * Math.exp(-(1898079 * d2) / 50000000)) / 20000) * (327 / 5000 - (129 * ((2316157
+ * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
+ / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
+ + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
+ * d2) / 50000000)) / 20000)) / 50000) * ((85 * (1 - (207 * (327 / 5000 - (129 * ((2316157
+ * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
+ / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
+ + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
+ * d2) / 50000000)) / 20000)) / 50000)) / 17)) / (327 / 5000 - (129 * ((2316157
+ * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
+ / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
+ + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
+ * d2) / 50000000)) / 20000)) / 50000) + (85 * (d1 / (((2316157 * Math.exp(-(522099 * d1)
+ / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000
+ + (5234627 * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693
+ * d1) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000) * t1)
+ + (207 * (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000))
+ / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627
+ * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1)
+ / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000))
+ / 17 - 1)) / (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000))
+ / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627
+ * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1)
+ / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000)));
+ return result;
+ },
+
+ /**
+ * Predict a race distance using the Purdy Points Model
+ * @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
+ * @returns {Number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1, d1, t2) {
+ // 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) / 100; // Derivative on its own is too slow
+ estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01);
+
+ // Return estimate
+ return estimate;
+ },
+};
+
+/*
+ * Methods that implement the VO2 Max race prediction model
* http://run-down.com/statistics/calcs_explained.php
- * @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 {Number} iterations The maximum number of times to refine the prediction
- * @returns {Number} The predicted time for the output race in seconds
*/
-function VO2MaxModel(d1, t1, d2, iterations = 500) {
- // Calculate input VO2 max
- const inputVO2 = VO2Max(d1, t1 / 60);
+const VO2MaxModel = {
+ /**
+ * Calculate a runner's VO2 max from their performance in a race
+ * @param {Number} d The race distance in meters
+ * @param {Number} t The finish time in minutes
+ * @returns {Number} The runner's VO2 max
+ */
+ VO2Max(d, t) {
+ const v = d / t;
+ const result = (-4.6 + (0.182 * v) + (0.000104 * (v ** 2)))
+ / (0.8 + (0.189 * Math.exp(-0.0128 * t)) + (0.299 * Math.exp(-0.193 * t)));
+ return result;
+ },
- // Initialize estimate
- let estimate = (t1 * d2) / (d1 * 60);
- let estimateVO2;
+ /**
+ * Calculate the derivative with respect to time of the VO2 max curve at a specific point
+ * @param {Number} d The race distance in meters
+ * @param {Number} t The finish time in minutes
+ * @return {Number} The derivative with respect to time
+ */
+ VO2MaxTimeDerivative(d, t) {
+ const result = -((-575000 * (t ** 2)) + (22750 * d * t) + (13 * (d ** 2))) / (125
+ * (t ** 2) * (189 * Math.exp((-8 * t) / 625) + (299 * Math.exp((-193 * t) / 1000) + 800)));
+ return result;
+ },
- for (let i = 0; i < iterations; i += 1) {
- // Get estimate's VO2 max
- estimateVO2 = VO2Max(d2, estimate);
+ /**
+ * Predict a race time using the VO2 Max Model
+ * @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
+ * @returns {Number} The predicted time for the output race in seconds
+ */
+ predictTime(d1, t1, d2) {
+ // Calculate input VO2 max
+ const inputVO2 = this.VO2Max(d1, t1 / 60);
- // Check if estimate is close enough
- if (Math.abs(inputVO2 - estimateVO2) < 0.0001) {
- break;
- }
+ // Initialize estimate
+ let estimate = (t1 * d2) / (d1 * 60);
// Refine estimate
- estimate += (estimateVO2 - inputVO2) / VO2MaxDerivative(d2, estimate);
- }
+ const method = (x) => this.VO2Max(d2, x);
+ const derivative = (x) => this.VO2MaxTimeDerivative(d2, x);
+ estimate = NewtonsMethod(estimate, inputVO2, method, derivative, 0.0001);
- // Return estimate
- return estimate * 60;
-}
+ // Return estimate
+ return estimate * 60;
+ },
-/**
- * Predict a race time using Dave Cameron's Model
+ /**
+ * Calculate the derivative with respect to distance of the VO2 max curve at a specific point
+ * @param {Number} d The race distance in meters
+ * @param {Number} t The finish time in minutes
+ * @return {Number} The derivative with respect to distance
+ */
+ VO2MaxDistanceDerivative(d, t) {
+ const result = ((26 * d) + (22750 * t)) / (125 * (t ** 2) * ((189 * Math.exp(-(8 * t) / 625))
+ + (299 * Math.exp(-(193 * t) / 1000)) + 800));
+ return result;
+ },
+
+ /**
+ * Predict a race distance using the VO2 Max Model
+ * @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
+ * @returns {Number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1, d1, t2) {
+ // Calculate input VO2 max
+ const inputVO2 = this.VO2Max(d1, t1 / 60);
+
+ // Initialize estimate
+ let estimate = (d1 * t2) / t1;
+
+ // Refine estimate
+ const method = (x) => this.VO2Max(x, t2 / 60);
+ const derivative = (x) => this.VO2MaxDistanceDerivative(x, t2 / 60);
+ estimate = NewtonsMethod(estimate, inputVO2, method, derivative, 0.0001);
+
+ // Return estimate
+ return estimate;
+ },
+};
+
+/*
+ * Methods that implement Dave Cameron's race prediction model
* https://www.cs.uml.edu/~phoffman/cammod.html
- * @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
- * @returns {Number} The predicted time for the output race in seconds
*/
-function CameronModel(d1, t1, d2) {
- const a = 13.49681 - (0.000030363 * d1) + (835.7114 / (d1 ** 0.7905));
- const b = 13.49681 - (0.000030363 * d2) + (835.7114 / (d2 ** 0.7905));
- return (t1 / d1) * (a / b) * d2;
-}
+const CameronModel = {
+ /**
+ * Predict a race time using Dave Cameron's Model
+ * @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
+ * @returns {Number} The predicted time for the output race in seconds
+ */
+ predictTime(d1, t1, d2) {
+ const a = 13.49681 - (0.000030363 * d1) + (835.7114 / (d1 ** 0.7905));
+ const b = 13.49681 - (0.000030363 * d2) + (835.7114 / (d2 ** 0.7905));
+ return (t1 / d1) * (a / b) * d2;
+ },
-/**
- * Predict a race time using Pete Riegel's Model
+ /**
+ * Calculate the derivative with respect to distance of the Cameron curve at a specific point
+ * @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
+ * @return {Number} The derivative with respect to distance
+ */
+ derivative(d1, t1, d2) {
+ const result = -(100 * (30363 * (d1 ** (3581 / 2000)) - 13496810000 * (d1 ** (1581 / 2000))
+ - 835711400000) * t1 * (134968100 * (d2 ** (3581 / 2000)) + 14963412617 * d2)) / ((d1 ** (3581
+ / 2000)) * (d2 ** (419 / 2000)) * ((30363 * (d2 ** (3581 / 2000)) - 13496810000 * (d2 ** (1581
+ / 2000)) - 835711400000) ** 2));
+ return result;
+ },
+
+ /**
+ * Predict a race distance using Dave Cameron's Model
+ * @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
+ * @returns {Number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1, d1, t2) {
+ // 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);
+ estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01);
+
+ // Return estimate
+ return estimate;
+ },
+};
+
+/*
+ * Methods that implement Pete Riegel's race prediction model
* https://en.wikipedia.org/wiki/Peter_Riegel
- * @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 {Number} c The value of the exponent in the equation
- * @returns {Number} The predicted time for the output race in seconds
*/
-function RiegelModel(d1, t1, d2, c = 1.06) {
- return t1 * ((d2 / d1) ** c);
-}
+const RiegelModel = {
+ /**
+ * Predict a race time using Pete Riegel's Model
+ * @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 {Number} c The value of the exponent in the equation
+ * @returns {Number} The predicted time for the output race in seconds
+ */
+ predictTime(d1, t1, d2, c = 1.06) {
+ return t1 * ((d2 / d1) ** c);
+ },
-/**
- * Predict a race time by averaging the results of different models
- * @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 {Number} c The value of the exponent in Pete Riegel's Model
- * @returns {Number} The predicted time for the output race in seconds
+ /**
+ * Predict a race distance using Pete Riegel's Model
+ * @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 {Number} c The value of the exponent in the equation
+ * @returns {Number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1, d1, t2, c = 1.06) {
+ return d1 * ((t2 / t1) ** (1 / c));
+ },
+};
+
+/*
+ * Methods that average the results of different race prediction models
*/
-function AverageModel(d1, t1, d2, c = 1.06) {
- const purdy = PurdyPointsModel(d1, t1, d2);
- const vo2max = VO2MaxModel(d1, t1, d2);
- const cameron = CameronModel(d1, t1, d2);
- const riegel = RiegelModel(d1, t1, d2, c);
- return (purdy + vo2max + cameron + riegel) / 4;
-}
+const AverageModel = {
+ /**
+ * Predict a race time by averaging the results of different models
+ * @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 {Number} c The value of the exponent in Pete Riegel's Model
+ * @returns {Number} The predicted time for the output race in seconds
+ */
+ predictTime(d1, t1, d2, c = 1.06) {
+ const purdy = PurdyPointsModel.predictTime(d1, t1, d2);
+ const vo2max = VO2MaxModel.predictTime(d1, t1, d2);
+ const cameron = CameronModel.predictTime(d1, t1, d2);
+ const riegel = RiegelModel.predictTime(d1, t1, d2, c);
+ return (purdy + vo2max + cameron + riegel) / 4;
+ },
+
+ /**
+ * Predict a race distance by averaging the results of different models
+ * @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 {Number} c The value of the exponent in Pete Riegel's Model
+ * @returns {Number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1, d1, t2, c = 1.06) {
+ const purdy = PurdyPointsModel.predictDistance(t1, d1, t2);
+ const vo2max = VO2MaxModel.predictDistance(t1, d1, t2);
+ const cameron = CameronModel.predictDistance(t1, d1, t2);
+ const riegel = RiegelModel.predictDistance(t1, d1, t2, c);
+ return (purdy + vo2max + cameron + riegel) / 4;
+ },
+};
export default {
PurdyPointsModel,
diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue
@@ -15,7 +15,7 @@
<p>is the same pace as running</p>
- <time-table class="output" :calculate-result="calculatePace" :default-targets="defaultTargets"
+ <target-table class="output" :calculate-result="calculatePace" :default-targets="defaultTargets"
storage-key="pace-calculator-targets"/>
</div>
</template>
@@ -26,7 +26,7 @@ import unitUtils from '@/utils/units';
import DecimalInput from '@/components/DecimalInput.vue';
import TimeInput from '@/components/TimeInput.vue';
-import TimeTable from '@/components/TimeTable.vue';
+import TargetTable from '@/components/TargetTable.vue';
export default {
name: 'PaceCalculator',
@@ -34,7 +34,7 @@ export default {
components: {
DecimalInput,
TimeInput,
- TimeTable,
+ TargetTable,
},
data() {
@@ -52,7 +52,7 @@ export default {
/**
* The input time value
*/
- inputTime: 10 * 60,
+ inputTime: 8 * 60,
/**
* The names of the distance units
@@ -63,37 +63,41 @@ export default {
* The default output targets
*/
defaultTargets: [
- { distanceValue: 100, distanceUnit: 'meters' },
- { distanceValue: 200, distanceUnit: 'meters' },
- { distanceValue: 300, distanceUnit: 'meters' },
- { distanceValue: 400, distanceUnit: 'meters' },
- { distanceValue: 600, distanceUnit: 'meters' },
- { distanceValue: 800, distanceUnit: 'meters' },
- { distanceValue: 1000, distanceUnit: 'meters' },
- { distanceValue: 1200, distanceUnit: 'meters' },
- { distanceValue: 1500, distanceUnit: 'meters' },
- { distanceValue: 1600, distanceUnit: 'meters' },
- { distanceValue: 3200, distanceUnit: 'meters' },
-
- { distanceValue: 2, distanceUnit: 'kilometers' },
- { distanceValue: 3, distanceUnit: 'kilometers' },
- { distanceValue: 4, distanceUnit: 'kilometers' },
- { distanceValue: 5, distanceUnit: 'kilometers' },
- { distanceValue: 6, distanceUnit: 'kilometers' },
- { distanceValue: 8, distanceUnit: 'kilometers' },
- { distanceValue: 10, distanceUnit: 'kilometers' },
- { distanceValue: 15, distanceUnit: 'kilometers' },
-
- { distanceValue: 1, distanceUnit: 'miles' },
- { distanceValue: 2, distanceUnit: 'miles' },
- { distanceValue: 3, distanceUnit: 'miles' },
- { distanceValue: 5, distanceUnit: 'miles' },
- { distanceValue: 6, distanceUnit: 'miles' },
- { distanceValue: 8, distanceUnit: 'miles' },
- { distanceValue: 10, distanceUnit: 'miles' },
-
- { distanceValue: 0.5, distanceUnit: 'marathons' },
- { distanceValue: 1, distanceUnit: 'marathons' },
+ { result: 'time', distanceValue: 100, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 200, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 300, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 400, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 600, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 800, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 1000, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 1200, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 1500, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 1600, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 3200, distanceUnit: 'meters' },
+
+ { result: 'time', distanceValue: 2, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 3, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 4, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 6, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 8, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 10, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 15, distanceUnit: 'kilometers' },
+
+ { result: 'time', distanceValue: 1, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 2, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 3, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 5, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 6, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 8, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 10, distanceUnit: 'miles' },
+
+ { result: 'time', distanceValue: 0.5, distanceUnit: 'marathons' },
+ { result: 'time', distanceValue: 1, distanceUnit: 'marathons' },
+
+ { result: 'distance', distanceUnit: 'miles', time: 600 },
+ { result: 'distance', distanceUnit: 'miles', time: 1800 },
+ { result: 'distance', distanceUnit: 'miles', time: 3600 },
],
};
},
@@ -116,19 +120,40 @@ export default {
* @returns {Object} The result
*/
calculatePace(target) {
- // Convert distance into meters
- const distance = unitUtils.convertDistance(target.distanceValue, target.distanceUnit,
- unitUtils.DISTANCE_UNITS.meters);
-
- // Calculate time to travel distance at input pace
- const time = paceUtils.getTime(this.pace, distance);
-
- // Return result
- return {
+ // Initialize result
+ const result = {
distanceValue: target.distanceValue,
distanceUnit: target.distanceUnit,
- time,
+ 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,
+ unitUtils.DISTANCE_UNITS.meters);
+
+ // Calculate time to travel distance at input pace
+ const time = paceUtils.getTime(this.pace, d2);
+
+ // Update result
+ result.time = time;
+ } else {
+ // Calculate distance traveled in time at input pace
+ let distance = paceUtils.getDistance(this.pace, target.time);
+
+ // Convert output distance into miles
+ distance = unitUtils.convertDistance(distance, unitUtils.DISTANCE_UNITS.meters,
+ unitUtils.DISTANCE_UNITS.miles);
+
+ // Update result
+ result.distanceValue = distance;
+ result.distanceUnit = 'miles';
+ }
+
+ // Return result
+ return result;
},
},
};
diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue
@@ -14,7 +14,7 @@
<p>is approximately equivalent to running</p>
- <time-table class="output" :calculate-result="predictTime" :default-targets="defaultTargets"
+ <target-table class="output" :calculate-result="predictTime" :default-targets="defaultTargets"
storage-key="race-calculator-targets"/>
</div>
</template>
@@ -25,7 +25,7 @@ import unitUtils from '@/utils/units';
import DecimalInput from '@/components/DecimalInput.vue';
import TimeInput from '@/components/TimeInput.vue';
-import TimeTable from '@/components/TimeTable.vue';
+import TargetTable from '@/components/TargetTable.vue';
export default {
name: 'RaceCalculator',
@@ -33,7 +33,7 @@ export default {
components: {
DecimalInput,
TimeInput,
- TimeTable,
+ TargetTable,
},
data() {
@@ -62,28 +62,31 @@ export default {
* The default output targets
*/
defaultTargets: [
- { distanceValue: 400, distanceUnit: 'meters' },
- { distanceValue: 800, distanceUnit: 'meters' },
- { distanceValue: 1000, distanceUnit: 'meters' },
- { distanceValue: 1200, distanceUnit: 'meters' },
- { distanceValue: 1500, distanceUnit: 'meters' },
- { distanceValue: 1600, distanceUnit: 'meters' },
- { distanceValue: 3200, distanceUnit: 'meters' },
-
- { distanceValue: 3, distanceUnit: 'kilometers' },
- { distanceValue: 5, distanceUnit: 'kilometers' },
- { distanceValue: 8, distanceUnit: 'kilometers' },
- { distanceValue: 10, distanceUnit: 'kilometers' },
- { distanceValue: 15, distanceUnit: 'kilometers' },
-
- { distanceValue: 1, distanceUnit: 'miles' },
- { distanceValue: 2, distanceUnit: 'miles' },
- { distanceValue: 3, distanceUnit: 'miles' },
- { distanceValue: 5, distanceUnit: 'miles' },
- { distanceValue: 10, distanceUnit: 'miles' },
-
- { distanceValue: 0.5, distanceUnit: 'marathons' },
- { distanceValue: 1, distanceUnit: 'marathons' },
+ { result: 'time', distanceValue: 400, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 800, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 1000, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 1200, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 1500, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 1600, distanceUnit: 'meters' },
+ { result: 'time', distanceValue: 3200, distanceUnit: 'meters' },
+
+ { result: 'time', distanceValue: 3, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 8, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 10, distanceUnit: 'kilometers' },
+ { result: 'time', distanceValue: 15, distanceUnit: 'kilometers' },
+
+ { result: 'time', distanceValue: 1, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 2, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 3, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 5, distanceUnit: 'miles' },
+ { result: 'time', distanceValue: 10, distanceUnit: 'miles' },
+
+ { result: 'time', distanceValue: 0.5, distanceUnit: 'marathons' },
+ { result: 'time', distanceValue: 1, distanceUnit: 'marathons' },
+
+ { result: 'distance', distanceUnit: 'miles', time: 600 },
+ { result: 'distance', distanceUnit: 'miles', time: 3600 },
],
};
},
@@ -95,21 +98,44 @@ export default {
* @returns {Object} The result
*/
predictTime(target) {
- // Convert distances into meters
+ // Convert input race distance into meters
const d1 = unitUtils.convertDistance(this.inputDistance, this.inputUnit,
unitUtils.DISTANCE_UNITS.meters);
- const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit,
- unitUtils.DISTANCE_UNITS.meters);
-
- // Get prediction
- const time = raceUtils.AverageModel(d1, this.inputTime, d2);
- // Return result
- return {
+ // Initialize result
+ const result = {
distanceValue: target.distanceValue,
distanceUnit: target.distanceUnit,
- time,
+ 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,
+ unitUtils.DISTANCE_UNITS.meters);
+
+ // Get prediction
+ const time = raceUtils.AverageModel.predictTime(d1, this.inputTime, d2);
+
+ // Update result
+ result.time = time;
+ } else {
+ // Get prediction
+ let distance = raceUtils.AverageModel.predictDistance(this.inputTime, d1, target.time);
+
+ // Convert output distance into miles
+ distance = unitUtils.convertDistance(distance, unitUtils.DISTANCE_UNITS.meters,
+ unitUtils.DISTANCE_UNITS.miles);
+
+ // Update result
+ result.distanceValue = distance;
+ result.distanceUnit = 'miles';
+ }
+
+ // Return result
+ return result;
},
},
};
diff --git a/tests/unit/components/TargetTable.spec.js b/tests/unit/components/TargetTable.spec.js
@@ -0,0 +1,34 @@
+/* eslint-disable no-underscore-dangle */
+
+import { expect } from 'chai';
+import { shallowMount } from '@vue/test-utils';
+import TargetTable from '@/components/TargetTable.vue';
+
+describe('components/TargetTable.vue', () => {
+ it('results should be correct and sorted by time', () => {
+ // Initialize component
+ const wrapper = shallowMount(TargetTable, {
+ propsData: {
+ calculateResult: (row) => ({
+ distanceValue: row.distanceValue,
+ distanceUnit: row.distanceUnit,
+ time: row.distanceValue + 1,
+ }),
+ defaultTargets: [
+ { distanceValue: 20, distanceUnit: 'meters' },
+ { distanceValue: 100, distanceUnit: 'meters' },
+ { distanceValue: 1, distanceUnit: 'kilometers' },
+ { distanceValue: 10, distanceUnit: 'meters' },
+ ],
+ },
+ });
+
+ // Assert results are correct
+ expect(wrapper.vm._computedWatchers.results.value).to.deep.equal([
+ { distanceValue: 1, distanceUnit: 'kilometers', time: 2 },
+ { distanceValue: 10, distanceUnit: 'meters', time: 11 },
+ { distanceValue: 20, distanceUnit: 'meters', time: 21 },
+ { distanceValue: 100, distanceUnit: 'meters', time: 101 },
+ ]);
+ });
+});
diff --git a/tests/unit/components/TimeTable.spec.js b/tests/unit/components/TimeTable.spec.js
@@ -1,34 +0,0 @@
-/* eslint-disable no-underscore-dangle */
-
-import { expect } from 'chai';
-import { shallowMount } from '@vue/test-utils';
-import TimeTable from '@/components/TimeTable.vue';
-
-describe('components/TimeTable.vue', () => {
- it('results should be correct and sorted by time', () => {
- // Initialize component
- const wrapper = shallowMount(TimeTable, {
- propsData: {
- calculateResult: (row) => ({
- distanceValue: row.distanceValue,
- distanceUnit: row.distanceUnit,
- time: row.distanceValue + 1,
- }),
- defaultTargets: [
- { distanceValue: 20, distanceUnit: 'meters' },
- { distanceValue: 100, distanceUnit: 'meters' },
- { distanceValue: 1, distanceUnit: 'kilometers' },
- { distanceValue: 10, distanceUnit: 'meters' },
- ],
- },
- });
-
- // Assert results are correct
- expect(wrapper.vm._computedWatchers.results.value).to.deep.equal([
- { distanceValue: 1, distanceUnit: 'kilometers', time: 2 },
- { distanceValue: 10, distanceUnit: 'meters', time: 11 },
- { distanceValue: 20, distanceUnit: 'meters', time: 21 },
- { distanceValue: 100, distanceUnit: 'meters', time: 101 },
- ]);
- });
-});
diff --git a/tests/unit/utils/races.spec.js b/tests/unit/utils/races.spec.js
@@ -2,67 +2,145 @@ import { expect } from 'chai';
import raceUtils from '@/utils/races';
describe('utils/races.js', () => {
- describe('PurdyPointsModel method', () => {
- it('Predictions should be approximately correct', () => {
- const result = raceUtils.PurdyPointsModel(5000, 1200, 10000);
- expect(result).to.be.closeTo(2490, 1);
+ describe('PurdyPointsModel', () => {
+ describe('PredictTime method', () => {
+ it('Predictions should be approximately correct', () => {
+ const result = raceUtils.PurdyPointsModel.predictTime(5000, 1200, 10000);
+ expect(result).to.be.closeTo(2490, 1);
+ });
+
+ it('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.PurdyPointsModel.predictTime(5000, 1200, 5000);
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
});
- it('Should predict identical times for itentical distances', () => {
- const result = raceUtils.PurdyPointsModel(5000, 1200, 5000);
- expect(result).to.be.closeTo(1200, 0.001);
+ describe('PredictDistance method', () => {
+ it('Predictions should be approximately correct', () => {
+ const result = raceUtils.PurdyPointsModel.predictDistance(1200, 5000, 2490);
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ it('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 method', () => {
- it('Predictions should be approximately correct', () => {
- const result = raceUtils.VO2MaxModel(5000, 1200, 10000);
- expect(result).to.be.closeTo(2488, 1);
+ describe('VO2MaxModel', () => {
+ describe('PredictTime method', () => {
+ it('Predictions should be approximately correct', () => {
+ const result = raceUtils.VO2MaxModel.predictTime(5000, 1200, 10000);
+ expect(result).to.be.closeTo(2488, 1);
+ });
+
+ it('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.VO2MaxModel.predictTime(5000, 1200, 5000);
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
});
- it('Should predict identical times for itentical distances', () => {
- const result = raceUtils.VO2MaxModel(5000, 1200, 5000);
- expect(result).to.be.closeTo(1200, 0.001);
+ describe('PredictDistance method', () => {
+ it('Predictions should be approximately correct', () => {
+ const result = raceUtils.VO2MaxModel.predictDistance(1200, 5000, 2488);
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ it('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.VO2MaxModel.predictDistance(1200, 5000, 1200);
+ expect(result).to.be.closeTo(5000, 0.001);
+ });
});
});
- describe('CameronModel method', () => {
- it('Predictions should be approximately correct', () => {
- const result = raceUtils.CameronModel(5000, 1200, 10000);
- expect(result).to.be.closeTo(2500, 1);
+ describe('CameronModel', () => {
+ describe('PredictTime method', () => {
+ it('Predictions should be approximately correct', () => {
+ const result = raceUtils.CameronModel.predictTime(5000, 1200, 10000);
+ expect(result).to.be.closeTo(2500, 1);
+ });
+
+ it('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.CameronModel.predictTime(5000, 1200, 5000);
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
});
- it('Should predict identical times for itentical distances', () => {
- const result = raceUtils.CameronModel(5000, 1200, 5000);
- expect(result).to.be.closeTo(1200, 0.001);
+ describe('PredictDistance method', () => {
+ it('Predictions should be approximately correct', () => {
+ const result = raceUtils.CameronModel.predictDistance(1200, 5000, 2500);
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ it('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.CameronModel.predictDistance(1200, 5000, 1200);
+ expect(result).to.be.closeTo(5000, 0.001);
+ });
});
});
- describe('RiegelModel method', () => {
- it('Predictions should be approximately correct', () => {
- const result = raceUtils.RiegelModel(5000, 1200, 10000);
- expect(result).to.be.closeTo(2502, 1);
+ describe('RiegelModel', () => {
+ describe('PredictTime method', () => {
+ it('Predictions should be approximately correct', () => {
+ const result = raceUtils.RiegelModel.predictTime(5000, 1200, 10000);
+ expect(result).to.be.closeTo(2502, 1);
+ });
+
+ it('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.RiegelModel.predictTime(5000, 1200, 5000);
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
});
- it('Should predict identical times for itentical distances', () => {
- const result = raceUtils.RiegelModel(5000, 1200, 5000);
- expect(result).to.be.closeTo(1200, 0.001);
+ describe('PredictDistance method', () => {
+ it('Predictions should be approximately correct', () => {
+ const result = raceUtils.RiegelModel.predictDistance(1200, 5000, 2502);
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ it('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.RiegelModel.predictDistance(1200, 5000, 1200);
+ expect(result).to.be.closeTo(5000, 0.001);
+ });
});
});
- describe('AverageModel method', () => {
- it('Predictions should be correct', () => {
- const result = raceUtils.AverageModel(5000, 1200, 10000);
- const riegel = raceUtils.RiegelModel(5000, 1200, 10000);
- const cameron = raceUtils.CameronModel(5000, 1200, 10000);
- const purdyPoints = raceUtils.PurdyPointsModel(5000, 1200, 10000);
- const vo2Max = raceUtils.VO2MaxModel(5000, 1200, 10000);
- expect(result).to.equal((riegel + cameron + purdyPoints + vo2Max) / 4);
+ describe('AverageModel', () => {
+ describe('PredictTime method', () => {
+ it('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);
+ });
+
+ it('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.AverageModel.predictTime(5000, 1200, 5000);
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
});
- it('Should predict identical times for itentical distances', () => {
- const result = raceUtils.AverageModel(5000, 1200, 5000);
- expect(result).to.be.closeTo(1200, 0.001);
+ describe('PredictDistance method', () => {
+ it('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.predictDistance(1200, 5000, expected);
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ it('Should predict identical times for itentical distances', () => {
+ const result = raceUtils.AverageModel.predictDistance(1200, 5000, 1200);
+ expect(result).to.be.closeTo(5000, 0.001);
+ });
});
});
});
diff --git a/tests/unit/views/PaceCalculator.spec.js b/tests/unit/views/PaceCalculator.spec.js
@@ -5,7 +5,7 @@ import { shallowMount } from '@vue/test-utils';
import PaceCalculator from '@/views/PaceCalculator.vue';
describe('views/PaceCalculator.vue', () => {
- it('should correctly calculate paces', async () => {
+ it('should correctly calculate times', async () => {
// Initialize component
const wrapper = shallowMount(PaceCalculator);
@@ -20,6 +20,7 @@ describe('views/PaceCalculator.vue', () => {
const result = wrapper.vm.calculatePace({
distanceValue: 20,
distanceUnit: 'meters',
+ result: 'time',
});
// Assert result is correct
@@ -27,6 +28,33 @@ describe('views/PaceCalculator.vue', () => {
distanceValue: 20,
distanceUnit: 'meters',
time: 2,
+ result: 'time',
+ });
+ });
+
+ it('should correctly calculate distances', async () => {
+ // Initialize component
+ const wrapper = shallowMount(PaceCalculator);
+
+ // Override input values
+ await wrapper.setData({
+ inputDistance: 1,
+ inputUnit: 'miles',
+ inputTime: 100,
+ });
+
+ // Calculate paces
+ const result = wrapper.vm.calculatePace({
+ time: 200,
+ result: 'distance',
+ });
+
+ // Assert result is correct
+ expect(result).to.deep.equal({
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 200,
+ result: 'distance',
});
});
});
diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js
@@ -3,6 +3,7 @@
import { expect } from 'chai';
import { shallowMount } from '@vue/test-utils';
import raceUtils from '@/utils/races';
+import unitUtils from '@/utils/units';
import RaceCalculator from '@/views/RaceCalculator.vue';
describe('views/RaceCalculator.vue', () => {
@@ -14,21 +15,50 @@ describe('views/RaceCalculator.vue', () => {
await wrapper.setData({
inputDistance: 5,
inputUnit: 'kilometers',
- inputTime: 20 * 60,
+ inputTime: 1200,
});
// Predict race times
const result = wrapper.vm.predictTime({
distanceValue: 10,
distanceUnit: 'kilometers',
+ result: 'time',
});
// Assert result is correct
- const prediction = raceUtils.AverageModel(5000, 1200, 10000);
+ const prediction = raceUtils.AverageModel.predictTime(5000, 1200, 10000);
expect(result).to.deep.equal({
distanceValue: 10,
distanceUnit: 'kilometers',
time: prediction,
+ result: 'time',
+ });
+ });
+
+ it('should correctly predict race distances', async () => {
+ // Initialize component
+ const wrapper = shallowMount(RaceCalculator);
+
+ // Override input values
+ await wrapper.setData({
+ inputDistance: 5,
+ inputUnit: 'kilometers',
+ inputTime: 1200,
+ });
+
+ // Predict race distances
+ const result = wrapper.vm.predictTime({
+ time: 2460,
+ result: 'distance',
+ });
+
+ // Assert result is correct
+ const prediction = raceUtils.AverageModel.predictDistance(1200, 5000, 2460);
+ expect(result).to.deep.equal({
+ distanceValue: unitUtils.convertDistance(prediction, 'meters', 'miles'),
+ distanceUnit: 'miles',
+ time: 2460,
+ result: 'distance',
});
});
});