running-tools

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

commit e627065b86effa14786bb1b59f0f54314d8e0f4d
parent f8f62e95150a7e2ce9a4d5541c0f6c56ae52570f
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Fri, 13 Aug 2021 09:17:56 -0700

Implement unit calculator

Diffstat:
Msrc/router/index.js | 6++++++
Msrc/views/PaceCalculator.vue | 8++++----
Asrc/views/UnitCalculator.vue | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/unit/PaceCalculator.spec.js | 4++--
Atests/unit/UnitCalculator.spec.js | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 363 insertions(+), 6 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 UnitCalculator from '../views/UnitCalculator.vue'; Vue.use(VueRouter); @@ -24,6 +25,11 @@ const routes = [ name: 'calculate-paces', component: PaceCalculator, }, + { + path: '/calculate/units', + name: 'calculate-units', + component: UnitCalculator, + }, ]; const router = new VueRouter({ diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue @@ -1,5 +1,5 @@ <template> - <div class="calc-pace"> + <div class="pace-calculator"> <div class="input"> Running <decimal-input v-model="inputDistance" :min="0" :digits="2"/> @@ -48,7 +48,7 @@ import DecimalInput from '@/components/DecimalInput.vue'; import TimeInput from '@/components/TimeInput.vue'; export default { - name: 'Home', + name: 'PaceCalculator', components: { DecimalInput, @@ -166,9 +166,9 @@ export default { } </script> -<style> +<style scoped> /* container */ -.calc-pace { +.pace-calculator { display: flex; flex-direction: column; align-items: center; diff --git a/src/views/UnitCalculator.vue b/src/views/UnitCalculator.vue @@ -0,0 +1,253 @@ +<template> + <div class="unit-calculator"> + <select class="category" v-model="category"> + <option value="distance">Distance</option> + <option value="time">Time</option> + <option value="speed_and_pace">Speed & Pace</option> + </select> + + <time-input v-if="getUnitType(inputUnit) === 'time'" class="input-value" + v-model="inputValue"/> + <decimal-input v-else class="input-value" v-model="inputValue" :min="0" + :digits="2"/> + + <select v-model="inputUnit" class="input-units"> + <option v-for="(value, key) in unitNames" :key="key" :value="key"> + {{ value }} + </option> + </select> + + <span class="equals"> = </span> + + <span v-if="getUnitType(outputUnit) === 'time' "class="output-value"> + {{ formatDuration(outputValue) }} + </span> + <span v-else class="output-value"> + {{ outputValue.toFixed(2) }} + </span> + + <select v-model="outputUnit" class="output-units"> + <option v-for="(value, key) in unitNames" :key="key" :value="key"> + {{ value }} + </option> + </select> + </div> +</template> + +<script> +import unitUtils from '@/utils/units.js'; + +import DecimalInput from '@/components/DecimalInput.vue'; +import TimeInput from '@/components/TimeInput.vue'; + +export default { + name: 'UnitCalculator', + + components: { + DecimalInput, + TimeInput, + }, + + data: function() { + return { + /** + * The input value + */ + inputValue: 1.0, + + /** + * The unit of the input + */ + inputUnit: 'miles', + + /** + * The unit of the output + */ + outputUnit: 'meters', + + /** + * The unit category + */ + category: 'distance', + + /** + * The formatDuration method + */ + formatDuration: unitUtils.formatDuration, + }; + }, + + computed: { + /** + * The names of the units in the current category + */ + unitNames: function() { + if (this.category === 'distance') { + return unitUtils.DISTANCE_UNIT_NAMES; + } + else if (this.category === 'time') { + return {...unitUtils.TIME_UNIT_NAMES, 'hh:mm:ss': 'hh:mm:ss'}; + } + else if (this.category === 'speed_and_pace') { + return {...unitUtils.PACE_UNIT_NAMES, ...unitUtils.SPEED_UNIT_NAMES}; + } + }, + + /** + * The output value + */ + outputValue: function() { + if (this.category === 'distance') { + return unitUtils.convertDistance(this.inputValue, this.inputUnit, + this.outputUnit); + } + else if (this.category === 'time') { + // Correct input and output units for 'hh:mm:ss' unit + let realInput, realOutput; + if (this.inputUnit === 'hh:mm:ss') { + realInput = unitUtils.TIME_UNITS.seconds; + } + else { + realInput = this.inputUnit; + } + if (this.outputUnit === 'hh:mm:ss') { + realOutput = unitUtils.TIME_UNITS.seconds; + } + else { + realOutput = this.outputUnit; + } + + // Calculate conversion + return unitUtils.convertTime(this.inputValue, realInput, realOutput); + } + else if (this.category === 'speed_and_pace') { + return unitUtils.convertSpeedPace(this.inputValue, this.inputUnit, + this.outputUnit); + } + }, + }, + + watch: { + /** + * Reset inputValue, inputUnit, and outputUnit + */ + category: function(newValue) { + if (newValue === 'distance') { + this.inputValue = 1; + this.inputUnit = 'miles'; + this.outputUnit = 'meters'; + } + else if (newValue === 'time') { + this.inputValue = 1; + this.inputUnit = 'seconds'; + this.outputUnit = 'hh:mm:ss'; + } + else if (newValue === 'speed_and_pace') { + this.inputValue = 1; + this.inputUnit = 'miles_per_hour'; + this.outputUnit = 'seconds_per_mile'; + } + }, + }, + + methods: { + /** + * Get the type of a unit + * @param {String} unit The unit + * @returns {String} The type ('decimal' or 'time') + */ + getUnitType: function(unit) { + if (unit in unitUtils.DISTANCE_UNITS) { + return 'decimal'; + } + else if (unit in unitUtils.TIME_UNITS) { + return 'decimal'; + } + else if (unit === 'hh:mm:ss') { + return 'time'; + } + else if (['seconds_per_kilometer', 'seconds_per_mile'].includes(unit)) { + return 'time'; + } + else { + return 'decimal'; + } + }, + }, +}; +</script> + +<style scoped> +.unit-calculator { + margin: 0px auto; + display: grid; + grid-template-columns: 1fr auto 1fr; + grid-template-rows: auto auto auto; + width: 450px; + grid-gap: 0.2em; +} +.unit-calculator .category { + grid-row: 1; + grid-column: 1 / 4; +} +.unit-calculator .input-value { + grid-row: 2; + grid-column: 1; + + width: 100%; + text-align: center; +} +.unit-calculator .input-units { + grid-row: 3; + grid-column: 1; +} +.unit-calculator .equals { + grid-row: 2 / 4; + grid-column: 2; + + text-align: center; + padding: 0em 0.5em; + font-size: 2em; +} +.unit-calculator .output-value { + grid-row: 2; + grid-column: 3; + + width: 100%; + text-align: center; +} +.unit-calculator .output-units { + grid-row: 3; + grid-column: 3; +} + +@media only screen and (max-width: 500px) { + /* switch to mobile friendly layout */ + .unit-calculator { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto auto auto auto; + width: 100%; + } + .unit-calculator * { + grid-column: 1 !important; + } + .unit-calculator .category { + grid-row: 1; + } + .unit-calculator .input-value { + grid-row: 2; + } + .unit-calculator .input-units { + grid-row: 3; + } + .unit-calculator .equals { + grid-row: 4; + } + .unit-calculator .output-value { + grid-row: 5; + } + .unit-calculator .output-units { + grid-row: 6; + } +} +</style> diff --git a/tests/unit/PaceCalculator.spec.js b/tests/unit/PaceCalculator.spec.js @@ -8,7 +8,7 @@ describe('PaceCalculator.vue', () => { const wrapper = shallowMount(PaceCalculator); // Override input values - wrapper.setData({ + await wrapper.setData({ inputDistance: 1, inputUnit: 'kilometers', inputTime: 100, @@ -36,7 +36,7 @@ describe('PaceCalculator.vue', () => { const wrapper = shallowMount(PaceCalculator); // Override input values - wrapper.setData({ + await wrapper.setData({ inputDistance: 1, inputUnit: 'kilometers', inputTime: 100, diff --git a/tests/unit/UnitCalculator.spec.js b/tests/unit/UnitCalculator.spec.js @@ -0,0 +1,98 @@ +import { expect } from 'chai'; +import { shallowMount } from '@vue/test-utils'; +import UnitCalculator from '@/views/UnitCalculator.vue'; + +describe('UnitCalculator.vue', () => { + it('should correctly update controls when category changes', async () => { + // Initialize component + const wrapper = shallowMount(UnitCalculator); + + // Change category + await wrapper.setData({ category: 'time' }); + + // Assert controls are correct + expect(wrapper.vm._data.inputValue).to.equal(1); + expect(wrapper.vm._data.inputUnit).to.equal('seconds'); + expect(wrapper.vm._data.outputUnit).to.equal('hh:mm:ss'); + + // Change category + 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'); + + // Change category + await wrapper.setData({ category: 'distance' }); + + // 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'); + }); + + it('outputValue should be correct', async () => { + // Initialize component + const wrapper = shallowMount(UnitCalculator); + + // Change category and update input + await wrapper.setData({ category: 'distance' }); + await wrapper.setData({ + inputValue: 2, + inputUnit: 'kilometers', + outputUnit: 'meters' + }); + + // Assert controls are correct + expect(wrapper.vm._computedWatchers.outputValue.value).to.equal(2000); + + // Change category and update input + await wrapper.setData({ category: 'time' }); + await wrapper.setData({ + inputValue: 3, + inputUnit: 'minutes', + outputUnit: 'seconds', + }); + + // Assert controls are correct + expect(wrapper.vm._computedWatchers.outputValue.value).to.equal(3 * 60); + + // Change category and update input + await wrapper.setData({ category: 'speed_and_pace' }); + await wrapper.setData({ + inputValue: 2, + inputUnit: 'miles_per_hour', + outputUnit: 'seconds_per_mile', + }); + + // Assert controls are correct + expect(wrapper.vm._computedWatchers.outputValue.value).to.be.closeTo(30 * 60, 0.001); + }); + + it('should correctly convert to and from hh:mm:ss', async () => { + // Initialize component + const wrapper = shallowMount(UnitCalculator); + + // Change category and update input + await wrapper.setData({ category: 'time' }); + await wrapper.setData({ + inputValue: 60, + inputUnit: 'hh:mm:ss', + outputUnit: 'minutes', + }); + + // Assert controls are correct + expect(wrapper.vm._computedWatchers.outputValue.value).to.equal(1); + + // Update input + await wrapper.setData({ + inputValue: 1, + inputUnit: 'minutes', + outputUnit: 'hh:mm:ss', + }); + + // Assert controls are correct + expect(wrapper.vm._computedWatchers.outputValue.value).to.equal(60); + }); +});