2つの時刻の間でn時を跨いだか判定する関数

# 概要
オンラインゲームではよく、画面遷移時にある時間を跨いでいたら更新が入る仕様があります。
そのような実装に使える関数をC#で書きました。

# コード

``` C#
/// <summary>
/// beforeTime と afterTime の間で、targetHour を跨いだか判定する
/// </summary>
/// <returns>跨いだ</returns>
public static bool CheckCrossedHour(DateTime beforeTime, DateTime afterTime, int targetHour)
{
    if ((afterTime - beforeTime).TotalMilliseconds < 0)
    {
        Console.WriteLine("beforeTime が afterTime の時刻を上回っています。 beforeTime[" + beforeTime + "] afterTime[" + afterTime + "]");
        return false;
    }

    // 1日以上経過していたら、確実に targetHour を跨いでいる
    if ((afterTime - beforeTime).Days >= 1) return true;
    // targetHour を0～23時の間にする
    targetHour %= 24;

    // beforeTime が targetHour を跨ぐ前
    if (beforeTime.Hour < targetHour)
    {
        // afterTime が targetHour を跨いだか、afterTime.Hour が beforeTime.Hour よりが小さければ、targetHour を跨いでいる
        return afterTime.Hour >= targetHour || afterTime.Hour < beforeTime.Hour;
    }
    // afterTime も targetHour を跨いでいて、afterTime.Hour が beforeTime.Hour よりが小さければ、targetHour を跨いでいる
    return afterTime.Hour >= targetHour && afterTime.Hour < beforeTime.Hour;
}
```

# 解説

下記のコード部分はコメントに書いてあるままなので、解説を省略します。

``` C#
if ((afterTime - beforeTime).TotalMilliseconds < 0)
{
    Console.WriteLine("beforeTime が afterTime の時刻を上回っています。 beforeTime[" + beforeTime + "] afterTime[" + afterTime + "]");
    return false;
}
// 1日以上経過していたら、確実に targetHour を跨いでいる
if ((afterTime - beforeTime).Days >= 1) return true;
// targetHour を0～23時の間にする
targetHour %= 24;
```

ここから先は、beforeTime と afterTime の差が24時間未満の場合の処理になります。
話がややこしくなるので、targetHour を4時に置き換えて解説します。

4時を跨いでいるか判定する際にまず思い浮かべるのは、下記のようなコードだと思います。

``` C#
if (beforeTime.Hour < 4)
{
    return afterTime.Hour >= 4;
}
```

一見、beforeTime.Hour が4時より前であれば上記の条件式で良い気もしますが、afterTime.Hour が翌日の2時を指している場合に対応できません。
afterTime.Hour が翌日の時刻を指している場合、beforeTime と afterTime の差は24時間未満なので、afterTime.Hour は beforeTime.Hour より小さい値になっているはずです。
そこで、return の条件式に afterTime.Hour < beforeTime.Hour を追加すれば、afterTime.Hour が翌日の時刻を指していても対応できるようになります。

``` C#
if (beforeTime.Hour < 4)
{
    return afterTime.Hour >= 4 || afterTime.Hour < beforeTime.Hour;
}
```

ここまでで、beforeTime.Hour が4時より前の場合は正常に動作するようになりました。
次に、beforeTime.Hour が4時を超えている場合を考えます。

beforeTime.Hour が4時を超えているときに afterTime.Hour が4時を跨ぐためには、afterTime.Hour が翌日の時刻を指している必要があります。
その上、afterTime.Hour も4時を超えている必要があります。
それを踏まえると、条件式は下記のようになります。

``` C#
if (beforeTime.Hour < 4)
{
    return afterTime.Hour >= 4 || afterTime.Hour < beforeTime.Hour;
}

return afterTime.Hour >= 4 && afterTime.Hour < beforeTime.Hour;
```

これで、beforeTime と afterTime が4時を跨いだか判定できるようになりました。
最後に、4を targetHour に置き換えれば、指定した時間を跨いだか判定できるようになります。

# 動作テスト

