214 lines
6.4 KiB
JavaScript
214 lines
6.4 KiB
JavaScript
/**
|
||
* S02 – Säubert + dedupliziert Phrasen aus raw-phrases.json → clean-phrases.json
|
||
*/
|
||
import { readFileSync, writeFileSync } from "fs";
|
||
import { dirname, join } from "path";
|
||
import { fileURLToPath } from "url";
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const RAW_PATH = join(__dirname, "raw-phrases.json");
|
||
const OUT_PATH = join(__dirname, "clean-phrases.json");
|
||
|
||
function decodeEntities(s) {
|
||
return s
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, '"')
|
||
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
||
}
|
||
|
||
/** trim + Whitespace normalisieren + optionale Endpunkte entfernen (für Dedup-Key) */
|
||
function normalizeKey(text) {
|
||
return decodeEntities(String(text))
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.replace(/\.+$/, "")
|
||
.toLowerCase();
|
||
}
|
||
|
||
/** Anzeigeform: Whitespace normalisieren, Endpunkt entfernen (Heft-Stil ohne Punkt) */
|
||
function displayText(text) {
|
||
return decodeEntities(String(text))
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.replace(/\.+$/, "")
|
||
.trim();
|
||
}
|
||
|
||
const HEADER_RE =
|
||
/^(Abteilung|Ausbildungsnachweis|Für die Zeit|Name|Datum|Zweites|Erstes|Drittes|Betriebliche|Zuordnung|Thema der Woche|Berufsschule|Lfd\.?\s*Nr|Ausbilderin|Ausbilder)\b/i;
|
||
|
||
const JUNK_EXACT_RE =
|
||
/^(43180|Nico|Baumann|Ferien|Feiertag|Urlaub|Schultag|Krank|Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag|Additive,?$|\d{4,}|\d+)$/i;
|
||
|
||
function isJunk(text) {
|
||
const s = displayText(text);
|
||
if (!s || s.length < 3) return true;
|
||
if (HEADER_RE.test(s)) return true;
|
||
if (JUNK_EXACT_RE.test(s)) return true;
|
||
if (/^[.\-_\s…·•]+$/.test(s)) return true;
|
||
if (/…{2,}|\.{3,}/.test(s)) return true;
|
||
if (/Unterschrift/i.test(s)) return true;
|
||
if (/Ausbildungsnachweis|Für die Zeit|Betriebliche Tätig|Zuordnung zum Lernziel|Thema der Woche|Berufsschule|Lfd\.?\s*Nr/i.test(s))
|
||
return true;
|
||
if (/^\d{5,}/.test(s)) return true;
|
||
if (/^[\d\s./-]+$/.test(s)) return true; // reine Zahlen / Ziffernblöcke
|
||
if (/<\/?w:|xml|xmlns/i.test(s)) return true;
|
||
if (!/[a-zäöüß]/i.test(s)) return true;
|
||
return false;
|
||
}
|
||
|
||
function isFerienActivity(s) {
|
||
return /^ferien\b/i.test(s) || /^ferien$/i.test(s);
|
||
}
|
||
|
||
/**
|
||
* @param {Map<string, { text: string, count: number, sources?: Set<string> }>} map
|
||
* @param {string} raw
|
||
* @param {string | null} sourceFile
|
||
* @param {{ trackSources?: boolean, minLen?: number, maxLen?: number }} opts
|
||
*/
|
||
function addPhrase(map, raw, sourceFile, opts = {}) {
|
||
const { trackSources = false, minLen = 4, maxLen = 180 } = opts;
|
||
if (isJunk(raw)) return;
|
||
const text = displayText(raw);
|
||
if (!text || text.length < minLen || text.length > maxLen) return;
|
||
if (trackSources && isFerienActivity(text)) return;
|
||
|
||
const key = normalizeKey(text);
|
||
if (!key) return;
|
||
|
||
const prev = map.get(key);
|
||
if (prev) {
|
||
prev.count += 1;
|
||
// kürzere / punktlose Form bevorzugen
|
||
if (text.length < prev.text.length || (!/\.$/.test(raw) && /\.$/.test(prev.text))) {
|
||
prev.text = text;
|
||
}
|
||
if (trackSources && sourceFile) prev.sources.add(sourceFile);
|
||
} else {
|
||
map.set(key, {
|
||
text,
|
||
count: 1,
|
||
...(trackSources ? { sources: new Set(sourceFile ? [sourceFile] : []) } : {}),
|
||
});
|
||
}
|
||
}
|
||
|
||
function toSortedList(map, { withSources = false } = {}) {
|
||
return [...map.values()]
|
||
.map((entry) => {
|
||
if (withSources) {
|
||
return {
|
||
text: entry.text,
|
||
count: entry.count,
|
||
sources: [...entry.sources].sort((a, b) => a.localeCompare(b, "de")),
|
||
};
|
||
}
|
||
return { text: entry.text, count: entry.count };
|
||
})
|
||
.sort((a, b) => b.count - a.count || a.text.localeCompare(b.text, "de"));
|
||
}
|
||
|
||
function main() {
|
||
let raw;
|
||
try {
|
||
raw = JSON.parse(readFileSync(RAW_PATH, "utf8"));
|
||
} catch (err) {
|
||
console.error(
|
||
"raw-phrases.json fehlt oder ist ungültig. Zuerst S01 (extract-phrases.mjs) ausführen.",
|
||
);
|
||
console.error(err.message);
|
||
process.exit(1);
|
||
}
|
||
|
||
const hefte = raw.hefte ?? [];
|
||
if (!hefte.length) {
|
||
console.error("raw-phrases.json enthält keine Hefte.");
|
||
process.exit(1);
|
||
}
|
||
|
||
const activities = new Map();
|
||
const themes = new Map();
|
||
const school = new Map();
|
||
|
||
for (const heft of hefte) {
|
||
const source = heft.sourceFile ?? null;
|
||
|
||
// Pro Heft einmal pro normalisierter Phrase zählen (Tabellen-Duplikate)
|
||
const seenAct = new Set();
|
||
for (const a of heft.activities ?? []) {
|
||
const key = normalizeKey(a);
|
||
if (!key || seenAct.has(key)) continue;
|
||
seenAct.add(key);
|
||
addPhrase(activities, a, source, {
|
||
trackSources: true,
|
||
minLen: 6,
|
||
maxLen: 160,
|
||
});
|
||
}
|
||
|
||
const seenTheme = new Set();
|
||
for (const t of heft.weekThemes ?? []) {
|
||
const key = normalizeKey(t);
|
||
if (!key || seenTheme.has(key)) continue;
|
||
seenTheme.add(key);
|
||
if (/ferien/i.test(displayText(t))) continue;
|
||
addPhrase(themes, t, null, { minLen: 3, maxLen: 80 });
|
||
}
|
||
|
||
const seenSchool = new Set();
|
||
for (const s of heft.schoolTopics ?? []) {
|
||
const key = normalizeKey(s);
|
||
if (!key || seenSchool.has(key)) continue;
|
||
seenSchool.add(key);
|
||
// Ferien gehört ins Berufsschule-Feld der Woche, nicht als Suggestion-Phrase
|
||
if (/ferien/i.test(displayText(s))) continue;
|
||
addPhrase(school, s, null, { minLen: 4, maxLen: 160 });
|
||
}
|
||
}
|
||
|
||
const actList = toSortedList(activities, { withSources: true }).filter(
|
||
(a) =>
|
||
!/^\d/.test(a.text) &&
|
||
!/Lfd/i.test(a.text) &&
|
||
/[a-zäöüß]{3,}/i.test(a.text),
|
||
);
|
||
const themeList = toSortedList(themes).filter(
|
||
(t) => t.text !== "-" && !/^[.\-]+$/.test(t.text),
|
||
);
|
||
const schoolList = toSortedList(school).filter(
|
||
(s) => !/^krank$/i.test(s.text) && !/stillarbeit$/i.test(s.text),
|
||
);
|
||
|
||
const clean = {
|
||
activities: actList,
|
||
themes: themeList,
|
||
school: schoolList,
|
||
meta: {
|
||
source: "raw-phrases.json",
|
||
hefte: hefte.length,
|
||
activities: actList.length,
|
||
themes: themeList.length,
|
||
school: schoolList.length,
|
||
generatedAt: new Date().toISOString(),
|
||
},
|
||
};
|
||
|
||
writeFileSync(OUT_PATH, JSON.stringify(clean, null, 2), "utf8");
|
||
|
||
console.log(
|
||
`Clean: ${hefte.length} Hefte → ${actList.length} Activities, ${themeList.length} Themes, ${schoolList.length} School`,
|
||
);
|
||
console.log(
|
||
"Top Activities:",
|
||
actList
|
||
.slice(0, 12)
|
||
.map((a) => `${a.text} (${a.count})`)
|
||
.join(" · "),
|
||
);
|
||
}
|
||
|
||
main();
|