commit b20593e4b112365acd9426dd663c087b05e61b5e
parent 4306a1e6fa2d6248fc4a4c0b8020d1aff715664c
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date: Thu, 16 Sep 2021 19:57:41 -0700
Merge branch 'dev'
Version 1.1.0
Diffstat:
28 files changed, 1568 insertions(+), 881 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## [1.1.0] - 2021-09-16
+
+### Added
+- Time based targets in pace and race calculators
+- Advanced race calculator options and output
+
+### Changed
+- Pace calculator shows pace of each result
+- Improved calculator interfaces
+- Default units are chosen automatically
+
## [1.0.0] - 2021-08-30
### Added
@@ -23,6 +34,7 @@
### Added
- Basic app structure
-[1.0.0]: https://github.com/ashermorgan/running-tools/compare/0.2.0...1.0.0
-[0.2.0]: https://github.com/ashermorgan/running-tools/compare/0.1.0...0.2.0
+[1.1.0]: https://github.com/ashermorgan/running-tools/releases/tag/1.1.0
+[1.0.0]: https://github.com/ashermorgan/running-tools/releases/tag/1.0.0
+[0.2.0]: https://github.com/ashermorgan/running-tools/releases/tag/0.2.0
[0.1.0]: https://github.com/ashermorgan/running-tools/releases/tag/0.1.0
diff --git a/README.md b/README.md
@@ -4,8 +4,8 @@ A collection of tools for runners and their coaches. Try it out [here](https://a
## Features
-- [Pace Calculator](https://ashermorgan.github.io/running-tools/#/calculate/paces): Calculate times for different distances at the same pace
-- [Race Calculator](https://ashermorgan.github.io/running-tools/#/calculate/races): Estimate equivalent times for races of different distances
+- [Pace Calculator](https://ashermorgan.github.io/running-tools/#/calculate/paces): Calculate distances and times that are at the same pace
+- [Race Calculator](https://ashermorgan.github.io/running-tools/#/calculate/races): Estimate equivalent results for races of different distances and/or times
- [Unit Calculator](https://ashermorgan.github.io/running-tools/#/calculate/units): Convert between different distance, time, speed, and pace units
diff --git a/package-lock.json b/package-lock.json
@@ -1,15 +1,17 @@
{
"name": "running-tools",
- "version": "1.0.0",
+ "version": "1.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "version": "1.0.0",
+ "name": "running-tools",
+ "version": "1.1.0",
"dependencies": {
"core-js": "^3.6.5",
"register-service-worker": "^1.7.1",
"vue": "^2.6.11",
+ "vue-feather-icons": "^5.1.0",
"vue-router": "^3.2.0"
},
"devDependencies": {
@@ -3304,6 +3306,11 @@
"node": ">=4"
}
},
+ "node_modules/babel-helper-vue-jsx-merge-props": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
+ "integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg=="
+ },
"node_modules/babel-loader": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz",
@@ -15820,6 +15827,14 @@
"node": ">=8.0.0"
}
},
+ "node_modules/vue-feather-icons": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vue-feather-icons/-/vue-feather-icons-5.1.0.tgz",
+ "integrity": "sha512-ZyM2yFGmL9DYLZYHm63KV1zCQOj8czC2LzDSkxoIp9o6zMAOY4yv1FkxbX+XNUwcH3RRrAuvf25Ij7CnUUsQVA==",
+ "dependencies": {
+ "babel-helper-vue-jsx-merge-props": "^2.0.2"
+ }
+ },
"node_modules/vue-hot-reload-api": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
@@ -19932,6 +19947,11 @@
"babylon": "^6.18.0"
}
},
+ "babel-helper-vue-jsx-merge-props": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
+ "integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg=="
+ },
"babel-loader": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz",
@@ -30085,6 +30105,14 @@
}
}
},
+ "vue-feather-icons": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vue-feather-icons/-/vue-feather-icons-5.1.0.tgz",
+ "integrity": "sha512-ZyM2yFGmL9DYLZYHm63KV1zCQOj8czC2LzDSkxoIp9o6zMAOY4yv1FkxbX+XNUwcH3RRrAuvf25Ij7CnUUsQVA==",
+ "requires": {
+ "babel-helper-vue-jsx-merge-props": "^2.0.2"
+ }
+ },
"vue-hot-reload-api": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
diff --git a/package.json b/package.json
@@ -1,6 +1,6 @@
{
"name": "running-tools",
- "version": "1.0.0",
+ "version": "1.1.0",
"description": "A collection of tools for runners and their coaches that calculate splits, predict race times, convert units, and more",
"private": true,
"scripts": {
@@ -13,6 +13,7 @@
"core-js": "^3.6.5",
"register-service-worker": "^1.7.1",
"vue": "^2.6.11",
+ "vue-feather-icons": "^5.1.0",
"vue-router": "^3.2.0"
},
"devDependencies": {
diff --git a/src/App.vue b/src/App.vue
@@ -3,7 +3,7 @@
<header>
<router-link :to="{ name: $route.meta.back }" v-if="$route.meta.back"
class="icon" title="Back">
- <img alt="" src="@/assets/chevron-left.svg"/>
+ <chevron-left-icon/>
</router-link>
<h1 v-if="$route.meta.title">
@@ -22,6 +22,18 @@
</div>
</template>
+<script>
+import { ChevronLeftIcon } from 'vue-feather-icons';
+
+export default {
+ name: 'App',
+
+ components: {
+ ChevronLeftIcon,
+ },
+};
+</script>
+
<style scoped>
header {
background-color: var(--theme);
@@ -36,9 +48,9 @@ header a {
height: 2em;
width: 2em;
}
-header a img {
+header a svg {
padding: 0em;
- filter: invert(0%) !important;
+ color: #000000;
}
h1 {
grid-column: 3;
diff --git a/src/assets/chevron-left.svg b/src/assets/chevron-left.svg
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg>
diff --git a/src/assets/edit.svg b/src/assets/edit.svg
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
diff --git a/src/assets/global.css b/src/assets/global.css
@@ -15,6 +15,21 @@ input, select, button {
button {
cursor: pointer;
}
+.link, .link:focus, .link:active, .link:hover {
+ border: none;
+ background: none;
+}
+a, .link {
+ text-decoration: none;
+}
+a:focus, .link:focus {
+ text-decoration: underline;
+}
+@media (hover: hover) {
+ a:hover, .link:hover {
+ text-decoration: underline;
+ }
+}
/* styles for icons */
.icon {
@@ -24,7 +39,7 @@ button {
cursor: pointer;
vertical-align: middle;
}
-.icon img {
+.icon svg {
width: 100%;
height: 100%;
padding: 0.3em;
@@ -38,7 +53,7 @@ button {
}
/* element colors */
-body, input, select, button, option {
+body, input, select, button, option, .icon svg {
color: var(--foreground);
}
body {
@@ -61,6 +76,9 @@ button:active {
button, input, select, tr {
border: 1px solid var(--background5);
}
+a, .link {
+ color: var(--link);
+}
/* light/default theme */
:root {
@@ -84,6 +102,9 @@ button, input, select, tr {
/* The foreground color of app elements */
--foreground: #000000;
+
+ /* The color of links */
+ --link: hsl(210, 100%, 40%);
}
/* dark mode */
@@ -95,9 +116,7 @@ button, input, select, tr {
--background4: hsl(210, 20%, 25%);
--background5: hsl(210, 20%, 30%);
--foreground: #e8e8e8;
- }
- .icon img {
- filter: invert(90%);
+ --link: hsl(210, 100%, 65%);
}
}
@@ -110,5 +129,6 @@ button, input, select, tr {
--background4: #ffffff;
--background5: #000000;
--foreground: #000000;
+ --link: #0000ff;
}
}
diff --git a/public/img/icons/icon.svg b/src/assets/icon.svg
diff --git a/src/assets/plus-circle.svg b/src/assets/plus-circle.svg
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
diff --git a/src/assets/rotate-ccw.svg b/src/assets/rotate-ccw.svg
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-ccw"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>
diff --git a/src/assets/trash-2.svg b/src/assets/trash-2.svg
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash-2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
diff --git a/src/assets/x.svg b/src/assets/x.svg
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
diff --git a/src/components/TargetTable.vue b/src/components/TargetTable.vue
@@ -0,0 +1,342 @@
+<template>
+ <div class="time-table">
+ <table class="results" v-show="!inEditMode">
+ <thead>
+ <tr>
+ <th>Distance</th>
+
+ <th>Time</th>
+
+ <th v-if="showPace">Pace</th>
+
+ <th>
+ <button class="icon" title="Edit Targets" @click="inEditMode=true" v-blur>
+ <edit-icon/>
+ </button>
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr v-for="(item, index) in results" :key="index">
+ <td :class="item.result === 'distance' ? 'result' : ''">
+ {{ item.distanceValue.toFixed(2) }}
+ {{ distanceUnits[item.distanceUnit].symbol }}
+ </td>
+
+ <td :colspan="showPace ? 1 : 2" :class="item.result === 'time' ? 'result' : ''">
+ {{ formatDuration(item.time, 0, 2) }}
+ </td>
+
+ <td v-if="showPace" colspan="2">
+ {{ formatDuration(getPace(item), 0, 0) }}
+ / {{ distanceUnits[getDefaultDistanceUnit()].symbol }}
+ </td>
+ </tr>
+
+ <tr v-if="results.length === 0" class="empty-message">
+ <td colspan="4">
+ There aren't any targets yet,<br>
+ click
+ <edit-icon/>
+ 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>
+ <rotate-ccw-icon/>
+ </button>
+ <button class="icon" title="Close" @click="inEditMode=false" v-blur>
+ <x-icon/>
+ </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.name }}
+ </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>
+ <trash-2-icon/>
+ </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: getDefaultDistanceUnit() })" 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 {
+ EditIcon,
+ RotateCcwIcon,
+ Trash2Icon,
+ XIcon,
+} from 'vue-feather-icons';
+
+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,
+
+ EditIcon,
+ RotateCcwIcon,
+ Trash2Icon,
+ XIcon,
+ },
+
+ 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,
+ },
+
+ /**
+ * Whether to show result paces
+ */
+ showPace: {
+ type: Boolean,
+ default: false,
+ },
+ },
+
+ data() {
+ return {
+ /**
+ * The distance units
+ */
+ distanceUnits: unitUtils.DISTANCE_UNITS,
+
+ /**
+ * The formatDuration method
+ */
+ formatDuration: unitUtils.formatDuration,
+
+ /**
+ * The getDefaultDistanceUnit method
+ */
+ getDefaultDistanceUnit: unitUtils.getDefaultDistanceUnit,
+
+ /**
+ * 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),
+ ];
+ },
+
+ /**
+ * Get the pace of a result
+ * @param {Object} result The result
+ */
+ getPace(result) {
+ return result.time / unitUtils.convertDistance(result.distanceValue, result.distanceUnit,
+ unitUtils.getDefaultDistanceUnit());
+ },
+ },
+
+ /**
+ * 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 svg {
+ height: 1em;
+ width: 1em;
+ color: var(--foreground);
+}
+</style>
diff --git a/src/components/TimeInput.vue b/src/components/TimeInput.vue
@@ -31,7 +31,7 @@ export default {
type: Number,
default: 0,
validator(value) {
- return value >= 0 && value <= 86399.99;
+ return value >= 0 && value <= 359999.99;
},
},
},
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,412 @@
/**
- * 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 = {
+ /**
+ * Calculate the Purdy Point variables for a distance
+ * @param {Number} d The distance in meters
+ * @returns {Object} The Purdy Point variables
+ */
+ getVariables(d) {
+ // 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 v = (-c1 * Math.exp(-r1 * d))
+ + (c2 * Math.exp(-r2 * d))
+ + (c3 * Math.exp(-r3 * d))
+ + (c4 * Math.exp(-r4 * d))
+ + (c5 * Math.exp(-r5 * d));
+
+ // Calculate world record time
+ const twsec = d / v;
+
+ // Calculate constants
+ const k = 0.0654 - (0.00258 * v);
+ const a = 85 / k;
+ const b = 1 - (1035 / a);
+
+ // Return Purdy Point variables
+ return {
+ twsec,
+ a,
+ b,
+ };
+ },
+
+ /**
+ * Get the Purdy Points for a race
+ * @param {Number} d The distance of the race in meters
+ * @param {Number} t The finish time of the race in seconds
+ * @returns {Number} The Purdy Points for the race
+ */
+ getPurdyPoints(d, t) {
+ // Get variables
+ const variables = this.getVariables(d);
+
+ // Calculate Purdy Points
+ const points = variables.a * ((variables.twsec / t) - variables.b);
+
+ // Return Purdy Points
+ return points;
+ },
+
+ /**
+ * 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) {
+ // Calculate Purdy Points for distance 1
+ const points = this.getPurdyPoints(d1, t1);
+
+ // Calculate time for distance 2
+ const variables = this.getVariables(d2);
+ const seconds = (variables.a * variables.twsec) / (points + (variables.a * variables.b));
+
+ // 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 the VO2 of a runner during a race
+ * @param {Number} d The race distance in meters
+ * @param {Number} t The finish time in seconds
+ * @returns {Number} The VO2
+ */
+ getVO2(d, t) {
+ const minutes = t / 60;
+ const v = d / minutes;
+ const result = -4.6 + (0.182 * v) + (0.000104 * (v ** 2));
+ return result;
+ },
- // Initialize estimate
- let estimate = (t1 * d2) / (d1 * 60);
- let estimateVO2;
+ /**
+ * Calculate the percentage of VO2 max a runner is at during a race
+ * @param {Number} t The race time in seconds
+ * @returns {Number} The percentage of VO2 max
+ */
+ getVO2Percentage(t) {
+ const minutes = t / 60;
+ const result = 0.8 + (0.189 * Math.exp(-0.0128 * minutes)) + (0.299 * Math.exp(-0.193
+ * minutes));
+ return result;
+ },
- for (let i = 0; i < iterations; i += 1) {
- // Get estimate's VO2 max
- estimateVO2 = VO2Max(d2, estimate);
+ /**
+ * Calculate a runner's VO2 max from a race result
+ * @param {Number} d The race distance in meters
+ * @param {Number} t The finish time in seconds
+ * @returns {Number} The runner's VO2 max
+ */
+ getVO2Max(d, t) {
+ const result = this.getVO2(d, t) / this.getVO2Percentage(t);
+ return result;
+ },
- // Check if estimate is close enough
- if (Math.abs(inputVO2 - estimateVO2) < 0.0001) {
- break;
- }
+ /**
+ * 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 seconds
+ * @return {Number} The derivative with respect to time
+ */
+ VO2MaxTimeDerivative(d, t) {
+ const result = (-(273 * d) / (25 * (t ** 2)) - (468 * (d ** 2)) / (625 * (t ** 3))) / ((189
+ * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5)
+ - (((273 * d) / (25 * t) + (234 * (d ** 2)) / (625 * (t ** 2)) - 23 / 5) * (-(63
+ * Math.exp(-(2 * t) / 9375)) / 1562500 - (57707 * Math.exp(-(193 * t) / 60000)) / 60000000))
+ / (((189 * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000
+ + 4 / 5) ** 2);
+ return result;
+ },
+
+ /**
+ * 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.getVO2Max(d1, t1);
+
+ // Initialize estimate
+ let estimate = (t1 * d2) / d1;
// Refine estimate
- estimate += (estimateVO2 - inputVO2) / VO2MaxDerivative(d2, estimate);
- }
+ const method = (x) => this.getVO2Max(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;
+ },
-/**
- * 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 seconds
+ * @return {Number} The derivative with respect to distance
+ */
+ VO2MaxDistanceDerivative(d, t) {
+ const result = ((468 * d) / (625 * (t ** 2)) + 273 / (25 * t)) / ((189 * Math.exp(-(2 * t)
+ / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5);
+ 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.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);
+ 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/utils/units.js b/src/utils/units.js
@@ -2,152 +2,94 @@
* The time units
*/
const TIME_UNITS = {
- seconds: 'seconds',
- minutes: 'minutes',
- hours: 'hours',
-};
-
-/**
- * The time unit names
- */
-const TIME_UNIT_NAMES = {
- seconds: 'Seconds',
- minutes: 'Minutes',
- hours: 'Hours',
-};
-
-/**
- * The time unit symbols
- */
-const TIME_UNIT_SYMBOLS = {
- seconds: 's',
- minutes: 'min',
- hours: 'hr',
-};
-
-/**
- * The value of each time unit in seconds
- */
-const TIME_UNIT_VALUES = {
- seconds: 1,
- minutes: 1 * 60,
- hours: 1 * 60 * 60,
+ seconds: {
+ name: 'Seconds',
+ symbol: 's',
+ value: 1,
+ },
+ minutes: {
+ name: 'Minutes',
+ symbol: 'min',
+ value: 60,
+ },
+ hours: {
+ name: 'Hours',
+ symbol: 'hr',
+ value: 3600,
+ },
};
/**
* The distance units
*/
const DISTANCE_UNITS = {
- meters: 'meters',
- kilometers: 'kilometers',
- yards: 'yards',
- miles: 'miles',
- marathons: 'marathons',
-};
-
-/**
- * The distance unit names
- */
-const DISTANCE_UNIT_NAMES = {
- meters: 'Meters',
- kilometers: 'Kilometers',
- yards: 'Yards',
- miles: 'Miles',
- marathons: 'Marathons',
-};
-
-/**
- * The distance unit symbols
- */
-const DISTANCE_UNIT_SYMBOLS = {
- meters: 'm',
- kilometers: 'km',
- yards: 'yd',
- miles: 'mi',
- marathons: 'marathons',
-};
-
-/**
- * The value of each distance unit in meters
- */
-const DISTANCE_UNIT_VALUES = {
- meters: 1,
- kilometers: 1000,
- yards: 0.9144,
- miles: 1609.3499,
- marathons: 42195,
+ meters: {
+ name: 'Meters',
+ symbol: 'm',
+ value: 1,
+ },
+ yards: {
+ name: 'Yards',
+ symbol: 'yd',
+ value: 0.9144,
+ },
+ kilometers: {
+ name: 'Kilometers',
+ symbol: 'km',
+ value: 1000,
+ },
+ miles: {
+ name: 'Miles',
+ symbol: 'mi',
+ value: 1609.3499,
+ },
+ marathons: {
+ name: 'Marathons',
+ symbol: 'marathons',
+ value: 42195,
+ },
};
/**
* The speed units
*/
const SPEED_UNITS = {
- meters_per_second: 'meters_per_second',
- kilometers_per_hour: 'kilometers_per_hour',
- miles_per_hour: 'miles_per_hour',
-};
-
-/**
- * The speed unit names
- */
-const SPEED_UNIT_NAMES = {
- meters_per_second: 'Meters per Second',
- kilometers_per_hour: 'Kilometers per Hour',
- miles_per_hour: 'Miles per Hour',
-};
-
-/**
- * The speed unit symbols
- */
-const SPEED_UNIT_SYMBOLS = {
- meters_per_second: 'm/s',
- kilometers_per_hour: 'kph',
- miles_per_hour: 'mph',
-};
-
-/**
- * The value of each speed unit in meters per second
- */
-const SPEED_UNIT_VALUES = {
- meters_per_second: 1,
- kilometers_per_hour: DISTANCE_UNIT_VALUES.kilometers / TIME_UNIT_VALUES.hours,
- miles_per_hour: DISTANCE_UNIT_VALUES.miles / TIME_UNIT_VALUES.hours,
-};
-
-/**
- * The pace units
- */
-const PACE_UNITS = {
- seconds_per_meter: 'seconds_per_meter',
- seconds_per_kilometer: 'seconds_per_kilometer',
- seconds_per_mile: 'seconds_per_mile',
-};
-
-/**
- * The pace unit names
- */
-const PACE_UNIT_NAMES = {
- seconds_per_meter: 'Seconds per Meter',
- seconds_per_kilometer: 'Time per Kilometer',
- seconds_per_mile: 'Time per Mile',
-};
-
-/**
- * The pace unit symbols
- */
-const PACE_UNIT_SYMBOLS = {
- seconds_per_meter: 's/m',
- seconds_er_kilometer: '/km',
- seconds_per_mile: '/mi',
+ meters_per_second: {
+ name: 'Meters per Second',
+ symbol: 'm/s',
+ value: 1,
+ },
+ kilometers_per_hour: {
+ name: 'Kilometers per Hour',
+ symbol: 'kph',
+ value: DISTANCE_UNITS.kilometers.value / TIME_UNITS.hours.value,
+ },
+ miles_per_hour: {
+ name: 'Miles per Hour',
+ symbol: 'mph',
+ value: DISTANCE_UNITS.miles.value / TIME_UNITS.hours.value,
+ },
};
/**
* The value of each pace unit in seconds per meter
*/
-const PACE_UNIT_VALUES = {
- seconds_per_meter: 1,
- seconds_per_kilometer: TIME_UNIT_VALUES.seconds / DISTANCE_UNIT_VALUES.kilometers,
- seconds_per_mile: TIME_UNIT_VALUES.seconds / DISTANCE_UNIT_VALUES.miles,
+const PACE_UNITS = {
+ seconds_per_meter: {
+ name: 'Seconds per Meter',
+ symbol: 's/m',
+ value: 1,
+ },
+ seconds_per_kilometer: {
+ name: 'Time per Kilometer',
+ symbol: '/ km',
+ value: TIME_UNITS.seconds.value / DISTANCE_UNITS.kilometers.value,
+ },
+ seconds_per_mile: {
+ name: 'Time per Mile',
+ symbol: '/ mi',
+ value: TIME_UNITS.seconds.value / DISTANCE_UNITS.miles.value,
+ },
};
/**
@@ -157,8 +99,8 @@ const PACE_UNIT_VALUES = {
* @param {String} outputUnit The unit of the output
* @returns {Number} The output
*/
-function convertTime(inputValue, inputUnits, outputUnits) {
- return (inputValue * TIME_UNIT_VALUES[inputUnits]) / TIME_UNIT_VALUES[outputUnits];
+function convertTime(inputValue, inputUnit, outputUnit) {
+ return (inputValue * TIME_UNITS[inputUnit].value) / TIME_UNITS[outputUnit].value;
}
/**
@@ -168,8 +110,8 @@ function convertTime(inputValue, inputUnits, outputUnits) {
* @param {String} outputUnit The unit of the output
* @returns {Number} The output
*/
-function convertDistance(inputValue, inputUnits, outputUnits) {
- return (inputValue * DISTANCE_UNIT_VALUES[inputUnits]) / DISTANCE_UNIT_VALUES[outputUnits];
+function convertDistance(inputValue, inputUnit, outputUnit) {
+ return (inputValue * DISTANCE_UNITS[inputUnit].value) / DISTANCE_UNITS[outputUnit].value;
}
/**
@@ -179,8 +121,8 @@ function convertDistance(inputValue, inputUnits, outputUnits) {
* @param {String} outputUnit The unit of the output
* @returns {Number} The output
*/
-function convertSpeed(inputValue, inputUnits, outputUnits) {
- return (inputValue * SPEED_UNIT_VALUES[inputUnits]) / SPEED_UNIT_VALUES[outputUnits];
+function convertSpeed(inputValue, inputUnit, outputUnit) {
+ return (inputValue * SPEED_UNITS[inputUnit].value) / SPEED_UNITS[outputUnit].value;
}
/**
@@ -190,8 +132,8 @@ function convertSpeed(inputValue, inputUnits, outputUnits) {
* @param {String} outputUnit The unit of the output
* @returns {Number} The output
*/
-function convertPace(inputValue, inputUnits, outputUnits) {
- return (inputValue * PACE_UNIT_VALUES[inputUnits]) / PACE_UNIT_VALUES[outputUnits];
+function convertPace(inputValue, inputUnit, outputUnit) {
+ return (inputValue * PACE_UNITS[inputUnit].value) / PACE_UNITS[outputUnit].value;
}
/**
@@ -201,20 +143,20 @@ function convertPace(inputValue, inputUnits, outputUnits) {
* @param {String} outputUnit The unit of the output
* @returns {Number} The output
*/
-function convertSpeedPace(inputValue, inputUnits, outputUnits) {
+function convertSpeedPace(inputValue, inputUnit, outputUnit) {
// Calculate input speed
let speed;
- if (inputUnits in PACE_UNIT_VALUES) {
- speed = 1 / (inputValue * PACE_UNIT_VALUES[inputUnits]);
+ if (inputUnit in PACE_UNITS) {
+ speed = 1 / (inputValue * PACE_UNITS[inputUnit].value);
} else {
- speed = inputValue * SPEED_UNIT_VALUES[inputUnits];
+ speed = inputValue * SPEED_UNITS[inputUnit].value;
}
// Calculate output
- if (outputUnits in PACE_UNIT_VALUES) {
- return (1 / speed) / PACE_UNIT_VALUES[outputUnits];
+ if (outputUnit in PACE_UNITS) {
+ return (1 / speed) / PACE_UNITS[outputUnit].value;
}
- return speed / SPEED_UNIT_VALUES[outputUnits];
+ return speed / SPEED_UNITS[outputUnit].value;
}
/**
@@ -273,22 +215,48 @@ function formatDuration(value, padding = 6, digits = 2) {
return result;
}
+/**
+ * Get the default unit system
+ * @returns {String} The default unit system
+ */
+function getDefaultUnitSystem() {
+ const language = navigator.language || navigator.userLanguage;
+ if (language.endsWith('-US') || language.endsWith('-MM')) {
+ return 'imperial';
+ }
+ return 'metric';
+}
+
+/**
+ * Get the default distance unit
+ * @returns {String} The default distance unit
+ */
+function getDefaultDistanceUnit() {
+ return getDefaultUnitSystem() === 'metric' ? 'kilometers' : 'miles';
+}
+
+/**
+ * Get the default speed unit
+ * @returns {String} The default speed unit
+ */
+function getDefaultSpeedUnit() {
+ return getDefaultUnitSystem() === 'metric' ? 'kilometers_per_hour' : 'miles_per_hour';
+}
+
+/**
+ * Get the default pace unit
+ * @returns {String} The default pace unit
+ */
+function getDefaultPaceUnit() {
+ return getDefaultUnitSystem() === 'metric' ? 'seconds_per_kilometer' : 'seconds_per_mile';
+}
+
export default {
TIME_UNITS,
DISTANCE_UNITS,
SPEED_UNITS,
PACE_UNITS,
- TIME_UNIT_NAMES,
- DISTANCE_UNIT_NAMES,
- SPEED_UNIT_NAMES,
- PACE_UNIT_NAMES,
-
- TIME_UNIT_SYMBOLS,
- DISTANCE_UNIT_SYMBOLS,
- SPEED_UNIT_SYMBOLS,
- PACE_UNIT_SYMBOLS,
-
convertTime,
convertDistance,
convertSpeed,
@@ -296,4 +264,9 @@ export default {
convertSpeedPace,
formatDuration,
+
+ getDefaultUnitSystem,
+ getDefaultDistanceUnit,
+ getDefaultSpeedUnit,
+ getDefaultPaceUnit,
};
diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue
@@ -1,21 +1,26 @@
<template>
<div class="pace-calculator">
+ <h2>Input Pace</h2>
<div class="input">
- Running
- <decimal-input v-model="inputDistance" aria-label="distance value"
- :min="0" :digits="2"/>
- <select v-model="inputUnit" aria-label="distance unit">
- <option v-for="(value, key) in distanceUnits" :key="key" :value="key">
- {{ value }}
- </option>
- </select>
- in
- <time-input v-model="inputTime"/>
+ <div>
+ Distance:
+ <decimal-input v-model="inputDistance" aria-label="distance value"
+ :min="0" :digits="2"/>
+ <select v-model="inputUnit" aria-label="distance unit">
+ <option v-for="(value, key) in distanceUnits" :key="key" :value="key">
+ {{ value.name }}
+ </option>
+ </select>
+ </div>
+ <div>
+ Time:
+ <time-input v-model="inputTime"/>
+ </div>
</div>
- <p>is the same pace as running</p>
+ <h2>Equivalent Paces</h2>
- <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 +31,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 +39,7 @@ export default {
components: {
DecimalInput,
TimeInput,
- TimeTable,
+ TargetTable,
},
data() {
@@ -42,58 +47,61 @@ export default {
/**
* The input distance value
*/
- inputDistance: 1,
+ inputDistance: 5,
/**
* The input distance unit
*/
- inputUnit: 'miles',
+ inputUnit: 'kilometers',
/**
* The input time value
*/
- inputTime: 10 * 60,
+ inputTime: 20 * 60,
/**
* The names of the distance units
*/
- distanceUnits: unitUtils.DISTANCE_UNIT_NAMES,
+ distanceUnits: unitUtils.DISTANCE_UNITS,
/**
* 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', time: 600 },
+ { result: 'distance', time: 1800 },
],
};
},
@@ -103,8 +111,7 @@ export default {
* The input pace (in seconds per meter)
*/
pace() {
- const distance = unitUtils.convertDistance(this.inputDistance,
- this.inputUnit, unitUtils.DISTANCE_UNITS.meters);
+ const distance = unitUtils.convertDistance(this.inputDistance, this.inputUnit, 'meters');
return paceUtils.getPace(distance, this.inputTime);
},
},
@@ -116,19 +123,38 @@ 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, '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 default distance unit
+ distance = unitUtils.convertDistance(distance, 'meters', unitUtils.getDefaultDistanceUnit());
+
+ // Update result
+ result.distanceValue = distance;
+ result.distanceUnit = unitUtils.getDefaultDistanceUnit();
+ }
+
+ // Return result
+ return result;
},
},
};
@@ -142,11 +168,16 @@ export default {
align-items: center;
}
-/* calculator input */
-.input {
- text-align: center;
- margin-bottom: 5px;
+/* headings */
+h2 {
+ font-size: 1.3em;
+ margin-bottom: 0.2em;
}
+* + h2 {
+ margin-top: 0.5em;
+}
+
+/* calculator input */
.input>* {
margin-bottom: 5px; /* adds space between wrapped lines */
}
@@ -156,7 +187,6 @@ export default {
/* calculator output */
.output {
- margin-top: 10px;
min-width: 300px;
}
@media only screen and (max-width: 500px) {
diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue
@@ -1,31 +1,72 @@
<template>
<div class="race-calculator">
+ <h2>Input Race Result</h2>
<div class="input">
- Running
- <decimal-input v-model="inputDistance" aria-label="Distance value" :min="0" :digits="2"/>
- <select v-model="inputUnit" aria-label="Distance unit">
- <option v-for="(value, key) in distanceUnits" :key="key" :value="key">
- {{ value }}
- </option>
- </select>
- in
- <time-input v-model="inputTime"/>
+ <div>
+ Distance:
+ <decimal-input v-model="inputDistance" aria-label="Distance value" :min="0" :digits="2"/>
+ <select v-model="inputUnit" aria-label="Distance unit">
+ <option v-for="(value, key) in distanceUnits" :key="key" :value="key">
+ {{ value.name }}
+ </option>
+ </select>
+ </div>
+ <div>
+ Time:
+ <time-input v-model="inputTime"/>
+ </div>
</div>
- <p>is approximately equivalent to running</p>
+ <h2>
+ Advanced
+ <button class="link" @click="showAdvancedOptions=!showAdvancedOptions">
+ {{ showAdvancedOptions ? '[hide]' : '[show]' }}
+ </button>
+ </h2>
+ <div class="advanced-options" v-show="showAdvancedOptions">
+ <div>
+ Prediction Model:
+ <select v-model="model" aria-label="Prediction Model">
+ <option value="AverageModel">Average</option>
+ <option value="PurdyPointsModel">Purdy Points Model</option>
+ <option value="VO2MaxModel">V̇O₂ Max Model</option>
+ <option value="CameronModel">Cameron's Model</option>
+ <option value="RiegelModel">Riegel's Model</option>
+ </select>
+ </div>
+ <div>
+ Riegel Exponent:
+ <decimal-input v-model="riegelExponent" aria-label="Riegel Exponent" :min="1" :max="1.3"
+ :digits="2" :step="0.01"/>
+ (default: 1.06)
+ </div>
+ <div>
+ Purdy Points: <b>{{ purdyPoints.toFixed(1) }}</b>
+ </div>
+ <div>
+ V̇O₂: <b>{{ vo2.toFixed(1) }}</b> ml/kg/min
+ (<b>{{ vo2Percentage.toFixed(1) }}%</b> of max)
+ </div>
+ <div>
+ V̇O₂ Max: <b>{{ vo2Max.toFixed(1) }}</b> ml/kg/min
+ </div>
+ </div>
+
+ <h2>Equivalent Race Results</h2>
- <time-table class="output" :calculate-result="predictTime" :default-targets="defaultTargets"
- storage-key="race-calculator-targets"/>
+ <target-table class="output" :calculate-result="predictResult" :default-targets="defaultTargets"
+ storage-key="race-calculator-targets" show-pace/>
</div>
</template>
<script>
import raceUtils from '@/utils/races';
+import storage from '@/utils/localStorage';
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 +74,7 @@ export default {
components: {
DecimalInput,
TimeInput,
- TimeTable,
+ TargetTable,
},
data() {
@@ -54,62 +95,202 @@ export default {
inputTime: 20 * 60,
/**
+ * The race prediction model
+ */
+ model: storage.get('race-calculator-model', 'AverageModel'),
+
+ /**
+ * The value of the exponent in Riegel's Model
+ */
+ riegelExponent: storage.get('race-calculator-riegel-exponent', 1.06),
+
+ /**
+ * Whether to show the advanced options
+ */
+ showAdvancedOptions: storage.get('race-calculator-show-advanced-options', false),
+
+ /**
* The names of the distance units
*/
- distanceUnits: unitUtils.DISTANCE_UNIT_NAMES,
+ distanceUnits: unitUtils.DISTANCE_UNITS,
/**
* 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', time: 600 },
+ { result: 'distance', time: 3600 },
],
};
},
methods: {
/**
- * Predict race times from a target
+ * Predict race results from a target
* @param {Object} target The target
* @returns {Object} The result
*/
- predictTime(target) {
- // Convert distances 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 {
+ predictResult(target) {
+ // 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, 'meters');
+
+ // Get prediction
+ let time;
+ switch (this.model) {
+ default:
+ case 'AverageModel':
+ time = raceUtils.AverageModel.predictTime(this.d1, this.inputTime, d2,
+ this.riegelExponent);
+ break;
+ case 'PurdyPointsModel':
+ time = raceUtils.PurdyPointsModel.predictTime(this.d1, this.inputTime, d2);
+ break;
+ case 'VO2MaxModel':
+ time = raceUtils.VO2MaxModel.predictTime(this.d1, this.inputTime, d2);
+ break;
+ case 'RiegelModel':
+ time = raceUtils.RiegelModel.predictTime(this.d1, this.inputTime, d2,
+ this.riegelExponent);
+ break;
+ case 'CameronModel':
+ time = raceUtils.CameronModel.predictTime(this.d1, this.inputTime, d2);
+ break;
+ }
+
+ // Update result
+ result.time = time;
+ } else {
+ // Get prediction
+ let distance;
+ switch (this.model) {
+ default:
+ case 'AverageModel':
+ distance = raceUtils.AverageModel.predictDistance(this.inputTime, this.d1, target.time,
+ this.riegelExponent);
+ break;
+ case 'PurdyPointsModel':
+ distance = raceUtils.PurdyPointsModel.predictDistance(this.inputTime, this.d1,
+ target.time);
+ break;
+ case 'VO2MaxModel':
+ distance = raceUtils.VO2MaxModel.predictDistance(this.inputTime, this.d1, target.time);
+ break;
+ case 'RiegelModel':
+ distance = raceUtils.RiegelModel.predictDistance(this.inputTime, this.d1, target.time,
+ this.riegelExponent);
+ break;
+ case 'CameronModel':
+ distance = raceUtils.CameronModel.predictDistance(this.inputTime, this.d1, target.time);
+ break;
+ }
+
+ // Convert output distance into default distance unit
+ distance = unitUtils.convertDistance(distance, 'meters', unitUtils.getDefaultDistanceUnit());
+
+ // Update result
+ result.distanceValue = distance;
+ result.distanceUnit = unitUtils.getDefaultDistanceUnit();
+ }
+
+ // Return result
+ return result;
+ },
+ },
+
+ computed: {
+ /**
+ * The input distance in meters
+ */
+ d1() {
+ return unitUtils.convertDistance(this.inputDistance, this.inputUnit, 'meters');
+ },
+
+ /**
+ * The Purdy Points for the input race
+ */
+ purdyPoints() {
+ const result = raceUtils.PurdyPointsModel.getPurdyPoints(this.d1, this.inputTime);
+ return result;
+ },
+
+ /**
+ * The VO2 Max calculated from the input race
+ */
+ vo2Max() {
+ const result = raceUtils.VO2MaxModel.getVO2Max(this.d1, this.inputTime);
+ return result;
+ },
+
+ /**
+ * The VO2 calculated from the input race
+ */
+ vo2() {
+ const result = raceUtils.VO2MaxModel.getVO2(this.d1, this.inputTime);
+ return result;
+ },
+
+ /**
+ * The percentage of VO2 Max calculated from the input race
+ */
+ vo2Percentage() {
+ const result = raceUtils.VO2MaxModel.getVO2Percentage(this.inputTime) * 100;
+ return result;
+ },
+ },
+
+ watch: {
+ /**
+ * Save prediction model
+ */
+ model(newValue) {
+ storage.set('race-calculator-model', newValue);
+ },
+
+ /**
+ * Save Riegel Model exponent
+ */
+ riegelExponent(newValue) {
+ storage.set('race-calculator-riegel-exponent', newValue);
+ },
+
+ /**
+ * Save advanced options state
+ */
+ showAdvancedOptions(newValue) {
+ storage.set('race-calculator-show-advanced-options', newValue);
},
},
};
@@ -123,11 +304,16 @@ export default {
align-items: center;
}
-/* calculator input */
-.input {
- text-align: center;
- margin-bottom: 5px;
+/* headings */
+h2 {
+ font-size: 1.3em;
+ margin-bottom: 0.2em;
+}
+* + h2 {
+ margin-top: 0.5em;
}
+
+/* calculator input */
.input>* {
margin-bottom: 5px; /* adds space between wrapped lines */
}
@@ -135,9 +321,13 @@ export default {
margin-left: 5px;
}
+/* advanced options */
+.advanced-options>* {
+ margin-bottom: 5px;
+}
+
/* calculator output */
.output {
- margin-top: 10px;
min-width: 300px;
}
@media only screen and (max-width: 500px) {
diff --git a/src/views/UnitCalculator.vue b/src/views/UnitCalculator.vue
@@ -12,23 +12,23 @@
v-model="inputValue" :min="0" :digits="2"/>
<select v-model="inputUnit" class="input-units" aria-label="input units">
- <option v-for="(value, key) in unitNames" :key="key" :value="key">
- {{ value }}
+ <option v-for="(value, key) in units" :key="key" :value="key">
+ {{ value.name }}
</option>
</select>
<span class="equals"> = </span>
<span v-if="getUnitType(outputUnit) === 'time'" class="output-value">
- {{ formatDuration(outputValue) }}
+ {{ formatDuration(outputValue, 6, 3) }}
</span>
<span v-else class="output-value">
- {{ outputValue.toFixed(2) }}
+ {{ outputValue.toFixed(3) }}
</span>
<select v-model="outputUnit" class="output-units" aria-label="output units">
- <option v-for="(value, key) in unitNames" :key="key" :value="key">
- {{ value }}
+ <option v-for="(value, key) in units" :key="key" :value="key">
+ {{ value.name }}
</option>
</select>
</div>
@@ -63,7 +63,7 @@ export default {
/**
* The unit of the output
*/
- outputUnit: 'meters',
+ outputUnit: 'kilometers',
/**
* The unit category
@@ -81,16 +81,23 @@ export default {
/**
* The names of the units in the current category
*/
- unitNames() {
+ units() {
switch (this.category) {
case 'distance': {
- return unitUtils.DISTANCE_UNIT_NAMES;
+ return unitUtils.DISTANCE_UNITS;
}
case 'time': {
- return { ...unitUtils.TIME_UNIT_NAMES, 'hh:mm:ss': 'hh:mm:ss' };
+ return {
+ ...unitUtils.TIME_UNITS,
+ 'hh:mm:ss': {
+ name: 'hh:mm:ss',
+ symbol: '',
+ value: null,
+ },
+ };
}
case 'speed_and_pace': {
- return { ...unitUtils.PACE_UNIT_NAMES, ...unitUtils.SPEED_UNIT_NAMES };
+ return { ...unitUtils.PACE_UNITS, ...unitUtils.SPEED_UNITS };
}
default: {
return {};
@@ -108,18 +115,8 @@ export default {
}
case 'time': {
// Correct input and output units for 'hh:mm:ss' unit
- let realInput;
- if (this.inputUnit === 'hh:mm:ss') {
- realInput = unitUtils.TIME_UNITS.seconds;
- } else {
- realInput = this.inputUnit;
- }
- let realOutput;
- if (this.outputUnit === 'hh:mm:ss') {
- realOutput = unitUtils.TIME_UNITS.seconds;
- } else {
- realOutput = this.outputUnit;
- }
+ const realInput = this.inputUnit === 'hh:mm:ss' ? 'seconds' : this.inputUnit;
+ const realOutput = this.outputUnit === 'hh:mm:ss' ? 'seconds' : this.outputUnit;
// Calculate conversion
return unitUtils.convertTime(this.inputValue, realInput, realOutput);
@@ -143,7 +140,7 @@ export default {
case 'distance': {
this.inputValue = 1;
this.inputUnit = 'miles';
- this.outputUnit = 'meters';
+ this.outputUnit = 'kilometers';
break;
}
case 'time': {
@@ -153,9 +150,9 @@ export default {
break;
}
case 'speed_and_pace': {
- this.inputValue = 1;
- this.inputUnit = 'miles_per_hour';
- this.outputUnit = 'seconds_per_mile';
+ this.inputValue = unitUtils.getDefaultPaceUnit() === 'seconds_per_mile' ? 600 : 300;
+ this.inputUnit = unitUtils.getDefaultPaceUnit();
+ this.outputUnit = unitUtils.getDefaultSpeedUnit();
break;
}
default: {
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,173 @@ 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('getPurdyPoints method', () => {
+ it('Result should be approximately correct', () => {
+ const result = raceUtils.PurdyPointsModel.getPurdyPoints(5000, 1200);
+ expect(result).to.be.closeTo(454, 1);
+ });
});
- it('Should predict identical times for itentical distances', () => {
- const result = raceUtils.PurdyPointsModel(5000, 1200, 5000);
- expect(result).to.be.closeTo(1200, 0.001);
+ 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);
+ });
+ });
+
+ 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('getVO2 method', () => {
+ it('Result should be approximately correct', () => {
+ const result = raceUtils.VO2MaxModel.getVO2(5000, 1200);
+ expect(result).to.be.closeTo(47.4, 0.1);
+ });
+ });
+
+ describe('getVO2Percentage method', () => {
+ it('Result should be approximately correct', () => {
+ const result = raceUtils.VO2MaxModel.getVO2Percentage(660);
+ expect(result).to.be.closeTo(1, 0.001);
+ });
+ });
+
+ describe('getVO2Max method', () => {
+ it('Result should be approximately correct', () => {
+ const result = raceUtils.VO2MaxModel.getVO2Max(5000, 1200);
+ expect(result).to.be.closeTo(49.8, 0.1);
+ });
+ });
+
+ 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/utils/units.spec.js b/tests/unit/utils/units.spec.js
@@ -4,100 +4,60 @@ import units from '@/utils/units';
describe('utils/units.js', () => {
describe('convertTime method', () => {
it('90 seconds should equal 1.5 minutes', () => {
- const result = units.convertTime(
- 90,
- units.TIME_UNITS.seconds,
- units.TIME_UNITS.minutes,
- );
+ const result = units.convertTime(90, 'seconds', 'minutes');
expect(result).to.equal(1.5);
});
it('1.5 minutes should equal 95 seconds', () => {
- const result = units.convertTime(
- 1.5,
- units.TIME_UNITS.minutes,
- units.TIME_UNITS.seconds,
- );
+ const result = units.convertTime(1.5, 'minutes', 'seconds');
expect(result).to.equal(90);
});
});
describe('convertDistance method', () => {
it('100 meters should equal 0.1 kilometers', () => {
- const result = units.convertDistance(
- 100,
- units.DISTANCE_UNITS.meters,
- units.DISTANCE_UNITS.kilometers,
- );
+ const result = units.convertDistance(100, 'meters', 'kilometers');
expect(result).to.equal(0.1);
});
it('0.1 kilometers should equal 100 meters', () => {
- const result = units.convertDistance(
- 0.1,
- units.DISTANCE_UNITS.kilometers,
- units.DISTANCE_UNITS.meters,
- );
+ const result = units.convertDistance(0.1, 'kilometers', 'meters');
expect(result).to.equal(100);
});
});
describe('convertSpeed method', () => {
it('1000 meters per seconds should equal 3600 kilometers per hour', () => {
- const result = units.convertSpeed(
- 1000,
- units.SPEED_UNITS.meters_per_second,
- units.SPEED_UNITS.kilometers_per_hour,
- );
+ const result = units.convertSpeed(1000, 'meters_per_second', 'kilometers_per_hour');
expect(result).to.equal(3600);
});
it('3600 kilometers per hour should equal 1000 meters per second', () => {
- const result = units.convertSpeed(
- 3600,
- units.SPEED_UNITS.kilometers_per_hour,
- units.SPEED_UNITS.meters_per_second,
- );
+ const result = units.convertSpeed(3600, 'kilometers_per_hour', 'meters_per_second');
expect(result).to.equal(1000);
});
});
describe('convertPace method', () => {
it('1 second per meter should equal 1000 seconds per kilometer', () => {
- const result = units.convertPace(
- 1,
- units.PACE_UNITS.seconds_per_meter,
- units.PACE_UNITS.seconds_per_kilometer,
- );
+ const result = units.convertPace(1, 'seconds_per_meter', 'seconds_per_kilometer');
expect(result).to.equal(1000);
});
it('1000 seconds per kilometer should equal 1 second per meter', () => {
- const result = units.convertPace(
- 1000,
- units.PACE_UNITS.seconds_per_kilometer,
- units.PACE_UNITS.seconds_per_meter,
- );
+ const result = units.convertPace(1000, 'seconds_per_kilometer', 'seconds_per_meter');
expect(result).to.equal(1);
});
});
describe('convertSpeedPace method', () => {
it('3600 kilometers per hour should equal 1 second per kilometer', () => {
- const result = units.convertSpeedPace(
- 3600,
- units.SPEED_UNITS.kilometers_per_hour,
- units.PACE_UNITS.seconds_per_kilometer,
- );
+ const result = units.convertSpeedPace(3600, 'kilometers_per_hour', 'seconds_per_kilometer');
expect(result).to.equal(1);
});
it('1 second per kilometer should equal 3600 kilometers per hour', () => {
- const result = units.convertSpeedPace(
- 3600,
- units.PACE_UNITS.seconds_per_kilometer,
- units.SPEED_UNITS.kilometers_per_hour,
- );
+ const result = units.convertSpeedPace(3600, 'seconds_per_kilometer', 'kilometers_per_hour');
expect(result).to.equal(1);
});
});
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({
+ const result = wrapper.vm.predictResult({
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.predictResult({
+ 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',
});
});
});
diff --git a/tests/unit/views/UnitCalculator.spec.js b/tests/unit/views/UnitCalculator.spec.js
@@ -2,6 +2,7 @@
import { expect } from 'chai';
import { shallowMount } from '@vue/test-utils';
+import unitUtils from '@/utils/units';
import UnitCalculator from '@/views/UnitCalculator.vue';
describe('views/UnitCalculator.vue', () => {
@@ -21,9 +22,11 @@ describe('views/UnitCalculator.vue', () => {
await wrapper.setData({ category: 'speed_and_pace' });
// Assert controls are correct
- expect(wrapper.vm._data.inputValue).to.equal(1);
- expect(wrapper.vm._data.inputUnit).to.equal('miles_per_hour');
- expect(wrapper.vm._data.outputUnit).to.equal('seconds_per_mile');
+ expect(wrapper.vm._data.inputValue).to.equal(
+ unitUtils.getDefaultPaceUnit() === 'seconds_per_mile' ? 600 : 300,
+ );
+ expect(wrapper.vm._data.inputUnit).to.equal(unitUtils.getDefaultPaceUnit());
+ expect(wrapper.vm._data.outputUnit).to.equal(unitUtils.getDefaultSpeedUnit());
// Change category
await wrapper.setData({ category: 'distance' });
@@ -31,7 +34,7 @@ describe('views/UnitCalculator.vue', () => {
// Assert controls are correct
expect(wrapper.vm._data.inputValue).to.equal(1);
expect(wrapper.vm._data.inputUnit).to.equal('miles');
- expect(wrapper.vm._data.outputUnit).to.equal('meters');
+ expect(wrapper.vm._data.outputUnit).to.equal('kilometers');
});
it('outputValue should be correct', async () => {