using System.Text; namespace JSMR.Infrastructure.Data.Repositories.VoiceWorks; public static class MySqlBooleanQuery { public static string Normalize(string input) { if (string.IsNullOrWhiteSpace(input)) return string.Empty; // Split into top-level tokens by spaces (not inside quotes/parentheses) var tokens = SplitTopLevel(input.Trim(), ' '); var parts = new List(tokens.Count); foreach (var raw in tokens) { var t = raw.Trim(); if (t.Length == 0) continue; if (t is "|") continue; // Preserve explicit boolean operators user may already supply if (t[0] == '-' || t[0] == '+') { if (t.Length == 1) continue; // Token already has a sign; normalize rest var sign = t[0]; var body = t[1..].Trim(); parts.Add(sign + NormalizePositive(body)); continue; } // Default: required term parts.Add("+" + NormalizePositive(t)); } return string.Join(' ', parts.Where(p => p.Length > 1 || p == "+" || p == "-")).Trim(); } // Normalize a positive (non-signed) token: handles ORs, phrases, grouping private static string NormalizePositive(string token) { if (string.IsNullOrWhiteSpace(token)) return string.Empty; // If token is a quoted phrase already -> keep quotes if (IsQuoted(token)) { return EnsureBalancedQuotes(token); } // If token starts/ends with parentheses, leave as-is (user grouping) if (token.StartsWith('(') && token.EndsWith(')') && token.Length > 2) { // Optionally normalize inside the group if you want; // here we trust user's grouping. return token; } // If token contains OR '|' at top level, convert to (a|b|...) if (ContainsTopLevel(token, '|')) { var orParts = SplitTopLevel(token, '|') .Select(p => NormalizeOrAtom(p.Trim())) .Where(p => p.Length > 0); return "(" + string.Join(" ", orParts) + ")"; } // Plain atom -> as-is return token; } // Normalize one OR-side atom (could be phrase or bare word) private static string NormalizeOrAtom(string atom) { if (string.IsNullOrWhiteSpace(atom)) return string.Empty; // Allow nested quotes inside OR if (IsQuoted(atom)) return EnsureBalancedQuotes(atom); // If it contains whitespace, quote it to become a phrase if (atom.Any(char.IsWhiteSpace)) return $"\"{EscapeQuotes(atom)}\""; return atom; } // -------------- helpers -------------- private static bool IsQuoted(string s) => s.Length >= 2 && s[0] == '"' && s[^1] == '"'; private static string EnsureBalancedQuotes(string s) { // If user typed starting quote but forgot closing, close it. if (s.Length >= 1 && s[0] == '"' && (s.Length == 1 || s[^1] != '"')) return s + "\""; // If user typed closing quote but no opening, open it. if (s.Length >= 1 && s[^1] == '"' && (s.Length == 1 || s[0] != '"')) return "\"" + s; // Also escape any embedded quotes (rare) if (IsQuoted(s)) { var inner = s[1..^1]; return $"\"{EscapeQuotes(inner)}\""; } return s; } private static string EscapeQuotes(string s) => s.Replace("\"", "\\\""); private static bool ContainsTopLevel(string s, char sep) { int depth = 0; bool inQuotes = false; foreach (var ch in s) { if (ch == '"' && depth == 0) inQuotes = !inQuotes; else if (!inQuotes) { if (ch == '(') depth++; else if (ch == ')' && depth > 0) depth--; else if (ch == sep && depth == 0) return true; } } return false; } private static List SplitTopLevel(string s, char sep) { var list = new List(); var sb = new StringBuilder(); int depth = 0; bool inQuotes = false; for (int i = 0; i < s.Length; i++) { var ch = s[i]; if (ch == '"' && depth == 0) { inQuotes = !inQuotes; sb.Append(ch); continue; } if (!inQuotes) { if (ch == '(') { depth++; sb.Append(ch); continue; } if (ch == ')') { depth = Math.Max(0, depth - 1); sb.Append(ch); continue; } if (ch == sep && depth == 0) { // split var piece = sb.ToString().Trim(); if (piece.Length > 0) list.Add(piece); sb.Clear(); continue; } } sb.Append(ch); } var tail = sb.ToString().Trim(); if (tail.Length > 0) list.Add(tail); return list; } }