01_勤務表比較__timesheetcomparator__

Chapter 1: 勤務表比較 (TimesheetComparator)

こんにちは!このチュートリアルへようこそ。ここでは timesheet_compare プロジェクトの心臓部である「勤務表比較」機能について学んでいきましょう。

皆さんは、会社に提出する勤務表と、お客様先で記録する勤務表の時間が微妙にずれていて、確認に手間取った経験はありませんか?例えば、社内システムには9:00~18:00と記録したけれど、お客様先の記録では9:05~18:05になっていた、などです。

この timesheet_compare ツールは、まさにその問題を解決するために作られました。特に重要な役割を担うのが、今回紹介する 勤務表比較 (TimesheetComparator) です。

勤務表比較 (TimesheetComparator) とは?

勤務表比較 (TimesheetComparator) は、このプロジェクトの中核機能です。まるで探偵が二つの証言(勤務表)を注意深く照らし合わせ、矛盾点を見つけ出すように、客先勤務表と社内勤務表のデータを比較し、日付ごとの不一致(勤務時間、休憩時間など)を検出します。

主な仕事は以下の通りです:

  1. 二つの勤務表データを受け取る: 社内用の勤務表データと、お客様先から提出される勤務表データ(PDFやExcelなど様々な形式があり得ます)を入力として受け取ります。これらのデータの具体的な形については、第2章: 勤務表データモデル (Timesheet Data Models)で詳しく学びます。
  2. 日付ごとに照合する: 両方の勤務表を日付でマッチングさせます。同じ日付の記録を見つけ、それらを比較対象とします。
  3. 詳細項目を比較する: 同じ日付の記録が見つかったら、出勤時間、退勤時間、休憩時間、実働時間などの項目を一つずつ比較します。
  4. 不一致を検出する: 比較した結果、値が異なる項目(例えば、社内勤務表では休憩時間が1時間なのに、客先勤務表では45分になっているなど)を「不一致」としてリストアップします。わずかな時間のずれ(例えば5分以内)は許容範囲とする設定も可能です。
  5. 比較結果をまとめる: すべての日付の比較が終わったら、見つかった不一致点のリストを含む、整理された比較結果レポートを作成します。この結果は、後で第8章: 結果表示 (ResultViewer)によって画面に表示されます。

どのように使われるのか?

TimesheetComparator は、ユーザーが直接操作するものではありません。第3章: メインウィンドウ (MainWindow)がユーザーからの指示を受け取り、内部で TimesheetComparator を呼び出して比較処理を実行します。

概念的なコードの流れを見てみましょう(これは実際のコードを簡略化したイメージです)。

# メインウィンドウ (MainWindow) の中で...

# 1. ファイル選択部品を使ってファイルパスを取得 (これは FileSelector の役割)
client_file_path = file_selector.get_client_file_path()
internal_file_path = file_selector.get_internal_file_path()

# 2. 各ファイルを読み込むためのリーダーを取得 (これは Reader の役割)
#    (詳細は Reader の章で!)
client_reader = ClientTimesheetReader() # 例: PDFReader や ExcelReader
internal_reader = InternalTimesheetReader()

# 3. リーダーを使って勤務表データを読み込む
client_timesheet_list = client_reader.read(client_file_path)
internal_timesheet_list = internal_reader.read(internal_file_path)

# 4. TimesheetComparator を作成
comparator = TimesheetComparator()

# 5. 比較を実行!
#    (許容する稼働時間の誤差を مثلاً 5分として渡す)
allowable_minutes = 5
comparison_results = comparator.compare(
    client_timesheet_list,
    internal_timesheet_list,
    allowable_minutes
)

# 6. 結果を表示する (これは ResultViewer の役割)
result_viewer.show_result(comparison_results)

