commit 033fc052c51ba689bd8e44ccf370f9262f951760
parent 4c16d9764e24b6f53bb2ff8afb813f75f884246c
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sat, 12 Jul 2025 14:36:28 -0700
Rename src/utils/ to src/core/
In addition, combine misc and storage utils into src/core/utils.ts and
rename races.ts to racePrediction.ts.
Diffstat:
43 files changed, 2771 insertions(+), 2752 deletions(-)
diff --git a/src/components/AdvancedOptionsInput.vue b/src/components/AdvancedOptionsInput.vue
@@ -44,10 +44,10 @@
</template>
<script setup lang="ts">
-import { Calculators } from '@/utils/calculators';
-import type { StandardOptions, RaceOptions, WorkoutOptions } from '@/utils/calculators';
-import type { TargetSets } from '@/utils/targets';
-import { UnitSystems } from '@/utils/units';
+import { Calculators } from '@/core/calculators';
+import type { StandardOptions, RaceOptions, WorkoutOptions } from '@/core/calculators';
+import type { TargetSets } from '@/core/targets';
+import { UnitSystems } from '@/core/units';
import DecimalInput from '@/components/DecimalInput.vue';
import TargetSetSelector from '@/components/TargetSetSelector.vue';
diff --git a/src/components/DecimalInput.vue b/src/components/DecimalInput.vue
@@ -4,7 +4,7 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
-import { formatNumber } from '@/utils/units';
+import { formatNumber } from '@/core/units';
/**
* The component value
diff --git a/src/components/DoubleOutputTable.vue b/src/components/DoubleOutputTable.vue
@@ -29,11 +29,11 @@
<script setup lang="ts">
import { computed } from 'vue';
-import { ResultType } from '@/utils/calculators';
-import type { TargetResult } from '@/utils/calculators';
-import type { Target } from '@/utils/targets';
-import { formatDistance, formatDuration } from '@/utils/units';
-import type { Distance, DistanceTime } from '@/utils/units';
+import { ResultType } from '@/core/calculators';
+import type { TargetResult } from '@/core/calculators';
+import type { Target } from '@/core/targets';
+import { formatDistance, formatDuration } from '@/core/units';
+import type { Distance, DistanceTime } from '@/core/units';
interface Props {
/**
diff --git a/src/components/PaceInput.vue b/src/components/PaceInput.vue
@@ -18,8 +18,8 @@
</template>
<script setup lang="ts">
-import { DistanceUnits, DistanceUnitData } from '@/utils/units';
-import type { DistanceTime } from '@/utils/units';
+import { DistanceUnits, DistanceUnitData } from '@/core/units';
+import type { DistanceTime } from '@/core/units';
import DecimalInput from '@/components/DecimalInput.vue';
import TimeInput from '@/components/TimeInput.vue';
diff --git a/src/components/RaceOptionsInput.vue b/src/components/RaceOptionsInput.vue
@@ -18,7 +18,7 @@
</template>
<script setup lang="ts">
-import type { RaceOptions } from '@/utils/calculators';
+import type { RaceOptions } from '@/core/calculators';
import DecimalInput from '@/components/DecimalInput.vue';
import useObjectModel from '@/composables/useObjectModel';
diff --git a/src/components/SingleOutputTable.vue b/src/components/SingleOutputTable.vue
@@ -39,8 +39,8 @@
<script setup lang="ts">
import { computed } from 'vue';
-import type { TargetResult } from '@/utils/calculators';
-import type { Target } from '@/utils/targets';
+import type { TargetResult } from '@/core/calculators';
+import type { Target } from '@/core/targets';
interface Props {
/**
diff --git a/src/components/SplitOutputTable.vue b/src/components/SplitOutputTable.vue
@@ -46,10 +46,10 @@
<script setup lang="ts">
import { computed } from 'vue';
-import type { SplitTarget } from '@/utils/targets';
+import type { SplitTarget } from '@/core/targets';
import { DistanceUnits, UnitSystems, convertDistance, formatDistance, formatDuration,
- formatPace, getDefaultPaceUnit } from '@/utils/units';
-import type { Distance, DistanceTime } from '@/utils/units';
+ formatPace, getDefaultPaceUnit } from '@/core/units';
+import type { Distance, DistanceTime } from '@/core/units';
import TimeInput from '@/components/TimeInput.vue';
import useObjectModel from '@/composables/useObjectModel';
diff --git a/src/components/TargetEditor.vue b/src/components/TargetEditor.vue
@@ -90,10 +90,10 @@
<script setup lang="ts">
import VueFeather from 'vue-feather';
-import { Calculators } from '@/utils/calculators';
-import { TargetTypes, workoutTargetToString } from '@/utils/targets';
-import type { StandardTargetSet, TargetSet, WorkoutTarget, WorkoutTargetSet } from '@/utils/targets';
-import { DistanceUnitData, UnitSystems, getDefaultDistanceUnit } from '@/utils/units';
+import { Calculators } from '@/core/calculators';
+import { TargetTypes, workoutTargetToString } from '@/core/targets';
+import type { StandardTargetSet, TargetSet, WorkoutTarget, WorkoutTargetSet } from '@/core/targets';
+import { DistanceUnitData, UnitSystems, getDefaultDistanceUnit } from '@/core/units';
import DecimalInput from '@/components/DecimalInput.vue';
import TimeInput from '@/components/TimeInput.vue';
diff --git a/src/components/TargetSetSelector.vue b/src/components/TargetSetSelector.vue
@@ -25,11 +25,11 @@ import { computed, nextTick, ref } from 'vue';
import VueFeather from 'vue-feather';
-import { Calculators } from '@/utils/calculators';
-import { deepCopy } from '@/utils/misc';
-import { sort, defaultTargetSets } from '@/utils/targets';
-import type { TargetSet, TargetSets } from '@/utils/targets';
-import { UnitSystems } from '@/utils/units';
+import { Calculators } from '@/core/calculators';
+import { sort, defaultTargetSets } from '@/core/targets';
+import type { TargetSet, TargetSets } from '@/core/targets';
+import { deepCopy } from '@/core/utils';
+import { UnitSystems } from '@/core/units';
import TargetEditor from '@/components/TargetEditor.vue';
import useObjectModel from '@/composables/useObjectModel';
diff --git a/src/composables/useObjectModel.ts b/src/composables/useObjectModel.ts
@@ -1,7 +1,7 @@
import { ref, watch } from 'vue';
import type { Ref } from 'vue';
-import { deepCopy, deepEqual } from '@/utils/misc';
+import { deepCopy, deepEqual } from '@/core/utils';
/*
* Generate an internal ref that implements support for v-model with objects
diff --git a/src/composables/useStorage.ts b/src/composables/useStorage.ts
@@ -1,7 +1,7 @@
import { ref, onActivated, watchEffect } from 'vue';
import type { Ref } from 'vue';
-import * as storage from '@/utils/storage';
+import { deepCopy, getLocalStorage, setLocalStorage } from '@/core/utils';
/*
* Create a reactive value that is synced with a localStorage item
@@ -10,12 +10,12 @@ import * as storage from '@/utils/storage';
* @returns {Ref<Type>} The synchronized ref
*/
export default function useStorage<Type>(key: string, defaultValue: Type): Ref<Type> {
- const clonedDefault: Type = JSON.parse(JSON.stringify(defaultValue));
+ const clonedDefault: Type = deepCopy(defaultValue);
const value: Ref<Type> = ref<Type>(clonedDefault) as Ref<Type>;
// (Re)load value from localStorage
function updateValue() {
- const parsedValue = storage.get<Type>(key);
+ const parsedValue = getLocalStorage<Type>(key);
if (parsedValue !== null) value.value = parsedValue;
}
updateValue();
@@ -24,7 +24,7 @@ export default function useStorage<Type>(key: string, defaultValue: Type): Ref<T
// Save value to localStorage when modified
watchEffect(() => {
if (typeof localStorage !== 'undefined') {
- storage.set<Type>(key, value.value);
+ setLocalStorage<Type>(key, value.value);
}
})
diff --git a/src/core/calculators.ts b/src/core/calculators.ts
@@ -0,0 +1,247 @@
+/*
+ * Contains types and functions for core calculator functionality
+ */
+
+import * as racePrediction from '@/core/racePrediction';
+import { TargetTypes, workoutTargetToString } from '@/core/targets';
+import type { StandardTarget, WorkoutTarget } from '@/core/targets';
+import { DistanceUnits, UnitSystems, convertDistance, formatDistance, formatDuration, formatPace,
+ getDefaultDistanceUnit, getDefaultPaceUnit } from '@/core/units';
+import type { DistanceTime } from '@/core/units';
+
+/*
+ * The four main calculators (batch and unit calculators not included)
+ *
+ * Used to determine available options and target set format
+ */
+export enum Calculators {
+ Pace = 'pace',
+ Race = 'race',
+ Split = 'split',
+ Workout = 'workout',
+}
+
+/*
+ * The type for the available race statistics
+ */
+export interface RaceStats {
+ purdyPoints: number,
+ vo2Max: number,
+ vo2: number,
+ vo2MaxPercentage: number,
+};
+
+/*
+ * The type for the options specific to each calculator
+ */
+export interface StandardOptions {
+ selectedTargetSet: string,
+}
+export interface RaceOptions extends StandardOptions {
+ model: racePrediction.RacePredictionModel,
+ riegelExponent: number,
+};
+export interface WorkoutOptions extends RaceOptions {
+ customTargetNames: boolean,
+};
+export interface BatchOptions {
+ calculator: Calculators.Pace | Calculators.Race | Calculators.Workout,
+ increment: number,
+ rows: number,
+};
+
+/*
+ * The two possible result fields of a target result: "key" and "value"
+ */
+export enum ResultType {
+ Key = 'key',
+ Value = 'value',
+};
+
+/*
+ * The type for target results
+ */
+export interface TargetResult {
+ key: string,
+ value: string,
+ pace: string,
+ result: ResultType,
+ sort: number,
+};
+
+/*
+ * The default input and options for each calculator
+ */
+export const defaultInput: DistanceTime = {
+ distanceValue: 5,
+ distanceUnit: DistanceUnits.Kilometers,
+ time: 1200,
+};
+export const defaultBatchOptions: BatchOptions = {
+ calculator: Calculators.Workout,
+ increment: 15,
+ rows: 20,
+};
+export const defaultPaceOptions: StandardOptions = {
+ selectedTargetSet: '_pace_targets',
+};
+export const defaultRaceOptions: RaceOptions = {
+ model: racePrediction.RacePredictionModel.AverageModel,
+ riegelExponent: 1.06,
+ selectedTargetSet: '_race_targets',
+};
+export const defaultSplitOptions: StandardOptions = {
+ selectedTargetSet: '_split_targets',
+};
+export const defaultWorkoutOptions: WorkoutOptions = {
+ customTargetNames: false,
+ ...defaultRaceOptions,
+ selectedTargetSet: '_workout_targets',
+};
+
+/**
+ * Calculate results for a standard target
+ * @param {DistanceTime} input The input pace
+ * @param {StandardTarget} target The standard target
+ * @param {Function} calculateTime The function for calculating time results
+ * @param {Function} calculateDistance The function for calculating distance results
+ * @param {UnitSystems} defaultUnitSystem The default unit system (imperial or metric)
+ * @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
+ * @returns {TargetResult} The result
+ */
+function calculateStandardResult(input: DistanceTime, target: StandardTarget,
+ calculateTime: (d1: number, t1: number, d2: number) => number,
+ calculateDistance: (t1: number, d1: number, t2: number) => number, defaultUnitSystem: UnitSystems,
+ preciseDurations: boolean = true): TargetResult {
+
+ let distanceValue, distanceUnit, time;
+ const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
+ if (target.type === TargetTypes.Distance) {
+ // Add target distance to result
+ distanceValue = target.distanceValue;
+ distanceUnit = target.distanceUnit;
+
+ // Calculate time result
+ const d2 = convertDistance(target.distanceValue, target.distanceUnit, DistanceUnits.Meters);
+ time = calculateTime(d1, input.time, d2);
+ } else {
+ // Add target time to result
+ time = target.time;
+
+ // Calculate distance result
+ const d2 = calculateDistance(input.time, d1, target.time);
+ const units = getDefaultDistanceUnit(defaultUnitSystem);
+ distanceValue = convertDistance(d2, DistanceUnits.Meters, units);
+ distanceUnit = units;
+ }
+
+ return {
+ // Convert distance to key string
+ key: formatDistance({ distanceValue, distanceUnit }, target.type === TargetTypes.Time),
+
+ // Convert time to time string
+ value: formatDuration(time, 3, preciseDurations ? 2 : 0, target.type === TargetTypes.Distance),
+
+ // Convert pace to pace string
+ pace: formatPace({ time, distanceValue, distanceUnit }, getDefaultPaceUnit(defaultUnitSystem)),
+
+ // Convert dist/time result to key/value
+ result: target.type === TargetTypes.Distance ? ResultType.Value : ResultType.Key,
+
+ // Use time (in seconds) as sort key
+ sort: time,
+ };
+}
+
+/**
+ * Calculate paces from a target
+ * @param {DistanceTime} input The input pace
+ * @param {StandardTarget} target The pace target
+ * @param {UnitSystems} defaultUnitSystem The default unit system (imperial or metric)
+ * @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
+ * @returns {TargetResult} The result
+ */
+export function calculatePaceResults(input: DistanceTime, target: StandardTarget,
+ defaultUnitSystem: UnitSystems,
+ preciseDurations: boolean = true): TargetResult {
+
+ return calculateStandardResult(input, target, (d1, t1, d2) => ((t1 / d1) * d2),
+ (t1, d1, t2) => ((d1 / t1) * t2), defaultUnitSystem, preciseDurations);
+}
+
+/**
+ * Predict race results from a target
+ * @param {DistanceTime} input The input race
+ * @param {StandardTarget} target The race target
+ * @param {RaceOptions} options The race prediction options
+ * @param {UnitSystems} defaultUnitSystem The default unit system (imperial or metric)
+ * @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
+ * @returns {TargetResult} The result
+ */
+export function calculateRaceResults(input: DistanceTime, target: StandardTarget,
+ options: RaceOptions, defaultUnitSystem: UnitSystems,
+ preciseDurations: boolean = true): TargetResult {
+
+ return calculateStandardResult(input, target,
+ (d1, t1, d2) => racePrediction.predictTime(d1, t1, d2, options.model, options.riegelExponent),
+ (t1, d1, t2) => racePrediction.predictDistance(t1, d1, t2, options.model, options.riegelExponent),
+ defaultUnitSystem, preciseDurations);
+}
+
+/**
+ * Calculate race statistics from an input race
+ * @param {DistanceTime} input The input race
+ * @returns {RaceStats} The race statistics
+ */
+export function calculateRaceStats(input: DistanceTime): RaceStats {
+ const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
+
+ return {
+ purdyPoints: racePrediction.getPurdyPoints(d1, input.time),
+ vo2Max: racePrediction.getVO2Max(d1, input.time),
+ vo2: racePrediction.getVO2(d1, input.time),
+ vo2MaxPercentage: racePrediction.getVO2Percentage(input.time) * 100,
+ }
+}
+
+/**
+ * Predict workout results from a target
+ * @param {DistanceTime} input The input race
+ * @param {WorkoutTarget} target The workout target
+ * @param {WorkoutOptions} options The workout options
+ * @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
+ * @returns {TargetResult} The result
+ */
+export function calculateWorkoutResults(input: DistanceTime, target: WorkoutTarget,
+ options: WorkoutOptions,
+ preciseDurations: boolean = true): TargetResult {
+ // Initialize distance and time variables
+ const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
+ const t1 = input.time;
+ const d3 = convertDistance(target.splitValue, target.splitUnit, DistanceUnits.Meters);
+ let d2, t2;
+
+ // Calculate result
+ if (target.type === 'distance') {
+ // Convert target distance into meters
+ d2 = convertDistance(target.distanceValue, target.distanceUnit, DistanceUnits.Meters);
+
+ // Get workout split prediction
+ t2 = racePrediction.predictTime(d1, input.time, d2, options.model, options.riegelExponent);
+ } else {
+ t2 = target.time;
+
+ // Get workout split prediction
+ d2 = racePrediction.predictDistance(t1, d1, t2, options.model, options.riegelExponent);
+ }
+ const t3 = (t2 / d2) * d3;
+
+ // Return result
+ return {
+ key: (options.customTargetNames && target.customName) || workoutTargetToString(target),
+ value: formatDuration(t3, 3, preciseDurations ? 2 : 0, true),
+ pace: '', // Pace not used in workout calculator
+ result: ResultType.Value,
+ sort: t3,
+ }
+}
diff --git a/src/core/racePrediction.ts b/src/core/racePrediction.ts
@@ -0,0 +1,491 @@
+/*
+ * Implements various race prediction models
+ */
+
+/*
+ * The available race prediction models
+ */
+export enum RacePredictionModel {
+ AverageModel = 'AverageModel',
+ PurdyPointsModel = 'PurdyPointsModel',
+ VO2MaxModel = 'VO2MaxModel',
+ RiegelModel = 'RiegelModel',
+ CameronModel = 'CameronModel',
+};
+
+/*
+ * The type for internal variables used by the Purdy Points race prediction model
+ */
+interface PurdyPointsVariables {
+ twsec: number,
+ a: number,
+ b: number,
+};
+
+/**
+ * Estimate the point at which a function returns a target value using Newton's Method
+ * @param {number} initialEstimate The initial estimate
+ * @param {number} target The target function output
+ * @param {Function} method The function
+ * @param {Function} derivative The function derivative
+ * @param {number} precision The acceptable precision
+ * @returns {number} The refined estimate
+ */
+function NewtonsMethod(initialEstimate: number, target: number, method: (x: number) => number,
+ derivative: (x: number) => number, precision: number): number {
+ // Initialize estimate
+ let estimate = initialEstimate;
+ let estimateValue;
+
+ for (let i = 0; i < 500; i += 1) {
+ // Evaluate function at estimate
+ estimateValue = method(estimate);
+
+ // Check if estimate is close enough (usually occurs way before i = 500)
+ if (Math.abs(target - estimateValue) < precision) {
+ break;
+ }
+
+ // Refine estimate
+ estimate -= (estimateValue - target) / derivative(estimate);
+ }
+
+ // Return refined estimate
+ return estimate;
+}
+
+/*
+ * Methods that implement the Purdy Points race prediction model
+ * https://www.cs.uml.edu/~phoffman/xcinfo3.html
+ */
+const PurdyPointsModel = {
+ /**
+ * Calculate the Purdy Point variables for a distance
+ * @param {number} d The distance in meters
+ * @returns {PurdyPointsVariables} The Purdy Point variables
+ */
+ getVariables(d: number): PurdyPointsVariables {
+ // Declare constants
+ const c1 = 11.15895;
+ const c2 = 4.304605;
+ const c3 = 0.5234627;
+ const c4 = 4.031560;
+ const c5 = 2.316157;
+ const r1 = 3.796158e-2;
+ const r2 = 1.646772e-3;
+ const r3 = 4.107670e-4;
+ const r4 = 7.068099e-6;
+ const r5 = 5.220990e-9;
+
+ // Calculate world record velocity from running curve
+ const v = (-c1 * Math.exp(-r1 * d))
+ + (c2 * Math.exp(-r2 * d))
+ + (c3 * Math.exp(-r3 * d))
+ + (c4 * Math.exp(-r4 * d))
+ + (c5 * Math.exp(-r5 * d));
+
+ // Calculate world record time
+ const twsec = d / v;
+
+ // Calculate constants
+ const k = 0.0654 - (0.00258 * v);
+ const a = 85 / k;
+ const b = 1 - (1035 / a);
+
+ // Return Purdy Point variables
+ return {
+ twsec,
+ a,
+ b,
+ };
+ },
+
+ /**
+ * Get the Purdy Points for a race
+ * @param {number} d The distance of the race in meters
+ * @param {number} t The finish time of the race in seconds
+ * @returns {number} The Purdy Points for the race
+ */
+ getPurdyPoints(d: number, t: number): number {
+ // Get variables
+ const variables = PurdyPointsModel.getVariables(d);
+
+ // Calculate Purdy Points
+ const points = variables.a * ((variables.twsec / t) - variables.b);
+
+ // Return Purdy Points
+ return points;
+ },
+
+ /**
+ * Predict a race time using the Purdy Points Model
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d2 The distance of the output race in meters
+ * @returns {number} The predicted time for the output race in seconds
+ */
+ predictTime(d1: number, t1: number, d2: number): number {
+ // Calculate Purdy Points for distance 1
+ const points = PurdyPointsModel.getPurdyPoints(d1, t1);
+
+ // Calculate time for distance 2
+ const variables = PurdyPointsModel.getVariables(d2);
+ const seconds = (variables.a * variables.twsec) / (points + (variables.a * variables.b));
+
+ // Return predicted time
+ return seconds;
+ },
+
+ /**
+ * Calculate the derivative with respect to distance of the Purdy Points curve at a specific point
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d2 The distance of the output race in meters
+ * @return {number} The derivative with respect to distance
+ */
+ derivative(d1: number, t1: number, d2: number): number {
+ const result = (85 * d2) / (((2316157 * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000
+ + (100789 * Math.exp(-(7068099 * d2) / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767
+ * d2) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000
+ - (223179 * Math.exp(-(1898079 * d2) / 50000000)) / 20000) * (327 / 5000 - (129 * ((2316157
+ * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
+ / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
+ + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
+ * d2) / 50000000)) / 20000)) / 50000) * ((85 * (1 - (207 * (327 / 5000 - (129 * ((2316157
+ * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
+ / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
+ + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
+ * d2) / 50000000)) / 20000)) / 50000)) / 17)) / (327 / 5000 - (129 * ((2316157
+ * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
+ / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
+ + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
+ * d2) / 50000000)) / 20000)) / 50000) + (85 * (d1 / (((2316157 * Math.exp(-(522099 * d1)
+ / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000
+ + (5234627 * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693
+ * d1) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000) * t1)
+ + (207 * (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000))
+ / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627
+ * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1)
+ / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000))
+ / 17 - 1)) / (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000))
+ / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627
+ * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1)
+ / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000)));
+ return result;
+ },
+
+ /**
+ * Predict a race distance using the Purdy Points Model
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t2 The finish time of the output race in seconds
+ * @returns {number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1: number, d1: number, t2: number): number {
+ // Initialize estimate
+ let estimate = (d1 * t2) / t1;
+
+ // Refine estimate (derivative on its own is too slow)
+ const method = (x: number) => PurdyPointsModel.predictTime(d1, t1, x);
+ const derivative = (x: number) => PurdyPointsModel.derivative(d1, t1, x) / 500;
+ estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01);
+
+ // Return estimate
+ return estimate;
+ },
+};
+
+/*
+ * Methods that implement the VO2 Max race prediction model
+ * http://run-down.com/statistics/calcs_explained.php
+ * https://vdoto2.com/Calculator
+ */
+const VO2MaxModel = {
+ /**
+ * Calculate the VO2 of a runner during a race
+ * @param {number} d The race distance in meters
+ * @param {number} t The finish time in seconds
+ * @returns {number} The VO2
+ */
+ getVO2(d: number, t: number): number {
+ const minutes = t / 60;
+ const v = d / minutes;
+ const result = -4.6 + (0.182258 * v) + (0.000104 * (v ** 2));
+ return result;
+ },
+
+ /**
+ * Calculate the percentage of VO2 max a runner is at during a race
+ * @param {number} t The race time in seconds
+ * @returns {number} The percentage of VO2 max
+ */
+ getVO2Percentage(t: number): number {
+ const minutes = t / 60;
+ const result = 0.8 + (0.189439 * Math.exp(-0.012778 * minutes)) + (0.298956 * Math.exp(-0.193261
+ * minutes));
+ return result;
+ },
+
+ /**
+ * Calculate a runner's VO2 max from a race result
+ * @param {number} d The race distance in meters
+ * @param {number} t The finish time in seconds
+ * @returns {number} The runner's VO2 max
+ */
+ getVO2Max(d: number, t: number): number {
+ const result = VO2MaxModel.getVO2(d, t) / VO2MaxModel.getVO2Percentage(t);
+ return result;
+ },
+
+ /**
+ * Calculate the derivative with respect to time of the VO2 max curve at a specific point
+ * @param {number} d The race distance in meters
+ * @param {number} t The finish time in seconds
+ * @return {number} The derivative with respect to time
+ */
+ VO2MaxTimeDerivative(d: number, t: number): number {
+ const result = (-(273 * d) / (25 * (t ** 2)) - (468 * (d ** 2)) / (625 * (t ** 3))) / ((189
+ * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5)
+ - (((273 * d) / (25 * t) + (234 * (d ** 2)) / (625 * (t ** 2)) - 23 / 5) * (-(63
+ * Math.exp(-(2 * t) / 9375)) / 1562500 - (57707 * Math.exp(-(193 * t) / 60000)) / 60000000))
+ / (((189 * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000
+ + 4 / 5) ** 2);
+ return result;
+ },
+
+ /**
+ * Predict a race time using the VO2 Max Model
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d2 The distance of the output race in meters
+ * @returns {number} The predicted time for the output race in seconds
+ */
+ predictTime(d1: number, t1: number, d2: number): number {
+ // Calculate input VO2 max
+ const inputVO2Max = VO2MaxModel.getVO2Max(d1, t1);
+
+ // Initialize estimate
+ let estimate = (t1 * d2) / d1;
+
+ // Refine estimate
+ const method = (x: number) => VO2MaxModel.getVO2Max(d2, x);
+ const derivative = (x: number) => VO2MaxModel.VO2MaxTimeDerivative(d2, x);
+ estimate = NewtonsMethod(estimate, inputVO2Max, method, derivative, 0.0001);
+
+ // Return estimate
+ return estimate;
+ },
+
+ /**
+ * Calculate the derivative with respect to distance of the VO2 max curve at a specific point
+ * @param {number} d The race distance in meters
+ * @param {number} t The finish time in seconds
+ * @return {number} The derivative with respect to distance
+ */
+ VO2MaxDistanceDerivative(d: number, t: number): number {
+ const result = ((468 * d) / (625 * (t ** 2)) + 273 / (25 * t)) / ((189 * Math.exp(-(2 * t)
+ / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5);
+ return result;
+ },
+
+ /**
+ * Predict a race distance using the VO2 Max Model
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t2 The finish time of the output race in seconds
+ * @returns {number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1: number, d1: number, t2: number): number {
+ // Calculate input VO2 max
+ const inputVO2 = VO2MaxModel.getVO2Max(d1, t1);
+
+ // Initialize estimate
+ let estimate = (d1 * t2) / t1;
+
+ // Refine estimate
+ const method = (x: number) => VO2MaxModel.getVO2Max(x, t2);
+ const derivative = (x: number) => VO2MaxModel.VO2MaxDistanceDerivative(x, t2);
+ estimate = NewtonsMethod(estimate, inputVO2, method, derivative, 0.0001);
+
+ // Return estimate
+ return estimate;
+ },
+};
+
+/*
+ * Methods that implement Dave Cameron's race prediction model
+ * https://www.cs.uml.edu/~phoffman/cammod.html
+ */
+const CameronModel = {
+ /**
+ * Predict a race time using Dave Cameron's Model
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d2 The distance of the output race in meters
+ * @returns {number} The predicted time for the output race in seconds
+ */
+ predictTime(d1: number, t1: number, d2: number): number {
+ const a = 13.49681 - (0.000030363 * d1) + (835.7114 / (d1 ** 0.7905));
+ const b = 13.49681 - (0.000030363 * d2) + (835.7114 / (d2 ** 0.7905));
+ return (t1 / d1) * (a / b) * d2;
+ },
+
+ /**
+ * Calculate the derivative with respect to distance of the Cameron curve at a specific point
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d2 The distance of the output race in meters
+ * @return {number} The derivative with respect to distance
+ */
+ derivative(d1: number, t1: number, d2: number): number {
+ const result = -(100 * (30363 * (d1 ** (3581 / 2000)) - 13496810000 * (d1 ** (1581 / 2000))
+ - 835711400000) * t1 * (134968100 * (d2 ** (3581 / 2000)) + 14963412617 * d2)) / ((d1 ** (3581
+ / 2000)) * (d2 ** (419 / 2000)) * ((30363 * (d2 ** (3581 / 2000)) - 13496810000 * (d2 ** (1581
+ / 2000)) - 835711400000) ** 2));
+ return result;
+ },
+
+ /**
+ * Predict a race distance using Dave Cameron's Model
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t2 The finish time of the output race in seconds
+ * @returns {number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1: number, d1: number, t2: number): number {
+ // Initialize estimate
+ let estimate = (d1 * t2) / t1;
+
+ // Refine estimate
+ const method = (x: number) => CameronModel.predictTime(d1, t1, x);
+ const derivative = (x: number) => CameronModel.derivative(d1, t1, x);
+ estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01);
+
+ // Return estimate
+ return estimate;
+ },
+};
+
+/*
+ * Methods that implement Pete Riegel's race prediction model
+ * https://en.wikipedia.org/wiki/Peter_Riegel
+ */
+const RiegelModel = {
+ /**
+ * Predict a race time using Pete Riegel's Model
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d2 The distance of the output race in meters
+ * @param {number} c The value of the exponent in the equation
+ * @returns {number} The predicted time for the output race in seconds
+ */
+ predictTime(d1: number, t1: number, d2: number, c: number = 1.06): number {
+ return t1 * ((d2 / d1) ** c);
+ },
+
+ /**
+ * Predict a race distance using Pete Riegel's Model
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t2 The finish time of the output race in seconds
+ * @param {number} c The value of the exponent in the equation
+ * @returns {number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1: number, d1: number, t2: number, c: number = 1.06) {
+ return d1 * ((t2 / t1) ** (1 / c));
+ },
+};
+
+/*
+ * Methods that average the results of different race prediction models
+ */
+const AverageModel = {
+ /**
+ * Predict a race time by averaging the results of different models
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d2 The distance of the output race in meters
+ * @param {number} c The value of the exponent in Pete Riegel's Model
+ * @returns {number} The predicted time for the output race in seconds
+ */
+ predictTime(d1: number, t1: number, d2: number, c: number = 1.06): number {
+ const purdy = PurdyPointsModel.predictTime(d1, t1, d2);
+ const vo2max = VO2MaxModel.predictTime(d1, t1, d2);
+ const cameron = CameronModel.predictTime(d1, t1, d2);
+ const riegel = RiegelModel.predictTime(d1, t1, d2, c);
+ return (purdy + vo2max + cameron + riegel) / 4;
+ },
+
+ /**
+ * Predict a race distance by averaging the results of different models
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t2 The finish time of the output race in seconds
+ * @param {number} c The value of the exponent in Pete Riegel's Model
+ * @returns {number} The predicted distance for the output race in meters
+ */
+ predictDistance(t1: number, d1: number, t2: number, c: number = 1.06) {
+ const purdy = PurdyPointsModel.predictDistance(t1, d1, t2);
+ const vo2max = VO2MaxModel.predictDistance(t1, d1, t2);
+ const cameron = CameronModel.predictDistance(t1, d1, t2);
+ const riegel = RiegelModel.predictDistance(t1, d1, t2, c);
+ return (purdy + vo2max + cameron + riegel) / 4;
+ },
+};
+
+/**
+ * Predict a race time
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d2 The distance of the output race in meters
+ * @param {string} model The race prediction model to use
+ * @param {number} c The value of the exponent in Pete Riegel's Model
+ */
+export function predictTime(d1: number, t1: number, d2: number,
+ model: RacePredictionModel = RacePredictionModel.AverageModel,
+ c: number = 1.06): number {
+ switch (model) {
+ default:
+ case RacePredictionModel.AverageModel:
+ return AverageModel.predictTime(d1, t1, d2, c);
+ case RacePredictionModel.PurdyPointsModel:
+ return PurdyPointsModel.predictTime(d1, t1, d2);
+ case RacePredictionModel.VO2MaxModel:
+ return VO2MaxModel.predictTime(d1, t1, d2);
+ case RacePredictionModel.RiegelModel:
+ return RiegelModel.predictTime(d1, t1, d2, c);
+ case RacePredictionModel.CameronModel:
+ return CameronModel.predictTime(d1, t1, d2);
+ }
+}
+
+/**
+ * Predict a race distance
+ * @param {number} t1 The finish time of the input race in seconds
+ * @param {number} d1 The distance of the input race in meters
+ * @param {number} t2 The finish time of the output race in seconds
+ * @param {string} model The race prediction model to use
+ * @param {number} c The value of the exponent in Pete Riegel's Model
+ */
+export function predictDistance(t1: number, d1: number, t2: number,
+ model: RacePredictionModel = RacePredictionModel.AverageModel,
+ c: number = 1.06) {
+ switch (model) {
+ default:
+ case RacePredictionModel.AverageModel:
+ return AverageModel.predictDistance(t1, d1, t2, c);
+ case RacePredictionModel.PurdyPointsModel:
+ return PurdyPointsModel.predictDistance(t1, d1, t2);
+ case RacePredictionModel.VO2MaxModel:
+ return VO2MaxModel.predictDistance(t1, d1, t2);
+ case RacePredictionModel.RiegelModel:
+ return RiegelModel.predictDistance(t1, d1, t2, c);
+ case RacePredictionModel.CameronModel:
+ return CameronModel.predictDistance(t1, d1, t2);
+ }
+}
+
+export const getPurdyPoints = PurdyPointsModel.getPurdyPoints;
+export const getVO2 = VO2MaxModel.getVO2;
+export const getVO2Percentage = VO2MaxModel.getVO2Percentage;
+export const getVO2Max = VO2MaxModel.getVO2Max;
diff --git a/src/core/targets.ts b/src/core/targets.ts
@@ -0,0 +1,211 @@
+/*
+ * Contains types and helper functions for calculator target sets
+ */
+
+import { DistanceUnits, convertDistance, formatDistance, formatDuration } from '@/core/units';
+import type { Distance } from '@/core/units';
+
+/*
+ * The two basic types of targets: those defined by distance and those defined by time
+ */
+export enum TargetTypes {
+ Distance = 'distance',
+ Time = 'time',
+};
+
+/*
+ * The types for basic standard targets and target sets used by the pace and race calculators
+ */
+interface DistanceTarget {
+ type: TargetTypes.Distance,
+ distanceValue: number,
+ distanceUnit: DistanceUnits,
+};
+interface TimeTarget {
+ type: TargetTypes.Time,
+ time: number,
+};
+export type StandardTarget = DistanceTarget | TimeTarget;
+export interface StandardTargetSet {
+ name: string,
+ targets: Array<StandardTarget>,
+};
+export interface StandardTargetSets {
+ [key: string]: StandardTargetSet,
+};
+
+/*
+ * The types for split calculator targets and target sets
+ */
+export type SplitTarget = DistanceTarget & {
+ splitTime?: number
+};
+export interface SplitTargetSet {
+ name: string,
+ targets: Array<SplitTarget>,
+};
+export interface SplitTargetSets {
+ [key: string]: SplitTargetSet,
+};
+
+/*
+ * The types for workout calculator targets and target sets
+ */
+export type WorkoutTarget = StandardTarget & {
+ splitValue: number,
+ splitUnit: DistanceUnits,
+ customName?: string,
+};
+export interface WorkoutTargetSet {
+ name: string,
+ targets: Array<WorkoutTarget>,
+};
+export interface WorkoutTargetSets {
+ [key: string]: WorkoutTargetSet,
+};
+
+/*
+ * The types for generic targets and target sets
+ */
+export type Target = StandardTarget | SplitTarget | WorkoutTarget;
+export type TargetSet = StandardTargetSet | SplitTargetSet | WorkoutTargetSet;
+export type TargetSets = StandardTargetSets | SplitTargetSets | WorkoutTargetSets;
+
+/**
+ * Sort an array of targets
+ * @param {Array<Target>} targets The array of targets
+ * @returns {Array<Target>} The sorted targets
+ */
+export function sort(targets: Array<Target>): Array<Target> {
+ return [
+ ...targets.filter((item) => item.type === TargetTypes.Distance)
+ .sort((a, b) => convertDistance(a.distanceValue, a.distanceUnit, DistanceUnits.Meters)
+ - convertDistance(b.distanceValue, b.distanceUnit, DistanceUnits.Meters)),
+
+ ...targets.filter((item) => item.type === TargetTypes.Time)
+ .sort((a, b) => a.time - b.time),
+ ];
+}
+
+/**
+ * Generate a string description of a workout target
+ * @param {WorkoutTarget} target The workout target
+ * @return {string} The string description
+ */
+export function workoutTargetToString(target: WorkoutTarget): string {
+ let result = formatDistance({ distanceValue: target.splitValue, distanceUnit: target.splitUnit },
+ false);
+
+ if (target.type === TargetTypes.Time) {
+ result += ' @ ' + formatDuration(target.time, 3, 2, false);
+ } else if (target.distanceValue != target.splitValue || target.distanceUnit != target.splitUnit) {
+ result += ' @ ' + formatDistance(target as Distance, false);
+ }
+ return result;
+}
+
+/*
+ * The default target sets for each calculator
+ */
+export const defaultTargetSets: { [key: string]: TargetSet } = {
+ '_pace_targets': {
+ name: 'Common Pace Targets',
+ targets: sort([
+ { type: TargetTypes.Distance, distanceValue: 100, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 200, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 300, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 400, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 600, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 800, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1000, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1200, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1500, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1600, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 3200, distanceUnit: DistanceUnits.Meters },
+
+ { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 4, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Kilometers },
+
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Miles },
+
+ { type: TargetTypes.Distance, distanceValue: 0.5, distanceUnit: DistanceUnits.Marathons },
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons },
+
+ { type: TargetTypes.Time, time: 600 },
+ { type: TargetTypes.Time, time: 1800 },
+ { type: TargetTypes.Time, time: 3600 },
+ ]),
+ }, '_race_targets': {
+ name: 'Common Race Targets',
+ targets: sort([
+ { type: TargetTypes.Distance, distanceValue: 400, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 800, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1500, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1600, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 3000, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 3200, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
+
+ { type: TargetTypes.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 15, distanceUnit: DistanceUnits.Kilometers },
+
+ { type: TargetTypes.Distance, distanceValue: 0.5, distanceUnit: DistanceUnits.Marathons },
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons },
+ ]),
+ }, '_split_targets': {
+ name: '5K Mile Splits',
+ targets: [
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
+ ],
+ }, '_workout_targets': {
+ name: 'Common Workout Targets',
+ targets: [
+ {
+ splitValue: 400, splitUnit: DistanceUnits.Meters,
+ type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles,
+ },
+ {
+ splitValue: 800, splitUnit: DistanceUnits.Meters,
+ type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers,
+ },
+ {
+ splitValue: 1600, splitUnit: DistanceUnits.Meters,
+ type: TargetTypes.Time, time: 3600,
+ },
+ {
+ splitValue: 1, splitUnit: DistanceUnits.Miles,
+ type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons,
+ },
+ ],
+ },
+};
+export const defaultPaceTargetSets: StandardTargetSets = {
+ '_pace_targets': defaultTargetSets._pace_targets,
+};
+export const defaultRaceTargetSets: StandardTargetSets = {
+ '_race_targets': defaultTargetSets._race_targets,
+};
+export const defaultSplitTargetSets: SplitTargetSets = {
+ '_split_targets': defaultTargetSets._split_targets as SplitTargetSet,
+};
+export const defaultWorkoutTargetSets: WorkoutTargetSets = {
+ '_workout_targets': defaultTargetSets._workout_targets as WorkoutTargetSet,
+};
diff --git a/src/core/units.ts b/src/core/units.ts
@@ -0,0 +1,396 @@
+/*
+ * Implements handling of distance, pace, speed, and time units
+ */
+
+/*
+ * The type for the data available for each unit
+ */
+export interface UnitData {
+ name: string,
+ symbol: string,
+ value: number,
+};
+
+/*
+ * The available time units
+ */
+export enum TimeUnits {
+ Seconds = 'seconds',
+ Minutes = 'minutes',
+ Hours = 'hours',
+};
+export const TimeUnitData: { [key in TimeUnits]: UnitData } = {
+ [TimeUnits.Seconds]: {
+ name: 'Seconds',
+ symbol: 's',
+ value: 1,
+ },
+ [TimeUnits.Minutes]: {
+ name: 'Minutes',
+ symbol: 'min',
+ value: 60,
+ },
+ [TimeUnits.Hours]: {
+ name: 'Hours',
+ symbol: 'hr',
+ value: 3600,
+ },
+};
+
+/*
+ * The available distance units
+ */
+export enum DistanceUnits {
+ Meters = 'meters',
+ Yards = 'yards',
+ Kilometers = 'kilometers',
+ Miles = 'miles',
+ Marathons = 'marathons',
+};
+export const DistanceUnitData: { [key in DistanceUnits]: UnitData } = {
+ [DistanceUnits.Meters]: {
+ name: 'Meters',
+ symbol: 'm',
+ value: 1,
+ },
+ [DistanceUnits.Yards]: {
+ name: 'Yards',
+ symbol: 'yd',
+ value: 0.9144,
+ },
+ [DistanceUnits.Kilometers]: {
+ name: 'Kilometers',
+ symbol: 'km',
+ value: 1000,
+ },
+ [DistanceUnits.Miles]: {
+ name: 'Miles',
+ symbol: 'mi',
+ value: 1609.344,
+ },
+ [DistanceUnits.Marathons]: {
+ name: 'Marathons',
+ symbol: 'Mar',
+ value: 42195,
+ },
+};
+
+/*
+ * The available speed units
+ */
+export enum SpeedUnits {
+ MetersPerSecond = 'meters_per_second',
+ KilometersPerHour = 'kilometers_per_hour',
+ MilesPerHour = 'miles_per_hour',
+};
+export const SpeedUnitData: { [key in SpeedUnits]: UnitData } = {
+ [SpeedUnits.MetersPerSecond]: {
+ name: 'Meters per Second',
+ symbol: 'm/s',
+ value: 1,
+ },
+ [SpeedUnits.KilometersPerHour]: {
+ name: 'Kilometers per Hour',
+ symbol: 'kph',
+ value: DistanceUnitData[DistanceUnits.Kilometers].value / TimeUnitData[TimeUnits.Hours].value,
+ },
+ [SpeedUnits.MilesPerHour]: {
+ name: 'Miles per Hour',
+ symbol: 'mph',
+ value: DistanceUnitData[DistanceUnits.Miles].value / TimeUnitData[TimeUnits.Hours].value,
+ },
+};
+
+/*
+ * The available pace units
+ */
+export enum PaceUnits {
+ SecondsPerMeter = 'seconds_per_meter',
+ TimePerKilometer = 'seconds_per_kilometer',
+ TimePerMile = 'seconds_per_mile',
+};
+export const PaceUnitData: { [key in PaceUnits]: UnitData } = {
+ [PaceUnits.SecondsPerMeter]: {
+ name: 'Seconds per Meter',
+ symbol: 's/m',
+ value: 1,
+ },
+ [PaceUnits.TimePerKilometer]: {
+ name: 'Time per Kilometer',
+ symbol: '/ km',
+ value: TimeUnitData[TimeUnits.Seconds].value / DistanceUnitData[DistanceUnits.Kilometers].value,
+ },
+ [PaceUnits.TimePerMile]: {
+ name: 'Time per Mile',
+ symbol: '/ mi',
+ value: TimeUnitData[TimeUnits.Seconds].value / DistanceUnitData[DistanceUnits.Miles].value,
+ },
+};
+
+/*
+ * The available speed and pace units
+ */
+export type SpeedPaceUnits = SpeedUnits | PaceUnits;
+
+/*
+ * The type for a distance input
+ */
+export interface Distance {
+ distanceValue: number,
+ distanceUnit: DistanceUnits,
+};
+
+/*
+ * The type for a distance/time input pair
+ */
+export interface DistanceTime extends Distance {
+ time: number,
+};
+
+/*
+ * The available unit systems
+ */
+export enum UnitSystems {
+ Metric = 'metric',
+ Imperial = 'imperial',
+};
+
+/**
+ * Convert between time units
+ * @param {number} inputValue The input value
+ * @param {string} inputUnit The unit of the input
+ * @param {string} outputUnit The unit of the output
+ * @returns {number} The output
+ */
+export function convertTime(inputValue: number, inputUnit: TimeUnits,
+ outputUnit: TimeUnits): number {
+ return (inputValue * TimeUnitData[inputUnit].value) / TimeUnitData[outputUnit].value;
+}
+
+/**
+ * Convert between distance units
+ * @param {number} inputValue The input value
+ * @param {string} inputUnit The unit of the input
+ * @param {string} outputUnit The unit of the output
+ * @returns {number} The output
+ */
+export function convertDistance(inputValue: number, inputUnit: DistanceUnits,
+ outputUnit: DistanceUnits): number {
+ return (inputValue * DistanceUnitData[inputUnit].value) / DistanceUnitData[outputUnit].value;
+}
+
+/**
+ * Convert between speed units
+ * @param {number} inputValue The input value
+ * @param {string} inputUnit The unit of the input
+ * @param {string} outputUnit The unit of the output
+ * @returns {number} The output
+ */
+export function convertSpeed(inputValue: number, inputUnit: SpeedUnits,
+ outputUnit: SpeedUnits): number {
+ return (inputValue * SpeedUnitData[inputUnit].value) / SpeedUnitData[outputUnit].value;
+}
+
+/**
+ * Convert between pace units
+ * @param {number} inputValue The input value
+ * @param {string} inputUnit The unit of the input
+ * @param {string} outputUnit The unit of the output
+ * @returns {number} The output
+ */
+export function convertPace(inputValue: number, inputUnit: PaceUnits,
+ outputUnit: PaceUnits): number {
+ return (inputValue * PaceUnitData[inputUnit].value) / PaceUnitData[outputUnit].value;
+}
+
+/**
+ * Convert between speed and/or pace units
+ * @param {number} inputValue The input value
+ * @param {string} inputUnit The unit of the input
+ * @param {string} outputUnit The unit of the output
+ * @returns {number} The output
+ */
+export function convertSpeedPace(inputValue: number, inputUnit: SpeedPaceUnits,
+ outputUnit: SpeedPaceUnits): number {
+ // Calculate input speed
+ let speed;
+ if (inputUnit in PaceUnitData) {
+ speed = 1 / (inputValue * PaceUnitData[inputUnit as PaceUnits].value);
+ } else {
+ speed = inputValue * SpeedUnitData[inputUnit as SpeedUnits].value;
+ }
+
+ // Calculate output
+ if (outputUnit in PaceUnitData) {
+ return (1 / speed) / PaceUnitData[outputUnit as PaceUnits].value;
+ }
+ return speed / SpeedUnitData[outputUnit as SpeedUnits].value;
+}
+
+/**
+ * Detect the user's default unit system
+ * @returns {UnitSystems} The default unit system
+ */
+export function detectDefaultUnitSystem(): UnitSystems {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const language = (navigator.language || (navigator as any).userLanguage).toLowerCase();
+ if (language.endsWith('-us') || language.endsWith('-mm')) {
+ return UnitSystems.Imperial;
+ }
+ return UnitSystems.Metric;
+}
+
+/**
+ * Format a number as a string
+ * @param {number} value The number
+ * @param {number} minPadding The minimum number of digits to show before the decimal point
+ * @param {number} maxDigits The maximum number of digits to show after the decimal point
+ * @param {boolean} extraDigits Whether to show extra zeros after the decimal point
+ * @returns {string} The formatted number
+ */
+export function formatNumber(value: number, minPadding: number = 0, maxDigits: number = 2,
+ extraDigits: boolean = true): string {
+
+ // Initialize result
+ let result = '';
+
+ // Remove sign
+ const negative = value < 0;
+ const fixedValue = Math.abs(value);
+
+ // Address edge cases
+ if (Number.isNaN(fixedValue)) {
+ return 'NaN';
+ }
+ if (fixedValue === Infinity) {
+ return negative ? '-Infinity' : 'Infinity';
+ }
+
+ // Convert number to string
+ if (extraDigits) {
+ result = fixedValue.toFixed(maxDigits);
+ } else {
+ const power = 10 ** maxDigits;
+ result = (Math.round((fixedValue + Number.EPSILON) * power) / power).toString();
+ }
+
+ // Add padding
+ const currentPadding = result.split('.')[0].length;
+ result = result.padStart(result.length - currentPadding + minPadding, '0');
+
+ // Add negative sign
+ if (negative) {
+ result = `-${result}`;
+ }
+
+ // Return result
+ return result;
+}
+
+/**
+ * Format a distance as a string
+ * @param {Distance} input The distance
+ * @param {boolean} extraDigits Whether to show extra zeros after the decimal point
+ * @returns {string} The formatted distance
+ */
+export function formatDistance(input: Distance, extraDigits: boolean) {
+ return formatNumber(input.distanceValue, 0, 2, extraDigits) + ' '
+ + DistanceUnitData[input.distanceUnit].symbol;
+}
+
+/**
+ * Format a duration as a string
+ * @param {number} value The duration (in seconds)
+ * @param {number} minPadding The minimum number of digits to show before the decimal point
+ * @param {number} maxDigits The maximum number of digits to show after the decimal point
+ * @param {boolean} extraDigits Whether to show extra zeros after the decimal point
+ * @returns {string} The formatted duration
+ */
+export function formatDuration(value: number, minPadding: number = 6, maxDigits: number = 2,
+ extraDigits: boolean = true): string {
+ // Check if value is NaN
+ if (Number.isNaN(value)) {
+ return 'NaN';
+ }
+
+ // Initialize result
+ let result = '';
+
+ // Check value sign
+ if (value < 0) {
+ result += '-';
+ }
+
+ // Check if value is valid
+ if (Math.abs(value) === Infinity) {
+ return `${result}Infinity`;
+ }
+
+ // Validate padding
+ let fixedPadding = Math.min(minPadding, 6);
+
+ // Prevent rounding errors
+ const fixedValue = parseFloat(Math.abs(value).toFixed(maxDigits));
+
+ // Calculate parts
+ const hours = Math.floor(fixedValue / 3600);
+ const minutes = Math.floor((fixedValue % 3600) / 60);
+ const seconds = fixedValue % 60;
+
+ // Format parts
+ if (hours !== 0 || fixedPadding >= 5) {
+ result += hours.toString().padStart(fixedPadding - 4, '0');
+ result += ':';
+ fixedPadding = 4;
+ }
+ if (minutes !== 0 || fixedPadding >= 3) {
+ result += minutes.toString().padStart(fixedPadding - 2, '0');
+ result += ':';
+ fixedPadding = 2;
+ }
+ result += formatNumber(seconds, fixedPadding, maxDigits, extraDigits);
+
+ // Return result
+ return result;
+}
+
+/**
+ * Calculate the pace of a distance/time pair and format it as a string
+ * @param {DistanceTime} input The input distance/time pair
+ * @param {PaceUnits} unit The desired pace unit
+ * @returns {string} The formatted pace
+ */
+export function formatPace(input: DistanceTime, unit: PaceUnits) {
+ const dist = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
+ const pace = convertPace(input.time / dist, PaceUnits.SecondsPerMeter, unit)
+ const result = formatDuration(pace, 3, 0, true) + ' ' + PaceUnitData[unit].symbol;
+ return result;
+}
+
+/**
+ * Get the default distance unit in a unit system
+ * @param {UnitSystems} unitSystem The unit system
+ * @returns {DistanceUnits} The default distance unit
+ */
+export function getDefaultDistanceUnit(unitSystem: UnitSystems): DistanceUnits {
+ return unitSystem === UnitSystems.Metric ? DistanceUnits.Kilometers : DistanceUnits.Miles;
+}
+
+/**
+ * Get the default speed unit in a unit system
+ * @param {UnitSystems} unitSystem The unit system
+ * @returns {SpeedUnits} The default speed unit
+ */
+export function getDefaultSpeedUnit(unitSystem: UnitSystems): SpeedUnits {
+ return unitSystem === UnitSystems.Metric ? SpeedUnits.KilometersPerHour
+ : SpeedUnits.MilesPerHour;
+}
+
+/**
+ * Get the default pace unit in a unit system
+ * @param {UnitSystems} unitSystem The unit system
+ * @returns {PaceUnits} The default pace unit
+ */
+export function getDefaultPaceUnit(unitSystem: UnitSystems): PaceUnits {
+ return unitSystem === UnitSystems.Metric ? PaceUnits.TimePerKilometer : PaceUnits.TimePerMile;
+}
diff --git a/src/core/utils.ts b/src/core/utils.ts
@@ -0,0 +1,116 @@
+/*
+ * Contains utility functions for handling nested objects and interacting with localStorage
+ */
+
+import { defaultRaceOptions, defaultWorkoutOptions } from '@/core/calculators';
+
+// The global localStorage prefix
+const LocalStoragePrefix = 'running-tools';
+
+/**
+ * Create a deep copy of an object
+ * @param {Type} value The object to copy
+ * @returns {Type} The copied object
+ */
+export function deepCopy<Type>(value: Type): Type {
+ return JSON.parse(JSON.stringify(value));
+}
+
+/**
+ * Test whether two objects are deeply equal
+ * @param {Type} value1 The first object
+ * @param {Type} value2 The second object
+ * @returns {boolean} Whether the two objects are equal
+ */
+export function deepEqual<Type>(value1: Type, value2: Type): boolean {
+ return JSON.stringify(value1) === JSON.stringify(value2);
+}
+
+/**
+ * Read an object from a localStorage item
+ * @param {string} key The localStorage item's key
+ * @returns {Type} The object
+ */
+export function getLocalStorage<Type>(key: string): Type | null {
+ try {
+ return JSON.parse(localStorage.getItem(`${LocalStoragePrefix}.${key}`) || '');
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Write an object to a localStorage item
+ * @param {string} key The localStorage item's key
+ * @param {Type} value The object to write
+ */
+export function setLocalStorage<Type>(key: string, value: Type) {
+ localStorage.setItem(`${LocalStoragePrefix}.${key}`, JSON.stringify(value));
+}
+
+/**
+ * Delete a localStorage item
+ * @param {string} key The localStorage item's key
+ */
+export function unsetLocalStorage(key: string) {
+ localStorage.removeItem(`${LocalStoragePrefix}.${key}`);
+}
+
+/**
+ * Migrate outdated localStorage options
+ */
+export function migrateLocalStorage() {
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+
+ // Move pace-calculator-target-set into new pace-calculator-options (>1.4.1)
+ const paceSelectedTargetSet = getLocalStorage<string>('pace-calculator-target-set');
+ if (paceSelectedTargetSet !== null) {
+ const paceOptions = { selectedTargetSet: paceSelectedTargetSet };
+ setLocalStorage('pace-calculator-options', paceOptions);
+ unsetLocalStorage('pace-calculator-target-set');
+ }
+
+ // Move race-calculator-target-set into race-calculator-options (>1.4.1)
+ const raceSelectedTargetSet = getLocalStorage<string>('race-calculator-target-set');
+ const raceOptions = getLocalStorage<any>('race-calculator-options')
+ || deepCopy(defaultRaceOptions);
+ if (raceSelectedTargetSet !== null) {
+ raceOptions.selectedTargetSet = raceSelectedTargetSet;
+ setLocalStorage('race-calculator-options', raceOptions);
+ unsetLocalStorage('race-calculator-target-set');
+ }
+ if (raceOptions !== null && raceOptions.selectedTargetSet === undefined) {
+ raceOptions.selectedTargetSet = defaultRaceOptions.selectedTargetSet;
+ setLocalStorage('race-calculator-options', raceOptions);
+ }
+
+ // Move split-calculator-target-set into new split-calculator-options (>1.4.1)
+ const splitSelectedTargetSet = getLocalStorage<string>('split-calculator-target-set');
+ if (splitSelectedTargetSet !== null) {
+ const splitOptions = { selectedTargetSet: splitSelectedTargetSet };
+ setLocalStorage('split-calculator-options', splitOptions);
+ unsetLocalStorage('split-calculator-target-set');
+ }
+
+ // Move workout-calculator-target-set into workout-calculator-options (>1.4.1)
+ const workoutSelectedTargetSet = getLocalStorage<string>('workout-calculator-target-set');
+ const workoutOptions = getLocalStorage<any>('workout-calculator-options')
+ || deepCopy(defaultWorkoutOptions);
+ if (workoutSelectedTargetSet !== null) {
+ workoutOptions.selectedTargetSet = workoutSelectedTargetSet;
+ setLocalStorage('workout-calculator-options', workoutOptions);
+ unsetLocalStorage('workout-calculator-target-set');
+ }
+ if (workoutOptions !== null && workoutOptions.selectedTargetSet === undefined) {
+ workoutOptions.selectedTargetSet = defaultWorkoutOptions.selectedTargetSet;
+ setLocalStorage('workout-calculator-options', workoutOptions);
+ }
+
+ // Add customTargetNames property to workout-calculator-options (>1.4.1)
+ if (workoutOptions.customTargetNames === undefined) {
+ workoutOptions.customTargetNames = false;
+ setLocalStorage('workout-calculator-options', workoutOptions);
+ }
+
+ /* eslint-enable @typescript-eslint/no-explicit-any */
+}
diff --git a/src/main.ts b/src/main.ts
@@ -2,11 +2,11 @@ import { createApp } from 'vue';
import App from '@/App.vue';
import router from '@/router';
-import * as storage from '@/utils/storage';
+import { migrateLocalStorage } from '@/core/utils';
import '@/assets/global.css';
-storage.migrate();
+migrateLocalStorage();
const app = createApp(App);
app.use(router);
diff --git a/src/utils/calculators.ts b/src/utils/calculators.ts
@@ -1,243 +0,0 @@
-import * as raceUtils from '@/utils/races';
-import { TargetTypes, workoutTargetToString } from '@/utils/targets';
-import type { StandardTarget, WorkoutTarget } from '@/utils/targets';
-import { DistanceUnits, UnitSystems, convertDistance, formatDistance, formatDuration, formatPace,
- getDefaultDistanceUnit, getDefaultPaceUnit } from '@/utils/units';
-import type { DistanceTime } from '@/utils/units';
-
-/*
- * The four main calculators (batch and unit calculators not included)
- *
- * Used to determine available options and target set format
- */
-export enum Calculators {
- Pace = 'pace',
- Race = 'race',
- Split = 'split',
- Workout = 'workout',
-}
-
-/*
- * The type for the available race statistics
- */
-export interface RaceStats {
- purdyPoints: number,
- vo2Max: number,
- vo2: number,
- vo2MaxPercentage: number,
-};
-
-/*
- * The type for the options specific to each calculator
- */
-export interface StandardOptions {
- selectedTargetSet: string,
-}
-export interface RaceOptions extends StandardOptions {
- model: raceUtils.RacePredictionModel,
- riegelExponent: number,
-};
-export interface WorkoutOptions extends RaceOptions {
- customTargetNames: boolean,
-};
-export interface BatchOptions {
- calculator: Calculators.Pace | Calculators.Race | Calculators.Workout,
- increment: number,
- rows: number,
-};
-
-/*
- * The two possible result fields of a target result: "key" and "value"
- */
-export enum ResultType {
- Key = 'key',
- Value = 'value',
-};
-
-/*
- * The type for target results
- */
-export interface TargetResult {
- key: string,
- value: string,
- pace: string,
- result: ResultType,
- sort: number,
-};
-
-/*
- * The default input and options for each calculator
- */
-export const defaultInput: DistanceTime = {
- distanceValue: 5,
- distanceUnit: DistanceUnits.Kilometers,
- time: 1200,
-};
-export const defaultBatchOptions: BatchOptions = {
- calculator: Calculators.Workout,
- increment: 15,
- rows: 20,
-};
-export const defaultPaceOptions: StandardOptions = {
- selectedTargetSet: '_pace_targets',
-};
-export const defaultRaceOptions: RaceOptions = {
- model: raceUtils.RacePredictionModel.AverageModel,
- riegelExponent: 1.06,
- selectedTargetSet: '_race_targets',
-};
-export const defaultSplitOptions: StandardOptions = {
- selectedTargetSet: '_split_targets',
-};
-export const defaultWorkoutOptions: WorkoutOptions = {
- customTargetNames: false,
- ...defaultRaceOptions,
- selectedTargetSet: '_workout_targets',
-};
-
-/**
- * Calculate results for a standard target
- * @param {DistanceTime} input The input pace
- * @param {StandardTarget} target The standard target
- * @param {Function} calculateTime The function for calculating time results
- * @param {Function} calculateDistance The function for calculating distance results
- * @param {UnitSystems} defaultUnitSystem The default unit system (imperial or metric)
- * @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
- * @returns {TargetResult} The result
- */
-function calculateStandardResult(input: DistanceTime, target: StandardTarget,
- calculateTime: (d1: number, t1: number, d2: number) => number,
- calculateDistance: (t1: number, d1: number, t2: number) => number, defaultUnitSystem: UnitSystems,
- preciseDurations: boolean = true): TargetResult {
-
- let distanceValue, distanceUnit, time;
- const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
- if (target.type === TargetTypes.Distance) {
- // Add target distance to result
- distanceValue = target.distanceValue;
- distanceUnit = target.distanceUnit;
-
- // Calculate time result
- const d2 = convertDistance(target.distanceValue, target.distanceUnit, DistanceUnits.Meters);
- time = calculateTime(d1, input.time, d2);
- } else {
- // Add target time to result
- time = target.time;
-
- // Calculate distance result
- const d2 = calculateDistance(input.time, d1, target.time);
- const units = getDefaultDistanceUnit(defaultUnitSystem);
- distanceValue = convertDistance(d2, DistanceUnits.Meters, units);
- distanceUnit = units;
- }
-
- return {
- // Convert distance to key string
- key: formatDistance({ distanceValue, distanceUnit }, target.type === TargetTypes.Time),
-
- // Convert time to time string
- value: formatDuration(time, 3, preciseDurations ? 2 : 0, target.type === TargetTypes.Distance),
-
- // Convert pace to pace string
- pace: formatPace({ time, distanceValue, distanceUnit }, getDefaultPaceUnit(defaultUnitSystem)),
-
- // Convert dist/time result to key/value
- result: target.type === TargetTypes.Distance ? ResultType.Value : ResultType.Key,
-
- // Use time (in seconds) as sort key
- sort: time,
- };
-}
-
-/**
- * Calculate paces from a target
- * @param {DistanceTime} input The input pace
- * @param {StandardTarget} target The pace target
- * @param {UnitSystems} defaultUnitSystem The default unit system (imperial or metric)
- * @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
- * @returns {TargetResult} The result
- */
-export function calculatePaceResults(input: DistanceTime, target: StandardTarget,
- defaultUnitSystem: UnitSystems,
- preciseDurations: boolean = true): TargetResult {
-
- return calculateStandardResult(input, target, (d1, t1, d2) => ((t1 / d1) * d2),
- (t1, d1, t2) => ((d1 / t1) * t2), defaultUnitSystem, preciseDurations);
-}
-
-/**
- * Predict race results from a target
- * @param {DistanceTime} input The input race
- * @param {StandardTarget} target The race target
- * @param {RaceOptions} options The race prediction options
- * @param {UnitSystems} defaultUnitSystem The default unit system (imperial or metric)
- * @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
- * @returns {TargetResult} The result
- */
-export function calculateRaceResults(input: DistanceTime, target: StandardTarget,
- options: RaceOptions, defaultUnitSystem: UnitSystems,
- preciseDurations: boolean = true): TargetResult {
-
- return calculateStandardResult(input, target,
- (d1, t1, d2) => raceUtils.predictTime(d1, t1, d2, options.model, options.riegelExponent),
- (t1, d1, t2) => raceUtils.predictDistance(t1, d1, t2, options.model, options.riegelExponent),
- defaultUnitSystem, preciseDurations);
-}
-
-/**
- * Calculate race statistics from an input race
- * @param {DistanceTime} input The input race
- * @returns {RaceStats} The race statistics
- */
-export function calculateRaceStats(input: DistanceTime): RaceStats {
- const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
-
- return {
- purdyPoints: raceUtils.getPurdyPoints(d1, input.time),
- vo2Max: raceUtils.getVO2Max(d1, input.time),
- vo2: raceUtils.getVO2(d1, input.time),
- vo2MaxPercentage: raceUtils.getVO2Percentage(input.time) * 100,
- }
-}
-
-/**
- * Predict workout results from a target
- * @param {DistanceTime} input The input race
- * @param {WorkoutTarget} target The workout target
- * @param {WorkoutOptions} options The workout options
- * @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
- * @returns {TargetResult} The result
- */
-export function calculateWorkoutResults(input: DistanceTime, target: WorkoutTarget,
- options: WorkoutOptions,
- preciseDurations: boolean = true): TargetResult {
- // Initialize distance and time variables
- const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
- const t1 = input.time;
- const d3 = convertDistance(target.splitValue, target.splitUnit, DistanceUnits.Meters);
- let d2, t2;
-
- // Calculate result
- if (target.type === 'distance') {
- // Convert target distance into meters
- d2 = convertDistance(target.distanceValue, target.distanceUnit, DistanceUnits.Meters);
-
- // Get workout split prediction
- t2 = raceUtils.predictTime(d1, input.time, d2, options.model, options.riegelExponent);
- } else {
- t2 = target.time;
-
- // Get workout split prediction
- d2 = raceUtils.predictDistance(t1, d1, t2, options.model, options.riegelExponent);
- }
- const t3 = (t2 / d2) * d3;
-
- // Return result
- return {
- key: (options.customTargetNames && target.customName) || workoutTargetToString(target),
- value: formatDuration(t3, 3, preciseDurations ? 2 : 0, true),
- pace: '', // Pace not used in workout calculator
- result: ResultType.Value,
- sort: t3,
- }
-}
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
@@ -1,18 +0,0 @@
-/**
- * Create a deep copy of an object
- * @param {Type} value The object to copy
- * @returns {Type} The copied object
- */
-export function deepCopy<Type>(value: Type): Type {
- return JSON.parse(JSON.stringify(value));
-}
-
-/**
- * Test whether two objects are deeply equal
- * @param {Type} value1 The first object
- * @param {Type} value2 The second object
- * @returns {boolean} Whether the two objects are equal
- */
-export function deepEqual<Type>(value1: Type, value2: Type): boolean {
- return JSON.stringify(value1) === JSON.stringify(value2);
-}
diff --git a/src/utils/races.ts b/src/utils/races.ts
@@ -1,487 +0,0 @@
-/*
- * The available race prediction models
- */
-export enum RacePredictionModel {
- AverageModel = 'AverageModel',
- PurdyPointsModel = 'PurdyPointsModel',
- VO2MaxModel = 'VO2MaxModel',
- RiegelModel = 'RiegelModel',
- CameronModel = 'CameronModel',
-};
-
-/*
- * The type for internal variables used by the Purdy Points race prediction model
- */
-interface PurdyPointsVariables {
- twsec: number,
- a: number,
- b: number,
-};
-
-/**
- * Estimate the point at which a function returns a target value using Newton's Method
- * @param {number} initialEstimate The initial estimate
- * @param {number} target The target function output
- * @param {Function} method The function
- * @param {Function} derivative The function derivative
- * @param {number} precision The acceptable precision
- * @returns {number} The refined estimate
- */
-function NewtonsMethod(initialEstimate: number, target: number, method: (x: number) => number,
- derivative: (x: number) => number, precision: number): number {
- // Initialize estimate
- let estimate = initialEstimate;
- let estimateValue;
-
- for (let i = 0; i < 500; i += 1) {
- // Evaluate function at estimate
- estimateValue = method(estimate);
-
- // Check if estimate is close enough (usually occurs way before i = 500)
- if (Math.abs(target - estimateValue) < precision) {
- break;
- }
-
- // Refine estimate
- estimate -= (estimateValue - target) / derivative(estimate);
- }
-
- // Return refined estimate
- return estimate;
-}
-
-/*
- * Methods that implement the Purdy Points race prediction model
- * https://www.cs.uml.edu/~phoffman/xcinfo3.html
- */
-const PurdyPointsModel = {
- /**
- * Calculate the Purdy Point variables for a distance
- * @param {number} d The distance in meters
- * @returns {PurdyPointsVariables} The Purdy Point variables
- */
- getVariables(d: number): PurdyPointsVariables {
- // Declare constants
- const c1 = 11.15895;
- const c2 = 4.304605;
- const c3 = 0.5234627;
- const c4 = 4.031560;
- const c5 = 2.316157;
- const r1 = 3.796158e-2;
- const r2 = 1.646772e-3;
- const r3 = 4.107670e-4;
- const r4 = 7.068099e-6;
- const r5 = 5.220990e-9;
-
- // Calculate world record velocity from running curve
- const v = (-c1 * Math.exp(-r1 * d))
- + (c2 * Math.exp(-r2 * d))
- + (c3 * Math.exp(-r3 * d))
- + (c4 * Math.exp(-r4 * d))
- + (c5 * Math.exp(-r5 * d));
-
- // Calculate world record time
- const twsec = d / v;
-
- // Calculate constants
- const k = 0.0654 - (0.00258 * v);
- const a = 85 / k;
- const b = 1 - (1035 / a);
-
- // Return Purdy Point variables
- return {
- twsec,
- a,
- b,
- };
- },
-
- /**
- * Get the Purdy Points for a race
- * @param {number} d The distance of the race in meters
- * @param {number} t The finish time of the race in seconds
- * @returns {number} The Purdy Points for the race
- */
- getPurdyPoints(d: number, t: number): number {
- // Get variables
- const variables = PurdyPointsModel.getVariables(d);
-
- // Calculate Purdy Points
- const points = variables.a * ((variables.twsec / t) - variables.b);
-
- // Return Purdy Points
- return points;
- },
-
- /**
- * Predict a race time using the Purdy Points Model
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d2 The distance of the output race in meters
- * @returns {number} The predicted time for the output race in seconds
- */
- predictTime(d1: number, t1: number, d2: number): number {
- // Calculate Purdy Points for distance 1
- const points = PurdyPointsModel.getPurdyPoints(d1, t1);
-
- // Calculate time for distance 2
- const variables = PurdyPointsModel.getVariables(d2);
- const seconds = (variables.a * variables.twsec) / (points + (variables.a * variables.b));
-
- // Return predicted time
- return seconds;
- },
-
- /**
- * Calculate the derivative with respect to distance of the Purdy Points curve at a specific point
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d2 The distance of the output race in meters
- * @return {number} The derivative with respect to distance
- */
- derivative(d1: number, t1: number, d2: number): number {
- const result = (85 * d2) / (((2316157 * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000
- + (100789 * Math.exp(-(7068099 * d2) / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767
- * d2) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000
- - (223179 * Math.exp(-(1898079 * d2) / 50000000)) / 20000) * (327 / 5000 - (129 * ((2316157
- * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
- / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
- + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
- * d2) / 50000000)) / 20000)) / 50000) * ((85 * (1 - (207 * (327 / 5000 - (129 * ((2316157
- * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
- / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
- + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
- * d2) / 50000000)) / 20000)) / 50000)) / 17)) / (327 / 5000 - (129 * ((2316157
- * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
- / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
- + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
- * d2) / 50000000)) / 20000)) / 50000) + (85 * (d1 / (((2316157 * Math.exp(-(522099 * d1)
- / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000
- + (5234627 * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693
- * d1) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000) * t1)
- + (207 * (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000))
- / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627
- * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1)
- / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000))
- / 17 - 1)) / (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000))
- / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627
- * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1)
- / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000)));
- return result;
- },
-
- /**
- * Predict a race distance using the Purdy Points Model
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t2 The finish time of the output race in seconds
- * @returns {number} The predicted distance for the output race in meters
- */
- predictDistance(t1: number, d1: number, t2: number): number {
- // Initialize estimate
- let estimate = (d1 * t2) / t1;
-
- // Refine estimate (derivative on its own is too slow)
- const method = (x: number) => PurdyPointsModel.predictTime(d1, t1, x);
- const derivative = (x: number) => PurdyPointsModel.derivative(d1, t1, x) / 500;
- estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01);
-
- // Return estimate
- return estimate;
- },
-};
-
-/*
- * Methods that implement the VO2 Max race prediction model
- * http://run-down.com/statistics/calcs_explained.php
- * https://vdoto2.com/Calculator
- */
-const VO2MaxModel = {
- /**
- * Calculate the VO2 of a runner during a race
- * @param {number} d The race distance in meters
- * @param {number} t The finish time in seconds
- * @returns {number} The VO2
- */
- getVO2(d: number, t: number): number {
- const minutes = t / 60;
- const v = d / minutes;
- const result = -4.6 + (0.182258 * v) + (0.000104 * (v ** 2));
- return result;
- },
-
- /**
- * Calculate the percentage of VO2 max a runner is at during a race
- * @param {number} t The race time in seconds
- * @returns {number} The percentage of VO2 max
- */
- getVO2Percentage(t: number): number {
- const minutes = t / 60;
- const result = 0.8 + (0.189439 * Math.exp(-0.012778 * minutes)) + (0.298956 * Math.exp(-0.193261
- * minutes));
- return result;
- },
-
- /**
- * Calculate a runner's VO2 max from a race result
- * @param {number} d The race distance in meters
- * @param {number} t The finish time in seconds
- * @returns {number} The runner's VO2 max
- */
- getVO2Max(d: number, t: number): number {
- const result = VO2MaxModel.getVO2(d, t) / VO2MaxModel.getVO2Percentage(t);
- return result;
- },
-
- /**
- * Calculate the derivative with respect to time of the VO2 max curve at a specific point
- * @param {number} d The race distance in meters
- * @param {number} t The finish time in seconds
- * @return {number} The derivative with respect to time
- */
- VO2MaxTimeDerivative(d: number, t: number): number {
- const result = (-(273 * d) / (25 * (t ** 2)) - (468 * (d ** 2)) / (625 * (t ** 3))) / ((189
- * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5)
- - (((273 * d) / (25 * t) + (234 * (d ** 2)) / (625 * (t ** 2)) - 23 / 5) * (-(63
- * Math.exp(-(2 * t) / 9375)) / 1562500 - (57707 * Math.exp(-(193 * t) / 60000)) / 60000000))
- / (((189 * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000
- + 4 / 5) ** 2);
- return result;
- },
-
- /**
- * Predict a race time using the VO2 Max Model
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d2 The distance of the output race in meters
- * @returns {number} The predicted time for the output race in seconds
- */
- predictTime(d1: number, t1: number, d2: number): number {
- // Calculate input VO2 max
- const inputVO2Max = VO2MaxModel.getVO2Max(d1, t1);
-
- // Initialize estimate
- let estimate = (t1 * d2) / d1;
-
- // Refine estimate
- const method = (x: number) => VO2MaxModel.getVO2Max(d2, x);
- const derivative = (x: number) => VO2MaxModel.VO2MaxTimeDerivative(d2, x);
- estimate = NewtonsMethod(estimate, inputVO2Max, method, derivative, 0.0001);
-
- // Return estimate
- return estimate;
- },
-
- /**
- * Calculate the derivative with respect to distance of the VO2 max curve at a specific point
- * @param {number} d The race distance in meters
- * @param {number} t The finish time in seconds
- * @return {number} The derivative with respect to distance
- */
- VO2MaxDistanceDerivative(d: number, t: number): number {
- const result = ((468 * d) / (625 * (t ** 2)) + 273 / (25 * t)) / ((189 * Math.exp(-(2 * t)
- / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5);
- return result;
- },
-
- /**
- * Predict a race distance using the VO2 Max Model
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t2 The finish time of the output race in seconds
- * @returns {number} The predicted distance for the output race in meters
- */
- predictDistance(t1: number, d1: number, t2: number): number {
- // Calculate input VO2 max
- const inputVO2 = VO2MaxModel.getVO2Max(d1, t1);
-
- // Initialize estimate
- let estimate = (d1 * t2) / t1;
-
- // Refine estimate
- const method = (x: number) => VO2MaxModel.getVO2Max(x, t2);
- const derivative = (x: number) => VO2MaxModel.VO2MaxDistanceDerivative(x, t2);
- estimate = NewtonsMethod(estimate, inputVO2, method, derivative, 0.0001);
-
- // Return estimate
- return estimate;
- },
-};
-
-/*
- * Methods that implement Dave Cameron's race prediction model
- * https://www.cs.uml.edu/~phoffman/cammod.html
- */
-const CameronModel = {
- /**
- * Predict a race time using Dave Cameron's Model
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d2 The distance of the output race in meters
- * @returns {number} The predicted time for the output race in seconds
- */
- predictTime(d1: number, t1: number, d2: number): number {
- const a = 13.49681 - (0.000030363 * d1) + (835.7114 / (d1 ** 0.7905));
- const b = 13.49681 - (0.000030363 * d2) + (835.7114 / (d2 ** 0.7905));
- return (t1 / d1) * (a / b) * d2;
- },
-
- /**
- * Calculate the derivative with respect to distance of the Cameron curve at a specific point
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d2 The distance of the output race in meters
- * @return {number} The derivative with respect to distance
- */
- derivative(d1: number, t1: number, d2: number): number {
- const result = -(100 * (30363 * (d1 ** (3581 / 2000)) - 13496810000 * (d1 ** (1581 / 2000))
- - 835711400000) * t1 * (134968100 * (d2 ** (3581 / 2000)) + 14963412617 * d2)) / ((d1 ** (3581
- / 2000)) * (d2 ** (419 / 2000)) * ((30363 * (d2 ** (3581 / 2000)) - 13496810000 * (d2 ** (1581
- / 2000)) - 835711400000) ** 2));
- return result;
- },
-
- /**
- * Predict a race distance using Dave Cameron's Model
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t2 The finish time of the output race in seconds
- * @returns {number} The predicted distance for the output race in meters
- */
- predictDistance(t1: number, d1: number, t2: number): number {
- // Initialize estimate
- let estimate = (d1 * t2) / t1;
-
- // Refine estimate
- const method = (x: number) => CameronModel.predictTime(d1, t1, x);
- const derivative = (x: number) => CameronModel.derivative(d1, t1, x);
- estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01);
-
- // Return estimate
- return estimate;
- },
-};
-
-/*
- * Methods that implement Pete Riegel's race prediction model
- * https://en.wikipedia.org/wiki/Peter_Riegel
- */
-const RiegelModel = {
- /**
- * Predict a race time using Pete Riegel's Model
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d2 The distance of the output race in meters
- * @param {number} c The value of the exponent in the equation
- * @returns {number} The predicted time for the output race in seconds
- */
- predictTime(d1: number, t1: number, d2: number, c: number = 1.06): number {
- return t1 * ((d2 / d1) ** c);
- },
-
- /**
- * Predict a race distance using Pete Riegel's Model
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t2 The finish time of the output race in seconds
- * @param {number} c The value of the exponent in the equation
- * @returns {number} The predicted distance for the output race in meters
- */
- predictDistance(t1: number, d1: number, t2: number, c: number = 1.06) {
- return d1 * ((t2 / t1) ** (1 / c));
- },
-};
-
-/*
- * Methods that average the results of different race prediction models
- */
-const AverageModel = {
- /**
- * Predict a race time by averaging the results of different models
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d2 The distance of the output race in meters
- * @param {number} c The value of the exponent in Pete Riegel's Model
- * @returns {number} The predicted time for the output race in seconds
- */
- predictTime(d1: number, t1: number, d2: number, c: number = 1.06): number {
- const purdy = PurdyPointsModel.predictTime(d1, t1, d2);
- const vo2max = VO2MaxModel.predictTime(d1, t1, d2);
- const cameron = CameronModel.predictTime(d1, t1, d2);
- const riegel = RiegelModel.predictTime(d1, t1, d2, c);
- return (purdy + vo2max + cameron + riegel) / 4;
- },
-
- /**
- * Predict a race distance by averaging the results of different models
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t2 The finish time of the output race in seconds
- * @param {number} c The value of the exponent in Pete Riegel's Model
- * @returns {number} The predicted distance for the output race in meters
- */
- predictDistance(t1: number, d1: number, t2: number, c: number = 1.06) {
- const purdy = PurdyPointsModel.predictDistance(t1, d1, t2);
- const vo2max = VO2MaxModel.predictDistance(t1, d1, t2);
- const cameron = CameronModel.predictDistance(t1, d1, t2);
- const riegel = RiegelModel.predictDistance(t1, d1, t2, c);
- return (purdy + vo2max + cameron + riegel) / 4;
- },
-};
-
-/**
- * Predict a race time
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d2 The distance of the output race in meters
- * @param {string} model The race prediction model to use
- * @param {number} c The value of the exponent in Pete Riegel's Model
- */
-export function predictTime(d1: number, t1: number, d2: number,
- model: RacePredictionModel = RacePredictionModel.AverageModel,
- c: number = 1.06): number {
- switch (model) {
- default:
- case RacePredictionModel.AverageModel:
- return AverageModel.predictTime(d1, t1, d2, c);
- case RacePredictionModel.PurdyPointsModel:
- return PurdyPointsModel.predictTime(d1, t1, d2);
- case RacePredictionModel.VO2MaxModel:
- return VO2MaxModel.predictTime(d1, t1, d2);
- case RacePredictionModel.RiegelModel:
- return RiegelModel.predictTime(d1, t1, d2, c);
- case RacePredictionModel.CameronModel:
- return CameronModel.predictTime(d1, t1, d2);
- }
-}
-
-/**
- * Predict a race distance
- * @param {number} t1 The finish time of the input race in seconds
- * @param {number} d1 The distance of the input race in meters
- * @param {number} t2 The finish time of the output race in seconds
- * @param {string} model The race prediction model to use
- * @param {number} c The value of the exponent in Pete Riegel's Model
- */
-export function predictDistance(t1: number, d1: number, t2: number,
- model: RacePredictionModel = RacePredictionModel.AverageModel,
- c: number = 1.06) {
- switch (model) {
- default:
- case RacePredictionModel.AverageModel:
- return AverageModel.predictDistance(t1, d1, t2, c);
- case RacePredictionModel.PurdyPointsModel:
- return PurdyPointsModel.predictDistance(t1, d1, t2);
- case RacePredictionModel.VO2MaxModel:
- return VO2MaxModel.predictDistance(t1, d1, t2);
- case RacePredictionModel.RiegelModel:
- return RiegelModel.predictDistance(t1, d1, t2, c);
- case RacePredictionModel.CameronModel:
- return CameronModel.predictDistance(t1, d1, t2);
- }
-}
-
-export const getPurdyPoints = PurdyPointsModel.getPurdyPoints;
-export const getVO2 = VO2MaxModel.getVO2;
-export const getVO2Percentage = VO2MaxModel.getVO2Percentage;
-export const getVO2Max = VO2MaxModel.getVO2Max;
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
@@ -1,94 +0,0 @@
-import { deepCopy } from '@/utils/misc';
-import { defaultRaceOptions, defaultWorkoutOptions } from '@/utils/calculators';
-
-// The global localStorage prefix
-const LocalStoragePrefix = 'running-tools';
-
-/**
- * Read an object from a localStorage item
- * @param {string} key The localStorage item's key
- * @returns {Type} The object
- */
-export function get<Type>(key: string): Type | null {
- try {
- return JSON.parse(localStorage.getItem(`${LocalStoragePrefix}.${key}`) || '');
- } catch {
- return null;
- }
-}
-
-/**
- * Write an object to a localStorage item
- * @param {string} key The localStorage item's key
- * @param {Type} value The object to write
- */
-export function set<Type>(key: string, value: Type) {
- localStorage.setItem(`${LocalStoragePrefix}.${key}`, JSON.stringify(value));
-}
-
-/**
- * Delete a localStorage item
- * @param {string} key The localStorage item's key
- */
-export function unset(key: string) {
- localStorage.removeItem(`${LocalStoragePrefix}.${key}`);
-}
-
-/**
- * Migrate outdated localStorage options
- */
-export function migrate() {
- /* eslint-disable @typescript-eslint/no-explicit-any */
-
- // Move pace-calculator-target-set into new pace-calculator-options (>1.4.1)
- const paceSelectedTargetSet = get<string>('pace-calculator-target-set');
- if (paceSelectedTargetSet !== null) {
- const paceOptions = { selectedTargetSet: paceSelectedTargetSet };
- set('pace-calculator-options', paceOptions);
- unset('pace-calculator-target-set');
- }
-
- // Move race-calculator-target-set into race-calculator-options (>1.4.1)
- const raceSelectedTargetSet = get<string>('race-calculator-target-set');
- const raceOptions = get<any>('race-calculator-options')
- || deepCopy(defaultRaceOptions);
- if (raceSelectedTargetSet !== null) {
- raceOptions.selectedTargetSet = raceSelectedTargetSet;
- set('race-calculator-options', raceOptions);
- unset('race-calculator-target-set');
- }
- if (raceOptions !== null && raceOptions.selectedTargetSet === undefined) {
- raceOptions.selectedTargetSet = defaultRaceOptions.selectedTargetSet;
- set('race-calculator-options', raceOptions);
- }
-
- // Move split-calculator-target-set into new split-calculator-options (>1.4.1)
- const splitSelectedTargetSet = get<string>('split-calculator-target-set');
- if (splitSelectedTargetSet !== null) {
- const splitOptions = { selectedTargetSet: splitSelectedTargetSet };
- set('split-calculator-options', splitOptions);
- unset('split-calculator-target-set');
- }
-
- // Move workout-calculator-target-set into workout-calculator-options (>1.4.1)
- const workoutSelectedTargetSet = get<string>('workout-calculator-target-set');
- const workoutOptions = get<any>('workout-calculator-options')
- || deepCopy(defaultWorkoutOptions);
- if (workoutSelectedTargetSet !== null) {
- workoutOptions.selectedTargetSet = workoutSelectedTargetSet;
- set('workout-calculator-options', workoutOptions);
- unset('workout-calculator-target-set');
- }
- if (workoutOptions !== null && workoutOptions.selectedTargetSet === undefined) {
- workoutOptions.selectedTargetSet = defaultWorkoutOptions.selectedTargetSet;
- set('workout-calculator-options', workoutOptions);
- }
-
- // Add customTargetNames property to workout-calculator-options (>1.4.1)
- if (workoutOptions.customTargetNames === undefined) {
- workoutOptions.customTargetNames = false;
- set('workout-calculator-options', workoutOptions);
- }
-
- /* eslint-enable @typescript-eslint/no-explicit-any */
-}
diff --git a/src/utils/targets.ts b/src/utils/targets.ts
@@ -1,207 +0,0 @@
-import { DistanceUnits, convertDistance, formatDistance, formatDuration } from '@/utils/units';
-import type { Distance } from '@/utils/units';
-
-/*
- * The two basic types of targets: those defined by distance and those defined by time
- */
-export enum TargetTypes {
- Distance = 'distance',
- Time = 'time',
-};
-
-/*
- * The types for basic standard targets and target sets used by the pace and race calculators
- */
-interface DistanceTarget {
- type: TargetTypes.Distance,
- distanceValue: number,
- distanceUnit: DistanceUnits,
-};
-interface TimeTarget {
- type: TargetTypes.Time,
- time: number,
-};
-export type StandardTarget = DistanceTarget | TimeTarget;
-export interface StandardTargetSet {
- name: string,
- targets: Array<StandardTarget>,
-};
-export interface StandardTargetSets {
- [key: string]: StandardTargetSet,
-};
-
-/*
- * The types for split calculator targets and target sets
- */
-export type SplitTarget = DistanceTarget & {
- splitTime?: number
-};
-export interface SplitTargetSet {
- name: string,
- targets: Array<SplitTarget>,
-};
-export interface SplitTargetSets {
- [key: string]: SplitTargetSet,
-};
-
-/*
- * The types for workout calculator targets and target sets
- */
-export type WorkoutTarget = StandardTarget & {
- splitValue: number,
- splitUnit: DistanceUnits,
- customName?: string,
-};
-export interface WorkoutTargetSet {
- name: string,
- targets: Array<WorkoutTarget>,
-};
-export interface WorkoutTargetSets {
- [key: string]: WorkoutTargetSet,
-};
-
-/*
- * The types for generic targets and target sets
- */
-export type Target = StandardTarget | SplitTarget | WorkoutTarget;
-export type TargetSet = StandardTargetSet | SplitTargetSet | WorkoutTargetSet;
-export type TargetSets = StandardTargetSets | SplitTargetSets | WorkoutTargetSets;
-
-/**
- * Sort an array of targets
- * @param {Array<Target>} targets The array of targets
- * @returns {Array<Target>} The sorted targets
- */
-export function sort(targets: Array<Target>): Array<Target> {
- return [
- ...targets.filter((item) => item.type === TargetTypes.Distance)
- .sort((a, b) => convertDistance(a.distanceValue, a.distanceUnit, DistanceUnits.Meters)
- - convertDistance(b.distanceValue, b.distanceUnit, DistanceUnits.Meters)),
-
- ...targets.filter((item) => item.type === TargetTypes.Time)
- .sort((a, b) => a.time - b.time),
- ];
-}
-
-/**
- * Generate a string description of a workout target
- * @param {WorkoutTarget} target The workout target
- * @return {string} The string description
- */
-export function workoutTargetToString(target: WorkoutTarget): string {
- let result = formatDistance({ distanceValue: target.splitValue, distanceUnit: target.splitUnit },
- false);
-
- if (target.type === TargetTypes.Time) {
- result += ' @ ' + formatDuration(target.time, 3, 2, false);
- } else if (target.distanceValue != target.splitValue || target.distanceUnit != target.splitUnit) {
- result += ' @ ' + formatDistance(target as Distance, false);
- }
- return result;
-}
-
-/*
- * The default target sets for each calculator
- */
-export const defaultTargetSets: { [key: string]: TargetSet } = {
- '_pace_targets': {
- name: 'Common Pace Targets',
- targets: sort([
- { type: TargetTypes.Distance, distanceValue: 100, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 200, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 300, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 400, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 600, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 800, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 1000, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 1200, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 1500, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 1600, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 3200, distanceUnit: DistanceUnits.Meters },
-
- { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 4, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Kilometers },
-
- { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Miles },
-
- { type: TargetTypes.Distance, distanceValue: 0.5, distanceUnit: DistanceUnits.Marathons },
- { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons },
-
- { type: TargetTypes.Time, time: 600 },
- { type: TargetTypes.Time, time: 1800 },
- { type: TargetTypes.Time, time: 3600 },
- ]),
- }, '_race_targets': {
- name: 'Common Race Targets',
- targets: sort([
- { type: TargetTypes.Distance, distanceValue: 400, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 800, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 1500, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 1600, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 3000, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 3200, distanceUnit: DistanceUnits.Meters },
- { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
-
- { type: TargetTypes.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetTypes.Distance, distanceValue: 15, distanceUnit: DistanceUnits.Kilometers },
-
- { type: TargetTypes.Distance, distanceValue: 0.5, distanceUnit: DistanceUnits.Marathons },
- { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons },
- ]),
- }, '_split_targets': {
- name: '5K Mile Splits',
- targets: [
- { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
- { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
- ],
- }, '_workout_targets': {
- name: 'Common Workout Targets',
- targets: [
- {
- splitValue: 400, splitUnit: DistanceUnits.Meters,
- type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles,
- },
- {
- splitValue: 800, splitUnit: DistanceUnits.Meters,
- type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers,
- },
- {
- splitValue: 1600, splitUnit: DistanceUnits.Meters,
- type: TargetTypes.Time, time: 3600,
- },
- {
- splitValue: 1, splitUnit: DistanceUnits.Miles,
- type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons,
- },
- ],
- },
-};
-export const defaultPaceTargetSets: StandardTargetSets = {
- '_pace_targets': defaultTargetSets._pace_targets,
-};
-export const defaultRaceTargetSets: StandardTargetSets = {
- '_race_targets': defaultTargetSets._race_targets,
-};
-export const defaultSplitTargetSets: SplitTargetSets = {
- '_split_targets': defaultTargetSets._split_targets as SplitTargetSet,
-};
-export const defaultWorkoutTargetSets: WorkoutTargetSets = {
- '_workout_targets': defaultTargetSets._workout_targets as WorkoutTargetSet,
-};
diff --git a/src/utils/units.ts b/src/utils/units.ts
@@ -1,392 +0,0 @@
-/*
- * The type for the data available for each unit
- */
-export interface UnitData {
- name: string,
- symbol: string,
- value: number,
-};
-
-/*
- * The available time units
- */
-export enum TimeUnits {
- Seconds = 'seconds',
- Minutes = 'minutes',
- Hours = 'hours',
-};
-export const TimeUnitData: { [key in TimeUnits]: UnitData } = {
- [TimeUnits.Seconds]: {
- name: 'Seconds',
- symbol: 's',
- value: 1,
- },
- [TimeUnits.Minutes]: {
- name: 'Minutes',
- symbol: 'min',
- value: 60,
- },
- [TimeUnits.Hours]: {
- name: 'Hours',
- symbol: 'hr',
- value: 3600,
- },
-};
-
-/*
- * The available distance units
- */
-export enum DistanceUnits {
- Meters = 'meters',
- Yards = 'yards',
- Kilometers = 'kilometers',
- Miles = 'miles',
- Marathons = 'marathons',
-};
-export const DistanceUnitData: { [key in DistanceUnits]: UnitData } = {
- [DistanceUnits.Meters]: {
- name: 'Meters',
- symbol: 'm',
- value: 1,
- },
- [DistanceUnits.Yards]: {
- name: 'Yards',
- symbol: 'yd',
- value: 0.9144,
- },
- [DistanceUnits.Kilometers]: {
- name: 'Kilometers',
- symbol: 'km',
- value: 1000,
- },
- [DistanceUnits.Miles]: {
- name: 'Miles',
- symbol: 'mi',
- value: 1609.344,
- },
- [DistanceUnits.Marathons]: {
- name: 'Marathons',
- symbol: 'Mar',
- value: 42195,
- },
-};
-
-/*
- * The available speed units
- */
-export enum SpeedUnits {
- MetersPerSecond = 'meters_per_second',
- KilometersPerHour = 'kilometers_per_hour',
- MilesPerHour = 'miles_per_hour',
-};
-export const SpeedUnitData: { [key in SpeedUnits]: UnitData } = {
- [SpeedUnits.MetersPerSecond]: {
- name: 'Meters per Second',
- symbol: 'm/s',
- value: 1,
- },
- [SpeedUnits.KilometersPerHour]: {
- name: 'Kilometers per Hour',
- symbol: 'kph',
- value: DistanceUnitData[DistanceUnits.Kilometers].value / TimeUnitData[TimeUnits.Hours].value,
- },
- [SpeedUnits.MilesPerHour]: {
- name: 'Miles per Hour',
- symbol: 'mph',
- value: DistanceUnitData[DistanceUnits.Miles].value / TimeUnitData[TimeUnits.Hours].value,
- },
-};
-
-/*
- * The available pace units
- */
-export enum PaceUnits {
- SecondsPerMeter = 'seconds_per_meter',
- TimePerKilometer = 'seconds_per_kilometer',
- TimePerMile = 'seconds_per_mile',
-};
-export const PaceUnitData: { [key in PaceUnits]: UnitData } = {
- [PaceUnits.SecondsPerMeter]: {
- name: 'Seconds per Meter',
- symbol: 's/m',
- value: 1,
- },
- [PaceUnits.TimePerKilometer]: {
- name: 'Time per Kilometer',
- symbol: '/ km',
- value: TimeUnitData[TimeUnits.Seconds].value / DistanceUnitData[DistanceUnits.Kilometers].value,
- },
- [PaceUnits.TimePerMile]: {
- name: 'Time per Mile',
- symbol: '/ mi',
- value: TimeUnitData[TimeUnits.Seconds].value / DistanceUnitData[DistanceUnits.Miles].value,
- },
-};
-
-/*
- * The available speed and pace units
- */
-export type SpeedPaceUnits = SpeedUnits | PaceUnits;
-
-/*
- * The type for a distance input
- */
-export interface Distance {
- distanceValue: number,
- distanceUnit: DistanceUnits,
-};
-
-/*
- * The type for a distance/time input pair
- */
-export interface DistanceTime extends Distance {
- time: number,
-};
-
-/*
- * The available unit systems
- */
-export enum UnitSystems {
- Metric = 'metric',
- Imperial = 'imperial',
-};
-
-/**
- * Convert between time units
- * @param {number} inputValue The input value
- * @param {string} inputUnit The unit of the input
- * @param {string} outputUnit The unit of the output
- * @returns {number} The output
- */
-export function convertTime(inputValue: number, inputUnit: TimeUnits,
- outputUnit: TimeUnits): number {
- return (inputValue * TimeUnitData[inputUnit].value) / TimeUnitData[outputUnit].value;
-}
-
-/**
- * Convert between distance units
- * @param {number} inputValue The input value
- * @param {string} inputUnit The unit of the input
- * @param {string} outputUnit The unit of the output
- * @returns {number} The output
- */
-export function convertDistance(inputValue: number, inputUnit: DistanceUnits,
- outputUnit: DistanceUnits): number {
- return (inputValue * DistanceUnitData[inputUnit].value) / DistanceUnitData[outputUnit].value;
-}
-
-/**
- * Convert between speed units
- * @param {number} inputValue The input value
- * @param {string} inputUnit The unit of the input
- * @param {string} outputUnit The unit of the output
- * @returns {number} The output
- */
-export function convertSpeed(inputValue: number, inputUnit: SpeedUnits,
- outputUnit: SpeedUnits): number {
- return (inputValue * SpeedUnitData[inputUnit].value) / SpeedUnitData[outputUnit].value;
-}
-
-/**
- * Convert between pace units
- * @param {number} inputValue The input value
- * @param {string} inputUnit The unit of the input
- * @param {string} outputUnit The unit of the output
- * @returns {number} The output
- */
-export function convertPace(inputValue: number, inputUnit: PaceUnits,
- outputUnit: PaceUnits): number {
- return (inputValue * PaceUnitData[inputUnit].value) / PaceUnitData[outputUnit].value;
-}
-
-/**
- * Convert between speed and/or pace units
- * @param {number} inputValue The input value
- * @param {string} inputUnit The unit of the input
- * @param {string} outputUnit The unit of the output
- * @returns {number} The output
- */
-export function convertSpeedPace(inputValue: number, inputUnit: SpeedPaceUnits,
- outputUnit: SpeedPaceUnits): number {
- // Calculate input speed
- let speed;
- if (inputUnit in PaceUnitData) {
- speed = 1 / (inputValue * PaceUnitData[inputUnit as PaceUnits].value);
- } else {
- speed = inputValue * SpeedUnitData[inputUnit as SpeedUnits].value;
- }
-
- // Calculate output
- if (outputUnit in PaceUnitData) {
- return (1 / speed) / PaceUnitData[outputUnit as PaceUnits].value;
- }
- return speed / SpeedUnitData[outputUnit as SpeedUnits].value;
-}
-
-/**
- * Detect the user's default unit system
- * @returns {UnitSystems} The default unit system
- */
-export function detectDefaultUnitSystem(): UnitSystems {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const language = (navigator.language || (navigator as any).userLanguage).toLowerCase();
- if (language.endsWith('-us') || language.endsWith('-mm')) {
- return UnitSystems.Imperial;
- }
- return UnitSystems.Metric;
-}
-
-/**
- * Format a number as a string
- * @param {number} value The number
- * @param {number} minPadding The minimum number of digits to show before the decimal point
- * @param {number} maxDigits The maximum number of digits to show after the decimal point
- * @param {boolean} extraDigits Whether to show extra zeros after the decimal point
- * @returns {string} The formatted number
- */
-export function formatNumber(value: number, minPadding: number = 0, maxDigits: number = 2,
- extraDigits: boolean = true): string {
-
- // Initialize result
- let result = '';
-
- // Remove sign
- const negative = value < 0;
- const fixedValue = Math.abs(value);
-
- // Address edge cases
- if (Number.isNaN(fixedValue)) {
- return 'NaN';
- }
- if (fixedValue === Infinity) {
- return negative ? '-Infinity' : 'Infinity';
- }
-
- // Convert number to string
- if (extraDigits) {
- result = fixedValue.toFixed(maxDigits);
- } else {
- const power = 10 ** maxDigits;
- result = (Math.round((fixedValue + Number.EPSILON) * power) / power).toString();
- }
-
- // Add padding
- const currentPadding = result.split('.')[0].length;
- result = result.padStart(result.length - currentPadding + minPadding, '0');
-
- // Add negative sign
- if (negative) {
- result = `-${result}`;
- }
-
- // Return result
- return result;
-}
-
-/**
- * Format a distance as a string
- * @param {Distance} input The distance
- * @param {boolean} extraDigits Whether to show extra zeros after the decimal point
- * @returns {string} The formatted distance
- */
-export function formatDistance(input: Distance, extraDigits: boolean) {
- return formatNumber(input.distanceValue, 0, 2, extraDigits) + ' '
- + DistanceUnitData[input.distanceUnit].symbol;
-}
-
-/**
- * Format a duration as a string
- * @param {number} value The duration (in seconds)
- * @param {number} minPadding The minimum number of digits to show before the decimal point
- * @param {number} maxDigits The maximum number of digits to show after the decimal point
- * @param {boolean} extraDigits Whether to show extra zeros after the decimal point
- * @returns {string} The formatted duration
- */
-export function formatDuration(value: number, minPadding: number = 6, maxDigits: number = 2,
- extraDigits: boolean = true): string {
- // Check if value is NaN
- if (Number.isNaN(value)) {
- return 'NaN';
- }
-
- // Initialize result
- let result = '';
-
- // Check value sign
- if (value < 0) {
- result += '-';
- }
-
- // Check if value is valid
- if (Math.abs(value) === Infinity) {
- return `${result}Infinity`;
- }
-
- // Validate padding
- let fixedPadding = Math.min(minPadding, 6);
-
- // Prevent rounding errors
- const fixedValue = parseFloat(Math.abs(value).toFixed(maxDigits));
-
- // Calculate parts
- const hours = Math.floor(fixedValue / 3600);
- const minutes = Math.floor((fixedValue % 3600) / 60);
- const seconds = fixedValue % 60;
-
- // Format parts
- if (hours !== 0 || fixedPadding >= 5) {
- result += hours.toString().padStart(fixedPadding - 4, '0');
- result += ':';
- fixedPadding = 4;
- }
- if (minutes !== 0 || fixedPadding >= 3) {
- result += minutes.toString().padStart(fixedPadding - 2, '0');
- result += ':';
- fixedPadding = 2;
- }
- result += formatNumber(seconds, fixedPadding, maxDigits, extraDigits);
-
- // Return result
- return result;
-}
-
-/**
- * Calculate the pace of a distance/time pair and format it as a string
- * @param {DistanceTime} input The input distance/time pair
- * @param {PaceUnits} unit The desired pace unit
- * @returns {string} The formatted pace
- */
-export function formatPace(input: DistanceTime, unit: PaceUnits) {
- const dist = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
- const pace = convertPace(input.time / dist, PaceUnits.SecondsPerMeter, unit)
- const result = formatDuration(pace, 3, 0, true) + ' ' + PaceUnitData[unit].symbol;
- return result;
-}
-
-/**
- * Get the default distance unit in a unit system
- * @param {UnitSystems} unitSystem The unit system
- * @returns {DistanceUnits} The default distance unit
- */
-export function getDefaultDistanceUnit(unitSystem: UnitSystems): DistanceUnits {
- return unitSystem === UnitSystems.Metric ? DistanceUnits.Kilometers : DistanceUnits.Miles;
-}
-
-/**
- * Get the default speed unit in a unit system
- * @param {UnitSystems} unitSystem The unit system
- * @returns {SpeedUnits} The default speed unit
- */
-export function getDefaultSpeedUnit(unitSystem: UnitSystems): SpeedUnits {
- return unitSystem === UnitSystems.Metric ? SpeedUnits.KilometersPerHour
- : SpeedUnits.MilesPerHour;
-}
-
-/**
- * Get the default pace unit in a unit system
- * @param {UnitSystems} unitSystem The unit system
- * @returns {PaceUnits} The default pace unit
- */
-export function getDefaultPaceUnit(unitSystem: UnitSystems): PaceUnits {
- return unitSystem === UnitSystems.Metric ? PaceUnits.TimePerKilometer : PaceUnits.TimePerMile;
-}
diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue
@@ -42,12 +42,12 @@
<script setup lang="ts">
import { computed } from 'vue';
-import * as calcUtils from '@/utils/calculators';
+import * as calculators from '@/core/calculators';
import type { BatchOptions, RaceOptions, StandardOptions, TargetResult,
- WorkoutOptions } from '@/utils/calculators';
-import * as targetUtils from '@/utils/targets';
-import { UnitSystems, detectDefaultUnitSystem } from '@/utils/units';
-import type { Distance, DistanceTime } from '@/utils/units';
+ WorkoutOptions } from '@/core/calculators';
+import * as targetUtils from '@/core/targets';
+import { UnitSystems, detectDefaultUnitSystem } from '@/core/units';
+import type { Distance, DistanceTime } from '@/core/units';
import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue';
import DoubleOutputTable from '@/components/DoubleOutputTable.vue';
@@ -60,12 +60,13 @@ import useStorage from '@/composables/useStorage';
/*
* The input pace
*/
-const input = useStorage<DistanceTime>('batch-calculator-input', calcUtils.defaultInput);
+const input = useStorage<DistanceTime>('batch-calculator-input', calculators.defaultInput);
/*
* The batch input options
*/
-const options = useStorage<BatchOptions>('batch-calculator-options', calcUtils.defaultBatchOptions);
+const options = useStorage<BatchOptions>('batch-calculator-options',
+ calculators.defaultBatchOptions);
/*
* The default unit system
@@ -79,18 +80,18 @@ const paceTargetSets = useStorage<targetUtils.StandardTargetSets>('pace-calculat
targetUtils.defaultPaceTargetSets);
const raceTargetSets = useStorage<targetUtils.StandardTargetSets>('race-calculator-target-sets',
targetUtils.defaultRaceTargetSets);
-const workoutTargetSets = useStorage<targetUtils.WorkoutTargetSets>('workout-calculator-target-sets',
- targetUtils.defaultWorkoutTargetSets);
+const workoutTargetSets = useStorage<targetUtils.WorkoutTargetSets>(
+ 'workout-calculator-target-sets', targetUtils.defaultWorkoutTargetSets);
/*
* The options for each calculator
*/
const paceOptions = useStorage<StandardOptions>('pace-calculator-options',
- calcUtils.defaultPaceOptions);
+ calculators.defaultPaceOptions);
const raceOptions = useStorage<RaceOptions>('race-calculator-options',
- calcUtils.defaultRaceOptions);
+ calculators.defaultRaceOptions);
const workoutOptions = useStorage<WorkoutOptions>('workout-calculator-options',
- calcUtils.defaultWorkoutOptions);
+ calculators.defaultWorkoutOptions);
/*
* The input distance
@@ -117,30 +118,30 @@ const inputTimes = computed<Array<number>>(() => {
const targetSets = computed<targetUtils.TargetSets>({
get: () => {
switch (options.value.calculator) {
- case (calcUtils.Calculators.Pace): {
+ case (calculators.Calculators.Pace): {
return paceTargetSets.value;
}
- case (calcUtils.Calculators.Race): {
+ case (calculators.Calculators.Race): {
return raceTargetSets.value;
}
default:
- case (calcUtils.Calculators.Workout): {
+ case (calculators.Calculators.Workout): {
return workoutTargetSets.value;
}
}
},
set: (newValue: targetUtils.TargetSets) => {
switch (options.value.calculator) {
- case (calcUtils.Calculators.Pace): {
+ case (calculators.Calculators.Pace): {
paceTargetSets.value = newValue as targetUtils.StandardTargetSets;
break;
}
- case (calcUtils.Calculators.Race): {
+ case (calculators.Calculators.Race): {
raceTargetSets.value = newValue as targetUtils.StandardTargetSets;
break;
}
default:
- case (calcUtils.Calculators.Workout): {
+ case (calculators.Calculators.Workout): {
workoutTargetSets.value = newValue as targetUtils.WorkoutTargetSets;
break;
}
@@ -154,30 +155,30 @@ const targetSets = computed<targetUtils.TargetSets>({
const calcOptions = computed<StandardOptions | RaceOptions | WorkoutOptions>({
get: () => {
switch (options.value.calculator) {
- case (calcUtils.Calculators.Pace): {
+ case (calculators.Calculators.Pace): {
return paceOptions.value;
}
- case (calcUtils.Calculators.Race): {
+ case (calculators.Calculators.Race): {
return raceOptions.value;
}
default:
- case (calcUtils.Calculators.Workout): {
+ case (calculators.Calculators.Workout): {
return workoutOptions.value;
}
}
},
set: (newValue: StandardOptions | RaceOptions | WorkoutOptions) => {
switch(options.value.calculator) {
- case (calcUtils.Calculators.Pace): {
+ case (calculators.Calculators.Pace): {
paceOptions.value = newValue as StandardOptions;
break;
}
- case (calcUtils.Calculators.Race): {
+ case (calculators.Calculators.Race): {
raceOptions.value = newValue as RaceOptions;
break;
}
default:
- case (calcUtils.Calculators.Workout): {
+ case (calculators.Calculators.Workout): {
workoutOptions.value = newValue as WorkoutOptions;
break;
}
@@ -190,16 +191,16 @@ const calcOptions = computed<StandardOptions | RaceOptions | WorkoutOptions>({
*/
const calculateResult = computed<(x: DistanceTime, y: targetUtils.Target) => TargetResult>(() => {
switch(options.value.calculator) {
- case (calcUtils.Calculators.Pace): {
- return (x,y) => calcUtils.calculatePaceResults(x, y, defaultUnitSystem.value, false);
+ case (calculators.Calculators.Pace): {
+ return (x,y) => calculators.calculatePaceResults(x, y, defaultUnitSystem.value, false);
}
- case (calcUtils.Calculators.Race): {
- return (x,y) => calcUtils.calculateRaceResults(x, y, raceOptions.value,
+ case (calculators.Calculators.Race): {
+ return (x,y) => calculators.calculateRaceResults(x, y, raceOptions.value,
defaultUnitSystem.value, false);
}
default:
- case (calcUtils.Calculators.Workout): {
- return (x,y) => calcUtils.calculateWorkoutResults(x, y as targetUtils.WorkoutTarget,
+ case (calculators.Calculators.Workout): {
+ return (x,y) => calculators.calculateWorkoutResults(x, y as targetUtils.WorkoutTarget,
workoutOptions.value, false);
}
}
diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue
@@ -23,12 +23,12 @@
<script setup lang="ts">
import { Calculators, calculatePaceResults, defaultInput,
- defaultPaceOptions } from '@/utils/calculators';
-import type { StandardOptions } from '@/utils/calculators';
-import { defaultPaceTargetSets } from '@/utils/targets';
-import type { StandardTargetSets } from '@/utils/targets';
-import { UnitSystems, detectDefaultUnitSystem } from '@/utils/units';
-import type { DistanceTime } from '@/utils/units';
+ defaultPaceOptions } from '@/core/calculators';
+import type { StandardOptions } from '@/core/calculators';
+import { defaultPaceTargetSets } from '@/core/targets';
+import type { StandardTargetSets } from '@/core/targets';
+import { UnitSystems, detectDefaultUnitSystem } from '@/core/units';
+import type { DistanceTime } from '@/core/units';
import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue';
import PaceInput from '@/components/PaceInput.vue';
diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue
@@ -42,12 +42,12 @@
import { computed } from 'vue';
import { Calculators, calculateRaceResults, calculateRaceStats, defaultInput,
- defaultRaceOptions } from '@/utils/calculators';
-import type { RaceOptions, RaceStats } from '@/utils/calculators';
-import { defaultRaceTargetSets } from '@/utils/targets';
-import type { StandardTargetSets } from '@/utils/targets';
-import { UnitSystems, detectDefaultUnitSystem, formatNumber } from '@/utils/units';
-import type { DistanceTime } from '@/utils/units';
+ defaultRaceOptions } from '@/core/calculators';
+import type { RaceOptions, RaceStats } from '@/core/calculators';
+import { defaultRaceTargetSets } from '@/core/targets';
+import type { StandardTargetSets } from '@/core/targets';
+import { UnitSystems, detectDefaultUnitSystem, formatNumber } from '@/core/units';
+import type { DistanceTime } from '@/core/units';
import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue';
import PaceInput from '@/components/PaceInput.vue';
diff --git a/src/views/SplitCalculator.vue b/src/views/SplitCalculator.vue
@@ -14,11 +14,11 @@
<script setup lang="ts">
import { computed } from 'vue';
-import { Calculators, defaultSplitOptions } from '@/utils/calculators';
-import type { StandardOptions } from '@/utils/calculators';
-import { defaultSplitTargetSets } from '@/utils/targets';
-import type { SplitTargetSets } from '@/utils/targets';
-import { UnitSystems, detectDefaultUnitSystem } from '@/utils/units';
+import { Calculators, defaultSplitOptions } from '@/core/calculators';
+import type { StandardOptions } from '@/core/calculators';
+import { defaultSplitTargetSets } from '@/core/targets';
+import type { SplitTargetSets } from '@/core/targets';
+import { UnitSystems, detectDefaultUnitSystem } from '@/core/units';
import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue';
import SplitOutputTable from '@/components/SplitOutputTable.vue';
diff --git a/src/views/UnitCalculator.vue b/src/views/UnitCalculator.vue
@@ -12,7 +12,7 @@
v-model="input.inputValue" :min="0" :digits="2"/>
<select v-model="input.inputUnit" class="input-units" aria-label="Input units">
- <option v-for="(value, key) in units" :key="key" :value="key">
+ <option v-for="(value, key) in categoryUnits" :key="key" :value="key">
{{ value?.name }}
</option>
</select>
@@ -20,14 +20,14 @@
<span class="equals"> = </span>
<span v-if="isTimeUnit(input.outputUnit)" class="output-value" aria-label="Output value">
- {{ unitUtils.formatDuration(outputValue, 6, 3, true) }}
+ {{ units.formatDuration(outputValue, 6, 3, true) }}
</span>
<span v-else class="output-value" aria-label="Output value">
- {{ unitUtils.formatNumber(outputValue, 0, 3, true) }}
+ {{ units.formatNumber(outputValue, 0, 3, true) }}
</span>
<select v-model="input.outputUnit" class="output-units" aria-label="Output units">
- <option v-for="(value, key) in units" :key="key" :value="key">
+ <option v-for="(value, key) in categoryUnits" :key="key" :value="key">
{{ value?.name }}
</option>
</select>
@@ -37,7 +37,7 @@
<script setup lang="ts">
import { computed } from 'vue';
-import * as unitUtils from '@/utils/units';
+import * as units from '@/core/units';
import DecimalInput from '@/components/DecimalInput.vue';
import TimeInput from '@/components/TimeInput.vue';
@@ -56,12 +56,12 @@ enum UnitTypes {
/*
* The supported time units: Hours, Minutes, Seconds, and 'hh:mm:ss'
*/
-type ExtendedTimeUnits = unitUtils.TimeUnits | 'hh:mm:ss';
+type ExtendedTimeUnits = units.TimeUnits | 'hh:mm:ss';
/*
* The support units from all categories
*/
-type AllUnits = unitUtils.DistanceUnits | ExtendedTimeUnits | unitUtils.SpeedPaceUnits;
+type AllUnits = units.DistanceUnits | ExtendedTimeUnits | units.SpeedPaceUnits;
/*
* The type of the calculator inputs
@@ -69,8 +69,8 @@ type AllUnits = unitUtils.DistanceUnits | ExtendedTimeUnits | unitUtils.SpeedPac
interface UnitCalculatorInputs {
[UnitTypes.Distance]: {
inputValue: number,
- inputUnit: unitUtils.DistanceUnits,
- outputUnit: unitUtils.DistanceUnits,
+ inputUnit: units.DistanceUnits,
+ outputUnit: units.DistanceUnits,
},
[UnitTypes.Time]: {
inputValue: number,
@@ -79,8 +79,8 @@ interface UnitCalculatorInputs {
},
[UnitTypes.SpeedPace]: {
inputValue: number,
- inputUnit: unitUtils.SpeedPaceUnits,
- outputUnit: unitUtils.SpeedPaceUnits,
+ inputUnit: units.SpeedPaceUnits,
+ outputUnit: units.SpeedPaceUnits,
},
};
@@ -90,18 +90,18 @@ interface UnitCalculatorInputs {
const inputs = useStorage<UnitCalculatorInputs>('unit-calculator-inputs', {
[UnitTypes.Distance]: {
inputValue: 1,
- inputUnit: unitUtils.DistanceUnits.Miles,
- outputUnit: unitUtils.DistanceUnits.Kilometers,
+ inputUnit: units.DistanceUnits.Miles,
+ outputUnit: units.DistanceUnits.Kilometers,
},
[UnitTypes.Time]: {
inputValue: 1,
- inputUnit: unitUtils.TimeUnits.Seconds,
+ inputUnit: units.TimeUnits.Seconds,
outputUnit: 'hh:mm:ss',
},
[UnitTypes.SpeedPace]: {
inputValue: 600,
- inputUnit: unitUtils.PaceUnits.TimePerMile,
- outputUnit: unitUtils.SpeedUnits.MilesPerHour,
+ inputUnit: units.PaceUnits.TimePerMile,
+ outputUnit: units.SpeedUnits.MilesPerHour,
},
});
@@ -123,8 +123,8 @@ const input = computed<{ inputValue: number, inputUnit: AllUnits, outputUnit: Al
case UnitTypes.Distance: {
inputs.value[category.value] = {
inputValue: newValue.inputValue,
- inputUnit: newValue.inputUnit as unitUtils.DistanceUnits,
- outputUnit: newValue.outputUnit as unitUtils.DistanceUnits,
+ inputUnit: newValue.inputUnit as units.DistanceUnits,
+ outputUnit: newValue.outputUnit as units.DistanceUnits,
};
break;
}
@@ -139,8 +139,8 @@ const input = computed<{ inputValue: number, inputUnit: AllUnits, outputUnit: Al
case UnitTypes.SpeedPace: {
inputs.value[category.value] = {
inputValue: newValue.inputValue,
- inputUnit: newValue.inputUnit as unitUtils.SpeedPaceUnits,
- outputUnit: newValue.outputUnit as unitUtils.SpeedPaceUnits,
+ inputUnit: newValue.inputUnit as units.SpeedPaceUnits,
+ outputUnit: newValue.outputUnit as units.SpeedPaceUnits,
};
break;
}
@@ -151,15 +151,15 @@ const input = computed<{ inputValue: number, inputUnit: AllUnits, outputUnit: Al
/*
* The data for the units in the current category
*/
-const units = computed<{ [key in AllUnits]?: unitUtils.UnitData }>(() => {
+const categoryUnits = computed<{ [key in AllUnits]?: units.UnitData }>(() => {
switch (category.value) {
default:
case UnitTypes.Distance: {
- return unitUtils.DistanceUnitData;
+ return units.DistanceUnitData;
}
case UnitTypes.Time: {
return {
- ...unitUtils.TimeUnitData,
+ ...units.TimeUnitData,
'hh:mm:ss': {
name: 'hh:mm:ss',
symbol: '',
@@ -168,7 +168,7 @@ const units = computed<{ [key in AllUnits]?: unitUtils.UnitData }>(() => {
};
}
case UnitTypes.SpeedPace: {
- return { ...unitUtils.PaceUnitData, ...unitUtils.SpeedUnitData };
+ return { ...units.PaceUnitData, ...units.SpeedUnitData };
}
}
});
@@ -180,9 +180,9 @@ const outputValue = computed<number>(() => {
switch (category.value) {
default:
case UnitTypes.Distance: {
- return unitUtils.convertDistance(input.value.inputValue,
- input.value.inputUnit as unitUtils.DistanceUnits,
- input.value.outputUnit as unitUtils.DistanceUnits);
+ return units.convertDistance(input.value.inputValue,
+ input.value.inputUnit as units.DistanceUnits,
+ input.value.outputUnit as units.DistanceUnits);
}
case UnitTypes.Time: {
// Correct input and output units for 'hh:mm:ss' unit
@@ -190,13 +190,13 @@ const outputValue = computed<number>(() => {
const realOutput = input.value.outputUnit === 'hh:mm:ss' ? 'seconds' : input.value.outputUnit;
// Calculate conversion
- return unitUtils.convertTime(input.value.inputValue, realInput as unitUtils.TimeUnits,
- realOutput as unitUtils.TimeUnits);
+ return units.convertTime(input.value.inputValue, realInput as units.TimeUnits,
+ realOutput as units.TimeUnits);
}
case UnitTypes.SpeedPace: {
- return unitUtils.convertSpeedPace(input.value.inputValue,
- input.value.inputUnit as unitUtils.SpeedPaceUnits,
- input.value.outputUnit as unitUtils.SpeedPaceUnits);
+ return units.convertSpeedPace(input.value.inputValue,
+ input.value.inputUnit as units.SpeedPaceUnits,
+ input.value.outputUnit as units.SpeedPaceUnits);
}
}
});
@@ -207,7 +207,7 @@ const outputValue = computed<number>(() => {
* @returns {boolean} Whether the unit should be represented as a time
*/
function isTimeUnit(unit: AllUnits): boolean {
- return unit in unitUtils.PaceUnitData || unit === 'hh:mm:ss';
+ return unit in units.PaceUnitData || unit === 'hh:mm:ss';
}
</script>
diff --git a/src/views/WorkoutCalculator.vue b/src/views/WorkoutCalculator.vue
@@ -23,12 +23,12 @@
<script setup lang="ts">
import { Calculators, calculateWorkoutResults, defaultInput,
- defaultWorkoutOptions } from '@/utils/calculators';
-import type { WorkoutOptions } from '@/utils/calculators';
-import { defaultWorkoutTargetSets } from '@/utils/targets';
-import type { WorkoutTarget, WorkoutTargetSets } from '@/utils/targets';
-import { UnitSystems, detectDefaultUnitSystem } from '@/utils/units';
-import type { DistanceTime } from '@/utils/units';
+ defaultWorkoutOptions } from '@/core/calculators';
+import type { WorkoutOptions } from '@/core/calculators';
+import { defaultWorkoutTargetSets } from '@/core/targets';
+import type { WorkoutTarget, WorkoutTargetSets } from '@/core/targets';
+import { UnitSystems, detectDefaultUnitSystem } from '@/core/units';
+import type { DistanceTime } from '@/core/units';
import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue';
import PaceInput from '@/components/PaceInput.vue';
diff --git a/tests/unit/core/calculators.spec.js b/tests/unit/core/calculators.spec.js
@@ -0,0 +1,255 @@
+import { describe, test, expect } from 'vitest';
+import * as calculators from '@/core/calculators';
+
+describe('calculatePaceResults method', () => {
+ test('should correctly calculate pace times', () => {
+ const input = {
+ distanceValue: 1,
+ distanceUnit: 'kilometers',
+ time: 100,
+ };
+ const target = {
+ distanceValue: 20,
+ distanceUnit: 'meters',
+ type: 'distance',
+ };
+
+ const result = calculators.calculatePaceResults(input, target, 'metric');
+
+ expect(result).to.deep.equal({
+ key: '20 m',
+ value: '0:02.00',
+ pace: '1:40 / km',
+ result: 'value',
+ sort: 2,
+ });
+ });
+
+ test('should correctly calculate pace distances according to default units setting', () => {
+ const input = {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 1200,
+ };
+ const target = {
+ time: 600,
+ type: 'time',
+ };
+
+ const result1 = calculators.calculatePaceResults(input, target, 'metric');
+ const result2 = calculators.calculatePaceResults(input, target, 'imperial');
+
+ expect(result1.key).to.equal('1.61 km');
+ expect(result1.value).to.equal('10:00');
+ expect(result1.pace).to.equal('6:13 / km');
+ expect(result1.result).to.equal('key');
+ expect(result1.sort).to.be.closeTo(600, 0.01);
+
+ expect(result2.key).to.equal('1.00 mi');
+ expect(result2.value).to.equal('10:00');
+ expect(result2.pace).to.equal('10:00 / mi');
+ expect(result2.result).to.equal('key');
+ expect(result2.sort).to.be.closeTo(600, 0.01);
+ });
+});
+
+describe('calculateRaceResults method', () => {
+ test('should correctly predict race times', () => {
+ const input = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ };
+ const target = {
+ distanceValue: 10,
+ distanceUnit: 'kilometers',
+ type: 'distance',
+ };
+ const options = {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ }
+
+ const result = calculators.calculateRaceResults(input, target, options, 'imperial');
+
+ expect(result.key).to.equal('10 km');
+ expect(result.value).to.equal('41:34.80');
+ expect(result.pace).to.equal('6:41 / mi');
+ expect(result.result).to.equal('value');
+ expect(result.sort).to.be.closeTo(2494.80, 0.01);
+ });
+
+ test('should correctly calculate race distances according to default units setting', () => {
+ const input = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ };
+ const target = {
+ time: 2495,
+ type: 'time',
+ };
+ const options = {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ }
+
+ const result1 = calculators.calculateRaceResults(input, target, options, 'metric');
+ const result2 = calculators.calculateRaceResults(input, target, options, 'imperial');
+
+ expect(result1.key).to.equal('10.00 km');
+ expect(result1.value).to.equal('41:35');
+ expect(result1.pace).to.equal('4:09 / km');
+ expect(result1.result).to.equal('key');
+ expect(result1.sort).to.equal(2495);
+
+ expect(result2.key).to.equal('6.21 mi');
+ expect(result2.value).to.equal('41:35');
+ expect(result2.pace).to.equal('6:41 / mi');
+ expect(result2.result).to.equal('key');
+ expect(result2.sort).to.equal(2495);
+ });
+
+ test('should correctly predict race times according to race options', () => {
+ const input = {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ };
+ const target = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ type: 'distance',
+ };
+ const options = {
+ model: 'RiegelModel',
+ riegelExponent: 1.12,
+ }
+
+ const result = calculators.calculateRaceResults(input, target, options, 'imperial');
+
+ expect(result.key).to.equal('5 km');
+ expect(result.value).to.equal('17:11.78');
+ expect(result.pace).to.equal('5:32 / mi');
+ expect(result.result).to.equal('value');
+ expect(result.sort).to.be.closeTo(1031.77, 0.01);
+ });
+});
+
+describe('calculateRaceStats method', () => {
+ test('should correctly calculate race statistics', () => {
+ const input = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ };
+
+ const results = calculators.calculateRaceStats(input);
+
+ expect(results.purdyPoints).to.be.closeTo(454.5, 0.1);
+ expect(results.vo2).to.be.closeTo(47.4, 0.1);
+ expect(results.vo2MaxPercentage).to.be.closeTo(95.3, 0.1);
+ expect(results.vo2Max).to.be.closeTo(49.8, 0.1);
+ });
+});
+
+describe('calculateWorkoutResults method', () => {
+ test('should correctly calculate distance-based workouts according to race options', () => {
+ const input = {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ };
+ const target = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers', // 5k split is ~17:11.77
+ splitValue: 1000,
+ splitUnit: 'meters',
+ type: 'distance',
+ };
+ const options = {
+ customTargetNames: false,
+ model: 'RiegelModel',
+ riegelExponent: 1.12,
+ }
+
+ const result = calculators.calculateWorkoutResults(input, target, options);
+
+ expect(result.key).to.equal('1000 m @ 5 km');
+ expect(result.value).to.equal('3:26.36');
+ expect(result.pace).to.equal('');
+ expect(result.result).to.equal('value');
+ expect(result.sort).to.be.closeTo(206.35, 0.01);
+ });
+
+ test('should correctly calculate distance-based workouts according to custom names', () => {
+ const input = {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ };
+ const target_1 = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers', // 5k split is ~17:11.77
+ splitValue: 1000,
+ splitUnit: 'meters',
+ type: 'distance',
+ // no custom name
+ };
+ const target_2 = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers', // 5k split is ~17:11.77
+ splitValue: 1000,
+ splitUnit: 'meters',
+ type: 'distance',
+ customName: 'my custom name',
+ };
+ const options_a = {
+ customTargetNames: false,
+ model: 'RiegelModel',
+ riegelExponent: 1.12,
+ };
+ const options_b = {
+ customTargetNames: true,
+ model: 'RiegelModel',
+ riegelExponent: 1.12,
+ };
+
+ const result1a = calculators.calculateWorkoutResults(input, target_1, options_a);
+ const result1b = calculators.calculateWorkoutResults(input, target_1, options_b);
+ const result2a = calculators.calculateWorkoutResults(input, target_2, options_a);
+ const result2b = calculators.calculateWorkoutResults(input, target_2, options_b);
+
+ expect(result1a.key).to.equal('1000 m @ 5 km');
+ expect(result1b.key).to.equal('1000 m @ 5 km');
+ expect(result2a.key).to.equal('1000 m @ 5 km');
+ expect(result2b.key).to.equal('my custom name');
+ });
+
+ test('should correctly calculate time-based workouts', () => {
+ const input = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ };
+ const target = {
+ time: 2495, // ~10k split is 41:35
+ splitValue: 1,
+ splitUnit: 'miles',
+ type: 'time',
+ };
+ const options = {
+ customTargetNames: false,
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ }
+
+ const result = calculators.calculateWorkoutResults(input, target, options);
+
+ expect(result.key).to.equal('1 mi @ 41:35');
+ expect(result.value).to.equal('6:41.50');
+ expect(result.pace).to.equal('');
+ expect(result.result).to.equal('value');
+ expect(result.sort).to.be.closeTo(401.50, 0.01);
+ });
+});
diff --git a/tests/unit/core/racePrediction.spec.js b/tests/unit/core/racePrediction.spec.js
@@ -0,0 +1,166 @@
+import { describe, test, expect } from 'vitest';
+import * as racePrediction from '@/core/racePrediction';
+
+describe('predictTime method', () => {
+ describe('PredictTime method', () => {
+ test('Average Model', () => {
+ const riegel = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const cameron = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const vo2Max = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
+
+ const result = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ expect(result).to.equal(expected);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictTime(5000, 1200, 5000, 'AverageModel');
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
+ });
+
+ describe('Purdy Points Model', () => {
+ test('Predictions should be approximately correct', () => {
+ const result = racePrediction.predictTime(5000, 1200, 10000, 'PurdyPointsModel');
+ expect(result).to.be.closeTo(2490, 1);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictTime(5000, 1200, 5000, 'PurdyPointsModel');
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
+ });
+
+ describe('VO2 Max Model', () => {
+ test('Predictions should be approximately correct', () => {
+ const result = racePrediction.predictTime(5000, 1200, 10000, 'VO2MaxModel');
+ expect(result).to.be.closeTo(2488, 1);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictTime(5000, 1200, 5000, 'VO2MaxModel');
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
+ });
+
+ describe('Cameron Model', () => {
+ test('Predictions should be approximately correct', () => {
+ const result = racePrediction.predictTime(5000, 1200, 10000, 'CameronModel');
+ expect(result).to.be.closeTo(2500, 1);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictTime(5000, 1200, 5000, 'CameronModel');
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
+ });
+
+ describe('Riegel Model', () => {
+ test('Predictions should be approximately correct', () => {
+ const result = racePrediction.predictTime(5000, 1200, 10000, 'RiegelModel');
+ expect(result).to.be.closeTo(2502, 1);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictTime(5000, 1200, 5000, 'RiegelModel');
+ expect(result).to.be.closeTo(1200, 0.001);
+ });
+ });
+});
+
+describe('predictDistance method', () => {
+ describe('Average Model', () => {
+ test('Predictions should be correct', () => {
+ const riegel = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const cameron = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const vo2Max = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
+
+ const result = racePrediction.predictDistance(1200, 5000, expected);
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictDistance(1200, 5000, 1200, 'AverageModel');
+ expect(result).to.be.closeTo(5000, 0.001);
+ });
+ });
+
+ describe('Purdy Points Model', () => {
+ test('Predictions should be approximately correct', () => {
+ const result = racePrediction.predictDistance(1200, 5000, 2490, 'PurdyPointsModel');
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictDistance(1200, 5000, 1200, 'PurdyPointsModel');
+ expect(result).to.be.closeTo(5000, 0.001);
+ });
+ });
+
+ describe('VO2 Max Model', () => {
+ test('Predictions should be approximately correct', () => {
+ const result = racePrediction.predictDistance(1200, 5000, 2488, 'VO2MaxModel');
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictDistance(1200, 5000, 1200, 'VO2MaxModel');
+ expect(result).to.be.closeTo(5000, 0.001);
+ });
+ });
+
+ describe('Cameron Model', () => {
+ test('Predictions should be approximately correct', () => {
+ const result = racePrediction.predictDistance(1200, 5000, 2500, 'CameronModel');
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictDistance(1200, 5000, 1200, 'CameronModel');
+ expect(result).to.be.closeTo(5000, 0.001);
+ });
+ });
+
+ describe('Riegel Model', () => {
+ test('Predictions should be approximately correct', () => {
+ const result = racePrediction.predictDistance(1200, 5000, 2502, 'RiegelModel');
+ expect(result).to.be.closeTo(10000, 10);
+ });
+
+ test('Should predict identical times for itentical distances', () => {
+ const result = racePrediction.predictDistance(1200, 5000, 1200, 'RiegelModel');
+ expect(result).to.be.closeTo(5000, 0.001);
+ });
+ });
+});
+
+describe('getVO2 method', () => {
+ test('Result should be approximately correct', () => {
+ const result = racePrediction.getVO2(5000, 1200);
+ expect(result).to.be.closeTo(47.4, 0.1);
+ });
+});
+
+describe('getVO2Percentage method', () => {
+ test('Result should be approximately correct', () => {
+ const result = racePrediction.getVO2Percentage(660);
+ expect(result).to.be.closeTo(1, 0.001);
+ });
+});
+
+describe('getVO2Max method', () => {
+ test('Result should be approximately correct', () => {
+ const result = racePrediction.getVO2Max(5000, 1200);
+ expect(result).to.be.closeTo(49.8, 0.1);
+ });
+});
+
+describe('getPurdyPoints method', () => {
+ test('Result should be approximately correct', () => {
+ const result = racePrediction.getPurdyPoints(5000, 1200);
+ expect(result).to.be.closeTo(454, 1);
+ });
+});
diff --git a/tests/unit/core/targets.spec.js b/tests/unit/core/targets.spec.js
@@ -0,0 +1,59 @@
+import { describe, test, expect } from 'vitest';
+import * as targets from '@/core/targets';
+
+describe('sort method', () => {
+ test('should correctly sort targets', () => {
+ // Initialize unsorted and sorted targets
+ const input = [
+ { time: 60, type: 'time' },
+ { distanceUnit: 'kilometers', distanceValue: 5, type: 'distance' },
+ { distanceUnit: 'miles', distanceValue: 3, type: 'distance' },
+ ];
+ const expected = [
+ { distanceUnit: 'miles', distanceValue: 3, type: 'distance' },
+ { distanceUnit: 'kilometers', distanceValue: 5, type: 'distance' },
+ { time: 60, type: 'time' },
+ ];
+
+ // Assert sort method sorts targets correctly
+ expect(targets.sort(input)).to.deep.equal(expected);
+ });
+});
+
+describe('workoutTargetToString method', () => {
+ test('should correctly stringify time target', () => {
+ // Initialize original and stringified target
+ const input = {
+ splitValue: 1600, splitUnit: 'meters',
+ type: 'time', time: 3600,
+ };
+ const expected = '1600 m @ 1:00:00';
+
+ // Assert sort method sorts targets correctly
+ expect(targets.workoutTargetToString(input)).to.deep.equal(expected);
+ });
+
+ test('should correctly stringify distance target', () => {
+ // Initialize original and stringified target
+ const input = {
+ splitValue: 800, splitUnit: 'meters',
+ type: 'distance', distanceValue: 5, distanceUnit: 'kilometers',
+ };
+ const expected = '800 m @ 5 km';
+
+ // Assert sort method sorts targets correctly
+ expect(targets.workoutTargetToString(input)).to.deep.equal(expected);
+ });
+
+ test('should correctly stringify race target', () => {
+ // Initialize original and stringified target
+ const input = {
+ splitValue: 5, splitUnit: 'kilometers',
+ type: 'distance', distanceValue: 5, distanceUnit: 'kilometers',
+ };
+ const expected = '5 km';
+
+ // Assert sort method sorts targets correctly
+ expect(targets.workoutTargetToString(input)).to.deep.equal(expected);
+ });
+});
diff --git a/tests/unit/core/units.spec.js b/tests/unit/core/units.spec.js
@@ -0,0 +1,439 @@
+import { describe, test, expect } from 'vitest';
+import * as units from '@/core/units';
+
+describe('convertTime method', () => {
+ test('90 seconds should equal 1.5 minutes', () => {
+ const result = units.convertTime(90, 'seconds', 'minutes');
+ expect(result).to.equal(1.5);
+ });
+
+ test('1.5 minutes should equal 95 seconds', () => {
+ const result = units.convertTime(1.5, 'minutes', 'seconds');
+ expect(result).to.equal(90);
+ });
+});
+
+describe('convertDistance method', () => {
+ test('100 meters should equal 0.1 kilometers', () => {
+ const result = units.convertDistance(100, 'meters', 'kilometers');
+ expect(result).to.equal(0.1);
+ });
+
+ test('0.1 kilometers should equal 100 meters', () => {
+ const result = units.convertDistance(0.1, 'kilometers', 'meters');
+ expect(result).to.equal(100);
+ });
+});
+
+describe('convertSpeed method', () => {
+ test('1000 meters per seconds should equal 3600 kilometers per hour', () => {
+ const result = units.convertSpeed(1000, 'meters_per_second', 'kilometers_per_hour');
+ expect(result).to.equal(3600);
+ });
+
+ test('3600 kilometers per hour should equal 1000 meters per second', () => {
+ const result = units.convertSpeed(3600, 'kilometers_per_hour', 'meters_per_second');
+ expect(result).to.equal(1000);
+ });
+});
+
+describe('convertPace method', () => {
+ test('1 second per meter should equal 1000 seconds per kilometer', () => {
+ const result = units.convertPace(1, 'seconds_per_meter', 'seconds_per_kilometer');
+ expect(result).to.equal(1000);
+ });
+
+ test('1000 seconds per kilometer should equal 1 second per meter', () => {
+ const result = units.convertPace(1000, 'seconds_per_kilometer', 'seconds_per_meter');
+ expect(result).to.equal(1);
+ });
+});
+
+describe('convertSpeedPace method', () => {
+ test('3600 kilometers per hour should equal 1 second per kilometer', () => {
+ const result = units.convertSpeedPace(3600, 'kilometers_per_hour', 'seconds_per_kilometer');
+ expect(result).to.equal(1);
+ });
+
+ test('1 second per kilometer should equal 3600 kilometers per hour', () => {
+ const result = units.convertSpeedPace(3600, 'seconds_per_kilometer', 'kilometers_per_hour');
+ expect(result).to.equal(1);
+ });
+});
+
+describe('formatNumber method', () => {
+ test('should correctly format number when padding is not 0', () => {
+ let result = units.formatNumber(12.3, 3, 0);
+ expect(result).to.equal('012');
+
+ result = units.formatNumber(12.3, 3, 2);
+ expect(result).to.equal('012.30');
+
+ result = units.formatNumber(123, 2, 0);
+ expect(result).to.equal('123');
+
+ result = units.formatNumber(-12, 3, 0);
+ expect(result).to.equal('-012');
+ });
+
+ test('should correctly format number when extraDigits is true', () => {
+ let result = units.formatNumber(1234, 0, 2);
+ expect(result).to.equal('1234.00');
+
+ result = units.formatNumber(1234.5, 0, 2);
+ expect(result).to.equal('1234.50');
+
+ result = units.formatNumber(1234.56, 0, 2);
+ expect(result).to.equal('1234.56');
+
+ result = units.formatNumber(1234.567, 0, 2);
+ expect(result).to.equal('1234.57');
+
+ result = units.formatNumber(1234.56, 0, 0);
+ expect(result).to.equal('1235');
+ });
+
+ test('should correctly format number when extraDigits is false', () => {
+ let result = units.formatNumber(1234, 0, 2, false);
+ expect(result).to.equal('1234');
+
+ result = units.formatNumber(1234.5, 0, 2, false);
+ expect(result).to.equal('1234.5');
+
+ result = units.formatNumber(1234.56, 0, 2, false);
+ expect(result).to.equal('1234.56');
+
+ result = units.formatNumber(1234.567, 0, 2, false);
+ expect(result).to.equal('1234.57');
+
+ result = units.formatNumber(1234.56, 0, 0, false);
+ expect(result).to.equal('1235');
+ });
+
+ test('should correctly format undefined', () => {
+ let result = units.formatNumber(undefined, 0, 2);
+ expect(result).to.equal('NaN');
+
+ result = units.formatNumber(undefined, 0, 2, false);
+ expect(result).to.equal('NaN');
+
+ result = units.formatNumber(undefined, 5, 2);
+ expect(result).to.equal('NaN');
+ });
+
+ test('should correctly format NaN', () => {
+ let result = units.formatNumber(NaN, 0, 0);
+ expect(result).to.equal('NaN');
+
+ result = units.formatNumber(NaN, 0, 2, false);
+ expect(result).to.equal('NaN');
+
+ result = units.formatNumber(NaN, 5, 2);
+ expect(result).to.equal('NaN');
+ });
+
+ test('should correctly format +/- Infinity', () => {
+ let result = units.formatNumber(Infinity);
+ expect(result).to.equal('Infinity');
+
+ result = units.formatNumber(Infinity, 10, 2);
+ expect(result).to.equal('Infinity');
+
+ result = units.formatNumber(-Infinity);
+ expect(result).to.equal('-Infinity');
+ });
+
+ test('should correctly format numbers smaller than 1', () => {
+ let result = units.formatNumber(0.123, 0, 0);
+ expect(result).to.equal('0');
+
+ result = units.formatNumber(0.123, 0, 2);
+ expect(result).to.equal('0.12');
+ });
+
+ test('should correctly format negative numbers', () => {
+ let result = units.formatNumber(-12, 0, 2, false);
+ expect(result).to.equal('-12');
+
+ result = units.formatNumber(-12, 0, 2);
+ expect(result).to.equal('-12.00');
+
+ result = units.formatNumber(-12.34, 0, 2);
+ expect(result).to.equal('-12.34');
+
+ result = units.formatNumber(-12.34, 3, 2);
+ expect(result).to.equal('-012.34');
+
+ result = units.formatNumber(-0.12, 0, 2);
+ expect(result).to.equal('-0.12');
+ });
+});
+
+describe('formatDistance method', () => {
+ test('should correctly format distances with a variety of units', () => {
+ let result = units.formatDistance({
+ distanceValue: 1,
+ distanceUnit: units.DistanceUnits.Yards,
+ }, false);
+ expect(result).to.equal('1 yd');
+
+ result = units.formatDistance({
+ distanceValue: 2,
+ distanceUnit: units.DistanceUnits.Meters,
+ }, false);
+ expect(result).to.equal('2 m');
+
+ result = units.formatDistance({
+ distanceValue: 3,
+ distanceUnit: units.DistanceUnits.Kilometers,
+ }, false);
+ expect(result).to.equal('3 km');
+
+ result = units.formatDistance({
+ distanceValue: 4,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('4 mi');
+
+ result = units.formatDistance({
+ distanceValue: 5,
+ distanceUnit: units.DistanceUnits.Marathons,
+ }, false);
+ expect(result).to.equal('5 Mar');
+ });
+
+ test('should correctly format distance when extraDigits is true', () => {
+ let result = units.formatDistance({
+ distanceValue: 1234,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, true);
+ expect(result).to.equal('1234.00 mi');
+
+ result = units.formatDistance({
+ distanceValue: 1234.5,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, true);
+ expect(result).to.equal('1234.50 mi');
+
+ result = units.formatDistance({
+ distanceValue: 1234.56,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, true);
+ expect(result).to.equal('1234.56 mi');
+
+ result = units.formatDistance({
+ distanceValue: 1234.567,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, true);
+ expect(result).to.equal('1234.57 mi');
+ });
+
+ test('should correctly format distance when extraDigits is false', () => {
+ let result = units.formatDistance({
+ distanceValue: 1234,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('1234 mi');
+
+ result = units.formatDistance({
+ distanceValue: 1234.5,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('1234.5 mi');
+
+ result = units.formatDistance({
+ distanceValue: 1234.56,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('1234.56 mi');
+
+ result = units.formatDistance({
+ distanceValue: 1234.567,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('1234.57 mi');
+ });
+
+ test('should correctly format distances smaller than 1', () => {
+ let result = units.formatDistance({
+ distanceValue: 0,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('0 mi');
+
+ result = units.formatDistance({
+ distanceValue: 0,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, true);
+ expect(result).to.equal('0.00 mi');
+
+ result = units.formatDistance({
+ distanceValue: 0.1,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('0.1 mi');
+
+ result = units.formatDistance({
+ distanceValue: 0.1,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, true);
+ expect(result).to.equal('0.10 mi');
+
+ result = units.formatDistance({
+ distanceValue: 0.12,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('0.12 mi');
+ });
+
+ test('should correctly format negative distances', () => {
+ let result = units.formatDistance({
+ distanceValue: -1234,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('-1234 mi');
+
+ result = units.formatDistance({
+ distanceValue: -1234,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, true);
+ expect(result).to.equal('-1234.00 mi');
+
+ result = units.formatDistance({
+ distanceValue: -1234.56,
+ distanceUnit: units.DistanceUnits.Miles,
+ }, false);
+ expect(result).to.equal('-1234.56 mi');
+ });
+});
+
+describe('formatDuration method', () => {
+ test('should correctly divide durations into parts', () => {
+ const result = units.formatDuration(3600 + 120 + 3 + 0.4);
+ expect(result).to.equal('01:02:03.40');
+ });
+
+ test('should correctly format duration when padding is 7', () => {
+ const result = units.formatDuration(3600 + 120 + 3 + 0.4, 7);
+ expect(result).to.equal('01:02:03.40');
+ });
+
+ test('should correctly format duration when padding is 3', () => {
+ let result = units.formatDuration(3600 + 120 + 3 + 0.4, 3);
+ expect(result).to.equal('1:02:03.40');
+
+ result = units.formatDuration(120 + 3 + 0.4, 3);
+ expect(result).to.equal('2:03.40');
+
+ result = units.formatDuration(3 + 0.4, 3);
+ expect(result).to.equal('0:03.40');
+ });
+
+ test('should correctly format duration when padding is 0', () => {
+ const result = units.formatDuration(0.4, 0);
+ expect(result).to.equal('0.40');
+ });
+
+ test('should correctly format duration when digits is 3', () => {
+ const result = units.formatDuration(3600 + 120 + 3 + 0.4567, 0, 3);
+ expect(result).to.equal('1:02:03.457');
+ });
+
+ test('should correctly format duration when digits is 0', () => {
+ const result = units.formatDuration(3600 + 120 + 3 + 0.456, 0, 0);
+ expect(result).to.equal('1:02:03');
+ });
+
+ test('should correctly format NaN', () => {
+ const result = units.formatDuration(NaN);
+ expect(result).to.equal('NaN');
+ });
+
+ test('should correctly format +/- Infinity', () => {
+ let result = units.formatDuration(Infinity);
+ expect(result).to.equal('Infinity');
+
+ result = units.formatDuration(-Infinity);
+ expect(result).to.equal('-Infinity');
+ });
+
+ test('should correctly format 0 when padding is 0', () => {
+ const result = units.formatDuration(0, 0);
+ expect(result).to.equal('0.00');
+ });
+
+ test('should correctly format negative durations', () => {
+ const result = units.formatDuration(-3600 - 120 - 3 - 0.4);
+ expect(result).to.equal('-01:02:03.40');
+ });
+
+ test('should correctly format 59.9999', () => {
+ const result = units.formatDuration(59.9999);
+ expect(result).to.equal('00:01:00.00');
+ });
+
+ test('should correctly format duration when extraDigits is false', () => {
+ let result = units.formatDuration(83, 0, 2, false);
+ expect(result).to.equal('1:23');
+
+ result = units.formatDuration(83.4, 0, 2, false);
+ expect(result).to.equal('1:23.4');
+
+ result = units.formatDuration(83.45, 0, 2, false);
+ expect(result).to.equal('1:23.45');
+
+ result = units.formatDuration(83.456, 0, 2, false);
+ expect(result).to.equal('1:23.46');
+
+ result = units.formatDuration(83.45, 0, 0, false);
+ expect(result).to.equal('1:23');
+ });
+});
+
+describe('formatPace method', () => {
+ test('should correctly format paces in a variety of units', () => {
+ let result = units.formatPace({
+ distanceValue: 1,
+ distanceUnit: units.DistanceUnits.Meters,
+ time: 600,
+ }, units.PaceUnits.SecondsPerMeter);
+ expect(result).to.equal('10:00 s/m');
+
+ result = units.formatPace({
+ distanceValue: 2,
+ distanceUnit: units.DistanceUnits.Kilometers,
+ time: 600,
+ }, units.PaceUnits.TimePerKilometer);
+ expect(result).to.equal('5:00 / km');
+
+ result = units.formatPace({
+ distanceValue: 3,
+ distanceUnit: units.DistanceUnits.Miles,
+ time: 600,
+ }, units.PaceUnits.TimePerMile);
+ expect(result).to.equal('3:20 / mi');
+ });
+
+ test('should correctly format paces that require distance conversion', () => {
+ let result = units.formatPace({
+ distanceValue: 100,
+ distanceUnit: units.DistanceUnits.Meters,
+ time: 600,
+ }, units.PaceUnits.TimePerKilometer);
+ expect(result).to.equal('1:40:00 / km');
+
+ result = units.formatPace({
+ distanceValue: 2,
+ distanceUnit: units.DistanceUnits.Kilometers,
+ time: 600,
+ }, units.PaceUnits.TimePerMile);
+ expect(result).to.equal('8:03 / mi');
+
+ result = units.formatPace({
+ distanceValue: 0.03,
+ distanceUnit: units.DistanceUnits.Miles,
+ time: 600,
+ }, units.PaceUnits.SecondsPerMeter);
+ expect(result).to.equal('0:12 s/m');
+ });
+});
diff --git a/tests/unit/core/utils.spec.js b/tests/unit/core/utils.spec.js
@@ -0,0 +1,267 @@
+import { beforeEach, describe, test, expect } from 'vitest';
+import * as utils from '@/core/utils';
+
+beforeEach(() => {
+ localStorage.clear();
+});
+
+describe('deepCopy method', () => {
+ test('should deeply clone an object', () => {
+ let input = {
+ foo: 123,
+ bar: ['a', 'b', 'c'],
+ baz: {
+ baz: {
+ baz: {
+ baz: 'baz'
+ }
+ }
+ }
+ };
+ let output = utils.deepCopy(input);
+
+ // Output should equal input
+ expect(output).to.deep.equal(input);
+
+ // Modifying input should not modify output
+ input.foo = 1234;
+ input.baz.baz.baz.baz = 'baz2';
+ expect(output).to.deep.equal({
+ foo: 123,
+ bar: ['a', 'b', 'c'],
+ baz: {
+ baz: {
+ baz: {
+ baz: 'baz'
+ }
+ }
+ }
+ });
+
+ // Modifying output should not modify input
+ output.foo = 12345;
+ output.baz.baz.baz.baz = 'baz3';
+ expect(input).to.deep.equal({
+ foo: 1234,
+ bar: ['a', 'b', 'c'],
+ baz: {
+ baz: {
+ baz: {
+ baz: 'baz2'
+ }
+ }
+ }
+ });
+ });
+});
+
+describe('deepEqual method', () => {
+ test('should correctly compare an object with itself', () => {
+ let obj1 = {
+ foo: 123,
+ bar: ['a', 'b', 'c'],
+ baz: {
+ baz: {
+ baz: {
+ baz: 'baz'
+ }
+ }
+ }
+ };
+
+ // obj1 should equal obj1
+ expect(utils.deepEqual(obj1, obj1)).to.equal(true);
+ });
+
+ test('should correctly compare identical objects', () => {
+ let obj1 = {
+ foo: 123,
+ bar: ['a', 'b', 'c'],
+ baz: {
+ baz: {
+ baz: {
+ baz: 'baz'
+ }
+ }
+ }
+ };
+ let obj2 = {
+ foo: 123,
+ bar: ['a', 'b', 'c'],
+ baz: {
+ baz: {
+ baz: {
+ baz: 'baz'
+ }
+ }
+ }
+ };
+
+ // obj1 should equal obj2
+ expect(utils.deepEqual(obj1, obj2)).to.equal(true);
+ });
+
+ test('should correctly compare unequal objects', () => {
+ let obj1 = {
+ foo: 123,
+ bar: ['a', 'b', 'c'],
+ baz: {
+ baz: {
+ baz: {
+ baz: 'baz'
+ }
+ }
+ }
+ };
+ let obj2 = {
+ foo: 123,
+ bar: ['a', 'b', 'c'],
+ baz: {
+ baz: {
+ baz: {
+ baz: 'baz2'
+ }
+ }
+ }
+ };
+
+ // obj1 should not equal obj2
+ expect(utils.deepEqual(obj1, obj2)).to.equal(false);
+ });
+});
+
+describe('get method', () => {
+ test('should correctly parse correct localStorage item', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.foo', '{"bar":123}');
+
+ // Assert result is correct
+ expect(utils.getLocalStorage('foo')).to.deep.equal({ bar: 123 });
+ });
+
+ test('should return null for corrupt localStorage item', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.foo', 'invalid json');
+
+ // Assert result is correct
+ expect(utils.getLocalStorage('foo')).to.equal(null);
+ });
+
+ test('should return null for missing localStorage item', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.foo', '{"bar":123}');
+
+ // Assert result is correct
+ expect(utils.getLocalStorage('baz')).to.equal(null);
+ });
+});
+
+describe('set method', () => {
+ test('should correctly set new localStorage item', async () => {
+ // Set localStorage item
+ utils.setLocalStorage('foo', { baz: 456 });
+
+ // Assert result is correct
+ expect(localStorage.getItem('running-tools.foo')).to.equal('{"baz":456}');
+ });
+
+ test('should correctly override existing localStorage item', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.foo', '{"bar":123}');
+
+ // Set localStorage item
+ utils.setLocalStorage('foo', { baz: 456 });
+
+ // Assert result is correct
+ expect(localStorage.getItem('running-tools.foo')).to.equal('{"baz":456}');
+ });
+});
+
+describe('migrate method', () => {
+ test('should correctly migrate <=1.4.1 calculator options', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.pace-calculator-target-set', '"A"');
+ localStorage.setItem('running-tools.race-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.07}');
+ localStorage.setItem('running-tools.race-calculator-target-set', '"B"');
+ localStorage.setItem('running-tools.split-calculator-target-set', '"C"');
+ localStorage.setItem('running-tools.workout-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.08}');
+ localStorage.setItem('running-tools.workout-calculator-target-set', '"D"');
+
+ // Run migrations
+ utils.migrateLocalStorage();
+
+ // Assert localStorage entries correctly migrated
+ expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(
+ '{"selectedTargetSet":"A"}');
+ expect(localStorage.getItem('running-tools.pace-calculator-target-set')).to.equal(null);
+ expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
+ '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"B"}');
+ expect(localStorage.getItem('running-tools.race-calculator-target-set')).to.equal(null);
+ expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(
+ '{"selectedTargetSet":"C"}');
+ expect(localStorage.getItem('running-tools.split-calculator-target-set')).to.equal(null);
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
+ '{"model":"RiegelModel","riegelExponent":1.08,"selectedTargetSet":"D",' +
+ '"customTargetNames":false}');
+ expect(localStorage.getItem('running-tools.workout-calculator-target-set')).to.equal(null);
+ });
+
+ test('should correctly migrate partial <=1.4.1 calculator options', async () => {
+ // Initialize localStorage (*-target-set options missing)
+ localStorage.setItem('running-tools.race-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.07}');
+ localStorage.setItem('running-tools.workout-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.08}');
+
+ // Run migrations
+ utils.migrateLocalStorage();
+
+ // Assert localStorage entries correctly migrated
+ expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
+ '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"_race_targets"}');
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
+ '{"model":"RiegelModel","riegelExponent":1.08,"selectedTargetSet":"_workout_targets",' +
+ '"customTargetNames":false}');
+ });
+
+
+ test('should not modify >1.4.1 calculator options', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.pace-calculator-options',
+ '{"selectedTargetSet":"A"}');
+ localStorage.setItem('running-tools.race-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"B"}');
+ localStorage.setItem('running-tools.split-calculator-options',
+ '{"selectedTargetSet":"C"}');
+ localStorage.setItem('running-tools.workout-calculator-options',
+ '{"customTargetNames":true,"model":"PurdyPointsModel","riegelExponent":1.08,' +
+ '"selectedTargetSet":"D"}');
+
+ // Run migrations
+ utils.migrateLocalStorage();
+
+ // Assert localStorage entries not modified
+ expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(
+ '{"selectedTargetSet":"A"}');
+ expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
+ '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"B"}');
+ expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(
+ '{"selectedTargetSet":"C"}');
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
+ '{"customTargetNames":true,"model":"PurdyPointsModel","riegelExponent":1.08,' +
+ '"selectedTargetSet":"D"}');
+ });
+
+ test('should not modify missing calculator options', async () => {
+ // Run migrations
+ utils.migrateLocalStorage();
+
+ // Assert localStorage entries not modified
+ expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(null);
+ expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(null);
+ expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(null);
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(null);
+ });
+});
diff --git a/tests/unit/utils/calculators.spec.js b/tests/unit/utils/calculators.spec.js
@@ -1,255 +0,0 @@
-import { describe, test, expect } from 'vitest';
-import * as calculatorUtils from '@/utils/calculators';
-
-describe('calculatePaceResults method', () => {
- test('should correctly calculate pace times', () => {
- const input = {
- distanceValue: 1,
- distanceUnit: 'kilometers',
- time: 100,
- };
- const target = {
- distanceValue: 20,
- distanceUnit: 'meters',
- type: 'distance',
- };
-
- const result = calculatorUtils.calculatePaceResults(input, target, 'metric');
-
- expect(result).to.deep.equal({
- key: '20 m',
- value: '0:02.00',
- pace: '1:40 / km',
- result: 'value',
- sort: 2,
- });
- });
-
- test('should correctly calculate pace distances according to default units setting', () => {
- const input = {
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 1200,
- };
- const target = {
- time: 600,
- type: 'time',
- };
-
- const result1 = calculatorUtils.calculatePaceResults(input, target, 'metric');
- const result2 = calculatorUtils.calculatePaceResults(input, target, 'imperial');
-
- expect(result1.key).to.equal('1.61 km');
- expect(result1.value).to.equal('10:00');
- expect(result1.pace).to.equal('6:13 / km');
- expect(result1.result).to.equal('key');
- expect(result1.sort).to.be.closeTo(600, 0.01);
-
- expect(result2.key).to.equal('1.00 mi');
- expect(result2.value).to.equal('10:00');
- expect(result2.pace).to.equal('10:00 / mi');
- expect(result2.result).to.equal('key');
- expect(result2.sort).to.be.closeTo(600, 0.01);
- });
-});
-
-describe('calculateRaceResults method', () => {
- test('should correctly predict race times', () => {
- const input = {
- distanceValue: 5,
- distanceUnit: 'kilometers',
- time: 1200,
- };
- const target = {
- distanceValue: 10,
- distanceUnit: 'kilometers',
- type: 'distance',
- };
- const options = {
- model: 'AverageModel',
- riegelExponent: 1.06,
- }
-
- const result = calculatorUtils.calculateRaceResults(input, target, options, 'imperial');
-
- expect(result.key).to.equal('10 km');
- expect(result.value).to.equal('41:34.80');
- expect(result.pace).to.equal('6:41 / mi');
- expect(result.result).to.equal('value');
- expect(result.sort).to.be.closeTo(2494.80, 0.01);
- });
-
- test('should correctly calculate race distances according to default units setting', () => {
- const input = {
- distanceValue: 5,
- distanceUnit: 'kilometers',
- time: 1200,
- };
- const target = {
- time: 2495,
- type: 'time',
- };
- const options = {
- model: 'AverageModel',
- riegelExponent: 1.06,
- }
-
- const result1 = calculatorUtils.calculateRaceResults(input, target, options, 'metric');
- const result2 = calculatorUtils.calculateRaceResults(input, target, options, 'imperial');
-
- expect(result1.key).to.equal('10.00 km');
- expect(result1.value).to.equal('41:35');
- expect(result1.pace).to.equal('4:09 / km');
- expect(result1.result).to.equal('key');
- expect(result1.sort).to.equal(2495);
-
- expect(result2.key).to.equal('6.21 mi');
- expect(result2.value).to.equal('41:35');
- expect(result2.pace).to.equal('6:41 / mi');
- expect(result2.result).to.equal('key');
- expect(result2.sort).to.equal(2495);
- });
-
- test('should correctly predict race times according to race options', () => {
- const input = {
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 630,
- };
- const target = {
- distanceValue: 5,
- distanceUnit: 'kilometers',
- type: 'distance',
- };
- const options = {
- model: 'RiegelModel',
- riegelExponent: 1.12,
- }
-
- const result = calculatorUtils.calculateRaceResults(input, target, options, 'imperial');
-
- expect(result.key).to.equal('5 km');
- expect(result.value).to.equal('17:11.78');
- expect(result.pace).to.equal('5:32 / mi');
- expect(result.result).to.equal('value');
- expect(result.sort).to.be.closeTo(1031.77, 0.01);
- });
-});
-
-describe('calculateRaceStats method', () => {
- test('should correctly calculate race statistics', () => {
- const input = {
- distanceValue: 5,
- distanceUnit: 'kilometers',
- time: 1200,
- };
-
- const results = calculatorUtils.calculateRaceStats(input);
-
- expect(results.purdyPoints).to.be.closeTo(454.5, 0.1);
- expect(results.vo2).to.be.closeTo(47.4, 0.1);
- expect(results.vo2MaxPercentage).to.be.closeTo(95.3, 0.1);
- expect(results.vo2Max).to.be.closeTo(49.8, 0.1);
- });
-});
-
-describe('calculateWorkoutResults method', () => {
- test('should correctly calculate distance-based workouts according to race options', () => {
- const input = {
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 630,
- };
- const target = {
- distanceValue: 5,
- distanceUnit: 'kilometers', // 5k split is ~17:11.77
- splitValue: 1000,
- splitUnit: 'meters',
- type: 'distance',
- };
- const options = {
- customTargetNames: false,
- model: 'RiegelModel',
- riegelExponent: 1.12,
- }
-
- const result = calculatorUtils.calculateWorkoutResults(input, target, options);
-
- expect(result.key).to.equal('1000 m @ 5 km');
- expect(result.value).to.equal('3:26.36');
- expect(result.pace).to.equal('');
- expect(result.result).to.equal('value');
- expect(result.sort).to.be.closeTo(206.35, 0.01);
- });
-
- test('should correctly calculate distance-based workouts according to custom names', () => {
- const input = {
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 630,
- };
- const target_1 = {
- distanceValue: 5,
- distanceUnit: 'kilometers', // 5k split is ~17:11.77
- splitValue: 1000,
- splitUnit: 'meters',
- type: 'distance',
- // no custom name
- };
- const target_2 = {
- distanceValue: 5,
- distanceUnit: 'kilometers', // 5k split is ~17:11.77
- splitValue: 1000,
- splitUnit: 'meters',
- type: 'distance',
- customName: 'my custom name',
- };
- const options_a = {
- customTargetNames: false,
- model: 'RiegelModel',
- riegelExponent: 1.12,
- };
- const options_b = {
- customTargetNames: true,
- model: 'RiegelModel',
- riegelExponent: 1.12,
- };
-
- const result1a = calculatorUtils.calculateWorkoutResults(input, target_1, options_a);
- const result1b = calculatorUtils.calculateWorkoutResults(input, target_1, options_b);
- const result2a = calculatorUtils.calculateWorkoutResults(input, target_2, options_a);
- const result2b = calculatorUtils.calculateWorkoutResults(input, target_2, options_b);
-
- expect(result1a.key).to.equal('1000 m @ 5 km');
- expect(result1b.key).to.equal('1000 m @ 5 km');
- expect(result2a.key).to.equal('1000 m @ 5 km');
- expect(result2b.key).to.equal('my custom name');
- });
-
- test('should correctly calculate time-based workouts', () => {
- const input = {
- distanceValue: 5,
- distanceUnit: 'kilometers',
- time: 1200,
- };
- const target = {
- time: 2495, // ~10k split is 41:35
- splitValue: 1,
- splitUnit: 'miles',
- type: 'time',
- };
- const options = {
- customTargetNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
- }
-
- const result = calculatorUtils.calculateWorkoutResults(input, target, options);
-
- expect(result.key).to.equal('1 mi @ 41:35');
- expect(result.value).to.equal('6:41.50');
- expect(result.pace).to.equal('');
- expect(result.result).to.equal('value');
- expect(result.sort).to.be.closeTo(401.50, 0.01);
- });
-});
diff --git a/tests/unit/utils/misc.spec.js b/tests/unit/utils/misc.spec.js
@@ -1,127 +0,0 @@
-import { describe, test, expect } from 'vitest';
-import * as miscUtils from '@/utils/misc';
-
-describe('deepCopy method', () => {
- test('should deeply clone an object', () => {
- let input = {
- foo: 123,
- bar: ['a', 'b', 'c'],
- baz: {
- baz: {
- baz: {
- baz: 'baz'
- }
- }
- }
- };
- let output = miscUtils.deepCopy(input);
-
- // Output should equal input
- expect(output).to.deep.equal(input);
-
- // Modifying input should not modify output
- input.foo = 1234;
- input.baz.baz.baz.baz = 'baz2';
- expect(output).to.deep.equal({
- foo: 123,
- bar: ['a', 'b', 'c'],
- baz: {
- baz: {
- baz: {
- baz: 'baz'
- }
- }
- }
- });
-
- // Modifying output should not modify input
- output.foo = 12345;
- output.baz.baz.baz.baz = 'baz3';
- expect(input).to.deep.equal({
- foo: 1234,
- bar: ['a', 'b', 'c'],
- baz: {
- baz: {
- baz: {
- baz: 'baz2'
- }
- }
- }
- });
- });
-});
-
-describe('deepEqual method', () => {
- test('should correctly compare an object with itself', () => {
- let obj1 = {
- foo: 123,
- bar: ['a', 'b', 'c'],
- baz: {
- baz: {
- baz: {
- baz: 'baz'
- }
- }
- }
- };
-
- // obj1 should equal obj1
- expect(miscUtils.deepEqual(obj1, obj1)).to.equal(true);
- });
-
- test('should correctly compare identical objects', () => {
- let obj1 = {
- foo: 123,
- bar: ['a', 'b', 'c'],
- baz: {
- baz: {
- baz: {
- baz: 'baz'
- }
- }
- }
- };
- let obj2 = {
- foo: 123,
- bar: ['a', 'b', 'c'],
- baz: {
- baz: {
- baz: {
- baz: 'baz'
- }
- }
- }
- };
-
- // obj1 should equal obj2
- expect(miscUtils.deepEqual(obj1, obj2)).to.equal(true);
- });
-
- test('should correctly compare unequal objects', () => {
- let obj1 = {
- foo: 123,
- bar: ['a', 'b', 'c'],
- baz: {
- baz: {
- baz: {
- baz: 'baz'
- }
- }
- }
- };
- let obj2 = {
- foo: 123,
- bar: ['a', 'b', 'c'],
- baz: {
- baz: {
- baz: {
- baz: 'baz2'
- }
- }
- }
- };
-
- // obj1 should not equal obj2
- expect(miscUtils.deepEqual(obj1, obj2)).to.equal(false);
- });
-});
diff --git a/tests/unit/utils/races.spec.js b/tests/unit/utils/races.spec.js
@@ -1,166 +0,0 @@
-import { describe, test, expect } from 'vitest';
-import * as raceUtils from '@/utils/races';
-
-describe('predictTime method', () => {
- describe('PredictTime method', () => {
- test('Average Model', () => {
- const riegel = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
- const cameron = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
- const purdyPoints = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
- const vo2Max = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
- const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
-
- const result = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
- expect(result).to.equal(expected);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictTime(5000, 1200, 5000, 'AverageModel');
- expect(result).to.be.closeTo(1200, 0.001);
- });
- });
-
- describe('Purdy Points Model', () => {
- test('Predictions should be approximately correct', () => {
- const result = raceUtils.predictTime(5000, 1200, 10000, 'PurdyPointsModel');
- expect(result).to.be.closeTo(2490, 1);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictTime(5000, 1200, 5000, 'PurdyPointsModel');
- expect(result).to.be.closeTo(1200, 0.001);
- });
- });
-
- describe('VO2 Max Model', () => {
- test('Predictions should be approximately correct', () => {
- const result = raceUtils.predictTime(5000, 1200, 10000, 'VO2MaxModel');
- expect(result).to.be.closeTo(2488, 1);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictTime(5000, 1200, 5000, 'VO2MaxModel');
- expect(result).to.be.closeTo(1200, 0.001);
- });
- });
-
- describe('Cameron Model', () => {
- test('Predictions should be approximately correct', () => {
- const result = raceUtils.predictTime(5000, 1200, 10000, 'CameronModel');
- expect(result).to.be.closeTo(2500, 1);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictTime(5000, 1200, 5000, 'CameronModel');
- expect(result).to.be.closeTo(1200, 0.001);
- });
- });
-
- describe('Riegel Model', () => {
- test('Predictions should be approximately correct', () => {
- const result = raceUtils.predictTime(5000, 1200, 10000, 'RiegelModel');
- expect(result).to.be.closeTo(2502, 1);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictTime(5000, 1200, 5000, 'RiegelModel');
- expect(result).to.be.closeTo(1200, 0.001);
- });
- });
-});
-
-describe('predictDistance method', () => {
- describe('Average Model', () => {
- test('Predictions should be correct', () => {
- const riegel = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
- const cameron = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
- const purdyPoints = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
- const vo2Max = raceUtils.predictTime(5000, 1200, 10000, 'AverageModel');
- const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
-
- const result = raceUtils.predictDistance(1200, 5000, expected);
- expect(result).to.be.closeTo(10000, 10);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictDistance(1200, 5000, 1200, 'AverageModel');
- expect(result).to.be.closeTo(5000, 0.001);
- });
- });
-
- describe('Purdy Points Model', () => {
- test('Predictions should be approximately correct', () => {
- const result = raceUtils.predictDistance(1200, 5000, 2490, 'PurdyPointsModel');
- expect(result).to.be.closeTo(10000, 10);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictDistance(1200, 5000, 1200, 'PurdyPointsModel');
- expect(result).to.be.closeTo(5000, 0.001);
- });
- });
-
- describe('VO2 Max Model', () => {
- test('Predictions should be approximately correct', () => {
- const result = raceUtils.predictDistance(1200, 5000, 2488, 'VO2MaxModel');
- expect(result).to.be.closeTo(10000, 10);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictDistance(1200, 5000, 1200, 'VO2MaxModel');
- expect(result).to.be.closeTo(5000, 0.001);
- });
- });
-
- describe('Cameron Model', () => {
- test('Predictions should be approximately correct', () => {
- const result = raceUtils.predictDistance(1200, 5000, 2500, 'CameronModel');
- expect(result).to.be.closeTo(10000, 10);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictDistance(1200, 5000, 1200, 'CameronModel');
- expect(result).to.be.closeTo(5000, 0.001);
- });
- });
-
- describe('Riegel Model', () => {
- test('Predictions should be approximately correct', () => {
- const result = raceUtils.predictDistance(1200, 5000, 2502, 'RiegelModel');
- expect(result).to.be.closeTo(10000, 10);
- });
-
- test('Should predict identical times for itentical distances', () => {
- const result = raceUtils.predictDistance(1200, 5000, 1200, 'RiegelModel');
- expect(result).to.be.closeTo(5000, 0.001);
- });
- });
-});
-
-describe('getVO2 method', () => {
- test('Result should be approximately correct', () => {
- const result = raceUtils.getVO2(5000, 1200);
- expect(result).to.be.closeTo(47.4, 0.1);
- });
-});
-
-describe('getVO2Percentage method', () => {
- test('Result should be approximately correct', () => {
- const result = raceUtils.getVO2Percentage(660);
- expect(result).to.be.closeTo(1, 0.001);
- });
-});
-
-describe('getVO2Max method', () => {
- test('Result should be approximately correct', () => {
- const result = raceUtils.getVO2Max(5000, 1200);
- expect(result).to.be.closeTo(49.8, 0.1);
- });
-});
-
-describe('getPurdyPoints method', () => {
- test('Result should be approximately correct', () => {
- const result = raceUtils.getPurdyPoints(5000, 1200);
- expect(result).to.be.closeTo(454, 1);
- });
-});
diff --git a/tests/unit/utils/storage.spec.js b/tests/unit/utils/storage.spec.js
@@ -1,142 +0,0 @@
-import { beforeEach, describe, test, expect } from 'vitest';
-import * as storage from '@/utils/storage';
-
-beforeEach(() => {
- localStorage.clear();
-})
-
-describe('get method', () => {
- test('should correctly parse correct localStorage item', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.foo', '{"bar":123}');
-
- // Assert result is correct
- expect(storage.get('foo')).to.deep.equal({ bar: 123 });
- });
-
- test('should return null for corrupt localStorage item', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.foo', 'invalid json');
-
- // Assert result is correct
- expect(storage.get('foo')).to.equal(null);
- });
-
- test('should return null for missing localStorage item', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.foo', '{"bar":123}');
-
- // Assert result is correct
- expect(storage.get('baz')).to.equal(null);
- });
-});
-
-describe('set method', () => {
- test('should correctly set new localStorage item', async () => {
- // Set localStorage item
- storage.set('foo', { baz: 456 });
-
- // Assert result is correct
- expect(localStorage.getItem('running-tools.foo')).to.equal('{"baz":456}');
- });
-
- test('should correctly override existing localStorage item', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.foo', '{"bar":123}');
-
- // Set localStorage item
- storage.set('foo', { baz: 456 });
-
- // Assert result is correct
- expect(localStorage.getItem('running-tools.foo')).to.equal('{"baz":456}');
- });
-});
-
-describe('migrate method', () => {
- test('should correctly migrate <=1.4.1 calculator options', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.pace-calculator-target-set', '"A"');
- localStorage.setItem('running-tools.race-calculator-options',
- '{"model":"RiegelModel","riegelExponent":1.07}');
- localStorage.setItem('running-tools.race-calculator-target-set', '"B"');
- localStorage.setItem('running-tools.split-calculator-target-set', '"C"');
- localStorage.setItem('running-tools.workout-calculator-options',
- '{"model":"RiegelModel","riegelExponent":1.08}');
- localStorage.setItem('running-tools.workout-calculator-target-set', '"D"');
-
- // Run migrations
- storage.migrate();
-
- // Assert localStorage entries correctly migrated
- expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(
- '{"selectedTargetSet":"A"}');
- expect(localStorage.getItem('running-tools.pace-calculator-target-set')).to.equal(null);
- expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
- '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"B"}');
- expect(localStorage.getItem('running-tools.race-calculator-target-set')).to.equal(null);
- expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(
- '{"selectedTargetSet":"C"}');
- expect(localStorage.getItem('running-tools.split-calculator-target-set')).to.equal(null);
- expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
- '{"model":"RiegelModel","riegelExponent":1.08,"selectedTargetSet":"D",' +
- '"customTargetNames":false}');
- expect(localStorage.getItem('running-tools.workout-calculator-target-set')).to.equal(null);
- });
-
- test('should correctly migrate partial <=1.4.1 calculator options', async () => {
- // Initialize localStorage (*-target-set options missing)
- localStorage.setItem('running-tools.race-calculator-options',
- '{"model":"RiegelModel","riegelExponent":1.07}');
- localStorage.setItem('running-tools.workout-calculator-options',
- '{"model":"RiegelModel","riegelExponent":1.08}');
-
- // Run migrations
- storage.migrate();
-
- // Assert localStorage entries correctly migrated
- expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
- '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"_race_targets"}');
- expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
- '{"model":"RiegelModel","riegelExponent":1.08,"selectedTargetSet":"_workout_targets",' +
- '"customTargetNames":false}');
- });
-
-
- test('should not modify >1.4.1 calculator options', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.pace-calculator-options',
- '{"selectedTargetSet":"A"}');
- localStorage.setItem('running-tools.race-calculator-options',
- '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"B"}');
- localStorage.setItem('running-tools.split-calculator-options',
- '{"selectedTargetSet":"C"}');
- localStorage.setItem('running-tools.workout-calculator-options',
- '{"customTargetNames":true,"model":"PurdyPointsModel","riegelExponent":1.08,' +
- '"selectedTargetSet":"D"}');
-
- // Run migrations
- storage.migrate();
-
- // Assert localStorage entries not modified
- expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(
- '{"selectedTargetSet":"A"}');
- expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
- '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"B"}');
- expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(
- '{"selectedTargetSet":"C"}');
- expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
- '{"customTargetNames":true,"model":"PurdyPointsModel","riegelExponent":1.08,' +
- '"selectedTargetSet":"D"}');
- });
-
- test('should not modify missing calculator options', async () => {
- // Run migrations
- storage.migrate();
-
- // Assert localStorage entries not modified
- expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(null);
- expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(null);
- expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(null);
- expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(null);
- });
-});
diff --git a/tests/unit/utils/targets.spec.js b/tests/unit/utils/targets.spec.js
@@ -1,59 +0,0 @@
-import { describe, test, expect } from 'vitest';
-import * as targets from '@/utils/targets';
-
-describe('sort method', () => {
- test('should correctly sort targets', () => {
- // Initialize unsorted and sorted targets
- const input = [
- { time: 60, type: 'time' },
- { distanceUnit: 'kilometers', distanceValue: 5, type: 'distance' },
- { distanceUnit: 'miles', distanceValue: 3, type: 'distance' },
- ];
- const expected = [
- { distanceUnit: 'miles', distanceValue: 3, type: 'distance' },
- { distanceUnit: 'kilometers', distanceValue: 5, type: 'distance' },
- { time: 60, type: 'time' },
- ];
-
- // Assert sort method sorts targets correctly
- expect(targets.sort(input)).to.deep.equal(expected);
- });
-});
-
-describe('workoutTargetToString method', () => {
- test('should correctly stringify time target', () => {
- // Initialize original and stringified target
- const input = {
- splitValue: 1600, splitUnit: 'meters',
- type: 'time', time: 3600,
- };
- const expected = '1600 m @ 1:00:00';
-
- // Assert sort method sorts targets correctly
- expect(targets.workoutTargetToString(input)).to.deep.equal(expected);
- });
-
- test('should correctly stringify distance target', () => {
- // Initialize original and stringified target
- const input = {
- splitValue: 800, splitUnit: 'meters',
- type: 'distance', distanceValue: 5, distanceUnit: 'kilometers',
- };
- const expected = '800 m @ 5 km';
-
- // Assert sort method sorts targets correctly
- expect(targets.workoutTargetToString(input)).to.deep.equal(expected);
- });
-
- test('should correctly stringify race target', () => {
- // Initialize original and stringified target
- const input = {
- splitValue: 5, splitUnit: 'kilometers',
- type: 'distance', distanceValue: 5, distanceUnit: 'kilometers',
- };
- const expected = '5 km';
-
- // Assert sort method sorts targets correctly
- expect(targets.workoutTargetToString(input)).to.deep.equal(expected);
- });
-});
diff --git a/tests/unit/utils/units.spec.js b/tests/unit/utils/units.spec.js
@@ -1,439 +0,0 @@
-import { describe, test, expect } from 'vitest';
-import * as units from '@/utils/units';
-
-describe('convertTime method', () => {
- test('90 seconds should equal 1.5 minutes', () => {
- const result = units.convertTime(90, 'seconds', 'minutes');
- expect(result).to.equal(1.5);
- });
-
- test('1.5 minutes should equal 95 seconds', () => {
- const result = units.convertTime(1.5, 'minutes', 'seconds');
- expect(result).to.equal(90);
- });
-});
-
-describe('convertDistance method', () => {
- test('100 meters should equal 0.1 kilometers', () => {
- const result = units.convertDistance(100, 'meters', 'kilometers');
- expect(result).to.equal(0.1);
- });
-
- test('0.1 kilometers should equal 100 meters', () => {
- const result = units.convertDistance(0.1, 'kilometers', 'meters');
- expect(result).to.equal(100);
- });
-});
-
-describe('convertSpeed method', () => {
- test('1000 meters per seconds should equal 3600 kilometers per hour', () => {
- const result = units.convertSpeed(1000, 'meters_per_second', 'kilometers_per_hour');
- expect(result).to.equal(3600);
- });
-
- test('3600 kilometers per hour should equal 1000 meters per second', () => {
- const result = units.convertSpeed(3600, 'kilometers_per_hour', 'meters_per_second');
- expect(result).to.equal(1000);
- });
-});
-
-describe('convertPace method', () => {
- test('1 second per meter should equal 1000 seconds per kilometer', () => {
- const result = units.convertPace(1, 'seconds_per_meter', 'seconds_per_kilometer');
- expect(result).to.equal(1000);
- });
-
- test('1000 seconds per kilometer should equal 1 second per meter', () => {
- const result = units.convertPace(1000, 'seconds_per_kilometer', 'seconds_per_meter');
- expect(result).to.equal(1);
- });
-});
-
-describe('convertSpeedPace method', () => {
- test('3600 kilometers per hour should equal 1 second per kilometer', () => {
- const result = units.convertSpeedPace(3600, 'kilometers_per_hour', 'seconds_per_kilometer');
- expect(result).to.equal(1);
- });
-
- test('1 second per kilometer should equal 3600 kilometers per hour', () => {
- const result = units.convertSpeedPace(3600, 'seconds_per_kilometer', 'kilometers_per_hour');
- expect(result).to.equal(1);
- });
-});
-
-describe('formatNumber method', () => {
- test('should correctly format number when padding is not 0', () => {
- let result = units.formatNumber(12.3, 3, 0);
- expect(result).to.equal('012');
-
- result = units.formatNumber(12.3, 3, 2);
- expect(result).to.equal('012.30');
-
- result = units.formatNumber(123, 2, 0);
- expect(result).to.equal('123');
-
- result = units.formatNumber(-12, 3, 0);
- expect(result).to.equal('-012');
- });
-
- test('should correctly format number when extraDigits is true', () => {
- let result = units.formatNumber(1234, 0, 2);
- expect(result).to.equal('1234.00');
-
- result = units.formatNumber(1234.5, 0, 2);
- expect(result).to.equal('1234.50');
-
- result = units.formatNumber(1234.56, 0, 2);
- expect(result).to.equal('1234.56');
-
- result = units.formatNumber(1234.567, 0, 2);
- expect(result).to.equal('1234.57');
-
- result = units.formatNumber(1234.56, 0, 0);
- expect(result).to.equal('1235');
- });
-
- test('should correctly format number when extraDigits is false', () => {
- let result = units.formatNumber(1234, 0, 2, false);
- expect(result).to.equal('1234');
-
- result = units.formatNumber(1234.5, 0, 2, false);
- expect(result).to.equal('1234.5');
-
- result = units.formatNumber(1234.56, 0, 2, false);
- expect(result).to.equal('1234.56');
-
- result = units.formatNumber(1234.567, 0, 2, false);
- expect(result).to.equal('1234.57');
-
- result = units.formatNumber(1234.56, 0, 0, false);
- expect(result).to.equal('1235');
- });
-
- test('should correctly format undefined', () => {
- let result = units.formatNumber(undefined, 0, 2);
- expect(result).to.equal('NaN');
-
- result = units.formatNumber(undefined, 0, 2, false);
- expect(result).to.equal('NaN');
-
- result = units.formatNumber(undefined, 5, 2);
- expect(result).to.equal('NaN');
- });
-
- test('should correctly format NaN', () => {
- let result = units.formatNumber(NaN, 0, 0);
- expect(result).to.equal('NaN');
-
- result = units.formatNumber(NaN, 0, 2, false);
- expect(result).to.equal('NaN');
-
- result = units.formatNumber(NaN, 5, 2);
- expect(result).to.equal('NaN');
- });
-
- test('should correctly format +/- Infinity', () => {
- let result = units.formatNumber(Infinity);
- expect(result).to.equal('Infinity');
-
- result = units.formatNumber(Infinity, 10, 2);
- expect(result).to.equal('Infinity');
-
- result = units.formatNumber(-Infinity);
- expect(result).to.equal('-Infinity');
- });
-
- test('should correctly format numbers smaller than 1', () => {
- let result = units.formatNumber(0.123, 0, 0);
- expect(result).to.equal('0');
-
- result = units.formatNumber(0.123, 0, 2);
- expect(result).to.equal('0.12');
- });
-
- test('should correctly format negative numbers', () => {
- let result = units.formatNumber(-12, 0, 2, false);
- expect(result).to.equal('-12');
-
- result = units.formatNumber(-12, 0, 2);
- expect(result).to.equal('-12.00');
-
- result = units.formatNumber(-12.34, 0, 2);
- expect(result).to.equal('-12.34');
-
- result = units.formatNumber(-12.34, 3, 2);
- expect(result).to.equal('-012.34');
-
- result = units.formatNumber(-0.12, 0, 2);
- expect(result).to.equal('-0.12');
- });
-});
-
-describe('formatDistance method', () => {
- test('should correctly format distances with a variety of units', () => {
- let result = units.formatDistance({
- distanceValue: 1,
- distanceUnit: units.DistanceUnits.Yards,
- }, false);
- expect(result).to.equal('1 yd');
-
- result = units.formatDistance({
- distanceValue: 2,
- distanceUnit: units.DistanceUnits.Meters,
- }, false);
- expect(result).to.equal('2 m');
-
- result = units.formatDistance({
- distanceValue: 3,
- distanceUnit: units.DistanceUnits.Kilometers,
- }, false);
- expect(result).to.equal('3 km');
-
- result = units.formatDistance({
- distanceValue: 4,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('4 mi');
-
- result = units.formatDistance({
- distanceValue: 5,
- distanceUnit: units.DistanceUnits.Marathons,
- }, false);
- expect(result).to.equal('5 Mar');
- });
-
- test('should correctly format distance when extraDigits is true', () => {
- let result = units.formatDistance({
- distanceValue: 1234,
- distanceUnit: units.DistanceUnits.Miles,
- }, true);
- expect(result).to.equal('1234.00 mi');
-
- result = units.formatDistance({
- distanceValue: 1234.5,
- distanceUnit: units.DistanceUnits.Miles,
- }, true);
- expect(result).to.equal('1234.50 mi');
-
- result = units.formatDistance({
- distanceValue: 1234.56,
- distanceUnit: units.DistanceUnits.Miles,
- }, true);
- expect(result).to.equal('1234.56 mi');
-
- result = units.formatDistance({
- distanceValue: 1234.567,
- distanceUnit: units.DistanceUnits.Miles,
- }, true);
- expect(result).to.equal('1234.57 mi');
- });
-
- test('should correctly format distance when extraDigits is false', () => {
- let result = units.formatDistance({
- distanceValue: 1234,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('1234 mi');
-
- result = units.formatDistance({
- distanceValue: 1234.5,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('1234.5 mi');
-
- result = units.formatDistance({
- distanceValue: 1234.56,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('1234.56 mi');
-
- result = units.formatDistance({
- distanceValue: 1234.567,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('1234.57 mi');
- });
-
- test('should correctly format distances smaller than 1', () => {
- let result = units.formatDistance({
- distanceValue: 0,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('0 mi');
-
- result = units.formatDistance({
- distanceValue: 0,
- distanceUnit: units.DistanceUnits.Miles,
- }, true);
- expect(result).to.equal('0.00 mi');
-
- result = units.formatDistance({
- distanceValue: 0.1,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('0.1 mi');
-
- result = units.formatDistance({
- distanceValue: 0.1,
- distanceUnit: units.DistanceUnits.Miles,
- }, true);
- expect(result).to.equal('0.10 mi');
-
- result = units.formatDistance({
- distanceValue: 0.12,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('0.12 mi');
- });
-
- test('should correctly format negative distances', () => {
- let result = units.formatDistance({
- distanceValue: -1234,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('-1234 mi');
-
- result = units.formatDistance({
- distanceValue: -1234,
- distanceUnit: units.DistanceUnits.Miles,
- }, true);
- expect(result).to.equal('-1234.00 mi');
-
- result = units.formatDistance({
- distanceValue: -1234.56,
- distanceUnit: units.DistanceUnits.Miles,
- }, false);
- expect(result).to.equal('-1234.56 mi');
- });
-});
-
-describe('formatDuration method', () => {
- test('should correctly divide durations into parts', () => {
- const result = units.formatDuration(3600 + 120 + 3 + 0.4);
- expect(result).to.equal('01:02:03.40');
- });
-
- test('should correctly format duration when padding is 7', () => {
- const result = units.formatDuration(3600 + 120 + 3 + 0.4, 7);
- expect(result).to.equal('01:02:03.40');
- });
-
- test('should correctly format duration when padding is 3', () => {
- let result = units.formatDuration(3600 + 120 + 3 + 0.4, 3);
- expect(result).to.equal('1:02:03.40');
-
- result = units.formatDuration(120 + 3 + 0.4, 3);
- expect(result).to.equal('2:03.40');
-
- result = units.formatDuration(3 + 0.4, 3);
- expect(result).to.equal('0:03.40');
- });
-
- test('should correctly format duration when padding is 0', () => {
- const result = units.formatDuration(0.4, 0);
- expect(result).to.equal('0.40');
- });
-
- test('should correctly format duration when digits is 3', () => {
- const result = units.formatDuration(3600 + 120 + 3 + 0.4567, 0, 3);
- expect(result).to.equal('1:02:03.457');
- });
-
- test('should correctly format duration when digits is 0', () => {
- const result = units.formatDuration(3600 + 120 + 3 + 0.456, 0, 0);
- expect(result).to.equal('1:02:03');
- });
-
- test('should correctly format NaN', () => {
- const result = units.formatDuration(NaN);
- expect(result).to.equal('NaN');
- });
-
- test('should correctly format +/- Infinity', () => {
- let result = units.formatDuration(Infinity);
- expect(result).to.equal('Infinity');
-
- result = units.formatDuration(-Infinity);
- expect(result).to.equal('-Infinity');
- });
-
- test('should correctly format 0 when padding is 0', () => {
- const result = units.formatDuration(0, 0);
- expect(result).to.equal('0.00');
- });
-
- test('should correctly format negative durations', () => {
- const result = units.formatDuration(-3600 - 120 - 3 - 0.4);
- expect(result).to.equal('-01:02:03.40');
- });
-
- test('should correctly format 59.9999', () => {
- const result = units.formatDuration(59.9999);
- expect(result).to.equal('00:01:00.00');
- });
-
- test('should correctly format duration when extraDigits is false', () => {
- let result = units.formatDuration(83, 0, 2, false);
- expect(result).to.equal('1:23');
-
- result = units.formatDuration(83.4, 0, 2, false);
- expect(result).to.equal('1:23.4');
-
- result = units.formatDuration(83.45, 0, 2, false);
- expect(result).to.equal('1:23.45');
-
- result = units.formatDuration(83.456, 0, 2, false);
- expect(result).to.equal('1:23.46');
-
- result = units.formatDuration(83.45, 0, 0, false);
- expect(result).to.equal('1:23');
- });
-});
-
-describe('formatPace method', () => {
- test('should correctly format paces in a variety of units', () => {
- let result = units.formatPace({
- distanceValue: 1,
- distanceUnit: units.DistanceUnits.Meters,
- time: 600,
- }, units.PaceUnits.SecondsPerMeter);
- expect(result).to.equal('10:00 s/m');
-
- result = units.formatPace({
- distanceValue: 2,
- distanceUnit: units.DistanceUnits.Kilometers,
- time: 600,
- }, units.PaceUnits.TimePerKilometer);
- expect(result).to.equal('5:00 / km');
-
- result = units.formatPace({
- distanceValue: 3,
- distanceUnit: units.DistanceUnits.Miles,
- time: 600,
- }, units.PaceUnits.TimePerMile);
- expect(result).to.equal('3:20 / mi');
- });
-
- test('should correctly format paces that require distance conversion', () => {
- let result = units.formatPace({
- distanceValue: 100,
- distanceUnit: units.DistanceUnits.Meters,
- time: 600,
- }, units.PaceUnits.TimePerKilometer);
- expect(result).to.equal('1:40:00 / km');
-
- result = units.formatPace({
- distanceValue: 2,
- distanceUnit: units.DistanceUnits.Kilometers,
- time: 600,
- }, units.PaceUnits.TimePerMile);
- expect(result).to.equal('8:03 / mi');
-
- result = units.formatPace({
- distanceValue: 0.03,
- distanceUnit: units.DistanceUnits.Miles,
- time: 600,
- }, units.PaceUnits.SecondsPerMeter);
- expect(result).to.equal('0:12 s/m');
- });
-});
diff --git a/tests/unit/views/PaceCalculator.spec.js b/tests/unit/views/PaceCalculator.spec.js
@@ -1,7 +1,7 @@
import { beforeEach, test, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import PaceCalculator from '@/views/PaceCalculator.vue';
-import { defaultTargetSets } from '@/utils/targets';
+import { defaultTargetSets } from '@/core/targets';
beforeEach(() => {
localStorage.clear();
diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js
@@ -1,7 +1,7 @@
import { beforeEach, test, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import RaceCalculator from '@/views/RaceCalculator.vue';
-import { defaultTargetSets } from '@/utils/targets';
+import { defaultTargetSets } from '@/core/targets';
beforeEach(() => {
localStorage.clear();
diff --git a/tests/unit/views/WorkoutCalculator.spec.js b/tests/unit/views/WorkoutCalculator.spec.js
@@ -1,7 +1,7 @@
import { beforeEach, test, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import WorkoutCalculator from '@/views/WorkoutCalculator.vue';
-import { defaultTargetSets } from '@/utils/targets';
+import { defaultTargetSets } from '@/core/targets';
beforeEach(() => {
localStorage.clear();