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