Skip to content

Commit a8772ec

Browse files
feat!: migrate from dayjs to Temporal API
Replace dayjs with temporal-kit v0.2.1 for modern date/time handling using the TC39 Temporal proposal. Add TemporalHelper.mjs with formatting utilities. Breaking changes: - Config: `displayLastUpdateFormat` (string) → `displayLastUpdateOptions` (object) - Config: New `language` option (defaults to MagicMirror's config.language) Improvements: - Modern ES syntax: nullish coalescing (??), simplified conditionals - Better parameter naming (departure → when for clarity) - Convert Temporal to Date for hafas-client compatibility - All 55 tests updated and passing Closes #253
1 parent be58162 commit a8772ec

9 files changed

Lines changed: 214 additions & 97 deletions

MMM-PublicTransportHafas.js

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* global dayjs Module Log config */
1+
/* global Module Log config */
22

33
/*
44
* UserPresence Management (PIR sensor)
@@ -17,14 +17,20 @@ Module.register("MMM-PublicTransportHafas", {
1717
hidden: false,
1818
updatesEvery: 120, // How often should the table be updated in s?
1919
timeFormat: config.timeFormat, // Since we don't use moment.js, we need to handle the time format ourselves. This is the default time format of the mirror.
20+
language: config.language, // Use MagicMirror's language setting for date/time formatting
2021

2122
// Header
2223
headerPrefix: "",
2324
headerAppendix: "",
2425

2526
// Display last update time
2627
displayLastUpdate: true, // Add line after the tasks with the last server update time
27-
displayLastUpdateFormat: "dd - HH:mm:ss", // Format to display the last update. See dayjs.js documentation for all display possibilities
28+
displayLastUpdateOptions: { // Intl.DateTimeFormat options for last update display
29+
weekday: "short", // e.g., "Mon" or "Mo"
30+
hour: "2-digit",
31+
minute: "2-digit",
32+
second: "2-digit"
33+
},
2834

2935
// Error handling
3036
discardSocketErrorThreshold: 3, // How many consecutive socket errors should be tolerated before showing an error message? (0 = show errors immediately)
@@ -77,13 +83,11 @@ Module.register("MMM-PublicTransportHafas", {
7783

7884
await this.sanitizeConfig();
7985

80-
// Load ESM DOM builder module and initialize with dayjs
81-
// dayjs plugins are loaded globally via getScripts()
82-
dayjs.extend(window.dayjs_plugin_relativeTime);
83-
dayjs.extend(window.dayjs_plugin_localizedFormat);
84-
86+
// Load DOM builder ESM module
8587
const {default: PtDomBuilder} = await import("./core/PtDomBuilder.mjs");
86-
this.domBuilder = new PtDomBuilder(this.config, dayjs);
88+
const {formatTime, formatDateTime, fromUnixSeconds} = await import("./core/TemporalHelper.mjs");
89+
this.TemporalHelper = {formatTime, formatDateTime, fromUnixSeconds};
90+
this.domBuilder = new PtDomBuilder(this.config);
8791

8892
if (!this.config.stationID) {
8993
this.error.message = this.translate("NO_STATION_ID_SET");
@@ -128,8 +132,7 @@ Module.register("MMM-PublicTransportHafas", {
128132
}
129133
}, 30_000);
130134

131-
// Set locale
132-
dayjs.locale(config.language);
135+
// Temporal uses Intl for locale-specific formatting (no separate config needed)
133136
},
134137

135138
suspend () {
@@ -244,9 +247,14 @@ Module.register("MMM-PublicTransportHafas", {
244247
updateText = `Update (socket issues: ${this.errorCount})`;
245248
}
246249

247-
updateInfo.textContent = `${updateText}: ${dayjs
248-
.unix(this.lastUpdate)
249-
.format(this.config.displayLastUpdateFormat)}`;
250+
const updateTime = this.TemporalHelper.fromUnixSeconds(this.lastUpdate);
251+
const formattedTime = this.TemporalHelper.formatDateTime(
252+
updateTime,
253+
this.config.language,
254+
this.config.displayLastUpdateOptions
255+
);
256+
257+
updateInfo.textContent = `${updateText}: ${formattedTime}`;
250258
wrapper.appendChild(updateInfo);
251259
}
252260

@@ -265,12 +273,7 @@ Module.register("MMM-PublicTransportHafas", {
265273
},
266274

267275
getScripts () {
268-
return [
269-
this.file("node_modules/dayjs/dayjs.min.js"),
270-
this.file("node_modules/dayjs/plugin/localizedFormat.js"),
271-
this.file("node_modules/dayjs/plugin/relativeTime.js"),
272-
this.file(`node_modules/dayjs/locale/${config.language}.js`)
273-
];
276+
return [this.file("node_modules/temporal-kit/dist/temporal-kit.browser.polyfilled.global.js")];
274277
},
275278

276279
getTranslations () {
@@ -294,11 +297,7 @@ Module.register("MMM-PublicTransportHafas", {
294297
this.lastUpdate = Date.now() / 1_000; // Save the timestamp of the last update to be able to display it
295298
}
296299

297-
Log.log(`[MMM-PublicTransportHafas] Update OK, station: ${
298-
this.config.stationName
299-
} at: ${dayjs
300-
.unix(this.lastUpdate)
301-
.format(this.config.displayLastUpdateFormat)}`);
300+
Log.log(`[MMM-PublicTransportHafas] Update OK, station: ${this.config.stationName} at: ${new Date().toLocaleTimeString()}`);
302301

303302
// Reset error object and error count on successful fetch
304303
this.error = {};
@@ -317,14 +316,8 @@ Module.register("MMM-PublicTransportHafas", {
317316
this.departures = [];
318317
}
319318

320-
// Only show the error message if threshold is exceeded
321-
if (this.errorCount > this.config.discardSocketErrorThreshold) {
322-
this.updateDom(this.config.animationSpeed);
323-
} else {
324-
// Update DOM to show socket issue count in "Last update" line
325-
this.updateDom(this.config.animationSpeed);
326-
}
327-
319+
// Always update DOM (either to show error or socket issue count)
320+
this.updateDom(this.config.animationSpeed);
328321
break;
329322
}
330323
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ The module is quite configurable. The only option you really have to set is `sta
150150
| `customLineStyles` | <p>A string value describing the name of a custom CSS file.</p><p>**Type:** `string`<br>**Example:** `"dresden"`<br>**Default value:** `"leipzig"`</p><p>**Note:** If the setting `showColoredLineSymbols` is `true` the module will try to use colored labels for the line name. Per default it uses the colors used in Leipzig. This style works best if `showOnlyLineNumbers` is set to `true`. If it doesn’t suit your taste you can provide your own settings. See [Providing a custom CSS file](#providing-a-custom-css-file).</p>|
151151
| `showOnlyLineNumbers` | <p>A boolean value denoting whether the line name should be displayed as a number only or the full name should be used.</p><p>**Type:** `boolean`<br>**Default value:** `false`<br>**Possible values:** `true` and `false`</p><p>**Note:** If set to `true` the module will try to separate line numbers from the line name and display only these. If the line name is “STR 11” only “11” will be displayed. This only works if there are blanks present in the line name. This setting is only tested with departures in Leipzig. If you encounter problems [let me know](https://github.com/KristjanESPERANTO/MMM-PublicTransportHafas/issues).</p>|
152152
| `displayLastUpdate` | <p>If true this will display the last update time at the end of the task list. See screenshot above</p><p>**Type:** `boolean`<br>**Default value:** `true`<br>**Possible values:** `true` and `false`</p>|
153-
| `displayLastUpdateFormat` | <p>Format to use for the time display if displayLastUpdate:true</p><p>**Type:** `string`<br>**Example:** `'HH:mm:ss'`<br>**Default value:** `'dd - HH:mm:ss'`</p>See [dayjs.js formats](https://day.js.org/docs/en/parse/string-format) for the other format possibilities.</p>|
153+
| `displayLastUpdateOptions` | <p>Options object for formatting the last update time using [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options).</p><p>**Type:** `object`<br>**Default value:** `{ weekday: "short", hour: "2-digit", minute: "2-digit", second: "2-digit" }`<br>**Examples:**<br>- Time only: `{ hour: "2-digit", minute: "2-digit" }`<br>- With date: `{ month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" }`<br>- Short format: `{ dateStyle: "short", timeStyle: "short" }`</p>See [MDN Intl.DateTimeFormat options](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options) for all available formatting options.</p>|
154154
| `animationSpeed` | <p>Speed of the update animation.</p><p>**Type:** `integer`<br>**Possible values:** `0` - `5000`<br>**Default value:** `2000`<br>**Unit:** `milliseconds`</p>|
155155
<!-- prettier-ignore-end -->
156156

core/DepartureFetcher.mjs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1+
import "temporal-kit/polyfilled";
12
import Log from "../../../js/logger.js";
2-
import dayjs from "dayjs";
3-
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
4-
import packageJson from "../package.json" with {type: "json"};
53

6-
dayjs.extend(isSameOrAfter);
4+
import packageJson from "../package.json" with {type: "json"};
75

86
/**
97
* Helper function to determine the difference between two arrays.
@@ -87,9 +85,11 @@ export default class DepartureFetcher {
8785

8886
// Build promises for parallel API calls
8987
const promises = directions.map((direction) => {
88+
const departureTime = this.getDepartureTime();
9089
const options = {
9190
duration: this.getTimeInFuture(),
92-
when: this.getDepartureTime()
91+
// Convert Temporal to Date for hafas-client compatibility
92+
when: new Date(departureTime.epochMilliseconds)
9393
};
9494

9595
if (direction) {
@@ -163,14 +163,14 @@ export default class DepartureFetcher {
163163
let departureTime = this.getReachableTime();
164164

165165
if (this.config.maxUnreachableDepartures > 0) {
166-
departureTime = departureTime.subtract(this.leadTime, "minutes");
166+
departureTime = departureTime.subtract({minutes: this.leadTime});
167167
}
168168

169169
return departureTime;
170170
}
171171

172172
getReachableTime () {
173-
return dayjs().add(this.config.timeToStation, "minutes");
173+
return Temporal.Now.zonedDateTimeISO().add({minutes: this.config.timeToStation});
174174
}
175175

176176
getTimeInFuture () {
@@ -268,6 +268,8 @@ export default class DepartureFetcher {
268268
}
269269

270270
isReachable (departure) {
271-
return dayjs(departure.when).isSameOrAfter(this.getReachableTime());
271+
const departureInstant = Temporal.Instant.from(departure.when);
272+
const reachableInstant = this.getReachableTime().toInstant();
273+
return Temporal.Instant.compare(departureInstant, reachableInstant) >= 0;
272274
}
273275
}

core/PtDomBuilder.mjs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@ import PtTableBodyBuilder from "./PtTableBodyBuilder.mjs";
22

33
/**
44
* Builds the DOM structure for the departure display.
5-
* Uses dependency injection for dayjs to enable testing and ESM compatibility.
65
*/
76
export default class PtDomBuilder {
87
/**
98
* @param {object} config - Module configuration
10-
* @param {object} dayjsInstance - dayjs instance with plugins loaded
119
*/
12-
constructor (config, dayjsInstance) {
10+
constructor (config) {
1311
this.config = config;
14-
this.dayjs = dayjsInstance;
1512

1613
this.headingSymbols = {
1714
direction: "fa fa-exchange",
@@ -87,7 +84,7 @@ export default class PtDomBuilder {
8784
table.appendChild(tableHeader);
8885
}
8986

90-
const tableBodyBuilder = new PtTableBodyBuilder(this.config, this.dayjs);
87+
const tableBodyBuilder = new PtTableBodyBuilder(this.config);
9188
const tableBody = tableBodyBuilder.getDeparturesTableBody(
9289
departures,
9390
noDepartureMessage

core/PtTableBodyBuilder.mjs

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1+
import * as TemporalHelper from "./TemporalHelper.mjs";
2+
13
/**
24
* Builds the table body for departure display.
3-
* Uses dependency injection for dayjs to enable testing and ESM compatibility.
5+
* Uses Temporal API for modern date/time handling.
46
*/
57
export default class PtTableBodyBuilder {
68
/**
79
* @param {object} config - Module configuration
8-
* @param {object} dayjsInstance - dayjs instance with plugins loaded
910
*/
10-
constructor (config, dayjsInstance) {
11+
constructor (config) {
1112
this.config = config;
12-
this.dayjs = dayjsInstance;
1313
this.remarksCollector = []; // the array with warnings that are already displayed
1414
}
1515

@@ -195,13 +195,7 @@ export default class PtTableBodyBuilder {
195195
}
196196

197197
case "platform": {
198-
let {platform} = departure;
199-
if (platform === null) {
200-
platform = departure.plannedPlatform;
201-
}
202-
if (platform === null) {
203-
platform = "";
204-
}
198+
const platform = departure.platform ?? departure.plannedPlatform ?? "";
205199
cell = this.getPlatformCell(platform);
206200
break;
207201
}
@@ -210,11 +204,11 @@ export default class PtTableBodyBuilder {
210204
return cell;
211205
}
212206

213-
getTimeCell (departure, delay) {
214-
const time = this.getDisplayDepartureTime(departure, delay);
207+
getTimeCell (when, delay) {
208+
const time = this.getDisplayDepartureTime(when, delay);
215209
const cell = document.createElement("td");
216210

217-
if (this.dayjs(departure).isValid()) {
211+
if (TemporalHelper.isValidTemporal(when)) {
218212
cell.className = "mmm-pth-time-cell";
219213
cell.appendChild(document.createTextNode(time));
220214

@@ -264,23 +258,23 @@ export default class PtTableBodyBuilder {
264258
}
265259

266260
getDisplayDepartureTime (when, delay) {
267-
let time = this.dayjs(when);
268-
let format = "HH:mm";
269-
270-
if (this.config.timeFormat === 12) {
271-
format = "h:mm A";
272-
}
261+
const instant = Temporal.Instant.from(when);
262+
const now = Temporal.Now.instant();
273263

274264
if (this.config.showAbsoluteTime) {
275-
time = this.dayjs(when).subtract(delay, "seconds");
276-
return time.format(format);
265+
// Subtract delay to show scheduled time
266+
const adjustedInstant = instant.subtract(Temporal.Duration.from({seconds: delay ?? 0}));
267+
return TemporalHelper.formatTime(adjustedInstant, this.config.language ?? "en", this.config.timeFormat);
277268
}
278269

279-
if (this.dayjs(when).diff(this.dayjs()) > this.config.showRelativeTimeOnlyUnder) {
280-
return time.format(format);
270+
// Calculate difference in milliseconds
271+
const diffMs = instant.epochMilliseconds - now.epochMilliseconds;
272+
273+
if (diffMs > this.config.showRelativeTimeOnlyUnder) {
274+
return TemporalHelper.formatTime(instant, this.config.language ?? "en", this.config.timeFormat);
281275
}
282276

283-
return time.fromNow();
277+
return TemporalHelper.formatRelativeTime(instant, this.config.language ?? "en");
284278
}
285279

286280
getLineId (lineName) {

0 commit comments

Comments
 (0)