【C#】時間範囲を示す自作クラス(TimePeriod)

# はじめに

**boost::posix_time::time_period** 相当のクラスは C# 標準にはないのだけど、誰かが作っているだろうと探してみたのですが、どれも仕様が合致せず。仕方なく、自分で作ることにしました。
ほぼ動いているのでここで晒しておきます。

## boost::posix_time::time_period の仕様

[boost::posix_time::time_period](https://www.boost.org/doc/libs/1_68_0/doc/html/date_time/posix_time.html#date_time.posix_time.time_period) の詳細は本家（英語）を参照してもらうことにして、仕様を簡単に記しておきます。

- 期間を **［開始, 終了）** として作成（開始は含まれるが終了は含まれない）
- 「終了 ≦ 開始」は無効と定義される

となっています。

合致しなかったのは、期間の定義です。探した範囲では、どれも終了が期間に含まれていました。指定した期間にチェックする日時が含まれているどうかを判定する時に、終了は含んで欲しくないのですよね。（自分が探し足りないだけでもしかしたら存在するのかもしれませんが・・・）。

例えば、大小関係が A < B < C の3つの日時があったとして、X が

1. Aより前
2. A～B
3. B～C
4. C以後

のどの期間にあるのかを判定する時に、終了が含まれてしまうと、例えば B と同じ日時で判定した場合に、2番 と 3番 の 2 つの期間に含まれていることになってしまいます。これは期間に応じて処理をしたい場合などにおいてちょっとまずいことになります。

かといって、終了を指定する際にわざわざ Tick 分だけ差し引いた日時を指定するのも面倒なので、自作することにしました。

## メソッドなど

boost の time_period に含まれる関数相当の機能はほぼ網羅しています。

## 確認した環境

Visual Studio 2015 で動作確認をしています。C#6.0 以上であれば使えるはずです。

## ソースコード

```C#
using System;
using System.Text;

namespace TimePeriod
{
    /// <summary>
    /// 2つの時刻の範囲を表現するクラス
    /// 
    /// (boostのtime_periodを参考に作成)
    /// </summary>
    public class TimePeriod
    {
        public DateTime Begin { get; protected set; }      //!< 開始時刻(期間の最初)
        public DateTime Last { get; protected set; }       //!< 終了時刻(期間の最後)
        public DateTime End {  get { return Last + TimeSpan.FromTicks(1); } }   //!< 期間の最後の次

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="start">開始(この時刻は期間に含まれる)</param>
        /// <param name="last">終了(この時刻は期間に含まれない)</param>
        public TimePeriod(DateTime start, DateTime end)
        {
            Begin = start;
            Last = end - TimeSpan.FromTicks(1);
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="start">開始(この時刻は期間に含まれる)</param>
        /// <param name="len">期間の長さ</param>
        public TimePeriod(DateTime start, TimeSpan len)
        {
            Begin = start;
            Last = start + len - TimeSpan.FromTicks(1);
        }

        /// <summary>
        /// コピーコンストラクタ
        /// </summary>
        /// <param name="tp">範囲</param>
        public TimePeriod(TimePeriod tp)
        {
            Begin = tp.Begin;
            Last = tp.Last;
        }

        /// <summary>
        /// 期間が正しいかどうかの検査
        /// </summary>
        /// <returns>true:期間が正しい形式でない時(begin >= end)</returns>
        public bool IsNull()
            => End <= Begin;

        /// <summary>
        /// 期間の長さをTimeSpanで返す
        /// </summary>
        /// <returns>期間の長さ</returns>
        public TimeSpan Length()
            => End - Begin;

        /// <summary>
        /// 範囲をずらす
        /// </summary>
        /// <param name="d">ずらし量</param>
        public void Shift(TimeSpan d)
        {
            Begin += d;
            Last += d;
        }

        /// <summary>
        /// 範囲を拡張する(両端同じ長さ)
        /// </summary>
        /// <param name="d">拡張幅</param>
        public void Expnad(TimeSpan d)
        {
            Begin -= d;
            Last += d;
        }

        /// <summary>
        /// 指定した日時が範囲に含まれるかの検査
        /// </summary>
        /// <param name="t">チェックする日時</param>
        /// <returns>true:範囲に含まれる、false:範囲に含まれない</returns>
        public bool Contains(DateTime t)
            => ((t >= Begin) && (t <= Last));

        /// <summary>
        /// 指定した範囲が含まれているかの検査
        /// </summary>
        /// <param name="range">チェックする範囲</param>
        /// <returns>true:範囲に含まれる、false:範囲に含まれない</returns>
        public bool Contains(TimePeriod range)
            => ((Begin <= range.Begin) && (Last >= range.Last));

        /// <summary>
        /// 指定した範囲が隣接(adjacent)しているかどうかの検査
        /// </summary>
        /// <param name="range">チェックする範囲</param>
        /// <returns>true:隣接している、false:隣接していない(重なっている場合もある)</returns>
        public bool IsAdjacent(TimePeriod range)
            => ((range.Begin == End) || (Begin == range.End));

        /// <summary>
        /// 指定した日時より後かどうかの検査
        /// </summary>
        /// <param name="t">指定日時</param>
        /// <returns>true:この範囲が指定日よりあと</returns>
        public bool IsAfter(DateTime t)
        {
            if (IsNull())
            {
                return false;
            }
            return t < Begin;
        }

        /// <summary>
        /// 指定した日時より前かどうかの検査
        /// </summary>
        /// <param name="t">指定日時</param>
        /// <returns>true:この範囲が指定日より前</returns>
        public bool IsBefore(DateTime t)
        {
            if (IsNull())
            {
                return false;
            }
            return Last < t;
        }

        /// <summary>
        /// 指定した範囲が重なっているかどうかの検査
        /// </summary>
        /// <param name="range">チェックする範囲</param>
        /// <returns>true:期間が重なっている、false:期間の重なりはない</returns>
        public bool Intersects(TimePeriod range)
            => (Contains(range.Begin) ||
                range.Contains(Begin) ||
                ((range.Begin < Begin) && (range.Last >= Begin)));

        /// <summary>
        /// 指定した範囲と重複する部分を期間として返す
        /// </summary>
        /// <param name="range">チェックする範囲</param>
        /// <returns></returns>
        public TimePeriod Intersection(TimePeriod range)
        {
            if (Begin > range.Begin)
            {
                if (Last <= range.Last)
                {
                    return this;
                }
                return new TimePeriod(Begin, range.End);
            }
            else
            {
                if (Last <= range.Last)
                {
                    return new TimePeriod(range.Begin, End);
                }
                return range;
            }
        }

        /// <summary>
        /// 2つの範囲を結合して返す
        /// </summary>
        /// <param name="range">結合する範囲</param>
        /// <returns>結合された範囲(重複しない場合はnullな期間を返す)</returns>
        public TimePeriod Merge(TimePeriod range)
        {
            if (Intersects(range))
            {
                if (Begin < range.Begin)
                {
                    return new TimePeriod(Begin, (Last > range.Last) ? End : range.End);
                }
                return new TimePeriod(range.Begin, (Last > range.Last) ? End : range.End);
            }
            return new TimePeriod(Begin, Begin);
        }

        /// <summary>
        /// 2つの範囲の一番早い時刻と一番遅い時刻の範囲を返す
        /// </summary>
        /// <param name="range">もう1つの時刻範囲</param>
        /// <returns>結合した時刻範囲</returns>
        public TimePeriod Span(TimePeriod range)
        {
            var start = (Begin < range.Begin) ? Begin : range.Begin;
            var newend = (Last < range.Last) ? range.End : End;
            return new TimePeriod(start, newend);
        }

        /// <summary>
        /// 比較演算子(==)
        /// </summary>
        /// <param name="l">比較する側</param>
        /// <param name="r">比較される側</param>
        /// <returns>true:範囲が一致</returns>
        public static bool operator==(TimePeriod l, TimePeriod r)
        {
            // 参照が同じならtrue
            if (ReferenceEquals(l, r))
            {
                return true;
            }
            // どちらかがnullならfalse
            // objectでキャストしないと無限ループになるので注意
            if (((object)l == null) || ((object)r == null))
            {
                return false;
            }

            return ((l.Begin == r.Begin) && (l.Last == r.Last));
        }

        /// <summary>
        /// 比較演算子(!=)
        /// </summary>
        /// <param name="l">比較する側</param>
        /// <param name="r">比較される側</param>
        /// <returns>true:範囲が違う</returns>
        public static bool operator!=(TimePeriod l, TimePeriod r)
            => !(l == r);

        /// <summary>
        /// 比較演算子(＜)
        /// </summary>
        /// <param name="l">比較する側</param>
        /// <param name="r">比較される側</param>
        /// <returns>true:左側のLastが右側のBeginより小さい</returns>
        public static bool operator<(TimePeriod l, TimePeriod r)
            => (l.Last < r.Begin);

        /// <summary>
        /// 比較演算子(＞)
        /// </summary>
        /// <param name="l">比較する側</param>
        /// <param name="r">比較される側</param>
        /// <returns>true:左側のLastが右側のBeginより大きい</returns>
        public static bool operator>(TimePeriod l, TimePeriod r)
            => (r.Last < l.Begin);

        /// <summary>
        /// Equalsのオーバーライド
        /// </summary>
        /// <param name="obj">オブジェクト</param>
        /// <returns>一致していればtrueを返す</returns>
        public override bool Equals(object obj)
        {
            if (obj == null || GetType() != obj.GetType())
            {
                return false;
            }

            TimePeriod tp = (TimePeriod)obj;
            return this == tp;
        }

        /// <summary>
        /// GetHashCodeのオーバーライド
        /// </summary>
        /// <returns>Hash Code</returns>
        public override int GetHashCode()
            => Begin.GetHashCode() ^ Last.GetHashCode();

        /// <summary>
        /// ToString()のオーバーライド関数
        /// </summary>
        /// <returns>文字列</returns>
        public override string ToString()
        {
            var build = new StringBuilder();
            build.AppendFormat("Begin:{0},", Begin)
                .AppendFormat("End:{0}", End);
            return build.ToString();
        }
    }
}
```

## 最後に

ソースはご自由に改変して使ってもらって構いません。

しかし、何で C# には TimePeriod 的なクラスがないのだろう？