From 5bdce685a2ca1422bbe4df80ef9c35133bf23498 Mon Sep 17 00:00:00 2001 From: David Piepgrass Date: Sun, 29 Mar 2020 21:22:04 -0600 Subject: [PATCH] Add `ROLSlice` for slicing `IReadOnlyList` This is an improvement over Slice_ which can't slice BCL lists, though Slice_.TryGet would often perform better for Loyc lists. Includes unit tests for the new slice type and two existing ones. Also: - rename `Throws` to `ThrowsAny` to match convention in other libraries - `AListBase.Slice` should have a default parameter --- Core/AssemblyVersion.cs | 4 +- Core/Loyc.Collections/ALists/AList.cs | 2 +- .../Collections/Adapters/ROLSlice.cs | 143 ++++++++++++++++++ .../Collections/Adapters/Slice.cs | 1 + .../Loyc.Essentials.net45.csproj | 1 + Core/Loyc.Essentials/Utilities/MiniTest.cs | 26 +++- .../Collections/ALists/SparseAListTests.cs | 6 +- Core/Tests/Collections/DictionaryTests.cs | 4 +- Core/Tests/Collections/HeapTests.cs | 4 +- .../Collections/ReadOnlyDictionaryTests.cs | 2 +- Core/Tests/Essentials/SliceTests.cs | 115 ++++++++++++++ Core/Tests/LoycCore.Tests.net45.csproj | 1 + Core/Tests/Program.cs | 3 + Core/Tests/Utilities/GoInterfaceTests.cs | 10 +- appveyor.yml | 2 +- 15 files changed, 299 insertions(+), 25 deletions(-) create mode 100644 Core/Loyc.Essentials/Collections/Adapters/ROLSlice.cs create mode 100644 Core/Tests/Essentials/SliceTests.cs diff --git a/Core/AssemblyVersion.cs b/Core/AssemblyVersion.cs index efcc4df69..0a6f59b1a 100644 --- a/Core/AssemblyVersion.cs +++ b/Core/AssemblyVersion.cs @@ -14,5 +14,5 @@ // command to change the version number which, I guess, produces an incompatible // assembly in the presence of strong names (strong naming prevents two assemblies // from linking together without an exact match.) -[assembly: AssemblyVersion("2.7.1.3")] -[assembly: AssemblyFileVersion("2.7.1.3")] +[assembly: AssemblyVersion("2.7.1.4")] +[assembly: AssemblyFileVersion("2.7.1.4")] diff --git a/Core/Loyc.Collections/ALists/AList.cs b/Core/Loyc.Collections/ALists/AList.cs index faa9832e0..6cabd0802 100644 --- a/Core/Loyc.Collections/ALists/AList.cs +++ b/Core/Loyc.Collections/ALists/AList.cs @@ -805,7 +805,7 @@ public AListBase RemoveSection(int start, int count) return cov_RemoveSection(start, count); } - public new ListSlice Slice(int start, int length) + public new ListSlice Slice(int start, int length = int.MaxValue) { return new ListSlice(this, start, length); } diff --git a/Core/Loyc.Essentials/Collections/Adapters/ROLSlice.cs b/Core/Loyc.Essentials/Collections/Adapters/ROLSlice.cs new file mode 100644 index 000000000..1c601f442 --- /dev/null +++ b/Core/Loyc.Essentials/Collections/Adapters/ROLSlice.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Loyc.Collections; +using Loyc.Math; + +namespace Loyc.Collections +{ + public static partial class ListExt + { + public static ROLSlice Slice(this TList list, int start, int count = int.MaxValue) where TList : IReadOnlyList + => new ROLSlice(list, start, count); + } + + /// Adapter: a random-access range for a slice of an . + /// Item type in the list + /// List type + public struct ROLSlice : IListSource, IRange, ICloneable> where TList : IReadOnlyList + { + TList _list; + int _start, _count; + + /// Initializes a slice. + /// The start index was below zero. + /// The (start, count) range is allowed to be invalid, as long + /// as 'start' and 'count' are zero or above. + ///
    + ///
  • If 'start' is above the original Count, the Count of the new slice + /// is set to zero.
  • + ///
  • if (start + count) is above the original Count, the Count of the new + /// slice is reduced to list.Count - start.
  • + ///
