重构
This commit is contained in:
236
packages/shared/src/match-time.ts
Normal file
236
packages/shared/src/match-time.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
export const PLATFORM_TIME_ZONE = 'Asia/Kuala_Lumpur';
|
||||
export const PLATFORM_TIME_ZONE_OFFSET_MINUTES = 8 * 60;
|
||||
export const PLATFORM_TIME_ZONE_OFFSET_LABEL = 'UTC+8';
|
||||
|
||||
const PICKER_DATETIME_RE =
|
||||
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
||||
|
||||
function pad2(value: number): string {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
function validDate(value: string | Date | null | undefined): Date | null {
|
||||
if (value == null) return null;
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
function formatDateTime(
|
||||
date: Date,
|
||||
locale: string,
|
||||
options: Intl.DateTimeFormatOptions,
|
||||
): string {
|
||||
try {
|
||||
return new Intl.DateTimeFormat(locale || 'en-US', options).format(date);
|
||||
} catch {
|
||||
return new Intl.DateTimeFormat('en-US', options).format(date);
|
||||
}
|
||||
}
|
||||
|
||||
function timeZoneParts(date: Date, timeZone: string) {
|
||||
const parts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
}).formatToParts(date);
|
||||
return new Map(parts.map((part) => [part.type, part.value]));
|
||||
}
|
||||
|
||||
function dayKeyInTimeZone(date: Date, timeZone: string): string {
|
||||
const parts = timeZoneParts(date, timeZone);
|
||||
return `${parts.get('year')}-${parts.get('month')}-${parts.get('day')}`;
|
||||
}
|
||||
|
||||
function offsetMinutesInTimeZone(date: Date, timeZone: string): number {
|
||||
const parts = timeZoneParts(date, timeZone);
|
||||
const asUtc = Date.UTC(
|
||||
Number(parts.get('year')),
|
||||
Number(parts.get('month')) - 1,
|
||||
Number(parts.get('day')),
|
||||
Number(parts.get('hour')),
|
||||
Number(parts.get('minute')),
|
||||
Number(parts.get('second')),
|
||||
);
|
||||
return Math.round((asUtc - date.getTime()) / 60000);
|
||||
}
|
||||
|
||||
function parsePickerParts(value: string) {
|
||||
const match = PICKER_DATETIME_RE.exec(value.trim());
|
||||
if (!match) return null;
|
||||
const [, y, mo, d, h, mi, s = '00'] = match;
|
||||
const parts = {
|
||||
year: Number(y),
|
||||
month: Number(mo),
|
||||
day: Number(d),
|
||||
hour: Number(h),
|
||||
minute: Number(mi),
|
||||
second: Number(s),
|
||||
};
|
||||
const wallAsUtc = new Date(
|
||||
Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second),
|
||||
);
|
||||
if (
|
||||
wallAsUtc.getUTCFullYear() !== parts.year ||
|
||||
wallAsUtc.getUTCMonth() !== parts.month - 1 ||
|
||||
wallAsUtc.getUTCDate() !== parts.day ||
|
||||
wallAsUtc.getUTCHours() !== parts.hour ||
|
||||
wallAsUtc.getUTCMinutes() !== parts.minute ||
|
||||
wallAsUtc.getUTCSeconds() !== parts.second
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function platformPickerDateTimeToIso(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return '';
|
||||
const parts = parsePickerParts(trimmed);
|
||||
if (!parts) return trimmed;
|
||||
const platformWallMs = Date.UTC(
|
||||
parts.year,
|
||||
parts.month - 1,
|
||||
parts.day,
|
||||
parts.hour,
|
||||
parts.minute,
|
||||
parts.second,
|
||||
);
|
||||
return new Date(platformWallMs - PLATFORM_TIME_ZONE_OFFSET_MINUTES * 60 * 1000).toISOString();
|
||||
}
|
||||
|
||||
export function isoToPlatformPickerDateTime(value?: string | null): string {
|
||||
if (!value?.trim()) return '';
|
||||
const date = validDate(value);
|
||||
if (!date) return value.slice(0, 19);
|
||||
const platform = new Date(date.getTime() + PLATFORM_TIME_ZONE_OFFSET_MINUTES * 60 * 1000);
|
||||
return [
|
||||
platform.getUTCFullYear(),
|
||||
pad2(platform.getUTCMonth() + 1),
|
||||
pad2(platform.getUTCDate()),
|
||||
].join('-') + `T${pad2(platform.getUTCHours())}:${pad2(platform.getUTCMinutes())}:${pad2(platform.getUTCSeconds())}`;
|
||||
}
|
||||
|
||||
export function isValidIsoDateTime(value: string | Date | null | undefined): boolean {
|
||||
return validDate(value) !== null;
|
||||
}
|
||||
|
||||
export function formatPlatformMatchDateTime(
|
||||
value: string | Date | null | undefined,
|
||||
locale = 'en-US',
|
||||
): string {
|
||||
const date = validDate(value);
|
||||
if (!date) return '';
|
||||
const formatted = formatDateTime(date, locale, {
|
||||
timeZone: PLATFORM_TIME_ZONE,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
return `${formatted} ${PLATFORM_TIME_ZONE_OFFSET_LABEL}`;
|
||||
}
|
||||
|
||||
export function getLocalGmtOffsetLabel(value: string | Date = new Date(), timeZone?: string): string {
|
||||
const date = validDate(value) ?? new Date();
|
||||
const offsetMinutes = timeZone
|
||||
? offsetMinutesInTimeZone(date, timeZone)
|
||||
: -date.getTimezoneOffset();
|
||||
const sign = offsetMinutes >= 0 ? '+' : '-';
|
||||
const abs = Math.abs(offsetMinutes);
|
||||
const hours = Math.floor(abs / 60);
|
||||
const minutes = abs % 60;
|
||||
return minutes === 0 ? `GMT${sign}${hours}` : `GMT${sign}${hours}:${pad2(minutes)}`;
|
||||
}
|
||||
|
||||
export function isSameLocalCalendarDay(
|
||||
value: string | Date,
|
||||
now = new Date(),
|
||||
timeZone?: string,
|
||||
): boolean {
|
||||
const date = validDate(value);
|
||||
if (!date) return false;
|
||||
if (timeZone) {
|
||||
return dayKeyInTimeZone(date, timeZone) === dayKeyInTimeZone(now, timeZone);
|
||||
}
|
||||
return (
|
||||
date.getFullYear() === now.getFullYear() &&
|
||||
date.getMonth() === now.getMonth() &&
|
||||
date.getDate() === now.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
export function isInLocalToday(value: string | Date, now = new Date(), timeZone?: string): boolean {
|
||||
const date = validDate(value);
|
||||
if (!date) return false;
|
||||
if (timeZone) {
|
||||
return dayKeyInTimeZone(date, timeZone) === dayKeyInTimeZone(now, timeZone);
|
||||
}
|
||||
const start = new Date(now);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 1);
|
||||
return date >= start && date < end;
|
||||
}
|
||||
|
||||
export function isAfterLocalToday(value: string | Date, now = new Date(), timeZone?: string): boolean {
|
||||
const date = validDate(value);
|
||||
if (!date) return false;
|
||||
if (timeZone) {
|
||||
return dayKeyInTimeZone(date, timeZone) > dayKeyInTimeZone(now, timeZone);
|
||||
}
|
||||
const end = new Date(now);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
end.setDate(end.getDate() + 1);
|
||||
return date >= end;
|
||||
}
|
||||
|
||||
export function formatLocalMatchDateTime(
|
||||
value: string | Date | null | undefined,
|
||||
locale = 'en-US',
|
||||
options: {
|
||||
variant?: 'compact' | 'full';
|
||||
todayLabel?: string;
|
||||
includeSeconds?: boolean;
|
||||
includeTimeZone?: boolean;
|
||||
timeZone?: string;
|
||||
} = {},
|
||||
): string {
|
||||
const date = validDate(value);
|
||||
if (!date) return '';
|
||||
const variant = options.variant ?? 'compact';
|
||||
const time = formatDateTime(date, locale, {
|
||||
...(options.timeZone ? { timeZone: options.timeZone } : {}),
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
...(options.includeSeconds ? { second: '2-digit' as const } : {}),
|
||||
});
|
||||
let text: string;
|
||||
if (variant === 'compact') {
|
||||
if (options.todayLabel && isSameLocalCalendarDay(date, new Date(), options.timeZone)) {
|
||||
text = `${options.todayLabel} ${time}`;
|
||||
} else {
|
||||
const day = formatDateTime(date, locale, {
|
||||
...(options.timeZone ? { timeZone: options.timeZone } : {}),
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
});
|
||||
text = `${day} ${time}`;
|
||||
}
|
||||
} else {
|
||||
const day = formatDateTime(date, locale, {
|
||||
...(options.timeZone ? { timeZone: options.timeZone } : {}),
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
text = `${day} ${time}`;
|
||||
}
|
||||
if (options.includeTimeZone === false) return text;
|
||||
return `${text} ${getLocalGmtOffsetLabel(date, options.timeZone)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user