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 }