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
This commit is contained in:
Maurice
2026-09-20 20:28:23 +02:00
parent 9bd9ff4a80
commit bf0507ddb5
2 changed files with 67 additions and 1 deletions
@@ -0,0 +1,56 @@
import { describe, it, expect } from 'vitest'
import { convertHoursLine, isUnknownHoursLine, splitHoursLine } from './placeHoursFormat'
describe('convertHoursLine', () => {
it('FE-PLANNER-HOURS-001: a range that stays in one half of the day carries the meridiem back to its opener', () => {
// Google writes "1:00 – 3:00 PM" for one to three in the afternoon (#2412). The
// opener used to stay "1:00" next to a converted "15:00".
expect(convertHoursLine('Wednesday: 1:00 – 3:00 PM, 5:00 – 8:00 PM', '24h'))
.toBe('Wednesday: 13:00 – 15:00, 17:00 – 20:00')
})
it('FE-PLANNER-HOURS-002: a range spelled on both sides is converted as written', () => {
expect(convertHoursLine('Monday: 11:00 AM – 1:00 PM', '24h')).toBe('Monday: 11:00 – 13:00')
expect(convertHoursLine('Friday: 5:00 PM – 2:00 AM', '24h')).toBe('Friday: 17:00 – 02:00')
})
it('FE-PLANNER-HOURS-003: twelve stays twelve in the afternoon and turns to zero after midnight', () => {
expect(convertHoursLine('Sunday: 12:00 – 3:00 PM', '24h')).toBe('Sunday: 12:00 – 15:00')
expect(convertHoursLine('Sunday: 12:00 – 1:00 AM', '24h')).toBe('Sunday: 00:00 – 01:00')
})
it('FE-PLANNER-HOURS-004: a morning range keeps its morning', () => {
expect(convertHoursLine('Tuesday: 8:00 – 11:30 AM', '24h')).toBe('Tuesday: 08:00 – 11:30')
})
it('FE-PLANNER-HOURS-005: a plain hyphen separates the same way the en dash does', () => {
expect(convertHoursLine('Thursday: 1:00-3:00 PM', '24h')).toBe('Thursday: 13:00-15:00')
})
it('FE-PLANNER-HOURS-006: the 12h display and lines without a meridiem are untouched', () => {
expect(convertHoursLine('Wednesday: 1:00 – 3:00 PM', '12h')).toBe('Wednesday: 1:00 – 3:00 PM')
expect(convertHoursLine('Montag: 11:30–23:00', '24h')).toBe('Montag: 11:30–23:00')
expect(convertHoursLine('Saturday: Open 24 hours', '24h')).toBe('Saturday: Open 24 hours')
expect(convertHoursLine('', '24h')).toBe('')
})
it('FE-PLANNER-HOURS-007: 24h lines become 12h, with the German "Uhr" dropped', () => {
expect(convertHoursLine('Montag: 09:00 – 21:00 Uhr', '12h')).toBe('Montag: 9:00 AM – 9:00 PM')
expect(convertHoursLine('Monday: 00:30 – 12:00', '12h')).toBe('Monday: 12:30 AM – 12:00 PM')
})
})
describe('splitHoursLine', () => {
it('FE-PLANNER-HOURS-008: splits on the first colon only, the fullwidth one included', () => {
expect(splitHoursLine('Monday: 09:00-18:00')).toEqual(['Monday', '09:00-18:00'])
expect(splitHoursLine('月曜日:9:00~18:00')).toEqual(['月曜日', '9:00~18:00'])
expect(splitHoursLine('Open 24 hours')).toEqual(['', 'Open 24 hours'])
})
})
describe('isUnknownHoursLine', () => {
it('FE-PLANNER-HOURS-009: a bare question mark is the tag saying nothing', () => {
expect(isUnknownHoursLine(' ? ')).toBe(true)
expect(isUnknownHoursLine('09:00-18:00')).toBe(false)
})
})
@@ -31,8 +31,18 @@ export function convertHoursLine(line: string, timeFormat: string): string {
})
}
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 line.replace(/(\d{1,2}):(\d{2})\s*(AM|PM)/gi, (_, h, m, p) => {
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