timeUtils.ts (1655B)
1 import { addHours, isAfter, startOfHour, setHours, addDays } from 'date-fns'; 2 import { toZonedTime, fromZonedTime } from 'date-fns-tz'; 3 4 const JST_TZ = 'Asia/Tokyo'; 5 6 // カレンダーの移動制限範囲(運用開始月~現在の月) 7 export const CALENDAR_LIMITS = { 8 MIN: new Date(2025, 12, 1), 9 MAX: new Date(), 10 } as const; 11 12 // 境界線(4時/16時)を求める 13 export const getNextBoundary = (date: Date): Date => { 14 const jst = toZonedTime(date, JST_TZ); 15 const hour = jst.getHours(); 16 17 let boundary = startOfHour(jst); 18 if (hour < 4) { 19 boundary = setHours(boundary, 4); 20 } else if (hour < 16) { 21 boundary = setHours(boundary, 16); 22 } else { 23 boundary = setHours(addDays(boundary, 1), 4); 24 } 25 return fromZonedTime(boundary, JST_TZ); 26 }; 27 28 // UI表示用の終了時刻計算 (3時間後 or 境界線の早い方) 29 export const getSessionEndTime = (lastTapTime: Date | null): Date | null => { 30 if (!lastTapTime) return null; 31 32 const baseTime = new Date(lastTapTime); 33 baseTime.setMilliseconds(0); 34 35 const standardEnd = addHours(baseTime, 3); 36 const boundary = getNextBoundary(baseTime); 37 38 return isAfter(standardEnd, boundary) ? boundary : standardEnd; 39 }; 40 41 // 通知を予約すべきか判定 42 export const shouldScheduleNotification = (tapTime: Date): boolean => { 43 const jst = toZonedTime(tapTime, JST_TZ); 44 const h = jst.getHours(); 45 46 if ((h >= 1 && h < 4) || (h >= 13 && h < 16)) return false; 47 48 const endTime = getSessionEndTime(tapTime); 49 if (!endTime) return false; 50 51 const standardEnd = addHours(tapTime, 3); 52 standardEnd.setMilliseconds(0); 53 54 return endTime.getTime() === standardEnd.getTime(); 55 };