diff --git a/packages/utils/package.json b/packages/utils/package.json index ddf9fc2f8..b5e74330f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -13,11 +13,9 @@ }, "dependencies": { "deepmerge-ts": "^5.1.0", - "luxon": "^3.4.4", "nanoid": "^5.0.4" }, "devDependencies": { - "@types/luxon": "^3.4.0", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "eslint": "^8.53.0", diff --git a/packages/utils/src/date-utils/timeFormatting.test.ts b/packages/utils/src/date-utils/timeFormatting.test.ts index c2004ad20..6213efc80 100644 --- a/packages/utils/src/date-utils/timeFormatting.test.ts +++ b/packages/utils/src/date-utils/timeFormatting.test.ts @@ -1,6 +1,6 @@ import { dayInMs } from '../timeConstants'; import { MILLIS_PER_HOUR } from './conversionUtils'; -import { millisToString, removeLeadingZero } from './timeFormatting'; +import { formatFromMillis, millisToString, removeLeadingZero } from './timeFormatting'; describe('millisToString()', () => { it('returns fallback if millis is null', () => { @@ -61,3 +61,159 @@ describe('removeLeadingZero()', () => { expect(removeLeadingZero('-00:08:47')).toBe('-8:47'); }); }); + +describe('formatFromMillis()', () => { + it('milliseconds', () => { + const millis = 76211123; // Jan 1, 1970 21:10:11.123 UTC + const format = 'S'; + const expectedResult = '123'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('seconds (no padding)', () => { + const millis = 76211123; + const format = 's'; + const expectedResult = '11'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('seconds (padding)', () => { + const millis = 76211123; + const format = 'ss'; + const expectedResult = '11'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('minute (no padding)', () => { + const millis = 76211123; + const format = 'm'; + const expectedResult = '10'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('minute (padding)', () => { + const millis = 76211123; + const format = 'mm'; + const expectedResult = '10'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('hour - 12 (no padding)', () => { + const millis = 76211123; + const format = 'h'; + const expectedResult = '9'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('hour - 12 (padding)', () => { + const millis = 76211123; + const format = 'hh'; + const expectedResult = '09'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('hour - 24 (no padding)', () => { + const millis = 76211123; + const format = 'H'; + const expectedResult = '21'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('hour - 24 (padding)', () => { + const millis = 76211123; + const format = 'HH'; + const expectedResult = '21'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('formatted time HH:mm:ss', () => { + const millis = 76211123; + const format = 'HH:mm:ss'; + const expectedResult = '21:10:11'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('formatted time hh:mm a', () => { + const millis = 76211123; + const format = 'hh:mm a'; + const expectedResult = '09:10 PM'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('milliseconds', () => { + const millis = 29167345; // 08:06:08.345 + const format = 'S'; + const expectedResult = '345'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('seconds (no padding)', () => { + const millis = 29167345; + const format = 's'; + const expectedResult = '7'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('seconds (padding)', () => { + const millis = 29167345; + const format = 'ss'; + const expectedResult = '07'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('minute (no padding)', () => { + const millis = 29167345; + const format = 'm'; + const expectedResult = '6'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('minute (padding)', () => { + const millis = 29167345; + const format = 'mm'; + const expectedResult = '06'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('hour - 12 (no padding)', () => { + const millis = 29167345; + const format = 'h'; + const expectedResult = '8'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('hour - 12 (padding)', () => { + const millis = 29167345; + const format = 'hh'; + const expectedResult = '08'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('hour - 24 (no padding)', () => { + const millis = 29167345; + const format = 'H'; + const expectedResult = '8'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('hour - 24 (padding)', () => { + const millis = 29167345; + const format = 'HH'; + const expectedResult = '08'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('formatted time HH:mm:ss', () => { + const millis = 29167345; + const format = 'HH:mm:ss'; + const expectedResult = '08:06:07'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); + + it('formatted time hh:mm a', () => { + const millis = 29167345; + const format = 'hh:mm a'; + const expectedResult = '08:06 AM'; + expect(formatFromMillis(millis, format)).toBe(expectedResult); + }); +}); diff --git a/packages/utils/src/date-utils/timeFormatting.ts b/packages/utils/src/date-utils/timeFormatting.ts index ee6667337..1f1a67737 100644 --- a/packages/utils/src/date-utils/timeFormatting.ts +++ b/packages/utils/src/date-utils/timeFormatting.ts @@ -1,4 +1,3 @@ -import { DateTime } from 'luxon'; import type { MaybeNumber } from 'ontime-types'; import { millisToHours, millisToMinutes, millisToSeconds } from './conversionUtils.js'; @@ -71,11 +70,60 @@ export function removeSeconds(timer: string): string { } /** - * @description utility function to format a date in milliseconds using luxon - * @param {number} millis - * @param {string} format - * @return {string} + * Formats a given date into a custom string format based on UTC time. + * + * @param millis - The number of milliseconds. + * @param format - A string specifying the desired output format. + * For example, 'ss' will format the millis as '07' seconds. + * + * @returns The formatted date as a string according to the provided `format` string. + * If input `millis` is smaller than zero, it returns undefined. + * */ -export function formatFromMillis(millis: number, format: string): string { - return DateTime.fromMillis(millis).toUTC().toFormat(format); +export function formatFromMillis(millis: number, format: string): string | undefined { + if (millis < 0) { + return undefined; + } + + const date: Date = new Date(millis); + + const hour24Padded = date.getUTCHours().toString().padStart(2, '0'); + const hour24 = date.getUTCHours().toString(); + const minutePadded = date.getUTCMinutes().toString().padStart(2, '0'); + const minute = date.getUTCMinutes().toString(); + const secondPadded = date.getUTCSeconds().toString().padStart(2, '0'); + const second = date.getUTCSeconds().toString(); + const milliseconds = date.getUTCMilliseconds().toString().padStart(3, '0'); + const hour12 = ((date.getUTCHours() % 12) || 12).toString(); + const hour12Padded = hour12.padStart(2, '0'); + const amPm = date.getUTCHours() >= 12 ? 'PM' : 'AM'; + + const replacements: Record = { + 'HH': hour24Padded, + 'H': hour24, + 'hh': hour12Padded, + 'h': hour12, + 'mm': minutePadded, + 'm': minute, + 'ss': secondPadded, + 's': second, + 'S': milliseconds, + 'a': amPm + }; + + return applyReplacements(format, replacements); +} + +/** + * Applies replacements to a template string based on a dictionary of tokens and their corresponding values. + * + * @param {string} template - The format template string containing tokens to be replaced. + * @param {Record} replacements - A record of tokens and their corresponding values. + * @returns {string} The formatted string with all tokens replaced by their values. + */ +function applyReplacements(template: string, replacements: Record): string { + return Object.keys(replacements).reduce((result, token) => { + const regex = new RegExp(`\\b${token}\\b`, 'g'); + return result.replace(regex, replacements[token]); + }, template); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e997e6272..01d7cc92e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -391,16 +391,10 @@ importers: deepmerge-ts: specifier: ^5.1.0 version: 5.1.0 - luxon: - specifier: ^3.4.4 - version: 3.4.4 nanoid: specifier: ^5.0.4 version: 5.0.4 devDependencies: - '@types/luxon': - specifier: ^3.4.0 - version: 3.4.0 '@typescript-eslint/eslint-plugin': specifier: ^6.10.0 version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.4.3) @@ -430,7 +424,7 @@ importers: version: 5.4.3 vitest: specifier: ^1.2.2 - version: 1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1) + version: 1.2.2 packages: @@ -3312,10 +3306,6 @@ packages: resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: false - /@types/luxon@3.4.0: - resolution: {integrity: sha512-PEVoA4MOfSsFNaPrZjIUGUZujBDxnO/tj2A2N9KfzlR+pNgpBdDuk0TmRvSMAVUP5q4q8IkMEZ8UOp3MIr+QgA==} - dev: true - /@types/mime@3.0.1: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} dev: true @@ -7060,11 +7050,6 @@ packages: yallist: 4.0.0 dev: true - /luxon@3.4.4: - resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} - engines: {node: '>=12'} - dev: false - /lz-string@1.4.4: resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} hasBin: true @@ -9239,6 +9224,27 @@ packages: dev: true optional: true + /vite-node@1.2.2: + resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.1 + picocolors: 1.0.0 + vite: 5.1.0 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-node@1.2.2(@types/node@18.11.18)(sass@1.57.1): resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9301,6 +9307,41 @@ packages: - typescript dev: true + /vite@5.1.0: + resolution: {integrity: sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.19.10 + postcss: 8.4.35 + rollup: 4.9.1 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vite@5.1.0(@types/node@18.11.18)(sass@1.57.1): resolution: {integrity: sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9338,6 +9379,62 @@ packages: fsevents: 2.3.3 dev: true + /vitest@1.2.2: + resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': ^1.0.0 + '@vitest/ui': ^1.0.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@vitest/expect': 1.2.2 + '@vitest/runner': 1.2.2 + '@vitest/snapshot': 1.2.2 + '@vitest/spy': 1.2.2 + '@vitest/utils': 1.2.2 + acorn-walk: 8.3.2 + cac: 6.7.14 + chai: 4.3.10 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.5 + pathe: 1.1.1 + picocolors: 1.0.0 + std-env: 3.6.0 + strip-literal: 1.3.0 + tinybench: 2.5.1 + tinypool: 0.8.2 + vite: 5.1.0 + vite-node: 1.2.2 + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vitest@1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1): resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==} engines: {node: ^18.0.0 || >=20.0.0}