Files
OnebotCatalog/src/CatalogCore/ParametricEngine.cs

157 lines
7.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace Ounibo.Catalog.Core
{
/// <summary>参数化选型结果: 标准档位命中 → Variant; 非标 → 模板生成的编码+参数 (无 STEP)。</summary>
public class ParamResult
{
public string ModelCode;
public Dictionary<string, object> Params;
public bool IsStandard;
public Variant Variant; // IsStandard 时非空 (即 CSV 网格行)
public string Error; // 非法原因键: param=参数不全 / domain=域外 / rule=违背规则 / template=模板错误 (上层本地化)
}
/// <summary>
/// 参数化引擎 (一期, 2026-08-25):
/// 参数域校验 + 型号编码模板渲染 + 非标组合生成 + 最近标准档位。
/// 与网页 app.js 的参数化函数语义一一对应 (同一份规则 JSON + 同一份测试向量 parametric-vectors.json 对拍)。
/// </summary>
public static class ParametricEngine
{
public static bool IsParametric(Series s) { return s != null && s.mode == "parametric"; }
static string S(object o) { return o == null ? "" : Convert.ToString(o, CultureInfo.InvariantCulture); }
static bool Eq(object a, object b) { return string.Equals(S(a), S(b), StringComparison.OrdinalIgnoreCase); }
static double D(object o) { return Convert.ToDouble(o, CultureInfo.InvariantCulture); }
/// <summary>模板渲染: {field} 或 {field:padN} (padN=前补零到 N 位)。未知字段/缺值/非法 token → null。</summary>
public static string RenderCode(string template, Dictionary<string, object> p)
{
if (string.IsNullOrEmpty(template)) return null;
var sb = new StringBuilder();
int i = 0;
while (i < template.Length)
{
var c = template[i];
if (c == '{')
{
var close = template.IndexOf('}', i + 1);
if (close < 0) return null;
var token = template.Substring(i + 1, close - i - 1);
var parts = token.Split(':');
var field = parts[0].Trim();
object val;
if (!p.TryGetValue(field, out val)) return null;
var sv = S(val);
if (parts.Length > 1)
{
int pad;
var padTok = parts[1].Trim();
if (padTok.StartsWith("pad", StringComparison.OrdinalIgnoreCase)) padTok = padTok.Substring(3);
if (!int.TryParse(padTok, out pad) || pad < 1) return null;
sv = sv.PadLeft(pad, '0');
}
sb.Append(sv);
i = close + 1;
}
else { sb.Append(c); i++; }
}
return sb.ToString();
}
/// <summary>参数取值在其域内: enum → values 命中; range → [min,max] 且按 step 对齐 (相对 min 的整数倍)。</summary>
public static bool DomainValid(ParameterDef p, object v)
{
if (v == null) return false;
if (p.type == "range")
{
double d;
if (!double.TryParse(S(v), NumberStyles.Float, CultureInfo.InvariantCulture, out d)) return false;
if (p.min != null && d < p.min.Value) return false;
if (p.max != null && d > p.max.Value) return false;
var st = p.step ?? 1;
var baseV = p.min ?? 0;
if (st > 0 && Math.Abs(d - baseV - Math.Round((d - baseV) / st) * st) > 1e-6) return false;
return true;
}
return (p.values ?? new List<object>()).Any(x => Eq(x, v));
}
/// <summary>组合校验: 参数齐全 + 域 + 规则。返回错误键 (null=合法)。</summary>
public static string ValidateCombo(Series s, Dictionary<string, object> sel)
{
foreach (var p in s.parameters ?? new List<ParameterDef>())
{
object v;
if (!sel.TryGetValue(p.code, out v)) return "param";
if (!DomainValid(p, v)) return "domain";
}
var cfg = new Configurator(s);
var fake = new Variant { modelCode = "", @params = sel };
if (cfg.RuleViolated(fake)) return "rule";
return null;
}
/// <summary>
/// 部分选择校验 (联动用): 只校验已选参数的域 + 规则 (Then 参数未选不判违背)。
/// 与 Configurator.RuleViolatedPartial 配套, 与网页 validatePartial 同语义。
/// </summary>
public static string ValidatePartial(Series s, Dictionary<string, object> sel)
{
foreach (var p in s.parameters ?? new List<ParameterDef>())
{
object v;
if (!sel.TryGetValue(p.code, out v)) continue; // 未选参数不校验
if (!DomainValid(p, v)) return "domain";
}
var cfg = new Configurator(s);
if (cfg.RuleViolatedPartial(sel)) return "rule";
return null;
}
/// <summary>生成结果: 网格命中 → Variant (IsStandard); 否则非标 (模板生成编码, 无 STEP)。不合法 → Error 非空。</summary>
public static ParamResult Generate(Series s, Configurator cfg, Dictionary<string, object> sel)
{
var r = new ParamResult { Params = sel };
if (sel.Count < (s.parameters ?? new List<ParameterDef>()).Count) { r.Error = "param"; return r; }
r.Error = ValidateCombo(s, sel);
if (r.Error != null) return r;
r.ModelCode = RenderCode(s.modelCodeTemplate, sel);
if (r.ModelCode == null) { r.Error = "template"; return r; }
var v = cfg.ValidVariants().FirstOrDefault(x => string.Equals(x.modelCode, r.ModelCode, StringComparison.OrdinalIgnoreCase));
if (v != null) { r.IsStandard = true; r.Variant = v; }
return r;
}
/// <summary>
/// 最近标准档位: 全部有效网格变体按参数距离排序 — range 参数按数值差累加, 枚举不等记大惩罚 (1e6);
/// 距离相同取编码字典序小者 (与网页同语义)。
/// </summary>
public static Variant NearestStandard(Series s, Configurator cfg, Dictionary<string, object> sel)
{
Variant best = null;
double bestDist = double.MaxValue;
foreach (var v in cfg.ValidVariants())
{
double dist = 0;
foreach (var p in s.parameters ?? new List<ParameterDef>())
{
object a, b;
if (!sel.TryGetValue(p.code, out a) || !v.@params.TryGetValue(p.code, out b)) continue;
if (p.type == "range") dist += Math.Abs(D(a) - D(b));
else if (!Eq(a, b)) dist += 1e6;
}
if (best == null || dist < bestDist - 1e-9 ||
(Math.Abs(dist - bestDist) < 1e-9 && string.CompareOrdinal(v.modelCode, best.modelCode) < 0))
{ best = v; bestDist = dist; }
}
return best;
}
}
}