Files
TREK/client/src/components/Planner/placeHoursFormat.ts
T
Maurice bf0507ddb5 fix(planner): Google hours that share a half of the day convert whole to 24h
Google writes a range that stays in one half of the day with the meridiem on
the closing time only, "1:00 – 3:00 PM" for one to three in the afternoon.
The 24-hour conversion only saw times carrying their own AM/PM, so the
opener stayed "1:00" beside a converted "15:00". The closing meridiem is
handed back to an opener that has none before converting; a range spelled
on both sides, the 12h display and lines without a meridiem are untouched.

Closes #2412
2026-09-20 20:28:23 +02:00

81 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Formatting for a provider's weekday opening-hours lines.
*
* These arrive as display text — "Monday: 11:30 AM – 11:00 PM" from Google in
* whatever language was requested, "Monday: 09:00-18:00" from the OSM tag
* parser. They cannot be computed from (that is what `placeOpenState` and the
* structured periods are for), only rendered. Lifted out of PlaceInspector so
* the add-place detail column shows hours the same way the inspector does
* rather than growing a second, subtly different implementation.
*/
/** Rewrites a line between 12h and 24h clocks, leaving it alone when it already fits. */
export function convertHoursLine(line: string, timeFormat: string): string {
if (!line) return ''
const hasAmPm = /\d{1,2}:\d{2}\s*(AM|PM)/i.test(line)
if (timeFormat === '12h' && !hasAmPm) {
// 24h → 12h: "10:00" → "10:00 AM", "21:00" → "9:00 PM", "Uhr" entfernen.
// split/trimEnd rather than /\s*Uhr/g: that pattern walks the rest of the line
// from every space it passes, so a line with no "Uhr" in it costs a pass per
// space. Cutting on the word and trimming the piece in front of it is the
// same edit, once.
const parts = line.split('Uhr')
const withoutUhr = parts.map((part, i) => (i < parts.length - 1 ? part.trimEnd() : part)).join('')
return withoutUhr.replace(/(\d{1,2}):(\d{2})/g, (match, h, m) => {
const hour = Number.parseInt(h)
if (Number.isNaN(hour)) return match
const period = hour >= 12 ? 'PM' : 'AM'
const h12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
return `${h12}:${m} ${period}`
})
}
if (timeFormat !== '12h' && hasAmPm) {
// Google spells a range that stays in one half of the day with the meridiem on
// the closing time only: "1:00 – 3:00 PM" is one to three in the afternoon. The
// conversion below only sees times that carry their own AM/PM, so the opener
// stayed "1:00" next to a "15:00" (#2412). Hand the closing meridiem back to an
// opener that has none. It fires only where nothing but the dash sits between
// the two times, so "11:00 AM – 1:00 PM" keeps both halves as written.
const paired = line.replace(
/(\d{1,2}:\d{2})(\s*[–—-]\s*)(\d{1,2}:\d{2}\s*)(AM|PM)\b/gi,
(_, open, sep, close, p) => `${open} ${p}${sep}${close}${p}`,
)
// 12h → 24h: "10:00 AM" → "10:00", "9:00 PM" → "21:00"
return paired.replace(/(\d{1,2}):(\d{2})\s*(AM|PM)/gi, (_, h, m, p) => {
let hour = Number.parseInt(h)
if (p.toUpperCase() === 'PM' && hour !== 12) hour += 12
if (p.toUpperCase() === 'AM' && hour === 12) hour = 0
return `${String(hour).padStart(2, '0')}:${m}`
})
}
return line
}
/**
* Splits "Monday: 09:00-18:00" into its day and its times.
*
* On the FIRST separator only, because the times contain colons of their own.
* The fullwidth colon is in there because that is what Google returns for
* Chinese and Japanese, and splitting on the ASCII one alone left those lines
* unsplit and the whole string in the day column.
*
* A line with no separator at all comes back with an empty day, which callers
* render across the full width rather than squeezing into a two-column row.
*/
export function splitHoursLine(line: string): [day: string, times: string] {
const at = line.search(/[::]/)
if (at < 0) return ['', line.trim()]
return [line.slice(0, at).trim(), line.slice(at + 1).trim()]
}
/**
* Whether a line says nothing about that day.
*
* `parseOpeningHours` writes "Monday: ?" for a day the OSM tag did not cover.
* Showing a bare question mark reads like a bug, so callers render a dash.
*/
export function isUnknownHoursLine(times: string): boolean {
return times.trim() === '?'
}