Skip to content

Commit 2fb3eff

Browse files
Fix sort exception when using binary search (#984)
* Fix sort exception when using binary search
1 parent 9420bf4 commit 2fb3eff

File tree

3 files changed

+78
-18
lines changed

3 files changed

+78
-18
lines changed

src/DynamicData.Tests/Cache/SortAndBindFixture.cs

+43-8
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,57 @@ protected override (ChangeSetAggregator<Person, string> Aggregrator, IList<Perso
9191
}
9292

9393
// Bind to a readonly observable collection using binary search
94-
public sealed class SortAndBindWithBinarySearch : SortAndBindFixture
94+
public sealed class SortAndBindWithBinarySearch1 : SortAndBindFixture
9595
{
9696
protected override (ChangeSetAggregator<Person, string> Aggregrator, IList<Person> List) SetUpTests()
9797
{
98-
var options = new SortAndBindOptions { UseBinarySearch = true };
98+
var options = new SortAndBindOptions { UseBinarySearch = true, UseReplaceForUpdates = false};
9999
var aggregator = _source.Connect().SortAndBind(out var list, _comparer, options).AsAggregator();
100100

101101
return (aggregator, list);
102102
}
103103
}
104104

105+
public sealed class SortAndBindWithBinarySearch2 : SortAndBindFixture
106+
{
107+
protected override (ChangeSetAggregator<Person, string> Aggregrator, IList<Person> List) SetUpTests()
108+
{
109+
var options = new SortAndBindOptions { UseBinarySearch = true, UseReplaceForUpdates = true };
110+
var aggregator = _source.Connect().SortAndBind(out var list, _comparer, options).AsAggregator();
111+
112+
return (aggregator, list);
113+
}
114+
}
115+
116+
public class SortAndBindBinarySearch_ForSameKeyAndObjectValues: IDisposable
117+
{
118+
private readonly List<int> _target = new();
119+
private readonly SourceCache<int, int> _strings = new(i=> i);
120+
121+
[Theory]
122+
[InlineData(false)]
123+
[InlineData(true)]
124+
public void UpdateAnyWhereShouldNotBreak(bool useReplaceForUpdates)
125+
{
126+
var options = new SortAndBindOptions { UseBinarySearch = true, UseReplaceForUpdates = useReplaceForUpdates };
127+
128+
using var subscription = _strings.Connect().SortAndBind(_target, SortExpressionComparer<int>.Ascending(i=>i), options).Subscribe();
129+
130+
var items = Enumerable.Range(1, 10).ToList();
131+
132+
_strings.AddOrUpdate(items);
133+
_strings.AddOrUpdate(1);
134+
_strings.AddOrUpdate(5);
135+
_strings.AddOrUpdate(10);
136+
137+
_target.SequenceEqual(items).Should().BeTrue();
138+
}
139+
140+
public void Dispose() => _strings.Dispose();
141+
}
142+
143+
144+
105145
// Bind to a readonly observable collection - using default comparer
106146
public sealed class SortAndBindToReadOnlyObservableCollectionDefaultComparer : SortAndBindFixture
107147
{
@@ -291,9 +331,9 @@ public void InsertAtEnd()
291331
last.Should().Be(toInsert);
292332

293333
_boundList.SequenceEqual(_source.Items.OrderBy(p => p, _comparer)).Should().BeTrue();
294-
295334
}
296335

336+
297337
[Fact]
298338
public void InsertInMiddle()
299339
{
@@ -568,8 +608,6 @@ public void UpdateFirst()
568608
[Fact]
569609
public void UpdateLast()
570610
{
571-
//TODO: fixed Text
572-
573611
var people = _generator.Take(100).ToList();
574612
_source.AddOrUpdate(people);
575613

@@ -582,9 +620,6 @@ public void UpdateLast()
582620
int IndexFromKey(string key) => people.FindIndex(p => p.Key == key);
583621

584622
people.OrderBy(p => p, _comparer).SequenceEqual(_boundList).Should().BeTrue();
585-
586-
587-
588623
}
589624

590625

src/DynamicData/Binding/SortAndBind.cs

+24-10
Original file line numberDiff line numberDiff line change
@@ -180,21 +180,35 @@ private void ApplyChanges(IChangeSet<TObject, TKey> changes)
180180
break;
181181
case ChangeReason.Update:
182182
{
183-
var currentIndex = GetCurrentPosition(change.Previous.Value);
184-
var updatedIndex = GetInsertPosition(item);
183+
if (!options.UseReplaceForUpdates)
184+
{
185+
// If using binary search, it works best when we remove then add,
186+
// so let's optimise for that first.
185187

186-
// We need to recalibrate as GetCurrentPosition includes the current item
187-
updatedIndex = currentIndex < updatedIndex ? updatedIndex - 1 : updatedIndex;
188+
var currentIndex = GetCurrentPosition(change.Previous.Value);
189+
target.RemoveAt(currentIndex);
188190

189-
// Some control suites and platforms do not support replace, whiles others do, so we opt in.
190-
if (options.UseReplaceForUpdates && currentIndex == updatedIndex)
191-
{
192-
target[currentIndex] = item;
191+
var updatedIndex = GetInsertPosition(item);
192+
target.Insert(updatedIndex, item);
193193
}
194194
else
195195
{
196-
target.RemoveAt(currentIndex);
197-
target.Insert(updatedIndex, item);
196+
var currentIndex = GetCurrentPosition(change.Previous.Value);
197+
var updatedIndex = GetInsertPosition(item);
198+
199+
// We need to recalibrate as GetCurrentPosition includes the current item
200+
updatedIndex = currentIndex < updatedIndex ? updatedIndex - 1 : updatedIndex;
201+
202+
// Some control suites and platforms do not support replace, whiles others do, so we opt in.
203+
if (currentIndex == updatedIndex)
204+
{
205+
target[currentIndex] = item;
206+
}
207+
else
208+
{
209+
target.RemoveAt(currentIndex);
210+
target.Insert(updatedIndex, item);
211+
}
198212
}
199213
}
200214
break;

src/DynamicData/Cache/Internal/SortExtensions.cs

+11
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ public static int GetInsertPositionBinary<TItem>(this IList<TItem> list, TItem t
3535
// sort is not returning uniqueness
3636
if (insertIndex < 0)
3737
{
38+
/*
39+
* Binary search should not strictly already contain the item (or an item with the same value) when
40+
* attempting to find the insert position (it does for updates). This can result in the insert position not being found.
41+
* In this case revert to linear search.
42+
*/
43+
index = list.GetInsertPositionLinear(t, c);
44+
if (index >= 0)
45+
{
46+
return index;
47+
}
48+
3849
throw new SortException("Binary search has been specified, yet the sort does not yield uniqueness");
3950
}
4051

0 commit comments

Comments
 (0)