diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/RangeHelperTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/RangeHelperTests.cs new file mode 100644 index 0000000000..8b23f4a9c6 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/RangeHelperTests.cs @@ -0,0 +1,484 @@ +using System.Linq; +using FSharp.Compiler.Text; +using Shouldly; +using Stryker.Core.Helpers; +using Xunit; + +namespace Stryker.Core.UnitTest.Helpers +{ + public class RangeHelperTests : TestBase + { + [Fact] + public void Reduce_Empty() + { + var result = Enumerable.Empty().Reduce("test.fs"); + + result.ShouldBeEmpty(); + } + + [Fact] + public void Reduce_Hollow() + { + var range = GetRange((42, 42), (42, 42)); + + var result = new[] { range }.Reduce("test.fs"); + + result.ShouldBeEmpty(); + } + + [Fact] + public void Reduce_One() + { + var range = GetRange((0, 0), (42, 42)); + + var result = new[] { range }.Reduce("test.fs"); + + result.ShouldBe(new[] { range }); + } + + [Fact] + public void Reduce_TwoEqual() + { + var range1 = GetRange((0, 0), (42, 42)); + var range2 = GetRange((0, 0), (42, 42)); + + var result = new[] { range1, range2 }.Reduce("test.fs"); + + result.ShouldBe(new[] { range1 }); + } + + [Fact] + public void Reduce_TwoSequential() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((0, 0), (22, 22)); + var merged = GetRange((0, 0), (22, 22)); + + var result = new[] { range1, range2 }.Reduce("test.fs"); + + result.ShouldBe(new[] { merged }); + } + + [Fact] + public void Reduce_TwoIntersecting() + { + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + var intersection = GetRange((0, 0), (33, 33)); + + var result = new[] { range1, range2 }.Reduce("test.fs"); + + result.ShouldBe(new[] { intersection }); + } + + [Fact] + public void Reduce_TwoNotIntersecting() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + + var result = new[] { range1, range2 }.Reduce("test.fs"); + + result.ShouldBe(new[] { range1, range2 }); + } + + [Fact] + public void Reduce_ThreeSequential() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((11, 11), (22, 22)); + var range3 = GetRange((22, 22), (33, 33)); + var merged = GetRange((0, 0), (33, 33)); + + var result = new[] { range1, range2, range3 }.Reduce("test.fs"); + + result.ShouldBe(new[] { merged }); + } + + [Fact] + public void Reduce_ThreePartiallyIntersecting() + { + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + var range3 = GetRange((44, 44), (55, 55)); + var intersection = GetRange((0, 0), (33, 33)); + + var result = new[] { range1, range2, range3 }.Reduce("test.fs"); + + result.ShouldBe(new[] { intersection, range3 }, ignoreOrder: true); + } + + [Fact] + public void RemoveOverlap_Empty() + { + var result = Enumerable.Empty().RemoveOverlap(Enumerable.Empty(), "test.fs"); + + result.ShouldBeEmpty(); + } + + [Fact] + public void RemoveOverlap_OverlappingPartiallyLeft() + { + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (22, 22)); + var rangeWithoutOverlap = GetRange((0, 0), (11, 11)); + + var result = new[] { range1 }.RemoveOverlap(new[] { range2 }, "test.fs"); + + result.ShouldBe(new[] { rangeWithoutOverlap }); + } + + [Fact] + public void RemoveOverlap_OverlappingPartiallyRight() + { + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((0, 0), (11, 11)); + var rangeWithoutOverlap = GetRange((11, 11), (22, 22)); + + var result = new[] { range1 }.RemoveOverlap(new[] { range2 }, "test.fs"); + + result.ShouldBe(new[] { rangeWithoutOverlap }); + } + + [Fact] + public void RemoveOverlap_OverlappingBySequentialRanges() + { + var range1 = GetRange((0, 0), (33, 33)); + var range2 = GetRange((0, 0), (11, 11)); + var range3 = GetRange((11, 11), (22, 22)); + var rangeWithoutOverlap = GetRange((22, 22), (33, 33)); + + var result = new[] { range1 }.RemoveOverlap(new[] { range2, range3 }, "test.fs"); + + result.ShouldBe(new[] { rangeWithoutOverlap }); + } + + [Fact] + public void RemoveOverlap_OverlappingByRangesFromSides() + { + var range1 = GetRange((0, 0), (33, 33)); + var range2 = GetRange((0, 0), (11, 11)); + var range3 = GetRange((22, 22), (33, 33)); + var rangeWithoutOverlap = GetRange((11, 11), (22, 22)); + + var result = new[] { range1 }.RemoveOverlap(new[] { range2, range3 }, "test.fs"); + + result.ShouldBe(new[] { rangeWithoutOverlap }); + } + + [Fact] + public void RemoveOverlap_OverlappingCompletely() + { + var range1 = GetRange((0, 0), (42, 42)); + var range2 = GetRange((0, 0), (42, 42)); + + var result = new[] { range1 }.RemoveOverlap(new[] { range2 }, "test.fs"); + + result.ShouldBeEmpty(); + } + + [Fact] + public void RemoveOverlap_OverlappingByBigger() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((0, 0), (44, 44)); + + var result = new[] { range1 }.RemoveOverlap(new[] { range2 }, "test.fs"); + + result.ShouldBeEmpty(); + } + + [Fact] + public void RemoveOverlap_CutInTheMiddle() + { + var range1 = GetRange((0, 0), (33, 33)); + var range2 = GetRange((11, 11), (22, 22)); + var leftPart = GetRange((0, 0), (11, 11)); + var rightPart = GetRange((22, 22), (33, 33)); + + var result = new[] { range1 }.RemoveOverlap(new[] { range2 }, "test.fs"); + + result.ShouldBe(new[] { leftPart, rightPart }); + } + + [Fact] + public void RemoveOverlap_NotOverlapping() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + + var result = new[] { range1 }.RemoveOverlap(new[] { range2 }, "test.fs"); + + result.ShouldBe(new[] { range1 }); + } + + [Fact] + public void OverlapsWith_OverlappingLeft() + { + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + + range1.OverlapsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void OverlapsWith_OverlappingRight() + { + var range1 = GetRange((11, 11), (33, 33)); + var range2 = GetRange((0, 0), (22, 22)); + + range1.OverlapsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void OverlapsWith_OverlappingBetween() + { + var range1 = GetRange((0, 0), (33, 33)); + var range2 = GetRange((11, 11), (22, 22)); + + range1.OverlapsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void OverlapsWith_NotOverlapping() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + + range1.OverlapsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void OverlapsWith_EmptyLeft() + { + var range1 = GetRange((0, 0), (42, 42)); + var range2 = Range.Zero; + + range1.OverlapsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void OverlapsWith_EmptyRight() + { + var range1 = Range.Zero; + var range2 = GetRange((0, 0), (42, 42)); + + range1.OverlapsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void OverlapsWith_EmptyBoth() + { + var range1 = Range.Zero; + var range2 = Range.Zero; + + range1.OverlapsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void Overlap_Overlapping() + { + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + var overlap = GetRange((11, 11), (22, 22)); + + range1.Overlap(range2, "test.fs").ShouldBe(overlap); + } + + [Fact] + public void Overlap_Sequential() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((11, 11), (22, 22)); + + range1.Overlap(range2, "test.fs").ShouldBeNull(); + } + + [Fact] + public void Overlap_NotOverlapping() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + + range1.Overlap(range2, "test.fs").ShouldBeNull(); + } + + [Fact] + public void Max_Greater() + { + var position1 = PositionModule.mkPos(42, 42); + var position2 = PositionModule.pos0; + + var actual = RangeHelper.Max(position1, position2); + + actual.ShouldBe(position1); + } + + [Fact] + public void Max_Equal() + { + var position1 = PositionModule.pos0; + var position2 = PositionModule.pos0; + + var actual = RangeHelper.Max(position1, position2); + + actual.ShouldBe(position1); + } + + [Fact] + public void Max_Less() + { + var position1 = PositionModule.pos0; + var position2 = PositionModule.mkPos(42, 42); + + var actual = RangeHelper.Max(position1, position2); + + actual.ShouldBe(position2); + } + + [Fact] + public void Min_Greater() + { + var position1 = PositionModule.mkPos(42, 42); + var position2 = PositionModule.pos0; + + var actual = RangeHelper.Min(position1, position2); + + actual.ShouldBe(position2); + } + + [Fact] + public void Min_Equal() + { + var position1 = PositionModule.pos0; + var position2 = PositionModule.pos0; + + var actual = RangeHelper.Min(position1, position2); + + actual.ShouldBe(position2); + } + + [Fact] + public void Min_Less() + { + var position1 = PositionModule.pos0; + var position2 = PositionModule.mkPos(42, 42); + + var actual = RangeHelper.Min(position1, position2); + + actual.ShouldBe(position1); + } + + [Fact] + public void IsEmpty_ZeroRange() + { + var range = Range.Zero; + + range.IsEmpty().ShouldBeTrue(); + } + + [Fact] + public void IsEmpty_HollowRange() + { + var range = GetRange((42, 42), (42, 42)); + + range.IsEmpty().ShouldBeTrue(); + } + + [Fact] + public void IsEmpty_NotEmptyRange() + { + var range = GetRange((0, 0), (42, 42)); + + range.IsEmpty().ShouldBeFalse(); + } + + [Fact] + public void IntersectsWith_Intersecting_Left() + { + var range1 = GetRange((0, 0), (22, 22)); + var range2 = GetRange((11, 11), (33, 33)); + + range1.IntersectsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void IntersectsWith_IntersectingRight() + { + var range1 = GetRange((11, 11), (33, 33)); + var range2 = GetRange((0, 0), (22, 22)); + + range1.IntersectsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void IntersectsWith_IntersectingBetween() + { + var range1 = GetRange((0, 0), (33, 33)); + var range2 = GetRange((11, 11), (22, 22)); + + range1.IntersectsWith(range2).ShouldBeTrue(); + } + + [Fact] + public void IntersectsWith_NoIntersection() + { + var range1 = GetRange((0, 0), (11, 11)); + var range2 = GetRange((22, 22), (33, 33)); + + range1.IntersectsWith(range2).ShouldBeFalse(); + } + + [Fact] + public void GetPosition_OneLine() + { + var text = "Line1"; + var index = 1; + var position = PositionModule.mkPos(0, 1); + + var result = RangeHelper.GetPosition(text, index); + + result.ShouldBe(position); + } + + [Fact] + public void GetPosition_OutOfBounds() + { + var text = "Line1"; + var index = 42; + var position = PositionModule.mkPos(0, 5); + + var result = RangeHelper.GetPosition(text, index); + + result.ShouldBe(position); + } + + [Fact] + public void GetIndex_OneLine() + { + var text = "Line1"; + var position = PositionModule.mkPos(0, 1); + + var result = RangeHelper.GetIndex(text, position); + + result.ShouldBe(1); + } + + [Fact] + public void GetIndex_OutOfBounds() + { + var text = "Line1"; + var position = PositionModule.mkPos(42, 42); + + var result = RangeHelper.GetIndex(text, position); + + result.ShouldBe(-1); + } + + private static Range GetRange((int Line, int Column) start, (int Line, int Column) end) => + RangeModule.mkRange( + "test.fs", + PositionModule.mkPos(start.Line, start.Column), + PositionModule.mkPos(end.Line, end.Column)); + } +} diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/ProjectComponentExtensionsTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/TextSpanHelperTests.cs similarity index 94% rename from src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/ProjectComponentExtensionsTests.cs rename to src/Stryker.Core/Stryker.Core.UnitTest/Helpers/TextSpanHelperTests.cs index 88c3551606..c52563b257 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/ProjectComponentExtensionsTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Helpers/TextSpanHelperTests.cs @@ -1,14 +1,14 @@ -using Microsoft.CodeAnalysis.Text; -using Shouldly; -using Stryker.Core.ProjectComponents; using System; using System.Collections.Generic; using System.Linq; +using Microsoft.CodeAnalysis.Text; +using Shouldly; +using Stryker.Core.Helpers; using Xunit; -namespace Stryker.Core.UnitTest.ProjectComponents +namespace Stryker.Core.UnitTest.Helpers { - public class ProjectComponentExtensionsTests : TestBase + public class TextSpanHelperTests : TestBase { [Theory] [InlineData(new int[0], new int[0])] diff --git a/src/Stryker.Core/Stryker.Core/FilePattern.cs b/src/Stryker.Core/Stryker.Core/FilePattern.cs index 49fe5facfa..040bae02c4 100644 --- a/src/Stryker.Core/Stryker.Core/FilePattern.cs +++ b/src/Stryker.Core/Stryker.Core/FilePattern.cs @@ -1,6 +1,6 @@ using DotNet.Globbing; using Microsoft.CodeAnalysis.Text; -using Stryker.Core.ProjectComponents; +using Stryker.Core.Helpers; using System; using System.Collections.Generic; using System.Linq; diff --git a/src/Stryker.Core/Stryker.Core/Helpers/RangeHelper.cs b/src/Stryker.Core/Stryker.Core/Helpers/RangeHelper.cs new file mode 100644 index 0000000000..e1b5875d52 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Helpers/RangeHelper.cs @@ -0,0 +1,190 @@ +using System.Collections.Generic; +using System.Linq; +using FSharp.Compiler.Text; + +namespace Stryker.Core.Helpers +{ + public static class RangeHelper + { + /// + /// Reduces a set of ranges to the smallest set of ranges possible. + /// Two can be combined if they intersect. + /// + /// The set of s to reduce. + /// The reduced set. + public static IReadOnlyCollection Reduce(this IEnumerable ranges, string filePath) + { + var rangeList = new List(ranges); + var shouldContinue = true; + + while (shouldContinue) + { + shouldContinue = false; + + foreach (var current in rangeList) + { + // Check if any of the other ranges intersects with the current one + var other = rangeList.FirstOrDefault(s => !RangeModule.equals(s, current) && s.IntersectsWith(current)); + if (!RangeModule.equals(other, Range.Zero)) + { + // Remove the original ranges + rangeList.Remove(current); + rangeList.Remove(other); + + // Add the newly combined range. + rangeList.Add(FromBounds(filePath, Min(current.Start, other.Start), Max(current.End, other.End))); + + // We changed the list, so we have to restart the foreach. + shouldContinue = true; + break; + } + } + } + + return rangeList.Distinct().Where(x => !x.IsEmpty()).ToList(); + } + + /// + /// Removes all overlaps of two sets of and returns the resulting set. + /// + /// The first set. + /// The second set. + /// All ranges and part of ranges of that do not overlap with any ranges in . + public static IReadOnlyCollection RemoveOverlap(this IEnumerable left, IEnumerable right, string filePath) + { + var rangeList = new List(left); + var shouldContinue = true; + + while (shouldContinue) + { + shouldContinue = false; + + foreach (var current in rangeList) + { + // Check if any range overlaps the current range. + var other = right.FirstOrDefault(o => o.OverlapsWith(current)); + + if (!RangeModule.equals(other, Range.Zero)) + { + // Remove the current range add the new range(s). + rangeList.Remove(current); + rangeList.AddRange(RemoveOverlap(current, other)); + + // We changed the list, so we have to restart the foreach. + shouldContinue = true; + break; + } + } + } + + return rangeList; + + IReadOnlyCollection RemoveOverlap(Range current, Range other) + { + // The the current range is completely contained inside the other, nothing will be left. + if (RangeModule.rangeContainsRange(other, current)) + return System.Array.Empty(); + + // Check if there is any overlap. + var overlap = current.Overlap(other, filePath); + + if (!overlap.HasValue) + { + return new[] { current }; + } + + return new[] { FromBounds(filePath, current.Start, overlap.Value.Start), FromBounds(filePath, overlap.Value.End, current.End) }.Where(s => !s.IsEmpty()).ToList(); + } + } + + public static bool OverlapsWith(this Range range1, Range range2) + { + var overlapStart = Max(range1.Start, range2.Start); + var overlapEnd = Min(range1.End, range2.End); + + return PositionModule.posLt(overlapStart, overlapEnd); + } + + public static Range? Overlap(this Range range1, Range range2, string filePath) + { + var overlapStart = Max(range1.Start, range2.Start); + var overlapEnd = Min(range1.End, range2.End); + + return PositionModule.posLt(overlapStart, overlapEnd) + ? FromBounds(filePath, overlapStart, overlapEnd) + : null; + } + + public static Position Max(Position pos1, Position pos2) + => PositionModule.posGeq(pos1, pos2) ? pos1 : pos2; + + public static Position Min(Position pos1, Position pos2) + => PositionModule.posLt(pos1, pos2) ? pos1 : pos2; + + public static bool IsEmpty(this Range range) + => PositionModule.posEq(range.Start, range.End); + + public static bool IntersectsWith(this Range range1, Range range2) + => PositionModule.posGeq(range1.End, range2.Start) + && PositionModule.posGeq(range2.End, range1.Start); + + public static Position GetPosition(string text, int index) + { + var line = 0; + var col = 0; + + for (var i = 0; i < System.Math.Min(index, text.Length); i++) + { + // TODO: handle x-platform + if (text[i] == '\n') + { + line++; + col = 0; + } + else + { + col++; + } + } + + return PositionModule.mkPos(line, col); + } + + public static int GetIndex(string text, Position pos) + { + var line = 0; + var col = 0; + + for (var i = 0; i < text.Length; i++) + { + if (line == pos.Line && col == pos.Column) + { + return i; + } + + // TODO: handle x-platform + if (text[i] == '\n') + { + line++; + col = 0; + } + else + { + col++; + } + } + + return -1; + } + + public static Range FromBounds(string filePath, Position startPos, Position endPos) + => RangeModule.mkRange(filePath, startPos, endPos); + + public static Range FromBounds(string filePath, string text, int startIndex, int endIndex) + { + var startPos = GetPosition(text, startIndex); + var endPos = GetPosition(text, endIndex); + return RangeModule.mkRange(filePath, startPos, endPos); + } + } +} diff --git a/src/Stryker.Core/Stryker.Core/Helpers/TextSpanHelper.cs b/src/Stryker.Core/Stryker.Core/Helpers/TextSpanHelper.cs new file mode 100644 index 0000000000..2162865615 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Helpers/TextSpanHelper.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis.Text; + +namespace Stryker.Core.Helpers +{ + public static class TextSpanHelper + { + /// + /// Reduces a set of text spans to the smallest set of text spans possible. + /// Two can be combined if they intersect. + /// + /// The set of s to reduce. + /// The reduced set. + public static IReadOnlyCollection Reduce(this IEnumerable textSpans) + { + var spans = new List(textSpans); + var shouldContinue = true; + + while (shouldContinue) + { + shouldContinue = false; + + foreach (var current in spans) + { + // Check if any of the other spans intersects with the current one + var other = spans.FirstOrDefault(s => s != current && s.IntersectsWith(current)); + if (other != default) + { + // Remove the original spans + spans.Remove(current); + spans.Remove(other); + + // Add the newly combined span. + spans.Add(TextSpan.FromBounds(Math.Min(current.Start, other.Start), Math.Max(current.End, other.End))); + + // We changed the list, so we have to restart the foreach. + shouldContinue = true; + break; + } + } + } + + return spans.Distinct().Where(x => !x.IsEmpty).ToList(); + } + + /// + /// Removes all overlaps of two sets of and returns the resulting set. + /// + /// The first set. + /// The second set. + /// All spans and part of spans of that do not overlap with any span in . + public static IReadOnlyCollection RemoveOverlap(this IEnumerable left, IEnumerable right) + { + var spanList = new List(left); + var shouldContinue = true; + + while (shouldContinue) + { + shouldContinue = false; + + foreach (var current in spanList) + { + // Check if any span overlaps the current span. + var other = right.FirstOrDefault(o => o.OverlapsWith(current)); + + if (other != default) + { + // Remove the current span add the new span(s). + spanList.Remove(current); + spanList.AddRange(RemoveOverlap(current, other)); + + // We changed the list, so we have to restart the foreach. + shouldContinue = true; + break; + } + } + } + + return spanList; + + IReadOnlyCollection RemoveOverlap(TextSpan current, TextSpan other) + { + // The the current span is completely contained inside the other, nothing will be left. + if (other.Contains(current)) + return Array.Empty(); + + // Check if there is any overlap. + var overlap = current.Overlap(other); + + if (!overlap.HasValue) + { + return new[] { current }; + } + + return new[] { TextSpan.FromBounds(current.Start, overlap.Value.Start), TextSpan.FromBounds(overlap.Value.End, current.End) }.Where(s => !s.IsEmpty).ToList(); + } + } + } +} diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/ProjectComponentsExtensions.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/ProjectComponentsExtensions.cs index 6b8d7ef9df..d3b9686e9f 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/ProjectComponentsExtensions.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/ProjectComponentsExtensions.cs @@ -1,7 +1,6 @@ -using Microsoft.CodeAnalysis.Text; -using System; using System.Collections.Generic; using System.Linq; +using Stryker.Core.Helpers; namespace Stryker.Core.ProjectComponents { @@ -37,96 +36,5 @@ bool MatchesFilePattern(FilePattern pattern) => pattern.Glob.IsMatch(projectComponent.FullPath) || pattern.Glob.IsMatch(projectComponent.RelativePath); } - - /// - /// Reduces a set of text spans to the smallest set of text spans possible. - /// Two can be combined if they intersect. - /// - /// The set of s to reduce. - /// The reduced set. - public static IReadOnlyCollection Reduce(this IEnumerable textSpans) - { - var spans = new List(textSpans); - var shouldContinue = true; - - while (shouldContinue) - { - shouldContinue = false; - - foreach (var current in spans) - { - // Check if any of the other spans intersects with the current one - var other = spans.FirstOrDefault(s => s != current && s.IntersectsWith(current)); - if (other != default) - { - // Remove the original spans - spans.Remove(current); - spans.Remove(other); - - // Add the newly combined span. - spans.Add(TextSpan.FromBounds(Math.Min(current.Start, other.Start), Math.Max(current.End, other.End))); - - // We changed the list, so we have to restart the foreach. - shouldContinue = true; - break; - } - } - } - - return spans.Distinct().Where(x => !x.IsEmpty).ToList(); - } - - /// - /// Removes all overlaps of two sets of and returns the resulting set. - /// - /// The first set. - /// The second set. - /// All spans and part of spans of that do not overlap with any span in . - public static IReadOnlyCollection RemoveOverlap(this IEnumerable left, IEnumerable right) - { - var spanList = new List(left); - var shouldContinue = true; - - while (shouldContinue) - { - shouldContinue = false; - - foreach (var current in spanList) - { - // Check if any span overlaps the current span. - var other = right.FirstOrDefault(o => o.OverlapsWith(current)); - - if (other != default) - { - // Remove the current span add the new span(s). - spanList.Remove(current); - spanList.AddRange(RemoveOverlap(current, other)); - - // We changed the list, so we have to restart the foreach. - shouldContinue = true; - break; - } - } - } - - return spanList; - - IReadOnlyCollection RemoveOverlap(TextSpan current, TextSpan other) - { - // The the current span is completely contained inside the other, nothing will be left. - if (other.Contains(current)) - return Array.Empty(); - - // Check if there is any overlap. - var overlap = current.Overlap(other); - - if (!overlap.HasValue) - { - return new[] { current }; - } - - return new[] { TextSpan.FromBounds(current.Start, overlap.Value.Start), TextSpan.FromBounds(overlap.Value.End, current.End) }.Where(s => !s.IsEmpty).ToList(); - } - } } }