このコードは、大まかな流れを示しています。

  1. まず、第4章: ファイル選択 (FileSelector) を使って、比較したい二つの勤務表ファイル(客先と社内)を指定します。
  2. 次に、それぞれのファイル形式に応じたリーダー(第5章: 社内勤務表リーダー (InternalTimesheetReader) や 第6章: 客先勤務表リーダー (ClientTimesheetReader))がファイルを読み込み、プログラムが理解できる勤務表データモデルに変換します。
  3. そして、いよいよ TimesheetComparator の出番です。compare メソッドが呼び出され、二つの勤務表データと、許容する時間のずれ(この例では allowable_minutes)が渡されます。
  4. TimesheetComparator は内部で比較処理を行い、結果を ComparisonResult という形のデータにまとめます。
  5. 最後に、その ComparisonResult が 第8章: 結果表示 (ResultViewer) に渡され、ユーザーに分かりやすく表示されます。

内部では何が起きている? (少し詳しく)

TimesheetComparator の compare メソッドが呼び出されると、内部では以下のようなステップで処理が進みます。

sequenceDiagram
    participant MW as メインウィンドウ (MainWindow)
    participant TC as 勤務表比較 (TimesheetComparator)
    participant ClientTS as 客先勤務表データ
    participant InternalTS as 社内勤務表データ
    participant CR as 比較結果 (ComparisonResult)

    MW->>TC: compare(客先データリスト, 社内データリスト, 許容時間) を呼び出す
    Note right of TC: 比較開始!
    loop 各客先勤務表データごと
        TC->>InternalTS: 同じ社員名の社内データを探す
        InternalTS-->>TC: 対応する社内データ (または見つからない)
        alt 見つかった場合
            TC->>TC: _validate_data(客先データ, 社内データ) で基本情報をチェック
            TC->>TC: _compare_records(客先日次記録, 社内日次記録, 許容時間) で日付ごとに比較
            Note right of TC: 出勤/退勤/休憩/稼働時間などを比較
            TC->>CR: 不一致情報を ComparisonDataInfo として記録
        else 見つからなかった場合
            TC->>TC: 警告ログを出力 (処理スキップ)
        end
    end
    TC-->>MW: ComparisonResult のリストを返す
  1. データ受け取り: compare メソッドが、客先勤務表データのリストと社内勤務表データのリスト、そして許容される稼働時間のずれ(分単位)を受け取ります。
  2. 社員のマッチング: まず、客先勤務表データに記載されている社員名と同じ社員名の社内勤務表データがあるかを探します。もし対応する社内データがなければ、その客先データは比較対象外となります。
  3. データ検証: 対応するデータが見つかったら、基本的な情報(期間など)が比較可能か簡単なチェックを行います (_validate_data)。
  4. 日次比較ループ: 客先勤務表の日次記録を一つずつ取り出します。
  5. 対応記録検索: 取り出した客先記録と同じ日付の社内記録を探します (_find_matching_record)。
  6. 項目比較: 同じ日付の記録が見つかったら、_compare_records メソッドで詳細項目(出勤時刻、退勤時刻、休憩時間、計算された稼働時間など)を比較します。この時、compare メソッドで受け取った許容時間が考慮されます。例えば、稼働時間の差が許容時間以内であれば「一致」とみなされます。
  7. 結果記録: 比較結果(日付、各項目の客先/社内の値、一致/不一致フラグ、社内記録のコメントなど)を ComparisonDataInfo という形式でまとめ、リストに追加していきます。
  8. 結果返却: 全ての日付の比較が終わったら、社員ごと、期間ごとの比較結果を ComparisonResult オブジェクトにまとめ、そのリストを呼び出し元(通常は MainWindow)に返します。

実際のコード (src/core/timesheet_comparator.py) の一部を見てみましょう。これは compare メソッドの中核部分のイメージです。

# src/core/timesheet_comparator.py より (簡略化)

