Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/helpers/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* entsoe-api-client
*
* @file Helper for ENTSO-e date formatting
*
* @author Hexagon <hexagon@GitHub>
* @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 };
50 changes: 5 additions & 45 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -205,23 +206,8 @@ const ComposeQuery = (securityToken: string, params: QueryParameters, force?: bo
if (!params.endDateTimeUpdate) {
throw new Error("endDateTimeUpdate must be specified when startDateTimeUpdate is provided");
}
const formatEntsoeIsoDate = (d: Date | string) => {
if (typeof d === "string") {
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");
}
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");
} 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);
}
Expand All @@ -231,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);
}
Expand Down
71 changes: 71 additions & 0 deletions tests/date.test.ts
Original file line number Diff line number Diff line change
@@ -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",
);
});
Loading