[kotlinメモ][LocalDate] 期間が連続しているかを判定したい


2021/07/14追記

↑かなりスマートにまとまった改善案がありますので、
是非そちらをご覧ください。


はじめに

期間のリストに対し、リスト内の個々の期間を結合した際に歯抜けが出ないかを確認するものです。
リスト内での期間の重なりは許容するものとします。
例1: 1/1~1/151/16~1/30 は連続しているのでOKとする
例2: 1/1~1/151/12~1/30 は一部重なってるけどOKとする
例3: 1/1~1/151/17~1/30 は歯抜け(1/16)があるのでNGとする

用語定義

  • 期間
    • ここでは java.time.LocalDate 型の開始日と終了日のペアのこと
// 例
    // String->LocalDate変換を逐一書くのが面倒なので拡張関数化します
    val formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd")
    val String.toLocalDate(): LocalDate = LocalDate.parse(
        this, formatter.withResolverStyle(ResolverStyle.STRICT)
    )

    // 期間のリスト
    val ranges = listOf(
        "2020-01-01".toLocalDate() to "2020-01-02".toLocalDate(),
        "2020-01-03".toLocalDate() to "2020-01-04".toLocalDate(),
        "2020-01-05".toLocalDate() to "2020-01-31".toLocalDate(),
        "2020-02-01".toLocalDate() to "2020-02-28".toLocalDate()
    )

コード

    /** 複数の期間が重なるか連続する */
    fun overlaps(vararg ranges: Pair<LocalDate, LocalDate>): Boolean {
        if (ranges.size < 2) return true
        ranges.sortBy { (start, _) -> start }
        if (overlaps(ranges[0], ranges[1])) {
            val (aStart, aEnd) = ranges[0]
            val (_, bEnd) = ranges[1]
            val laterEnd = if (aEnd.isAfter(bEnd)) aEnd else bEnd
            return overlaps(
                Pair(aStart, laterEnd),
                *ranges.slice(2 until ranges.size)
                    .toTypedArray()
            )
        }
        return false
    }

    /** 2つの期間が重なるか連続する */
    fun overlaps(
        aRange: Pair<LocalDate, LocalDate>, bRange: Pair<LocalDate, LocalDate>
    ): Boolean {
        val (aStart, aEnd) = aRange
        val (bStart, bEnd) = bRange
        // 期間Aの開始日 <= 期間Bの終了日+1 && 期間Bの開始日 <= 期間Aの終了日+1
        return aStart.isBefore(bEnd.plusDays(2)) 
            && bStart.isBefore(aEnd.plusDays(2))
    }

考え方

説明下手なので図示します。
色違いの帯が、それぞれの期間を表しています。
帯の左端が開始日、右端が終了日と考えてください。

まず開始日が若い順にソートします。

期間リストからindex0とindex1を抜き出し、連続あるいは重複していることを確認して結合します。
この際、期間に歯抜けがあったら中断してfalseを返します。
結合できたら、結合した期間とindex2以降の残りの期間とでリストを作り直し、
そのリストに対して上記の結合操作を、リストのサイズが1になるまで続けます。

リストサイズが1になるまで無事結合できれば、連続しているとみなしてtrueを返します。

最後に

まず間違いなく、これよりいい手法が存在すると思います。色々イマイチです。
が、探した範囲では見つからなかったので、自前で組みました。
そもそも需要が皆無なのかもしれない

改良案・代替案等ありましたら、どうぞよろしくおねがいします。