running-tools

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

units.ts (11275B)


      1 /*
      2  * Implements handling of distance, pace, speed, and time units
      3  */
      4 
      5 /*
      6  * The type for the data available for each unit
      7  */
      8 export interface UnitData {
      9   name: string,
     10   symbol: string,
     11   value: number,
     12 };
     13 
     14 /*
     15  * The available time units
     16  */
     17 export enum TimeUnits {
     18   Seconds = 'seconds',
     19   Minutes = 'minutes',
     20   Hours = 'hours',
     21 };
     22 export const TimeUnitData: { [key in TimeUnits]: UnitData } = {
     23   [TimeUnits.Seconds]: {
     24     name: 'Seconds',
     25     symbol: 's',
     26     value: 1,
     27   },
     28   [TimeUnits.Minutes]: {
     29     name: 'Minutes',
     30     symbol: 'min',
     31     value: 60,
     32   },
     33   [TimeUnits.Hours]: {
     34     name: 'Hours',
     35     symbol: 'hr',
     36     value: 3600,
     37   },
     38 };
     39 
     40 /*
     41  * The available distance units
     42  */
     43 export enum DistanceUnits {
     44   Meters = 'meters',
     45   Yards = 'yards',
     46   Kilometers = 'kilometers',
     47   Miles = 'miles',
     48   Marathons = 'marathons',
     49 };
     50 export const DistanceUnitData: { [key in DistanceUnits]: UnitData } = {
     51   [DistanceUnits.Meters]: {
     52     name: 'Meters',
     53     symbol: 'm',
     54     value: 1,
     55   },
     56   [DistanceUnits.Yards]: {
     57     name: 'Yards',
     58     symbol: 'yd',
     59     value: 0.9144,
     60   },
     61   [DistanceUnits.Kilometers]: {
     62     name: 'Kilometers',
     63     symbol: 'km',
     64     value: 1000,
     65   },
     66   [DistanceUnits.Miles]: {
     67     name: 'Miles',
     68     symbol: 'mi',
     69     value: 1609.344,
     70   },
     71   [DistanceUnits.Marathons]: {
     72     name: 'Marathons',
     73     symbol: 'Mar',
     74     value: 42195,
     75   },
     76 };
     77 
     78 /*
     79  * The available speed units
     80  */
     81 export enum SpeedUnits {
     82   MetersPerSecond = 'meters_per_second',
     83   KilometersPerHour = 'kilometers_per_hour',
     84   MilesPerHour = 'miles_per_hour',
     85 };
     86 export const SpeedUnitData: { [key in SpeedUnits]: UnitData } = {
     87   [SpeedUnits.MetersPerSecond]: {
     88     name: 'Meters per Second',
     89     symbol: 'm/s',
     90     value: 1,
     91   },
     92   [SpeedUnits.KilometersPerHour]: {
     93     name: 'Kilometers per Hour',
     94     symbol: 'kph',
     95     value: DistanceUnitData[DistanceUnits.Kilometers].value / TimeUnitData[TimeUnits.Hours].value,
     96   },
     97   [SpeedUnits.MilesPerHour]: {
     98     name: 'Miles per Hour',
     99     symbol: 'mph',
    100     value: DistanceUnitData[DistanceUnits.Miles].value / TimeUnitData[TimeUnits.Hours].value,
    101   },
    102 };
    103 
    104 /*
    105  * The available pace units
    106  */
    107 export enum PaceUnits {
    108   SecondsPerMeter = 'seconds_per_meter',
    109   TimePerKilometer = 'seconds_per_kilometer',
    110   TimePerMile = 'seconds_per_mile',
    111 };
    112 export const PaceUnitData: { [key in PaceUnits]: UnitData } = {
    113   [PaceUnits.SecondsPerMeter]: {
    114     name: 'Seconds per Meter',
    115     symbol: 's/m',
    116     value: 1,
    117   },
    118   [PaceUnits.TimePerKilometer]: {
    119     name: 'Time per Kilometer',
    120     symbol: '/ km',
    121     value: TimeUnitData[TimeUnits.Seconds].value / DistanceUnitData[DistanceUnits.Kilometers].value,
    122   },
    123   [PaceUnits.TimePerMile]: {
    124     name: 'Time per Mile',
    125     symbol: '/ mi',
    126     value: TimeUnitData[TimeUnits.Seconds].value / DistanceUnitData[DistanceUnits.Miles].value,
    127   },
    128 };
    129 
    130 /*
    131  * The available speed and pace units
    132  */
    133 export type SpeedPaceUnits = SpeedUnits | PaceUnits;
    134 
    135 /*
    136  * The type for a distance input
    137  */
    138 export interface Distance {
    139   distanceValue: number,
    140   distanceUnit: DistanceUnits,
    141 };
    142 
    143 /*
    144  * The type for a distance/time input pair
    145  */
    146 export interface DistanceTime extends Distance {
    147   time: number,
    148 };
    149 
    150 /*
    151  * The available unit systems
    152  */
    153 export enum UnitSystems {
    154   Metric = 'metric',
    155   Imperial = 'imperial',
    156 };
    157 
    158 /**
    159  * Convert between time units
    160  * @param {number} inputValue The input value
    161  * @param {string} inputUnit The unit of the input
    162  * @param {string} outputUnit The unit of the output
    163  * @returns {number} The output
    164  */
    165 export function convertTime(inputValue: number, inputUnit: TimeUnits,
    166                             outputUnit: TimeUnits): number {
    167   return (inputValue * TimeUnitData[inputUnit].value) / TimeUnitData[outputUnit].value;
    168 }
    169 
    170 /**
    171  * Convert between distance units
    172  * @param {number} inputValue The input value
    173  * @param {string} inputUnit The unit of the input
    174  * @param {string} outputUnit The unit of the output
    175  * @returns {number} The output
    176  */
    177 export function convertDistance(inputValue: number, inputUnit: DistanceUnits,
    178                                 outputUnit: DistanceUnits): number {
    179   return (inputValue * DistanceUnitData[inputUnit].value) / DistanceUnitData[outputUnit].value;
    180 }
    181 
    182 /**
    183  * Convert between speed units
    184  * @param {number} inputValue The input value
    185  * @param {string} inputUnit The unit of the input
    186  * @param {string} outputUnit The unit of the output
    187  * @returns {number} The output
    188  */
    189 export function convertSpeed(inputValue: number, inputUnit: SpeedUnits,
    190                              outputUnit: SpeedUnits): number {
    191   return (inputValue * SpeedUnitData[inputUnit].value) / SpeedUnitData[outputUnit].value;
    192 }
    193 
    194 /**
    195  * Convert between pace units
    196  * @param {number} inputValue The input value
    197  * @param {string} inputUnit The unit of the input
    198  * @param {string} outputUnit The unit of the output
    199  * @returns {number} The output
    200  */
    201 export function convertPace(inputValue: number, inputUnit: PaceUnits,
    202                             outputUnit: PaceUnits): number {
    203   return (inputValue * PaceUnitData[inputUnit].value) / PaceUnitData[outputUnit].value;
    204 }
    205 
    206 /**
    207  * Convert between speed and/or pace units
    208  * @param {number} inputValue The input value
    209  * @param {string} inputUnit The unit of the input
    210  * @param {string} outputUnit The unit of the output
    211  * @returns {number} The output
    212  */
    213 export function convertSpeedPace(inputValue: number, inputUnit: SpeedPaceUnits,
    214                                  outputUnit: SpeedPaceUnits): number {
    215   // Calculate input speed
    216   let speed;
    217   if (inputUnit in PaceUnitData) {
    218     speed = 1 / (inputValue * PaceUnitData[inputUnit as PaceUnits].value);
    219   } else {
    220     speed = inputValue * SpeedUnitData[inputUnit as SpeedUnits].value;
    221   }
    222 
    223   // Calculate output
    224   if (outputUnit in PaceUnitData) {
    225     return (1 / speed) / PaceUnitData[outputUnit as PaceUnits].value;
    226   }
    227   return speed / SpeedUnitData[outputUnit as SpeedUnits].value;
    228 }
    229 
    230 /**
    231  * Detect the user's default unit system
    232  * @returns {UnitSystems} The default unit system
    233  */
    234 export function detectDefaultUnitSystem(): UnitSystems {
    235   // eslint-disable-next-line @typescript-eslint/no-explicit-any
    236   const language = (navigator.language || (navigator as any).userLanguage).toLowerCase();
    237   if (language.endsWith('-us') || language.endsWith('-mm')) {
    238     return UnitSystems.Imperial;
    239   }
    240   return UnitSystems.Metric;
    241 }
    242 
    243 /**
    244  * Format a number as a string
    245  * @param {number} value The number
    246  * @param {number} minPadding The minimum number of digits to show before the decimal point
    247  * @param {number} maxDigits The maximum number of digits to show after the decimal point
    248  * @param {boolean} extraDigits Whether to show extra zeros after the decimal point
    249  * @returns {string} The formatted number
    250  */
    251 export function formatNumber(value: number, minPadding: number = 0, maxDigits: number = 2,
    252                              extraDigits: boolean = true): string {
    253 
    254   // Initialize result
    255   let result = '';
    256 
    257   // Remove sign
    258   const negative = value < 0;
    259   const fixedValue = Math.abs(value);
    260 
    261   // Address edge cases
    262   if (Number.isNaN(fixedValue)) {
    263     return 'NaN';
    264   }
    265   if (fixedValue === Infinity) {
    266     return negative ? '-Infinity' : 'Infinity';
    267   }
    268 
    269   // Convert number to string
    270   if (extraDigits) {
    271     result = fixedValue.toFixed(maxDigits);
    272   } else {
    273     const power = 10 ** maxDigits;
    274     result = (Math.round((fixedValue + Number.EPSILON) * power) / power).toString();
    275   }
    276 
    277   // Add padding
    278   const currentPadding = result.split('.')[0].length;
    279   result = result.padStart(result.length - currentPadding + minPadding, '0');
    280 
    281   // Add negative sign
    282   if (negative) {
    283     result = `-${result}`;
    284   }
    285 
    286   // Return result
    287   return result;
    288 }
    289 
    290 /**
    291  * Format a distance as a string
    292  * @param {Distance} input The distance
    293  * @param {boolean} extraDigits Whether to show extra zeros after the decimal point
    294  * @returns {string} The formatted distance
    295  */
    296 export function formatDistance(input: Distance, extraDigits: boolean) {
    297   return formatNumber(input.distanceValue, 0, 2, extraDigits) + ' '
    298     + DistanceUnitData[input.distanceUnit].symbol;
    299 }
    300 
    301 /**
    302  * Format a duration as a string
    303  * @param {number} value The duration (in seconds)
    304  * @param {number} minPadding The minimum number of digits to show before the decimal point
    305  * @param {number} maxDigits The maximum number of digits to show after the decimal point
    306  * @param {boolean} extraDigits Whether to show extra zeros after the decimal point
    307  * @returns {string} The formatted duration
    308  */
    309 export function formatDuration(value: number, minPadding: number = 6, maxDigits: number = 2,
    310                                extraDigits: boolean = true): string {
    311   // Check if value is NaN
    312   if (Number.isNaN(value)) {
    313     return 'NaN';
    314   }
    315 
    316   // Initialize result
    317   let result = '';
    318 
    319   // Check value sign
    320   if (value < 0) {
    321     result += '-';
    322   }
    323 
    324   // Check if value is valid
    325   if (Math.abs(value) === Infinity) {
    326     return `${result}Infinity`;
    327   }
    328 
    329   // Validate padding
    330   let fixedPadding = Math.min(minPadding, 6);
    331 
    332   // Prevent rounding errors
    333   const fixedValue = parseFloat(Math.abs(value).toFixed(maxDigits));
    334 
    335   // Calculate parts
    336   const hours = Math.floor(fixedValue / 3600);
    337   const minutes = Math.floor((fixedValue % 3600) / 60);
    338   const seconds = fixedValue % 60;
    339 
    340   // Format parts
    341   if (hours !== 0 || fixedPadding >= 5) {
    342     result += hours.toString().padStart(fixedPadding - 4, '0');
    343     result += ':';
    344     fixedPadding = 4;
    345   }
    346   if (minutes !== 0 || fixedPadding >= 3) {
    347     result += minutes.toString().padStart(fixedPadding - 2, '0');
    348     result += ':';
    349     fixedPadding = 2;
    350   }
    351   result += formatNumber(seconds, fixedPadding, maxDigits, extraDigits);
    352 
    353   // Return result
    354   return result;
    355 }
    356 
    357 /**
    358  * Calculate the pace of a distance/time pair and format it as a string
    359  * @param {DistanceTime} input The input distance/time pair
    360  * @param {PaceUnits} unit The desired pace unit
    361  * @returns {string} The formatted pace
    362  */
    363 export function formatPace(input: DistanceTime, unit: PaceUnits) {
    364   const dist = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
    365   const pace = convertPace(input.time / dist, PaceUnits.SecondsPerMeter, unit)
    366   const result = formatDuration(pace, 3, 0, true) + ' ' + PaceUnitData[unit].symbol;
    367   return result;
    368 }
    369 
    370 /**
    371  * Get the default distance unit in a unit system
    372  * @param {UnitSystems} unitSystem The unit system
    373  * @returns {DistanceUnits} The default distance unit
    374  */
    375 export function getDefaultDistanceUnit(unitSystem: UnitSystems): DistanceUnits {
    376   return unitSystem === UnitSystems.Metric ? DistanceUnits.Kilometers : DistanceUnits.Miles;
    377 }
    378 
    379 /**
    380  * Get the default speed unit in a unit system
    381  * @param {UnitSystems} unitSystem The unit system
    382  * @returns {SpeedUnits} The default speed unit
    383  */
    384 export function getDefaultSpeedUnit(unitSystem: UnitSystems): SpeedUnits {
    385   return unitSystem === UnitSystems.Metric ? SpeedUnits.KilometersPerHour
    386     : SpeedUnits.MilesPerHour;
    387 }
    388 
    389 /**
    390  * Get the default pace unit in a unit system
    391  * @param {UnitSystems} unitSystem The unit system
    392  * @returns {PaceUnits} The default pace unit
    393  */
    394 export function getDefaultPaceUnit(unitSystem: UnitSystems): PaceUnits {
    395   return unitSystem === UnitSystems.Metric ? PaceUnits.TimePerKilometer : PaceUnits.TimePerMile;
    396 }