diff --git a/Frontmatter.Test/PropertyMergerTests.cs b/Frontmatter.Test/PropertyMergerTests.cs index 6d758e5..8a72a51 100644 --- a/Frontmatter.Test/PropertyMergerTests.cs +++ b/Frontmatter.Test/PropertyMergerTests.cs @@ -745,4 +745,48 @@ public void CombineFrontmatter_WithMaximumStrategy_MergesAllRelevantProperties() // Tags variants should be merged Assert.IsTrue(extractedFrontmatter.ContainsKey("tags"), "Result should contain key 'tags'"); } + + [TestMethod] + public void MergeSimilarProperties_SingleListWithRepeatedItems_KeepsEveryItem() + { + Dictionary frontmatter = new() + { + { "steps", new List { "mix", "wait", "mix" } }, + { "scores", new List { "1", "1", "2" } } + }; + + Dictionary result = PropertyMerger.MergeSimilarProperties(frontmatter, FrontmatterMergeStrategy.Conservative); + + CollectionAssert.AreEqual(new object[] { "mix", "wait", "mix" }, (System.Collections.ICollection)result["steps"]); + CollectionAssert.AreEqual(new object[] { "1", "1", "2" }, (System.Collections.ICollection)result["scores"]); + } + + [TestMethod] + public void MergeSimilarProperties_MergedListsRepeatWithinOneList_KeepsTheRepeatsAndDropsOnlyCrossListDuplicates() + { + Dictionary frontmatter = new() + { + { "tags", new List { "a", "a", "c" } }, + { "tag", new List { "b", "c", "b" } } + }; + + Dictionary result = PropertyMerger.MergeSimilarProperties(frontmatter, FrontmatterMergeStrategy.Conservative); + + Assert.HasCount(1, result); + CollectionAssert.AreEqual(new object[] { "a", "a", "c", "b", "b" }, (System.Collections.ICollection)result["tags"]); + } + + [TestMethod] + public void CombineFrontmatter_SingleBlockWithRepeatedListItems_RoundTripsTheLists() + { + string nl = Environment.NewLine; + string input = $"---{nl}steps: [mix, wait, mix]{nl}scores: [1, 1, 2]{nl}---{nl}Body{nl}"; + + string result = Frontmatter.CombineFrontmatter(input, FrontmatterNaming.AsIs, FrontmatterOrder.AsIs, FrontmatterMergeStrategy.Conservative); + Dictionary? frontmatter = Frontmatter.ExtractFrontmatter(result); + + Assert.IsNotNull(frontmatter); + CollectionAssert.AreEqual(new object[] { "mix", "wait", "mix" }, (System.Collections.ICollection)frontmatter["steps"]); + CollectionAssert.AreEqual(new object[] { "1", "1", "2" }, (System.Collections.ICollection)frontmatter["scores"]); + } } diff --git a/Frontmatter/PropertyMerger.cs b/Frontmatter/PropertyMerger.cs index 9eac550..70ab447 100644 --- a/Frontmatter/PropertyMerger.cs +++ b/Frontmatter/PropertyMerger.cs @@ -172,18 +172,20 @@ private static void MergeArrayValues( string canonicalKey, List originalKeys) { - HashSet mergedList = []; + // Lists are appended in order. An item is dropped only when an earlier list being merged already + // supplied it, never because it repeats within its own list: a list such as recipe steps or + // scores can legitimately hold the same value more than once. + List mergedList = []; + HashSet fromEarlierLists = []; foreach (string key in originalKeys) { - object value = source[key]; - if (value is IList list) + if (source[key] is not IList items) { - mergedList.UnionWith(list); - } - else if (value is object[] array) - { - mergedList.UnionWith(array); + continue; } + + mergedList.AddRange(items.Where(item => !fromEarlierLists.Contains(item))); + fromEarlierLists.UnionWith(items); } target[canonicalKey] = mergedList.ToArray();