/** * Shared Utility Functions * Common utilities used across the Climate Scheduler frontend */ // ============================================================================ // TIMEZONE UTILITIES // ============================================================================ /** * Get a Date-like object in the server's timezone * @param {Date} date - The UTC date to convert * @param {string} serverTimeZone - The server's timezone (e.g., 'America/New_York') * @returns {Object} Date-like object with methods that return values in server timezone */ function getServerDate(date, serverTimeZone) { if (!serverTimeZone) return date; // Fallback to local time if no timezone set // Use Intl.DateTimeFormat to get date components in server timezone const formatter = new Intl.DateTimeFormat('en-US', { timeZone: serverTimeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); const parts = formatter.formatToParts(date); const partsObj = {}; parts.forEach(part => { partsObj[part.type] = part.value; }); return { getFullYear: () => parseInt(partsObj.year), getMonth: () => parseInt(partsObj.month) - 1, // JS months are 0-indexed getDate: () => parseInt(partsObj.day), getHours: () => parseInt(partsObj.hour), getMinutes: () => parseInt(partsObj.minute), getSeconds: () => parseInt(partsObj.second), getTime: () => date.getTime(), _originalDate: date }; } /** * Get current server time as Date-like object * @param {string} serverTimeZone - The server's timezone * @returns {Object} Date-like object with current time in server timezone */ function getServerNow(serverTimeZone) { return getServerDate(new Date(), serverTimeZone); } /** * Convert UTC timestamp to server timezone Date-like object * @param {Date} utcDate - The UTC date to convert * @param {string} serverTimeZone - The server's timezone * @returns {Object} Date-like object in server timezone */ function utcToServerDate(utcDate, serverTimeZone) { return getServerDate(utcDate, serverTimeZone); } // ============================================================================ // TEMPERATURE CONVERSION UTILITIES // ============================================================================ /** * Convert Celsius to Fahrenheit * @param {number} celsius - Temperature in Celsius * @returns {number} Temperature in Fahrenheit */ function celsiusToFahrenheit(celsius) { return (celsius * 9/5) + 32; } /** * Convert Fahrenheit to Celsius * @param {number} fahrenheit - Temperature in Fahrenheit * @returns {number} Temperature in Celsius */ function fahrenheitToCelsius(fahrenheit) { return (fahrenheit - 32) * 5/9; } /** * Convert temperature between units * @param {number} temp - Temperature value * @param {string} fromUnit - Source unit ('°C' or '°F') * @param {string} toUnit - Target unit ('°C' or '°F') * @returns {number} Converted temperature */ function convertTemperature(temp, fromUnit, toUnit) { if (fromUnit === toUnit) return temp; if (fromUnit === '°C' && toUnit === '°F') return celsiusToFahrenheit(temp); if (fromUnit === '°F' && toUnit === '°C') return fahrenheitToCelsius(temp); return temp; } // ============================================================================ // TIME UTILITIES // ============================================================================ /** * Convert time string to minutes since midnight * @param {string} timeStr - Time in 'HH:MM' format * @returns {number} Minutes since midnight */ function timeToMinutes(timeStr) { const [hours, minutes] = timeStr.split(':').map(Number); return hours * 60 + minutes; } /** * Convert minutes since midnight to time string * @param {number} minutes - Minutes since midnight * @returns {string} Time in 'HH:MM' format */ function minutesToTime(minutes) { const hours = Math.floor(minutes / 60); const mins = Math.floor(minutes % 60); return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; } /** * Format hours and minutes as 'HH:MM' time string * @param {number} hours - Hours (0-23) * @param {number} minutes - Minutes (0-59) * @returns {string} Time in 'HH:MM' format */ function formatTimeString(hours, minutes) { return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; } /** * Adjust time by adding/subtracting minutes with 24-hour wraparound * @param {string} timeStr - Time in 'HH:MM' format * @param {number} minutesToAdd - Minutes to add (negative to subtract) * @returns {string} New time in 'HH:MM' format */ function adjustTime(timeStr, minutesToAdd) { let totalMinutes = timeToMinutes(timeStr) + minutesToAdd; // Handle wraparound while (totalMinutes < 0) totalMinutes += 1440; while (totalMinutes >= 1440) totalMinutes -= 1440; return minutesToTime(totalMinutes); } /** * Interpolate temperature at a given time (step function - hold until next node) * @param {Array} nodes - Array of schedule nodes with {time, temp} properties * @param {string} timeStr - Time in 'HH:MM' format * @returns {number} Interpolated temperature */ function interpolateTemperature(nodes, timeStr) { if (nodes.length === 0) return 18; const sorted = [...nodes].sort((a, b) => timeToMinutes(a.time) - timeToMinutes(b.time)); const currentMinutes = timeToMinutes(timeStr); // Find the most recent node before or at current time let activeNode = null; for (let i = 0; i < sorted.length; i++) { const nodeMinutes = timeToMinutes(sorted[i].time); if (nodeMinutes <= currentMinutes) { activeNode = sorted[i]; } else { break; } } // If no node found before current time, use last node (wrap around from previous day) if (!activeNode) { activeNode = sorted[sorted.length - 1]; } return activeNode.temp; }