with one click
dotnet-generic-matching
こんなときに使う: .NETドメイン層で汎用的な重み付きフィールドマッチングとスコアリングを実装。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
こんなときに使う: .NETドメイン層で汎用的な重み付きフィールドマッチングとスコアリングを実装。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | dotnet-generic-matching |
| description | こんなときに使う: .NETドメイン層で汎用的な重み付きフィールドマッチングとスコアリングを実装。 |
ドメイン層で汎用的かつ再利用可能なフィールドマッチングシステムを構築するためのエンドツーエンドワークフロー:比較結果とスコアの値オブジェクト、類似度ユーティリティ(レーベンシュタイン距離、数値比較)、汎用 FieldMatchingService<TSource, TCandidate>、および品質閾値のためのSpecificationパターン。
以下の場合にこのスキルを使用してください:
前提条件:
dotnet-ocr-matching-workflow — このマッチング基盤をOCR-データベースレコードマッチングに使用dotnet-oracle-wpf-integration — マッチング用の候補データをOracleから提供dotnet-wpf-comparison-view — マッチング結果をWPF比較UIで表示tdd-standard-practice — Red-Green-Refactorで生成コードをテストgit-commit-practices — 各ステップをアトミックな変更としてコミットFieldMatchingService<TSource, TCandidate> は任意のエンティティペアで動作(成長の複利)decimal 型を使用。float や double は禁止(基礎と型)フィールド比較、スコアリング、マッチング結果の不変な結果型を定義するときに使用します。
マッチングユースケースディレクトリ配下のドメイン層に値オブジェクトを作成します(例:Mercury.Domain/Matching/)。
FieldComparison.cs — 単一フィールドの比較結果:
namespace Mercury.Domain.Matching
{
public class FieldComparison
{
public FieldComparison(string fieldName, string sourceAValue, string sourceBValue, double similarity)
{
FieldName = fieldName;
SourceAValue = sourceAValue;
SourceBValue = sourceBValue;
Similarity = similarity;
IsMatch = similarity >= 0.8;
}
public string FieldName { get; }
public string SourceAValue { get; }
public string SourceBValue { get; }
public double Similarity { get; }
public bool IsMatch { get; }
}
}
MatchingScore.cs — 重み付きスコア集計:
namespace Mercury.Domain.Matching
{
public class MatchingScore
{
public MatchingScore(IEnumerable<FieldComparison> comparisons, Dictionary<string, double> weights)
{
Comparisons = comparisons.ToList();
double totalScore = 0, totalWeight = 0;
foreach (var comp in Comparisons)
{
if (weights.TryGetValue(comp.FieldName, out var weight))
{
totalScore += comp.Similarity * weight;
totalWeight += weight;
}
}
ScorePercent = totalWeight > 0 ? (totalScore / totalWeight) * 100 : 0;
}
public IReadOnlyList<FieldComparison> Comparisons { get; }
public double ScorePercent { get; }
}
}
MatchingResult.cs — ソースと最良候補を紐付ける汎用結果:
namespace Mercury.Domain.Matching
{
public class MatchingResult<TSource, TCandidate>
{
public MatchingResult(TSource source, TCandidate? bestMatch, MatchingScore score, double successThreshold)
{
Source = source;
BestMatch = bestMatch;
Score = score;
SuccessThreshold = successThreshold;
}
public TSource Source { get; }
public TCandidate? BestMatch { get; }
public MatchingScore Score { get; }
public bool IsSuccessful => Score.ScorePercent >= SuccessThreshold;
public double SuccessThreshold { get; }
}
}
Values: 基礎と型 / 成長の複利
文字列および数値の比較関数をフィールドマッチング用に構築するときに使用します。
ドメイン層に静的ユーティリティを作成します。レーベンシュタイン距離(0.0〜1.0に正規化)、金額値の厳密なdecimal比較、寸法のための許容範囲ベースのdouble比較をサポートします。
SimilarityCalculator.cs:
namespace Mercury.Domain.Matching
{
public static class SimilarityCalculator
{
/// <summary>
/// Normalized Levenshtein similarity (0.0 = completely different, 1.0 = identical).
/// Strings are normalized before comparison (lowercase, whitespace removed).
/// </summary>
public static double StringSimilarity(string? s1, string? s2)
{
var a = Normalize(s1);
var b = Normalize(s2);
if (a.Length == 0 && b.Length == 0) return 1.0;
if (a.Length == 0 || b.Length == 0) return 0.0;
int maxLen = Math.Max(a.Length, b.Length);
int distance = LevenshteinDistance(a, b);
return 1.0 - (double)distance / maxLen;
}
/// <summary>
/// Exact match for monetary values. Use decimal to avoid floating-point errors.
/// Returns 1.0 if equal, 0.0 otherwise.
/// </summary>
public static double NumericSimilarityDecimal(decimal a, decimal b)
=> a == b ? 1.0 : 0.0;
/// <summary>
/// Tolerance-based comparison for dimensions (width, height, weight).
/// Parses strings to double; returns similarity based on relative difference.
/// </summary>
public static double NumericSimilarityDouble(string? s1, string? s2)
{
if (!double.TryParse(Normalize(s1), out var a)
|| !double.TryParse(Normalize(s2), out var b))
return 0.0;
if (a == 0 && b == 0) return 1.0;
double maxVal = Math.Max(Math.Abs(a), Math.Abs(b));
if (maxVal == 0) return 1.0;
double diff = Math.Abs(a - b) / maxVal;
return Math.Max(0.0, 1.0 - diff);
}
private static string Normalize(string? value)
=> (value ?? string.Empty).Trim().Replace(" ", "").Replace(" ", "").ToLowerInvariant();
private static int LevenshteinDistance(string s1, string s2)
{
int m = s1.Length, n = s2.Length;
var dp = new int[m + 1, n + 1];
for (int i = 0; i <= m; i++) dp[i, 0] = i;
for (int j = 0; j <= n; j++) dp[0, j] = j;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
{
int cost = s1[i - 1] == s2[j - 1] ? 0 : 1;
dp[i, j] = Math.Min(
Math.Min(dp[i - 1, j] + 1, dp[i, j - 1] + 1),
dp[i - 1, j - 1] + cost);
}
return dp[m, n];
}
}
}
⚠️ 重要: 金額値(単価、合計金額)には decimal を使用してください。金額に float や double を使用しないでください — 浮動小数点の丸め誤差が誤った不一致を引き起こします。
Values: 基礎と型 / ニュートラル
ソースを候補と比較する汎用マッチングサービスを構築するときに使用します。
設定可能なフィールド定義を持つ FieldMatchingService<TSource, TCandidate> を作成します。各フィールド定義は、値の抽出方法と使用する比較関数を指定します。
FieldDefinition.cs — マッチング可能な1つのフィールドを記述:
namespace Mercury.Domain.Matching
{
public class FieldDefinition<TSource, TCandidate>
{
public string FieldName { get; init; } = string.Empty;
public Func<TSource, string> SourceExtractor { get; init; } = _ => string.Empty;
public Func<TCandidate, string> CandidateExtractor { get; init; } = _ => string.Empty;
public Func<string, string, double> CompareFunction { get; init; }
= SimilarityCalculator.StringSimilarity;
public double Weight { get; init; } = 1.0;
}
}
FieldMatchingService.cs — 汎用マッチングエンジン:
namespace Mercury.Domain.Matching
{
public class FieldMatchingService<TSource, TCandidate>
{
private readonly List<FieldDefinition<TSource, TCandidate>> _fields;
private readonly double _successThreshold;
public FieldMatchingService(
IEnumerable<FieldDefinition<TSource, TCandidate>> fields,
double successThreshold = 70.0)
{
_fields = fields.ToList();
_successThreshold = successThreshold;
}
public MatchingResult<TSource, TCandidate> FindBestMatch(
TSource source, IEnumerable<TCandidate> candidates)
{
var candidateList = candidates.ToList();
if (candidateList.Count == 0)
return new MatchingResult<TSource, TCandidate>(
source, default, new MatchingScore([], BuildWeights()), _successThreshold);
MatchingScore? bestScore = null;
TCandidate? bestCandidate = default;
foreach (var candidate in candidateList)
{
var comparisons = _fields.Select(f => new FieldComparison(
f.FieldName,
f.SourceExtractor(source),
f.CandidateExtractor(candidate),
f.CompareFunction(f.SourceExtractor(source), f.CandidateExtractor(candidate))
)).ToList();
var score = new MatchingScore(comparisons, BuildWeights());
if (bestScore == null || score.ScorePercent > bestScore.ScorePercent)
{
bestScore = score;
bestCandidate = candidate;
}
}
return new MatchingResult<TSource, TCandidate>(
source, bestCandidate, bestScore!, _successThreshold);
}
private Dictionary<string, double> BuildWeights()
=> _fields.ToDictionary(f => f.FieldName, f => f.Weight);
}
}
ユーザーに確認: マッチングするフィールド、割り当てる重み、各フィールドの比較タイプ(文字列/数値/厳密一致)を確認してください。
Values: 成長の複利 / 基礎と型
マッチング結果に対する品質制約をファーストクラスのドメイン概念として適用するときに使用します。
マッチング結果のセットが最低品質閾値を満たしているかどうかを検証するSpecificationを作成します。これにより閾値ロジックがアプリケーション層にマジックナンバーとして散在するのではなく、ドメイン層に保持されます。
ISpecification.cs:
namespace Mercury.Domain.Matching
{
public interface ISpecification<T>
{
bool IsSatisfiedBy(T entity);
}
}
HighQualityMatchingSpecification.cs:
namespace Mercury.Domain.Matching
{
public class HighQualityMatchingSpecification<TSource, TCandidate>
: ISpecification<IEnumerable<MatchingResult<TSource, TCandidate>>>
{
private readonly double _minimumScorePercent;
public HighQualityMatchingSpecification(double minimumScorePercent = 70.0)
{
_minimumScorePercent = minimumScorePercent;
}
public bool IsSatisfiedBy(IEnumerable<MatchingResult<TSource, TCandidate>> results)
{
var list = results.ToList();
return list.Any()
&& list.All(r => r.Score.ScorePercent >= _minimumScorePercent);
}
}
}
使用例:
var spec = new HighQualityMatchingSpecification<OrderSheet, SofRecord>(minimumScorePercent: 80.0);
bool allHighQuality = spec.IsSatisfiedBy(matchingResults);
Values: 基礎と型 / 継続は力
マッチングサービスをオーケストレーションするユースケースを作成するときに使用します。
アプリケーション層のユースケースが候補の読み込み、マッチングサービスの実行、結果の返却を調整します。マッチングロジックは含まず — それはドメイン層に留まります。
namespace Mercury.Application.UseCases.Matching
{
public class MatchOrderWithSofUseCase
{
private readonly ISofRepository _sofRepository;
private readonly FieldMatchingService<OrderSheet, SofRecord> _matchingService;
public MatchOrderWithSofUseCase(
ISofRepository sofRepository,
FieldMatchingService<OrderSheet, SofRecord> matchingService)
{
_sofRepository = sofRepository;
_matchingService = matchingService;
}
public async Task<List<MatchingResult<OrderSheet, SofRecord>>> ExecuteAsync(
IEnumerable<OrderSheet> orders)
{
var candidates = await _sofRepository.GetAllAsync();
return orders
.Select(order => _matchingService.FindBestMatch(order, candidates))
.ToList();
}
}
}
Values: 基礎と型 / 成長の複利
特定のドメイン(例:注文書-SOFマッチング)用にマッチングサービスを設定するときに使用します。
ドメインエンティティに対して抽出関数、比較関数、重みを持つフィールド定義を定義します:
| フィールド | 比較タイプ | 重み | 理由 |
|---|---|---|---|
| 品名 | StringSimilarity | 3.0 | 主要識別子。OCRエラーが発生しやすい |
| 単価 | NumericSimilarityDecimal | 2.0 | 金額は厳密一致が必要 |
| 数量 | NumericSimilarityDouble | 1.5 | 許容範囲付きの数値比較 |
| 得意先コード | StringSimilarity | 2.0 | 主要な紐付けフィールド |
| 寸法 | NumericSimilarityDouble | 1.0 | 補助フィールド |
設定例:
var fields = new List<FieldDefinition<OrderSheet, SofRecord>>
{
new()
{
FieldName = "ProductName",
SourceExtractor = o => o.ProductName,
CandidateExtractor = s => s.ProductName,
CompareFunction = SimilarityCalculator.StringSimilarity,
Weight = 3.0
},
new()
{
FieldName = "UnitPrice",
SourceExtractor = o => o.UnitPrice.ToString(),
CandidateExtractor = s => s.UnitPrice.ToString(),
CompareFunction = (a, b) =>
decimal.TryParse(a, out var da) && decimal.TryParse(b, out var db)
? SimilarityCalculator.NumericSimilarityDecimal(da, db)
: 0.0,
Weight = 2.0
},
new()
{
FieldName = "Quantity",
SourceExtractor = o => o.Quantity.ToString(),
CandidateExtractor = s => s.Quantity.ToString(),
CompareFunction = SimilarityCalculator.NumericSimilarityDouble,
Weight = 1.5
}
};
var service = new FieldMatchingService<OrderSheet, SofRecord>(fields, successThreshold: 70.0);
Values: 継続は力 / 成長の複利
✅ 金額フィールドの比較(単価、合計金額)には必ず decimal を使用してください。浮動小数点型(float、double)は丸め誤差を引き起こし、厳密値での誤った不一致の原因になります。
// ✅ 正しい — 金額の厳密比較
public static double NumericSimilarityDecimal(decimal a, decimal b)
=> a == b ? 1.0 : 0.0;
✅ 類似度計算前に空白(全角スペースを含む)を除去し、小文字に変換してください。書式の違いによるスコア低下を防ぎます。
private static string Normalize(string? value)
=> (value ?? string.Empty).Trim().Replace(" ", "").Replace(" ", "").ToLowerInvariant();
✅ FieldDefinition の設定で重みを渡してください。異なるマッチングシナリオ(注文マッチング vs 在庫マッチング)では異なる重み配分が必要になる場合があります。
Problem: double の演算は丸め誤差を生じ(例:0.1 + 0.2 != 0.3)、厳密一致の金額比較が失敗します。
Solution: すべての金額値に decimal を使用し、比較には NumericSimilarityDecimal を使用してください。
// ❌ 間違い — 金額に浮動小数点比較
double price1 = 1234.56;
double price2 = 1234.56;
bool match = Math.Abs(price1 - price2) < 0.01; // 脆弱
// ✅ 正しい — decimalの厳密比較
decimal price1 = 1234.56m;
decimal price2 = 1234.56m;
bool match = price1 == price2; // 信頼性が高い
Problem: "ProductName" のようなフィールド名をマッチングループに直接埋め込むと、サービスが再利用できなくなります。
Solution: 設定可能な抽出関数と名前を持つ FieldDefinition<TSource, TCandidate> を使用してください。
Problem: 候補ゼロで FindBestMatch を呼び出すと NullReferenceException や誤った「100%一致」結果が発生します。
Solution: 候補リストが空の場合はスコアゼロの MatchingResult を返してください。
if (candidateList.Count == 0)
return new MatchingResult<TSource, TCandidate>(
source, default, new MatchingScore([], BuildWeights()), _successThreshold);
What: WPF ViewModelで類似度スコアの計算やマッチングループを実行すること。
Why It's Wrong: DDDのレイヤリングに違反します。マッチングはドメインロジックであり、UI依存なしでテスト可能であるべきです。
Better Approach: すべてのマッチングをドメイン層(FieldMatchingService)に保持してください。ViewModelはアプリケーション層のユースケースを呼び出し、結果をバインドするだけです。
What: 数値をパースせずに "1234.56" と "1234.560" を文字列として比較すること。
Why It's Wrong: 文字列比較では "1234.56" と "1234.560" は異なるものとして扱われます(レーベンシュタイン距離 = 1)が、同じ値を表しています。
Better Approach: 金額値には NumericSimilarityDecimal、寸法値には NumericSimilarityDouble を使用してください。
// ❌ 間違い — 数値に文字列比較
SimilarityCalculator.StringSimilarity("1234.56", "1234.560"); // ~0.93、1.0ではない
// ✅ 正しい — 数値比較
SimilarityCalculator.NumericSimilarityDecimal(1234.56m, 1234.560m); // 1.0
FieldComparison 値オブジェクトを作成(Step 1)MatchingScore を作成(Step 1)MatchingResult<TSource, TCandidate> 汎用結果を作成(Step 1)SimilarityCalculator を実装(Step 2)FieldDefinition<TSource, TCandidate> 設定クラスを作成(Step 3)FieldMatchingService<TSource, TCandidate> を実装(Step 3)ISpecification<T> と HighQualityMatchingSpecification を追加(Step 4)decimal 比較を使用していること| データ型 | 比較関数 | 戻り値 | 用途 |
|---|---|---|---|
| テキスト(名前、コード) | StringSimilarity | 0.0〜1.0(レーベンシュタイン) | 品名、得意先コード |
| 金額(価格、合計) | NumericSimilarityDecimal | 0.0 または 1.0(厳密一致) | 単価、合計金額 |
| 寸法(サイズ、重量) | NumericSimilarityDouble | 0.0〜1.0(許容範囲) | 幅、高さ、重量 |
| 優先度 | 重み範囲 | フィールド例 |
|---|---|---|
| 🆕 主要識別子 | 2.5〜3.0 | 品名、注文番号 |
| ✅ 主要紐付けフィールド | 1.5〜2.5 | 得意先コード、単価 |
| 補助 | 0.5〜1.5 | 寸法、数量 |
| ❌ 低信頼度 | 0.1〜0.5 | 自由記述の備考 |
dotnet-ocr-matching-workflow — このスキルを使用した完全なOCR-データベースマッチングワークフローdotnet-wpf-comparison-view — マッチング結果表示用のWPF UI| バージョン | 日付 | 変更内容 |
|---|---|---|
| 1.0.0 | 2025-07-13 | 🆕 初回リリース — 重み付きスコアリング付き汎用マッチング |
モダン C#(12+)で record、パターンマッチング、合成、Result 型エラーハンドリングを使った 慣用的で高性能なコードを書く。こんなときに使う: 新規 C# コードの作成、API 設計、 または C# 12+ イディオムへのリファクタリング。
こんなときに使う: WPFアプリケーションに社員番号入力ダイアログを追加し、 DPAPIで暗号化された設定として社員IDを安全に保存したいとき。
こんなときに使う: 繰り返し発生する業務問い合わせ(監査、アンケート、コンプライアンス調査)に対して、 過去の回答実績とエビデンスを活用して回答するためのテンプレートスキル。 再利用可能な7ステップワークフローとスキャフォールディングファイルを提供する。
こんなときに使う: PDFテキスト抽出、Optical Character Recognition (OCR)、結合/分割、フォーム処理をuvベースの再現可能コマンドで実行したいときに使う。
こんなときに使う: Migrate Access SQL to Oracle, generate .NET C# code. Use when converting Access queries to Oracle.
こんなときに使う: dotnet-tools.json によるローカル .NET ツールの管理。開発環境と CI/CD パイプライン全体で バージョン固定された一貫したツール環境を構築。リポジトリ単位の CLI ツール管理時に使用。