diff --git a/project/package.json b/project/package.json index 872f3dd4..f6f37803 100644 --- a/project/package.json +++ b/project/package.json @@ -29,6 +29,8 @@ }, "dependencies": { "atomically": "1.7.0", + "date-fns": "^2.30.0", + "date-fns-tz": "^2.0.0", "i18n": "0.15.1", "json-fixer": "1.6.15", "json5": "2.2.3", @@ -59,7 +61,6 @@ "@vitest/coverage-istanbul": "1.0.0-beta.3", "@vitest/ui": "1.0.0-beta.3", "cross-env": "7.0.3", - "date-fns": "^2.30.0", "eslint": "8.51.0", "gulp": "4.0.2", "gulp-execa": "5.0.1", diff --git a/project/src/utils/TimeUtil.ts b/project/src/utils/TimeUtil.ts index 9d2a5dbc..0ba63bd1 100644 --- a/project/src/utils/TimeUtil.ts +++ b/project/src/utils/TimeUtil.ts @@ -1,41 +1,77 @@ import { injectable } from "tsyringe"; +import { formatInTimeZone } from "date-fns-tz"; /** - * Utility class to handle time related problems + * Utility class to handle time related operations. */ @injectable() export class TimeUtil { - public static readonly oneHourAsSeconds = 3600; + public static readonly oneHourAsSeconds = 3600; // Number of seconds in one hour. + /** + * Pads a number with a leading zero if it is less than 10. + * + * @param {number} number - The number to pad. + * @returns {string} The padded number as a string. + */ + private pad(number: number): string + { + return String(number).padStart(2, "0"); + } + + /** + * Formats the time part of a date as a UTC string. + * + * @param {Date} date - The date to format in UTC. + * @returns {string} The formatted time as 'HH-MM-SS'. + */ public formatTime(date: Date): string { - const hours = `0${date.getHours()}`.substr(-2); - const minutes = `0${date.getMinutes()}`.substr(-2); - const seconds = `0${date.getSeconds()}`.substr(-2); + const hours = this.pad(date.getUTCHours()); + const minutes = this.pad(date.getUTCMinutes()); + const seconds = this.pad(date.getUTCSeconds()); return `${hours}-${minutes}-${seconds}`; } + /** + * Formats the date part of a date as a UTC string. + * + * @param {Date} date - The date to format in UTC. + * @returns {string} The formatted date as 'YYYY-MM-DD'. + */ public formatDate(date: Date): string { - const day = `0${date.getDate()}`.substr(-2); - const month = `0${date.getMonth() + 1}`.substr(-2); - return `${date.getFullYear()}-${month}-${day}`; + const day = this.pad(date.getUTCDate()); + const month = this.pad(date.getUTCMonth() + 1); // getUTCMonth returns 0-11 + const year = date.getUTCFullYear(); + return `${year}-${month}-${day}`; } + /** + * Gets the current date as a formatted UTC string. + * + * @returns {string} The current date as 'YYYY-MM-DD'. + */ public getDate(): string { return this.formatDate(new Date()); } + /** + * Gets the current time as a formatted UTC string. + * + * @returns {string} The current time as 'HH-MM-SS'. + */ public getTime(): string { return this.formatTime(new Date()); } /** - * Get timestamp in seconds - * @returns + * Gets the current timestamp in seconds in UTC. + * + * @returns {number} The current timestamp in seconds since the Unix epoch in UTC. */ public getTimestamp(): number { @@ -43,36 +79,33 @@ export class TimeUtil } /** - * mail in eft requires time be in a specific format - * @returns current time in format: 00:00 (hh:mm) + * Gets the current time in UTC in a format suitable for mail in EFT. + * + * @returns {string} The current time as 'HH:MM' in UTC. */ public getTimeMailFormat(): string { - const date = new Date(); - const hours = `0${date.getHours()}`.substr(-2); - const minutes = `0${date.getMinutes()}`.substr(-2); - return `${hours}:${minutes}`; + return formatInTimeZone(new Date(), "UTC", "HH:mm"); } /** - * Mail in eft requires date be in a specific format - * @returns current date in format: 00.00.0000 (dd.mm.yyyy) + * Gets the current date in UTC in a format suitable for emails in EFT. + * + * @returns {string} The current date as 'DD.MM.YYYY' in UTC. */ public getDateMailFormat(): string { - const date = new Date(); - const day = `0${date.getDate()}`.substr(-2); - const month = `0${date.getMonth() + 1}`.substr(-2); - return `${day}.${month}.${date.getFullYear()}`; + return formatInTimeZone(new Date(), "UTC", "dd.MM.yyyy"); } /** - * Convert hours into seconds - * @param hours hours to convert to seconds - * @returns number + * Converts a number of hours into seconds. + * + * @param {number} hours - The number of hours to convert. + * @returns {number} The equivalent number of seconds. */ public getHoursAsSeconds(hours: number): number { - return hours * 3600; + return hours * TimeUtil.oneHourAsSeconds; } } diff --git a/project/tests/utils/TimeUtil.test.ts b/project/tests/utils/TimeUtil.test.ts new file mode 100644 index 00000000..112b0aa9 --- /dev/null +++ b/project/tests/utils/TimeUtil.test.ts @@ -0,0 +1,122 @@ +import "reflect-metadata"; +import { container } from "tsyringe"; +import { vi, afterEach, describe, expect, it, beforeEach } from "vitest"; + +import { TimeUtil } from "@spt-aki/utils/TimeUtil"; + +describe("TimeUtil", () => +{ + let timeUtil: TimeUtil; + let mockedCurrentDate: Date; + + beforeEach(() => + { + timeUtil = container.resolve("TimeUtil"); + + mockedCurrentDate = new Date("2023-01-01T00:00:00Z"); + vi.useFakeTimers(); + vi.setSystemTime(mockedCurrentDate); + }); + + afterEach(() => + { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + describe("pad", () => + { + it("should pad a number with a leading zero if it is less than 10", () => + { + const paddedNumber = (timeUtil as any).pad(1); + expect(paddedNumber).toBe("01"); + }); + + it("should not pad a number larger than 10", () => + { + const paddedNumber = (timeUtil as any).pad(69); + expect(paddedNumber).toBe("69"); + }); + + it("should not pad a number in the hundreds", () => + { + const paddedNumber = (timeUtil as any).pad(420); + expect(paddedNumber).toBe("420"); + }); + }); + + describe("formatTime", () => + { + it("should format the time part of a date as \"HH-MM-SS\"", () => + { + const date = new Date("2023-01-01T12:34:56Z"); + const formattedTime = timeUtil.formatTime(date); + expect(formattedTime).toBe("12-34-56"); + }); + }); + + describe("formatDate", () => + { + it("should format the date part of a date as \"YYYY-MM-DD\"", () => + { + const date = new Date("2023-01-01T12:34:56Z"); + const formattedDate = timeUtil.formatDate(date); + expect(formattedDate).toBe("2023-01-01"); + }); + }); + + describe("getDate", () => + { + it("should get the current date as a formatted UTC string", () => + { + const currentDate = timeUtil.getDate(); + expect(currentDate).toBe("2023-01-01"); + }); + }); + + describe("getTime", () => + { + it("should get the current time as a formatted UTC string", () => + { + const currentTime = timeUtil.getTime(); + expect(currentTime).toBe("00-00-00"); // The mocked date is at midnight UTC. + }); + }); + + describe("getTimestamp", () => + { + it("should get the current timestamp in seconds in UTC", () => + { + const timestamp = timeUtil.getTimestamp(); + expect(timestamp).toBe(Math.floor(mockedCurrentDate.getTime() / 1000)); + }); + }); + + describe("getTimeMailFormat", () => + { + it("should get the current time in UTC in a format suitable for mail in EFT", () => + { + const timeMailFormat = timeUtil.getTimeMailFormat(); + expect(timeMailFormat).toBe("00:00"); // The mocked date is at midnight UTC. + }); + }); + + describe("getDateMailFormat", () => + { + it("should get the current date in UTC in a format suitable for emails in EFT", () => + { + const dateMailFormat = timeUtil.getDateMailFormat(); + expect(dateMailFormat).toBe("01.01.2023"); + }); + }); + + describe("getHoursAsSeconds", () => + { + it("should convert a number of hours into seconds", () => + { + const hours = 5; + const seconds = timeUtil.getHoursAsSeconds(hours); + expect(seconds).toBe(5 * 3600); + }); + }); +});