240 lines
7.4 KiB
Plaintext
240 lines
7.4 KiB
Plaintext
// 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();
|
|||
|
|
}
|
|||
|
|
}
|