using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; namespace Ounibo.Catalog.Core { /// 参数化选型结果: 标准档位命中 → Variant; 非标 → 模板生成的编码+参数 (无 STEP)。 public class ParamResult { public string ModelCode; public Dictionary Params; public bool IsStandard; public Variant Variant; // IsStandard 时非空 (即 CSV 网格行) public string Error; // 非法原因键: param=参数不全 / domain=域外 / rule=违背规则 / template=模板错误 (上层本地化) } /// /// 参数化引擎 (一期, 2026-08-25): /// 参数域校验 + 型号编码模板渲染 + 非标组合生成 + 最近标准档位。 /// 与网页 app.js 的参数化函数语义一一对应 (同一份规则 JSON + 同一份测试向量 parametric-vectors.json 对拍)。 /// 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); } /// 模板渲染: {field} 或 {field:padN} (padN=前补零到 N 位)。未知字段/缺值/非法 token → null。 public static string RenderCode(string template, Dictionary 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(); } /// 参数取值在其域内: enum → values 命中; range → [min,max] 且按 step 对齐 (相对 min 的整数倍)。 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()).Any(x => Eq(x, v)); } /// 组合校验: 参数齐全 + 域 + 规则。返回错误键 (null=合法)。 public static string ValidateCombo(Series s, Dictionary sel) { foreach (var p in s.parameters ?? new List()) { 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; } /// /// 部分选择校验 (联动用): 只校验已选参数的域 + 规则 (Then 参数未选不判违背)。 /// 与 Configurator.RuleViolatedPartial 配套, 与网页 validatePartial 同语义。 /// public static string ValidatePartial(Series s, Dictionary sel) { foreach (var p in s.parameters ?? new List()) { 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; } /// 生成结果: 网格命中 → Variant (IsStandard); 否则非标 (模板生成编码, 无 STEP)。不合法 → Error 非空。 public static ParamResult Generate(Series s, Configurator cfg, Dictionary sel) { var r = new ParamResult { Params = sel }; if (sel.Count < (s.parameters ?? new List()).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; } /// /// 最近标准档位: 全部有效网格变体按参数距离排序 — range 参数按数值差累加, 枚举不等记大惩罚 (1e6); /// 距离相同取编码字典序小者 (与网页同语义)。 /// public static Variant NearestStandard(Series s, Configurator cfg, Dictionary 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()) { 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; } } }