30 lines
1.7 KiB
TypeScript
30 lines
1.7 KiB
TypeScript
/** Use Intl directly: converting repeated hours through a local Date loses their UTC offset. */
|
|
export function localTimeInput(value: string, zone = 'Europe/Berlin') {
|
|
const parts = new Intl.DateTimeFormat('en-GB', {
|
|
timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
|
|
}).formatToParts(new Date(value));
|
|
const get = (type: string) => parts.find(part => part.type === type)!.value;
|
|
return `${get('year')}-${get('month')}-${get('day')}T${get('hour')}:${get('minute')}`;
|
|
}
|
|
|
|
/** Preserve existing instants. Require an explicit UTC input for ambiguous new local times. */
|
|
export function timeSpanInput(value: string, zone: string, original?: string | null) {
|
|
if (original && localTimeInput(original, zone) === value) return original;
|
|
const wall = Date.parse(`${value}:00Z`);
|
|
if (!Number.isFinite(wall)) throw new Error('Ungültiger Zeitpunkt.');
|
|
// Sample both sides of a transition; supports non-hour offsets and 30-minute DST changes.
|
|
const offsets = new Set<number>();
|
|
for (let hours = -36; hours <= 36; hours += 6) {
|
|
const sample = wall + hours * 3600000;
|
|
offsets.add(Date.parse(`${localTimeInput(new Date(sample).toISOString(), zone)}:00Z`) - sample);
|
|
}
|
|
const matches = [...offsets].map(offset => wall - offset)
|
|
.filter(candidate => localTimeInput(new Date(candidate).toISOString(), zone) === value);
|
|
if (!matches.length) throw new Error('Diese Ortszeit existiert nicht. Bitte Zeitumstellung beachten.');
|
|
if (matches.length > 1) {
|
|
throw new Error('Diese Ortszeit kommt zweimal vor. Bitte Zeitzone UTC wählen und den eindeutigen UTC-Zeitpunkt eingeben.');
|
|
}
|
|
return new Date(matches[0]).toISOString();
|
|
}
|