C# Tips | 日付・時間処理:休憩時間控除

C# C#
スポンサーリンク

はじめに 「休憩時間控除」は“働いた時間と、そうでない時間を分ける線引き”

勤務時間の計算で一番よく出てくるのが「休憩時間を引く」という処理です。
ここを雑にやると、「働いていない時間にまで給料を払ってしまう」「逆に未払いになる」など、かなりクリティカルな問題になります。

でも、やること自体はシンプルで、
本質は「勤務時間帯」と「休憩時間帯」の“重なっている部分”を引くだけです。
これを DateTimeTimeSpan でどう表現するかを、順番にかみ砕いていきます。


基本の考え方:勤務時間 − 休憩時間 = 実働時間

まずは「出勤〜退勤」の TimeSpan を作る

一番の土台は、「出勤時刻」と「退勤時刻」の差です。

TimeSpan total = clockOut - clockIn;
C#

ここで total は「その日に会社にいた時間」です。
この中から「休憩にあたる部分」を引いたものが、実際に働いた時間(実働時間)になります。

つまり、やりたいことはこうです。

実働時間 = (退勤 − 出勤) − 休憩時間の合計

この「休憩時間の合計」をどう計算するかが、休憩時間控除ユーティリティの中心です。


パターン1:固定休憩(例:12:00〜13:00 は必ず休憩)

「決まった時間帯は必ず休憩」の場合

まずは一番シンプルな、「毎日 12:00〜13:00 は休憩」というパターンです。
この場合、休憩は「時間帯」として表現できます。

public static TimeSpan GetWorkingTimeWithFixedBreak(
    DateTime clockIn,
    DateTime clockOut,
    TimeSpan breakStart,
    TimeSpan breakEnd)
{
    if (clockOut < clockIn)
        throw new ArgumentException("退勤時刻は出勤時刻より後である必要があります。");

    TimeSpan total = clockOut - clockIn;

    DateTime breakStartDateTime = clockIn.Date + breakStart;
    DateTime breakEndDateTime   = clockIn.Date + breakEnd;

    DateTime overlapStart = clockIn > breakStartDateTime ? clockIn : breakStartDateTime;
    DateTime overlapEnd   = clockOut < breakEndDateTime ? clockOut : breakEndDateTime;

    TimeSpan breakDuration = TimeSpan.Zero;

    if (overlapEnd > overlapStart)
    {
        breakDuration = overlapEnd - overlapStart;
    }

    return total - breakDuration;
}
C#

使い方の例です。

DateTime inTime  = new DateTime(2026, 2, 18, 9, 0, 0);   // 9:00
DateTime outTime = new DateTime(2026, 2, 18, 18, 0, 0);  // 18:00

TimeSpan breakStart = new TimeSpan(12, 0, 0); // 12:00
TimeSpan breakEnd   = new TimeSpan(13, 0, 0); // 13:00

TimeSpan work = GetWorkingTimeWithFixedBreak(inTime, outTime, breakStart, breakEnd);

Console.WriteLine(work.TotalHours); // 8
C#

ここでの重要ポイントは、「休憩時間帯と勤務時間帯の“重なり”だけを引いている」ことです。
出勤が 11:30、退勤が 12:30 のような場合でも、
重なっている 12:00〜12:30 だけが休憩として控除されます。


パターン2:打刻休憩(休憩開始・終了を打刻する場合)

「休憩ボタンを押す」タイプのシステム

次に、「休憩開始」「休憩終了」を打刻するタイプです。
この場合、休憩は「複数の時間帯のリスト」として扱うのが自然です。

public static TimeSpan GetWorkingTimeWithBreakPunches(
    DateTime clockIn,
    DateTime clockOut,
    IReadOnlyList<(DateTime BreakOut, DateTime BreakIn)> breaks)
{
    if (clockOut < clockIn)
        throw new ArgumentException("退勤時刻は出勤時刻より後である必要があります。");

    TimeSpan total = clockOut - clockIn;
    TimeSpan breakTotal = TimeSpan.Zero;

    foreach (var b in breaks)
    {
        if (b.BreakIn < b.BreakOut)
            throw new ArgumentException("休憩戻りは休憩開始より後である必要があります。");

        DateTime start = b.BreakOut < clockIn ? clockIn : b.BreakOut;
        DateTime end   = b.BreakIn  > clockOut ? clockOut : b.BreakIn;

        if (end > start)
        {
            breakTotal += (end - start);
        }
    }

    return total - breakTotal;
}
C#

使い方の例です。

DateTime inTime  = new DateTime(2026, 2, 18, 9, 0, 0);   // 9:00
DateTime outTime = new DateTime(2026, 2, 18, 18, 0, 0);  // 18:00

var breaks = new List<(DateTime, DateTime)>
{
    (new DateTime(2026, 2, 18, 12, 0, 0), new DateTime(2026, 2, 18, 13, 0, 0)), // 昼休憩 1h
    (new DateTime(2026, 2, 18, 15, 0, 0), new DateTime(2026, 2, 18, 15, 15, 0)) // 小休憩 15m
};

TimeSpan work = GetWorkingTimeWithBreakPunches(inTime, outTime, breaks);

Console.WriteLine(work.TotalHours); // 7.75
C#

