diff --git a/MoreLinq.Test/SplitAtTest.cs b/MoreLinq.Test/SplitAtTest.cs new file mode 100644 index 000000000..50b2ade56 --- /dev/null +++ b/MoreLinq.Test/SplitAtTest.cs @@ -0,0 +1,109 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2008 Jonathan Skeet. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq.Test +{ + using System; + using System.Collections.Generic; + using NUnit.Framework; + + [TestFixture] + public class SplitAtTest + { + [Test] + public void SplitAtIsLazy2() + { + new BreakingSequence().SplitAt(); + } + + [TestCase( 0, new int[0] , new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 })] + [TestCase( 2, new[] { 1, 2 }, new[] { 3, 4, 5, 6, 7, 8, 9, 10 })] + [TestCase( 5, new[] { 1, 2, 3, 4, 5 }, new[] { 6, 7, 8, 9, 10 })] + [TestCase(10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, new int[0] )] + [TestCase(20, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, new int[0] )] + [TestCase(-5, new int[0] , new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 })] + public void SplitAt(int index, int[] expected1, int[] expected2) + { + var ns = Enumerable.Range(1, 10).ToArray(); + + AssertParts(ns.AsTestingList() , input => input.SplitAt(index).Fold((xs, ys) => (xs, ys))); + AssertParts(ns.AsTestingSequence(), input => input.SplitAt(index).Fold((xs, ys) => (xs, ys))); + AssertParts(ns.AsTestingList() , input => input.SplitAt(index).Fold((xs, ys) => (xs, ys))); + AssertParts(ns.AsTestingSequence(), input => input.SplitAt(index).Fold((xs, ys) => (xs, ys))); + + void AssertParts(T input, Func, (IEnumerable, IEnumerable)> splitter) + where T : IEnumerable, IDisposable + { + using (input) + { + var (part1, part2) = splitter(input); + part1.AssertSequenceEqual(expected1); + part2.AssertSequenceEqual(expected2); + } + } + } + + [Ignore("TODO")] + [TestCase( 0)] + [TestCase(-1)] + public void SplitAtWithIndexZeroOrLessReturnsSourceAsSecond(int index) + { + Assert.That(index, Is.LessThanOrEqualTo(0)); + + var ns = Enumerable.Range(1, 10).ToArray(); + + AssertParts(ns.AsTestingList(), + input => input.SplitAt(index) + .Fold((xs, ys) => (xs, ys))); + + void AssertParts(T input, Func, (IEnumerable, IEnumerable)> splitter) + where T : IEnumerable, IDisposable + { + using (input) + { + var (part1, part2) = splitter(input); + Assert.That(part1, Is.Empty); + Assert.That(part2, Is.SameAs(input)); + } + } + } + + [Ignore("TODO")] + [TestCase(10)] + [TestCase(11)] + [TestCase(20)] + public void SplitAtWithIndexGreaterOrEqualToSourceLengthReturnsSourceAsFirst(int index) + { + var ns = Enumerable.Range(1, 10).ToArray(); + + AssertParts(ns.AsTestingList(), + input => input.SplitAt(index) + .Fold((xs, ys) => (xs, ys))); + + void AssertParts(T input, Func, (IEnumerable, IEnumerable)> splitter) + where T : IEnumerable, IDisposable + { + using (input) + { + var (part1, part2) = splitter(input); + Assert.That(part1, Is.SameAs(input)); + Assert.That(part2, Is.Empty); + } + } + } + } +} diff --git a/MoreLinq.Test/TestingList.cs b/MoreLinq.Test/TestingList.cs new file mode 100644 index 000000000..7a6d27d04 --- /dev/null +++ b/MoreLinq.Test/TestingList.cs @@ -0,0 +1,85 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2017 Atif Aziz. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq.Test +{ + using System; + using System.Collections; + using System.Collections.Generic; + using NUnit.Framework; + + static class TestingList + { + public static TestingList Of(params T[] elements) => + new TestingList(elements); + + public static TestingList AsTestingList(this IList source) => + source != null + ? new TestingList(source) + : throw new ArgumentNullException(nameof(source)); + + } + + /// + /// List that asserts whether its iterator has been disposed + /// when it is disposed itself and also whether GetEnumerator() is + /// called exactly once or not. + /// + + sealed class TestingList : IList, IDisposable + { + readonly IList _list; + bool? _disposed; + + public TestingList(IList list) => _list = list; + + void IDisposable.Dispose() + { + if (_disposed == null) + return; + Assert.That(_disposed, Is.True, "Expected enumerator to be disposed."); + _disposed = null; + } + + public IEnumerator GetEnumerator() + { + Assert.That(_disposed, Is.Null, "LINQ operators should not enumerate a sequence more than once."); + _disposed = false; + var e = new DisposeNotificationEnumerator(_list.GetEnumerator()); + e.Disposed += delegate { _disposed = true; }; + return e; + } + + public int Count => _list.Count; + public T this[int index] { get => _list[index]; set => _list[index] = value; } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool Contains(T item) => _list.Contains(item); + public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + public bool IsReadOnly => _list.IsReadOnly; + public int IndexOf(T item) => _list.IndexOf(item); + + public void Add(T item) => throw UnexpectedBehaviorError(); + public void Clear() => throw UnexpectedBehaviorError(); + public bool Remove(T item) => throw UnexpectedBehaviorError(); + public void Insert(int index, T item) => throw UnexpectedBehaviorError(); + public void RemoveAt(int index) => throw UnexpectedBehaviorError(); + + static Exception UnexpectedBehaviorError() => new Exception("LINQ operators should not modify the source."); + } +} diff --git a/MoreLinq.Test/TestingSequence.cs b/MoreLinq.Test/TestingSequence.cs index e541176fe..afff19c6c 100644 --- a/MoreLinq.Test/TestingSequence.cs +++ b/MoreLinq.Test/TestingSequence.cs @@ -65,7 +65,7 @@ void AssertDisposed() public IEnumerator GetEnumerator() { Assert.That(_sequence, Is.Not.Null, "LINQ operators should not enumerate a sequence more than once."); - var enumerator = new DisposeTestingSequenceEnumerator(_sequence.GetEnumerator()); + var enumerator = new DisposeNotificationEnumerator(_sequence.GetEnumerator()); _disposed = false; enumerator.Disposed += delegate { _disposed = true; }; enumerator.MoveNextCalled += delegate { MoveNextCallCount++; }; @@ -75,31 +75,32 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - sealed class DisposeTestingSequenceEnumerator : IEnumerator - { - readonly IEnumerator _sequence; + } + + sealed class DisposeNotificationEnumerator : IEnumerator + { + readonly IEnumerator _sequence; - public event EventHandler Disposed; - public event EventHandler MoveNextCalled; + public event EventHandler Disposed; + public event EventHandler MoveNextCalled; - public DisposeTestingSequenceEnumerator(IEnumerator sequence) => - _sequence = sequence; + public DisposeNotificationEnumerator(IEnumerator sequence) => + _sequence = sequence; - public T Current => _sequence.Current; - object IEnumerator.Current => Current; - public void Reset() => _sequence.Reset(); + public T Current => _sequence.Current; + object IEnumerator.Current => Current; + public void Reset() => _sequence.Reset(); - public bool MoveNext() - { - MoveNextCalled?.Invoke(this, EventArgs.Empty); - return _sequence.MoveNext(); - } + public bool MoveNext() + { + MoveNextCalled?.Invoke(this, EventArgs.Empty); + return _sequence.MoveNext(); + } - public void Dispose() - { - _sequence.Dispose(); - Disposed?.Invoke(this, EventArgs.Empty); - } + public void Dispose() + { + _sequence.Dispose(); + Disposed?.Invoke(this, EventArgs.Empty); } } } diff --git a/MoreLinq/MoreLinq.csproj b/MoreLinq/MoreLinq.csproj index 223594d74..25fb68458 100644 --- a/MoreLinq/MoreLinq.csproj +++ b/MoreLinq/MoreLinq.csproj @@ -78,6 +78,7 @@ - Slice - SortedMerge - Split + - SplitAt - StartsWith - Subsets - TagFirstLast @@ -118,7 +119,7 @@ true morelinq linq;extensions - Adds new operators: Flatten. See also https://github.com/morelinq/MoreLINQ/wiki/API-Changes. + Adds new operators: SplitAt. See also https://github.com/morelinq/MoreLINQ/wiki/API-Changes. https://morelinq.github.io/ http://www.apache.org/licenses/LICENSE-2.0 false diff --git a/MoreLinq/SplitAt.cs b/MoreLinq/SplitAt.cs new file mode 100644 index 000000000..661796e1a --- /dev/null +++ b/MoreLinq/SplitAt.cs @@ -0,0 +1,84 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2009 Atif Aziz. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +namespace MoreLinq +{ + using System; + using System.Collections.Generic; + using System.Linq; + + static partial class MoreEnumerable + { + /// + /// Splits the sequence into sub-sequences at given offsets into the + /// sequence. + /// + /// The source sequence. + /// + /// The zero-based offsets into the source sequence at which at which + /// to split the source sequence. + /// + /// The type of the element in the source sequence. + /// + /// A sequence of splits. + /// + /// This method uses deferred execution semantics and streams the + /// sub-sequences, where each sub-sequence is buffered. + /// + + public static IEnumerable> SplitAt( + this IEnumerable source, params int[] offsets) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (offsets == null) throw new ArgumentNullException(nameof(offsets)); + + return _(); IEnumerable> _() + { + using (var oe = offsets.Concat(int.MaxValue).GetEnumerator()) + { + oe.MoveNext(); + var offset = oe.Current; + + List list = null; + foreach (var e in source.Index()) + { + retry: + if (e.Key < offset) + { + if (list == null) + list = new List(); + list.Add(e.Value); + } + else + { + yield return list ?? Enumerable.Empty(); + offset = oe.MoveNext() ? oe.Current : -1; + list = null; + goto retry; + } + } + + if (list != null) + yield return list; + + while (oe.MoveNext()) + yield return Enumerable.Empty(); + } + } + } + } +} diff --git a/README.md b/README.md index 25bfc9cc9..417aee750 100644 --- a/README.md +++ b/README.md @@ -440,6 +440,12 @@ Splits the source sequence by a separator. This method has 12 overloads. +### SplitAt + +Splits the sequence in two at the given index. + +This method has 2 overloads. + ### StartsWith Determines whether the beginning of the first sequence is equivalent to the