/** * Format the date/time. * If the date is today, show the time. Otherwise, show the date in "day month year" format. * @param dateString The date string to format * @returns Formatted date/time string */ export const utilityFormatDate = (dateString: string): string => { const date = new Date(dateString); const today = new Date(); // save today date at midnight for comparison const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()); // check if the date is today if ( date >= todayMidnight && date < new Date(todayMidnight.getTime() + 86400000 /* milliseconds in a day sheeesh */) ) { // show time if today return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } else { // "day month year" format return date.toLocaleDateString(undefined, { day: "numeric", month: "long", year: "numeric" }); } }; /** * Format bytes into human-readable size * @param bytes Number of bytes to format * @returns Formatted size string */ export const utilityFormatSize = (bytes: number): string => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); // tf is pow return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; /** * Convert a formatted size string back to bytes * @param formattedSize Formatted size string (must be in the same format produced by `utilityFormatSize()`) * @returns Number of bytes */ export const utilityUnformatSize = (formattedSize: string): number => { const sizes = ["Bytes", "KB", "MB", "GB"]; const [value, unit] = formattedSize.split(" "); const index = sizes.indexOf(unit); return parseFloat(value) * Math.pow(1024, index); }; /** * Extract filename stem and extension from a file path * @param filePath The file path to parse * @returns Object containing `fileStem` and `fileExtension`. `fileStem` is the filename without the extension, and `fileExtension` is the extension in lowercase (or an empty string if there is no extension). * @throws Error if the file path is invalid */ export const utilityExtractFilename = (filePath: string): { fileStem: string; fileExtension: string } | null => { const lastSlashIndex = filePath.lastIndexOf("/"); const lastDotIndex = filePath.lastIndexOf("."); // check if the last slash comes before the last dot if (lastDotIndex !== -1 && lastSlashIndex > lastDotIndex) { console.error("Invalid file path: the last slash comes before the last dot"); throw new Error("Invalid file path: the last slash comes before the last dot"); } // check if the file path ends with a slash if (lastSlashIndex === filePath.length - 1) { console.error("Invalid file path: the file path ends with a slash"); throw new Error("Invalid file path: the file path ends with a slash"); } let fileStem: string, fileExtension: string; if (lastDotIndex === -1 || lastDotIndex < lastSlashIndex) { // if there's no dot after the last slash, the whole name is the fileStem fileStem = filePath.substring(lastSlashIndex + 1); fileExtension = ""; } else { fileStem = filePath.substring(lastSlashIndex + 1, lastDotIndex); fileExtension = filePath.substring(lastDotIndex + 1).toLowerCase(); } return { fileStem: fileStem, fileExtension: fileExtension }; }; /** * Clamps a value to a lower and upper bound * @param value Value to clamp * @param min Lower bound, defaults to negative `Number.MAX_VALUE` * @param max Upper bound, defaults to `Number.MAX_VALUE` * @returns Clamped value */ export function clampNumber(value: number, min: number = -Number.MAX_VALUE, max: number = Number.MAX_VALUE): number { if (isNaN(value)) { // if (CONSTANTS.isDevBranch) throw new Error("NaN passed into clampNumber()"); return min; } return Math.max(Math.min(value, max), min); } /** * Clamps a value to an integer within a lower and upper bound * @param value Value to clamp * @param min Lower bound, defaults to negative `Number.MAX_SAFE_INTEGER` * @param max Upper bound, defaults to `Number.MAX_SAFE_INTEGER` * @returns Clamped integer value */ export function clampInteger( value: number, min: number = -Number.MAX_SAFE_INTEGER, max: number = Number.MAX_SAFE_INTEGER, ): number { if (isNaN(value)) { // if (CONSTANTS.isDevBranch) throw new Error("NaN passed into clampInteger()"); return min; } return Math.round(Math.max(Math.min(value, max), min)); } /** * Checks that a variable is a valid number * A valid number must be of type "number" and cannot be NaN * @param n The number to check * @returns True if n is a valid number, false otherwise */ export function isValidNumber(n: number): boolean { return typeof n === "number" && !isNaN(n); } /** * Generate a random number between min and max (inclusive) * @param min Minimum value (inclusive) * @param max Maximum value (inclusive) * @returns Random number between min and max */ export function randomNumber(min: number, max: number): number { if (min > max) throw new Error("min cannot be greater than max"); return Math.random() * (max - min) + min; } /** * Generate a random integer between min and max (inclusive) * @param min Minimum value (inclusive) * @param max Maximum value (inclusive) * @returns Random integer between min and max */ export function randomInteger(min: number, max: number): number { return Math.floor(randomNumber(min, max + 1)); }