Skip to content

Commit e8262f6

Browse files
Copilotpetesramek
andauthored
feat: change Encode method parameter from ReadOnlySpan<TValue> to IEnumerable<TValue>
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/1e44410f-3278-4281-b17f-cdb7c9fd2078 Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent 1b87336 commit e8262f6

8 files changed

Lines changed: 75 additions & 50 deletions

File tree

benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncoderBenchmark.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace PolylineAlgorithm.Benchmarks;
44
using BenchmarkDotNet.Engines;
55
using PolylineAlgorithm;
66
using PolylineAlgorithm.Utility;
7+
using System.Collections.Generic;
78

89
/// <summary>
910
/// Benchmarks for <see cref="PolylineEncoder{TValue, TPolyline}"/>.
@@ -24,9 +25,9 @@ public class PolylineEncoderBenchmark {
2425
public (double Latitude, double Longitude)[] Array { get; private set; }
2526

2627
/// <summary>
27-
/// Coordinates as read-only memory.
28+
/// Coordinates as list.
2829
/// </summary>
29-
public ReadOnlyMemory<(double Latitude, double Longitude)> Memory { get; private set; }
30+
public List<(double Latitude, double Longitude)> List { get; private set; }
3031

3132
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
3233

@@ -53,15 +54,15 @@ public class PolylineEncoderBenchmark {
5354
[GlobalSetup]
5455
public void SetupData() {
5556
Array = [.. RandomValueProvider.GetCoordinates(CoordinatesCount)];
56-
Memory = Array.AsMemory();
57+
List = [.. RandomValueProvider.GetCoordinates(CoordinatesCount)];
5758
}
5859

5960
/// <summary>
60-
/// Benchmark: encode coordinates from span.
61+
/// Benchmark: encode coordinates from list.
6162
/// </summary>
6263
[Benchmark]
63-
public void PolylineEncoder_Encode_Span() {
64-
var polyline = _encoder.Encode(Memory.Span);
64+
public void PolylineEncoder_Encode_List() {
65+
var polyline = _encoder.Encode(List);
6566
_consumer.Consume(polyline);
6667
}
6768

src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
//
55

66
namespace PolylineAlgorithm.Abstraction;
7+
8+
using System.Collections.Generic;
9+
using System.Threading;
10+
711
/// <summary>
812
/// Contract for encoding a sequence of values into an encoded polyline representation.
913
/// Implementations interpret the generic <typeparamref name="TValue"/> type and produce an encoded
@@ -30,18 +34,17 @@ namespace PolylineAlgorithm.Abstraction;
3034
/// - Value precision and rounding rules (for example 1e-5 for 5-decimal precision).
3135
/// - Value ordering and whether altitude or additional dimensions are supported.
3236
/// - Thread-safety guarantees: whether instances are safe to reuse concurrently or must be instantiated per-call.
33-
/// - Implementations are encouraged to be memory-efficient; the API accepts a <see cref="ReadOnlySpan{T}"/>
34-
/// to avoid forced allocations when callers already have contiguous memory.
3537
/// </remarks>
3638
public interface IPolylineEncoder<TValue, TPolyline> {
3739
/// <summary>
3840
/// Encodes a sequence of values into an encoded polyline representation.
3941
/// The order of values in <paramref name="coordinates"/> is preserved in the encoded result.
4042
/// </summary>
4143
/// <param name="coordinates">
42-
/// The collection of <typeparamref name="TValue"/> instances to encode into a polyline.
43-
/// The span may be empty; implementations should return an appropriate empty encoded representation
44+
/// The sequence of <typeparamref name="TValue"/> instances to encode into a polyline.
45+
/// The sequence may be empty; implementations should return an appropriate empty encoded representation
4446
/// (for example an empty string or an empty memory slice) rather than <see langword="null"/>.
47+
/// Must not be <see langword="null"/>.
4548
/// </param>
4649
/// <param name="cancellationToken">
4750
/// A <see cref="System.Threading.CancellationToken"/> that can be used to cancel the encoding operation.
@@ -70,8 +73,11 @@ public interface IPolylineEncoder<TValue, TPolyline> {
7073
/// - For large input sequences, implementations may provide streaming or incremental encoders; those
7174
/// variants can still implement this interface by materializing the final encoded result.
7275
/// </remarks>
76+
/// <exception cref="System.ArgumentNullException">
77+
/// Thrown if <paramref name="coordinates"/> is <see langword="null"/>.
78+
/// </exception>
7379
/// <exception cref="System.OperationCanceledException">
7480
/// Thrown if the operation is canceled via <paramref name="cancellationToken"/>.
7581
/// </exception>
76-
TPolyline Encode(ReadOnlySpan<TValue> coordinates, PolylineEncodingOptions<TValue>? options = null, CancellationToken cancellationToken = default);
82+
TPolyline Encode(IEnumerable<TValue> coordinates, PolylineEncodingOptions<TValue>? options = null, CancellationToken cancellationToken = default);
7783
}

src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ namespace PolylineAlgorithm.Extensions;
77

88
using PolylineAlgorithm.Abstraction;
99
using PolylineAlgorithm.Internal.Diagnostics;
10-
using System;
10+
using System.Collections.Generic;
11+
using System.Threading;
1112

1213
/// <summary>
1314
/// Provides extension methods for the <see cref="IPolylineEncoder{TValue, TPolyline}"/> interface to facilitate encoding values into polylines.
@@ -27,7 +28,7 @@ public static class PolylineEncoderExtensions {
2728
/// <returns>
2829
/// A <typeparamref name="TPolyline"/> instance representing the encoded polyline for the provided values.
2930
/// </returns>
30-
/// <exception cref="ArgumentNullException">
31+
/// <exception cref="System.ArgumentNullException">
3132
/// Thrown when <paramref name="encoder"/> or <paramref name="values"/> is <see langword="null"/>.
3233
/// </exception>
3334
public static TPolyline Encode<TValue, TPolyline>(
@@ -43,6 +44,6 @@ public static TPolyline Encode<TValue, TPolyline>(
4344
ExceptionGuard.ThrowArgumentNull(nameof(values));
4445
}
4546

46-
return encoder.Encode(values.AsSpan(), options, cancellationToken);
47+
return encoder.Encode(values, options, cancellationToken);
4748
}
4849
}

src/PolylineAlgorithm/PolylineEncoder.cs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace PolylineAlgorithm;
1111
using PolylineAlgorithm.Internal.Diagnostics;
1212
using System;
1313
using System.Buffers;
14+
using System.Collections.Generic;
1415
using System.Diagnostics;
1516
using System.Runtime.CompilerServices;
1617
using System.Threading;
@@ -60,6 +61,9 @@ public PolylineEncoder(PolylineOptions<TValue, TPolyline> options) {
6061
/// <returns>
6162
/// An instance of <typeparamref name="TPolyline"/> representing the encoded values.
6263
/// </returns>
64+
/// <exception cref="ArgumentNullException">
65+
/// Thrown when <paramref name="coordinates"/> is <see langword="null"/>.
66+
/// </exception>
6367
/// <exception cref="ArgumentException">
6468
/// Thrown when <paramref name="coordinates"/> is empty.
6569
/// </exception>
@@ -69,21 +73,25 @@ public PolylineEncoder(PolylineOptions<TValue, TPolyline> options) {
6973
/// <exception cref="OperationCanceledException">
7074
/// Thrown when <paramref name="cancellationToken"/> is canceled.
7175
/// </exception>
72-
public TPolyline Encode(ReadOnlySpan<TValue> coordinates, CancellationToken cancellationToken = default) {
76+
public TPolyline Encode(IEnumerable<TValue> coordinates, CancellationToken cancellationToken = default) {
7377
const string OperationName = nameof(Encode);
7478

7579
_logger.LogOperationStartedDebug(OperationName);
7680

77-
Debug.Assert(coordinates.Length >= 0, "Count must be non-negative.");
81+
if (coordinates is null) {
82+
ExceptionGuard.ThrowArgumentNull(nameof(coordinates));
83+
}
84+
85+
IReadOnlyList<TValue> items = coordinates as IReadOnlyList<TValue> ?? [.. coordinates];
7886

79-
if (coordinates.Length < 1) {
87+
if (items.Count < 1) {
8088
_logger.LogOperationFailedDebug(OperationName);
8189
_logger.LogEmptyArgumentWarning(nameof(coordinates));
8290
ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates));
8391
}
8492

8593
int width = _formatter.Width;
86-
int length = GetMaxBufferLength(coordinates.Length, width);
94+
int length = GetMaxBufferLength(items.Count, width);
8795

8896
char[]? temp = length <= _options.StackAllocLimit
8997
? null
@@ -98,10 +106,10 @@ public TPolyline Encode(ReadOnlySpan<TValue> coordinates, CancellationToken canc
98106
SeedPrevious(previous, null);
99107

100108
try {
101-
for (int i = 0; i < coordinates.Length; i++) {
109+
for (int i = 0; i < items.Count; i++) {
102110
cancellationToken.ThrowIfCancellationRequested();
103111

104-
_formatter.GetValues(coordinates[i], values.AsSpan());
112+
_formatter.GetValues(items[i], values.AsSpan());
105113

106114
for (int j = 0; j < width; j++) {
107115
long current = values[j];
@@ -140,12 +148,15 @@ public TPolyline Encode(ReadOnlySpan<TValue> coordinates, CancellationToken canc
140148
/// Per-call options that control the starting delta baseline. Pass <see langword="null"/> or an
141149
/// instance with <see cref="PolylineEncodingOptions{TValue}.Previous"/> set to
142150
/// <see langword="null"/> to use the formatter's default baseline (same as calling
143-
/// <see cref="Encode(ReadOnlySpan{TValue}, CancellationToken)"/>).
151+
/// <see cref="Encode(IEnumerable{TValue}, CancellationToken)"/>).
144152
/// </param>
145153
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
146154
/// <returns>
147155
/// An instance of <typeparamref name="TPolyline"/> representing the encoded values.
148156
/// </returns>
157+
/// <exception cref="ArgumentNullException">
158+
/// Thrown when <paramref name="coordinates"/> is <see langword="null"/>.
159+
/// </exception>
149160
/// <exception cref="ArgumentException">
150161
/// Thrown when <paramref name="coordinates"/> is empty.
151162
/// </exception>
@@ -155,21 +166,25 @@ public TPolyline Encode(ReadOnlySpan<TValue> coordinates, CancellationToken canc
155166
/// <exception cref="OperationCanceledException">
156167
/// Thrown when <paramref name="cancellationToken"/> is canceled.
157168
/// </exception>
158-
public TPolyline Encode(ReadOnlySpan<TValue> coordinates, PolylineEncodingOptions<TValue>? options, CancellationToken cancellationToken) {
169+
public TPolyline Encode(IEnumerable<TValue> coordinates, PolylineEncodingOptions<TValue>? options, CancellationToken cancellationToken) {
159170
const string OperationName = nameof(Encode);
160171

161172
_logger.LogOperationStartedDebug(OperationName);
162173

163-
Debug.Assert(coordinates.Length >= 0, "Count must be non-negative.");
174+
if (coordinates is null) {
175+
ExceptionGuard.ThrowArgumentNull(nameof(coordinates));
176+
}
177+
178+
IReadOnlyList<TValue> items = coordinates as IReadOnlyList<TValue> ?? [.. coordinates];
164179

165-
if (coordinates.Length < 1) {
180+
if (items.Count < 1) {
166181
_logger.LogOperationFailedDebug(OperationName);
167182
_logger.LogEmptyArgumentWarning(nameof(coordinates));
168183
ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates));
169184
}
170185

171186
int width = _formatter.Width;
172-
int length = GetMaxBufferLength(coordinates.Length, width);
187+
int length = GetMaxBufferLength(items.Count, width);
173188

174189
char[]? temp = length <= _options.StackAllocLimit
175190
? null
@@ -184,10 +199,10 @@ public TPolyline Encode(ReadOnlySpan<TValue> coordinates, PolylineEncodingOption
184199
SeedPrevious(previous, options);
185200

186201
try {
187-
for (int i = 0; i < coordinates.Length; i++) {
202+
for (int i = 0; i < items.Count; i++) {
188203
cancellationToken.ThrowIfCancellationRequested();
189204

190-
_formatter.GetValues(coordinates[i], values.AsSpan());
205+
_formatter.GetValues(items[i], values.AsSpan());
191206

192207
for (int j = 0; j < width; j++) {
193208
long current = values[j];
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#nullable enable
2+
PolylineAlgorithm.PolylineEncoder<TValue, TPolyline>.Encode(System.Collections.Generic.IEnumerable<TValue>! coordinates, PolylineAlgorithm.PolylineEncodingOptions<TValue>? options, System.Threading.CancellationToken cancellationToken) -> TPolyline
3+
PolylineAlgorithm.PolylineEncoder<TValue, TPolyline>.Encode(System.Collections.Generic.IEnumerable<TValue>! coordinates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> TPolyline

tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineDecoderTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ public void Decode_With_Previous_Seeds_Accumulated_State() {
223223
(double Lat, double Lon)[] chunkB = [(43.252, -126.453)];
224224

225225
string polylineB = encoder.Encode(
226-
chunkB.AsSpan(),
226+
chunkB,
227227
new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]),
228228
CancellationToken.None);
229229

@@ -269,9 +269,9 @@ public void Decode_RoundTrip_Reproduces_Full_Sequence() {
269269
(double Lat, double Lon)[] chunkB = all[2..];
270270

271271
// Encode chunked
272-
string polylineA = encoder.Encode(chunkA.AsSpan());
272+
string polylineA = encoder.Encode(chunkA);
273273
string polylineB = encoder.Encode(
274-
chunkB.AsSpan(),
274+
chunkB,
275275
new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]),
276276
CancellationToken.None);
277277

tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineEncoderTests.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,15 @@ public void Constructor_With_Valid_Options_Creates_Instance() {
6060
// Encode — argument validation
6161
// ------------------------------------------------------------------
6262

63-
/// <summary>Tests that encoding an empty span throws <see cref="ArgumentException"/>.</summary>
63+
/// <summary>Tests that encoding an empty sequence throws <see cref="ArgumentException"/>.</summary>
6464
[TestMethod]
6565
public void Encode_With_Empty_Span_Throws_ArgumentException() {
6666
// Arrange
6767
PolylineEncoder<(double Lat, double Lon), string> encoder = CreateEncoder();
6868

6969
// Act & Assert
7070
Assert.ThrowsExactly<ArgumentException>(
71-
() => encoder.Encode(ReadOnlySpan<(double, double)>.Empty));
71+
() => encoder.Encode(Array.Empty<(double, double)>()));
7272
}
7373

7474
// ------------------------------------------------------------------
@@ -83,7 +83,7 @@ public void Encode_With_Single_Coordinate_Returns_Non_Empty_String() {
8383
(double, double)[] coordinates = [(0.0, 0.0)];
8484

8585
// Act
86-
string result = encoder.Encode(coordinates.AsSpan());
86+
string result = encoder.Encode(coordinates);
8787

8888
// Assert
8989
Assert.IsNotNull(result);
@@ -99,7 +99,7 @@ public void Encode_With_Known_Coordinates_Returns_Expected_Polyline() {
9999
string expected = StaticValueProvider.Valid.GetPolyline();
100100

101101
// Act
102-
string result = encoder.Encode(coordinates.AsSpan());
102+
string result = encoder.Encode(coordinates);
103103

104104
// Assert
105105
Assert.AreEqual(expected, result);
@@ -120,7 +120,7 @@ public void Encode_With_Pre_Cancelled_Token_Throws_OperationCanceledException()
120120

121121
// Act & Assert
122122
Assert.ThrowsExactly<OperationCanceledException>(
123-
() => encoder.Encode(coordinates.AsSpan(), cts.Token));
123+
() => encoder.Encode(coordinates, cts.Token));
124124
}
125125

126126
// ------------------------------------------------------------------
@@ -139,7 +139,7 @@ public void Encode_With_Small_Stack_Alloc_Limit_Uses_Heap_And_Produces_Correct_R
139139
string expected = StaticValueProvider.Valid.GetPolyline();
140140

141141
// Act
142-
string result = encoder.Encode(coordinates.AsSpan());
142+
string result = encoder.Encode(coordinates);
143143

144144
// Assert
145145
Assert.AreEqual(expected, result);
@@ -178,8 +178,8 @@ public void Encode_With_Baseline_Produces_Different_First_Delta() {
178178
(double, double)[] coordinates = [(1.0, 2.0)];
179179

180180
// Act
181-
string resultWith = encoderWithBaseline.Encode(coordinates.AsSpan());
182-
string resultWithout = encoderNoBaseline.Encode(coordinates.AsSpan());
181+
string resultWith = encoderWithBaseline.Encode(coordinates);
182+
string resultWithout = encoderNoBaseline.Encode(coordinates);
183183

184184
// Assert — the two encodings must differ because the first lat delta changes
185185
Assert.AreNotEqual(resultWithout, resultWith);
@@ -211,7 +211,7 @@ public void Encode_RoundTrip_Produces_Original_Coordinates() {
211211
(double Lat, double Lon)[] original = [.. StaticValueProvider.Valid.GetCoordinates()];
212212

213213
// Act
214-
string encoded = encoder.Encode(original.AsSpan());
214+
string encoded = encoder.Encode(original);
215215
(double Lat, double Lon)[] decoded = [.. decoder.Decode(encoded)];
216216

217217
// Assert
@@ -237,8 +237,8 @@ public void Encode_Chunked_With_Null_Options_Produces_Same_Result_As_Standard()
237237
(double, double)[] coordinates = [(38.5, -120.2), (40.7, -120.95), (43.252, -126.453)];
238238

239239
// Act
240-
string standard = encoder.Encode(coordinates.AsSpan());
241-
string chunked = encoder.Encode(coordinates.AsSpan(), null, CancellationToken.None);
240+
string standard = encoder.Encode(coordinates);
241+
string chunked = encoder.Encode(coordinates, null, CancellationToken.None);
242242

243243
// Assert
244244
Assert.AreEqual(standard, chunked);
@@ -256,9 +256,9 @@ public void Encode_Chunked_With_Previous_Seeds_Delta_Baseline() {
256256
(double Lat, double Lon)[] chunkB = [(43.252, -126.453)];
257257

258258
// Act — encode chunk B starting from zero (standard) and from chunkA's last point
259-
string standardB = encoder.Encode(chunkB.AsSpan());
259+
string standardB = encoder.Encode(chunkB);
260260
string chunkedB = encoder.Encode(
261-
chunkB.AsSpan(),
261+
chunkB,
262262
new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]),
263263
CancellationToken.None);
264264

@@ -296,14 +296,14 @@ public void Encode_Chunked_Concatenated_Decodes_To_Full_Sequence() {
296296
(double Lat, double Lon)[] chunkB = all[2..];
297297

298298
// Act
299-
string polylineA = encoder.Encode(chunkA.AsSpan());
299+
string polylineA = encoder.Encode(chunkA);
300300
string polylineB = encoder.Encode(
301-
chunkB.AsSpan(),
301+
chunkB,
302302
new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]),
303303
CancellationToken.None);
304304

305305
string concatenated = polylineA + polylineB;
306-
string fullEncoding = encoder.Encode(all.AsSpan());
306+
string fullEncoding = encoder.Encode(all);
307307

308308
(double Lat, double Lon)[] decodedConcatenated = [.. decoder.Decode(concatenated)];
309309
(double Lat, double Lon)[] decodedFull = [.. decoder.Decode(fullEncoding)];
@@ -332,7 +332,7 @@ public void Encode_Chunked_With_Empty_Span_Throws_ArgumentException() {
332332
// Act & Assert
333333
Assert.ThrowsExactly<ArgumentException>(
334334
() => encoder.Encode(
335-
ReadOnlySpan<(double, double)>.Empty,
335+
Array.Empty<(double, double)>(),
336336
new PolylineEncodingOptions<(double Lat, double Lon)>(),
337337
CancellationToken.None));
338338
}
@@ -351,6 +351,6 @@ public void Encode_Chunked_With_Pre_Cancelled_Token_Throws_OperationCanceledExce
351351

352352
// Act & Assert
353353
Assert.ThrowsExactly<OperationCanceledException>(
354-
() => encoder.Encode(coordinates.AsSpan(), null, cts.Token));
354+
() => encoder.Encode(coordinates, null, cts.Token));
355355
}
356356
}

0 commit comments

Comments
 (0)