05_社内勤務表リーダー__internaltimesheetreader__

Chapter 5: 社内勤務表リーダー (InternalTimesheetReader)

こんにちは!前の章、第4章: ファイル選択 (FileSelector)では、比較したい勤務表ファイルをアプリケーションに指定する方法を学びましたね。ユーザーがファイルを選んでくれる「受付係」の役割でした。

さて、ファイルが選ばれただけでは、まだ中身はコンピューターにとって意味不明な文字列の集まりです。比較処理を行うためには、ファイルの中身を読み取り、プログラムが理解できる形、つまり第2章: 勤務表データモデル (Timesheet Data Models)で学んだ「設計図」に沿った形に変換する必要があります。

今回は、その中でも社内で使われている特定のCSV形式の勤務表ファイルを読み込む専門家、社内勤務表リーダー (InternalTimesheetReader) について学んでいきましょう。

社内勤務表リーダー (InternalTimesheetReader) とは? - 社内文書専門のファイリング係

InternalTimesheetReader は、その名の通り、社内で標準的に使われているCSV形式の勤務表ファイルを読み込むことに特化した部品です。

社内には様々な書類がありますが、その中でも特定のフォーマット(ここではCSV形式の勤務表)だけを専門に扱い、その情報を決まった形式(このプロジェクトの共通言語である InternalTimesheet データモデル)に整理してくれる、社内文書専門のファイリング係のような存在だと考えてください。

他の形式のファイル(例えば客先から来るPDFやExcel)は扱えませんが、社内CSVファイルに関しては、その構造を熟知しており、正確に情報を抜き出してくれます。

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

  1. CSVファイルを受け取る: ファイル選択 (FileSelector) によって指定された、社内勤務表のCSVファイルのパス(ファイルの場所)を受け取ります。
  2. ファイルを開き、内容を読む: CSVファイルを開き、一行ずつ内容を読み取ります。
  3. データを解析する: 読み取ったデータの中から、必要な情報(日付、出勤・退勤時刻、社員名など)を見つけ出し、プログラムが扱いやすいように解釈します。(例:「“09:00”」という文字列を、時間のデータとして認識する)
  4. 共通形式に変換する: 解析した情報を、第2章: 勤務表データモデル (Timesheet Data Models) で定義された InternalTimesheetInternalDailyRecord の形にまとめ上げます。

これにより、CSVファイルという特定の形式が、プログラム全体で統一的に扱える InternalTimesheet データオブジェクトへと変換されるのです。

使い方の例

InternalTimesheetReader は、主にメインウィンドウ (MainWindow) から(間接的にファイル選択 (FileSelector) を通じて)呼び出されます。

ユーザーがファイルを選び、「実行(比較)」ボタンを押すと、内部的に次のような処理が行われます。(これは概念的なコードの流れです)

# FileSelector の get_internal_timesheet メソッド内で... (イメージ)

# 1. 社内勤務表リーダーのインスタンスを作成
internal_reader = InternalTimesheetReader()

# 2. ユーザーが選択したファイルのパスを取得 (これは FileSelector が覚えている)
internal_file_path = self._internal_file_path # 例: "C:/path/to/internal_timesheet.csv"

# 3. リーダーの read メソッドを呼び出してファイルを読み込む
try:
    # read メソッドにファイルパスを渡す
    internal_timesheet_list = internal_reader.read(internal_file_path)
    # 成功! internal_timesheet_list には InternalTimesheet オブジェクトのリストが入る
    print("社内勤務表の読み込みに成功しました!")
    # この後、このデータが比較処理に使われる
except FileNotFoundError:
    print("エラー: ファイルが見つかりません。")
except ValueError:
    print("エラー: CSVファイルの形式が正しくない可能性があります。")
except Exception as e:
    print(f"エラー: 予期せぬ問題が発生しました: {e}")

