/** * Time utility functions for KGSP */ import { TIME_CONSTANTS } from '../constants/coreConstants'; import type { TimeSignature } from '../types/projectTypes'; /** * Parse and validate time signature string in format "numerator/denominator" * @param timeSignatureStr - String in format "numerator/denominator" (e.g., "4/4", "3/4", "6/8") * @returns TimeSignature object if valid, null if invalid */ export function parseTimeSignature(timeSignatureStr: string): TimeSignature | null { // Remove any whitespace and check format const trimmed = timeSignatureStr.trim(); // Check if it contains exactly one slash const parts = trimmed.split('/'); if (parts.length !== 2) { return null; } // Parse numerator and denominator const numerator = parseInt(parts[0]); const denominator = parseInt(parts[1]); // Check if both are valid numbers if (isNaN(numerator) || isNaN(denominator)) { return null; } // Validate against available values if (!TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_NUMERATORS.includes(numerator)) { return null; } if (!TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_DENOMINATORS.includes(denominator)) { return null; } return { numerator, denominator }; } /** * Get formatted error message for invalid time signature * @returns User-friendly error message with available options */ export function getTimeSignatureErrorMessage(): string { const numerators = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_NUMERATORS.join(', '); const denominators = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_DENOMINATORS.join(', '); return `Invalid time signature format. Please use "numerator/denominator" format.\n\nAvailable numerators: ${numerators}\nAvailable denominators: ${denominators}\n\nExamples: 4/4, 3/4, 6/8, 12/8`; } /** * Convert beats to combined bar/beat and time format (BBB:B | mm:ss:mmm) * @param beats - Current position in beats * @param bpm - Beats per minute * @param timeSignature - Time signature object with numerator and denominator * @returns Formatted time string in BBB:B | mm:ss:mmm format */ export function beatsToTimeString( beats: number, bpm: number, timeSignature: { numerator: number; denominator: number } ): string { // Calculate bar and beat information const beatsPerBar = timeSignature.numerator; const currentBar = Math.floor(beats / beatsPerBar) + 1; // 1-indexed bars const beatInBar = Math.floor(beats % beatsPerBar) + 1; // 1-indexed beats within bar // Calculate total seconds from beats for time display const totalSeconds = (beats / bpm) * 60; // Extract minutes, seconds, and milliseconds const minutes = Math.floor(totalSeconds / 60); const seconds = Math.floor(totalSeconds % 60); const milliseconds = Math.floor((totalSeconds % 1) * 1000); // Format bar/beat part const BBB = currentBar.toString().padStart(3, '0'); const B = beatInBar.toString(); // Format time part with leading zeros const mm = minutes.toString().padStart(2, '0'); const ss = seconds.toString().padStart(2, '0'); const mmm = milliseconds.toString().padStart(3, '0'); return `${BBB}:${B} | ${mm}:${ss}:${mmm}`; } /** * Format a Date object into a human-readable local datetime string with timezone. * Example: 2025/08/21, 19:57:11 GMT-7 */ export function formatLocalDateTime(date: Date): string { const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short' }; return date.toLocaleString(undefined, options); }