157 lines
7.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|