Skip to content
109 changes: 109 additions & 0 deletions MoreLinq.Test/SplitAtTest.cs
Original file line number Diff line number Diff line change
@@ -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<object>().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>(T input, Func<IEnumerable<int>, (IEnumerable<int>, IEnumerable<int>)> splitter)
where T : IEnumerable<int>, 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>(T input, Func<IEnumerable<int>, (IEnumerable<int>, IEnumerable<int>)> splitter)
where T : IEnumerable<int>, 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>(T input, Func<IEnumerable<int>, (IEnumerable<int>, IEnumerable<int>)> splitter)
where T : IEnumerable<int>, IDisposable
{
using (input)
{
var (part1, part2) = splitter(input);
Assert.That(part1, Is.SameAs(input));
Assert.That(part2, Is.Empty);
}
}
}
}
}
85 changes: 85 additions & 0 deletions MoreLinq.Test/TestingList.cs
Original file line number Diff line number Diff line change
@@ -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<T> Of<T>(params T[] elements) =>
new TestingList<T>(elements);

public static TestingList<T> AsTestingList<T>(this IList<T> source) =>
source != null
? new TestingList<T>(source)
: throw new ArgumentNullException(nameof(source));

}

/// <summary>
/// List that asserts whether its iterator has been disposed
/// when it is disposed itself and also whether GetEnumerator() is
/// called exactly once or not.
/// </summary>

sealed class TestingList<T> : IList<T>, IDisposable
{
readonly IList<T> _list;
bool? _disposed;

public TestingList(IList<T> list) => _list = list;

void IDisposable.Dispose()
{
if (_disposed == null)
return;
Assert.That(_disposed, Is.True, "Expected enumerator to be disposed.");
_disposed = null;
}

public IEnumerator<T> GetEnumerator()
{
Assert.That(_disposed, Is.Null, "LINQ operators should not enumerate a sequence more than once.");
_disposed = false;
var e = new DisposeNotificationEnumerator<T>(_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.");
}
}
43 changes: 22 additions & 21 deletions MoreLinq.Test/TestingSequence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void AssertDisposed()
public IEnumerator<T> 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<T>(_sequence.GetEnumerator());
_disposed = false;
enumerator.Disposed += delegate { _disposed = true; };
enumerator.MoveNextCalled += delegate { MoveNextCallCount++; };
Expand All @@ -75,31 +75,32 @@ public IEnumerator<T> GetEnumerator()

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

sealed class DisposeTestingSequenceEnumerator : IEnumerator<T>
{
readonly IEnumerator<T> _sequence;
}

sealed class DisposeNotificationEnumerator<T> : IEnumerator<T>
{
readonly IEnumerator<T> _sequence;

public event EventHandler Disposed;
public event EventHandler MoveNextCalled;
public event EventHandler Disposed;
public event EventHandler MoveNextCalled;

public DisposeTestingSequenceEnumerator(IEnumerator<T> sequence) =>
_sequence = sequence;
public DisposeNotificationEnumerator(IEnumerator<T> 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);
}
}
}
3 changes: 2 additions & 1 deletion MoreLinq/MoreLinq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
- Slice
- SortedMerge
- Split
- SplitAt
- StartsWith
- Subsets
- TagFirstLast
Expand Down Expand Up @@ -118,7 +119,7 @@
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageId>morelinq</PackageId>
<PackageTags>linq;extensions</PackageTags>
<PackageReleaseNotes>Adds new operators: Flatten. See also https://github.com/morelinq/MoreLINQ/wiki/API-Changes.</PackageReleaseNotes>
<PackageReleaseNotes>Adds new operators: SplitAt. See also https://github.com/morelinq/MoreLINQ/wiki/API-Changes.</PackageReleaseNotes>
<PackageProjectUrl>https://morelinq.github.io/</PackageProjectUrl>
<PackageLicenseUrl>http://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
<GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
Expand Down
84 changes: 84 additions & 0 deletions MoreLinq/SplitAt.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Splits the sequence into sub-sequences at given offsets into the
/// sequence.
/// </summary>
/// <param name="source">The source sequence.</param>
/// <param name="offsets">
/// The zero-based offsets into the source sequence at which at which
/// to split the source sequence.</param>
/// <typeparam name="T">
/// The type of the element in the source sequence.</typeparam>
/// <returns>
/// A sequence of splits.</returns>
/// <remarks>
/// This method uses deferred execution semantics and streams the
/// sub-sequences, where each sub-sequence is buffered.
/// </remarks>

public static IEnumerable<IEnumerable<T>> SplitAt<T>(
this IEnumerable<T> source, params int[] offsets)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (offsets == null) throw new ArgumentNullException(nameof(offsets));

return _(); IEnumerable<IEnumerable<T>> _()
{
using (var oe = offsets.Concat(int.MaxValue).GetEnumerator())
{
oe.MoveNext();
var offset = oe.Current;

List<T> list = null;
foreach (var e in source.Index())
{
retry:
if (e.Key < offset)
{
if (list == null)
list = new List<T>();
list.Add(e.Value);
}
else
{
yield return list ?? Enumerable.Empty<T>();
offset = oe.MoveNext() ? oe.Current : -1;
list = null;
goto retry;
}
}

if (list != null)
yield return list;

while (oe.MoveNext())
yield return Enumerable.Empty<T>();
}
}
}
}
}
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down