C# Tips | 日付・時間処理:勤務時間計算

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

はじめに 「勤務時間計算」は“お金と信頼に直結するロジック”

勤務時間の計算は、
「出勤時刻と退勤時刻の差を取れば終わり」
…では、まったく終わりません。

休憩時間を引く
深夜時間を分ける
所定労働時間と残業時間を分ける
丸め(5分単位・15分単位)をどうするか

こういった“細かいルール”が積み重なって、
最終的に「給与」「残業代」「法令順守」に直結します。

ここでは、まず「一番シンプルな勤務時間計算」から始めて、
そこに「休憩」「丸め」「所定時間と残業の分離」を少しずつ足していく形で、
初心者向けにかみ砕いて説明していきます。


基本の形:出勤と退勤の差から勤務時間を出す

TimeSpan で「差」を表現する

まずは、休憩も何もない、
「出勤〜退勤がそのまま勤務時間」という一番シンプルなケースです。

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

    return clockOut - clockIn;
}
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 work = GetWorkingTimeBasic(inTime, outTime);

Console.WriteLine(work);                 // 09:00:00
Console.WriteLine(work.TotalHours);      // 9
Console.WriteLine(work.TotalMinutes);    // 540
C#

ここでの重要ポイントは、
「勤務時間は TimeSpan で持つ」
という感覚を身につけることです。

DateTime は「いつ」
TimeSpan は「どれくらい」

この役割分担を意識しておくと、
勤務時間計算のロジックが整理しやすくなります。


休憩時間を引く:固定休憩のケース

「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 だけが休憩として引かれます。


打刻ベースの休憩:休憩開始・終了時刻がある場合

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

次に、
「休憩開始」「休憩終了」の打刻があるケースを考えます。

この場合は、
「勤務時間 = 出勤〜退勤 − すべての休憩の合計」
という形になります。

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#

ここでの重要ポイントは、
「休憩も“時間帯のリスト”として扱い、勤務時間との重なりを合計する」
という構造です。

このパターンを押さえておくと、
休憩が複数あっても、日をまたいでも、柔軟に対応できます。


勤務時間の丸め:5分単位・15分単位など

「1分単位ではなく、キリのいい単位にそろえたい」

実務では、
「勤務時間は5分単位で計算する」
「15分未満は切り捨て」
といった丸めルールがよくあります。

勤務時間は TimeSpan なので、
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 work = TimeSpan.FromMinutes(482); // 8時間2分

TimeSpan rounded = Floor(work, TimeSpan.FromMinutes(5));

Console.WriteLine(work.TotalMinutes);    // 482
Console.WriteLine(rounded.TotalMinutes); // 480 (8時間ちょうど)
C#

ここでの重要ポイントは、
「丸めは“最後にまとめて”やる」ことです。

出勤・退勤・休憩の各時刻を丸めてしまうと、
誤差が積み重なってしまいます。

まずは“生の時刻”で勤務時間を計算し、
最後に勤務時間の TimeSpan を丸める、
という順番を守ると、ロジックが安定します。


所定労働時間と残業時間を分ける

「8時間までは通常勤務、それを超えた分は残業」

勤務時間が出せるようになったら、
次のステップは「所定時間」と「残業時間」の分離です。

例えば、
1日の所定労働時間が 8時間 の場合、
勤務時間が 9時間 なら「8時間+1時間残業」と分けたい、というイメージです。

public readonly struct WorkDuration
{
    public TimeSpan Regular { get; }
    public TimeSpan Overtime { get; }

    public WorkDuration(TimeSpan regular, TimeSpan overtime)
    {
        Regular = regular;
        Overtime = overtime;
    }

    public override string ToString()
        => $"通常: {Regular.TotalHours}h, 残業: {Overtime.TotalHours}h";
}

public static WorkDuration SplitRegularAndOvertime(TimeSpan work, TimeSpan regularLimit)
{
    if (work <= regularLimit)
    {
        return new WorkDuration(work, TimeSpan.Zero);
    }

    TimeSpan regular = regularLimit;
    TimeSpan overtime = work - regularLimit;

    return new WorkDuration(regular, overtime);
}
C#

使い方の例です。

TimeSpan work = TimeSpan.FromHours(9);      // 9時間勤務
TimeSpan regularLimit = TimeSpan.FromHours(8); // 所定8時間

WorkDuration result = SplitRegularAndOvertime(work, regularLimit);

Console.WriteLine(result); // 通常: 8h, 残業: 1h
C#

ここでの重要ポイントは、
「勤務時間を“意味のある2つの TimeSpan”に分解する」
という発想です。

この WorkDuration をさらに日次・月次で合計していけば、
「今月の通常時間」「今月の残業時間」を簡単に集計できます。


実務での注意点:ルールを“コードに埋め込む”のではなく“集約する”

あちこちでバラバラに if 文を書かない

勤務時間計算は、
会社ごとの就業規則や契約によってルールが変わります。

固定休憩か、打刻休憩か
丸めは切り捨てか、四捨五入か
所定時間は 7.5時間か、8時間か
深夜時間(22:00〜5:00)をどう扱うか

これらを、画面やバッチの中にバラバラに if 文で書き始めると、
必ずどこかで食い違いが出ます。

だからこそ、

勤務時間計算
休憩計算
丸め
所定・残業の分離

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


まとめ 「勤務時間計算ユーティリティ」は“時間とお金の境界線をコードに刻むもの」

勤務時間計算は、
単に「退勤−出勤」ではなく、
休憩、丸め、所定時間、残業時間といった“ビジネスルールの塊”です。

押さえておきたいポイントを整理すると、こうなります。

勤務時間は TimeSpan で扱い、「いつ」と「どれくらい」を分けて考える。
休憩は“時間帯”として扱い、勤務時間との“重なり”だけを引く。
丸めは最後に TimeSpan に対して行い、出勤・退勤の時刻自体は丸めない。
所定時間と残業時間は、小さな型(WorkDuration など)に分けて表現すると集計しやすい。
ルールはユーティリティや専用クラスに集約し、プロジェクト全体で同じロジックを使い回す。

ここまで押さえておけば、
「なんとなく時間を引き算している」状態から一歩進んで、
“給与や法令に耐えうる、実務で使える勤務時間計算ユーティリティ”を
自分の C# プロジェクトの中に、落ち着いて組み込めるようになります。

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