Skip to content

Instantly share code, notes, and snippets.

@mike-clark-8192
Last active March 27, 2024 12:34
Show Gist options
  • Save mike-clark-8192/8c2bdc22cbe24bcc5c1331191d88563b to your computer and use it in GitHub Desktop.
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
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