2024-01-01 16:37:07 +01:00
|
|
|
import strftime from "strftime";
|
|
|
|
import { fromUnixTime, intervalToDuration, formatDuration } from "date-fns";
|
2022-02-27 15:04:12 +01:00
|
|
|
export const longToDate = (long: number): Date => new Date(long * 1000);
|
|
|
|
export const epochToLong = (date: number): number => date / 1000;
|
|
|
|
export const dateToLong = (date: Date): number => epochToLong(date.getTime());
|
|
|
|
|
|
|
|
const getDateTimeYesterday = (dateTime: Date): Date => {
|
|
|
|
const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000;
|
|
|
|
return new Date(dateTime.getTime() - twentyFourHoursInMilliseconds);
|
2022-02-26 20:11:00 +01:00
|
|
|
};
|
2022-02-27 15:04:12 +01:00
|
|
|
|
|
|
|
const getNowYesterday = (): Date => {
|
|
|
|
return getDateTimeYesterday(new Date());
|
2022-02-26 20:11:00 +01:00
|
|
|
};
|
2022-02-27 15:04:12 +01:00
|
|
|
|
|
|
|
export const getNowYesterdayInLong = (): number => {
|
|
|
|
return dateToLong(getNowYesterday());
|
|
|
|
};
|
2023-01-15 16:43:40 +01:00
|
|
|
|
|
|
|
/**
|
2023-02-22 14:54:16 +01:00
|
|
|
* This function takes in a Unix timestamp, configuration options for date/time display, and an optional strftime format string,
|
|
|
|
* and returns a formatted date/time string.
|
|
|
|
*
|
|
|
|
* If the Unix timestamp is not provided, it returns "Invalid time".
|
|
|
|
*
|
|
|
|
* The configuration options determine how the date and time are formatted.
|
|
|
|
* The `timezone` option allows you to specify a specific timezone for the output, otherwise the user's browser timezone will be used.
|
|
|
|
* The `use12hour` option allows you to display time in a 12-hour format if true, and 24-hour format if false.
|
|
|
|
* The `dateStyle` and `timeStyle` options allow you to specify pre-defined formats for displaying the date and time.
|
|
|
|
* The `strftime_fmt` option allows you to specify a custom format using the strftime syntax.
|
|
|
|
*
|
|
|
|
* If both `strftime_fmt` and `dateStyle`/`timeStyle` are provided, `strftime_fmt` takes precedence.
|
|
|
|
*
|
|
|
|
* @param unixTimestamp The Unix timestamp to format
|
|
|
|
* @param config An object containing the configuration options for date/time display
|
|
|
|
* @returns The formatted date/time string, or "Invalid time" if the Unix timestamp is not provided or invalid.
|
2023-01-15 16:43:40 +01:00
|
|
|
*/
|
2023-02-22 14:54:16 +01:00
|
|
|
|
2023-07-11 13:19:58 +02:00
|
|
|
// only used as a fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat
|
|
|
|
const formatMap: {
|
|
|
|
[k: string]: {
|
2023-12-16 17:20:59 +01:00
|
|
|
date: {
|
|
|
|
year: "numeric" | "2-digit";
|
|
|
|
month: "long" | "short" | "2-digit";
|
|
|
|
day: "numeric" | "2-digit";
|
|
|
|
};
|
|
|
|
time: {
|
|
|
|
hour: "numeric";
|
|
|
|
minute: "numeric";
|
|
|
|
second?: "numeric";
|
|
|
|
timeZoneName?: "short" | "long";
|
|
|
|
};
|
2023-07-11 13:19:58 +02:00
|
|
|
};
|
|
|
|
} = {
|
|
|
|
full: {
|
2023-12-16 17:20:59 +01:00
|
|
|
date: { year: "numeric", month: "long", day: "numeric" },
|
|
|
|
time: {
|
|
|
|
hour: "numeric",
|
|
|
|
minute: "numeric",
|
|
|
|
second: "numeric",
|
|
|
|
timeZoneName: "long",
|
|
|
|
},
|
2023-07-11 13:19:58 +02:00
|
|
|
},
|
|
|
|
long: {
|
2023-12-16 17:20:59 +01:00
|
|
|
date: { year: "numeric", month: "long", day: "numeric" },
|
|
|
|
time: {
|
|
|
|
hour: "numeric",
|
|
|
|
minute: "numeric",
|
|
|
|
second: "numeric",
|
|
|
|
timeZoneName: "long",
|
|
|
|
},
|
2023-07-11 13:19:58 +02:00
|
|
|
},
|
|
|
|
medium: {
|
2023-12-16 17:20:59 +01:00
|
|
|
date: { year: "numeric", month: "short", day: "numeric" },
|
|
|
|
time: { hour: "numeric", minute: "numeric", second: "numeric" },
|
|
|
|
},
|
|
|
|
short: {
|
|
|
|
date: { year: "2-digit", month: "2-digit", day: "2-digit" },
|
|
|
|
time: { hour: "numeric", minute: "numeric" },
|
2023-07-11 13:19:58 +02:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attempts to get the system's time zone using Intl.DateTimeFormat. If that fails (for instance, in environments
|
|
|
|
* where Intl is not fully supported), it calculates the UTC offset for the current system time and returns
|
|
|
|
* it in a string format.
|
|
|
|
*
|
|
|
|
* Keeping the Intl.DateTimeFormat for now, as this is the recommended way to get the time zone.
|
|
|
|
* https://stackoverflow.com/a/34602679
|
|
|
|
*
|
|
|
|
* Intl.DateTimeFormat function as of April 2023, works in 95.03% of the browsers used globally
|
|
|
|
* https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_resolvedoptions_computed_timezone
|
|
|
|
*
|
|
|
|
* @returns {string} The resolved time zone or a calculated UTC offset.
|
|
|
|
* The returned string will either be a named time zone (e.g., "America/Los_Angeles"), or it will follow
|
|
|
|
* the format "UTC±HH:MM".
|
|
|
|
*/
|
|
|
|
const getResolvedTimeZone = () => {
|
|
|
|
try {
|
|
|
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
|
} catch (error) {
|
|
|
|
const offsetMinutes = new Date().getTimezoneOffset();
|
2023-12-16 17:20:59 +01:00
|
|
|
return `UTC${offsetMinutes < 0 ? "+" : "-"}${Math.abs(offsetMinutes / 60)
|
2023-07-11 13:19:58 +02:00
|
|
|
.toString()
|
2023-12-16 17:20:59 +01:00
|
|
|
.padStart(2, "0")}:${Math.abs(offsetMinutes % 60)
|
2023-07-11 13:19:58 +02:00
|
|
|
.toString()
|
2023-12-16 17:20:59 +01:00
|
|
|
.padStart(2, "0")}`;
|
2023-07-11 13:19:58 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Formats a Unix timestamp into a human-readable date/time string.
|
|
|
|
*
|
|
|
|
* The format of the output string is determined by a configuration object passed as an argument, which
|
|
|
|
* may specify a time zone, 12- or 24-hour time, and various stylistic options for the date and time.
|
|
|
|
* If these options are not specified, the function will use system defaults or sensible fallbacks.
|
|
|
|
*
|
|
|
|
* The function is robust to environments where the Intl API is not fully supported, and includes a
|
|
|
|
* fallback method to create a formatted date/time string in such cases.
|
|
|
|
*
|
|
|
|
* @param {number} unixTimestamp - The Unix timestamp to be formatted.
|
|
|
|
* @param {DateTimeStyle} config - User configuration object.
|
|
|
|
* @returns {string} A formatted date/time string.
|
|
|
|
*
|
|
|
|
* @throws {Error} If the given unixTimestamp is not a valid number, the function will return 'Invalid time'.
|
|
|
|
*/
|
2023-12-16 17:20:59 +01:00
|
|
|
export const formatUnixTimestampToDateTime = (
|
|
|
|
unixTimestamp: number,
|
|
|
|
config: {
|
|
|
|
timezone?: string;
|
|
|
|
time_format?: "browser" | "12hour" | "24hour";
|
|
|
|
date_style?: "full" | "long" | "medium" | "short";
|
|
|
|
time_style?: "full" | "long" | "medium" | "short";
|
|
|
|
strftime_fmt?: string;
|
2024-02-28 23:23:56 +01:00
|
|
|
},
|
2023-12-16 17:20:59 +01:00
|
|
|
): string => {
|
|
|
|
const { timezone, time_format, date_style, time_style, strftime_fmt } =
|
|
|
|
config;
|
|
|
|
const locale = window.navigator?.language || "en-US";
|
2023-01-15 16:43:40 +01:00
|
|
|
if (isNaN(unixTimestamp)) {
|
2023-12-16 17:20:59 +01:00
|
|
|
return "Invalid time";
|
2023-01-15 16:43:40 +01:00
|
|
|
}
|
2023-02-26 16:37:18 +01:00
|
|
|
|
2023-01-15 16:43:40 +01:00
|
|
|
try {
|
|
|
|
const date = new Date(unixTimestamp * 1000);
|
2023-07-11 13:19:58 +02:00
|
|
|
const resolvedTimeZone = getResolvedTimeZone();
|
2023-02-22 14:54:16 +01:00
|
|
|
|
2023-07-11 13:19:58 +02:00
|
|
|
// use strftime_fmt if defined in config
|
2023-02-22 14:54:16 +01:00
|
|
|
if (strftime_fmt) {
|
2023-07-11 13:19:58 +02:00
|
|
|
const offset = getUTCOffset(date, timezone || resolvedTimeZone);
|
2023-12-16 17:20:59 +01:00
|
|
|
const strftime_locale = strftime.timezone(offset);
|
2023-02-22 14:54:16 +01:00
|
|
|
return strftime_locale(strftime_fmt, date);
|
|
|
|
}
|
|
|
|
|
2023-07-11 13:19:58 +02:00
|
|
|
// DateTime format options
|
|
|
|
const options: Intl.DateTimeFormatOptions = {
|
2023-02-22 14:54:16 +01:00
|
|
|
dateStyle: date_style,
|
|
|
|
timeStyle: time_style,
|
2023-12-16 17:20:59 +01:00
|
|
|
hour12: time_format !== "browser" ? time_format == "12hour" : undefined,
|
2023-07-11 13:19:58 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Only set timeZone option when resolvedTimeZone does not match UTC±HH:MM format, or when timezone is set in config
|
|
|
|
const isUTCOffsetFormat = /^UTC[+-]\d{2}:\d{2}$/.test(resolvedTimeZone);
|
|
|
|
if (timezone || !isUTCOffsetFormat) {
|
|
|
|
options.timeZone = timezone || resolvedTimeZone;
|
|
|
|
}
|
|
|
|
|
|
|
|
const formatter = new Intl.DateTimeFormat(locale, options);
|
|
|
|
const formattedDateTime = formatter.format(date);
|
|
|
|
|
|
|
|
// Regex to check for existence of time. This is needed because dateStyle/timeStyle is not always supported.
|
|
|
|
const containsTime = /\d{1,2}:\d{1,2}/.test(formattedDateTime);
|
|
|
|
|
|
|
|
// fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat
|
|
|
|
// This works even tough the timezone is undefined, it will use the runtime's default time zone
|
|
|
|
if (!containsTime) {
|
2023-12-16 17:20:59 +01:00
|
|
|
const dateOptions = {
|
|
|
|
...formatMap[date_style ?? ""]?.date,
|
|
|
|
timeZone: options.timeZone,
|
|
|
|
hour12: options.hour12,
|
|
|
|
};
|
|
|
|
const timeOptions = {
|
|
|
|
...formatMap[time_style ?? ""]?.time,
|
|
|
|
timeZone: options.timeZone,
|
|
|
|
hour12: options.hour12,
|
|
|
|
};
|
2023-07-11 13:19:58 +02:00
|
|
|
|
2023-12-16 17:20:59 +01:00
|
|
|
return `${date.toLocaleDateString(
|
|
|
|
locale,
|
2024-02-28 23:23:56 +01:00
|
|
|
dateOptions,
|
2023-12-16 17:20:59 +01:00
|
|
|
)} ${date.toLocaleTimeString(locale, timeOptions)}`;
|
2023-07-11 13:19:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return formattedDateTime;
|
2023-01-15 16:43:40 +01:00
|
|
|
} catch (error) {
|
2023-12-16 17:20:59 +01:00
|
|
|
return "Invalid time";
|
2023-01-15 16:43:40 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This function takes in start and end time in unix timestamp,
|
|
|
|
* and returns the duration between start and end time in hours, minutes and seconds.
|
|
|
|
* If end time is not provided, it returns 'In Progress'
|
|
|
|
* @param start_time: number - Unix timestamp for start time
|
|
|
|
* @param end_time: number|null - Unix timestamp for end time
|
|
|
|
* @returns string - duration or 'In Progress' if end time is not provided
|
|
|
|
*/
|
2023-12-16 17:20:59 +01:00
|
|
|
export const getDurationFromTimestamps = (
|
|
|
|
start_time: number,
|
2024-02-28 23:23:56 +01:00
|
|
|
end_time: number | null,
|
2023-12-16 17:20:59 +01:00
|
|
|
): string => {
|
2023-01-15 16:43:40 +01:00
|
|
|
if (isNaN(start_time)) {
|
2023-12-16 17:20:59 +01:00
|
|
|
return "Invalid start time";
|
2023-01-15 16:43:40 +01:00
|
|
|
}
|
2023-12-16 17:20:59 +01:00
|
|
|
let duration = "In Progress";
|
2023-01-15 16:43:40 +01:00
|
|
|
if (end_time !== null) {
|
|
|
|
if (isNaN(end_time)) {
|
2023-12-16 17:20:59 +01:00
|
|
|
return "Invalid end time";
|
2023-01-15 16:43:40 +01:00
|
|
|
}
|
|
|
|
const start = fromUnixTime(start_time);
|
|
|
|
const end = fromUnixTime(end_time);
|
|
|
|
duration = formatDuration(intervalToDuration({ start, end }), {
|
2023-12-16 17:20:59 +01:00
|
|
|
format: ["hours", "minutes", "seconds"],
|
2024-03-21 15:26:13 +01:00
|
|
|
})
|
|
|
|
.replace("hours", "h")
|
|
|
|
.replace("minutes", "m")
|
|
|
|
.replace("seconds", "s");
|
2023-01-15 16:43:40 +01:00
|
|
|
}
|
|
|
|
return duration;
|
|
|
|
};
|
2023-03-18 13:32:39 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Adapted from https://stackoverflow.com/a/29268535 this takes a timezone string and
|
|
|
|
* returns the offset of that timezone from UTC in minutes.
|
|
|
|
* @param timezone string representation of the timezone the user is requesting
|
|
|
|
* @returns number of minutes offset from UTC
|
|
|
|
*/
|
2024-05-20 01:08:32 +02:00
|
|
|
export const getUTCOffset = (
|
|
|
|
date: Date,
|
|
|
|
timezone: string = getResolvedTimeZone(),
|
|
|
|
): number => {
|
2023-07-11 13:19:58 +02:00
|
|
|
// If timezone is in UTC±HH:MM format, parse it to get offset
|
|
|
|
const utcOffsetMatch = timezone.match(/^UTC([+-])(\d{2}):(\d{2})$/);
|
|
|
|
if (utcOffsetMatch) {
|
|
|
|
const hours = parseInt(utcOffsetMatch[2], 10);
|
|
|
|
const minutes = parseInt(utcOffsetMatch[3], 10);
|
2023-12-16 17:20:59 +01:00
|
|
|
return (utcOffsetMatch[1] === "+" ? 1 : -1) * (hours * 60 + minutes);
|
2023-07-11 13:19:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, calculate offset using provided timezone
|
2024-03-10 14:17:48 +01:00
|
|
|
const utcDate = new Date(date.getTime());
|
2023-03-18 13:32:39 +01:00
|
|
|
// locale of en-CA is required for proper locale format
|
2023-12-16 17:20:59 +01:00
|
|
|
let iso = utcDate
|
|
|
|
.toLocaleString("en-CA", { timeZone: timezone, hour12: false })
|
|
|
|
.replace(", ", "T");
|
|
|
|
iso += `.${utcDate.getMilliseconds().toString().padStart(3, "0")}`;
|
2023-11-11 01:12:48 +01:00
|
|
|
let target = new Date(`${iso}Z`);
|
|
|
|
|
|
|
|
// safari doesn't like the default format
|
|
|
|
if (isNaN(target.getTime())) {
|
|
|
|
iso = iso.replace("T", " ").split(".")[0];
|
|
|
|
target = new Date(`${iso}+000`);
|
|
|
|
}
|
|
|
|
|
2024-05-20 01:08:32 +02:00
|
|
|
return Math.round(
|
2024-03-10 14:17:48 +01:00
|
|
|
(target.getTime() - utcDate.getTime() - date.getTimezoneOffset()) /
|
2024-05-20 01:08:32 +02:00
|
|
|
60 /
|
|
|
|
1000,
|
2024-03-10 14:17:48 +01:00
|
|
|
);
|
2023-07-11 13:19:58 +02:00
|
|
|
};
|
2024-01-01 16:37:07 +01:00
|
|
|
|
|
|
|
export function getRangeForTimestamp(timestamp: number) {
|
|
|
|
const date = new Date(timestamp * 1000);
|
|
|
|
date.setMinutes(0, 0, 0);
|
|
|
|
const start = date.getTime() / 1000;
|
|
|
|
date.setHours(date.getHours() + 1);
|
2024-01-31 13:29:18 +01:00
|
|
|
|
|
|
|
// ensure not to go past current time
|
2024-02-06 00:54:08 +01:00
|
|
|
return { start, end: endOfHourOrCurrentTime(date.getTime() / 1000) };
|
|
|
|
}
|
|
|
|
|
|
|
|
export function endOfHourOrCurrentTime(timestamp: number) {
|
|
|
|
const now = new Date();
|
|
|
|
now.setMilliseconds(0);
|
|
|
|
return Math.min(timestamp, now.getTime() / 1000);
|
2024-01-01 16:37:07 +01:00
|
|
|
}
|
2024-01-31 13:29:18 +01:00
|
|
|
|
2024-02-25 20:04:44 +01:00
|
|
|
export function getEndOfDayTimestamp(date: Date) {
|
|
|
|
date.setHours(23, 59, 59, 999);
|
|
|
|
return date.getTime() / 1000;
|
|
|
|
}
|
|
|
|
|
2024-01-31 13:29:18 +01:00
|
|
|
export function isCurrentHour(timestamp: number) {
|
|
|
|
const now = new Date();
|
2024-05-28 16:09:17 +02:00
|
|
|
now.setUTCMinutes(0, 0, 0);
|
2024-05-22 17:27:00 +02:00
|
|
|
|
2024-05-28 16:09:17 +02:00
|
|
|
return timestamp > now.getTime() / 1000;
|
2024-01-31 13:29:18 +01:00
|
|
|
}
|
2024-09-18 20:18:16 +02:00
|
|
|
|
|
|
|
export const convertLocalDateToTimestamp = (dateString: string): number => {
|
|
|
|
// Ensure the date string is in the correct format (8 digits)
|
|
|
|
if (!/^\d{8}$/.test(dateString)) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine the local date format
|
|
|
|
const format = new Intl.DateTimeFormat()
|
|
|
|
.formatToParts(new Date())
|
|
|
|
.reduce((acc, part) => {
|
|
|
|
if (part.type === "day") acc.push("D");
|
|
|
|
if (part.type === "month") acc.push("M");
|
|
|
|
if (part.type === "year") acc.push("Y");
|
|
|
|
return acc;
|
|
|
|
}, [] as string[])
|
|
|
|
.join("");
|
|
|
|
|
|
|
|
let day: string, month: string, year: string;
|
|
|
|
|
|
|
|
// Parse the date string according to the detected format
|
|
|
|
switch (format) {
|
|
|
|
case "DMY":
|
|
|
|
[day, month, year] = [
|
|
|
|
dateString.slice(0, 2),
|
|
|
|
dateString.slice(2, 4),
|
|
|
|
dateString.slice(4),
|
|
|
|
];
|
|
|
|
break;
|
|
|
|
case "MDY":
|
|
|
|
[month, day, year] = [
|
|
|
|
dateString.slice(0, 2),
|
|
|
|
dateString.slice(2, 4),
|
|
|
|
dateString.slice(4),
|
|
|
|
];
|
|
|
|
break;
|
|
|
|
case "YMD":
|
|
|
|
[year, month, day] = [
|
|
|
|
dateString.slice(0, 2),
|
|
|
|
dateString.slice(2, 4),
|
|
|
|
dateString.slice(4),
|
|
|
|
];
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a Date object based on the local timezone
|
|
|
|
const localDate = new Date(`${year}-${month}-${day}T00:00:00`);
|
|
|
|
|
|
|
|
// Check if the date is valid
|
|
|
|
if (isNaN(localDate.getTime())) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert local date to UTC timestamp
|
|
|
|
const timestamp = localDate.getTime();
|
|
|
|
|
|
|
|
return timestamp;
|
|
|
|
};
|
|
|
|
|
|
|
|
export function getIntlDateFormat() {
|
|
|
|
return new Intl.DateTimeFormat()
|
|
|
|
.formatToParts(new Date())
|
|
|
|
.reduce((acc, part) => {
|
|
|
|
if (part.type === "day") acc.push("DD");
|
|
|
|
if (part.type === "month") acc.push("MM");
|
|
|
|
if (part.type === "year") acc.push("YYYY");
|
|
|
|
return acc;
|
|
|
|
}, [] as string[])
|
|
|
|
.join("");
|
|
|
|
}
|