diff --git a/Frontmatter.Test/StandardOrderCompareTotalOrderTests.cs b/Frontmatter.Test/StandardOrderCompareTotalOrderTests.cs
new file mode 100644
index 0000000..362c60a
--- /dev/null
+++ b/Frontmatter.Test/StandardOrderCompareTotalOrderTests.cs
@@ -0,0 +1,66 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Frontmatter.Test;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+///
+/// Names that differ only in case must not compare as equal, or a sorted collection
+/// built on throws or drops keys (#197).
+///
+[TestClass]
+public class StandardOrderCompareTotalOrderTests
+{
+ private static readonly Comparer Comparer = Comparer.Create(StandardOrder.Compare);
+
+ [TestMethod]
+ [DataRow("Title", "title")]
+ [DataRow("MyKey", "mykey")]
+ [DataRow("TAGS", "Tags")]
+ public void Compare_CaseVariants_AreNotEqualAndAntisymmetric(string a, string b)
+ {
+ int forward = StandardOrder.Compare(a, b);
+ int backward = StandardOrder.Compare(b, a);
+
+ Assert.AreNotEqual(0, forward, $"'{a}' and '{b}' are different strings and must not compare equal");
+ Assert.AreEqual(-Math.Sign(forward), Math.Sign(backward), "Compare must be antisymmetric");
+ }
+
+ [TestMethod]
+ public void Compare_SameString_IsZero()
+ {
+ Assert.AreEqual(0, StandardOrder.Compare("Title", "Title"));
+ Assert.AreEqual(0, StandardOrder.Compare("mykey", "mykey"));
+ }
+
+ [TestMethod]
+ public void SortedDictionary_KeepsCaseVariantKeys()
+ {
+ SortedDictionary dictionary = new(Comparer)
+ {
+ { "title", "a" },
+ { "Title", "b" },
+ };
+
+ Assert.HasCount(2, dictionary);
+ Assert.AreEqual("a", dictionary["title"]);
+ Assert.AreEqual("b", dictionary["Title"]);
+ }
+
+ [TestMethod]
+ public void SortedSet_KeepsCaseVariantKeys()
+ {
+ SortedSet set = new(["tags", "Tags", "myKey", "MyKey"], Comparer);
+
+ Assert.HasCount(4, set);
+ }
+
+ [TestMethod]
+ public void Compare_OrderAcrossPositionsIsUnchanged()
+ {
+ Assert.IsLessThan(0, StandardOrder.Compare("Title", "author"), "Known keys keep their standard order regardless of case");
+ Assert.IsLessThan(0, StandardOrder.Compare("Tags", "zzz_unknown"), "Known keys come before unknown keys");
+ Assert.IsLessThan(0, StandardOrder.Compare("Alpha", "beta"), "Unknown keys still sort case-insensitively");
+ Assert.IsGreaterThan(0, StandardOrder.Compare("zzz_unknown", "TITLE"), "Unknown keys come after known keys");
+ }
+}
diff --git a/Frontmatter/StandardOrder.cs b/Frontmatter/StandardOrder.cs
index a086b10..5dd6668 100644
--- a/Frontmatter/StandardOrder.cs
+++ b/Frontmatter/StandardOrder.cs
@@ -203,9 +203,13 @@ public static class StandardOrder
/// The second property name.
///
/// A negative value if 'a' should appear before 'b',
- /// zero if they are equal in order,
+ /// zero only if 'a' and 'b' are the same string,
/// a positive value if 'a' should appear after 'b'.
///
+ ///
+ /// Names that differ only in case share a position, so they are ordered against each other ordinally.
+ /// That keeps the comparison a total order, so it can back a sorted collection without losing keys.
+ ///
public static int Compare(string a, string b)
{
Ensure.NotNull(a);
@@ -216,6 +220,12 @@ public static int Compare(string a, string b)
return 0;
}
+ int result = CompareByPosition(a, b);
+ return result != 0 ? result : string.CompareOrdinal(a, b);
+ }
+
+ private static int CompareByPosition(string a, string b)
+ {
// Get the indices from the standard property list
int indexA = Array.IndexOf(PropertyNames, a.ToLowerInvariant());
int indexB = Array.IndexOf(PropertyNames, b.ToLowerInvariant());