Last active
March 27, 2024 12:34
-
-
Save mike-clark-8192/8c2bdc22cbe24bcc5c1331191d88563b to your computer and use it in GitHub Desktop.
İ, ı, I, i: an [overly] exhaustive exploration of sorting and comparing in .NET
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Combinatorics.Collections; | |
using System.Globalization; | |
namespace StringComparerTest | |
{ | |
using ComparerInfo = KeyValuePair<string, StringComparer>; | |
using StringPermutations = IEnumerable<IEnumerable<string>>; | |
internal class Program | |
{ | |
static void Main(string[] args) | |
{ | |
Console.OutputEncoding = System.Text.Encoding.UTF8; | |
var comparerInfos = new[] { | |
KVP("Invariant", StringComparer.InvariantCulture), | |
KVP("InvariantIgnore", StringComparer.InvariantCultureIgnoreCase), | |
KVP("Ordinal", StringComparer.Ordinal), | |
KVP("OrdinalIgnore", StringComparer.OrdinalIgnoreCase), | |
KVP("Culture", StringComparer.CurrentCulture), | |
KVP("CultureIgnore", StringComparer.CurrentCultureIgnoreCase), | |
KVP("Turkish", StringComparer.Create(new CultureInfo("tr-TR"), ignoreCase: false)), | |
KVP("TurkishIgnore", StringComparer.Create(new CultureInfo("tr-TR"), ignoreCase: true)), | |
}; | |
// \u0131 == ı (LATIN SMALL LETTER DOTLESS I) [Turkish] | |
// \u0130 == İ (LATIN CAPITAL LETTER I WITH DOT ABOVE) [Turkish] | |
var stringsToTest = new[] { "\u0130", "\u0131", "I", "i", "x" }; | |
var varNoRepeats = new[] | |
{ | |
DoComparisonAndOutput("VariationsNoRepeats", comparerInfos, stringsToTest, | |
x => PermUtil.VariationsNoRepeats(x, 2)), | |
DoComparisonAndOutput("Cx.VariationsNoRepeats", comparerInfos, stringsToTest, | |
x => new Variations<string>(x, 2, GenerateOption.WithoutRepetition).Select(p => p.ToList())), | |
}; | |
CompareComparisonsResults(varNoRepeats); | |
var variations = new[] | |
{ | |
DoComparisonAndOutput("Variations", comparerInfos, stringsToTest, x => PermUtil.VariationsMy(x, 2)), | |
DoComparisonAndOutput("Cx.Variations", comparerInfos, stringsToTest, | |
x => new Variations<string>(x, 2, GenerateOption.WithRepetition).Select(p => p.ToList())), | |
}; | |
CompareComparisonsResults(variations); | |
//// Generates permutations with Length > 2, which the program is not designed to handle. | |
//var permutations = new[] | |
//{ | |
// DoComparisonAndOutput("PermutationsRosetta", comparerInfos, stringsToTest, PermUtil.PermutationsRosetta), | |
// DoComparisonAndOutput("Cx.Permutations", comparerInfos, stringsToTest, | |
// x => new Permutations<string>(x).Select(p => p.ToList())), | |
//}; | |
//CompareComparisonsResults(permutations); | |
TestHelper.RunTests(); | |
} | |
public static ComparisonsResult DoComparisonAndOutput( | |
string title, | |
ComparerInfo[] comparerInfos, | |
IEnumerable<string> strings, | |
Func<IEnumerable<string>, StringPermutations> permGen | |
) | |
{ | |
Console.WriteLine($" {title} ".Center(80, '=')); | |
var results = GenerateResults(comparerInfos, strings, permGen); | |
OutputResults(results); | |
AssertUtil.AssertDeepEqualsOrderInsensitive(results.perms!, results.perms!); | |
Console.ReadKey(); | |
Console.WriteLine("".PadRight(80, '-')); | |
return results; | |
} | |
public static ComparisonsResult GenerateResults( | |
ComparerInfo[] comparerInfos, | |
IEnumerable<string> strings, | |
Func<IEnumerable<string>, StringPermutations> permGen | |
) | |
{ | |
var permutations = permGen(strings).Select(p => p.ToList()).ToList(); | |
var comparisonsResult = new ComparisonsResult(); | |
foreach (var p in permutations) | |
{ | |
var result = new ComparisonResult(); | |
var strsEq = BoolStr(p[0] == p[1]); | |
result.Add(KeyValuePair.Create("Basic", $"[{p[0]},{p[1]}] {strsEq}")); | |
foreach (var comparerInfo in comparerInfos) | |
{ | |
var comparerName = comparerInfo.Key; | |
var comparer = comparerInfo.Value; | |
var sorted = p.OrderBy(x => x, comparer).ToArray(); | |
var equal = BoolStr(comparer.Equals(sorted[0], sorted[1])); | |
result.Add(KeyValuePair.Create(comparerName, $"[{sorted[0]},{sorted[1]}] {equal}")); | |
} | |
comparisonsResult.results.Add(result); | |
} | |
comparisonsResult.perms = permutations; | |
return comparisonsResult; | |
} | |
public static void CompareComparisonsResults(ComparisonsResult[] cResults) | |
{ | |
for (var i = 0; i < cResults.Length; i++) | |
{ | |
AssertUtil.AssertDeepEqualsOrderInsensitive(cResults[i].perms!, cResults[(i + 1) % cResults.Length].perms!); | |
} | |
} | |
public static void OutputResults(ComparisonsResult comparisonsResult) | |
{ | |
var results = comparisonsResult.results; | |
var headers = results[0].Select(r => r.Key.Split(".").Last()).ToList(); | |
var headerMaxLen = headers.Select(h => h.Length).Max() + 2; | |
string paddedHeaders = headers.Aggregate("", (acc, h) => acc + h.PadRight(headerMaxLen)); | |
Console.WriteLine(paddedHeaders); | |
foreach (var result in results) | |
{ | |
foreach (var entry in result) | |
{ | |
var value = entry.Value ?? "null"; | |
Console.Write(value.PadRight(headerMaxLen)); | |
} | |
Console.WriteLine(); | |
} | |
} | |
static string BoolStr(bool b) => b ? "=" : "!"; | |
static KeyValuePair<K, V> KVP<K, V>(K key, V value) => new(key, value); | |
public class ComparisonResult : List<KeyValuePair<string, string>> { } | |
public class ComparisonsResult | |
{ | |
public List<ComparisonResult> results = new(); | |
public StringPermutations? perms = null; | |
} | |
} | |
public static class PermUtil | |
{ | |
public static IEnumerable<IEnumerable<T>> PermutationsRosetta<T>(IEnumerable<T> values) where T : IComparable<T> | |
{ | |
if (values.Count() == 1) | |
return new[] { values }; | |
return values.SelectMany(v => PermutationsRosetta(values.Where(x => x.CompareTo(v) != 0)), (v, p) => p.Prepend(v)); | |
} | |
public static IEnumerable<IEnumerable<T>> VariationsMy<T>(IEnumerable<T> items, int length) | |
{ | |
return VariationsMy(items, length, (_, _) => true); | |
} | |
public static IEnumerable<IEnumerable<T>> VariationsNoRepeats<T>(IEnumerable<T> items, int length) | |
{ | |
return VariationsMy(items, length, (entries, entry) => !entries.Contains(entry)); | |
} | |
public static IEnumerable<IEnumerable<T>> VariationsMy<T>(IEnumerable<T> items, int length, Func<IEnumerable<T>, T, bool> filter) | |
{ | |
if (length == 0) return new[] { Array.Empty<T>() }; | |
if (length == 1) return items.Where(item => filter(Enumerable.Empty<T>(), item)).Select(item => new[] { item }); | |
var variations = VariationsMy(items, length - 1, filter); | |
return items.SelectMany( | |
item => variations.Where(variation => filter(variation, item)), | |
(item, variation) => variation.Prepend(item)); | |
} | |
} | |
public static class AssertUtil | |
{ | |
public class ListComparer<T> : IComparer<List<T>> where T : IComparable<T> | |
{ | |
public int Compare(List<T>? x, List<T>? y) | |
{ | |
if (x == y) { return 0; } | |
if (x == null || y == null) return x == null ? -1 : 1; | |
if (x.Count != y.Count) return x.Count - y.Count; | |
for (int i = 0; i < x.Count; i++) | |
{ | |
int comparison = x[i].CompareTo(y[i]); | |
if (comparison != 0) | |
{ | |
return comparison; | |
} | |
} | |
return 0; | |
} | |
} | |
public static void AssertDeepEqualsOrderInsensitive<T>(IEnumerable<IEnumerable<T>> a, IEnumerable<IEnumerable<T>> b) where T : notnull, IComparable<T> | |
{ | |
var sortedMatrixA = a.Select(row => row.OrderBy(x => x).ToList()).OrderBy(row => row, new ListComparer<T>()).ToList(); | |
var sortedMatrixB = b.Select(row => row.OrderBy(x => x).ToList()).OrderBy(row => row, new ListComparer<T>()).ToList(); | |
if (sortedMatrixA.Count != sortedMatrixB.Count) | |
{ | |
throw new Exception("Matrices do not have the same number of rows."); | |
} | |
for (int rowIndex = 0; rowIndex < sortedMatrixA.Count; rowIndex++) | |
{ | |
var rowA = sortedMatrixA[rowIndex]; | |
var rowB = sortedMatrixB[rowIndex]; | |
if (rowA.Count != rowB.Count || new ListComparer<T>().Compare(rowA, rowB) != 0) | |
{ | |
throw new Exception($"Matrices differ at row {rowIndex}."); | |
} | |
} | |
} | |
} | |
public static class TestHelper | |
{ | |
public static void RunTest<T>(IEnumerable<IEnumerable<T>> matrixA, IEnumerable<IEnumerable<T>> matrixB, bool shouldPass, string testName) where T : notnull, IComparable<T> | |
{ | |
try | |
{ | |
AssertUtil.AssertDeepEqualsOrderInsensitive(matrixA, matrixB); | |
if (shouldPass) | |
{ | |
Console.WriteLine($"PASS: {testName}"); | |
} | |
else | |
{ | |
Console.WriteLine($"FAIL (expected to fail, but passed): {testName}"); | |
} | |
} | |
catch (Exception ex) | |
{ | |
if (!shouldPass) | |
{ | |
Console.WriteLine($"PASS (expected): {testName} - Failed as expected with message: {ex.Message}"); | |
} | |
else | |
{ | |
Console.WriteLine($"FAIL: {testName} - {ex.Message}"); | |
} | |
} | |
} | |
public static void RunTests() | |
{ | |
var a = new[] { new[] { "a", "b" }, new[] { "c", "d" } }; | |
var b = new[] { new[] { "b", "a" }, new[] { "d", "c" } }; | |
var c = new[] { new[] { "a", "b" }, new[] { "c", "d" }, new[] { "e", "f" } }; | |
var d = new[] { new[] { "b", "a" }, new[] { "d", "c" }, new[] { "f", "e" } }; | |
RunTest(a, b, true, "Test1"); | |
RunTest(a, c, false, "Test2"); | |
RunTest(a, d, false, "Test3"); | |
// Test Cases | |
var a1 = new List<List<string>> { new List<string> { "a", "a", "b" } }; | |
var b1 = new List<List<string>> { new List<string> { "a", "b", "b" } }; | |
var a2 = new List<List<string>> { new List<string> { "a", "a", "b" }, new List<string> { "a", "a", "b" }, new List<string> { "a", "b", "b" } }; | |
var b2 = new List<List<string>> { new List<string> { "a", "a", "b" }, new List<string> { "a", "b", "b" }, new List<string> { "a", "b", "b" } }; | |
var pa1 = new List<List<string>> { new List<string> { "a", "b", "a" } }; | |
var pb1 = new List<List<string>> { new List<string> { "b", "a", "a" } }; | |
var pa2 = new List<List<string>> { new List<string> { "c", "d", "e" }, new List<string> { "a", "b", "a" } }; | |
var pb2 = new List<List<string>> { new List<string> { "b", "a", "a" }, new List<string> { "c", "d", "e" } }; | |
// Failing Cases | |
RunTest(a1, b1, false, "Failing Test a1-b1"); | |
RunTest(b1, a1, false, "Failing Test b1-a1"); | |
RunTest(a2, b2, false, "Failing Test a2-b2"); | |
RunTest(b2, a2, false, "Failing Test b2-a2"); | |
// Passing Cases | |
RunTest(pa1, pb1, true, "Passing Test pa1-pb1"); | |
RunTest(pb1, pa1, true, "Passing Test pb1-pa1"); | |
RunTest(pa2, pb2, true, "Passing Test pa2-pb2"); | |
RunTest(pb2, pa2, true, "Passing Test pb2-pa2"); | |
} | |
} | |
public static class StringExtensions | |
{ | |
public static string Center(this string str, int width, char fillChar = ' ') | |
{ | |
if (str == null || width <= str.Length) | |
return str ?? "null"; | |
int padding = width - str.Length; | |
int padLeft = padding / 2 + str.Length; | |
return str.PadLeft(padLeft, fillChar).PadRight(width, fillChar); | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment