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:
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);
+ });
+});