このコードでは、まず InternalTimesheetReader の「実体」(インスタンス)を作ります。次に、ファイル選択 (FileSelector) が記憶している社内勤務表のファイルパスを使って、internal_readerread メソッドを呼び出します。

read メソッドは、指定されたCSVファイルを読み込み、解析し、その結果を InternalTimesheet オブジェクト(またはそのリスト)として返します。もしファイルが見つからなかったり、CSVの内容が期待通りでなかったりした場合は、エラーが発生することもあります。

入力例:社内勤務表CSVファイル

InternalTimesheetReader が読み込むCSVファイルは、例えば以下のような形式を想定しています。(実際のファイルはもっと複雑な場合があります)

"社員番号","社員名","処理年月","日付","勤務区分","出勤時刻","退勤時刻","出勤打刻時刻","退勤打刻時刻","コメント","プロジェクト名称","プロジェクト時間"
"99001","山田 太郎","2024/07","2024/07/01","通常","09:00","18:00","08:58","18:05","","プロジェクトA","08:00"
"99001","山田 太郎","2024/07","2024/07/02","通常","09:00","18:30","09:02","18:35","残業対応","プロジェクトA","08:30"
"99001","山田 太郎","2024/07","2024/07/03","午前半休","13:00","18:00","12:55","18:02","午前半休取得","プロジェクトA","04:00"
# ... 以下、一ヶ月分の日次データが続く ...

出力例:InternalTimesheet オブジェクト

上記のCSVファイルを InternalTimesheetReader で読み込むと、内部的に以下のような構造のデータ(InternalTimesheet オブジェクト)が作成されます。

# 生成される InternalTimesheet オブジェクトのイメージ
InternalTimesheet(
    employee_number="99001", # 社員番号
    employee_name="山田 太郎",  # 社員名
    target_period="2024年7月1日~2024年7月31日", # 対象期間 (CSVから計算される)
    daily_records=[ # 日々の記録のリスト
        InternalDailyRecord(
            date=date(2024, 7, 1), # 日付
            registered_start_time=time(9, 0), # 登録された出勤時刻
            registered_end_time=time(18, 0), # 登録された退勤時刻
            start_time=time(8, 58),  # 打刻された出勤時刻
            end_time=time(18, 5),   # 打刻された退勤時刻
            main_pj_work_time=timedelta(hours=8), # 主要プロジェクトの稼働時間
            break_time=timedelta(hours=1), # 休憩時間 (計算される場合も)
            work_time=timedelta(hours=8), # 実働時間 (CSVの値)
            comment="" # コメント
        ),
        InternalDailyRecord(
            date=date(2024, 7, 2),
            registered_start_time=time(9, 0),
            registered_end_time=time(18, 30),
            start_time=time(9, 2),
            end_time=time(18, 35),
            main_pj_work_time=timedelta(hours=8, minutes=30),
            break_time=timedelta(hours=1),
            work_time=timedelta(hours=8, minutes=30),
            comment="残業対応"
        ),
        # ... 他の日付の InternalDailyRecord が続く ...
    ]
)
# 実際には、ファイルごとに複数の InternalTimesheet オブジェクトがリストとして返される

CSVファイルの各行の情報が、プログラムで扱いやすい date, time, timedelta といった型を持つ InternalDailyRecord に変換され、それらが InternalTimesheet にまとめられているのが分かりますね。

