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: // YYC MARK:
// This file is synchronized with "dt.py". // This file is synchronized with "dt.py".
@@ -30,16 +32,366 @@ const PRECOMPILED_LOOP_STOP_RULES = {
// region: Core Functions // 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 // endregion
@@ -77,11 +429,11 @@ function daysCount(year: number, month: number, day: number): number {
} }
function monthsCount(year: number, month: number): number { function monthsCount(year: number, month: number): number {
return (year - 1) * 12 + (month - 1); return (year - 1) * 12 + (month - 1);
} }
function dayOfWeek(year: number, month: number, day: number): number { function dayOfWeek(year: number, month: number, day: number): number {
return daysCount(year, month, day) % 7; return daysCount(year, month, day) % 7;
} }
type DayInMonthInfo = [ type DayInMonthInfo = [
@@ -100,17 +452,17 @@ type DayInMonthInfo = [
]; ];
function getDayInMonth(year: number, month: number, day: number): DayInMonthInfo { function getDayInMonth(year: number, month: number, day: number): DayInMonthInfo {
let days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0); let days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0);
let firstDayOfWeek = dayOfWeek(year, month, 1); let firstDayOfWeek = dayOfWeek(year, month, 1);
let bilateralDayOfWeek = (firstDayOfWeek + day - 1) % 7; let bilateralDayOfWeek = (firstDayOfWeek + day - 1) % 7;
let dayForwards = day; let dayForwards = day;
let dayBackwards = days - day + 1; let dayBackwards = days - day + 1;
let weeksForward = Math.floor((dayForwards - 1) / 7) + 1; let weeksForward = Math.floor((dayForwards - 1) / 7) + 1;
let weeksBackwards = Math.floor((dayBackwards - 1) / 7) + 1; let weeksBackwards = Math.floor((dayBackwards - 1) / 7) + 1;
return [dayForwards, dayBackwards, weeksForward, bilateralDayOfWeek, weeksBackwards, bilateralDayOfWeek]; return [dayForwards, dayBackwards, weeksForward, bilateralDayOfWeek, weeksBackwards, bilateralDayOfWeek];
} }
// YYC MARK: // YYC MARK:
@@ -119,13 +471,13 @@ function getDayInMonth(year: number, month: number, day: number): DayInMonthInfo
type RemanagedDayInMonth = [ type RemanagedDayInMonth = [
/** Count forward by days (Day N) */ /** Count forward by days (Day N) */
forwardByDayCount: number, forwardByDayCount: number | undefined,
/** Count backward by days (Nth day from the end) */ /** Count backward by days (Nth day from the end) */
backwardByDayCount: number, backwardByDayCount: number | undefined,
/** Count forward by weeks (Week N, Youbi X) */ /** Count forward by weeks (Week N, Youbi X) */
forwardByWeekCount: number, forwardByWeekCount: number | undefined,
/** Count backward by weeks (Nth week from the end, Youbi X) */ /** Count backward by weeks (Nth week from the end, Youbi X) */
backwardByWeekCount: number, backwardByWeekCount: number | undefined,
]; ];
/** /**
@@ -140,37 +492,37 @@ type RemanagedDayInMonth = [
* @param isStrict * @param isStrict
* @returns * @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 ddata = getDayInMonth(oldYear, oldMonth, oldDay);
let mdata = getMonthWeekStatistics(newYear, newMonth); let mdata = getMonthWeekStatistics(newYear, newMonth);
let days = MONTH_DAY_COUNT[newMonth - 1]! + ((newMonth == 2 && isLeapYear(newYear)) ? 1 : 0); let days = MONTH_DAY_COUNT[newMonth - 1]! + ((newMonth == 2 && isLeapYear(newYear)) ? 1 : 0);
let firstDayOfWeek = dayOfWeek(newYear, newMonth, 1); let firstDayOfWeek = dayOfWeek(newYear, newMonth, 1);
//let lastDayOfWeek = (firstDayOfWeek + days - 1) % 7; //let lastDayOfWeek = (firstDayOfWeek + days - 1) % 7;
let methodA = undefined; let methodA = undefined;
let methodB = undefined; let methodB = undefined;
if (isStrict) { if (isStrict) {
methodA = ddata[0] > days ? undefined : ddata[0]; methodA = ddata[0] > days ? undefined : ddata[0];
methodB = ddata[1] > days ? undefined : (days - ddata[1] + 1); methodB = ddata[1] > days ? undefined : (days - ddata[1] + 1);
} else { } else {
methodA = Math.min(ddata[0], days); methodA = Math.min(ddata[0], days);
methodB = days - Math.min(ddata[1], days) + 1; methodB = days - Math.min(ddata[1], days) + 1;
} }
let methodC = undefined; let methodC = undefined;
if (ddata[2] <= mdata[ddata[3]]! || !isStrict) { if (ddata[2] <= mdata[ddata[3]]! || !isStrict) {
let targetWeek = Math.min(ddata[2], mdata[ddata[3]]!); let targetWeek = Math.min(ddata[2], mdata[ddata[3]]!);
methodC = 1 + (targetWeek - 1) * 7 + ((ddata[3] + 7 - firstDayOfWeek) % 7); methodC = 1 + (targetWeek - 1) * 7 + ((ddata[3] + 7 - firstDayOfWeek) % 7);
} }
let methodD = undefined; let methodD = undefined;
if (ddata[4] <= mdata[ddata[5]]! || !isStrict) { if (ddata[4] <= mdata[ddata[5]]! || !isStrict) {
// convert to type c and calc // convert to type c and calc
let targetWeek = mdata[ddata[5]]! - Math.min(ddata[4], mdata[ddata[5]]!) + 1; let targetWeek = mdata[ddata[5]]! - Math.min(ddata[4], mdata[ddata[5]]!) + 1;
methodD = 1 + (targetWeek - 1) * 7 + ((ddata[5] + 7 - firstDayOfWeek) % 7); methodD = 1 + (targetWeek - 1) * 7 + ((ddata[5] + 7 - firstDayOfWeek) % 7);
} }
return [methodA, methodB, methodC, methodD]; return [methodA, methodB, methodC, methodD];
} }
type MonthWeekStatistics = [ type MonthWeekStatistics = [
@@ -191,27 +543,27 @@ type MonthWeekStatistics = [
]; ];
function getMonthWeekStatistics(year: number, month: number): MonthWeekStatistics { function getMonthWeekStatistics(year: number, month: number): MonthWeekStatistics {
let days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0); let days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0);
let firstDayOfWeek = dayOfWeek(year, month, 1); let firstDayOfWeek = dayOfWeek(year, month, 1);
let result: MonthWeekStatistics = [4, 4, 4, 4, 4, 4, 4]; let result: MonthWeekStatistics = [4, 4, 4, 4, 4, 4, 4];
let remain = days % 7; let remain = days % 7;
let week = firstDayOfWeek; let week = firstDayOfWeek;
while (remain > 0) { while (remain > 0) {
result[week % 7]! += 1; result[week % 7]! += 1;
week++; week++;
remain--; remain--;
} }
return result; return result;
} }
function isLeapYear(year: number): boolean { function isLeapYear(year: number): boolean {
let isLeap = false; let isLeap = false;
if (year % 4 == 0) isLeap = true; if (year % 4 == 0) isLeap = true;
if (year % 100 == 0) isLeap = false; if (year % 100 == 0) isLeap = false;
if (year % 400 == 0) isLeap = true; if (year % 400 == 0) isLeap = true;
return isLeap; return isLeap;
} }
// endregion // endregion

View File

@@ -2,3 +2,65 @@ export enum Language {
English, English,
SimplifiedChinese, 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; 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. * Get Monday-first the day of week from given date instead of Sunday-first.
* @param date The date for getting weekday. * @param date The date for getting weekday.