色々な時刻で想定通りに動くか確認するため、Unityで下記のコードを書いて試しました。

``` テストコード.cs
using System;
using UnityEngine;

public class CheckCrossedHourTest : MonoBehaviour
{
    void Start()
    {
        Test("昨日の同じ時刻（エラーが出る）", false,
            new DateTime(2000, 02, 02, 03, 00, 00),
            new DateTime(2000, 02, 01, 03, 00, 00));
        Test("翌日の同じ時刻", true,
            new DateTime(2000, 02, 02, 03, 00, 00),
            new DateTime(2000, 02, 03, 03, 00, 00));
        Test("4時になる瞬間", true,
            new DateTime(2000, 02, 02, 03, 59, 59),
            new DateTime(2000, 02, 02, 04, 00, 00));
        Test("4時を過ぎた瞬間", false,
            new DateTime(2000, 02, 02, 04, 00, 00),
            new DateTime(2000, 02, 02, 04, 00, 01));
        Test("同日に4時を跨ぐ", true,
            new DateTime(2000, 02, 02, 03, 00, 00),
            new DateTime(2000, 02, 02, 05, 00, 00));
        Test("翌日に4時を跨ぐ", true,
            new DateTime(2000, 02, 02, 03, 00, 00),
            new DateTime(2000, 02, 03, 05, 00, 00));
        Test("翌日に4時を跨ぐ（双方4時以上）", true,
            new DateTime(2000, 02, 02, 06, 00, 00),
            new DateTime(2000, 02, 03, 05, 00, 00));
        Test("翌日に4時を跨ぐ（双方4時未満）", true,
            new DateTime(2000, 02, 02, 03, 00, 00),
            new DateTime(2000, 02, 03, 02, 00, 00));
        Test("同日に4時を跨がない（双方4時未満）", false,
            new DateTime(2000, 02, 02, 02, 00, 00),
            new DateTime(2000, 02, 02, 03, 00, 00));
        Test("同日に4時を跨がない（双方4時以上）", false,
            new DateTime(2000, 02, 02, 04, 00, 00),
            new DateTime(2000, 02, 02, 05, 00, 00));
        Test("翌日に4時を跨がない", false,
            new DateTime(2000, 02, 02, 05, 00, 00),
            new DateTime(2000, 02, 03, 03, 00, 00));
    }

    private void Test(string comment, bool assumption, DateTime beforeTime, DateTime afterTime)
    {
        Debug.Log(beforeTime + " / " + afterTime + " " + (CheckCrossedHour(beforeTime, afterTime, 4) == assumption) + " " + comment);
    }

    /// <summary>
    /// beforeTime と afterTime の間で、targetHour を跨いだか判定する
    /// </summary>
    /// <returns>跨いだ</returns>
    public static bool CheckCrossedHour(DateTime beforeTime, DateTime afterTime, int targetHour)
    {
        if ((afterTime - beforeTime).TotalMilliseconds < 0)
        {
            Debug.LogError("beforeTime が afterTime の時刻を上回っています。 beforeTime[" + beforeTime + "] afterTime[" + afterTime + "]");
            return false;
        }

        // 1日以上経過していたら、確実に targetHour を跨いでいる
        if ((afterTime - beforeTime).Days >= 1) return true;
        // targetHour を0～23時の間にする
        targetHour %= 24;

        // beforeTime が targetHour を跨ぐ前
        if (beforeTime.Hour < targetHour)
        {
            // afterTime が targetHour を跨いだか、afterTime.Hour が beforeTime.Hour よりが小さければ、targetHour を跨いでいる
            return afterTime.Hour >= targetHour || afterTime.Hour < beforeTime.Hour;
        }
        // afterTime も targetHour を跨いでいて、afterTime.Hour が beforeTime.Hour よりが小さければ、targetHour を跨いでいる
        return afterTime.Hour >= targetHour && afterTime.Hour < beforeTime.Hour;
    }
}
```

### テスト結果

全てのテストで想定通りの結果になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307985/f3eb7e28-50c7-7136-75ba-463ce72aeed9.png)