using System.Globalization; using System.Text.RegularExpressions; static class DimensionalToleranceParser { static readonly Regex FitDesignationRegex = new( @"(?[A-Za-z]{1,2})\s*(?01|0|1[0-8]|[1-9])(?!\d)", RegexOptions.Compiled | RegexOptions.CultureInvariant); static readonly Regex ExplicitDeviationRegex = new( @"\(\s*(?[+\-−]?\s*\d+(?:[.,]\d+)?)\s*/\s*(?[+\-−]?\s*\d+(?:[.,]\d+)?)\s*\)", RegexOptions.Compiled | RegexOptions.CultureInvariant); static readonly Regex PlusMinusRegex = new( @"[±]\s*(?\d+(?:[.,]\d+)?)", RegexOptions.Compiled | RegexOptions.CultureInvariant); public static Dictionary 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(); 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(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> ExtractFitDesignations(string text) { var result = new List>(); 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 { ["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*(?\d+(?:[.,]\d+)?)", "diameter" => @"[Φφ⌀Ø]\s*(?\d+(?:[.,]\d+)?)", _ => @"(?\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(); } }