【Java】日付の期間重複チェックサンプル

# 概要
日付で期間を持ってるデータを登録するときにすでに登録してある期間と重複しないようにしたい。
LocalDateを使ったJavaのサンプルがあまり見当たらなかったので公開します。

# 環境
- Java 1.8
- SpringBoot 2.2.1.RELEASE
- thymeleaf 3.0.11.RELEASE

# 期間が重複するパターン
日付の期間が重複するパターンは全部で4つ。
![Qiita用画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265229/f75d6dcf-99ee-6348-7c16-33354dc2cd73.png)
1. 前半がかぶるパターン(①の終了日と緑の開始日が同じも含む)
2. 後半がかぶるパターン(②の開始日と緑の終了日が同じも含む)
3. 全部かぶるパターン(開始日と終了日が全く同じも含む)
4. 一部がかぶるパターン

全パターンを網羅できる条件式は下記の通り。

```java:条件式
緑.開始日 <= 黒.終了日 && 緑.終了日 => 黒.開始日
```

# サンプル
画面から開始日と終了日を入力して、その入力期間が既存の期間(複数)と重複していない場合だけ登録できるサンプル。

## 登録する日付期間を持つModelクラス
登録する開始日と終了日、自動採番のIDを持つ画面に渡すModelクラス。

```Java:DurationModel.java
package com.tamorieeeen.sample.model;

import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 *
 * @author tamorieeeen
 *
 */
@Getter
@Setter
@NoArgsConstructor
public class DurationModel {

    // 保存時にauto_incrementで採番
    private int id;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
```

## 期間重複判定ロジックServiceクラス
Controllerから呼び出されるServiceクラス。

```Java:DurationService.java
package com.tamorieeeen.sample.service;

import java.time.LocalDate;
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.stereotype.Service;
import com.tamorieeeen.sample.model.DurationModel;

/**
 *
 * @author tamorieeeen
 *
 */
@Service
public class DurationService {

    /**
     * すでに登録済の期間とかぶってないかチェック
     */
    public boolean isInvalid(DurationModel model) {

        return this.getDurationList()
                .stream()
                .filter(u -> model.getId() != u.getId()) // ※1
                .anyMatch(u ->
                        (model.getStartDate().isBefore(u.getEndDate())
                        && model.getEndDate().isAfter(u.getStartDate()))
                        || model.getStartDate().isEqual(u.getEndDate())
                        || model.getEndDate().isEqual(u.getStartDate()));
    }

    /**
     * 一覧を取得
     */
    private List<DurationModel> getDurationList() {

        // TODO すでに登録済のデータを取得
    }

    /**
     * 新規登録/更新
     */
    @Transactional
    public void saveDuration(DurationModel model) {

        // TODO DBなどへのデータ保存処理
    }
}
```
※1: 比較元(model)のIDは期間チェックを除外する
こうしないとデータ更新時に`this.getDurationList()`に比較元データも含まれているため重複扱いでvalidationに引っかかってしまうため。
新規登録しか考えないのであれば、この行は不要。

## Controllerクラス
実際はバリデーションチェックに`@Validated`と`BindingResult`を使ってるけどその部分は省略。

```Java:DurationController.java
/**
 *
 * @author tamorieeeen
 *
 */
@Controller
public class DurationController {

    @Autowired
    private DurationService durationService;

    /**
     * 新規登録
     */
    @GetMapping("/duration/register")
    public String register(Model model) {

        model.addAttribute("duration", new DurationModel());

        return "duration/register";
    }

    /**
     * 新規登録処理
     */
    @PostMapping("/duration/register")
    public String registerComplete(Model model,
            @ModelAttribute("duration") DurationModel duration,
            RedirectAttributes redirect) {

        // バリデーションチェック
        if (durationService.isInvalid(duration)) {

            model.addAttribute("invalid", true);

            return "duration/register";
        }

        durationService.saveDuration(duration);

        redirect.addFlashAttribute("complete", true);

        return "redirect:/duration/register";
    }
}
```

## 画面側html(thymeleaf)
htmlヘッダーは共通化しているが今回は関係ないので省略。気になる人は [Thymeleafでヘッダーフッターを共通化する方法](https://qiita.com/tamorieeeen/items/4bc818e18f4ff8bdf0c2) を参考にしてください。

```html:register.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="common :: meta_header('sample',~{::link},~{::script},~{::meta})">
</head>
<body>
    <div th:if="${complete}">
        <p>期間を登録しました。</p>
    </div>
    <div th:if="${invalid}">
        <p>期間が重複しているため、登録できません。</p>
    </div>
    <form th:action="@{/duration/register}" method="post" th:object="${duration}">
        <table>
            <tr><td>開始日</td><td>
                <input type="date" th:field="*{startDate}" th:value="*{startDate}" />
            </td></tr>
            <tr><td>終了日</td><td>
                <input type="date" th:field="*{endDate}" th:value="*{endDate}" />
            </td></tr>
        </table>
        <input type="button" th:value="登録する" onclick="submit();" />
    </form>
</body>
</html>
```

# 参考
- [日付期間の重複チェック](https://qiita.com/yaju/items/a58a78f41ee41258a5fe)