fix: fix type error in datetime module
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user