+ ///
+ public ROLSlice(TList list, int start, int count = int.MaxValue) + { + _list = list; + _start = start; + _count = count; + if (start < 0) throw new ArgumentException("The start index was below zero."); + if (count < 0) throw new ArgumentException("The count was below zero."); + if (count > _list.Count - start) + _count = System.Math.Max(_list.Count - start, 0); + } + public ROLSlice(TList list) + { + _list = list; + _start = 0; + _count = list.Count; + } + + public int Count + { + get { return _count; } + } + public bool IsEmpty + { + get { return _count == 0; } + } + public T First + { + get { return this[0]; } + } + public T Last + { + get { return this[_count - 1]; } + } + + public T PopFirst(out bool empty) + { + if (_count != 0) + { + empty = false; + _count--; + return _list[_start++]; + } + empty = true; + return default(T); + } + public T PopLast(out bool empty) + { + if (_count != 0) + { + empty = false; + _count--; + return _list[_start + _count]; + } + empty = true; + return default(T); + } + + ROLSlice ICloneable>.Clone() => this; + IRange ICloneable>.Clone() => this; + IFRange ICloneable>.Clone() => this; + IBRange ICloneable>.Clone() => this; + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + public RangeEnumerator, T> GetEnumerator() + { + return new RangeEnumerator, T>(this); + } + + public T this[int index] + { + get { + if ((uint)index < (uint)_count) + return _list[_start + index]; + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + public T this[int index, T defaultValue] + { + get { + if ((uint)index < (uint)_count) + return _list[_start + index]; + return defaultValue; + } + } + public T TryGet(int index, out bool fail) + { + int i = _start + index; + if (!(fail = (uint)index >= (uint)_count || (uint)i >= (uint)_list.Count)) + return _list[i]; + else + return default(T); + } + + IRange IListSource.Slice(int start, int count) => Slice(start, count); + public ROLSlice Slice(int start, int count = int.MaxValue) + { + if (start < 0) throw new ArgumentException("The start index was below zero."); + if (count < 0) throw new ArgumentException("The count was below zero."); + var slice = new ROLSlice(); + slice._list = this._list; + slice._start = this._start + start; + slice._count = count; + if (slice._count > this._count - start) + slice._count = System.Math.Max(this._count - start, 0); + return slice; + } + } +} diff --git a/Core/Loyc.Essentials/Collections/Adapters/Slice.cs b/Core/Loyc.Essentials/Collections/Adapters/Slice.cs index 6309d94e3..a7cb81ff7 100644 --- a/Core/Loyc.Essentials/Collections/Adapters/Slice.cs +++ b/Core/Loyc.Essentials/Collections/Adapters/Slice.cs @@ -33,6 +33,7 @@ public static IRange AsRange(this IRange list) /// public struct Slice_ : IRange, ICloneable>, IIsEmpty { + [Obsolete("I doubt anyone is using this.")] public static readonly Slice_ Empty = new Slice_(); IListSource _list; diff --git a/Core/Loyc.Essentials/Loyc.Essentials.net45.csproj b/Core/Loyc.Essentials/Loyc.Essentials.net45.csproj index 28a289b0e..b52d4b441 100644 --- a/Core/Loyc.Essentials/Loyc.Essentials.net45.csproj +++ b/Core/Loyc.Essentials/Loyc.Essentials.net45.csproj @@ -164,6 +164,7 @@ Properties\AssemblyVersion.cs + diff --git a/Core/Loyc.Essentials/Utilities/MiniTest.cs b/Core/Loyc.Essentials/Utilities/MiniTest.cs index 4f3bd1b34..8b146e799 100644 --- a/Core/Loyc.Essentials/Utilities/MiniTest.cs +++ b/Core/Loyc.Essentials/Utilities/MiniTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Loyc.Math; using System.Collections; @@ -536,7 +536,7 @@ public static void Expect(bool condition) /// A method to run /// The message that will be displayed on failure /// Arguments to be used in formatting the message - public static Exception Throws(Type expectedExceptionType, Action code, string message, params object[] args) + public static Exception ThrowsAny(Type expectedExceptionType, Action code, string message, params object[] args) { try { code(); @@ -549,18 +549,28 @@ public static Exception Throws(Type expectedExceptionType, Action code, string m return null; // normally unreachable } - public static Exception Throws(Type expectedExceptionType, Action code) + public static Exception ThrowsAny(Type expectedExceptionType, Action code) { - return Throws(expectedExceptionType, code, null, null); + return ThrowsAny(expectedExceptionType, code, null, null); } + [Obsolete("Use ThrowsAny (this method inadvertantly means ThrowsAny anyway)")] public static T Throws(Action code, string message, params object[] args) where T : Exception { - return (T)Throws(typeof(T), code, message, args); + return (T)ThrowsAny(typeof(T), code, message, args); } + [Obsolete("Use ThrowsAny (this method inadvertantly means ThrowsAny anyway)")] public static T Throws(Action code) where T : Exception { - return (T)Throws(typeof(T), code); + return (T)ThrowsAny(typeof(T), code); + } + public static T ThrowsAny(Action code, string message, params object[] args) where T : Exception + { + return (T)ThrowsAny(typeof(T), code, message, args); + } + public static T ThrowsAny(Action code) where T : Exception + { + return (T)ThrowsAny(typeof(T), code); } /// @@ -571,7 +581,7 @@ public static T Throws(Action code) where T : Exception /// Arguments to be used in formatting the message public static Exception Catch(Action code, string message, params object[] args) { - return Throws(typeof(Exception), code, message, args); + return ThrowsAny(typeof(Exception), code, message, args); } /// @@ -581,7 +591,7 @@ public static Exception Catch(Action code, string message, params object[] args) /// A TestDelegate public static Exception Catch(Action code) { - return Throws(typeof(Exception), code); + return ThrowsAny(typeof(Exception), code); } /// diff --git a/Core/Tests/Collections/ALists/SparseAListTests.cs b/Core/Tests/Collections/ALists/SparseAListTests.cs index 71be75b37..720507f28 100644 --- a/Core/Tests/Collections/ALists/SparseAListTests.cs +++ b/Core/Tests/Collections/ALists/SparseAListTests.cs @@ -255,9 +255,9 @@ public void TestClearSpace() Assert.AreEqual(0, list[i1 - 1]); if (_testExceptions) { - Assert.Throws(() => { var _ = list[i1]; }); - Assert.Throws(() => { list.ClearSpace(0, -1); }); - Assert.Throws(() => { list.ClearSpace(-1, 10); }); + Assert.ThrowsAny(() => { var _ = list[i1]; }); + Assert.ThrowsAny(() => { list.ClearSpace(0, -1); }); + Assert.ThrowsAny(() => { list.ClearSpace(-1, 10); }); } list.ClearSpace(i0, i2 - i0); Assert.AreEqual(i2, list.Count); diff --git a/Core/Tests/Collections/DictionaryTests.cs b/Core/Tests/Collections/DictionaryTests.cs index acea04c1b..101f7346c 100644 --- a/Core/Tests/Collections/DictionaryTests.cs +++ b/Core/Tests/Collections/DictionaryTests.cs @@ -66,7 +66,7 @@ public void TestAllTheBasics() Assert.AreEqual(value, null); Assert.AreEqual(1, dict["A"]); Assert.AreEqual(2, dict[1]); - Assert.Throws(typeof(KeyNotFoundException), () => { var _ = dict["C"]; }); + Assert.ThrowsAny(typeof(KeyNotFoundException), () => { var _ = dict["C"]; }); dict.Clear(); ExpectSet(dict); @@ -94,7 +94,7 @@ public void TestAllTheBasics() Assert.IsFalse(Remove(dict, 2.0, 2.0)); Assert.IsTrue(Remove(dict, 2.0, null)); ExpectSet(dict, P("2", null), P(2F, null), P(2UL, "You're a 2ul!")); - Assert.Throws(typeof(ArgumentException), () => dict.Add("2", 2)); + Assert.ThrowsAny(typeof(ArgumentException), () => dict.Add("2", 2)); Assert.IsNull(dict["2"]); dict["2"] = 2; Assert.AreEqual(2, dict["2"]); diff --git a/Core/Tests/Collections/HeapTests.cs b/Core/Tests/Collections/HeapTests.cs index db3e1307c..26bf0e3f4 100644 --- a/Core/Tests/Collections/HeapTests.cs +++ b/Core/Tests/Collections/HeapTests.cs @@ -23,7 +23,7 @@ public void TestPopMax() Assert.AreEqual(7, heap.Pop()); Assert.AreEqual(6, heap.Pop()); Assert.AreEqual(1, heap.Pop()); - Assert.Throws(() => heap.Pop()); + Assert.ThrowsAny(() => heap.Pop()); } [Test] @@ -37,7 +37,7 @@ public void TestPopMin() Assert.AreEqual(6, heap.Pop()); Assert.AreEqual(9, heap.Pop()); Assert.AreEqual(11, heap.Pop()); - Assert.Throws(() => heap.Pop()); + Assert.ThrowsAny(() => heap.Pop()); } [Test] diff --git a/Core/Tests/Collections/ReadOnlyDictionaryTests.cs b/Core/Tests/Collections/ReadOnlyDictionaryTests.cs index 1328d5f5a..a983ccfaa 100644 --- a/Core/Tests/Collections/ReadOnlyDictionaryTests.cs +++ b/Core/Tests/Collections/ReadOnlyDictionaryTests.cs @@ -39,7 +39,7 @@ public void GeneralTests() AreEqual(_expect[k], _dict[k]); } else { AreEqual(default(V), v); - Throws(() => { var _ = _dict[k]; }); + ThrowsAny(() => { var _ = _dict[k]; }); } } } diff --git a/Core/Tests/Essentials/SliceTests.cs b/Core/Tests/Essentials/SliceTests.cs new file mode 100644 index 000000000..d4023b52d --- /dev/null +++ b/Core/Tests/Essentials/SliceTests.cs @@ -0,0 +1,115 @@ +using Loyc.Collections; +using Loyc.Collections.Impl; +using Loyc.MiniTest; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace Loyc.Collections.Tests +{ + public class SliceTests : TestHelpers where SliceT : IListSource, ICloneable + { + protected Func _new; + + public SliceTests(Func factory) + { + _new = factory; + } + + [Test] + public void TestConstructor() + { + Assert.ThrowsAny(() => _new(new string[] { "A", "B" }, -1, 2)); + Assert.ThrowsAny(() => _new(new string[] { "A", "B" }, 1, -1)); + } + + [Test] + public void TestEmpty() + { + // Get an empty slice in different ways + var slices = new SliceT[] { + _new(new string[0], 0, 1), + _new(new string[] { "A", "B" }, 1, 0), + _new(new string[] { "A", "B" }, int.MaxValue - 1, 1), + _new(new string[] { "A", "B" }, int.MaxValue - 1, int.MaxValue), + }; + foreach (var slice in slices) + { + StandardTest(slice, new string[0]); + Assert.ThrowsAny(() => { var _ = slice[0]; }); + Assert.ThrowsAny(() => { var _ = slice[-1]; }); + } + } + + [Test] + public void TestFullList() + { + string[] list; + list = new string[] { "A" }; + StandardTest(_new(list, 0, 1), list); + list = new string[] { "A", "B", "C", "D" }; + StandardTest(_new(list, 0, 4), list); + } + + [Test] + public void TestPartialList() + { + string[] list; + list = new string[] { "A", "B", "C", "D" }; + int M = int.MaxValue; + StandardTest(_new(list, 1, 4), new string[] { "B", "C", "D" }); + StandardTest(_new(list, 1, M), new string[] { "B", "C", "D" }); + StandardTest(_new(list, 0, 3), new string[] { "A", "B", "C" }); + StandardTest(_new(list, 1, 2), new string[] { "B", "C" }); + } + + // Requires a non-empty list + void StandardTest(SliceT slice, string[] items) + { + Assert.AreEqual(items.Length, slice.Count); + ExpectList(slice, items); + ExpectListByEnumerator(slice, items); + + Assert.AreEqual(null, slice.TryGet(-1, out bool fail)); + Assert.IsTrue(fail); + Assert.AreEqual(null, slice.TryGet(items.Length, out fail)); + Assert.IsTrue(fail); + Assert.AreEqual(null, slice.TryGet(int.MinValue, out fail)); + Assert.IsTrue(fail); + Assert.AreEqual(null, slice.TryGet(int.MaxValue, out fail)); + Assert.IsTrue(fail); + + for (int i = 0; i < items.Length; i++) + { + Assert.AreEqual(items[i], slice.TryGet(i, out fail)); + Assert.IsFalse(fail); + } + } + } + + // TODO: Tests in which the list length decreases after slice creation + + [TestFixture] + public class ListSourceSliceTests : SliceTests> + { + public ListSourceSliceTests() : base((list, start, count) => + new Slice_(new InternalList(list), start, count)) { } + } + + [TestFixture] + public class ROLSliceTests : SliceTests, string>> + { + public ROLSliceTests() : base((list, start, count) => + new ROLSlice, string>(new InternalList(list, list.Length), start, count)) { } + } + + [TestFixture] + public class ListSliceTests : SliceTests> + { + // TODO: test mutable access + public ListSliceTests() : base((list, start, count) => + new ListSlice(new InternalList(list), start, count)) { } + } +} diff --git a/Core/Tests/LoycCore.Tests.net45.csproj b/Core/Tests/LoycCore.Tests.net45.csproj index af3751e39..af348a16b 100644 --- a/Core/Tests/LoycCore.Tests.net45.csproj +++ b/Core/Tests/LoycCore.Tests.net45.csproj @@ -185,6 +185,7 @@ + diff --git a/Core/Tests/Program.cs b/Core/Tests/Program.cs index 9cfdee6e1..acf6b45c5 100644 --- a/Core/Tests/Program.cs +++ b/Core/Tests/Program.cs @@ -116,6 +116,9 @@ public static int Loyc_Essentials() public static int Loyc_Collections() { return MiniTest.RunTests.RunMany( + new ListSourceSliceTests(), + new ListSliceTests(), + new ROLSliceTests(), new HeapTests(_seed), new SimpleCacheTests(), new InvertibleSetTests(), diff --git a/Core/Tests/Utilities/GoInterfaceTests.cs b/Core/Tests/Utilities/GoInterfaceTests.cs index 35a1e2cb7..bbc706e27 100644 --- a/Core/Tests/Utilities/GoInterfaceTests.cs +++ b/Core/Tests/Utilities/GoInterfaceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using System.IO; @@ -597,10 +597,10 @@ public void TestAmbiguity() wrapped = GoInterface.ForceFrom(new Ambig()); int a = 0; - Assert.Throws(delegate() { wrapped.Strings("1", "2"); }); - Assert.Throws(delegate() { wrapped.RefMismatch(1, 2); }); - Assert.Throws(delegate() { wrapped.RefMismatch2(ref a, 2); }); - Assert.Throws(delegate() { wrapped.AmbigLarger(1); }); + Assert.ThrowsAny(delegate() { wrapped.Strings("1", "2"); }); + Assert.ThrowsAny(delegate() { wrapped.RefMismatch(1, 2); }); + Assert.ThrowsAny(delegate() { wrapped.RefMismatch2(ref a, 2); }); + Assert.ThrowsAny(delegate() { wrapped.AmbigLarger(1); }); } private void AssertThrows(Action @delegate) where Type:Exception diff --git a/appveyor.yml b/appveyor.yml index dd3f04583..a2385531b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,7 @@ before_build: build_script: # First, set some environment variables. # SEMVER is set manually. Not sure how this can be automated. - - set SEMVER=27.1.3 + - set SEMVER=27.1.4 - echo %APPVEYOR_REPO_TAG% # Build packages as SEMVER-ci{build} - ps: if ($env:APPVEYOR_REPO_TAG -eq $True) { $env:PKG_VERSION = $env:SEMVER; } else { $env:PKG_VERSION = "$($env:SEMVER)-ci$($env:APPVEYOR_BUILD_NUMBER)"; }