はじめに
今回はカレンダーの繰り返し予約のRRULEを学んでいきます。カレンダーで、「このMTGは8月まで毎日10時〜11時に設定する」などで利用するあれです!繰り返し予約は全部DBに保存しておくのではなく、ルールだけ保存しておいてフロント側で展開しています。
そもそもRRULEとは何か?
カレンダーアプリで「毎週の会議」や「毎月の締め切り」のような繰り返し予定を扱いたいときに使われるのが RRULE
(Recurrence Rule)です。
これはRFC 5545(iCalendar仕様)で定義されている、繰り返しルールを表すフォーマットです。
基本構文
RRULE
(繰り返しルール)は、カレンダーイベントの繰り返しを定義する文字列で、以下のような形式で記述します。
RRULE:FREQ=FREQUENCY;INTERVAL=n;COUNT=x;UNTIL=yyyyMMddTHHmmssZ;BYxxx=…
パラメータ | 説明 | 例 |
---|---|---|
FREQ |
繰り返しの頻度(必須) |
FREQ=WEEKLY → 毎週 |
INTERVAL |
繰り返しの間隔。省略時は1 |
INTERVAL=2 → 2週ごと |
COUNT |
繰り返す回数 |
COUNT=10 → 10回繰り返して終了 |
UNTIL |
繰り返しの終了日時(UTC形式) | UNTIL=20251231T235959Z |
BYDAY |
曜日を指定(MO =月曜、TU =火曜…) |
BYDAY=MO,WE → 月・水曜に繰り返す |
BYMONTHDAY |
月内の日付を指定 |
BYMONTHDAY=15 → 毎月15日に実行 |
BYMONTH |
月を指定 |
BYMONTH=1,7 → 1月と7月のみ実行 |
EXDATE |
繰り返しから除外する日付(UTC形式でカンマ区切り) |
EXDATE=20240510T000000Z → 5/10を除外 |
実践
react-big-calendar
で実践してみましょう!
環境作成
~/develop/rrule-big-calendar-demo (main)$ tree src/
src/
├── App.css
├── App.tsx
├── assets
│ └── react.svg
├── index.css
├── lib
│ ├── localizer.ts
│ └── rruleHelpers.ts
├── main.tsx
└── vite-env.d.ts
3 directories, 8 files
import { useMemo, useState } from "react";
import { Calendar } from "react-big-calendar";
import "react-big-calendar/lib/css/react-big-calendar.css";
import { rruleToEvents } from "./lib/rruleHelpers";
import { localizer } from "./lib/localizer";
// rruleパターン生成関数
function getRRulePattern(): string {
// JST 2024年5月1日 9:00 → UTC 2024年5月1日 0:00
const dtstart = "20240501T000000Z";
return `DTSTART:${dtstart}\nRRULE:FREQ=DAILY;COUNT=365`;
}
function App() {
const [date, setDate] = useState(new Date("2024-05-01T00:00:00Z"));
const rrule = getRRulePattern();
console.log("rrule", rrule);
// 2時間(120分)のイベント
const events = useMemo(
() => rruleToEvents(rrule, 120, "毎日のイベント"),
[rrule],
);
return (
<div style={{ height: "100vh", width: "100vw", margin: 0, padding: 0 }}>
<Calendar
localizer={localizer}
events={events}
defaultView="week"
date={date}
onNavigate={setDate}
style={{ height: "100%", width: "100%" }}
/>
</div>
);
}
export default App;
import { format, parse, startOfWeek, getDay } from "date-fns";
import { ja } from "date-fns/locale";
import { dateFnsLocalizer } from "react-big-calendar";
export const localizer = dateFnsLocalizer({
format,
parse,
startOfWeek,
getDay,
locales: { ja },
});
import { RRule } from "rrule";
import type { Event } from "react-big-calendar";
/**
* RRULE 文字列を react-big-calendar の events[] に展開
*/
export function rruleToEvents(
rruleString: string,
durationMinutes: number,
title: string,
): Event[] {
// RRULE文字列をパース
const rrule = RRule.fromString(rruleString);
// 繰り返しの日付を生成
const dates = rrule.all();
// イベントの配列を生成
return dates.map((date, index) => ({
id: index,
title,
start: date,
end: new Date(date.getTime() + durationMinutes * 60000),
}));
}
では実践していきます!!
パターン1:平日のみ毎日イベント
DTSTART:20240429T000000Z RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;COUNT=260
// rruleパターン生成関数
function getRRulePattern(): string {
// JST 2024年4月29日 9:00 → UTC 2024年4月29日 0:00
const dtstart = "20240429T000000Z";
return `DTSTART:${dtstart}\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;COUNT=260`;
}
→土曜と日曜が表示されなくなります。
パターン2:毎月第1月曜に定例会議を表示
// rruleパターン生成関数
function getRRulePattern(): string {
// 2024年5月6日(月)9:00 JST → 0:00 UTC
const dtstart = "20240506T000000Z";
return `DTSTART:${dtstart}\nRRULE:FREQ=MONTHLY;BYDAY=1MO;COUNT=12`;
}
→余談ですが、COUNT=12
をつけないと白飛びします。原因は無限に繰り返そうとして、メモリを食い切ることが原因のようです。繰り返しの範囲を指定する方法もあるんですが、一旦置いておきましょう。
パターン3:期間限定の毎週水曜キャンペーン(6月〜8月)
// rruleパターン生成関数(夏のキャンペーン:毎週水曜)
function getRRulePattern(): string {
// JST 2024年6月5日(水)9:00 → UTC 0:00
const dtstart = "20240605T000000Z";
const until = "20240828T000000Z";
return `DTSTART:${dtstart}\nRRULE:FREQ=WEEKLY;BYDAY=WE;UNTIL=${until}`;
}
パターン4:特定の日だけ繰り返しを除外する
DTSTART:20240605T000000ZRRULE:FREQ=WEEKLY;BYDAY=WE;UNTIL=20240828T000000ZEXDATE:20240612T000000Z