Aufwärmen · Aufgabe 1 von 7
Zum Aufwärmen aus Lektion I5.4. Was gibt das aus?
console.log(new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 1 }).format(1 / 6));// I5.5 · ca. 48 Min. · Aufbau
Sie bauen einen kleinen Logdatei-Analysator: Ein regulärer Ausdruck liest jede Zeile, eine Map zählt die Einträge nach Level und nach Stunde, und Intl schreibt die Zusammenfassung.
Lektion 5 von 5 in I5 Mit Daten arbeiten
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
console.log(new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 1 }).format(1 / 6));Vorhersagen · Aufgabe 2 von 7
const LINE = /^(?<time>\S+) (?<level>INFO|WARN|ERROR) (?<message>.+)$/;
const lines = [
"2026-10-05T08:15:02Z INFO server started",
"2026-10-05T08:16:00Z DEBUG cache warm",
"broken",
];
console.log(lines.filter((line) => LINE.test(line)).length);Üben · Aufgabe 3 von 7
const { level } = LINE.exec(line).____;Üben · Aufgabe 4 von 7
Üben · Aufgabe 5 von 7
const times = ["2026-10-05T08:15:02Z", "2026-10-05T08:59:59Z", "2026-10-05T09:00:00Z"];
const byHour = new Map();
for (const t of times) {
const hour = new Date(t).getUTCHours();
byHour.set(hour, (byHour.get(hour) ?? 0) + 1);
}
console.log([...byHour].join(" | "));Denksport · Aufgabe 6 von 7
const LINE = /^\S+ (INFO|WARN|ERROR) .+$/g;
const lines = ["t1 INFO a", "t2 INFO b", "t3 INFO c"];
console.log(lines.filter((line) => LINE.test(line)).length);Anwenden · Aufgabe 7 von 7
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Das ganze Programm in einer Datei. LOG enthält sieben Zeilen, eine davon kaputt. parseLine() macht aus jeder Zeile ein Objekt oder null, countBy() zählt nach Level und nach UTC-Stunde, und formatSummary() schreibt die Anteile mit Intl auf Deutsch. Das Programm gibt den Datumsbereich, die Zusammenfassung, einen Balken pro Stunde und die verschiedenen Fehlermeldungen aus, die ein Set sammelt. Die Übungen bauen die drei Funktionen nacheinander.
main.js
const LOG = `2026-10-05T08:15:02Z INFO server started on port 8080
2026-10-05T08:15:09Z WARN config file missing, using defaults
2026-10-05T09:02:44Z INFO user ada logged in
this line is broken
2026-10-05T09:47:13Z ERROR database timeout after 5000 ms
2026-10-05T10:05:00Z INFO user bo logged in
2026-10-05T10:31:27Z ERROR database timeout after 5000 ms`;
const LINE = /^(?<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) (?<level>INFO|WARN|ERROR) (?<message>.+)$/;
// One log line -> { time: Date, level, message }, or null if it does not match.
export function parseLine(line) {
const match = LINE.exec(line);
if (match === null) return null;
const { time, level, message } = match.groups;
return { time: new Date(time), level, message };
}
// Counts entries by the key that keyOf returns, in first-seen order.
export function countBy(entries, keyOf) {
const counts = new Map();
for (const entry of entries) {
const key = keyOf(entry);
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return counts;
}
export const countByLevel = (entries) => countBy(entries, (entry) => entry.level);
export const countByHour = (entries) => countBy(entries, (entry) => entry.time.getUTCHours());
// counts: Map of level -> count. Returns the summary lines for a locale.
export function formatSummary(counts, locale) {
const total = [...counts.values()].reduce((sum, n) => sum + n, 0);
const percent = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 });
const list = new Intl.ListFormat(locale, { type: "conjunction" });
const lines = [];
for (const [level, count] of counts) {
lines.push(`${level}: ${count} (${percent.format(count / total)})`);
}
lines.push(`levels: ${list.format([...counts.keys()])}`);
return lines;
}
const lines = LOG.split("\n");
const entries = lines.map(parseLine).filter((entry) => entry !== null);
console.log(`read ${entries.length} of ${lines.length} lines`);
const day = new Intl.DateTimeFormat("de-DE", { dateStyle: "long", timeZone: "UTC" });
const time = new Intl.DateTimeFormat("de-DE", { timeStyle: "short", timeZone: "UTC" });
console.log("Log vom", day.format(entries[0].time), "bis", time.format(entries.at(-1).time), "UTC");
for (const line of formatSummary(countByLevel(entries), "de-DE")) console.log(line);
for (const [hour, count] of countByHour(entries)) {
console.log(`${String(hour).padStart(2, "0")}:00 UTC ${"#".repeat(count)}`);
}
const errors = new Set(entries.filter((entry) => entry.level === "ERROR").map((entry) => entry.message));
console.log("different errors:", errors.size, "-", [...errors].join("; "));
Ausführen mit
node main.jsAusgabe
read 6 of 7 lines
Log vom 5. Oktober 2026 bis 10:31 UTC
INFO: 3 (50 %)
WARN: 1 (16,7 %)
ERROR: 2 (33,3 %)
levels: INFO, WARN und ERROR
08:00 UTC ##
09:00 UTC ##
10:00 UTC ##
different errors: 1 - database timeout after 5000 msTab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Übung 1 von 3
parseLine(line) bekommt eine Zeile wie "2026-10-05T09:47:13Z ERROR database timeout after 5000 ms" und gibt {time, level, message} zurück: time als Date, level als INFO, WARN oder ERROR und den ganzen Rest als message. Eine Zeile, die nicht passt, gibt null zurück. Der Startcode trennt an Leerzeichen, was die Meldung zerschneidet und nie null zurückgibt. Nutzen Sie ein verankertes Muster mit benannten Gruppen.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Drei benannte Gruppen, durch einzelne Leerzeichen getrennt: (?<time>…) (?<level>…) (?<message>…). Verankern Sie mit ^ und $.
Das Level ist eine Alternative: (?<level>INFO|WARN|ERROR). Die Meldung ist alles andere: (?<message>.+).
const match = LINE.exec(line); if (match === null) return null; dann match.groups lesen und time in new Date(…) einpacken.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
const LINE = /^(?<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) (?<level>INFO|WARN|ERROR) (?<message>.+)$/;
// One log line -> { time: Date, level, message }, or null if it does not match.
export function parseLine(line) {
const match = LINE.exec(line);
if (match === null) return null;
const { time, level, message } = match.groups;
return { time: new Date(time), level, message };
}
Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// One log line -> { time: Date, level, message }, or null if it does not match.
export function parseLine(line) {
const [time, level, message] = line.split(" ");
return { time: new Date(time), level, message };
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {parseLine} from './main.js';
test('Der ganze Rest der Zeile ist die Meldung', () => {
const entry = parseLine('2026-10-05T09:47:13Z ERROR database timeout after 5000 ms');
assert.equal(entry?.level, 'ERROR', `das Level ist ${entry?.level}`);
assert.equal(entry?.message, 'database timeout after 5000 ms', `die Meldung ist ${entry?.message}`);
});
test('Die Zeit ist ein Date zu diesem UTC-Moment', () => {
const entry = parseLine('2026-10-05T08:15:02Z INFO server started');
assert.ok(entry?.time instanceof Date, 'time sollte ein Date sein');
assert.equal(entry.time.toISOString(), '2026-10-05T08:15:02.000Z', `die Zeit ist ${entry.time.toISOString()}`);
});
test('Eine kaputte Zeile ergibt null', () => {
assert.equal(parseLine('this line is broken'), null, 'parseLine sollte für eine kaputte Zeile null zurückgeben');
});
test('Ein unbekanntes Level ergibt null', () => {
assert.equal(parseLine('2026-10-05T08:16:00Z DEBUG cache warm'), null, 'DEBUG ist keines der drei Level');
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenÜbung 2 von 3
countBy(entries, keyOf) soll eine Map von jedem Schlüssel, den keyOf zurückgibt, auf die Anzahl der Einträge mit diesem Schlüssel zurückgeben, in der Reihenfolge des ersten Auftretens. countByLevel und countByHour nutzen es, die zweite mit der UTC-Stunde von entry.time als Zahl. Das countBy im Startcode addiert 1 zu undefined, und countByHour nimmt das ganze Date als Schlüssel. Beheben Sie beides.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Führen Sie die Tests aus: Die Zähler sind NaN. Was ist counts.get(key), wenn ein Schlüssel zum ersten Mal vorkommt?
Beginnen Sie bei 0: counts.set(key, (counts.get(key) ?? 0) + 1).
Der Schlüssel für eine Stunde ist entry.time.getUTCHours(), eine Zahl von 0 bis 23.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// Counts entries by the key that keyOf returns, in first-seen order.
export function countBy(entries, keyOf) {
const counts = new Map();
for (const entry of entries) {
const key = keyOf(entry);
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return counts;
}
export const countByLevel = (entries) => countBy(entries, (entry) => entry.level);
export const countByHour = (entries) => countBy(entries, (entry) => entry.time.getUTCHours());
Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// Counts entries by the key that keyOf returns, in first-seen order.
export function countBy(entries, keyOf) {
const counts = new Map();
for (const entry of entries) {
const key = keyOf(entry);
counts.set(key, counts.get(key) + 1);
}
return counts;
}
export const countByLevel = (entries) => countBy(entries, (entry) => entry.level);
// TODO: count by the hour of entry.time, in UTC.
export const countByHour = (entries) => countBy(entries, (entry) => entry.time);
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {countByLevel, countByHour} from './main.js';
const at = (iso, level) => ({time: new Date(iso), level, message: 'x'});
const entries = [
at('2026-10-05T08:15:02Z', 'INFO'),
at('2026-10-05T08:40:00Z', 'ERROR'),
at('2026-10-05T09:02:44Z', 'INFO'),
at('2026-10-05T23:59:59Z', 'INFO')
];
test('Level werden in der Reihenfolge des ersten Auftretens gezählt', () => {
const counts = countByLevel(entries);
assert.deepEqual([...counts], [['INFO', 3], ['ERROR', 1]], `die Zähler sind ${JSON.stringify([...counts])}`);
});
test('Stunden sind UTC-Stunden als Zahlen', () => {
const counts = countByHour(entries);
assert.deepEqual([...counts], [[8, 2], [9, 1], [23, 1]], `die Zähler sind ${JSON.stringify([...counts])}`);
});
test('Keine Einträge ergeben eine leere Map', () => {
assert.equal(countByLevel([]).size, 0, 'countByLevel([]) sollte leer sein');
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenÜbung 3 von 3
formatSummary(counts, locale) bekommt eine Map von Level auf Anzahl und gibt ein Array von Zeilen zurück: eine Zeile "LEVEL: Anzahl (Anteil)" pro Level, mit dem Anteil als Prozentwert mit höchstens einer Nachkommastelle für das Locale, und dann "levels: " mit den Levelnamen als Liste für das Locale. Der Startcode baut Prozentwert und Liste von Hand, was weder für Deutsch noch für Englisch stimmt. Nutzen Sie Intl.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Führen Sie die Tests aus: Der deutsche Anteil hat einen Punkt. Welches Intl-Werkzeug schreibt Prozentwerte für ein Locale?
new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 }).format(count / total)
new Intl.ListFormat(locale, { type: "conjunction" }).format([...counts.keys()])
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// counts: Map of level -> count. Returns the summary lines for a locale.
export function formatSummary(counts, locale) {
const total = [...counts.values()].reduce((sum, n) => sum + n, 0);
const percent = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 });
const list = new Intl.ListFormat(locale, { type: "conjunction" });
const lines = [];
for (const [level, count] of counts) {
lines.push(`${level}: ${count} (${percent.format(count / total)})`);
}
lines.push(`levels: ${list.format([...counts.keys()])}`);
return lines;
}
Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// counts: Map of level -> count. Returns the summary lines for a locale.
export function formatSummary(counts, locale) {
const total = [...counts.values()].reduce((sum, n) => sum + n, 0);
const lines = [];
for (const [level, count] of counts) {
lines.push(`${level}: ${count} (${Math.round((count / total) * 1000) / 10}%)`);
}
lines.push(`levels: ${[...counts.keys()].join(", ")}`);
return lines;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatSummary} from './main.js';
const counts = new Map([['INFO', 3], ['WARN', 1], ['ERROR', 2]]);
test('Deutsche Anteile haben ein Komma und ein geschütztes Leerzeichen vor %', () => {
const lines = formatSummary(counts, 'de-DE');
assert.equal(lines[1], 'WARN: 1 (16,7\u00a0%)', `die WARN-Zeile ist ${JSON.stringify(lines[1])}`);
});
test('Englische Anteile haben einen Punkt und kein Leerzeichen', () => {
const lines = formatSummary(counts, 'en-US');
assert.equal(lines[2], 'ERROR: 2 (33.3%)', `die ERROR-Zeile ist ${JSON.stringify(lines[2])}`);
});
test('Die Level sind eine Liste in der Sprache des Locales', () => {
const german = formatSummary(counts, 'de-DE').at(-1);
const english = formatSummary(counts, 'en-US').at(-1);
assert.equal(german, 'levels: INFO, WARN und ERROR', `die deutsche Listenzeile ist ${german}`);
assert.equal(english, 'levels: INFO, WARN, and ERROR', `die englische Listenzeile ist ${english}`);
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenconst LINE = /^(?<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) (?<level>INFO|WARN|ERROR) (?<message>.+)$/;
// One log line -> { time: Date, level, message }, or null if it does not match.
function parseLine(line) {
const match = LINE.exec(line);
if (match === null) return null;
const { time, level, message } = match.groups;
return { time: new Date(time), level, message };
}
const lines = ["2026-10-05T08:15:02Z INFO started", "broken"];
const levels = lines.map(parseLine).map((entry) => entry.level);
console.log(levels);
Was Node.js ausgibt
TypeError: Cannot read properties of null (reading 'level')Warum, und die Lösung
parseLine gibt für eine Zeile, die nicht passt, null zurück, und null hat keine Eigenschaften. Filtern Sie die null-Werte heraus, bevor Sie die Einträge benutzen: lines.map(parseLine).filter((entry) => entry !== null). Eine Logdatei hat fast immer eine kaputte oder leere Zeile, etwa die letzte Zeile nach dem letzten Zeilenumbruch.
const log = "INFO started\nERROR timeout";
const LINE = /^(?<level>INFO|WARN|ERROR) (?<message>.+)$/m;
for (const match of log.matchAll(LINE)) console.log(match.groups.level);
Was Node.js ausgibt
TypeError: String.prototype.matchAll called with a non-global RegExp argumentWarum, und die Lösung
Den ganzen Text mit matchAll auf einmal zu lesen ist in Ordnung, aber matchAll braucht das Flag g: /^…$/gm. Das Flag m lässt ^ und $ an jeder Zeile treffen, g lässt matchAll jeden Treffer finden. Für Ja/Nein-Prüfungen mit test lassen Sie g weg.
const time = new Intl.DateTimeFormat("en-GB", { timeStyle: "short", timeZone: "UTC" });
const match = /^(?<time>\S+)/.exec("2026-10-05T08:15:02Z INFO started");
console.log(time.format(match.groups.time));
Was Node.js ausgibt
RangeError: Invalid time valueWarum, und die Lösung
Gruppen sind immer Strings. DateTimeFormat.format() will ein Date oder einen Timestamp und macht aus einem String eine Zahl, die für einen ISO-String NaN ergibt. Wandeln Sie zuerst um: time.format(new Date(match.groups.time)). Wenn Sie das einmal in parseLine tun, sieht der Rest des Programms den String nie.
JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.