Regex tooling: formula gene grouping, content patches, def validation
CI / build-test (push) Successful in 1m12s

Three regex-powered content tools (phase G5):

- Formula group functions gsum/gcount/gavg/gmin/gmax('regex') aggregate
  over every context variable whose name matches the pattern, e.g.
  gsum('leaf_.*'). Adds string literals to the formula grammar and an
  IFormulaContext.ResolveMatching hook; GenomeContext enumerates matching
  genes, so a trait can sum/average a gene group.
- DefDatabase content patches: a { "type": "Patch", patches:[{ defType,
  match (regex on defName), set:{fields} }] } file sets fields on every
  matching raw def before resolution — mods patch Core in bulk.
- DefDatabase.RegisterValidator(typeKey, field, regex): load-time check
  that a string field matches a pattern, throwing otherwise (naming/format
  conventions).

Covered by FormulaTests (group aggregation, composition, bad calls) and
DefDatabaseTests (patch set/match/unknown-type, validator pass/reject).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-13 03:59:36 +03:00
co-authored by Claude Opus 4.8
parent ead22517ee
commit d044cafad9
8 changed files with 440 additions and 1 deletions
@@ -109,4 +109,66 @@ public class FormulaTests
var formula = Formula.Compile("x + 1");
Assert.Throws<FormulaException>(() => formula.Evaluate());
}
// --- Group functions (regex aggregation over matching variables) ---
private sealed class GroupContext(Dictionary<string, float> values) : IFormulaContext
{
public float Resolve(string name) => values[name];
public IEnumerable<float> ResolveMatching(Func<string, bool> matches)
{
foreach (var (name, value) in values)
{
if (matches(name))
{
yield return value;
}
}
}
}
[Fact]
public void Evaluate_GroupFunctions_AggregateMatchingVariables()
{
var ctx = new GroupContext(
new()
{
["leaf_a"] = 2f,
["leaf_b"] = 4f,
["leaf_c"] = 6f,
["root_a"] = 100f,
}
);
Assert.Equal(12f, Formula.Compile("gsum('leaf_.*')").Evaluate(ctx), 4); // 2+4+6
Assert.Equal(3f, Formula.Compile("gcount('leaf_.*')").Evaluate(ctx), 4);
Assert.Equal(4f, Formula.Compile("gavg('leaf_.*')").Evaluate(ctx), 4);
Assert.Equal(2f, Formula.Compile("gmin('leaf_.*')").Evaluate(ctx), 4);
Assert.Equal(6f, Formula.Compile("gmax('leaf_.*')").Evaluate(ctx), 4);
}
[Fact]
public void Evaluate_GroupFunction_ComposesWithArithmetic()
{
var ctx = new GroupContext(new() { ["g1"] = 3f, ["g2"] = 5f });
Assert.Equal(16f, Formula.Compile("gsum('g.*') * 2").Evaluate(ctx), 4); // (3+5)*2
}
[Fact]
public void Evaluate_GroupFunction_NoMatches_IsZero()
{
var ctx = new GroupContext(new() { ["x"] = 1f });
Assert.Equal(0f, Formula.Compile("gsum('none_.*')").Evaluate(ctx), 4);
Assert.Equal(0f, Formula.Compile("gavg('none_.*')").Evaluate(ctx), 4);
}
[Theory]
[InlineData("gsum(leaf)")] // pattern must be a quoted string
[InlineData("gsum('[')")] // invalid regex
[InlineData("gsum('a' 'b')")] // extra token
public void Compile_BadGroupCall_Throws(string expr)
{
Assert.Throws<FormulaException>(() => Formula.Compile(expr));
}
}