Analyze&デザパタ
以下では、Strategyパターンを用いて、解析ロジックを柔軟に切り替えられるように設計します。呼び出し側は統一されたインターフェースを通じてデータの解析を行うことができます。
1. クラス設計の概要
IParserインターフェース:
解析処理の共通インターフェースを定義します。
Parseメソッドを持ち、入力データから標準化されたデータを返します。
BaseParserクラス:
IParserインターフェースを実装する抽象クラス。
共通の解析処理やヘルパーメソッドを提供します。
ConcreteParserクラス:
BaseParserを継承し、具体的な解析ロジックを実装します。
必要に応じて複数のConcreteParserを作成できます。
ParserFactoryクラス:
入力データに応じて適切なParserインスタンスを生成します。
呼び出し側はFactoryを通じてParserを取得します。
DataProcessorクラス:
Parserを利用してデータの解析と標準化を行います。
呼び出し側はDataProcessorを使用してデータを処理します。
2. コード実装
2.1 IParserインターフェース
public interface IParser
{
List<StandardizedData> Parse(DataTable dataTable);
}
2.2 StandardizedDataクラス
標準化されたデータを表すクラスです。
public class StandardizedData
{
public string SubmissionType { get; set; } // 提出区分
public string OfficeName { get; set; } // 事業所名
public string Location { get; set; } // 所在地
public int EmployeeCount { get; set; } // 従業員数
public int TempEmployeeCount { get; set; } // 仮従業員数
// 他の必要な項目があれば追加
}
2.3 BaseParserクラス
共通の処理やヘルパーメソッドを提供します。
using System.Text.RegularExpressions;
public abstract class BaseParser : IParser
{
public abstract List<StandardizedData> Parse(DataTable dataTable);
// 括弧内のテキストを抽出するヘルパーメソッド
protected string ExtractTextInBrackets(string input)
{
if (string.IsNullOrEmpty(input)) return string.Empty;
var match = Regex.Match(input, @"\(|((.*?)\)|)");
if (match.Success)
{
return match.Groups[1].Value;
}
return string.Empty;
}
// 括弧の前のテキストを抽出するヘルパーメソッド
protected string ExtractTextBeforeBrackets(string input)
{
if (string.IsNullOrEmpty(input)) return string.Empty;
var match = Regex.Match(input, @"^(.*?)\s*\(|(");
if (match.Success)
{
return match.Groups[1].Value.Trim();
}
return input; // 括弧がない場合は入力全体を返す
}
// 数値と仮数値を抽出するヘルパーメソッド
protected (int mainValue, int tempValue) ExtractNumbers(string input)
{
int mainValue = 0;
int tempValue = 0;
if (string.IsNullOrEmpty(input)) return (0, 0);
// メインの数値を抽出(先頭の数字)
var mainMatch = Regex.Match(input, @"^\s*(\d+)");
if (mainMatch.Success)
{
int.TryParse(mainMatch.Groups[1].Value, out mainValue);
}
// 括弧内の数値を抽出
var tempMatch = Regex.Match(input, @"\(|(\s*(\d+)\s*\)|)");
if (tempMatch.Success)
{
int.TryParse(tempMatch.Groups[1].Value, out tempValue);
}
return (mainValue, tempValue);
}
}
2.4 ConcreteParserクラス
具体的な解析ロジックを実装します。
public class ConcreteParser : BaseParser
{
public override List<StandardizedData> Parse(DataTable dataTable)
{
var result = new List<StandardizedData>();
if (dataTable == null || dataTable.Rows.Count == 0)
{
return result;
}
// ヘッダ行を取得
var headers = dataTable.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToList();
// 各項目の列インデックスを特定
int submissionTypeIndex = GetColumnIndex(headers, new string[] { "提出区分", "会社名", "提出会社" });
int officeNameIndex = GetColumnIndex(headers, new string[] { "事業所名", "事業所(所在地)", "事業所名-事業所名" });
int employeeCountIndex = GetColumnIndex(headers, new string[] { "従業員数", "従業員数(名)", "従業員数(人)" });
// データ行の処理
foreach (DataRow row in dataTable.Rows)
{
var data = new StandardizedData();
// 提出区分の取得
if (submissionTypeIndex != -1)
{
data.SubmissionType = row[submissionTypeIndex]?.ToString().Trim();
}
// 事業所名と所在地の取得
if (officeNameIndex != -1)
{
string officeCell = row[officeNameIndex]?.ToString().Trim();
data.OfficeName = ExtractTextBeforeBrackets(officeCell);
data.Location = ExtractTextInBrackets(officeCell);
}
// 従業員数と仮従業員数の取得
if (employeeCountIndex != -1)
{
string employeeCell = row[employeeCountIndex]?.ToString().Trim();
var (mainValue, tempValue) = ExtractNumbers(employeeCell);
data.EmployeeCount = mainValue;
data.TempEmployeeCount = tempValue;
}
// 必要に応じて他の項目も取得
result.Add(data);
}
return result;
}
// 列名のリストから、指定した候補名のいずれかにマッチする列のインデックスを取得する
private int GetColumnIndex(List<string> headers, string[] possibleNames)
{
for (int i = 0; i < headers.Count; i++)
{
foreach (var name in possibleNames)
{
if (headers[i].Contains(name))
{
return i;
}
}
}
return -1; // 見つからない場合
}
}
2.6 DataProcessorクラス
Parserを利用してデータの解析と標準化を行います。
public class DataProcessor
{
public List<StandardizedData> ProcessData(DataTable dataTable)
{
IParser parser = ParserFactory.GetParser(dataTable);
return parser.Parse(dataTable);
}
}
2.7 使用例
// CSVファイルをDataTableに読み込む(既存のCSV読み込みロジックを使用)
DataTable dt = LoadCsvToDataTable("input.csv");
// DataProcessorを使用してデータを処理
DataProcessor processor = new DataProcessor();
List<StandardizedData> standardizedData = processor.ProcessData(dt);
// 結果を表示またはCSVに出力
foreach (var data in standardizedData)
{
Console.WriteLine($"提出区分: {data.SubmissionType}, 事業所名: {data.OfficeName}, 所在地: {data.Location}, 従業員数: {data.EmployeeCount}, 仮従業員数: {data.TempEmployeeCount}");
}
// 必要に応じてCSVに出力する処理を追加
3. コードの説明
3.1 IParserインターフェース
役割:
解析処理の共通インターフェースを定義します。
Parseメソッドを実装することで、異なる解析ロジックを持つクラスでも統一された方法でデータを解析できます。
3.2 BaseParserクラス
役割:
IParserを実装する抽象クラスで、共通のヘルパーメソッドを提供します。
具体的な解析ロジックは派生クラスで実装します。
ヘルパーメソッド:
ExtractTextInBrackets:括弧内のテキストを抽出します。
ExtractTextBeforeBrackets:括弧の前のテキストを抽出します。
ExtractNumbers:数値と仮数値を抽出します。
3.3 ConcreteParserクラス
役割:
BaseParserを継承し、具体的な解析ロジックを実装します。
主な処理:
ヘッダ行から必要な項目の列インデックスを特定します。
データ行をループし、各行の必要な情報を抽出します。
ヘルパーメソッドを利用して、事業所名と所在地、従業員数と仮従業員数を抽出します。
柔軟性:
GetColumnIndexメソッドを使用して、異なる列名にも対応できます。
3.4 ParserFactoryクラス
役割:
入力データに応じて適切なParserインスタンスを返します。
データのヘッダや内容をチェックして、複数のConcreteParserから最適なものを選択できます。
将来の拡張:
新しいデータ形式に対応するParserを追加し、Factoryでの選択ロジックを拡張できます。
3.5 DataProcessorクラス
役割:
Parserを利用してデータの解析を行います。
呼び出し側はDataProcessorを使用するだけで、内部のParserの種類を意識する必要がありません。
3.6 使用例
流れ:
CSVファイルをDataTableに読み込みます(既存のロジックを使用)。
DataProcessorを使用してデータを処理し、標準化されたデータを取得します。
結果を表示またはCSVに出力します。
4. 考慮点と拡張性
異なるデータ形式への対応:
データ形式が異なる場合、新たなConcreteParserを作成し、ParserFactoryでの選択ロジックを追加することで対応可能です。
エラーハンドリング:
解析中にエラーが発生した場合でも、他のデータの解析を継続できるように、適切なエラーハンドリングを実装します。
デザインパターンの活用:
Strategyパターンを用いることで、解析ロジックの変更や追加が容易になります。
Factoryパターンにより、適切なParserを動的に選択できます。
パフォーマンスの最適化:
大量のデータを処理する場合、正規表現のパフォーマンスに注意し、必要に応じて事前コンパイルやキャッシュを検討します。
5. まとめ
目的:
各企業の設備情報から指定された項目を正確に抽出し、標準化するためのクラスを作成しました。
解析ロジックが異なる場合でも、呼び出し側がその違いを意識する必要がないように設計しています。
特徴:
柔軟性: 異なるデータ形式や解析ロジックに対応可能。
拡張性: 新しいParserを追加することで、容易に機能拡張が可能。
再利用性: 共通の処理をBaseParserに集約し、コードの重複を防止。
使用方法:
DataProcessorを使用してデータを処理するだけで、標準化されたデータを取得できます。