C#で日時と日時の期間を保持して比較したりするクラス

# はじめに

開始日時～終了日時を保持しておこうとするとわりと面倒です。
期間を比較したりするとさらに面倒です。

取り回しやすいように`DateTimePeriod`クラスを作成したのでメモ代わりに残します。

ついでに比較用の時刻の切り捨ても実装しています。
（秒単位で切り捨てて比較したい場合があった）

```CSharp:使い方
static void Main(string[] args)
{
    DateTime now = DateTime.Now;
    DateTime after = now.AddMinutes(19);
    DateTime target = new DateTime(2020, 2, 20);

    // 期間を作成
    DateTimePeriod period = new DateTimePeriod(now, after, TruncateLevel.Minute);
    DateTimePeriod period2 = new DateTimePeriod(after.AddHours(-10), after.AddHours(1), TruncateLevel.Minute);

    // periodの期間内にtargetの日付は含まれるか
    bool contains = period.Contains(target);

    // period1とperiod2に交差or内包する期間はあるか
    bool intersects = period.Intersects(period2);

    // 期間をそれぞれの単位で取得
    double hours = period.Hours; // 時間
    double minutes = period.Minutes; // 分
}
```

```CSharp:DateTimePeriod.cs
using System;
using System.Diagnostics;

namespace NameSpace
{
    /// <summary>
    /// 期間を保持するクラス
    /// </summary>
    [DebuggerDisplay("{TruncatedSince.ToString(\"MM/dd HH:mm:ss\")} ～ {TruncatedUntil.ToString(\"MM/dd HH:mm:ss\")} [ {Level} ]")]
    public class DateTimePeriod
    {
        /// <summary>開始日時</summary>
        public DateTime Since { get; }

        /// <summary>終了日時</summary>
        public DateTime Until { get; }

        /// <summary>切り捨て粒度</summary>
        public TruncateLevel Level { get; }

        /// <summary>開始日時(切り捨て)</summary>
        public DateTime TruncatedSince { get => Truncate(Since, Level); }

        /// <summary>終了日時(切り捨て)</summary>
        public DateTime TruncatedUntil { get => Truncate(Until, Level); }

        /// <summary>期間(年)</summary>
        public double Years { get => (TruncatedUntil - TruncatedSince).TotalDays / 365; }

        /// <summary>期間(月)</summary>
        public double Monthes { get => (TruncatedUntil - TruncatedSince).TotalDays / 12; }

        /// <summary>期間(日)</summary>
        public double Days { get => (TruncatedUntil - TruncatedSince).TotalDays; }

        /// <summary>期間(時間)</summary>
        public double Hours { get => (TruncatedUntil - TruncatedSince).TotalHours; }

        /// <summary>期間(分)</summary>
        public double Minutes { get => (TruncatedUntil - TruncatedSince).TotalMinutes; }

        /// <summary>期間(秒)</summary>
        public double Seconds { get => (TruncatedUntil - TruncatedSince).TotalSeconds; }

        /// <summary>期間(ミリ秒)</summary>
        public double MilliSeconds { get => (TruncatedUntil - TruncatedSince).TotalMilliseconds; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="sinceDateTime">開始日時</param>
        /// <param name="untilDateTime">終了日時</param>
        /// <param name="truncateLevel">切り捨て粒度</param>
        public DateTimePeriod(DateTime sinceDateTime, DateTime untilDateTime, TruncateLevel truncateLevel = TruncateLevel.Full)
        {
            if (sinceDateTime > untilDateTime) throw new ArgumentException("無効な期間です");

            Since = sinceDateTime;
            Until = untilDateTime;
            Level = truncateLevel;
        }

        /// <summary>
        /// 対象日時が期間に含まれているかを判定
        /// </summary>
        /// <param name="targetDateTime">比較対象の日時</param>
        /// <param name="allowSameTime">同時刻でもtrueとするか</param>
        /// <returns></returns>
        public bool Contains(DateTime targetDateTime, bool allowSameTime = true)
        {
            if (targetDateTime == default || targetDateTime == DateTime.MinValue || targetDateTime == DateTime.MaxValue) return false;

            bool result = false;

            // 期間と比較対象日時を同じ粒度で切り捨ててから比較
            DateTime truncatedTarget = Truncate(targetDateTime, Level);

            if ((allowSameTime && TruncatedSince <= truncatedTarget && truncatedTarget <= TruncatedUntil)
                || (!allowSameTime && TruncatedSince < truncatedTarget && truncatedTarget < TruncatedUntil))
            {
                result = true;
            }

            return result;
        }

        /// <summary>
        /// 対象期間と交差するかを判定
        /// </summary>
        /// <param name="targetPeriod"></param>
        /// <returns></returns>
        public bool Intersects(DateTimePeriod targetPeriod)
        {
            return Intersects(this, targetPeriod);
        }

        /// <summary>
        /// 期間同士が交差or内包するかを判定
        /// </summary>
        /// <param name="period1">比較する期間1</param>
        /// <param name="period2">比較する期間2</param>
        /// <returns></returns>
        private static bool Intersects(DateTimePeriod period1, DateTimePeriod period2)
        {
            // 交差も内包もしないのは2パターンしかない
            /*
            * |---Period1---|
            *                 |---Period2---|
            * もしくは
            *                 |---Period1---|
            * |---Period2---|
            */

            return !(period1.TruncatedUntil < period2.TruncatedSince || period2.TruncatedUntil < period1.TruncatedSince);
        }

        /// <summary>
        /// 時刻の切り捨て処理
        /// 比較を正規化するためDateTime内部のTicksを切り捨てる
        /// </summary>
        /// <param name="targetDateTime">切り捨て対象の日時</param>
        /// <param name="truncateLevel">切り捨て粒度</param>
        /// <returns></returns>
        private static DateTime Truncate(DateTime targetDateTime, TruncateLevel truncateLevel)
        {
            if (targetDateTime == DateTime.MinValue || targetDateTime == DateTime.MaxValue || truncateLevel == TruncateLevel.Full) return targetDateTime;

            TimeSpan timeSpan = default;

            switch (truncateLevel)
            {
                case TruncateLevel.Day: timeSpan = TimeSpan.FromHours(1); break;
                case TruncateLevel.Hour: timeSpan = TimeSpan.FromMinutes(1); break;
                case TruncateLevel.Minute: timeSpan = TimeSpan.FromSeconds(1); break;
                case TruncateLevel.Second: timeSpan = TimeSpan.FromMilliseconds(1); break;
                case TruncateLevel.MilliSecond: timeSpan = TimeSpan.FromMilliseconds(1); break;
                default: break;
            }

            return targetDateTime.AddTicks(-(targetDateTime.Ticks % timeSpan.Ticks));
        }
    }
}
```

```CSharp:TruncateLevel.cs
namespace NameSpace
{
    public enum TruncateLevel { Full, Day, Hour, Minute, Second, MilliSecond }
}
```

## おまけ
`DebuggerDisplay`アトリビュートを設定しているのでデバッグ時には変数の中身が見やすくなります。

![2020-02-08_01h09_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224096/3cf61499-7097-38e9-48ea-0be1eb5b6a33.png)

## 参考

[How to truncate milliseconds off of a .NET DateTime](https://stackoverflow.com/questions/1004698/how-to-truncate-milliseconds-off-of-a-net-datetime/40235320)