@airia-in/run-app-data-format
Version:
Shared data formatting library for Airia fitness platform
338 lines (297 loc) • 10.3 kB
text/typescript
/**
* Data Format Library
* Centralized formatting functions for time, distance, pace, HR, and other fitness metrics
*/
/**
* Converts speed from meters/second to minutes/km.
* @param ms Speed in meters/second
* @returns Pace in minutes/km (as a string "mm:ss")
*/
export function msToMinPerKm(ms: number | string): string {
if (typeof ms === "string") {
ms = parseFloat(ms);
}
if (ms <= 0 || isNaN(ms)) return "-";
const pace = 1000 / (60 * ms); // minutes per km
const minutes = Math.floor(pace);
const seconds = Math.round((pace - minutes) * 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
/**
* Converts distance from meters to kilometers or meters based on value.
* @param meters Distance in meters
* @returns Formatted distance string with unit
*/
export function mToKm(meters: number | string): string {
if (typeof meters === "string") {
meters = parseFloat(meters);
}
if (meters <= 0 || isNaN(meters)) return "-";
if (meters >= 1000) {
return `${(meters / 1000).toFixed(1)} km`;
}
return `${Math.round(meters)} m`;
}
/**
* Formats duration from seconds to various time formats.
* @param seconds Duration in seconds
* @param format Format type: 'hh:mm:ss', 'mm:ss', 'humanReadable'
* @returns Formatted duration string
*/
export function formatDuration(seconds: number | string, format: 'hh:mm:ss' | 'mm:ss' | 'humanReadable' = 'hh:mm:ss'): string {
if (typeof seconds === "string") {
seconds = parseFloat(seconds);
}
if (seconds <= 0 || isNaN(seconds)) return "-";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.round(seconds % 60);
switch (format) {
case 'mm:ss':
const totalMinutes = Math.floor(seconds / 60);
return `${totalMinutes}:${secs.toString().padStart(2, '0')}`;
case 'humanReadable':
const parts: string[] = [];
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
return parts.join(' ');
case 'hh:mm:ss':
default:
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
}
/**
* Formats heart rate value.
* @param hr Heart rate value
* @param includeUnit Whether to include 'bpm' unit
* @returns Formatted heart rate string
*/
export function formatHeartRate(hr: number | string, includeUnit: boolean = true): string {
if (typeof hr === "string") {
hr = parseFloat(hr);
}
if (hr <= 0 || isNaN(hr)) return "-";
const rounded = Math.round(hr);
return includeUnit ? `${rounded} bpm` : `${rounded}`;
}
/**
* Calculates and formats heart rate zones based on max HR.
* @param currentHR Current heart rate
* @param maxHR Maximum heart rate (if not provided, uses 220 - age)
* @param age User's age (used if maxHR not provided)
* @returns Object with zone information
*/
export function getHeartRateZone(currentHR: number, maxHR?: number, age?: number): {
zone: number;
zoneName: string;
percentage: number;
description: string;
} {
// Calculate max HR if not provided
if (!maxHR && age) {
maxHR = 220 - age;
}
if (!maxHR || currentHR <= 0) {
return {
zone: 0,
zoneName: 'Invalid',
percentage: 0,
description: 'Invalid heart rate data'
};
}
const percentage = (currentHR / maxHR) * 100;
if (percentage < 50) {
return { zone: 0, zoneName: 'Rest', percentage, description: 'Very light activity' };
} else if (percentage < 60) {
return { zone: 1, zoneName: 'Warm-up', percentage, description: 'Light activity' };
} else if (percentage < 70) {
return { zone: 2, zoneName: 'Fat Burn', percentage, description: 'Moderate activity' };
} else if (percentage < 80) {
return { zone: 3, zoneName: 'Cardio', percentage, description: 'Hard activity' };
} else if (percentage < 90) {
return { zone: 4, zoneName: 'Peak', percentage, description: 'Very hard activity' };
} else {
return { zone: 5, zoneName: 'Maximum', percentage, description: 'Maximum effort' };
}
}
/**
* Formats speed value to km/h or m/s.
* @param metersPerSecond Speed in m/s
* @param unit Output unit: 'kmh' or 'ms'
* @returns Formatted speed string with unit
*/
export function formatSpeed(metersPerSecond: number | string, unit: 'kmh' | 'ms' = 'kmh'): string {
if (typeof metersPerSecond === "string") {
metersPerSecond = parseFloat(metersPerSecond);
}
if (metersPerSecond <= 0 || isNaN(metersPerSecond)) return "-";
if (unit === 'kmh') {
const kmh = metersPerSecond * 3.6;
return `${kmh.toFixed(1)} km/h`;
}
return `${metersPerSecond.toFixed(1)} m/s`;
}
/**
* Formats elevation/altitude values.
* @param meters Elevation in meters
* @param unit Output unit: 'm' or 'ft'
* @returns Formatted elevation string with unit
*/
export function formatElevation(meters: number | string, unit: 'm' | 'ft' = 'm'): string {
if (typeof meters === "string") {
meters = parseFloat(meters);
}
if (isNaN(meters)) return "-";
if (unit === 'ft') {
const feet = meters * 3.28084;
return `${Math.round(feet)} ft`;
}
return `${Math.round(meters)} m`;
}
/**
* Formats power output (for cycling).
* @param watts Power in watts
* @param includeUnit Whether to include 'W' unit
* @returns Formatted power string
*/
export function formatPower(watts: number | string, includeUnit: boolean = true): string {
if (typeof watts === "string") {
watts = parseFloat(watts);
}
if (watts < 0 || isNaN(watts)) return "-";
const rounded = Math.round(watts);
return includeUnit ? `${rounded} W` : `${rounded}`;
}
/**
* Formats cadence (steps/min for running, rpm for cycling).
* @param cadence Cadence value
* @param type Activity type: 'running' or 'cycling'
* @returns Formatted cadence string with appropriate unit
*/
export function formatCadence(cadence: number | string, type: 'running' | 'cycling' = 'running'): string {
if (typeof cadence === "string") {
cadence = parseFloat(cadence);
}
if (cadence <= 0 || isNaN(cadence)) return "-";
const rounded = Math.round(cadence);
const unit = type === 'running' ? 'spm' : 'rpm';
return `${rounded} ${unit}`;
}
/**
* Formats temperature values.
* @param celsius Temperature in Celsius
* @param unit Output unit: 'C' or 'F'
* @returns Formatted temperature string with unit
*/
export function formatTemperature(celsius: number | string, unit: 'C' | 'F' = 'C'): string {
if (typeof celsius === "string") {
celsius = parseFloat(celsius);
}
if (isNaN(celsius)) return "-";
if (unit === 'F') {
const fahrenheit = (celsius * 9/5) + 32;
return `${Math.round(fahrenheit)}°F`;
}
return `${Math.round(celsius)}°C`;
}
/**
* Formats date to various formats.
* @param date Date string or Date object
* @param format Format type: 'short', 'long', 'iso', 'relative'
* @returns Formatted date string
*/
export function formatDate(date: string | Date, format: 'short' | 'long' | 'iso' | 'relative' = 'short'): string {
if (!date) return '-';
try {
const dateObj = typeof date === 'string' ? new Date(date) : date;
if (isNaN(dateObj.getTime())) return '-';
switch (format) {
case 'short':
return dateObj.toLocaleDateString('en-GB', {
day: '2-digit',
month: '2-digit',
year: '2-digit'
});
case 'long':
return dateObj.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
case 'iso':
return dateObj.toISOString();
case 'relative':
const now = new Date();
const diffMs = now.getTime() - dateObj.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) return 'Today';
if (diffDays === 1) return 'Yesterday';
if (diffDays < 7) return `${diffDays} days ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
return `${Math.floor(diffDays / 365)} years ago`;
default:
return dateObj.toLocaleDateString();
}
} catch (error) {
return '-';
}
}
/**
* Formats calories burned.
* @param calories Calories value
* @param includeUnit Whether to include 'cal' unit
* @returns Formatted calories string
*/
export function formatCalories(calories: number | string, includeUnit: boolean = true): string {
if (typeof calories === "string") {
calories = parseFloat(calories);
}
if (calories < 0 || isNaN(calories)) return "-";
const rounded = Math.round(calories);
return includeUnit ? `${rounded} cal` : `${rounded}`;
}
/**
* Converts and formats pace between different units.
* @param value Pace value
* @param fromUnit Input unit: 'min/km', 'min/mi', 's/m'
* @param toUnit Output unit: 'min/km', 'min/mi', 's/m'
* @returns Formatted pace string
*/
export function convertPace(value: number, fromUnit: 'min/km' | 'min/mi' | 's/m', toUnit: 'min/km' | 'min/mi' | 's/m'): string {
// First convert to seconds per meter (common unit)
let secondsPerMeter: number;
switch (fromUnit) {
case 'min/km':
secondsPerMeter = (value * 60) / 1000;
break;
case 'min/mi':
secondsPerMeter = (value * 60) / 1609.344;
break;
case 's/m':
secondsPerMeter = value;
break;
}
// Then convert to target unit
switch (toUnit) {
case "min/km": {
const minPerKm = (secondsPerMeter * 1000) / 60;
const minutes = Math.floor(minPerKm);
const seconds = Math.round((minPerKm - minutes) * 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
case "min/mi": {
const minPerMi = (secondsPerMeter * 1609.344) / 60;
const minutes = Math.floor(minPerMi);
const seconds = Math.round((minPerMi - minutes) * 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
case "s/m":
return secondsPerMeter.toFixed(2);
}
}