内部の仕組み

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

  1. ファイル存在確認: まず、指定されたパスに本当にファイルが存在するかを確認します。なければエラーを出します。
  2. 文字コード検出: CSVファイルがどの文字コード(Shift_JIS, UTF-8など)で書かれているかを自動で判別します。日本語を含むファイルでは文字コードの問題が起きやすいため、これは重要なステップです。(chardetというライブラリを使います)
  3. CSV読み込み: 適切な文字コードを使ってファイルを開き、内容を読み込みます。多くの場合、pandasというデータ分析ライブラリを使って、CSVデータを効率的に扱える表形式(DataFrame)に変換します。
  4. データ抽出と整形: 読み込んだ表(DataFrame)から、必要な列(例:"日付", "出勤時刻" など)のデータを取り出します。この際、文字列データを日付型 (date) や時刻型 (time)、時間間隔型 (timedelta) に変換します。
  5. データモデル作成: 整形したデータを使って、第2章: 勤務表データモデル (Timesheet Data Models) で定義された InternalDailyRecord オブジェクトを一日分ずつ作成します。
  6. 勤務表オブジェクト作成: 作成した InternalDailyRecord のリストと、社員名や対象期間などの情報を合わせて、最終的な InternalTimesheet オブジェクトを作成します。ファイルによっては複数の社員が含まれる可能性があるため、通常は InternalTimesheet のリストを返します。
  7. 結果を返す: 作成した InternalTimesheet オブジェクト(のリスト)を呼び出し元(通常は FileSelector)に返します。

この流れをシーケンス図で見てみましょう。

sequenceDiagram
    participant FS as ファイル選択 (FileSelector)
    participant ITR as 社内勤務表リーダー (InternalTimesheetReader)
    participant Lib as 外部ライブラリ (Pandas, Chardet)
    participant File as CSVファイル
    participant ITS as InternalTimesheet

    FS->>ITR: read(ファイルパス) を呼び出す
    ITR->>File: ファイル存在確認
    File-->>ITR: 存在する
    ITR->>Lib: 文字コード検出を依頼 (chardet)
    Lib-->>ITR: 文字コード (例: cp932)
    ITR->>Lib: CSV読み込みを依頼 (pandas.read_csv)
    Lib->>File: ファイル内容を読み込む
    File-->>Lib: CSVデータ
    Lib-->>ITR: DataFrame (表データ)
    ITR->>ITR: 必要なデータを抽出・整形
    loop 各行のデータごと
        ITR->>ITR: InternalDailyRecord を作成
    end
    ITR->>ITS: InternalTimesheet オブジェクトを作成
    ITS-->>ITR: 作成された InternalTimesheet (リスト)
    ITR-->>FS: InternalTimesheet (リスト) を返す

このように、InternalTimesheetReader はライブラリ(PandasやChardet)の助けを借りながら、CSVファイルを解析し、最終的に InternalTimesheet という標準化された形式に変換しているのです。

コードの中身 (少し詳しく)

InternalTimesheetReader の実装は src/core/internal_timesheet_reader.py にあります。主要な部分を見てみましょう。

初期化 (__init__)

リーダーが作成されるときに、必要な設定を行います。ここでは、CSVファイルに最低限含まれていてほしい列の名前を定義しています。

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

class InternalTimesheetReader:
    """内部タイムシートリーダークラス"""

    def __init__(self):
        """内部タイムシートリーダーを初期化します。"""
        # CSVファイルに必須の列名をリストで定義
        self.required_columns = [
            '"社員番号"',
            '"社員名"',
            # ... 他の必須列名 ...
            '"プロジェクト時間"',
        ]
        # 他にも初期設定があればここで行う

これにより、後でCSVファイルを読み込んだ際に、これらの必須列が存在するかどうかをチェックできます。

ファイル読み込み (read メソッド)

これがメインの処理を行うメソッドです。

# src/core/internal_timesheet_reader.py より (簡略化)
import pandas as pd # データ操作ライブラリ
import chardet     # 文字コード検出ライブラリ
from pathlib import Path
from ..models.internal_timesheet import InternalTimesheet

