1
0

fix: fix type error in datetime module

This commit is contained in:
2026-07-07 21:03:22 +08:00
parent 18f73b3084
commit b1b42ab2fb
3 changed files with 499 additions and 67 deletions

View File

@@ -1,3 +1,5 @@
import { lcm, format } from "./utils";
import { universalGetDayOfWeek } from "./i18n";
// YYC MARK:
// This file is synchronized with "dt.py".
@@ -30,16 +32,366 @@ const PRECOMPILED_LOOP_STOP_RULES = {
// region: Core Functions
export function resolveLoopRules4UI(strl: string) {
/** Year loop: [type=0, isStrict, yearSpan] */
type YearLoopRule = [0, boolean, number];
/** Month loop: [type=1, isStrict, mode, monthSpan] */
type MonthLoopRule = [1, boolean, 'A' | 'B' | 'C' | 'D', number];
/** Week loop: [type=2, 7 weekday booleans, weekSpan] */
type WeekLoopRule = [2, boolean, boolean, boolean, boolean, boolean, boolean, boolean, number];
/** Day loop: [type=3, daySpan] */
type DayLoopRule = [3, number];
type LoopRule = YearLoopRule | MonthLoopRule | WeekLoopRule | DayLoopRule;
/** Infinity stop: [type=0] */
type InfinityStopRule = [0];
/** Datetime stop: [type=1, timestamp] */
type DatetimeStopRule = [1, number];
/** Times stop: [type=2, times] */
type TimesStopRule = [2, number];
type LoopStopRule = InfinityStopRule | DatetimeStopRule | TimesStopRule;
/**
* Parses a raw loop rule string (e.g. "YS2-F") into structured rule arrays
* for UI display purposes.
* @param strl - The loop rule string in format "loopRule-stopRule".
* @returns A tuple of [LoopRule, LoopStopRule] on success, or undefined
* when the string is empty, malformed, or contains no valid rule.
*/
export function resolveLoopRules4UI(strl: string): [LoopRule, LoopStopRule] | undefined {
if (strl == '') return undefined;
let sp = strl.split('-');
if (sp.length != 2) return undefined;
let loopRules: LoopRule | undefined = undefined;
let loopStopRules: LoopStopRule | undefined = undefined;
let match: RegExpExecArray | null;
if ((match = PRECOMPILED_LOOP_RULES.year.exec(sp[0]!)) !== null) {
loopRules = [0, match[1]! == 'S', parseInt(match[2]!)];
} else if ((match = PRECOMPILED_LOOP_RULES.month.exec(sp[0]!)) !== null) {
loopRules = [1, match[1]! == 'S', match[2]! as 'A' | 'B' | 'C' | 'D', parseInt(match[3]!)];
} else if ((match = PRECOMPILED_LOOP_RULES.week.exec(sp[0]!)) !== null) {
const w = match[1]!;
loopRules = [2, w[0] == 'T', w[1] == 'T', w[2] == 'T', w[3] == 'T', w[4] == 'T', w[5] == 'T', w[6] == 'T', parseInt(match[2]!)] as WeekLoopRule;
} else if ((match = PRECOMPILED_LOOP_RULES.day.exec(sp[0]!)) !== null) {
loopRules = [3, parseInt(match[1]!)];
} else return undefined;
if ((match = PRECOMPILED_LOOP_STOP_RULES.infinity.exec(sp[1]!)) !== null) {
loopStopRules = [0];
} else if ((match = PRECOMPILED_LOOP_STOP_RULES.datetime.exec(sp[1]!)) !== null) {
loopStopRules = [1, parseInt(match[1]!)];
} else if ((match = PRECOMPILED_LOOP_STOP_RULES.times.exec(sp[1]!)) !== null) {
loopStopRules = [2, parseInt(match[1]!)];
} else return undefined;
return [loopRules, loopStopRules];
}
export function resolveLoopRules2Event(loopRules: string, loopDateTimeStart: number, loopDateTimeEnd: number, eventDateTimeStart: number, eventDateTimeEnd: number, timezoneOffset: number, clampStartDateTime: number) {
/** A pair of [start, end] timestamps (in minutes) representing a single event occurrence. */
type TimeRange = [number, number];
/**
* Resolves a loop rule string into concrete event date ranges by expanding
* the loop pattern across the detection window.
* @remark loopDateTimeStart's value does not correspond with the database;
* it is calculated by the program and should point to the closet
* potential event start datetime.
* @remark loopDateTimeEnd is performed like loopDateTimeStart and
* it indicates the possible time point of the tail of legal event.
* @remark clampStartDateTime is the real clamp datetime of event start
* datetime.
* @remark loopDateTimeStart is the start datetime for detection.
* @remark In this section, all times should be analysed with
* `Date((time + timezoneOffset) * 60000)` and use `.getUTC...()`
* functions.
* @param loopRules - The loop rule string (e.g. "YS2-F").
* @param loopDateTimeStart - Start of the detection window (minutes).
* @param loopDateTimeEnd - End of the detection window (minutes).
* @param eventDateTimeStart - Original event start datetime (minutes).
* @param eventDateTimeEnd - Original event end datetime (minutes).
* @param timezoneOffset - Timezone offset in minutes.
* @param clampStartDateTime - Earliest allowed start datetime (minutes).
* @returns An array of [start, end] timestamp pairs representing every
* occurrence within the window, or undefined if the rule is invalid.
*/
export function resolveLoopRules2Event(
fullLoopRules: string,
loopDateTimeStart: number,
loopDateTimeEnd: number,
eventDateTimeStart: number,
eventDateTimeEnd: number,
timezoneOffset: number,
clampStartDateTime: number): TimeRange[] | undefined {
if (fullLoopRules == '') return [
[Math.max(eventDateTimeStart, clampStartDateTime),
Math.max(loopDateTimeEnd, eventDateTimeEnd)]
];
let sp = fullLoopRules.split('-');
if (sp.length != 2) return undefined;
let loopRules = sp[0]!; // we don't need consider stop flag
let result: TimeRange[] = new Array();
// compute offset and duration
let eventDateTime = new Date((eventDateTimeStart + timezoneOffset) * 60000);
eventDateTime.setUTCHours(0, 0, 0, 0);
let eventOffset = eventDateTimeStart - (Math.floor(eventDateTime.getTime() / 60000) - timezoneOffset);
let eventDuration = eventDateTimeEnd - eventDateTimeStart;
let detectDateTime = new Date(loopDateTimeStart * 60000);
detectDateTime.setUTCHours(0, 0, 0, 0);
let originalYear = eventDateTime.getUTCFullYear();
let originalMonth = eventDateTime.getUTCMonth() + 1;
let originalDay = eventDateTime.getUTCDate();
// compute event
let match: RegExpExecArray | null;
if ((match = PRECOMPILED_LOOP_RULES.year.exec(loopRules)) !== null) {
let isStrict = match[1]! == 'S';
let loopSpan = parseInt(match[2]!);
let yearCount = detectDateTime.getFullYear() - originalYear;
let isSpecial = (originalMonth == 2 && originalDay == 29);
let realLoopSpan = (isSpecial && isStrict) ? lcm(4, loopSpan) : loopSpan;
//let fullSpanCount = Math.floor(yearCount / realLoopSpan);
let remainYear = yearCount % realLoopSpan;
//detectDateTime.setUTCFullYear(fullSpanCount + detectDateTime.getUTCFullYear(), 1, 1);
if (remainYear != 0)
detectDateTime.setUTCFullYear(realLoopSpan - remainYear + detectDateTime.getUTCFullYear(), 1 - 1, 1);
let skipFlag = false;
while (Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
skipFlag = false;
if (isSpecial) {
// is special day, 29 Feb
// try set it in 29 Feb
if (isStrict) {
if (isLeapYear(detectDateTime.getUTCFullYear())) detectDateTime.setUTCMonth(2 - 1, 29);
else skipFlag = true; // order skip
} else {
if (isLeapYear(detectDateTime.getUTCFullYear())) detectDateTime.setUTCMonth(2 - 1, 29);
else detectDateTime.setUTCMonth(2 - 1, 28);
}
} else detectDateTime.setUTCMonth(originalMonth - 1, originalDay);
if (!skipFlag) {
result.push(
[Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset,
Math.floor(detectDateTime.getTime() / 60000) + eventOffset + eventDuration - timezoneOffset]
);
}
detectDateTime.setUTCFullYear(realLoopSpan + detectDateTime.getUTCFullYear());
}
} else if ((match = PRECOMPILED_LOOP_RULES.month.exec(loopRules)) !== null) {
let isStrict = match[1]! == 'S';
let loopMethod = match[2]!;
let loopSpan = parseInt(match[3]!);
let monthsCountValue = monthsCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1) -
monthsCount(originalYear, originalMonth);
//let fullSpanCount = Math.floor(monthsCountValue / loopSpan);
let remainMonth = monthsCountValue % loopSpan;
//detectDateTime.setUTCMonth(fullSpanCount * loopSpan + detectDateTime.getUTCMonth(), 1);
detectDateTime.setUTCDate(1);
if (remainMonth != 0)
detectDateTime.setUTCMonth(loopSpan - remainMonth + detectDateTime.getUTCMonth(), 1);
while (Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
let data = getRemanagedDayInMonth(originalYear, originalMonth, originalDay, detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, isStrict);
let predictedDay: number | undefined = undefined;
switch (loopMethod) {
case 'A':
if (typeof (data[0]) !== 'undefined') predictedDay = data[0];
break;
case 'B':
if (typeof (data[1]) !== 'undefined') predictedDay = data[1];
break;
case 'C':
if (typeof (data[2]) !== 'undefined') predictedDay = data[2];
break;
case 'D':
if (typeof (data[3]) !== 'undefined') predictedDay = data[3];
break;
}
if (typeof (predictedDay) !== 'undefined') {
detectDateTime.setUTCDate(predictedDay);
result.push(
[Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset,
Math.floor(detectDateTime.getTime() / 60000) + eventOffset + eventDuration - timezoneOffset]
);
}
detectDateTime.setUTCMonth(loopSpan + detectDateTime.getUTCMonth(), 1);
}
} else if ((match = PRECOMPILED_LOOP_RULES.week.exec(loopRules)) !== null) {
let loopSpan = parseInt(match[2]!);
let weekOption: boolean[] = [];
let weekEventCount = 0
for (let i = 0; i < 7; i++) {
weekOption.push(match[1]![i] == 'T');
if (match[1]![i] == 'T') weekEventCount++;
}
let originalWeek = dayOfWeek(originalYear, originalMonth, originalDay);
// try insert original event
if (!weekOption[originalWeek]) {
result.push(
[eventDateTimeStart, eventDateTimeEnd]
);
}
let daysCountValue = daysCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, detectDateTime.getDate()) -
daysCount(originalYear, originalMonth, originalDay);
//let fullSpanCount = Math.floor(daysCountValue / (7 * loopSpan));
let remainFullSpanCount = Math.floor((daysCountValue % (7 * loopSpan)) / 7);
let remainDays = (daysCountValue % (7 * loopSpan)) % 7;
//detectDateTime.setUTCDate((7 * loopSpan * fullSpanCount) + detectDateTime.getUTCDate());
if (remainFullSpanCount != 0) {
detectDateTime.setUTCDate((loopSpan - remainFullSpanCount) * 7 + detectDateTime.getUTCDate());
}
let weekCounter = remainDays;
while (Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
if (weekOption[(weekCounter + originalWeek) % 7])
result.push(
[Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset,
Math.floor(detectDateTime.getTime() / 60000) + eventOffset + eventDuration - timezoneOffset]
);
weekCounter = (weekCounter + 1) % 7;
detectDateTime.setUTCDate(detectDateTime.getUTCDate() + 1);
if (weekCounter == 0)
detectDateTime.setUTCDate(detectDateTime.getUTCDate() + (loopSpan - 1) * 7);
}
} else if ((match = PRECOMPILED_LOOP_RULES.day.exec(loopRules)) !== null) {
let loopSpan = parseInt(match[1]!);
let daysCountValue = daysCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, detectDateTime.getUTCDate()) -
daysCount(originalYear, originalMonth, originalDay);
//let fullSpanCount = Math.floor(daysCountValue / loopSpan);
let remainDays = daysCountValue % loopSpan;
//detectDateTime.setUTCDate(fullSpanCount * loopSpan + detectDateTime.getUTCDate());
if (remainDays != 0)
detectDateTime.setUTCDate(loopSpan - remainDays + detectDateTime.getUTCDate());
while (Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
result.push(
[Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset,
Math.floor(detectDateTime.getTime() / 60000) + eventOffset + eventDuration - timezoneOffset]
);
detectDateTime.setUTCDate(detectDateTime.getUTCDate() + loopSpan);
}
} else return undefined;
// clamp item
let realResult: TimeRange[] = new Array();
for (let i of result) {
let start = i[0];
let end = i[1];
if (end > clampStartDateTime && start <= loopDateTimeEnd)
realResult.push([Math.max(start, clampStartDateTime), Math.min(end, loopDateTimeEnd)]);
}
return realResult;
}
export function resolveLoopRules4Text(strl: string, startDateTime: number, timezoneOffset: number) {
/**
* Resolves a loop rule string into a human-readable i18n-localised text
* description suitable for display in the UI.
* @param strl - The loop rule string in format "loopRule-stopRule".
* @param startDateTime - Reference start datetime (minutes) for computing
* contextual fields (e.g. day-in-month info).
* @param timezoneOffset - Timezone offset in minutes.
* @returns A concatenated human-readable string, or an empty string when
* the input is empty or cannot be parsed.
*/
export function resolveLoopRules4Text(strl: string, startDateTime: number, timezoneOffset: number): string {
if (strl == '') return "";
let sp = strl.split('-');
if (sp.length != 2) return "";
let loopRules: string;
let loopStopRules: string;
let datetimeInstance = new Date((startDateTime + timezoneOffset) * 60000)
let match: RegExpExecArray | null;
if ((match = PRECOMPILED_LOOP_RULES.year.exec(sp[0]!)) !== null) {
if (match[1]! == 'S')
loopRules = "严格模式。";
else
loopRules = "宽松模式。";
loopRules += format("每{0}年于{1}循环一次。",
parseInt(match[2]!), datetimeInstance.toLocaleDateString(undefined, { timeZone: "UTC" })
);
} else if ((match = PRECOMPILED_LOOP_RULES.month.exec(sp[0]!)) !== null) {
if (match[1]! == 'S')
loopRules = "严格模式。";
else
loopRules = "宽松模式。";
let dayInMonth = getDayInMonth(
datetimeInstance.getUTCFullYear(),
datetimeInstance.getUTCMonth() + 1,
datetimeInstance.getUTCDate());
switch (match[2]!) {
case 'A':
loopRules = format("每{0}月的第{1}日循环一次。",
parseInt(match[3]!), dayInMonth[0]
);
break;
case 'B':
loopRules = format("每{0}月的倒数第{1}日循环一次。",
parseInt(match[3]!), dayInMonth[1]
);
break;
case 'C':
loopRules = format("每{0}月的第{1}个星期{2}循环一次。",
parseInt(match[3]!), dayInMonth[2], dayInMonth[3]
);
break;
case 'D':
loopRules = format("每{0}月的倒数第{1}个星期{2}循环一次。",
parseInt(match[3]!), dayInMonth[4], dayInMonth[5]
);
break;
}
} else if ((match = PRECOMPILED_LOOP_RULES.week.exec(sp[0]!)) !== null) {
let weekOfDayCache = [];
for (let i = 0; i < 7; i++) {
if (match[1]![i] == 'T')
weekOfDayCache.push(universalGetDayOfWeek(i));
}
loopRules = format("每{0}周的{1}循环一次。",
parseInt(match[2]!), weekOfDayCache.join(', ')
);
} else if ((match = PRECOMPILED_LOOP_RULES.day.exec(sp[0]!)) !== null) {
loopRules = format("每{0}天循环一次。", parseInt(match[1]!));
} else return "";
if ((match = PRECOMPILED_LOOP_STOP_RULES.infinity.exec(sp[1]!)) !== null) {
loopStopRules = "永远循环。";
} else if ((match = PRECOMPILED_LOOP_STOP_RULES.datetime.exec(sp[1]!)) !== null) {
loopStopRules = format("到{0}停止循环。",
new Date(parseInt(match[1]!)).toLocaleDateString()
);
} else if ((match = PRECOMPILED_LOOP_STOP_RULES.times.exec(sp[1]!)) !== null) {
loopStopRules = format("循环{0}次。", parseInt(match[1]!));
} else return "";
return (loopRules + loopStopRules);
}
// endregion
@@ -119,13 +471,13 @@ function getDayInMonth(year: number, month: number, day: number): DayInMonthInfo
type RemanagedDayInMonth = [
/** Count forward by days (Day N) */
forwardByDayCount: number,
forwardByDayCount: number | undefined,
/** Count backward by days (Nth day from the end) */
backwardByDayCount: number,
backwardByDayCount: number | undefined,
/** Count forward by weeks (Week N, Youbi X) */
forwardByWeekCount: number,
forwardByWeekCount: number | undefined,
/** Count backward by weeks (Nth week from the end, Youbi X) */
backwardByWeekCount: number,
backwardByWeekCount: number | undefined,
];
/**
@@ -140,7 +492,7 @@ type RemanagedDayInMonth = [
* @param isStrict
* @returns
*/
function getRemanagedDayInMonth(oldYear: number, oldMonth: number, oldDay: number, newYear: number, newMonth: number, isStrict: boolean) {
function getRemanagedDayInMonth(oldYear: number, oldMonth: number, oldDay: number, newYear: number, newMonth: number, isStrict: boolean): RemanagedDayInMonth {
let ddata = getDayInMonth(oldYear, oldMonth, oldDay);
let mdata = getMonthWeekStatistics(newYear, newMonth);
let days = MONTH_DAY_COUNT[newMonth - 1]! + ((newMonth == 2 && isLeapYear(newYear)) ? 1 : 0);

View File

@@ -2,3 +2,65 @@ export enum Language {
English,
SimplifiedChinese,
}
/**
*
* @param month Zero-based month.
* @returns
*/
export function universalGetMonth(month: number): string {
switch (month) {
case 0:
return "January";
case 1:
return "February";
case 2:
return "March";
case 3:
return "April";
case 4:
return "May";
case 5:
return "June";
case 6:
return "July";
case 7:
return "August";
case 8:
return "September";
case 9:
return "October";
case 10:
return "November";
case 11:
return "December";
default:
return ""
}
}
/**
*
* @param dayOfWeek Zero-based day of week
* @returns
*/
export function universalGetDayOfWeek(dayOfWeek: number): string {
switch (dayOfWeek) {
case 0:
return "Monday";
case 1:
return "Tuesday";
case 2:
return "Wednesday";
case 3:
return "Thursday";
case 4:
return "Friday";
case 5:
return "Saturday";
case 6:
return "Sunday";
default:
return "";
}
}

View File

@@ -10,6 +10,24 @@ export function lcm(a: number, b: number): number {
return a / gcd(a, b) * b;
}
/**
* 将字符串中的 `{n}` 占位符替换为对应的参数值
* @param str 目标字符串,若为 `null`/`undefined`/空字符串则返回原值或空串(根据需求)
* @param args 用于替换的值列表
* @returns 格式化后的字符串
*/
export function format(str: string, ...args: any[]): string {
// 若 str 为假值null/undefined/''),直接返回空字符串
if (!str) return '';
return str.replace(/\{(\d+)\}/g, (match, index) => {
const idx = parseInt(index, 10);
const value = args[idx];
// 若参数存在且不为 null/undefined则转为字符串否则保留占位符
return value != null ? String(value) : match;
});
}
/**
* Get Monday-first the day of week from given date instead of Sunday-first.
* @param date The date for getting weekday.