Files
mech-ai/backend-csharp/Agent4.Api/DimensionalToleranceParser.cs
T
2026-07-17 17:45:56 +08:00

150 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using System.Text.RegularExpressions;
static class DimensionalToleranceParser
{
static readonly Regex FitDesignationRegex = new(
@"(?<![A-Za-z])(?<letters>[A-Za-z]{1,2})\s*(?<grade>01|0|1[0-8]|[1-9])(?!\d)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
static readonly Regex ExplicitDeviationRegex = new(
@"\(\s*(?<upper>[+\-]?\s*\d+(?:[.,]\d+)?)\s*/\s*(?<lower>[+\-]?\s*\d+(?:[.,]\d+)?)\s*\)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
static readonly Regex PlusMinusRegex = new(
@"[±]\s*(?<value>\d+(?:[.,]\d+)?)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public static Dictionary<string, object?> Parse(
string text,
string rawText = "",
double? measurement = null,
string objectName = "")
{
var normalized = Normalize(text);
var dimensionKind = DetectDimensionKind(normalized, objectName);
var nominal = ExtractNominalSize(normalized, measurement, dimensionKind);
var designations = ExtractFitDesignations(normalized);
var deviations = ExtractDeviations(normalized);
var warnings = new List<string>();
if (dimensionKind == "radius" && designations.Count > 0)
warnings.Add("fit_designation_applied_to_radius_dimension");
if (designations.Count == 1)
warnings.Add("single_tolerance_designation_does_not_define_complete_hole_shaft_fit");
if (nominal == null)
warnings.Add("nominal_size_not_parsed");
var hole = designations.FirstOrDefault(item => Equals(item["role"], "hole"));
var shaft = designations.FirstOrDefault(item => Equals(item["role"], "shaft"));
var fitCode = string.Join("/", designations.Select(item => Convert.ToString(item["designation"], CultureInfo.InvariantCulture)));
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["dimension_kind"] = dimensionKind,
["nominal_size_mm"] = nominal,
["interface_diameter_mm"] = nominal == null ? null : dimensionKind == "radius" ? nominal * 2 : nominal,
["fit_code"] = string.IsNullOrWhiteSpace(fitCode) ? null : fitCode,
["fit_grade"] = string.IsNullOrWhiteSpace(fitCode) ? null : fitCode,
["fit_designations"] = designations,
["hole_tolerance_designation"] = hole?["designation"],
["shaft_tolerance_designation"] = shaft?["designation"],
["fit_completeness"] = hole != null && shaft != null ? "hole_and_shaft_pair" : designations.Count > 0 ? "single_designation" : "none",
["upper_deviation_mm"] = deviations.Upper,
["lower_deviation_mm"] = deviations.Lower,
["deviation_source"] = deviations.Source,
["raw_text"] = rawText,
["text"] = normalized,
["parse_warnings"] = warnings
};
}
public static bool LooksLikeTolerance(string rawText, string text)
{
var merged = rawText + " " + text;
return merged.Contains(@"\S", StringComparison.OrdinalIgnoreCase) ||
FitDesignationRegex.IsMatch(Normalize(merged)) ||
Regex.IsMatch(merged, @"[+\-±]\s*\d+(?:\.\d+)?", RegexOptions.CultureInvariant);
}
static List<Dictionary<string, object?>> ExtractFitDesignations(string text)
{
var result = new List<Dictionary<string, object?>>();
foreach (Match match in FitDesignationRegex.Matches(text))
{
var letters = match.Groups["letters"].Value;
var grade = match.Groups["grade"].Value;
var role = letters == letters.ToUpperInvariant()
? "hole"
: letters == letters.ToLowerInvariant()
? "shaft"
: "unknown";
result.Add(new Dictionary<string, object?>
{
["designation"] = letters + grade,
["fundamental_deviation"] = letters,
["it_grade"] = grade,
["role"] = role
});
}
return result;
}
static string DetectDimensionKind(string text, string objectName)
{
if (Regex.IsMatch(text, @"^\s*R\s*\d", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) ||
objectName.Contains("RadialDimension", StringComparison.OrdinalIgnoreCase))
return "radius";
if (text.IndexOfAny(['Φ', 'φ', '⌀', 'Ø']) >= 0 ||
objectName.Contains("DiametricDimension", StringComparison.OrdinalIgnoreCase))
return "diameter";
return "linear_or_unspecified";
}
static double? ExtractNominalSize(string text, double? measurement, string dimensionKind)
{
var pattern = dimensionKind switch
{
"radius" => @"^\s*R\s*(?<value>\d+(?:[.,]\d+)?)",
"diameter" => @"[Φφ⌀Ø]\s*(?<value>\d+(?:[.,]\d+)?)",
_ => @"(?<value>\d+(?:[.,]\d+)?)"
};
var match = Regex.Match(text, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (match.Success && TryParseNumber(match.Groups["value"].Value, out var parsed))
return parsed;
return measurement is > 0 ? measurement : null;
}
static (double? Upper, double? Lower, string? Source) ExtractDeviations(string text)
{
var explicitMatch = ExplicitDeviationRegex.Match(text);
if (explicitMatch.Success &&
TryParseNumber(explicitMatch.Groups["upper"].Value, out var upper) &&
TryParseNumber(explicitMatch.Groups["lower"].Value, out var lower))
return (upper, lower, "explicit_upper_lower");
var plusMinusMatch = PlusMinusRegex.Match(text);
if (plusMinusMatch.Success && TryParseNumber(plusMinusMatch.Groups["value"].Value, out var value))
return (value, -value, "explicit_plus_minus");
return (null, null, null);
}
static bool TryParseNumber(string text, out double value) =>
double.TryParse(
text.Replace(" ", "", StringComparison.Ordinal).Replace('', '-').Replace(',', '.'),
NumberStyles.Float,
CultureInfo.InvariantCulture,
out value);
static string Normalize(string text)
{
var value = text ?? "";
value = Regex.Replace(value, @"\\S([^;]+)\^([^;]*);", "$1/$2", RegexOptions.CultureInvariant);
value = Regex.Replace(value, @"\\S([^;]+);", "$1", RegexOptions.CultureInvariant);
value = Regex.Replace(value, @"\\[AH][^;]*;", "", RegexOptions.CultureInvariant);
value = value.Replace("{", "", StringComparison.Ordinal).Replace("}", "", StringComparison.Ordinal);
return Regex.Replace(value, @"\s+", " ").Trim();
}
}