class InternalTimesheetReader:
    # ... (__init__ は省略) ...

    def read(self, file_path: Path | str) -> List[InternalTimesheet]:
        """CSVファイルを読み取り、InternalTimesheetオブジェクトのリストを返します。"""
        file_path = Path(file_path) # ファイルパスを確実にPathオブジェクトにする
        if not file_path.exists():
            raise FileNotFoundError(f"ファイルが見つかりません: {file_path}")

        try:
            # 1. 文字コードを検出
            encoding = self._detect_encoding(file_path)
            if encoding is None:
                encoding = 'cp932' # デフォルトとしてcp932 (Shift_JIS) を試すことも

            # 2. Pandas を使って CSV を読み込む
            #    (実際のコードでは、コメント行などを考慮してより複雑な読み込み方をしています)
            df = pd.read_csv(file_path, encoding=encoding)

            # 3. 必須列の存在チェック
            self._validate_columns(df)

            # 4. DataFrame から InternalTimesheet のリストを作成
            #    (この処理は _create_internal_data_sheets メソッドに分離されている)
            internal_sheets = self._create_internal_data_sheets(df)

            return internal_sheets

        except Exception as e:
            # エラーが発生したら、ログに記録して再発生させる
            logger.error(f"CSVの読み取りまたは解析に失敗しました: {e}")
            raise IOError(f"CSVの読み取りに失敗しました: {str(e)}") from e

    def _detect_encoding(self, file_path):
        """ファイルの文字コードを検出します。"""
        with open(file_path, "rb") as f:
            result = chardet.detect(f.read(10000)) # 先頭10000バイトで判断
        return result["encoding"]

    def _validate_columns(self, df):
        """DataFrameに必要な列が存在するかチェックします。"""
        for col in self.required_columns:
            if col not in df.columns:
                raise ValueError(f"必須列が不足しています: {col}")

    def _create_internal_data_sheets(self, df: pd.DataFrame) -> List[InternalTimesheet]:
        """DataFrameからInternalTimesheetオブジェクトのリストを作成します。"""
        # このメソッドの中で、DataFrameの各行をループし、
        # 日付、時刻などを抽出し、InternalDailyRecordを作成し、
        # 社員ごとに InternalTimesheet にまとめます。
        # (詳細な実装は省略)
        sheets = []
        # ... DataFrame を処理して sheets に InternalTimesheet を追加 ...
        logger.info(f"{len(sheets)} 件の社内勤務表データを読み込みました。")
        return sheets

read メソッドの流れは以下の通りです。

  1. 文字コード検出: _detect_encoding でファイルの文字コードを調べます。
  2. CSV読み込み: pandasread_csv 関数を使って、CSVファイルの内容を表形式のデータ (DataFrame) として読み込みます。このとき、検出した文字コードを指定します。
  3. 列検証: _validate_columns で、__init__ で定義した必須列が DataFrame にちゃんと存在するかを確認します。なければエラーになります。
  4. データモデル作成: _create_internal_data_sheets という別のメソッド(実際のデータ変換ロジックはこの中にあります)を呼び出して、DataFrame から InternalTimesheet オブジェクトのリストを作成します。
  5. 結果返却: 作成されたリストを返します。途中で何か問題が起きたら(try...except)、エラーを記録して処理を中断します。

_create_internal_data_sheets の中では、DataFrame の一行一行を処理し、日付や時刻の文字列を date 型や time 型に変換したり、必要な計算(例えば休憩時間の算出など)を行ったりしながら、InternalDailyRecord を作成し、最終的に社員ごとに InternalTimesheet としてまとめていきます。この部分が、まさにCSVの生データをプログラムが理解できる形に変換する核心部となります。

まとめ

この章では、社内用のCSV形式の勤務表ファイルを読み込む専門家、社内勤務表リーダー (InternalTimesheetReader) について学びました。

これで、ファイル選択 (FileSelector) で選ばれた社内勤務表ファイルが、どのようにしてプログラム内部で扱えるデータ形式になるのかが理解できたはずです。

しかし、比較するためにはもう一つ、客先の勤務表データも必要ですよね。客先の勤務表はPDFやExcelなど、様々な形式がありえます。次の章では、これらの客先勤務表ファイルを読み込むためのリーダーについて見ていきましょう。

次の章: 第6章: 客先勤務表リーダー (ClientTimesheetReader)


Generated by AI Codebase Knowledge Builder