From c582c7c4d62aec2175bc75cfc0c4062596814048 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Mon, 23 Feb 2026 06:12:39 +0100 Subject: [PATCH] feat: parse admission notes into structured price table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse semi-structured notes text (e.g. "Samstag: Erw. 15 €, Kinder 8 €") into grouped table rows instead of showing raw text below the table. - Split notes on sentence boundaries (". " + uppercase), handling abbreviations like "Erw." correctly - Extract "Label: Category Price €" patterns into table groups - Expand common abbreviations (Erw. → Erwachsene, Erm. → Ermäßigt) - Guard against "Cat: Price, Cat: Price" format (colons in pricesStr) - Guard against bare prices after colon (letter-start requirement) - Graceful fallback to flat table + raw notes when parsing fails --- web/src/routes/markt/[slug]/+page.svelte | 135 ++++++++++++++++++++--- 1 file changed, 117 insertions(+), 18 deletions(-) diff --git a/web/src/routes/markt/[slug]/+page.svelte b/web/src/routes/markt/[slug]/+page.svelte index 8c30f48..2383a0a 100644 --- a/web/src/routes/markt/[slug]/+page.svelte +++ b/web/src/routes/markt/[slug]/+page.svelte @@ -31,6 +31,80 @@ : null ); + interface ParsedPriceEntry { + category: string; + price: string; + } + + interface ParsedPriceGroup { + label: string; + entries: ParsedPriceEntry[]; + } + + const categoryLabels: Record = { + Erw: 'Erwachsene', + 'Erw.': 'Erwachsene', + Erm: 'Ermäßigt', + 'Erm.': 'Ermäßigt' + }; + + function parseAdmissionNotes(notes: string): { + groups: ParsedPriceGroup[]; + remaining: string; + } { + const groups: ParsedPriceGroup[] = []; + const remainingParts: string[] = []; + + // Split on ". " followed by uppercase — avoids breaking on "Erw." abbreviation + const segments = notes.split(/\.\s+(?=[A-ZÄÖÜ])/).filter((s) => s.trim()); + + for (const raw of segments) { + const segment = raw.replace(/\.+$/, '').trim(); + const colonIdx = segment.indexOf(':'); + if (colonIdx === -1) { + remainingParts.push(segment); + continue; + } + + const label = segment.substring(0, colonIdx).trim(); + const pricesStr = segment.substring(colonIdx + 1).trim(); + + // If pricesStr contains colons, it's "Cat: Price, Cat: Price" format — skip parsing + if (pricesStr.includes(':')) { + remainingParts.push(segment); + continue; + } + + const entries: ParsedPriceEntry[] = []; + // Category must start with a letter (prevents matching bare prices like "13,00 €") + const pricePattern = /(?:^|,\s*)([A-Za-zÄÖÜäöüß].*?)\s+(\d+(?:,\d+)?)\s*€/g; + let m; + + while ((m = pricePattern.exec(pricesStr)) !== null) { + const cat = m[1].trim(); + entries.push({ + category: categoryLabels[cat] ?? cat, + price: m[2] + ' €' + }); + } + + if (entries.length > 0) { + groups.push({ label, entries }); + } else { + remainingParts.push(segment); + } + } + + return { + groups, + remaining: remainingParts.length > 0 ? remainingParts.join('. ') + '.' : '' + }; + } + + const parsedNotes = $derived( + admission?.notes ? parseAdmissionNotes(admission.notes) : { groups: [], remaining: '' } + ); + const jsonLdBreadcrumbHtml = $derived( '