diff --git a/frontend/src/utils/datetime.ts b/frontend/src/utils/datetime.ts new file mode 100644 index 0000000..3dee2cb --- /dev/null +++ b/frontend/src/utils/datetime.ts @@ -0,0 +1,218 @@ + +// YYC MARK: +// This file is synchronized with "dt.py". +// If this file or dt.py have bugs, all code should be changed together. + +export const MONTH_DAY_COUNT: number[] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +export const MIN_YEAR: number = 1950; +export const MAX_YEAR: number = 2200; +export const MIN_DATETIME = new Date(Date.UTC(MIN_YEAR, 0, 1, 0, 0, 0, 0)); +export const MAX_DATETIME = new Date(Date.UTC(MAX_YEAR, 0, 1, 0, 0, 0, 0)); +export const MIN_TIMESTAMP = Math.floor(MIN_DATETIME.getTime() / 60000); +export const MAX_TIMESTAMP = Math.floor(MAX_DATETIME.getTime() / 60000); + +export const DAY1_SPAN: number = 60 * 24; +export const DAY7_SPAN: number = 7 * DAY1_SPAN; + +const PRECOMPILED_LOOP_RULES = { + year: new RegExp(/^Y([SR]{1})([1-9]\d*)$/), + month: new RegExp(/^M([SR]{1})([ABCD]{1})([1-9]\d*)$/), + week: new RegExp(/^W([TF]{7})([1-9]\d*)$/), + day: new RegExp(/^D([1-9]\d*)$/) +}; + +const PRECOMPILED_LOOP_STOP_RULES = { + infinity: new RegExp(/^F$/), + datetime: new RegExp(/^D([1-9]\d*|0)$/), + times: new RegExp(/^T([1-9]\d*)$/) +}; + +// region: Core Functions + +export function resolveLoopRules4UI(strl: string) { + +} + +export function resolveLoopRules2Event(loopRules: string, loopDateTimeStart: number, loopDateTimeEnd: number, eventDateTimeStart: number, eventDateTimeEnd: number, timezoneOffset: number, clampStartDateTime: number) { + +} + +export function resolveLoopRules4Text(strl: string, startDateTime: number, timezoneOffset: number) { + +} + +// endregion + +// region: Utility Functions + +function leapYearCountEx(endYear: number, includeThis: boolean, baseYear: number, includeBase: boolean): number { + if (!includeThis) endYear--; + if (includeBase) baseYear--; + + let endly = Math.floor(endYear / 4); + endly -= Math.floor(endYear / 100); + endly += Math.floor(endYear / 400); + + let basely = Math.floor(baseYear / 4); + basely -= Math.floor(baseYear / 100); + basely += Math.floor(baseYear / 400); + + return (endly - basely); +} + +function daysCount(year: number, month: number, day: number): number { + let ly = leapYearCountEx(year, false, 1, true); + let days = 365 * (year - 1); + days += ly; + + for (let index = 1; index < month; index++) + days += MONTH_DAY_COUNT[index - 1]!; + + if (month > 2 && isLeapYear(year)) + days += 1; + + days += day - 1; + return days; +} + +function monthsCount(year: number, month: number): number { + return (year - 1) * 12 + (month - 1); +} + +function dayOfWeek(year: number, month: number, day: number): number { + return daysCount(year, month, day) % 7; +} + +type DayInMonthInfo = [ + /** The day count to this day, counting from month head to tail. */ + daysForward: number, + /** The day count to this day, counting from month tail to head. */ + daysBackward: number, + /** The count of the week this day located, counting from month head to tail. */ + weeksForward: number, + /** The day count in this week. (for `weeksForward` using) */ + weeksForwardDayOfWeek: number, + /** The count of the week this day located, counting from month tail to head. */ + weeksBackward: number, + /** The day count in this week. (for `weeksBackward` using) */ + weeksBackwardDayOfWeek: number, +]; + +function getDayInMonth(year: number, month: number, day: number): DayInMonthInfo { + let days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0); + let firstDayOfWeek = dayOfWeek(year, month, 1); + let bilateralDayOfWeek = (firstDayOfWeek + day - 1) % 7; + + let dayForwards = day; + let dayBackwards = days - day + 1; + + let weeksForward = Math.floor((dayForwards - 1) / 7) + 1; + let weeksBackwards = Math.floor((dayBackwards - 1) / 7) + 1; + + return [dayForwards, dayBackwards, weeksForward, bilateralDayOfWeek, weeksBackwards, bilateralDayOfWeek]; +} + +// YYC MARK: +// I use Japanese word youbi (ようび) to present +// the concept of "the day of week" exactly. + +type RemanagedDayInMonth = [ + /** Count forward by days (Day N) */ + forwardByDayCount: number, + /** Count backward by days (Nth day from the end) */ + backwardByDayCount: number, + /** Count forward by weeks (Week N, Youbi X) */ + forwardByWeekCount: number, + /** Count backward by weeks (Nth week from the end, Youbi X) */ + backwardByWeekCount: number, +]; + +/** + * Pass in an old date and a new month, and it will calculate + * the new date corresponding to that old date in the new month + * using four different calendar rules: + * @param oldYear + * @param oldMonth + * @param oldDay + * @param newYear + * @param newMonth + * @param isStrict + * @returns + */ +function getRemanagedDayInMonth(oldYear: number, oldMonth: number, oldDay: number, newYear: number, newMonth: number, isStrict: boolean) { + let ddata = getDayInMonth(oldYear, oldMonth, oldDay); + let mdata = getMonthWeekStatistics(newYear, newMonth); + let days = MONTH_DAY_COUNT[newMonth - 1]! + ((newMonth == 2 && isLeapYear(newYear)) ? 1 : 0); + let firstDayOfWeek = dayOfWeek(newYear, newMonth, 1); + //let lastDayOfWeek = (firstDayOfWeek + days - 1) % 7; + + let methodA = undefined; + let methodB = undefined; + if (isStrict) { + methodA = ddata[0] > days ? undefined : ddata[0]; + methodB = ddata[1] > days ? undefined : (days - ddata[1] + 1); + } else { + methodA = Math.min(ddata[0], days); + methodB = days - Math.min(ddata[1], days) + 1; + } + + let methodC = undefined; + if (ddata[2] <= mdata[ddata[3]]! || !isStrict) { + let targetWeek = Math.min(ddata[2], mdata[ddata[3]]!); + methodC = 1 + (targetWeek - 1) * 7 + ((ddata[3] + 7 - firstDayOfWeek) % 7); + } + + let methodD = undefined; + if (ddata[4] <= mdata[ddata[5]]! || !isStrict) { + // convert to type c and calc + let targetWeek = mdata[ddata[5]]! - Math.min(ddata[4], mdata[ddata[5]]!) + 1; + methodD = 1 + (targetWeek - 1) * 7 + ((ddata[5] + 7 - firstDayOfWeek) % 7); + } + + return [methodA, methodB, methodC, methodD]; +} + +type MonthWeekStatistics = [ + /** The count of Youbi1 presented in this month. */ + youbi1: number, + /** The count of Youbi2 presented in this month. */ + youbi2: number, + /** The count of Youbi3 presented in this month. */ + youbi3: number, + /** The count of Youbi4 presented in this month. */ + youbi4: number, + /** The count of Youbi5 presented in this month. */ + youbi5: number, + /** The count of Youbi6 presented in this month. */ + youbi6: number, + /** The count of Youbi7 presented in this month. */ + youbi7: number, +]; + +function getMonthWeekStatistics(year: number, month: number): MonthWeekStatistics { + let days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0); + let firstDayOfWeek = dayOfWeek(year, month, 1); + + let result: MonthWeekStatistics = [4, 4, 4, 4, 4, 4, 4]; + let remain = days % 7; + let week = firstDayOfWeek; + while (remain > 0) { + result[week % 7]! += 1; + week++; + remain--; + } + + return result; +} + +function isLeapYear(year: number): boolean { + let isLeap = false; + if (year % 4 == 0) isLeap = true; + if (year % 100 == 0) isLeap = false; + if (year % 400 == 0) isLeap = true; + return isLeap; +} + +// endregion +