ここでの重要ポイントは、「休憩を“時間帯の集合”として扱い、勤務時間との重なりを合計して控除する」ことです。
この形にしておくと、休憩が何回あっても、日をまたいでも、同じロジックで処理できます。


パターン3:勤務時間に応じて自動で休憩を控除する

「6時間を超えたら45分、8時間を超えたら1時間」などのルール

就業規則によっては、
「勤務時間が◯時間を超えたら自動的に休憩◯分を控除する」
というルールもあります。

例えば、ざっくりとした例として、

6時間超〜8時間以下 → 45分休憩
8時間超 → 60分休憩

のようなルールを考えてみます。

public static TimeSpan GetAutoBreak(TimeSpan workBeforeBreak)
{
    if (workBeforeBreak.TotalHours <= 6)
    {
        return TimeSpan.Zero;
    }
    else if (workBeforeBreak.TotalHours <= 8)
    {
        return TimeSpan.FromMinutes(45);
    }
    else
    {
        return TimeSpan.FromMinutes(60);
    }
}

public static TimeSpan GetWorkingTimeWithAutoBreak(DateTime clockIn, DateTime clockOut)
{
    if (clockOut < clockIn)
        throw new ArgumentException("退勤時刻は出勤時刻より後である必要があります。");

    TimeSpan total = clockOut - clockIn;
    TimeSpan autoBreak = GetAutoBreak(total);

    return total - autoBreak;
}
C#

使い方の例です。

DateTime inTime  = new DateTime(2026, 2, 18, 9, 0, 0);
DateTime outTime = new DateTime(2026, 2, 18, 17, 30, 0); // 8.5時間

TimeSpan work = GetWorkingTimeWithAutoBreak(inTime, outTime);

Console.WriteLine(work.TotalHours); // 7.5 (8.5h - 1h休憩)
C#

ここでの重要ポイントは、「休憩時間そのものを打刻しない代わりに、“勤務時間の長さ”から休憩を控除するルールを関数化している」ことです。
この GetAutoBreak の中身が、まさに就業規則そのものになります。


丸めとの組み合わせ:「休憩控除後の時間」を丸める

勤務時間の丸めは“最後にまとめて”行う

多くの勤怠システムでは、
「勤務時間は5分単位」「15分単位」などの丸めルールがあります。

ここでやりがちなのが、
出勤・退勤・休憩の各時刻を先に丸めてしまうことですが、
これは誤差が積み重なりやすく危険です。

おすすめは、

  1. 出勤・退勤・休憩は“生の時刻”で扱う
  2. 休憩時間控除まで終わらせて「実働時間の TimeSpan」を出す
  3. 最後にその TimeSpan を丸める

という順番です。

public static TimeSpan Floor(TimeSpan value, TimeSpan interval)
{
    if (interval <= TimeSpan.Zero)
        throw new ArgumentOutOfRangeException(nameof(interval));

    long ticks = value.Ticks / interval.Ticks * interval.Ticks;
    return new TimeSpan(ticks);
}
C#

使い方のイメージです。

TimeSpan rawWork = GetWorkingTimeWithBreakPunches(inTime, outTime, breaks);

TimeSpan roundedWork = Floor(rawWork, TimeSpan.FromMinutes(5));
C#

ここでの重要ポイントは、「控除も丸めも、すべて TimeSpan に対して行う」ことです。
DateTime を直接いじり始めると、一気に混乱しやすくなります。


実務での設計ポイント:ルールを“1箇所に閉じ込める”

「休憩控除ロジック」を散らさない

休憩時間控除は、会社ごと・契約ごとにルールが違います。
固定休憩か、打刻休憩か、自動控除か、丸めはどうするか…。

これを画面やバッチの中にバラバラに if 文で書き始めると、
「画面Aと画面Bで計算結果が違う」といった事故が必ず起きます。

だからこそ、

休憩時間帯との重なりを計算するロジック
打刻休憩の合計を出すロジック
勤務時間に応じて自動控除するロジック
控除後の勤務時間を丸めるロジック

といったものを、
専用のユーティリティ(あるいはドメインサービス)に集約し、
「勤務時間を計算したいコードは必ずそこを通る」
という形にしておくのが、とても大事です。


まとめ 「休憩時間控除ユーティリティ」は“働いた時間の輪郭をはっきりさせる道具」

休憩時間控除は、
単に「休憩を引く」ではなく、
「どこからどこまでを“働いた時間”とみなすか」をコードで表現する作業です。

押さえておきたいポイントはこうです。

勤務時間は TimeSpan で扱い、「出勤〜退勤」と「休憩時間帯の重なり」を引き算する。
固定休憩も打刻休憩も、「休憩=時間帯」として扱い、勤務時間との重なりだけを控除する。
勤務時間に応じて自動で休憩を控除する場合は、そのルールを関数として切り出しておく。
丸めは最後に「控除後の実働時間」に対して行い、時刻そのものは丸めない。
休憩控除ロジックはユーティリティに集約し、プロジェクト全体で同じ計算を使い回す。

ここまで整理できれば、
「なんとなく休憩を引いている」状態から抜け出して、
“給与や法令に耐えうる、実務で使える休憩時間控除ユーティリティ”を
自分の C# コードの中に、落ち着いて組み込めるようになります。

タイトルとURLをコピーしました