Files
SyncCalendar/entry/src/main/ets/common/RruleUtil.ets
T

240 lines
7.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// entry/src/main/ets/common/RruleUtil.ets
// RRULE 展开:支持 DAILY / WEEKLY / MONTHLY / YEARLY
// 支持 INTERVAL、COUNT、UNTIL、BYDAY(周重复,如 MO,WE,FR)、BYMONTHDAY(月重复)
export const DAY_MS: number = 86400000;
export class RruleUtil {
/**
* 展开重复规则,返回窗口内的发生时刻(毫秒时间戳)。
* @param rrule 原始 RRULE 值(如 FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,TH
* @param dtstart 首次发生时间(毫秒)
* @param windowStart 窗口起点(毫秒)
* @param windowEnd 窗口终点(毫秒)
* @param exdates EXDATE 排除时刻列表(毫秒),按整天或精确值匹配
* @param maxCount 结果数量上限(防止超大结果)
*/
static expand(rrule: string, dtstart: number, windowStart: number, windowEnd: number,
exdates: number[], maxCount: number): number[] {
const result: number[] = [];
if (rrule.trim() === '') {
return result;
}
let freq: string = '';
let interval: number = 1;
let count: number = -1;
let until: number = 0;
let byday: string[] = [];
let bymonthday: number[] = [];
const parts: string[] = rrule.split(';');
for (const part of parts) {
const idx: number = part.indexOf('=');
if (idx <= 0) {
continue;
}
const key: string = part.substring(0, idx).trim().toUpperCase();
const value: string = part.substring(idx + 1).trim();
if (key === 'FREQ') {
freq = value.toUpperCase();
} else if (key === 'INTERVAL') {
interval = Math.max(1, Number(value));
} else if (key === 'COUNT') {
count = Number(value);
} else if (key === 'UNTIL') {
until = RruleUtil.parseUntil(value);
} else if (key === 'BYDAY') {
byday = value.split(',').map((s: string): string => s.trim().toUpperCase());
} else if (key === 'BYMONTHDAY') {
bymonthday = value.split(',').map((s: string): number => Number(s.trim()));
}
}
if (freq === '' || Number.isNaN(interval) || interval < 1) {
return result;
}
const base = new Date(dtstart);
const baseZero = new Date(dtstart);
baseZero.setHours(0, 0, 0, 0);
const baseZeroMs: number = baseZero.getTime();
const timeOfDay: number = dtstart - baseZeroMs;
let generated: number = 0;
// 接受一个发生:处理 COUNT/UNTIL/EXDATE/窗口过滤
const accept = (occ: number): boolean => {
if (count > 0 && generated >= count) {
return false;
}
if (until > 0 && occ > until) {
return false;
}
generated++;
if (occ >= dtstart && occ <= windowEnd + DAY_MS - 1000 && occ >= windowStart - 40 * DAY_MS) {
const occDay: number = RruleUtil.dayFloor(occ);
let excluded: boolean = false;
for (const ex of exdates) {
if (ex === occ || RruleUtil.dayFloor(ex) === occDay) {
excluded = true;
break;
}
}
if (!excluded && result.length < maxCount) {
result.push(occ);
}
}
return true;
};
if (freq === 'DAILY') {
let occ: number = dtstart;
let guard: number = 0;
while (occ <= windowEnd + DAY_MS && guard < 5000) {
guard++;
if (!accept(occ)) {
break;
}
const d = new Date(occ);
d.setDate(d.getDate() + interval);
occ = d.getTime();
}
} else if (freq === 'WEEKLY') {
const targets: number[] = [];
if (byday.length > 0) {
for (const code of byday) {
const wd: number = RruleUtil.weekdayFromCode(code);
if (wd >= 0) {
targets.push(wd);
}
}
}
if (targets.length === 0) {
targets.push(base.getDay());
}
const anchorWeek: number = RruleUtil.weekStartMs(dtstart);
let dayMs: number = baseZeroMs;
let guard: number = 0;
while (dayMs <= windowEnd + DAY_MS && guard < 5000) {
guard++;
const wd: number = new Date(dayMs).getDay();
if (targets.includes(wd)) {
const weekIndex: number =
Math.round((RruleUtil.weekStartMs(dayMs) - anchorWeek) / (7 * DAY_MS));
if (weekIndex % interval === 0) {
if (!accept(dayMs + timeOfDay)) {
break;
}
}
}
dayMs += DAY_MS;
}
} else if (freq === 'MONTHLY') {
let y: number = base.getFullYear();
let m: number = base.getMonth();
let guard: number = 0;
while (guard < 1500) {
guard++;
const monthFirst: number = new Date(y, m, 1).getTime();
if (monthFirst > windowEnd + DAY_MS) {
break;
}
const days: number[] = (bymonthday.length > 0 && !bymonthday.some((v: number): boolean => Number.isNaN(v)))
? bymonthday : [base.getDate()];
let stop: boolean = false;
for (const md of days) {
const dim: number = new Date(y, m + 1, 0).getDate();
if (md >= 1 && md <= dim) {
const occ: number = new Date(y, m, md,
base.getHours(), base.getMinutes(), base.getSeconds()).getTime();
if (!accept(occ)) {
stop = true;
break;
}
}
}
if (stop) {
break;
}
m += interval;
while (m > 11) {
m -= 12;
y++;
}
}
} else if (freq === 'YEARLY') {
let y: number = base.getFullYear();
let guard: number = 0;
while (guard < 300) {
guard++;
const occ: number = new Date(y, base.getMonth(), base.getDate(),
base.getHours(), base.getMinutes(), base.getSeconds()).getTime();
if (occ > windowEnd + DAY_MS) {
break;
}
if (!accept(occ)) {
break;
}
y += interval;
}
}
return result;
}
/** 当天 0 点 */
static dayFloor(ms: number): number {
const d = new Date(ms);
d.setHours(0, 0, 0, 0);
return d.getTime();
}
/** 所在周的周一 0 点 */
static weekStartMs(ms: number): number {
const d = new Date(ms);
d.setHours(0, 0, 0, 0);
const offset: number = (d.getDay() + 6) % 7;
return d.getTime() - offset * DAY_MS;
}
/** BYDAY 代码 → getDay() 星期值(0=周日);2TU 等带序号的取后两位 */
static weekdayFromCode(code: string): number {
if (code.startsWith('MO')) {
return 1;
}
if (code.startsWith('TU')) {
return 2;
}
if (code.startsWith('WE')) {
return 3;
}
if (code.startsWith('TH')) {
return 4;
}
if (code.startsWith('FR')) {
return 5;
}
if (code.startsWith('SA')) {
return 6;
}
if (code.startsWith('SU')) {
return 0;
}
return -1;
}
/** UNTIL 值:20260913T235959Z / 20260913T235959 / 20260913 */
private static parseUntil(value: string): number {
const v: string = value.trim().toUpperCase();
if (/^\d{8}$/.test(v)) {
return new Date(Number(v.substring(0, 4)), Number(v.substring(4, 6)) - 1,
Number(v.substring(6, 8)), 23, 59, 59).getTime();
}
const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z?$/.exec(v);
if (m === null) {
return 0;
}
if (v.endsWith('Z')) {
return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]),
Number(m[4]), Number(m[5]), Number(m[6]));
}
return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]),
Number(m[4]), Number(m[5]), Number(m[6])).getTime();
}
}