UNPKG

@airia-in/run-app-data-format

Version:

Shared data formatting library for Airia fitness platform

338 lines (297 loc) 10.3 kB
/** * 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); } }