From d7a013272f62f3d17ed531ea330e25d44b8fcc71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:14:44 +0000 Subject: [PATCH 1/3] Initial plan From 8798069494d0fac21d1fd2a205894f13db3f6632 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:19:50 +0000 Subject: [PATCH 2/3] Fix startDateTimeUpdate to use correct date format Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com> --- src/query.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/query.ts b/src/query.ts index 76df719..a9a95fc 100644 --- a/src/query.ts +++ b/src/query.ts @@ -205,17 +205,28 @@ const ComposeQuery = (securityToken: string, params: QueryParameters, force?: bo if (!params.endDateTimeUpdate) { throw new Error("endDateTimeUpdate must be specified when startDateTimeUpdate is provided"); } + // Accept Date or string (YYYYMMDDHHmm) const formatEntsoeIsoDate = (d: Date | string) => { + // Always return YYYY-MM-DDTHH:mmZ (no seconds) + const toEntsoe = (date: Date) => { + // Get YYYY-MM-DDTHH:mmZ + return date.getUTCFullYear() + + "-" + String(date.getUTCMonth() + 1).padStart(2, "0") + + "-" + String(date.getUTCDate()).padStart(2, "0") + + "T" + String(date.getUTCHours()).padStart(2, "0") + + ":" + String(date.getUTCMinutes()).padStart(2, "0") + "Z"; + }; if (typeof d === "string") { + // Accept ISO 8601 string, or try to parse to Date const iso = d.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z$/) ? d : null; if (iso) return iso; const parsed = new Date(d); if (!isNaN(parsed.getTime())) { - return parsed.toISOString().replace(":00.000Z", ":00Z"); + return toEntsoe(parsed); } throw new Error("startDateTimeUpdate/endDateTimeUpdate string must be ISO 8601 UTC (YYYY-MM-DDTHH:mmZ)"); } else if (d instanceof Date && !isNaN(d.getTime())) { - return d.toISOString().replace(":00.000Z", ":00Z"); + return toEntsoe(d); } else { throw new Error("startDateTimeUpdate/endDateTimeUpdate not valid, should be Date object or ISO 8601 string"); } From 561dc534e9faf89de8c74fa1b09a75808db9f623 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:27:49 +0000 Subject: [PATCH 3/3] Add date formatting tests and refactor into reusable helper Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com> --- src/helpers/date.ts | 58 ++++++++++++++++++++++++++++++++++++ src/query.ts | 61 ++++---------------------------------- tests/date.test.ts | 71 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 56 deletions(-) create mode 100644 src/helpers/date.ts create mode 100644 tests/date.test.ts diff --git a/src/helpers/date.ts b/src/helpers/date.ts new file mode 100644 index 0000000..82e03d1 --- /dev/null +++ b/src/helpers/date.ts @@ -0,0 +1,58 @@ +/** + * entsoe-api-client + * + * @file Helper for ENTSO-e date formatting + * + * @author Hexagon + * @license MIT + */ + +/** + * Converts a Date object to ENTSO-e API date format (YYYY-MM-DDTHH:mmZ) + * + * ENTSO-e API requires dates without seconds, in the format YYYY-MM-DDTHH:mmZ + * + * @param date - Date object to format + * @returns - Formatted date string in YYYY-MM-DDTHH:mmZ format + */ +const toEntsoeFormat = (date: Date): string => { + return date.getUTCFullYear() + + "-" + String(date.getUTCMonth() + 1).padStart(2, "0") + + "-" + String(date.getUTCDate()).padStart(2, "0") + + "T" + String(date.getUTCHours()).padStart(2, "0") + + ":" + String(date.getUTCMinutes()).padStart(2, "0") + "Z"; +}; + +/** + * Formats a Date object or ISO 8601 string to ENTSO-e API date format (YYYY-MM-DDTHH:mmZ) + * + * Accepts: + * - Date objects + * - ISO 8601 strings in YYYY-MM-DDTHH:mmZ format (returned as-is) + * - Other parseable date strings (converted to ENTSO-e format) + * + * @param d - Date object or string to format + * @param paramName - Parameter name for error messages (e.g., "startDateTime", "startDateTimeUpdate") + * @returns - Formatted date string in YYYY-MM-DDTHH:mmZ format + * @throws - Error if the input is not a valid date + */ +const formatEntsoeDate = (d: Date | string, paramName: string): string => { + if (typeof d === "string") { + // If already in correct format, return as-is + const iso = d.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z$/) ? d : null; + if (iso) return iso; + + // Try to parse the string as a date + const parsed = new Date(d); + if (!isNaN(parsed.getTime())) { + return toEntsoeFormat(parsed); + } + throw new Error(`${paramName} string must be ISO 8601 UTC (YYYY-MM-DDTHH:mmZ)`); + } else if (d instanceof Date && !isNaN(d.getTime())) { + return toEntsoeFormat(d); + } else { + throw new Error(`${paramName} not valid, should be Date object or ISO 8601 string`); + } +}; + +export { formatEntsoeDate, toEntsoeFormat }; diff --git a/src/query.ts b/src/query.ts index a9a95fc..23b3355 100644 --- a/src/query.ts +++ b/src/query.ts @@ -38,6 +38,7 @@ function hasGetData(entry: unknown): entry is ZipEntryWithData { } import { BusinessTypes } from "./definitions/businesstypes.ts"; import { QueryParameters } from "./parameters.ts"; +import { formatEntsoeDate } from "./helpers/date.ts"; /** * Helper to validate input parameters @@ -205,34 +206,8 @@ const ComposeQuery = (securityToken: string, params: QueryParameters, force?: bo if (!params.endDateTimeUpdate) { throw new Error("endDateTimeUpdate must be specified when startDateTimeUpdate is provided"); } - // Accept Date or string (YYYYMMDDHHmm) - const formatEntsoeIsoDate = (d: Date | string) => { - // Always return YYYY-MM-DDTHH:mmZ (no seconds) - const toEntsoe = (date: Date) => { - // Get YYYY-MM-DDTHH:mmZ - return date.getUTCFullYear() + - "-" + String(date.getUTCMonth() + 1).padStart(2, "0") + - "-" + String(date.getUTCDate()).padStart(2, "0") + - "T" + String(date.getUTCHours()).padStart(2, "0") + - ":" + String(date.getUTCMinutes()).padStart(2, "0") + "Z"; - }; - if (typeof d === "string") { - // Accept ISO 8601 string, or try to parse to Date - const iso = d.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z$/) ? d : null; - if (iso) return iso; - const parsed = new Date(d); - if (!isNaN(parsed.getTime())) { - return toEntsoe(parsed); - } - throw new Error("startDateTimeUpdate/endDateTimeUpdate string must be ISO 8601 UTC (YYYY-MM-DDTHH:mmZ)"); - } else if (d instanceof Date && !isNaN(d.getTime())) { - return toEntsoe(d); - } else { - throw new Error("startDateTimeUpdate/endDateTimeUpdate not valid, should be Date object or ISO 8601 string"); - } - }; - const start = formatEntsoeIsoDate(params.startDateTimeUpdate); - const end = formatEntsoeIsoDate(params.endDateTimeUpdate as Date | string); + const start = formatEntsoeDate(params.startDateTimeUpdate, "startDateTimeUpdate"); + const end = formatEntsoeDate(params.endDateTimeUpdate as Date | string, "endDateTimeUpdate"); const timeInterval = `${start}/${end}`; query.append("TimeIntervalUpdate", timeInterval); } @@ -242,34 +217,8 @@ const ComposeQuery = (securityToken: string, params: QueryParameters, force?: bo if (!params.endDateTime) { throw new Error("endDateTime must be specified when startDateTime is provided"); } - // Accept Date or string (YYYYMMDDHHmm) - const formatEntsoeIsoDate = (d: Date | string) => { - // Always return YYYY-MM-DDTHH:mmZ (no seconds) - const toEntsoe = (date: Date) => { - // Get YYYY-MM-DDTHH:mmZ - return date.getUTCFullYear() + - "-" + String(date.getUTCMonth() + 1).padStart(2, "0") + - "-" + String(date.getUTCDate()).padStart(2, "0") + - "T" + String(date.getUTCHours()).padStart(2, "0") + - ":" + String(date.getUTCMinutes()).padStart(2, "0") + "Z"; - }; - if (typeof d === "string") { - // Accept ISO 8601 string, or try to parse to Date - const iso = d.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z$/) ? d : null; - if (iso) return iso; - const parsed = new Date(d); - if (!isNaN(parsed.getTime())) { - return toEntsoe(parsed); - } - throw new Error("startDateTime/endDateTime string must be ISO 8601 UTC (YYYY-MM-DDTHH:mmZ)"); - } else if (d instanceof Date && !isNaN(d.getTime())) { - return toEntsoe(d); - } else { - throw new Error("startDateTime/endDateTime not valid, should be Date object or ISO 8601 string"); - } - }; - const start = formatEntsoeIsoDate(params.startDateTime); - const end = formatEntsoeIsoDate(params.endDateTime as Date | string); + const start = formatEntsoeDate(params.startDateTime, "startDateTime"); + const end = formatEntsoeDate(params.endDateTime as Date | string, "endDateTime"); const timeInterval = `${start}/${end}`; query.append("TimeInterval", timeInterval); } diff --git a/tests/date.test.ts b/tests/date.test.ts new file mode 100644 index 0000000..e6b894a --- /dev/null +++ b/tests/date.test.ts @@ -0,0 +1,71 @@ +import { assertEquals, assertThrows } from "https://deno.land/std@0.128.0/testing/asserts.ts"; +import { formatEntsoeDate, toEntsoeFormat } from "../src/helpers/date.ts"; + +Deno.test("toEntsoeFormat - formats Date objects correctly", function () { + // Basic date formatting + assertEquals(toEntsoeFormat(new Date("2024-01-15T10:30:00.000Z")), "2024-01-15T10:30Z"); + + // Dates with non-zero seconds should have seconds stripped + assertEquals(toEntsoeFormat(new Date("2024-01-15T10:30:45.123Z")), "2024-01-15T10:30Z"); + + // Edge case: midnight + assertEquals(toEntsoeFormat(new Date("2024-01-01T00:00:00.000Z")), "2024-01-01T00:00Z"); + + // Edge case: end of day + assertEquals(toEntsoeFormat(new Date("2024-12-31T23:59:59.999Z")), "2024-12-31T23:59Z"); + + // Single digit month and day should be zero-padded + assertEquals(toEntsoeFormat(new Date("2024-01-05T05:05:00.000Z")), "2024-01-05T05:05Z"); +}); + +Deno.test("formatEntsoeDate - handles Date objects", function () { + // Date objects should be formatted correctly + assertEquals(formatEntsoeDate(new Date("2024-01-15T10:30:00.000Z"), "testParam"), "2024-01-15T10:30Z"); + + // Dates with non-zero seconds should have seconds stripped + assertEquals(formatEntsoeDate(new Date("2024-01-15T10:30:45.123Z"), "testParam"), "2024-01-15T10:30Z"); +}); + +Deno.test("formatEntsoeDate - handles ISO 8601 strings in correct format", function () { + // Strings already in correct format should be returned as-is + assertEquals(formatEntsoeDate("2024-01-15T10:30Z", "testParam"), "2024-01-15T10:30Z"); + assertEquals(formatEntsoeDate("2024-12-31T23:59Z", "testParam"), "2024-12-31T23:59Z"); +}); + +Deno.test("formatEntsoeDate - converts parseable date strings", function () { + // ISO 8601 with seconds should be converted + assertEquals(formatEntsoeDate("2024-01-15T10:30:45.123Z", "testParam"), "2024-01-15T10:30Z"); + + // Standard ISO format with seconds + assertEquals(formatEntsoeDate("2024-01-15T10:30:00Z", "testParam"), "2024-01-15T10:30Z"); +}); + +Deno.test("formatEntsoeDate - throws on invalid strings", function () { + assertThrows( + () => formatEntsoeDate("not a date", "testParam"), + Error, + "testParam string must be ISO 8601 UTC", + ); +}); + +Deno.test("formatEntsoeDate - throws on invalid Date objects", function () { + assertThrows( + () => formatEntsoeDate(new Date("invalid"), "testParam"), + Error, + "testParam not valid", + ); +}); + +Deno.test("formatEntsoeDate - uses parameter name in error messages", function () { + assertThrows( + () => formatEntsoeDate("not a date", "startDateTime"), + Error, + "startDateTime", + ); + + assertThrows( + () => formatEntsoeDate("not a date", "startDateTimeUpdate"), + Error, + "startDateTimeUpdate", + ); +});