running-tools

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

commit 2c4ba07038a94b1f16b333c37de4786d78fa17ed
parent e297aed9c097caf19bf6bca64643b2d39905e55c
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Thu, 12 Aug 2021 16:03:22 -0700

Implement pace calculator

Diffstat:
Msrc/router/index.js | 18++++++++++++++----
Msrc/utils/units.js | 122++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/views/PaceCalculator.vue | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/unit/PaceCalculator.spec.js | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/unit/units.spec.js | 23+++++++++++++++++++++++
5 files changed, 429 insertions(+), 5 deletions(-)

diff --git a/src/router/index.js b/src/router/index.js @@ -1,6 +1,7 @@ -import Vue from 'vue' -import VueRouter from 'vue-router' -import Home from '../views/Home.vue' +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import Home from '../views/Home.vue'; +import PaceCalculator from '../views/PaceCalculator.vue'; Vue.use(VueRouter); @@ -14,10 +15,19 @@ const routes = [ name: 'home', component: Home, }, + { + path: '/calculate', + redirect: '/home', + }, + { + path: '/calculate/paces', + name: 'calculate-paces', + component: PaceCalculator, + }, ]; const router = new VueRouter({ routes }); -export default router +export default router; diff --git a/src/utils/units.js b/src/utils/units.js @@ -10,6 +10,28 @@ const TIME_UNITS = { /** + * The time unit names + */ +const TIME_UNIT_NAMES = { + second: 'Second', + minute: 'Minute', + hour: 'Hour', +}; + + + +/** + * The time unit symbols + */ +const TIME_UNIT_SYMBOLS = { + second: 's', + minute: 'min', + hour: 'hr', +}; + + + +/** * The value of each time unit in seconds */ const TIME_UNIT_VALUES = { @@ -34,6 +56,32 @@ const DISTANCE_UNITS = { /** + * The distance unit names + */ +const DISTANCE_UNIT_NAMES = { + meter: 'Meter', + kilometer: 'Kilometer', + yard: 'Yard', + mile: 'Mile', + marathon: 'Marathon', +}; + + + +/** + * The distance unit symbols + */ +const DISTANCE_UNIT_SYMBOLS = { + meter: 'm', + kilometer: 'km', + yard: 'yd', + mile: 'mi', + marathon: 'marathon', +}; + + + +/** * The value of each distance unit in meters */ const DISTANCE_UNIT_VALUES = { @@ -58,6 +106,28 @@ const SPEED_UNITS = { /** + * 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 = { @@ -80,6 +150,28 @@ const PACE_UNITS = { /** + * The pace unit names + */ +const PACE_UNIT_NAMES = { + seconds_per_meter: 'Seconds per Meter', + minutes_per_kilometer: 'Minutes per kilometer', + minutes_per_mile: 'Minutes per Mile', +}; + + + +/** + * The pace unit symbols + */ +const PACE_UNIT_SYMBOLS = { + seconds_per_meter: 's/m', + minutes_per_kilometer: 'min/km', + minutes_per_mile: 'min/mi', +}; + + + +/** * The value of each pace unit in seconds per meter */ const PACE_UNIT_VALUES = { @@ -178,6 +270,25 @@ function convertSpeedPace(inputValue, inputUnits, outputUnits) { * @returns {String} The formatted value */ function formatDuration(value, padding=6, digits=2) { + // Check if value is NaN + if (isNaN(value)) { + return 'NaN'; + } + + // Initialize result + let result = ''; + + // Check value sign + if (value < 0) { + result += '-'; + value = Math.abs(value); + } + + // Check if value is valid + if (value === Infinity) { + return result + 'Infinity'; + } + // Validate padding padding = Math.min(padding, 6); @@ -187,7 +298,6 @@ function formatDuration(value, padding=6, digits=2) { let seconds = value % 60; // Format parts - let result = ''; if (hours !== 0 || padding >= 5) { result += hours.toString().padStart(padding - 4, '0'); result += ':'; @@ -215,6 +325,16 @@ export default { 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, diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue @@ -0,0 +1,210 @@ +<template> + <div class="calc-pace"> + <div class="input"> + Running + <decimal-input v-model="inputDistance" :min="0" :digits="2"/> + <select v-model="inputUnit"> + <option v-for="(value, key) in distanceUnits" :key="key" :value="key"> + {{ value }}(s) + </option> + </select> + in + <time-input v-model="inputTime"/> + </div> + + <p>is the same pace as running</p> + + <table class="output"> + <thead> + <tr> + <th>Distance</th> + <th></th> + <th>Time</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 class="output-value"> + {{ formatDuration(item.time, 0, 2) }} + </td> + </tr> + </tbody> + </table> + </div> +</template> + +<script> +import paceUtils from '@/utils/paces.js'; +import unitUtils from '@/utils/units.js'; + +import DecimalInput from '@/components/DecimalInput.vue'; +import TimeInput from '@/components/TimeInput.vue'; + +export default { + name: 'Home', + + components: { + DecimalInput, + TimeInput, + }, + + data: function() { + return { + /** + * The input distance value + */ + inputDistance: 1, + + /** + * The input distance unit + */ + inputUnit: 'mile', + + /** + * The input time value + */ + inputTime: 10 * 60, + + /** + * 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, + + /** + * The output targets + */ + targets: [ + { distanceValue: 100, distanceUnit: 'meter' }, + { distanceValue: 200, distanceUnit: 'meter' }, + { distanceValue: 300, distanceUnit: 'meter' }, + { distanceValue: 400, distanceUnit: 'meter' }, + { distanceValue: 600, distanceUnit: 'meter' }, + { distanceValue: 800, distanceUnit: 'meter' }, + { distanceValue: 1000, distanceUnit: 'meter' }, + { distanceValue: 1200, distanceUnit: 'meter' }, + { distanceValue: 1500, distanceUnit: 'meter' }, + { distanceValue: 1600, distanceUnit: 'meter' }, + { distanceValue: 3200, distanceUnit: 'meter' }, + + { distanceValue: 1, distanceUnit: 'mile' }, + { distanceValue: 2, distanceUnit: 'mile' }, + { distanceValue: 3, distanceUnit: 'mile' }, + { distanceValue: 5, distanceUnit: 'mile' }, + { distanceValue: 10, distanceUnit: 'mile' }, + + { distanceValue: 2, distanceUnit: 'kilometer' }, + { distanceValue: 3, distanceUnit: 'kilometer' }, + { distanceValue: 4, distanceUnit: 'kilometer' }, + { distanceValue: 5, distanceUnit: 'kilometer' }, + { distanceValue: 6, distanceUnit: 'kilometer' }, + { distanceValue: 8, distanceUnit: 'kilometer' }, + { distanceValue: 10, distanceUnit: 'kilometer' }, + { distanceValue: 15, distanceUnit: 'kilometer' }, + + { distanceValue: 0.5, distanceUnit: 'marathon' }, + { distanceValue: 1, distanceUnit: 'marathon' }, + ], + }; + }, + + computed: { + /** + * The input pace (in seconds per meter) + */ + pace: function() { + let distance = unitUtils.convertDistance(this.inputDistance, + this.inputUnit, unitUtils.DISTANCE_UNITS.meter); + return paceUtils.getPace(distance, this.inputTime); + }, + + /** + * The output results + */ + results: function() { + // Calculate results + let result = []; + for (let row of this.targets) { + // Convert distance into meters + let distance = unitUtils.convertDistance(row.distanceValue, + row.distanceUnit, unitUtils.DISTANCE_UNITS.meter); + + // Calculate time to travel distance at input pace + let time = paceUtils.getTime(this.pace, distance); + + // Add result + result.push({ + distanceValue: row.distanceValue, + distanceUnit: row.distanceUnit, + time: time, + }); + } + + // Sort results by time + result.sort(function(a, b){return a.time-b.time}); + + // Return results + return result; + }, + }, +} +</script> + +<style> +/* container */ +.calc-pace { + 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 */ +table { + margin-top: 10px; + border-collapse: collapse; + min-width: 300px; +} +tr { + border: 1px solid #000000; +} +th, td { + padding: 0.2em; + text-align: left; +} +.output-value { + font-weight: bold; +} +@media only screen and (max-width: 400px) { + table { + width: 100%; + } +} +</style> diff --git a/tests/unit/PaceCalculator.spec.js b/tests/unit/PaceCalculator.spec.js @@ -0,0 +1,61 @@ +import { expect } from 'chai'; +import { shallowMount } from '@vue/test-utils'; +import PaceCalculator from '@/views/PaceCalculator.vue'; + +describe('PaceCalculator.vue', () => { + it('results should be correct', async () => { + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Override input values + wrapper.setData({ + inputDistance: 1, + inputUnit: 'kilometer', + inputTime: 100, + }); + + // Override targets + await wrapper.setData({ targets: [ + { distanceValue: 10, distanceUnit: 'meter' }, + { distanceValue: 20, distanceUnit: 'meter' }, + { distanceValue: 100, distanceUnit: 'meter' }, + { distanceValue: 1, distanceUnit: 'kilometer' }, + ]}); + + // Assert results are correct + expect(wrapper.vm._computedWatchers.results.value).to.deep.equal([ + { distanceValue: 10, distanceUnit: 'meter', time: 1 }, + { distanceValue: 20, distanceUnit: 'meter', time: 2 }, + { distanceValue: 100, distanceUnit: 'meter', time: 10 }, + { distanceValue: 1, distanceUnit: 'kilometer', time: 100 }, + ]); + }); + + it('results should be sorted by time', async () => { + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Override input values + wrapper.setData({ + inputDistance: 1, + inputUnit: 'kilometer', + inputTime: 100, + }); + + // Override targets + await wrapper.setData({ targets: [ + { distanceValue: 20, distanceUnit: 'meter' }, + { distanceValue: 100, distanceUnit: 'meter' }, + { distanceValue: 1, distanceUnit: 'kilometer' }, + { distanceValue: 10, distanceUnit: 'meter' }, + ]}); + + // Assert results are correct + expect(wrapper.vm._computedWatchers.results.value).to.deep.equal([ + { distanceValue: 10, distanceUnit: 'meter', time: 1 }, + { distanceValue: 20, distanceUnit: 'meter', time: 2 }, + { distanceValue: 100, distanceUnit: 'meter', time: 10 }, + { distanceValue: 1, distanceUnit: 'kilometer', time: 100 }, + ]); + }); +}); diff --git a/tests/unit/units.spec.js b/tests/unit/units.spec.js @@ -144,5 +144,28 @@ describe('utils/units.js', () => { let result = units.formatDuration(3600 + 120 + 3 + 0.456, 0, 0); expect(result).to.equal('1:02:03'); }); + + it('should correctly format NaN', () => { + let result = units.formatDuration(NaN); + expect(result).to.equal('NaN'); + }); + + it('should correctly format +/- Infinity', () => { + let result = units.formatDuration(Infinity); + expect(result).to.equal('Infinity'); + + result = units.formatDuration(-Infinity); + expect(result).to.equal('-Infinity'); + }); + + it('should correctly format 0 when padding is 0', () => { + let result = units.formatDuration(0, 0); + expect(result).to.equal('0.00'); + }); + + it('should correctly format negative durations', () => { + let result = units.formatDuration(-3600 - 120 - 3 - 0.4); + expect(result).to.equal('-01:02:03.40'); + }); }); });