From b38f9b039077dceab859189f3993600c0c1e6c63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=82=B3=E3=83=88=E3=83=AC?=
<102813037+cotore-game@users.noreply.github.com>
Date: Tue, 21 Jul 2026 12:55:09 +0900
Subject: [PATCH 01/28] =?UTF-8?q?refactor:=20=E6=9C=AA=E5=AE=8C=E6=88=90?=
=?UTF-8?q?=E3=81=AE=E5=A4=96=E9=83=A8=E3=82=AD=E3=83=BC=E5=88=B6=E7=B4=84?=
=?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../CSVLoader/Editor/CsvInspectorEditor.cs | 2 -
.../CsvValidation/CsvValidationAttributes.cs | 28 ---------
.../CsvValidation/CsvValidationContext.cs | 44 --------------
.../CsvValidationContext.cs.meta | 2 -
.../CsvValidation/CsvValidationSchema.cs | 5 --
.../Runtime/CsvValidation/CsvValidator.cs | 60 +------------------
6 files changed, 3 insertions(+), 138 deletions(-)
delete mode 100644 Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationContext.cs
delete mode 100644 Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationContext.cs.meta
diff --git a/Assets/Plugins/CSVLoader/Editor/CsvInspectorEditor.cs b/Assets/Plugins/CSVLoader/Editor/CsvInspectorEditor.cs
index 8a663c1..5a47ff0 100644
--- a/Assets/Plugins/CSVLoader/Editor/CsvInspectorEditor.cs
+++ b/Assets/Plugins/CSVLoader/Editor/CsvInspectorEditor.cs
@@ -314,8 +314,6 @@ private static string GetAttributeDisplayText(Attribute attribute)
return $"[MinLength: {minLength.MinLength}]";
case MaxLengthAttribute maxLength:
return $"[MaxLength: {maxLength.MaxLength}]";
- case ForeignKeyAttribute foreignKey:
- return $"[ForeignKey: {foreignKey.ReferenceEnumType.Name}.{foreignKey.ReferenceField}]";
default:
return null;
}
diff --git a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationAttributes.cs b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationAttributes.cs
index d028459..88f79d8 100644
--- a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationAttributes.cs
+++ b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationAttributes.cs
@@ -113,34 +113,6 @@ public AllowedValuesAttribute(params object[] allowedValues)
}
}
- ///
- /// セル文字列が参照先テーブルの指定列に存在することを要求します。
- ///
- ///
- /// 同じEnum型を指定した場合は検証対象テーブル内を参照します。別のEnum型を指定する場合は、
- /// で参照先を登録します。
- ///
- [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
- public class ForeignKeyAttribute : Attribute
- {
- /// 参照先テーブルを識別するEnum型を取得します。
- public Type ReferenceEnumType { get; }
-
- /// 参照先のヘッダー名を取得します。
- public string ReferenceField { get; }
-
- ///
- /// 外部キー制約を設定します。
- ///
- /// 参照先テーブルを識別するEnum型。
- /// 参照先のヘッダー名。
- public ForeignKeyAttribute(Type referenceEnumType, string referenceField)
- {
- ReferenceEnumType = referenceEnumType;
- ReferenceField = referenceField;
- }
- }
-
///
/// セル文字列が指定した最小長以上であることを要求します。
///
diff --git a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationContext.cs b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationContext.cs
deleted file mode 100644
index 1683ea0..0000000
--- a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationContext.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace CSV4Unity.Validation
-{
- ///
- /// ForeignKey検証で参照する別CSVテーブルを保持します。
- ///
- /// 参照先はEnum型をキーとして保持し、同じ型を再登録した場合は後のテーブルで置き換えます。
- public sealed class CsvValidationContext
- {
- private readonly Dictionary _documents = new Dictionary();
-
- /// Enum型を識別子として参照先テーブルを登録します。
- /// 参照先テーブルの列を表すEnum型。
- /// 登録する参照先テーブル。
- /// 連続して登録できるよう、このContext自身を返します。
- /// がです。
- public CsvValidationContext Register(CsvTable table) where TField : struct, Enum
- {
- if (table == null) throw new ArgumentNullException(nameof(table));
- _documents[typeof(TField)] = table.Document;
- return this;
- }
-
- internal bool TryGetColumn(Type enumType, string fieldName, out CsvColumn column)
- {
- if (_documents.TryGetValue(enumType, out CsvDocument document))
- {
- try
- {
- column = document.Column(fieldName);
- return true;
- }
- catch (KeyNotFoundException)
- {
- }
- }
-
- column = default;
- return false;
- }
- }
-}
diff --git a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationContext.cs.meta b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationContext.cs.meta
deleted file mode 100644
index c48d8b7..0000000
--- a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationContext.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: f3ba2c68f6acaa74dad9131c9425750d
\ No newline at end of file
diff --git a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationSchema.cs b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationSchema.cs
index 23985f7..0a598df 100644
--- a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationSchema.cs
+++ b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationSchema.cs
@@ -101,10 +101,6 @@ private static CsvFieldValidationRule CreateRule(FieldInfo fieldInfo, ob
rule.MaxLength = maxLength.MaxLength;
hasConstraint = true;
break;
- case ForeignKeyAttribute foreignKey:
- rule.ForeignKey = foreignKey;
- hasConstraint = true;
- break;
}
}
@@ -137,6 +133,5 @@ internal sealed class CsvFieldValidationRule where TField : struct, Enum
public HashSet AllowedValues { get; set; }
public int? MinLength { get; set; }
public int? MaxLength { get; set; }
- public ForeignKeyAttribute ForeignKey { get; set; }
}
}
diff --git a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidator.cs b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidator.cs
index 36e9348..272f87d 100644
--- a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidator.cs
+++ b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidator.cs
@@ -18,19 +18,16 @@ public static class CsvValidator
///
/// 使用するValidationスキーマ。の場合はを使用します。
///
- /// ForeignKeyの参照先テーブル。参照先が同じテーブルだけの場合はにできます。
/// 型変換と数値範囲検証に使用する形式。の場合はを使用します。
/// すべてのエラーとWarningを格納したValidation結果。
/// がです。
///
/// PrimaryKeyは空でなく一意、Uniqueは空セルを除いて一意であることを大文字小文字を区別して検証します。
- /// ForeignKeyの参照テーブルまたは列を解決できない場合、その制約を実行せずWarningを追加します。
/// 空セルはPrimaryKeyとNotNullを除くセル単位制約の対象外です。
///
public static CsvValidationResult Validate(
CsvTable table,
CsvValidationSchema validationSchema = null,
- CsvValidationContext context = null,
IFormatProvider formatProvider = null)
where TField : struct, Enum
{
@@ -43,7 +40,7 @@ public static CsvValidationResult Validate(
IReadOnlyList> rules = schema.Rules;
for (int i = 0; i < rules.Count; i++)
{
- ValidateField(table, rules[i], context, provider, result);
+ ValidateField(table, rules[i], provider, result);
}
return result;
@@ -52,7 +49,6 @@ public static CsvValidationResult Validate(
private static void ValidateField(
CsvTable table,
CsvFieldValidationRule rule,
- CsvValidationContext context,
IFormatProvider formatProvider,
CsvValidationResult result)
where TField : struct, Enum
@@ -69,8 +65,6 @@ private static void ValidateField(
ValidateDistinct(column, rule.FieldName, false, result);
}
- HashSet referenceValues = PrepareForeignKeyValues(table, rule, context, result);
-
for (int rowIndex = 0; rowIndex < column.Count; rowIndex++)
{
CsvCell cell = column[rowIndex];
@@ -82,7 +76,7 @@ private static void ValidateField(
if (cell.IsEmpty) continue;
- ValidateCell(cell, rowIndex, rule, formatProvider, referenceValues, result);
+ ValidateCell(cell, rowIndex, rule, formatProvider, result);
}
}
@@ -91,7 +85,6 @@ private static void ValidateCell(
int rowIndex,
CsvFieldValidationRule rule,
IFormatProvider formatProvider,
- HashSet referenceValues,
CsvValidationResult result)
where TField : struct, Enum
{
@@ -122,8 +115,7 @@ private static void ValidateCell(
}
bool requiresString = rule.Pattern != null || rule.AllowedValues != null ||
- rule.MinLength.HasValue || rule.MaxLength.HasValue ||
- referenceValues != null;
+ rule.MinLength.HasValue || rule.MaxLength.HasValue;
if (!requiresString) return;
string text = cell.GetString();
@@ -152,11 +144,6 @@ private static void ValidateCell(
rule.FieldName,
$"Length {text.Length} exceeds the maximum {rule.MaxLength.Value}.");
}
-
- if (referenceValues != null && !referenceValues.Contains(text))
- {
- result.AddError(rowIndex, rule.FieldName, $"Referenced value '{text}' was not found.");
- }
}
private static void ValidateDistinct(
@@ -189,46 +176,5 @@ private static void ValidateDistinct(
}
}
}
-
- private static HashSet PrepareForeignKeyValues(
- CsvTable table,
- CsvFieldValidationRule rule,
- CsvValidationContext context,
- CsvValidationResult result)
- where TField : struct, Enum
- {
- if (rule.ForeignKey == null) return null;
-
- CsvColumn referenceColumn;
- if (rule.ForeignKey.ReferenceEnumType == typeof(TField))
- {
- try
- {
- referenceColumn = table.Document.Column(rule.ForeignKey.ReferenceField);
- }
- catch (KeyNotFoundException)
- {
- result.AddWarning(-1, rule.FieldName, "Foreign key reference column was not found.");
- return null;
- }
- }
- else if (context == null || !context.TryGetColumn(
- rule.ForeignKey.ReferenceEnumType,
- rule.ForeignKey.ReferenceField,
- out referenceColumn))
- {
- result.AddWarning(-1, rule.FieldName, "Foreign key reference table is not registered.");
- return null;
- }
-
- var values = new HashSet(StringComparer.Ordinal);
- for (int rowIndex = 0; rowIndex < referenceColumn.Count; rowIndex++)
- {
- CsvCell cell = referenceColumn[rowIndex];
- if (!cell.IsEmpty) values.Add(cell.GetString());
- }
-
- return values;
- }
}
}
From da30960f6f85daaa126298a09869b44d516b49cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=82=B3=E3=83=88=E3=83=AC?=
<102813037+cotore-game@users.noreply.github.com>
Date: Tue, 21 Jul 2026 12:55:19 +0900
Subject: [PATCH 02/28] =?UTF-8?q?test:=20=E5=A4=96=E9=83=A8=E3=82=AD?=
=?UTF-8?q?=E3=83=BC=E5=88=B6=E7=B4=84=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?=
=?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Tests/EditMode/CsvValidationTableTests.cs | 39 -------------------
1 file changed, 39 deletions(-)
diff --git a/Assets/Scripts/Tests/EditMode/CsvValidationTableTests.cs b/Assets/Scripts/Tests/EditMode/CsvValidationTableTests.cs
index 4b7d618..d59ddf2 100644
--- a/Assets/Scripts/Tests/EditMode/CsvValidationTableTests.cs
+++ b/Assets/Scripts/Tests/EditMode/CsvValidationTableTests.cs
@@ -32,18 +32,6 @@ private enum CharacterField
Kind
}
- private enum ItemField
- {
- [PrimaryKey]
- Id
- }
-
- private enum ScenarioReferenceField
- {
- [ForeignKey(typeof(ItemField), nameof(ItemField.Id))]
- ItemId
- }
-
private enum FixtureValidationField
{
[PrimaryKey]
@@ -104,32 +92,5 @@ public void Validate_InvalidFixture_ReportsExpectedErrors()
Assert.That(result.Errors.Count, Is.EqualTo(6));
}
- [Test]
- public void Validate_ForeignKey_UsesRegisteredReferenceTable()
- {
- CsvTable items = CsvParser.Parse("Id\nA\nB").WithFields();
- CsvTable scenario = CsvParser.Parse("ItemId\nA\nC")
- .WithFields();
- var context = new CsvValidationContext().Register(items);
-
- CsvValidationResult result = CsvValidator.Validate(scenario, context: context);
-
- Assert.That(result.Errors.Count, Is.EqualTo(1));
- Assert.That(result.Errors[0].Row, Is.EqualTo(1));
- Assert.That(result.Errors[0].Column, Is.EqualTo(nameof(ScenarioReferenceField.ItemId)));
- }
-
- [Test]
- public void Validate_ForeignKeyWithoutContext_ReportsOneWarning()
- {
- CsvTable scenario = CsvParser.Parse("ItemId\nA")
- .WithFields();
-
- CsvValidationResult result = CsvValidator.Validate(scenario);
-
- Assert.That(result.IsValid, Is.True);
- Assert.That(result.Warnings.Count, Is.EqualTo(1));
- Assert.That(result.Warnings[0].Row, Is.EqualTo(-1));
- }
}
}
From 7af01ff72010775e63df5224c8fbe49e38dfe875 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=82=B3=E3=83=88=E3=83=AC?=
<102813037+cotore-game@users.noreply.github.com>
Date: Tue, 21 Jul 2026 12:55:19 +0900
Subject: [PATCH 03/28] =?UTF-8?q?docs:=20Validation=E3=81=AE=E5=AF=BE?=
=?UTF-8?q?=E5=BF=9C=E7=AF=84=E5=9B=B2=E3=82=92=E6=9B=B4=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 2 +-
docs/en/architecture.md | 3 +--
docs/ja/architecture.md | 4 +---
3 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 14e4eca..e53d826 100644
--- a/README.md
+++ b/README.md
@@ -192,7 +192,7 @@ foreach (ValidationError error in result.Errors)
}
```
-利用可能な制約は `PrimaryKey`、`NotNull`、`Unique`、`TypeConstraint`、`Range`、`Regex`、`AllowedValues`、`MinLength`、`MaxLength`、`ForeignKey` です。
+利用可能な制約は `PrimaryKey`、`NotNull`、`Unique`、`TypeConstraint`、`Range`、`Regex`、`AllowedValues`、`MinLength`、`MaxLength` です。
## Inspector Validation
diff --git a/docs/en/architecture.md b/docs/en/architecture.md
index a08bf43..61e3d73 100644
--- a/docs/en/architecture.md
+++ b/docs/en/architecture.md
@@ -121,12 +121,11 @@ Responsibility: adapt Unity inputs to the pure C# core.
- Delegates all parsing to `CsvParser`.
- Does not contain parsing, conversion, indexing, or validation algorithms.
-## Planned validation boundary
+## Validation boundary
Attribute metadata will be compiled into a validation schema once, then applied to `CsvTable`. Row-local rules and column/table rules must be separate:
- Row-local: required, type, range, regex, allowed values, length.
- Column/table: primary key and unique.
-- Cross-document: foreign key through an explicit validation context.
This prevents `Unique` from rescanning an entire column once per row and prevents the core data model from depending on reflection or validation attributes.
diff --git a/docs/ja/architecture.md b/docs/ja/architecture.md
index 3a69128..0c618c0 100644
--- a/docs/ja/architecture.md
+++ b/docs/ja/architecture.md
@@ -188,16 +188,14 @@ if (index.TryFindFirst("Text", out int rowIndex))
|---|---|
| `CsvValidationSchema` | Enum属性を一度読み取り、検証規則へ変換する |
| `CsvValidator` | `CsvTable` を規則に従って検証する |
-| `CsvValidationContext` | 外部キー検証で参照先CSVを登録する |
| `CsvValidationResult` | エラーと警告を保持する |
-Validationは次の3種類へ分けます。
+Validationは次の2種類へ分けます。
| 種類 | 制約 |
|---|---|
| セル・行単位 | `NotNull`、`TypeConstraint`、`Range`、`Regex`、`AllowedValues`、文字列長 |
| 列全体 | `PrimaryKey`、`Unique` |
-| CSV間 | `ForeignKey` |
`PrimaryKey` と `Unique` は、各行の検証中に列全体を繰り返し走査せず、列ごとに一度だけ検証します。
From 423a307b0ae070113e55c3f54efa6c4102e04ba9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=82=B3=E3=83=88=E3=83=AC?=
<102813037+cotore-game@users.noreply.github.com>
Date: Wed, 22 Jul 2026 06:31:04 +0900
Subject: [PATCH 04/28] =?UTF-8?q?feat:=20=E6=9D=A1=E4=BB=B6=E4=BB=98?=
=?UTF-8?q?=E3=81=8D=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7?=
=?UTF-8?q?=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../CSVLoader/Editor/CsvInspectorEditor.cs | 43 ++-
.../CsvValidation/CsvConditionEvaluator.cs | 232 +++++++++++++
.../CsvConditionEvaluator.cs.meta | 2 +
.../CsvValidation/CsvValidationAttributes.cs | 137 ++++++--
.../CsvValidation/CsvValidationSchema.cs | 310 ++++++++++++++----
.../Runtime/CsvValidation/CsvValidator.cs | 179 ++++++----
.../Runtime/Schema/CsvSchemaException.cs | 2 +-
7 files changed, 742 insertions(+), 163 deletions(-)
create mode 100644 Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvConditionEvaluator.cs
create mode 100644 Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvConditionEvaluator.cs.meta
diff --git a/Assets/Plugins/CSVLoader/Editor/CsvInspectorEditor.cs b/Assets/Plugins/CSVLoader/Editor/CsvInspectorEditor.cs
index 8a663c1..50d858f 100644
--- a/Assets/Plugins/CSVLoader/Editor/CsvInspectorEditor.cs
+++ b/Assets/Plugins/CSVLoader/Editor/CsvInspectorEditor.cs
@@ -294,31 +294,54 @@ private static string GetSelectionKey(string assetPath)
private static string GetAttributeDisplayText(Attribute attribute)
{
+ string label;
switch (attribute)
{
+ case ConditionAttribute condition:
+ string values = condition.Values.Length == 0
+ ? string.Empty
+ : $" {string.Join("|", condition.Values)}";
+ string group = condition.Group == 0 ? string.Empty : $" Group {condition.Group}:";
+ return $"[Condition:{group} {condition.Field} {condition.Comparison}{values}]";
case PrimaryKeyAttribute:
- return "[PrimaryKey]";
+ label = "PrimaryKey";
+ break;
case NotNullAttribute:
- return "[NotNull]";
+ label = "NotNull";
+ break;
case UniqueAttribute:
- return "[Unique]";
+ label = "Unique";
+ break;
case TypeConstraintAttribute typeConstraint:
- return $"[Type: {typeConstraint.ExpectedType.Name}]";
+ label = $"Type: {typeConstraint.ExpectedType.Name}";
+ break;
case Validation.RangeAttribute range:
- return $"[Range: {range.Min}-{range.Max}]";
+ label = $"Range: {range.Min}-{range.Max}";
+ break;
case RegexAttribute regex:
- return $"[Regex: {regex.Pattern}]";
+ label = $"Regex: {regex.Pattern}";
+ break;
case AllowedValuesAttribute allowed:
- return $"[Allowed: {string.Join("|", allowed.AllowedValues)}]";
+ label = $"Allowed: {string.Join("|", allowed.AllowedValues)}";
+ break;
case MinLengthAttribute minLength:
- return $"[MinLength: {minLength.MinLength}]";
+ label = $"MinLength: {minLength.MinLength}";
+ break;
case MaxLengthAttribute maxLength:
- return $"[MaxLength: {maxLength.MaxLength}]";
+ label = $"MaxLength: {maxLength.MaxLength}";
+ break;
case ForeignKeyAttribute foreignKey:
- return $"[ForeignKey: {foreignKey.ReferenceEnumType.Name}.{foreignKey.ReferenceField}]";
+ label = $"ForeignKey: {foreignKey.ReferenceEnumType.Name}.{foreignKey.ReferenceField}";
+ break;
default:
return null;
}
+
+ var validation = (CsvValidationAttribute)attribute;
+ string conditionGroup = validation.ConditionGroup == 0
+ ? string.Empty
+ : $", Group: {validation.ConditionGroup}";
+ return $"[{label}{conditionGroup}]";
}
private static CsvValidationResult ValidateDocument(CsvDocument document)
diff --git a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvConditionEvaluator.cs b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvConditionEvaluator.cs
new file mode 100644
index 0000000..ea921eb
--- /dev/null
+++ b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvConditionEvaluator.cs
@@ -0,0 +1,232 @@
+using System;
+
+namespace CSV4Unity.Validation
+{
+ ///
+ /// コンパイル済みの行条件を評価します。
+ ///
+ internal static class CsvConditionEvaluator
+ {
+ public static bool Matches(
+ CsvTable table,
+ int rowIndex,
+ CsvConditionRule[] conditions,
+ IFormatProvider formatProvider)
+ where TField : struct, Enum
+ {
+ for (int i = 0; i < conditions.Length; i++)
+ {
+ if (!Matches(table, rowIndex, conditions[i], formatProvider)) return false;
+ }
+
+ return true;
+ }
+
+ private static bool Matches(
+ CsvTable table,
+ int rowIndex,
+ CsvConditionRule condition,
+ IFormatProvider formatProvider)
+ where TField : struct, Enum
+ {
+ CsvCell cell = table.Cell(rowIndex, condition.Field);
+ switch (condition.Comparison)
+ {
+ case Compare.IsEmpty:
+ return cell.IsEmpty;
+ case Compare.IsNotEmpty:
+ return !cell.IsEmpty;
+ case Compare.In:
+ return MatchesAny(table, rowIndex, cell, condition, formatProvider);
+ case Compare.NotIn:
+ return !MatchesAny(table, rowIndex, cell, condition, formatProvider);
+ default:
+ return CompareCell(
+ table,
+ rowIndex,
+ cell,
+ condition.Values[0],
+ condition.Comparison,
+ condition.IgnoreCase,
+ formatProvider);
+ }
+ }
+
+ private static bool MatchesAny(
+ CsvTable table,
+ int rowIndex,
+ CsvCell cell,
+ CsvConditionRule condition,
+ IFormatProvider formatProvider)
+ where TField : struct, Enum
+ {
+ for (int i = 0; i < condition.Values.Length; i++)
+ {
+ if (CompareCell(
+ table,
+ rowIndex,
+ cell,
+ condition.Values[i],
+ Compare.Equal,
+ condition.IgnoreCase,
+ formatProvider))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool CompareCell(
+ CsvTable table,
+ int rowIndex,
+ CsvCell left,
+ object rightOperand,
+ Compare comparison,
+ bool ignoreCase,
+ IFormatProvider formatProvider)
+ where TField : struct, Enum
+ {
+ if (rightOperand is TField rightField)
+ {
+ return CompareCells(
+ left,
+ table.Cell(rowIndex, rightField),
+ comparison,
+ ignoreCase,
+ formatProvider);
+ }
+
+ if (rightOperand != null && IsNumericType(rightOperand.GetType()))
+ {
+ if (!left.TryGet(out double leftNumber, formatProvider))
+ {
+ return comparison == Compare.NotEqual;
+ }
+
+ double rightNumber = Convert.ToDouble(rightOperand, formatProvider);
+ return CompareNumbers(leftNumber, rightNumber, comparison);
+ }
+
+ if (rightOperand is bool rightBoolean)
+ {
+ if (!left.TryGet(out bool leftBoolean, formatProvider))
+ {
+ return comparison == Compare.NotEqual;
+ }
+
+ return CompareNumbers(leftBoolean ? 1 : 0, rightBoolean ? 1 : 0, comparison);
+ }
+
+ string rightText = Convert.ToString(rightOperand, formatProvider) ?? string.Empty;
+ return CompareCellText(left, rightText, comparison, ignoreCase);
+ }
+
+ private static bool CompareCells(
+ CsvCell left,
+ CsvCell right,
+ Compare comparison,
+ bool ignoreCase,
+ IFormatProvider formatProvider)
+ {
+ if (comparison == Compare.Equal || comparison == Compare.NotEqual)
+ {
+ return CompareCellTexts(left, right, comparison, ignoreCase);
+ }
+
+ if (!left.TryGet(out double leftNumber, formatProvider) ||
+ !right.TryGet(out double rightNumber, formatProvider))
+ {
+ return false;
+ }
+
+ return CompareNumbers(leftNumber, rightNumber, comparison);
+ }
+
+ private static bool CompareCellText(
+ CsvCell left,
+ string right,
+ Compare comparison,
+ bool ignoreCase)
+ {
+ StringComparison stringComparison = ignoreCase
+ ? StringComparison.OrdinalIgnoreCase
+ : StringComparison.Ordinal;
+ if (!left.HasEscapedQuotes)
+ {
+ int spanResult = left.RawSpan.CompareTo(right.AsSpan(), stringComparison);
+ return CompareResult(spanResult, comparison);
+ }
+
+ int stringResult = string.Compare(left.GetString(), right, stringComparison);
+ return CompareResult(stringResult, comparison);
+ }
+
+ private static bool CompareCellTexts(
+ CsvCell left,
+ CsvCell right,
+ Compare comparison,
+ bool ignoreCase)
+ {
+ StringComparison stringComparison = ignoreCase
+ ? StringComparison.OrdinalIgnoreCase
+ : StringComparison.Ordinal;
+ if (!left.HasEscapedQuotes && !right.HasEscapedQuotes)
+ {
+ int spanResult = left.RawSpan.CompareTo(right.RawSpan, stringComparison);
+ return CompareResult(spanResult, comparison);
+ }
+
+ int stringResult = string.Compare(left.GetString(), right.GetString(), stringComparison);
+ return CompareResult(stringResult, comparison);
+ }
+
+ private static bool CompareNumbers(double left, double right, Compare comparison)
+ {
+ return CompareResult(left.CompareTo(right), comparison);
+ }
+
+ private static bool CompareResult(int result, Compare comparison)
+ {
+ switch (comparison)
+ {
+ case Compare.Equal:
+ return result == 0;
+ case Compare.NotEqual:
+ return result != 0;
+ case Compare.GreaterThan:
+ return result > 0;
+ case Compare.GreaterThanOrEqual:
+ return result >= 0;
+ case Compare.LessThan:
+ return result < 0;
+ case Compare.LessThanOrEqual:
+ return result <= 0;
+ default:
+ return false;
+ }
+ }
+
+ private static bool IsNumericType(Type type)
+ {
+ switch (Type.GetTypeCode(type))
+ {
+ case TypeCode.SByte:
+ case TypeCode.Byte:
+ case TypeCode.Int16:
+ case TypeCode.UInt16:
+ case TypeCode.Int32:
+ case TypeCode.UInt32:
+ case TypeCode.Int64:
+ case TypeCode.UInt64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvConditionEvaluator.cs.meta b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvConditionEvaluator.cs.meta
new file mode 100644
index 0000000..b9327f6
--- /dev/null
+++ b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvConditionEvaluator.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e364276765b04cfca6bc95bca756cd58
diff --git a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationAttributes.cs b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationAttributes.cs
index d028459..f12fbc2 100644
--- a/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationAttributes.cs
+++ b/Assets/Plugins/CSVLoader/Runtime/CsvValidation/CsvValidationAttributes.cs
@@ -2,12 +2,109 @@
namespace CSV4Unity.Validation
{
+ ///
+ /// 条件付きValidationで使用する比較方法を表します。
+ ///
+ public enum Compare
+ {
+ /// 値が等しいことを判定します。
+ Equal,
+
+ /// 値が等しくないことを判定します。
+ NotEqual,
+
+ /// 値が比較対象より大きいことを判定します。
+ GreaterThan,
+
+ /// 値が比較対象以上であることを判定します。
+ GreaterThanOrEqual,
+
+ /// 値が比較対象より小さいことを判定します。
+ LessThan,
+
+ /// 値が比較対象以下であることを判定します。
+ LessThanOrEqual,
+
+ /// セルが空であることを判定します。
+ IsEmpty,
+
+ /// セルが空でないことを判定します。
+ IsNotEmpty,
+
+ /// 値が候補のいずれかと等しいことを判定します。
+ In,
+
+ /// 値がすべての候補と異なることを判定します。
+ NotIn
+ }
+
+ ///
+ /// Validation属性に適用する行条件を定義します。
+ ///
+ ///
+ /// 同じグループに属する条件はANDとして評価されます。条件値に同じEnum型の値を指定すると、
+ /// リテラルではなく同じ行の別列を参照します。数値として比較する場合は文字列ではなく数値リテラルを指定してください。
+ /// 条件不成立はエラーではなく、対応するValidation属性をその行で実行しないことを意味します。
+ ///
+ [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
+ public sealed class ConditionAttribute : Attribute
+ {
+ /// 条件を適用するグループ番号を取得します。
+ public int Group { get; }
+
+ /// 条件判定に使用するEnumフィールドを取得します。
+ public object Field { get; }
+
+ /// 比較方法を取得します。
+ public Compare Comparison { get; }
+
+ /// 比較する値またはEnumフィールドを取得します。
+ public object[] Values { get; }
+
+ /// 文字列比較で大文字小文字を無視するかを取得または設定します。
+ public bool IgnoreCase { get; set; }
+
+ /// グループ0に行条件を定義します。
+ /// 条件判定に使用するEnumフィールド。
+ /// 比較方法。
+ /// 比較する値。IsEmptyとIsNotEmptyでは省略します。
+ public ConditionAttribute(object field, Compare comparison, params object[] values)
+ : this(0, field, comparison, values)
+ {
+ }
+
+ /// 指定グループに行条件を定義します。
+ /// 0以上のグループ番号。
+ /// 条件判定に使用するEnumフィールド。
+ /// 比較方法。
+ /// 比較する値。IsEmptyとIsNotEmptyでは省略します。
+ public ConditionAttribute(int group, object field, Compare comparison, params object[] values)
+ {
+ Group = group;
+ Field = field;
+ Comparison = comparison;
+ Values = values ?? Array.Empty