diff --git a/src/Avalonia.Controls/IndexRange.cs b/src/Avalonia.Controls/IndexRange.cs index b1a112ab39..124f1e0500 100644 --- a/src/Avalonia.Controls/IndexRange.cs +++ b/src/Avalonia.Controls/IndexRange.cs @@ -3,12 +3,17 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. +using System; +using System.Collections.Generic; + #nullable enable namespace Avalonia.Controls { - internal readonly struct IndexRange + internal readonly struct IndexRange : IEquatable { + private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue); + public IndexRange(int begin, int end) { // Accept out of order begin/end pairs, just swap them. @@ -25,11 +30,9 @@ namespace Avalonia.Controls public int Begin { get; } public int End { get; } + public int Count => (End - Begin) + 1; - public bool Contains(int index) - { - return index >= Begin && index <= End; - } + public bool Contains(int index) => index >= Begin && index <= End; public bool Split(int splitIndex, out IndexRange before, out IndexRange after) { @@ -54,6 +57,164 @@ namespace Avalonia.Controls public bool Intersects(IndexRange other) { return (Begin <= other.End) && (End >= other.Begin); - } + } + + public bool Adjacent(IndexRange other) + { + return Begin == other.End + 1 || End == other.Begin - 1; + } + + public override bool Equals(object? obj) + { + return obj is IndexRange range && Equals(range); + } + + public bool Equals(IndexRange other) + { + return Begin == other.Begin && End == other.End; + } + + public override int GetHashCode() + { + var hashCode = 1903003160; + hashCode = hashCode * -1521134295 + Begin.GetHashCode(); + hashCode = hashCode * -1521134295 + End.GetHashCode(); + return hashCode; + } + + public override string ToString() => $"[{Begin}..{End}]"; + + public static bool operator ==(IndexRange left, IndexRange right) => left.Equals(right); + public static bool operator !=(IndexRange left, IndexRange right) => !(left == right); + + public static int Add( + IList ranges, + IndexRange range, + IList? added = null) + { + var result = 0; + + for (var i = 0; i < ranges.Count && range != s_invalid; ++i) + { + var existing = ranges[i]; + + if (range.Intersects(existing) || range.Adjacent(existing)) + { + if (range.Begin < existing.Begin) + { + var add = new IndexRange(range.Begin, existing.Begin - 1); + ranges[i] = new IndexRange(range.Begin, existing.End); + added?.Add(add); + result += add.Count; + } + + range = range.End <= existing.End ? + s_invalid : + new IndexRange(existing.End + 1, range.End); + } + else if (range.End < existing.Begin) + { + ranges.Insert(i, range); + added?.Add(range); + result += range.Count; + range = s_invalid; + } + } + + if (range != s_invalid) + { + ranges.Add(range); + added?.Add(range); + result += range.Count; + } + + MergeRanges(ranges); + return result; + } + + public static int Remove( + IList ranges, + IndexRange range, + IList? removed = null) + { + var result = 0; + + for (var i = 0; i < ranges.Count; ++i) + { + var existing = ranges[i]; + + if (range.Intersects(existing)) + { + if (range.Begin <= existing.Begin && range.End >= existing.End) + { + ranges.RemoveAt(i--); + removed?.Add(existing); + result += existing.Count; + } + else if (range.Begin > existing.Begin && range.End >= existing.End) + { + ranges[i] = new IndexRange(existing.Begin, range.Begin - 1); + removed?.Add(new IndexRange(range.Begin, existing.End)); + result += existing.End - (range.Begin - 1); + } + else if (range.Begin > existing.Begin && range.End < existing.End) + { + ranges[i] = new IndexRange(existing.Begin, range.Begin - 1); + ranges.Insert(++i, new IndexRange(range.End + 1, existing.End)); + removed?.Add(range); + result += range.Count; + } + else if (range.End <= existing.End) + { + var remove = new IndexRange(existing.Begin, range.End); + ranges[i] = new IndexRange(range.End + 1, existing.End); + removed?.Add(remove); + result += remove.Count; + } + } + } + + return result; + } + + public static IEnumerable Subtract( + IndexRange lhs, + IEnumerable rhs) + { + var result = new List { lhs }; + + foreach (var range in rhs) + { + Remove(result, range); + } + + return result; + } + + public static IEnumerable EnumerateIndices(IEnumerable ranges) + { + foreach (var range in ranges) + { + for (var i = range.Begin; i <= range.End; ++i) + { + yield return i; + } + } + } + + private static void MergeRanges(IList ranges) + { + for (var i = ranges.Count - 2; i >= 0; --i) + { + var r = ranges[i]; + var r1 = ranges[i + 1]; + + if (r.Intersects(r1) || r.End == r1.Begin - 1) + { + ranges[i] = new IndexRange(r.Begin, r1.End); + ranges.RemoveAt(i + 1); + } + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs new file mode 100644 index 0000000000..e0f46d9fa9 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class IndexRangeTests + { + [Fact] + public void Add_Should_Add_Range_To_Empty_List() + { + var ranges = new List(); + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected); + + Assert.Equal(5, result); + Assert.Equal(new[] { new IndexRange(0, 4) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 4) }, selected); + } + + [Fact] + public void Add_Should_Add_Non_Intersecting_Range_At_End() + { + var ranges = new List { new IndexRange(0, 4) }; + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected); + + Assert.Equal(3, result); + Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(8, 10) }, selected); + } + + [Fact] + public void Add_Should_Add_Non_Intersecting_Range_At_Beginning() + { + var ranges = new List { new IndexRange(8, 10) }; + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected); + + Assert.Equal(5, result); + Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 4) }, selected); + } + + [Fact] + public void Add_Should_Add_Non_Intersecting_Range_In_Middle() + { + var ranges = new List { new IndexRange(0, 4), new IndexRange(14, 16) }; + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected); + + Assert.Equal(3, result); + Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10), new IndexRange(14, 16) }, ranges); + Assert.Equal(new[] { new IndexRange(8, 10) }, selected); + } + + [Fact] + public void Add_Should_Add_Intersecting_Range_Start() + { + var ranges = new List { new IndexRange(8, 10) }; + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(6, 9), selected); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(6, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(6, 7) }, selected); + } + + [Fact] + public void Add_Should_Add_Intersecting_Range_End() + { + var ranges = new List { new IndexRange(8, 10) }; + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(9, 12), selected); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(8, 12) }, ranges); + Assert.Equal(new[] { new IndexRange(11, 12) }, selected); + } + + [Fact] + public void Add_Should_Add_Intersecting_Range_Both() + { + var ranges = new List { new IndexRange(8, 10) }; + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(6, 12), selected); + + Assert.Equal(4, result); + Assert.Equal(new[] { new IndexRange(6, 12) }, ranges); + Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 12) }, selected); + } + + [Fact] + public void Add_Should_Join_Two_Intersecting_Ranges() + { + var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14) }; + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(8, 14), selected); + + Assert.Equal(1, result); + Assert.Equal(new[] { new IndexRange(8, 14) }, ranges); + Assert.Equal(new[] { new IndexRange(11, 11) }, selected); + } + + [Fact] + public void Add_Should_Join_Two_Intersecting_Ranges_And_Add_Ranges() + { + var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14) }; + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(6, 18), selected); + + Assert.Equal(7, result); + Assert.Equal(new[] { new IndexRange(6, 18) }, ranges); + Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 11), new IndexRange(15, 18) }, selected); + } + + [Fact] + public void Add_Should_Not_Add_Already_Selected_Range() + { + var ranges = new List { new IndexRange(8, 10) }; + var selected = new List(); + var result = IndexRange.Add(ranges, new IndexRange(9, 10), selected); + + Assert.Equal(0, result); + Assert.Equal(new[] { new IndexRange(8, 10) }, ranges); + Assert.Empty(selected); + } + + [Fact] + public void Remove_Should_Remove_Entire_Range() + { + var ranges = new List { new IndexRange(8, 10) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected); + + Assert.Equal(3, result); + Assert.Empty(ranges); + Assert.Equal(new[] { new IndexRange(8, 10) }, deselected); + } + + [Fact] + public void Remove_Should_Remove_Start_Of_Range() + { + var ranges = new List { new IndexRange(8, 12) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected); + + Assert.Equal(3, result); + Assert.Equal(new[] { new IndexRange(11, 12) }, ranges); + Assert.Equal(new[] { new IndexRange(8, 10) }, deselected); + } + + [Fact] + public void Remove_Should_Remove_End_Of_Range() + { + var ranges = new List { new IndexRange(8, 12) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(10, 12), deselected); + + Assert.Equal(3, result); + Assert.Equal(new[] { new IndexRange(8, 9) }, ranges); + Assert.Equal(new[] { new IndexRange(10, 12) }, deselected); + } + + [Fact] + public void Remove_Should_Remove_Overlapping_End_Of_Range() + { + var ranges = new List { new IndexRange(8, 12) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(10, 14), deselected); + + Assert.Equal(3, result); + Assert.Equal(new[] { new IndexRange(8, 9) }, ranges); + Assert.Equal(new[] { new IndexRange(10, 12) }, deselected); + } + + [Fact] + public void Remove_Should_Remove_Middle_Of_Range() + { + var ranges = new List { new IndexRange(10, 20) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(12, 16), deselected); + + Assert.Equal(5, result); + Assert.Equal(new[] { new IndexRange(10, 11), new IndexRange(17, 20) }, ranges); + Assert.Equal(new[] { new IndexRange(12, 16) }, deselected); + } + + [Fact] + public void Remove_Should_Remove_Multiple_Ranges() + { + var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(6, 15), deselected); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(16, 18) }, ranges); + Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 14) }, deselected); + } + + [Fact] + public void Remove_Should_Remove_Multiple_And_Partial_Ranges_1() + { + var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(9, 15), deselected); + + Assert.Equal(5, result); + Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(16, 18) }, ranges); + Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 14) }, deselected); + } + + [Fact] + public void Remove_Should_Remove_Multiple_And_Partial_Ranges_2() + { + var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(8, 13), deselected); + + Assert.Equal(5, result); + Assert.Equal(new[] { new IndexRange(14, 14), new IndexRange(16, 18) }, ranges); + Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 13) }, deselected); + } + + [Fact] + public void Remove_Should_Remove_Multiple_And_Partial_Ranges_3() + { + var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(9, 13), deselected); + + Assert.Equal(4, result); + Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(14, 14), new IndexRange(16, 18) }, ranges); + Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 13) }, deselected); + } + + [Fact] + public void Remove_Should_Do_Nothing_For_Unselected_Range() + { + var ranges = new List { new IndexRange(8, 10) }; + var deselected = new List(); + var result = IndexRange.Remove(ranges, new IndexRange(2, 4), deselected); + + Assert.Equal(0, result); + Assert.Equal(new[] { new IndexRange(8, 10) }, ranges); + Assert.Empty(deselected); + } + + [Fact] + public void Stress_Test() + { + const int iterations = 100; + var random = new Random(0); + var selection = new List(); + var expected = new List(); + + IndexRange Generate() + { + var start = random.Next(100); + return new IndexRange(start, start + random.Next(20)); + } + + for (var i = 0; i < iterations; ++i) + { + var toAdd = random.Next(5); + + for (var j = 0; j < toAdd; ++j) + { + var range = Generate(); + IndexRange.Add(selection, range); + + for (var k = range.Begin; k <= range.End; ++k) + { + if (!expected.Contains(k)) + { + expected.Add(k); + } + } + + var actual = IndexRange.EnumerateIndices(selection).ToList(); + expected.Sort(); + Assert.Equal(expected, actual); + } + + var toRemove = random.Next(5); + + for (var j = 0; j < toRemove; ++j) + { + var range = Generate(); + IndexRange.Remove(selection, range); + + for (var k = range.Begin; k <= range.End; ++k) + { + expected.Remove(k); + } + + var actual = IndexRange.EnumerateIndices(selection).ToList(); + Assert.Equal(expected, actual); + } + + selection.Clear(); + expected.Clear(); + } + } + } +}