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) に渡され、ユーザーに分かりやすく表示されます。

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

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

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