期間の重複チェック(LocalTime, LocalDate, LocalDateTime)

毎回ロジックを考えている気がするのでメモしておく。

- 期間の終了がnull (つまり開始からこの先ずっと対象期間) というパターンもよくあるので、それにも対応。
- 「10:00 ~ 11:00」と「11:00 ~ 12:00」のような、期間1の終了と期間2の開始がぴったり一致している時も重複あり判定にしている。このパターンを重複なし判定にする場合は、 `!start2.isAfter(end1)` を `start2.isBefore(end1)`に、 `!end2.isBefore(start1)` を `end2.isAfter(start1)` にする。



```scala
 /**
   * 期間の重複チェック(LocalTime版)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 10:00 ~ 11:00
   *  期間2 => 11:00 ~ 12:00
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1 期間1の終了
   * @param start2 期間2の開始
   * @param end2 期間2の終了
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalTime, end1: LocalTime, start2: LocalTime, end2: LocalTime): Boolean =
    !start2.isAfter(end1) && !end2.isBefore(start1)

  /**
   * 期間の重複チェック(LocalTime版, 期間終了Option可)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 10:00 ~ 11:00
   *  期間2 => 11:00 ~ 12:00
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1Opt 期間1の終了 (Noneなら開始以降ずっと対象期間)
   * @param start2 期間2の開始
   * @param end2Opt 期間2の終了 (Noneなら開始以降ずっと対象期間)
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalTime, end1Opt: Option[LocalTime], start2: LocalTime, end2Opt: Option[LocalTime]): Boolean =
    end1Opt.fold(true)(!start2.isAfter(_)) && end2Opt.fold(true)(!_.isBefore(start1))

  /**
   * 期間の重複チェック(LocalDate版)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 2020-10-01 ~ 2020-10-10
   *  期間2 => 2020-10-10 ~ 2020-10-20
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1 期間1の終了
   * @param start2 期間2の開始
   * @param end2 期間2の終了
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalDate, end1: LocalDate, start2: LocalDate, end2: LocalDate): Boolean =
    !start2.isAfter(end1) && !end2.isBefore(start1)

  /**
   * 期間の重複チェック(LocalDate版, 期間終了Option可)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 2020-10-01 ~ 2020-10-10
   *  期間2 => 2020-10-10 ~ 2020-10-20
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1Opt 期間1の終了 (Noneなら開始以降ずっと対象期間)
   * @param start2 期間2の開始
   * @param end2Opt 期間2の終了 (Noneなら開始以降ずっと対象期間)
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalDate, end1Opt: Option[LocalDate], start2: LocalDate, end2Opt: Option[LocalDate]): Boolean =
    end1Opt.fold(true)(!start2.isAfter(_)) && end2Opt.fold(true)(!_.isBefore(start1))

  /**
   * 期間の重複チェック(LocalDateTime版)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 2020-10-01 10:00:00 ~ 2020-10-10 20:00:00
   *  期間2 => 2020-10-10 20:00:00 ~ 2020-10-20 20:00:00
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1 期間1の終了
   * @param start2 期間2の開始
   * @param end2 期間2の終了
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalDateTime, end1: LocalDateTime, start2: LocalDateTime, end2: LocalDateTime): Boolean =
    !start2.isAfter(end1) && !end2.isBefore(start1)

  /**
   * 期間の重複チェック(LocalDateTime版, 期間終了Option可)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 2020-10-01 10:00:00 ~ 2020-10-10 20:00:00
   *  期間2 => 2020-10-10 20:00:00 ~ 2020-10-20 20:00:00
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1Opt 期間1の終了 (Noneなら開始以降ずっと対象期間)
   * @param start2 期間2の開始
   * @param end2Opt 期間2の終了 (Noneなら開始以降ずっと対象期間)
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalDateTime, end1Opt: Option[LocalDateTime], start2: LocalDateTime, end2Opt: Option[LocalDateTime]): Boolean =
    end1Opt.fold(true)(!start2.isAfter(_)) && end2Opt.fold(true)(!_.isBefore(start1))
```