class TimesheetComparator:
    # ... (他のメソッドは省略)

    def compare(
        self,
        client_timesheet_list: List[ClientTimesheet],
        internal_timesheet_list: List[InternalTimesheet],
        allowable_work_time # 許容される稼働時間の誤差(分)
    ) -> List[ComparisonResult]:
        comparison_results = [] # 最終的な結果を入れるリスト

        # 1. 客先勤務表リストをループ
        for client_timesheet in client_timesheet_list:
            # 2. 対応する社内勤務表を探す (名前で)
            internal_timesheet = self._find_internal_timesheet_by_name(
                client_timesheet.employee_name, internal_timesheet_list
            )
            if internal_timesheet is None:
                logger.warning(f"社員名 {client_timesheet.employee_name} の社内データが見つかりません。")
                continue # 次の客先データへ

            # ... (データ検証など) ...

            daily_comparison_data = [] # 日々の比較結果を入れるリスト
            matched_days_count = 0

            # 3. 客先勤務表の日次データをループ
            for client_record in client_timesheet.daily_records:
                # 4. 同じ日付の社内日次データを検索
                internal_record = self._find_matching_record(
                    client_record.date, internal_timesheet.daily_records
                )
                if internal_record is None:
                    continue # 対応する社内記録がない日はスキップ

                # 5. 日次記録同士を比較
                comparison_info = self._compare_records(
                    client_record, internal_record, allowable_work_time
                )
                daily_comparison_data.append(comparison_info)
                if comparison_info.match: # 稼働時間が一致(許容範囲内)か?
                    matched_days_count += 1

            # ... (結果を ComparisonResult にまとめる処理) ...
            if daily_comparison_data: # 比較データがあれば結果を追加
                result = ComparisonResult(...) # 詳細をセット
                comparison_results.append(result)

        return comparison_results # 全員の比較結果リストを返す

    def _compare_records(self, client_record, internal_record, allowable_work_time) -> ComparisonDataInfo:
        # この中で、開始時間、終了時間、稼働時間、休憩時間などを比較し、
        # ComparisonDataInfo オブジェクトを作成して返す
        # 稼働時間の比較では allowable_work_time (許容時間) を考慮する
        # ... (詳細な比較ロジック) ...
        is_work_time_matched = self.match_work_time(
            client_record.work_time,
            internal_record.main_pj_work_time, # 社内データの該当PJ稼働時間
            allowable_work_time
        )
        return ComparisonDataInfo(
            date=client_record.date,
            start=ComparisonData(client_value=..., internal_value=...),
            end=ComparisonData(client_value=..., internal_value=...),
            work=ComparisonData(client_value=..., internal_value=...),
            breaktime=ComparisonData(client_value=..., internal_value=...),
            comment=internal_record.comment,
            match=is_work_time_matched # 主に稼働時間の一致で判定
        )

このコードから、compare メソッドがどのようにループ処理を使って日付ごとの比較を行い、_compare_records で実際の値の比較をしているかが分かりますね。allowable_work_time が稼働時間の一致判定に使われている点もポイントです。

比較結果は、最終的に以下のような ComparisonResult というデータ構造にまとめられます (src/core/timesheet_comparator.py で定義)。

# src/core/timesheet_comparator.py より

from dataclasses import dataclass
from datetime import date
from typing import List

@dataclass
class ComparisonData:
    client_value: str  # 客先データ値
    internal_value: str # 社内データ値

@dataclass
class ComparisonDataInfo:
    date: date             # 日付
    start: ComparisonData  # 開始時刻 (客先, 社内)
    end: ComparisonData    # 終了時刻 (客先, 社内)
    work: ComparisonData   # 稼働時間 (客先, 社内)
    breaktime: ComparisonData # 休憩時間 (客先, 社内)
    comment: str           # 社内コメント
    match: bool            # この日は一致したか (True/False)

@dataclass
class ComparisonResult:
    employee_name: str     # 社員名
    target_period: str     # 対象期間
    is_matched: bool       # 全体として完全に一致したか
    data: List[ComparisonDataInfo] # 日ごとの比較詳細リスト
    total_days: int        # 比較対象となった合計日数
    matched_days: int      # そのうち一致した日数

この ComparisonResult には、誰の、いつの期間の比較結果なのか、そして日々の詳細な比較データ (ComparisonDataInfo のリスト) が含まれています。これにより、どこが一致していて、どこが不一致なのかが一目で分かるようになっています。

まとめ

この章では、timesheet_compare プロジェクトの中心的なロジックである 勤務表比較 (TimesheetComparator) について学びました。

これで、二つの勤務表がどのように比較されるのか、基本的な仕組みが理解できたはずです。

次の章では、TimesheetComparator が入力として受け取り、また出力の一部としても使われるデータの「形」について詳しく見ていきます。

次の章: 第2章: 勤務表データモデル (Timesheet Data Models)


Generated by AI Codebase Knowledge Builder