qgenutils
Version:
A security-first Node.js utility library providing authentication, HTTP operations, URL processing, validation, datetime formatting, and template rendering. Designed as a lightweight alternative to heavy npm packages with comprehensive error handling and
205 lines (192 loc) • 9.93 kB
JavaScript
/*
* DateTime Utility Module
*
* OVERVIEW: These utilities rely on JavaScript's native Date APIs for broad compatibility
* and minimal dependencies. Locale awareness is baked in via methods like toLocaleString(),
* allowing dates to automatically render according to each user's system preferences.
*
* This module provides date and time formatting utilities optimized for web applications
* that need to display dates in user-friendly formats and calculate time durations.
*
* DESIGN PHILOSOPHY:
* - Graceful degradation: Invalid inputs return safe fallback values rather than crashing
* - Locale awareness: Uses browser/system locale for natural date formatting
* - Consistent duration format: Always returns HH:MM:SS format for predictable display
* - Error transparency: Logs all operations for debugging while handling errors gracefully
*
* COMMON USE CASES:
* - Displaying "last updated" timestamps in user interfaces
* - Showing elapsed time for processes, sessions, or activities
* - Converting server timestamps to user-readable formats
*/
const { qerrors } = require('qerrors'); // error logging utility
const logger = require('./logger'); // structured logger
/**
* Helper function to validate if a Date object is valid
* This centralizes the date validation logic used by multiple datetime functions
* Invalid inputs resolve to `false` so callers can return safe fallback values
* like "N/A" or "00:00:00" rather than throwing errors.
*
* @param {Date} date - Date object to validate
* @returns {boolean} True if date is valid, false otherwise
*/
function isValidDate(date) {
return !isNaN(date.getTime()) && date.toString() !== 'Invalid Date'; // treat NaN or Invalid Date as bad input
}
/**
* Format an ISO date string to locale-specific display format
*
* RATIONALE: APIs typically return dates in ISO format (2023-12-25T10:30:00.000Z)
* which is machine-readable but not user-friendly. This function converts ISO
* dates to formats that users expect to see in interfaces.
*
* IMPLEMENTATION DECISIONS:
* - Use toLocaleString() for automatic locale adaptation
* - Return "N/A" for empty/invalid inputs rather than throwing errors
* - Handle both null and empty string inputs gracefully
* - Preserve timezone information from the original date
*
* LOCALE BEHAVIOR:
* toLocaleString() automatically formats dates according to the user's system
* locale settings. For example:
* - US format: "12/25/2023, 10:30:00 AM"
* - European format: "25/12/2023, 10:30:00"
* - ISO format in some locales: "2023-12-25, 10:30:00"
*
* This automatic adaptation improves user experience by showing dates in
* formats users are familiar with.
*
* ERROR HANDLING STRATEGY:
* Rather than throwing exceptions for invalid dates, we return "N/A" to
* indicate missing or invalid data. This prevents date formatting errors
* from breaking entire page renders or API responses.
*
* @param {string} dateString - ISO date string to format (e.g., "2023-12-25T10:30:00.000Z")
* @returns {string} Formatted date string or "N/A" if input is invalid/empty
*/
function formatDateTime(dateString) {
console.log(`formatDateTime is running with ${dateString}`); // debug input
logger.debug(`formatDateTime is running with ${dateString}`); // persistent log
try {
// Handle empty or null input gracefully - common in optional date fields
// Returning "N/A" allows the UI to show a neutral value without crashing
if (!dateString) { // short-circuit when input missing or blank
console.log(`formatDateTime is returning N/A`); logger.debug(`formatDateTime is returning N/A`); // return neutral value
return 'N/A'; // return neutral value so UIs know no date was provided
}
// Convert ISO string to Date object and format according to user's locale
// Date constructor can handle various ISO formats automatically
const date = new Date(dateString); // parse ISO string into Date for conversion
// Handle invalid date strings using centralized validation
if (!isValidDate(date)) { // reject invalid dates to avoid misleading output
console.log(`formatDateTime is returning N/A`); logger.debug(`formatDateTime is returning N/A`);
return 'N/A'; // invalid date fallback prevents NaN in output
}
const formatted = date.toLocaleString(); // locale aware formatting for display
console.log(`formatDateTime is returning ${formatted}`); logger.debug(`formatDateTime is returning ${formatted}`);
return formatted; // send formatted date back to caller
} catch (err) {
// Handle invalid date strings that can't be parsed
qerrors(err, 'formatDateTime', dateString); // Log parsing error with input context
logger.error(`formatDateTime failed`, err); // record failure
return 'N/A'; // final fallback keeps consumer code simple on failure
}
}
/**
* Calculate and format duration between two dates in HH:MM:SS format
*
* RATIONALE: Duration calculation is common for showing elapsed time, process
* durations, or time differences. This function provides consistent formatting
* that's easy to read and compare across different time ranges.
*
* IMPLEMENTATION STRATEGY:
* - Use current time as default end date for "time since" scenarios
* - Calculate millisecond difference and convert to time components
* - Always return HH:MM:SS format with zero-padding for consistency
* - Handle edge cases like negative durations or invalid dates
*
* TIME CALCULATION PROCESS:
* 1. Parse both dates to milliseconds since epoch
* 2. Calculate absolute difference in milliseconds
* 3. Convert to hours, minutes, seconds using modular arithmetic
* 4. Format with zero-padding for consistent display width
*
* USE CASES:
* - Process execution time: formatDuration(processStart)
* - Event duration: formatDuration(eventStart, eventEnd)
* - Time tracking: formatDuration(sessionStart)
* - Log analysis: formatDuration(requestStart, requestEnd)
*
* ZERO-PADDING RATIONALE:
* Consistent formatting (01:05:03 vs 1:5:3) makes durations easier to:
* - Visually compare and sort
* - Align in tables and lists
* - Parse programmatically if needed
*
* @param {string} startDateString - ISO date string for start time
* @param {string} endDateString - ISO date string for end time (defaults to current time)
* @returns {string} Duration in HH:MM:SS format or "00:00:00" if start date is invalid
*
* INVALID INPUT POLICY:
* Empty or malformed start dates return "00:00:00" so downstream logic never
* receives NaN values. Invalid end dates throw so the caller can decide whether
* to default to "now" or surface an error.
*/
function formatDuration(startDateString, endDateString) {
console.log(`formatDuration is running with ${startDateString} and ${endDateString}`); logger.debug(`formatDuration is running with ${startDateString} and ${endDateString}`);
try {
// Handle edge cases gracefully
// Empty start date yields zero duration so UIs show neutral value
if (!startDateString || startDateString === '') { // guard against missing start time
console.log(`formatDuration is returning 00:00:00`); logger.debug(`formatDuration is returning 00:00:00`);
return '00:00:00';
}
// Validate start date using centralized validation
const startDate = new Date(startDateString); // parse provided start time
if (!isValidDate(startDate)) {
throw new Error('Invalid start date');
}
// Calculate end time (current time if not provided)
const endTime = endDateString ? new Date(endDateString) : new Date(); // default to now if end omitted
// Validate end date if provided using centralized validation
if (endDateString && !isValidDate(endTime)) { // check provided end date only when set
throw new Error('Invalid end date');
}
// Calculate duration in milliseconds and convert to absolute value
const durationMs = Math.abs(endTime - startDate); // use Math.abs so negative differences show as positive time intervals
// Convert milliseconds to time components using standard time conversion
const hours = Math.floor(durationMs / (1000 * 60 * 60)); // 1000ms*60s*60m = 1hr
const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); // remainder from hours to minutes
const seconds = Math.floor((durationMs % (1000 * 60)) / 1000); // remainder from minutes to seconds
// Format with zero-padding for consistent display (padStart ensures 2 digits)
const formatted = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; // pad for readability
console.log(`formatDuration is returning ${formatted}`); logger.debug(`formatDuration is returning ${formatted}`);
return formatted; // return the duration string
} catch (err) {
// Handle invalid date strings or calculation errors and re-throw
logger.error(`formatDuration failed`, err); qerrors(err, 'formatDuration', { startDateString, endDateString });
throw err; // let caller decide how to report invalid dates
}
}
/*
* Module Export Strategy:
*
* We export both date/time functions because they serve complementary purposes:
*
* 1. formatDateTime - Convert timestamps to human-readable format
* 2. formatDuration - Calculate and display time differences
*
* Both functions are commonly needed together in applications that display
* time-based information (logs, events, processes, etc.).
*
* FUTURE ENHANCEMENTS:
* - Add relative time formatting ("2 hours ago", "in 3 days")
* - Add custom date format options
* - Add timezone conversion utilities
* - Add business day/hour calculations
* - Add internationalization support for duration labels
*/
module.exports = {
formatDateTime, // export for date display
formatDuration // export for duration display
}; // expose utilities for consumption