running-tools

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

commit e4fc78a1aa40d710bb01979e611992af15d5e422
parent 583c500e154d0a25e26c579e4760bdadae4235ba
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sun, 22 Aug 2021 14:22:42 -0700

Implement race calculator

Diffstat:
Msrc/router/index.js | 10++++++++++
Msrc/views/Home.vue | 9+++++++--
Asrc/views/RaceCalculator.vue | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/unit/RaceCalculator.spec.js | 35+++++++++++++++++++++++++++++++++++
4 files changed, 199 insertions(+), 2 deletions(-)

diff --git a/src/router/index.js b/src/router/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; import Home from '../views/Home.vue'; import PaceCalculator from '../views/PaceCalculator.vue'; +import RaceCalculator from '../views/RaceCalculator.vue'; import UnitCalculator from '../views/UnitCalculator.vue'; Vue.use(VueRouter); @@ -34,6 +35,15 @@ const routes = [ }, }, { + path: '/calculate/races', + name: 'calculate-races', + component: RaceCalculator, + meta: { + title: 'Race Calculator', + back: 'home', + }, + }, + { path: '/calculate/units', name: 'calculate-units', component: UnitCalculator, diff --git a/src/views/Home.vue b/src/views/Home.vue @@ -4,12 +4,17 @@ A collection of tools for runners that calculate splits, convert units, and more </p> <div class="calculators"> - <router-link :to="{ name: 'calculate-paces' }" v-slot="{ navigate }"> + <router-link :to="{ name: 'calculate-paces' }" v-slot="{ navigate }" custom> <button @click="navigate"> Pace Calculator </button> </router-link> - <router-link :to="{ name: 'calculate-units' }" v-slot="{ navigate }"> + <router-link :to="{ name: 'calculate-races' }" v-slot="{ navigate }" custom> + <button @click="navigate"> + Race Calculator + </button> + </router-link> + <router-link :to="{ name: 'calculate-units' }" v-slot="{ navigate }" custom> <button @click="navigate"> Unit Calculator </button> diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -0,0 +1,147 @@ +<template> + <div class="race-calculator"> + <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> + + <p>is approximately equivalent to running</p> + + <time-table class="output" :calculate-result="predictTime" :default-targets="defaultTargets"/> + </div> +</template> + +<script> +import raceUtils from '@/utils/races'; +import unitUtils from '@/utils/units'; + +import DecimalInput from '@/components/DecimalInput.vue'; +import TimeInput from '@/components/TimeInput.vue'; +import TimeTable from '@/components/TimeTable.vue'; + +export default { + name: 'RaceCalculator', + + components: { + DecimalInput, + TimeInput, + TimeTable, + }, + + data() { + return { + /** + * The input distance value + */ + inputDistance: 5, + + /** + * The input distance unit + */ + inputUnit: 'kilometers', + + /** + * The input time value + */ + inputTime: 20 * 60, + + /** + * The names of the distance units + */ + distanceUnits: unitUtils.DISTANCE_UNIT_NAMES, + + /** + * 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: 1, distanceUnit: 'miles' }, + { distanceValue: 2, distanceUnit: 'miles' }, + { distanceValue: 3, distanceUnit: 'miles' }, + { distanceValue: 5, distanceUnit: 'miles' }, + { distanceValue: 10, distanceUnit: 'miles' }, + + { distanceValue: 3, distanceUnit: 'kilometers' }, + { distanceValue: 5, distanceUnit: 'kilometers' }, + { distanceValue: 8, distanceUnit: 'kilometers' }, + { distanceValue: 10, distanceUnit: 'kilometers' }, + { distanceValue: 15, distanceUnit: 'kilometers' }, + + { distanceValue: 0.5, distanceUnit: 'marathons' }, + { distanceValue: 1, distanceUnit: 'marathons' }, + ], + }; + }, + + methods: { + /** + * Predict race times 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.AverageFormula(d1, this.inputTime, d2); + + // Return result + return { + distanceValue: target.distanceValue, + distanceUnit: target.distanceUnit, + time, + }; + }, + }, +}; +</script> + +<style scoped> +/* container */ +.race-calculator { + display: flex; + flex-direction: column; + align-items: center; +} + +/* calculator input */ +.input { + text-align: center; + margin-bottom: 5px; +} +.input>* { + margin-bottom: 5px; /* adds space between wrapped lines */ +} +.input select { + margin-left: 5px; +} + +/* calculator output */ +.output { + margin-top: 10px; +} +@media only screen and (max-width: 500px) { + .output { + width: 100%; + min-width: 0px; + } +} +</style> diff --git a/tests/unit/RaceCalculator.spec.js b/tests/unit/RaceCalculator.spec.js @@ -0,0 +1,35 @@ +/* eslint-disable no-underscore-dangle */ + +import { expect } from 'chai'; +import { shallowMount } from '@vue/test-utils'; +import raceUtils from '@/utils/races'; +import RaceCalculator from '@/views/RaceCalculator.vue'; + +describe('RaceCalculator.vue', () => { + it('should correctly predict race times', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Override input values + await wrapper.setData({ + inputDistance: 5, + inputUnit: 'kilometers', + inputTime: 20 * 60, + }); + + // Predict race times + const result = wrapper.vm.predictTime({ + distanceValue: 10, + distanceUnit: 'kilometers', + }); + + // Assert result is correct + const riegel = raceUtils.RiegelFormula(5000, 20 * 60, 10000); + const cameron = raceUtils.CameronFormula(5000, 20 * 60, 10000); + expect(result).to.deep.equal({ + distanceValue: 10, + distanceUnit: 'kilometers', + time: (riegel + cameron) / 2, + }); + }); +});