/*============================================================================ File: StringFilters.cs Summary: Implements string filtering functions for use in MDX Queries. Date: July 12, 2006 ---------------------------------------------------------------------------- This file is part of the Analysis Services Stored Procedure Project. http://www.codeplex.com/Wiki/View.aspx?ProjectName=ASStoredProcedures THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. ============================================================================*/ using System; using System.Collections.Generic; using System.Collections; using System.Text; using System.Text.RegularExpressions; using Microsoft.AnalysisServices.AdomdServer; using Tuple = Microsoft.AnalysisServices.AdomdServer.Tuple; //resolves ambiguous reference in .NET 4 with System.Tuple namespace ASStoredProcs { /// /// /// public class StringFilters { static Hashtable regExCache = new Hashtable( new RegExCacheIndexComparer()); #region " Public 'Contains' functions" /// /// Contains are 3 times slower than using VB.InStr /// and should be used only when caseSensitive = true (InStr always case insensitive) /// /// /// /// [SafeToPrepare(true)] public Boolean IsContain(String valueToMatch, String substring) { return IsContain(valueToMatch, substring, false); } /// /// Contains are 3 times slower than using VB.InStr /// and should be used only when caseSensitive = true (InStr always case insensitive) /// /// /// /// /// [SafeToPrepare(true)] public Boolean IsContain(String valueToMatch, String substring, Boolean caseSensitive) { Context.TraceEvent(100, 0, "IsContain: Starting"); // Contains(...) with .ToLower().ToUpper() ~100 times faster then IndexOf(..., StringComparison.CurrentCultureIgnoreCase), but IndexOf is enough fast in absolute ticks // valueToMatch.IndexOf(substring, StringComparison.CurrentCultureIgnoreCase) >= 0; // can be replaced with valueToMatch.ToLower().ToUpper().Contains(substring.ToLower().ToUpper()) // as in some laguages (french, turkish) there is not 1-1 relation between upcase and lowcase chars // but it will be durty hack :) bool result = caseSensitive ? valueToMatch.Contains(substring) : valueToMatch.IndexOf(substring, StringComparison.CurrentCultureIgnoreCase) >= 0; Context.TraceEvent(100, 0, "IsContain: Finished"); return result; } /// /// Contains are 3 times slower than using VB.InStr /// and should be used only when caseSensitive = true (InStr always case insensitive) /// /// /// /// /// [SafeToPrepare(true)] public static Set Contains(Set setToFilter, String substring, Expression exp) { return Contains(setToFilter, substring, exp, false); } /// /// Contains are 3 times slower than using VB.InStr /// and should be used only when caseSensitive = true (InStr always case insensitive) /// /// /// /// /// /// [SafeToPrepare(true)] public static Set Contains(Set setToFilter, String substring, Expression exp, Boolean caseSensitive) { Context.TraceEvent(100, 0, "Contains: Starting"); if (substring.Length == 0) { Context.TraceEvent(100, 0, "Contains: Finished (No substring parameter)"); return setToFilter; } if (setToFilter == null) { throw new ArgumentNullException("setToFilter"); } else { using (SetBuilder sb = new SetBuilder()) { foreach (Tuple t in setToFilter.Tuples) { string val = (string) exp.Calculate(t); bool match = caseSensitive ? val.Contains(substring) : val.IndexOf(substring, StringComparison.CurrentCultureIgnoreCase) >= 0; if (match) { sb.Add(t); } } Context.TraceEvent(100, sb.Count, "Contains: Finished (returning " + sb.Count.ToString() + " tuples"); return sb.ToSet(); } } } /// /// Contains are 3 times slower than using VB.InStr /// and should be used only when caseSensitive = true (InStr always case insensitive) /// /// /// /// /// /// /// [SafeToPrepare(true)] public static Set Contains(Set setToFilter, String substring, Expression exp, int start, int count) { return Contains(setToFilter, substring, exp, false, start, count); } /// /// Contains are 3 times slower than using VB.InStr /// and should be used only when caseSensitive = true (InStr always case insensitive) /// /// /// /// /// The index of the first tuple in setToFilter to start pattern matching serch for /// Tuples count to check for from the start /// /// [SafeToPrepare(true)] public static Set Contains(Set setToFilter, String substring, Expression exp, Boolean caseSensitive, int start, int count) { Context.TraceEvent(100, 0, "Contains: Starting"); if (substring.Length == 0) { Context.TraceEvent(100, 0, "Contains: Finished (No substring parameter)"); return setToFilter; } if (setToFilter == null) { throw new ArgumentNullException("setToFilter"); } #region start-end SubSet adjustment / boundary checks if (start < 0) { start = 0; } if (count <= 0) { Context.TraceEvent(100, 0, "Contains: Finished (count parameter value is less or equal 0)"); return new SetBuilder().ToSet(); } #endregion using (SetBuilder sb = new SetBuilder()) { int indexBefore = 0; int itemsCount = 0; bool takeIt = start == 0; foreach (Tuple t in setToFilter.Tuples) { string val = (string) exp.Calculate(t); bool match = caseSensitive ? val.Contains(substring) : val.IndexOf(substring, StringComparison.CurrentCultureIgnoreCase) >= 0; if (match) { if (takeIt) { sb.Add(t); itemsCount++; if (itemsCount == count) { break; } } else { indexBefore++; if (indexBefore == start) { takeIt = true; } } } } Context.TraceEvent(100, sb.Count, "Contains: Finished (returning " + sb.Count.ToString() + " tuples"); return sb.ToSet(); } } #endregion #region " Public 'Like' functions" /// /// /// /// This is a string expression which /// This paramter uses a pattern in the same form as the T-SQL LIKE operator /// Boolean [SafeToPrepare(true)] public Boolean IsLike(String valueToMatch, String pattern) { return IsLike(valueToMatch, pattern, false); } [SafeToPrepare(true)] public Boolean IsLike(String valueToMatch, String pattern, Boolean caseSensitive) { bool result; Context.TraceEvent(100, 0, "IsLike: Starting"); if (caseSensitive) { // todo - write case sensitive SqlLikeStringUtilities.SqlLike // todo - cache regex objects here RegexOptions optRegex = RegexOptions.Compiled; // now it's always case sensitive - see "if" //if (!caseSensitive) //{ // optRegex = optRegex | RegexOptions.IgnoreCase; //} Regex r = getCachedRegEx(LikeToRegEx(pattern), optRegex); result = r.Match(valueToMatch).Success; } else { result = SqlLikeStringUtilities.SqlLike(pattern, valueToMatch); } Context.TraceEvent(100, 0, "IsLike: Finished"); return result; } [SafeToPrepare(true)] public static Set Like(Set setToFilter, String pattern, Expression exp) { Context.TraceEvent(100, 0, "Like: Starting"); if (pattern.Length == 0) { Context.TraceEvent(100, 0, "Like: Finished (No pattern parameter)"); return setToFilter; } if (setToFilter == null) { throw new ArgumentNullException("setToFilter"); } else { using (SetBuilder sb = new SetBuilder()) { foreach (Tuple t in setToFilter.Tuples) { string val = (string)exp.Calculate(t); if (SqlLikeStringUtilities.SqlLike(pattern, val)) { sb.Add(t); } } Context.TraceEvent(100, sb.Count, "Like: Finished (returning " + sb.Count.ToString() + " tuples"); return sb.ToSet(); } } } /// /// /// /// /// /// /// The index of the first tuple in setToFilter to start pattern matching serch for /// Tuples count to check for from the start /// [SafeToPrepare(true)] public static Set Like(Set setToFilter, String pattern, Expression exp, int start, int count) { Context.TraceEvent(100, 0, "Like: Starting"); if (pattern.Length == 0) { Context.TraceEvent(100, 0, "Like: Finished (No pattern parameter)"); return setToFilter; } if (setToFilter == null) { throw new ArgumentNullException("setToFilter"); } #region start-end SubSet adjustment / boundary checks if (start < 0) { start = 0; } if (count <= 0) { Context.TraceEvent(100, 0, "LikeFilter: Finished (count parameter value is less or equal 0)"); return new SetBuilder().ToSet(); } #endregion using (SetBuilder sb = new SetBuilder()) { int indexBefore = 0; int itemsCount = 0; bool takeIt = start == 0; foreach (Tuple t in setToFilter.Tuples) { string val = (string)exp.Calculate(t); if (SqlLikeStringUtilities.SqlLike(pattern, val)) { if (takeIt) { sb.Add(t); itemsCount++; if (itemsCount == count) { break; } } else { indexBefore++; if (indexBefore == start) { takeIt = true; } } } } Context.TraceEvent(100, sb.Count, "Like: Finished (returning " + sb.Count.ToString() + " tuples"); return sb.ToSet(); } } [SafeToPrepare(true)] public static Set Like(Set setToFilter, String pattern, Expression exp, Boolean caseSensitive, int start, int count) { if (caseSensitive) { // todo made case sensitive variant of Like return RegExFilter(setToFilter, LikeToRegEx(pattern), exp, true, start, count); } return Like(setToFilter, pattern, exp, start, count); } [SafeToPrepare(true)] public static Set Like(Set setToFilter, String pattern, Expression exp, Boolean caseSensitive) { if (caseSensitive) { // todo made case sensitive variant of Like return RegExFilter(setToFilter, LikeToRegEx(pattern), exp, true); } return Like(setToFilter, pattern, exp); } #endregion #region " Public 'Regex' functions" [SafeToPrepare(true)] public static Set RegExFilter(Set setToFilter, String pattern, Expression exp) { return RegExFilter(setToFilter, pattern, exp, false); } [SafeToPrepare(true)] public static Set RegExFilter(Set setToFilter, String pattern, Expression exp, int start, int count) { return RegExFilter(setToFilter, pattern, exp, false, start, count); } [SafeToPrepare(true)] public static Set RegExFilter(Set setToFilter, String pattern, Expression exp, Boolean caseSensitive) { Context.TraceEvent(100, 0, "RegExFilter: Starting"); // If pattern is empty, just return the whole set if (pattern.Length == 0) { Context.TraceEvent(100, 0, "RegExFilter: Finished (No pattern parameter)"); return setToFilter; } if (setToFilter == null) { throw new ArgumentNullException("setToFilter"); } using(SetBuilder sb = new SetBuilder()) { RegexOptions optRegex = RegexOptions.Compiled; if (!caseSensitive) { optRegex = optRegex | RegexOptions.IgnoreCase; } Regex r = getCachedRegEx(pattern, optRegex); foreach (Tuple t in setToFilter.Tuples) { string val = (string)exp.Calculate(t); if (r.Match(val).Success) { sb.Add(t); } } Context.TraceEvent(100, sb.Count, "RegExFilter: Finished (returning " + sb.Count.ToString() + " tuples"); return sb.ToSet(); } } [SafeToPrepare(true)] public static Set RegExFilter(Set setToFilter, String pattern, Expression exp, Boolean caseSensitive, int start, int count) { Context.TraceEvent(100, 0, "RegExFilter: Starting"); if (pattern.Length == 0) { Context.TraceEvent(100, 0, "RegExFilter: Finished (No pattern parameter)"); return setToFilter; } if (setToFilter == null) { throw new ArgumentNullException("setToFilter"); } #region start-end SubSet adjustment / boundary checks if (start < 0) { start = 0; } if (count <= 0) { Context.TraceEvent(100, 0, "RegExFilter: Finished (count parameter value is less or equal 0)"); return new SetBuilder().ToSet(); } #endregion using (SetBuilder sb = new SetBuilder()) { RegexOptions optRegex = RegexOptions.Compiled; if (!caseSensitive) { optRegex = optRegex | RegexOptions.IgnoreCase; } Regex r = getCachedRegEx(pattern, optRegex); int indexBefore = 0; int itemsCount = 0; bool takeIt = start == 0; foreach (Tuple t in setToFilter.Tuples) { string val = (string)exp.Calculate(t); if (r.Match(val).Success) { if (takeIt) { sb.Add(t); itemsCount++; if (itemsCount == count) { break; } } else { indexBefore++; if (indexBefore == start) { takeIt = true; } } } } Context.TraceEvent(100, sb.Count, "RegExFilter: Finished (returning " + sb.Count.ToString() + " tuples"); return sb.ToSet(); } } #endregion #region "Private Helper Functions" /// /// This function converts a pattern from the T-SQL LIKE format /// into a RegEx pattern. /// /// % = .* /// _ = . /// /// In a regex Characters other than . $ ^ { [ ( | ) * + ? \ match themselves. /// There fore the above characters need to be escaped so that they are correctly /// matched if they are present in the "like" pattern. /// /// This is the pattern to match in T-SQL LIKE format /// string public static string LikeToRegEx(string pattern) { Context.TraceEvent(100, 0, "Like: Converting Like to RegEx"); Context.CheckCancelled(); // Check if the user has cancelled StringBuilder sb = new StringBuilder(pattern); // the order of the following operations is important or one replacement // can end up corrupting a previous replacement. sb.Replace(@"\", @"\\"); // needs to be done first before any other backslashes are introduced sb.Replace("*", @"\*"); // needs to be done before the '.*' insertion sb.Replace(".", @"\."); // needs to be done before the '.*' insertion sb.Replace("%", ".*"); sb.Replace("$", @"\$"); sb.Replace("(", @"\("); sb.Replace(")", @"\)"); sb.Replace("|", @"\|"); sb.Replace("+", @"\+"); sb.Replace("?", @"\?"); sb.Replace("^", @"\^"); // fix up strings where the above replace was too agressive and the // ^ was being used as part of a 'not matching' expression. sb.Replace(@"[\^", @"[^"); sb.Replace("[[]", @"\["); sb.Replace("_", "."); // the above replacement incorrectly converts both _ to . and [_] to [.] // the next line converts the incorrect [.] to _ sb.Replace("[.]", @"_"); if (!pattern.StartsWith("%")) { sb.Insert(0, @"\A"); } if (!pattern.EndsWith("%")) { sb.Append(@"\z"); } return sb.ToString(); } private static Regex getCachedRegEx(string pattern, RegexOptions opt) { Context.CheckCancelled(); // Check if the user has cancelled Boolean caseSensitive = (opt & RegexOptions.IgnoreCase) == RegexOptions.IgnoreCase; RegExCacheIndex ri = new RegExCacheIndex(pattern, caseSensitive); if (regExCache.ContainsKey(ri)) { Context.TraceEvent(100, 0, "RegExFilter: Returning Cached RegEx"); Regex cachedRegEx; lock (regExCache.SyncRoot) { cachedRegEx = (Regex)regExCache[ri]; } return cachedRegEx; } Context.TraceEvent(100, 0, "RegExFilter: Adding RegEx to Cache"); Regex newRegEx = new Regex(pattern, opt); lock(regExCache.SyncRoot) { regExCache.Add(ri, newRegEx); } return newRegEx; } #endregion "Helper Functions" } // End class #region "Helper Classes" #region SQL Like string utilities // author is http://stackoverflow.com/users/13355/komma8-komma1 /* public class TestSqlLikeFunction { static void Main(string[] args) { TestSqlLikePattern(true, "%", ""); TestSqlLikePattern(true, "%", " "); TestSqlLikePattern(true, "%", "asdfa asdf asdf"); TestSqlLikePattern(true, "%", "%"); TestSqlLikePattern(false, "_", ""); TestSqlLikePattern(true, "_", " "); TestSqlLikePattern(true, "_", "4"); TestSqlLikePattern(true, "_", "C"); TestSqlLikePattern(false, "_", "CX"); TestSqlLikePattern(false, "[ABCD]", ""); TestSqlLikePattern(true, "[ABCD]", "A"); TestSqlLikePattern(true, "[ABCD]", "b"); TestSqlLikePattern(false, "[ABCD]", "X"); TestSqlLikePattern(false, "[ABCD]", "AB"); TestSqlLikePattern(true, "[B-D]", "C"); TestSqlLikePattern(true, "[B-D]", "D"); TestSqlLikePattern(false, "[B-D]", "A"); TestSqlLikePattern(false, "[^B-D]", "C"); TestSqlLikePattern(false, "[^B-D]", "D"); TestSqlLikePattern(true, "[^B-D]", "A"); TestSqlLikePattern(true, "%TEST[ABCD]XXX", "lolTESTBXXX"); TestSqlLikePattern(false, "%TEST[ABCD]XXX", "lolTESTZXXX"); TestSqlLikePattern(false, "%TEST[^ABCD]XXX", "lolTESTBXXX"); TestSqlLikePattern(true, "%TEST[^ABCD]XXX", "lolTESTZXXX"); TestSqlLikePattern(true, "%TEST[B-D]XXX", "lolTESTBXXX"); TestSqlLikePattern(true, "%TEST[^B-D]XXX", "lolTESTZXXX"); TestSqlLikePattern(true, "%Stuff.txt", "Stuff.txt"); TestSqlLikePattern(true, "%Stuff.txt", "MagicStuff.txt"); TestSqlLikePattern(false, "%Stuff.txt", "MagicStuff.txt.img"); TestSqlLikePattern(false, "%Stuff.txt", "Stuff.txt.img"); TestSqlLikePattern(false, "%Stuff.txt", "MagicStuff001.txt.img"); TestSqlLikePattern(true, "Stuff.txt%", "Stuff.txt"); TestSqlLikePattern(false, "Stuff.txt%", "MagicStuff.txt"); TestSqlLikePattern(false, "Stuff.txt%", "MagicStuff.txt.img"); TestSqlLikePattern(true, "Stuff.txt%", "Stuff.txt.img"); TestSqlLikePattern(false, "Stuff.txt%", "MagicStuff001.txt.img"); TestSqlLikePattern(true, "%Stuff.txt%", "Stuff.txt"); TestSqlLikePattern(true, "%Stuff.txt%", "MagicStuff.txt"); TestSqlLikePattern(true, "%Stuff.txt%", "MagicStuff.txt.img"); TestSqlLikePattern(true, "%Stuff.txt%", "Stuff.txt.img"); TestSqlLikePattern(false, "%Stuff.txt%", "MagicStuff001.txt.img"); TestSqlLikePattern(true, "%Stuff%.txt", "Stuff.txt"); TestSqlLikePattern(true, "%Stuff%.txt", "MagicStuff.txt"); TestSqlLikePattern(false, "%Stuff%.txt", "MagicStuff.txt.img"); TestSqlLikePattern(false, "%Stuff%.txt", "Stuff.txt.img"); TestSqlLikePattern(false, "%Stuff%.txt", "MagicStuff001.txt.img"); TestSqlLikePattern(true, "%Stuff%.txt", "MagicStuff001.txt"); TestSqlLikePattern(true, "Stuff%.txt%", "Stuff.txt"); TestSqlLikePattern(false, "Stuff%.txt%", "MagicStuff.txt"); TestSqlLikePattern(false, "Stuff%.txt%", "MagicStuff.txt.img"); TestSqlLikePattern(true, "Stuff%.txt%", "Stuff.txt.img"); TestSqlLikePattern(false, "Stuff%.txt%", "MagicStuff001.txt.img"); TestSqlLikePattern(false, "Stuff%.txt%", "MagicStuff001.txt"); TestSqlLikePattern(true, "%Stuff%.txt%", "Stuff.txt"); TestSqlLikePattern(true, "%Stuff%.txt%", "MagicStuff.txt"); TestSqlLikePattern(true, "%Stuff%.txt%", "MagicStuff.txt.img"); TestSqlLikePattern(true, "%Stuff%.txt%", "Stuff.txt.img"); TestSqlLikePattern(true, "%Stuff%.txt%", "MagicStuff001.txt.img"); TestSqlLikePattern(true, "%Stuff%.txt%", "MagicStuff001.txt"); TestSqlLikePattern(true, "_Stuff_.txt_", "1Stuff3.txt4"); TestSqlLikePattern(false, "_Stuff_.txt_", "1Stuff.txt4"); TestSqlLikePattern(false, "_Stuff_.txt_", "1Stuff3.txt"); TestSqlLikePattern(false, "_Stuff_.txt_", "Stuff3.txt4"); Console.ReadKey(); } public static void TestSqlLikePattern(bool expectedResult, string pattern, string testString) { bool result = testString.SqlLike(pattern); if (expectedResult != result) { Console.ForegroundColor = ConsoleColor.Red; System.Console.Out.Write("[SqlLike] FAIL"); } else { Console.ForegroundColor = ConsoleColor.Green; Console.Write("[SqlLike] PASS"); } Console.ForegroundColor = ConsoleColor.White; Console.WriteLine(": \"" + testString + "\" LIKE \"" + pattern + "\" == " + expectedResult); } } public static class SqlLikeStringExtensions { public static bool SqlLike(this string s, string pattern) { return SqlLikeStringUtilities.SqlLike(pattern, s); } } */ internal static class SqlLikeStringUtilities { public static bool SqlLike(string pattern, string str) { // todo bugfix extend with cases with mix of [A-H] and [XWZ] : TestSqlLikePattern(true, "[A-H1234-70]", "6") will fail // todo bugfix in some languages where national alphabet in codepage is not in alpabetical order the construction [A-Z] may parse to "List set" incorrectly: for example in russian [À-ß] not include "¨" // todo made case sensitive variant // todo common usecases optimization // todo think: can it be rewritten to make pattren cacheable? Is it worh to cache it? bool isMatch = true, isWildCardOn = false, isCharWildCardOn = false, isCharSetOn = false, isNotCharSetOn = false, endOfPattern = false; int lastWildCard = -1; int patternIndex = 0; List set = new List(); char p = '\0'; for (int i = 0; i < str.Length; i++) { char c = str[i]; endOfPattern = (patternIndex >= pattern.Length); if (!endOfPattern) { p = pattern[patternIndex]; if (!isWildCardOn && p == '%') { lastWildCard = patternIndex; isWildCardOn = true; while (patternIndex < pattern.Length && pattern[patternIndex] == '%') { patternIndex++; } p = patternIndex >= pattern.Length ? '\0' : pattern[patternIndex]; } else if (p == '_') { isCharWildCardOn = true; patternIndex++; } else if (p == '[') { if (pattern[++patternIndex] == '^') { isNotCharSetOn = true; patternIndex++; } else isCharSetOn = true; set.Clear(); if (pattern[patternIndex + 1] == '-' && pattern[patternIndex + 3] == ']') { char start = char.ToUpper(pattern[patternIndex]); patternIndex += 2; char end = char.ToUpper(pattern[patternIndex]); if (start <= end) { for (char ci = start; ci <= end; ci++) { set.Add(ci); } } patternIndex++; } while (patternIndex < pattern.Length && pattern[patternIndex] != ']') { set.Add(pattern[patternIndex]); patternIndex++; } patternIndex++; } } if (isWildCardOn) { if (char.ToUpper(c) == char.ToUpper(p)) { isWildCardOn = false; patternIndex++; } } else if (isCharWildCardOn) { isCharWildCardOn = false; } else if (isCharSetOn || isNotCharSetOn) { bool charMatch = (set.Contains(char.ToUpper(c))); if ((isNotCharSetOn && charMatch) || (isCharSetOn && !charMatch)) { if (lastWildCard >= 0) patternIndex = lastWildCard; else { isMatch = false; break; } } isNotCharSetOn = isCharSetOn = false; } else { if (char.ToUpper(c) == char.ToUpper(p)) { patternIndex++; } else { if (lastWildCard >= 0) patternIndex = lastWildCard; else { isMatch = false; break; } } } } endOfPattern = (patternIndex >= pattern.Length); if (isMatch && !endOfPattern) { bool isOnlyWildCards = true; for (int i = patternIndex; i < pattern.Length; i++) { if (pattern[i] != '%') { isOnlyWildCards = false; break; } } if (isOnlyWildCards) endOfPattern = true; } return isMatch && endOfPattern; } } #endregion #region RegEx helpers internal class RegExCacheIndex { public RegExCacheIndex(string pattern, Boolean caseSensitive) { m_caseSensitive = caseSensitive; m_pattern = pattern; } private Boolean m_caseSensitive = false; private String m_pattern = ""; public Boolean CaseSensitive { get { return m_caseSensitive; } //set { m_caseSensitive = value; } } public String Pattern { get { return m_pattern; } //set { m_pattern = value; } } } internal class RegExCacheIndexComparer: IEqualityComparer { #region IEqualityComparer Members bool IEqualityComparer.Equals(object x, object y) { RegExCacheIndex left = (RegExCacheIndex)x; RegExCacheIndex right = (RegExCacheIndex)y; if (left.CaseSensitive == right.CaseSensitive) { if (left.CaseSensitive == true) { return (string.Compare(left.Pattern, right.Pattern, false)==0); } else { return (string.Compare(left.Pattern, right.Pattern, true)==0); } } else { return false; } } int IEqualityComparer.GetHashCode(object obj) { RegExCacheIndex ri = (RegExCacheIndex)obj; string pat = (ri.CaseSensitive ? "T:" : "F:") + ri.Pattern; return pat.GetHashCode(); } #endregion } #endregion #endregion } // End Namespace