mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 19:20:22 +00:00
refactor: extract cron schedule and test runner helpers
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isOffsetlessIsoDateTime,
|
||||
parseOffsetlessIsoDateTimeInTimeZone,
|
||||
} from "./parse-offsetless-zoned-datetime.js";
|
||||
|
||||
describe("parseOffsetlessIsoDateTimeInTimeZone", () => {
|
||||
it("detects offset-less ISO datetimes", () => {
|
||||
expect(isOffsetlessIsoDateTime("2026-03-23T23:00:00")).toBe(true);
|
||||
expect(isOffsetlessIsoDateTime("2026-03-23T23:00:00+02:00")).toBe(false);
|
||||
expect(isOffsetlessIsoDateTime("+20m")).toBe(false);
|
||||
});
|
||||
|
||||
it("converts offset-less datetimes in the requested timezone", () => {
|
||||
expect(parseOffsetlessIsoDateTimeInTimeZone("2026-03-23T23:00:00", "Europe/Oslo")).toBe(
|
||||
"2026-03-23T22:00:00.000Z",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps DST boundary conversions on the intended wall-clock time", () => {
|
||||
expect(parseOffsetlessIsoDateTimeInTimeZone("2026-03-29T01:30:00", "Europe/Oslo")).toBe(
|
||||
"2026-03-29T00:30:00.000Z",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null for invalid input", () => {
|
||||
expect(parseOffsetlessIsoDateTimeInTimeZone("2026-03-23T23:00:00+02:00", "Europe/Oslo")).toBe(
|
||||
null,
|
||||
);
|
||||
expect(parseOffsetlessIsoDateTimeInTimeZone("2026-03-23T23:00:00", "Invalid/Timezone")).toBe(
|
||||
null,
|
||||
);
|
||||
});
|
||||
});
|
||||
58
src/infra/format-time/parse-offsetless-zoned-datetime.ts
Normal file
58
src/infra/format-time/parse-offsetless-zoned-datetime.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
const OFFSETLESS_ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?$/;
|
||||
|
||||
export function isOffsetlessIsoDateTime(raw: string): boolean {
|
||||
return OFFSETLESS_ISO_DATETIME_RE.test(raw);
|
||||
}
|
||||
|
||||
export function parseOffsetlessIsoDateTimeInTimeZone(raw: string, timeZone: string): string | null {
|
||||
if (!isOffsetlessIsoDateTime(raw)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date());
|
||||
|
||||
const naiveMs = new Date(`${raw}Z`).getTime();
|
||||
if (Number.isNaN(naiveMs)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Re-check the offset at the first candidate instant so DST boundaries
|
||||
// land on the intended wall-clock time instead of drifting by one hour.
|
||||
const firstOffsetMs = getTimeZoneOffsetMs(naiveMs, timeZone);
|
||||
const candidateMs = naiveMs - firstOffsetMs;
|
||||
const finalOffsetMs = getTimeZoneOffsetMs(candidateMs, timeZone);
|
||||
return new Date(naiveMs - finalOffsetMs).toISOString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeZoneOffsetMs(utcMs: number, timeZone: string): number {
|
||||
const utcDate = new Date(utcMs);
|
||||
const parts = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
}).formatToParts(utcDate);
|
||||
|
||||
const getNumericPart = (type: string) => {
|
||||
const part = parts.find((candidate) => candidate.type === type);
|
||||
return Number.parseInt(part?.value ?? "0", 10);
|
||||
};
|
||||
|
||||
const localAsUtc = Date.UTC(
|
||||
getNumericPart("year"),
|
||||
getNumericPart("month") - 1,
|
||||
getNumericPart("day"),
|
||||
getNumericPart("hour"),
|
||||
getNumericPart("minute"),
|
||||
getNumericPart("second"),
|
||||
);
|
||||
|
||||
return localAsUtc - utcMs;
|
||||
}
|
||||
Reference in New Issue
Block a user