イベント／予定／タスクの終了日時は「非包括的（non-inclusive）」で設計した方が良い

※この記事のサンプルコードはC#やJavaScriptで書かれていますが、記事の内容はプログラミング言語に依らない一般的な設計上の話です。

# 結論

カレンダー系のシステムにおいて、イベントの開始日時と終了日時があるとき、開始日時は「包括的（inclusive）」、**終了日時は「非包括的（non-inclusive）」（又は「排他的（exclusive）」）** なものとして扱った方が良い。

# 解説

開始日時が包括的である、とは、イベントの期間に開始日時が「含まれる」ということだ。
逆に **終了日時が非包括的である、とは、イベントの期間に終了日時が「含まれない」ということ** になる。

例えば、2023年10月23日と10月24日の2日間にわたるイベントがあった場合、システム上は、開始日と終了日を次のようなデータで持つ方が良い。

|フィールド|値|
|-|-|
|開始日時|2023-10-23 00:00:00|
|終了日時|2023-10-25 00:00:00|
|終日フラグ|true|

（※日時はDateTime型とする。GMTの指定はこの記事では考慮しない）

10月24日に終了するイベントなのに、データ上は「10月25日」として持つのは人間の直感に反している感じがするが、システム上はとても都合が良いし、iCalendar、Google Calendar、FullCalendar等多くのカレンダー表示ライブラリはこの考え方で設計されている為、トラブルも起こりにくい。

例えば、ある日時がイベントの期間内かどうかは次の判定式を用いることができる。

```C#
string format = "yyyy-MM-dd HH:mm:ss";

DateTime now = DateTime.Now;
DateTime start = DateTime.ParseExact("2023-10-23 00:00:00", format, CultureInfo.InvariantCulture);
DateTime end = DateTime.ParseExact("2023-10-25 00:00:00", format, CultureInfo.InvariantCulture);

if ( start <= now && now < end )
{
	// 期間内だった場合の処理
}
```

これがもし、終了日時に`2023-10-24 00:00:00`が入っていたり、終了日時に`2023-10-24 23:59:59` を入れておかなくてはならなかったりした場合のことを考えると憂鬱になってくるだろう。

また、イベントの日数や時間を計測する場合にも、単純に引き算をすれば良いので都合が良い。

```C#
TimeSpan diff = end - start;
Console.WriteLine($"{diff.Days}日間のイベントです"); // → 2日間のイベントです
```

# 時間指定のイベントの場合

これまで終日イベントを前提に話してきたが、時間指定の場合にはむしろこの考え方の方が直感的となる。
2023年10月23日の10:00に開始し、10月24日の17:00に終わるイベントを考えてみる。

|フィールド|値|
|-|-|
|開始日時|2023-10-23 10:00:00|
|終了日時|2023-10-24 17:00:00|
|終日フラグ|false|

「イベントは17時に終わる」と聞いて「17:59までやっているだろう」と考える人はあまりいないだろう。17:00にはイベントは終わっているのである。
つまり、「17時」はイベントの期間には含まれない。その時点でイベントは過去のものになっているべき（＝終了日時が期間に含まれない）なのである。

もしかすると「17:00ちょうど」の時間に到着した人が、デジタル時計を指して「ほら！まだ17:00だよ！！」とゴネるケースはあるかもしれない。その場合には、「17:00:00はイベントの期間に含まれるのか？」という定義が必要になる。（実際には、その人が時計を指さしている間に17:00:02になっている為、イベントは100%終わっているのだが）

答えは「含まれない」である。終了日時は「非包括的」なのだ。そうしないと、次のようなケースで困ることになる。

あなたは10月23日に「13時～15時の予定」と「15時～17時の予定」を入れたいとする。

**予定1**
|フィールド|値|
|-|-|
|開始日時|2023-10-23 13:00:00|
|終了日時|2023-10-23 15:00:00|
|終日フラグ|false|

**予定2**
|フィールド|値|
|-|-|
|開始日時|2023-10-23 15:00:00|
|終了日時|2023-10-23 17:00:00|
|終日フラグ|false|

もし終了日時が「包括的」つまり「期間に含まれる」としてしまうと、あなたは「15:00:00」の瞬間に同時に異なる予定に「ダブルブッキング」している扱いになってしまう。それを避ける為、予定1の終了日時と予定2の開始日時を比較して一致しているならOKとする、みたいな例外的処理を入れるのも面倒だ。

しかし、終了日時が「非包括的」として扱われていれば、上記の予定もキッチリ、問題なく入力できる。

# まとめ

あなたが構築するシステムで「予定」だとか「イベント」だとか「スケジュール」のような概念を扱うことになったならば、終了日時は「非包括的（排他的）」にした方が幸せになれるだろう。

但し、ユーザーに終了日時を表示する場合、特に「終日」イベントの場合には、直感に反しないよう見せ方を工夫した方が良い。


# 参考

## Google Calendarのサンプルコード

```JavaScript:Google Calendar
const gapi = require('gapi-client');

let event = {
  'summary': '2-day event',
  'start': {
    'date': '2023-10-23',
    'timeZone': 'Asia/Tokyo',
  },
  'end': {
    'date': '2023-10-25',  // ここも排他的な日付です
    'timeZone': 'Asia/Tokyo',
  },
};

gapi.client.calendar.events.insert({
  'calendarId': 'primary',
  'resource': event
}).then((response) => {
  console.log('Event created: ' + response.result.htmlLink);
});
```

## FullCalendarのサンプルコード

```JavaScript:FullCalendar
let calendar = new FullCalendar.Calendar(calendarEl, {
  initialView: 'dayGridMonth',
  timeZone: 'Asia/Tokyo',
  events: [
    {
      title: '2-day event',
      start: '2023-10-23',
      end: '2023-10-25',  // ここも排他的な日付です
      allDay: true
    }
  ]
});

calendar.render();
```