From e8262f681b688f0d5a2990277c2dbac06e915b6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:15:25 +0000 Subject: [PATCH 1/3] feat: change Encode method parameter from ReadOnlySpan to IEnumerable 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> --- .../PolylineEncoderBenchmark.cs | 13 +++--- .../Abstraction/IPolylineEncoder.cs | 16 +++++--- .../Extensions/PolylineEncoderExtensions.cs | 7 ++-- src/PolylineAlgorithm/PolylineEncoder.cs | 41 +++++++++++++------ src/PolylineAlgorithm/PublicAPI.Unshipped.txt | 3 ++ .../AbstractPolylineDecoderTests.cs | 6 +-- .../AbstractPolylineEncoderTests.cs | 36 ++++++++-------- .../PolylineEncoderExtensionsTests.cs | 3 +- 8 files changed, 75 insertions(+), 50 deletions(-) diff --git a/benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncoderBenchmark.cs b/benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncoderBenchmark.cs index 2e8d09f1..f78b0e9e 100644 --- a/benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncoderBenchmark.cs +++ b/benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncoderBenchmark.cs @@ -4,6 +4,7 @@ namespace PolylineAlgorithm.Benchmarks; using BenchmarkDotNet.Engines; using PolylineAlgorithm; using PolylineAlgorithm.Utility; +using System.Collections.Generic; /// /// Benchmarks for . @@ -24,9 +25,9 @@ public class PolylineEncoderBenchmark { public (double Latitude, double Longitude)[] Array { get; private set; } /// - /// Coordinates as read-only memory. + /// Coordinates as list. /// - public ReadOnlyMemory<(double Latitude, double Longitude)> Memory { get; private set; } + public List<(double Latitude, double Longitude)> List { get; private set; } #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. @@ -53,15 +54,15 @@ public class PolylineEncoderBenchmark { [GlobalSetup] public void SetupData() { Array = [.. RandomValueProvider.GetCoordinates(CoordinatesCount)]; - Memory = Array.AsMemory(); + List = [.. RandomValueProvider.GetCoordinates(CoordinatesCount)]; } /// - /// Benchmark: encode coordinates from span. + /// Benchmark: encode coordinates from list. /// [Benchmark] - public void PolylineEncoder_Encode_Span() { - var polyline = _encoder.Encode(Memory.Span); + public void PolylineEncoder_Encode_List() { + var polyline = _encoder.Encode(List); _consumer.Consume(polyline); } diff --git a/src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs b/src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs index bc16b5ab..75d613c4 100644 --- a/src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs +++ b/src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs @@ -4,6 +4,10 @@ // namespace PolylineAlgorithm.Abstraction; + +using System.Collections.Generic; +using System.Threading; + /// /// Contract for encoding a sequence of values into an encoded polyline representation. /// Implementations interpret the generic type and produce an encoded @@ -30,8 +34,6 @@ namespace PolylineAlgorithm.Abstraction; /// - Value precision and rounding rules (for example 1e-5 for 5-decimal precision). /// - Value ordering and whether altitude or additional dimensions are supported. /// - Thread-safety guarantees: whether instances are safe to reuse concurrently or must be instantiated per-call. -/// - Implementations are encouraged to be memory-efficient; the API accepts a -/// to avoid forced allocations when callers already have contiguous memory. /// public interface IPolylineEncoder { /// @@ -39,9 +41,10 @@ public interface IPolylineEncoder { /// The order of values in is preserved in the encoded result. /// /// - /// The collection of instances to encode into a polyline. - /// The span may be empty; implementations should return an appropriate empty encoded representation + /// The sequence of instances to encode into a polyline. + /// The sequence may be empty; implementations should return an appropriate empty encoded representation /// (for example an empty string or an empty memory slice) rather than . + /// Must not be . /// /// /// A that can be used to cancel the encoding operation. @@ -70,8 +73,11 @@ public interface IPolylineEncoder { /// - For large input sequences, implementations may provide streaming or incremental encoders; those /// variants can still implement this interface by materializing the final encoded result. /// + /// + /// Thrown if is . + /// /// /// Thrown if the operation is canceled via . /// - TPolyline Encode(ReadOnlySpan coordinates, PolylineEncodingOptions? options = null, CancellationToken cancellationToken = default); + TPolyline Encode(IEnumerable coordinates, PolylineEncodingOptions? options = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs b/src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs index b136c146..5694bfcd 100644 --- a/src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs +++ b/src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs @@ -7,7 +7,8 @@ namespace PolylineAlgorithm.Extensions; using PolylineAlgorithm.Abstraction; using PolylineAlgorithm.Internal.Diagnostics; -using System; +using System.Collections.Generic; +using System.Threading; /// /// Provides extension methods for the interface to facilitate encoding values into polylines. @@ -27,7 +28,7 @@ public static class PolylineEncoderExtensions { /// /// A instance representing the encoded polyline for the provided values. /// - /// + /// /// Thrown when or is . /// public static TPolyline Encode( @@ -43,6 +44,6 @@ public static TPolyline Encode( ExceptionGuard.ThrowArgumentNull(nameof(values)); } - return encoder.Encode(values.AsSpan(), options, cancellationToken); + return encoder.Encode(values, options, cancellationToken); } } diff --git a/src/PolylineAlgorithm/PolylineEncoder.cs b/src/PolylineAlgorithm/PolylineEncoder.cs index 703575c0..8ade0e9a 100644 --- a/src/PolylineAlgorithm/PolylineEncoder.cs +++ b/src/PolylineAlgorithm/PolylineEncoder.cs @@ -11,6 +11,7 @@ namespace PolylineAlgorithm; using PolylineAlgorithm.Internal.Diagnostics; using System; using System.Buffers; +using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; @@ -60,6 +61,9 @@ public PolylineEncoder(PolylineOptions options) { /// /// An instance of representing the encoded values. /// + /// + /// Thrown when is . + /// /// /// Thrown when is empty. /// @@ -69,21 +73,25 @@ public PolylineEncoder(PolylineOptions options) { /// /// Thrown when is canceled. /// - public TPolyline Encode(ReadOnlySpan coordinates, CancellationToken cancellationToken = default) { + public TPolyline Encode(IEnumerable coordinates, CancellationToken cancellationToken = default) { const string OperationName = nameof(Encode); _logger.LogOperationStartedDebug(OperationName); - Debug.Assert(coordinates.Length >= 0, "Count must be non-negative."); + if (coordinates is null) { + ExceptionGuard.ThrowArgumentNull(nameof(coordinates)); + } + + IReadOnlyList items = coordinates as IReadOnlyList ?? [.. coordinates]; - if (coordinates.Length < 1) { + if (items.Count < 1) { _logger.LogOperationFailedDebug(OperationName); _logger.LogEmptyArgumentWarning(nameof(coordinates)); ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates)); } int width = _formatter.Width; - int length = GetMaxBufferLength(coordinates.Length, width); + int length = GetMaxBufferLength(items.Count, width); char[]? temp = length <= _options.StackAllocLimit ? null @@ -98,10 +106,10 @@ public TPolyline Encode(ReadOnlySpan coordinates, CancellationToken canc SeedPrevious(previous, null); try { - for (int i = 0; i < coordinates.Length; i++) { + for (int i = 0; i < items.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); - _formatter.GetValues(coordinates[i], values.AsSpan()); + _formatter.GetValues(items[i], values.AsSpan()); for (int j = 0; j < width; j++) { long current = values[j]; @@ -140,12 +148,15 @@ public TPolyline Encode(ReadOnlySpan coordinates, CancellationToken canc /// Per-call options that control the starting delta baseline. Pass or an /// instance with set to /// to use the formatter's default baseline (same as calling - /// ). + /// ). /// /// A token that can be used to cancel the operation. /// /// An instance of representing the encoded values. /// + /// + /// Thrown when is . + /// /// /// Thrown when is empty. /// @@ -155,21 +166,25 @@ public TPolyline Encode(ReadOnlySpan coordinates, CancellationToken canc /// /// Thrown when is canceled. /// - public TPolyline Encode(ReadOnlySpan coordinates, PolylineEncodingOptions? options, CancellationToken cancellationToken) { + public TPolyline Encode(IEnumerable coordinates, PolylineEncodingOptions? options, CancellationToken cancellationToken) { const string OperationName = nameof(Encode); _logger.LogOperationStartedDebug(OperationName); - Debug.Assert(coordinates.Length >= 0, "Count must be non-negative."); + if (coordinates is null) { + ExceptionGuard.ThrowArgumentNull(nameof(coordinates)); + } + + IReadOnlyList items = coordinates as IReadOnlyList ?? [.. coordinates]; - if (coordinates.Length < 1) { + if (items.Count < 1) { _logger.LogOperationFailedDebug(OperationName); _logger.LogEmptyArgumentWarning(nameof(coordinates)); ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates)); } int width = _formatter.Width; - int length = GetMaxBufferLength(coordinates.Length, width); + int length = GetMaxBufferLength(items.Count, width); char[]? temp = length <= _options.StackAllocLimit ? null @@ -184,10 +199,10 @@ public TPolyline Encode(ReadOnlySpan coordinates, PolylineEncodingOption SeedPrevious(previous, options); try { - for (int i = 0; i < coordinates.Length; i++) { + for (int i = 0; i < items.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); - _formatter.GetValues(coordinates[i], values.AsSpan()); + _formatter.GetValues(items[i], values.AsSpan()); for (int j = 0; j < width; j++) { long current = values[j]; diff --git a/src/PolylineAlgorithm/PublicAPI.Unshipped.txt b/src/PolylineAlgorithm/PublicAPI.Unshipped.txt index e69de29b..e513fade 100644 --- a/src/PolylineAlgorithm/PublicAPI.Unshipped.txt +++ b/src/PolylineAlgorithm/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +#nullable enable +PolylineAlgorithm.PolylineEncoder.Encode(System.Collections.Generic.IEnumerable! coordinates, PolylineAlgorithm.PolylineEncodingOptions? options, System.Threading.CancellationToken cancellationToken) -> TPolyline +PolylineAlgorithm.PolylineEncoder.Encode(System.Collections.Generic.IEnumerable! coordinates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> TPolyline diff --git a/tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineDecoderTests.cs b/tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineDecoderTests.cs index 57c56b28..59a10c9a 100644 --- a/tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineDecoderTests.cs +++ b/tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineDecoderTests.cs @@ -223,7 +223,7 @@ public void Decode_With_Previous_Seeds_Accumulated_State() { (double Lat, double Lon)[] chunkB = [(43.252, -126.453)]; string polylineB = encoder.Encode( - chunkB.AsSpan(), + chunkB, new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]), CancellationToken.None); @@ -269,9 +269,9 @@ public void Decode_RoundTrip_Reproduces_Full_Sequence() { (double Lat, double Lon)[] chunkB = all[2..]; // Encode chunked - string polylineA = encoder.Encode(chunkA.AsSpan()); + string polylineA = encoder.Encode(chunkA); string polylineB = encoder.Encode( - chunkB.AsSpan(), + chunkB, new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]), CancellationToken.None); diff --git a/tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineEncoderTests.cs b/tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineEncoderTests.cs index f026df90..a68f81bf 100644 --- a/tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineEncoderTests.cs +++ b/tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineEncoderTests.cs @@ -60,7 +60,7 @@ public void Constructor_With_Valid_Options_Creates_Instance() { // Encode — argument validation // ------------------------------------------------------------------ - /// Tests that encoding an empty span throws . + /// Tests that encoding an empty sequence throws . [TestMethod] public void Encode_With_Empty_Span_Throws_ArgumentException() { // Arrange @@ -68,7 +68,7 @@ public void Encode_With_Empty_Span_Throws_ArgumentException() { // Act & Assert Assert.ThrowsExactly( - () => encoder.Encode(ReadOnlySpan<(double, double)>.Empty)); + () => encoder.Encode(Array.Empty<(double, double)>())); } // ------------------------------------------------------------------ @@ -83,7 +83,7 @@ public void Encode_With_Single_Coordinate_Returns_Non_Empty_String() { (double, double)[] coordinates = [(0.0, 0.0)]; // Act - string result = encoder.Encode(coordinates.AsSpan()); + string result = encoder.Encode(coordinates); // Assert Assert.IsNotNull(result); @@ -99,7 +99,7 @@ public void Encode_With_Known_Coordinates_Returns_Expected_Polyline() { string expected = StaticValueProvider.Valid.GetPolyline(); // Act - string result = encoder.Encode(coordinates.AsSpan()); + string result = encoder.Encode(coordinates); // Assert Assert.AreEqual(expected, result); @@ -120,7 +120,7 @@ public void Encode_With_Pre_Cancelled_Token_Throws_OperationCanceledException() // Act & Assert Assert.ThrowsExactly( - () => encoder.Encode(coordinates.AsSpan(), cts.Token)); + () => encoder.Encode(coordinates, cts.Token)); } // ------------------------------------------------------------------ @@ -139,7 +139,7 @@ public void Encode_With_Small_Stack_Alloc_Limit_Uses_Heap_And_Produces_Correct_R string expected = StaticValueProvider.Valid.GetPolyline(); // Act - string result = encoder.Encode(coordinates.AsSpan()); + string result = encoder.Encode(coordinates); // Assert Assert.AreEqual(expected, result); @@ -178,8 +178,8 @@ public void Encode_With_Baseline_Produces_Different_First_Delta() { (double, double)[] coordinates = [(1.0, 2.0)]; // Act - string resultWith = encoderWithBaseline.Encode(coordinates.AsSpan()); - string resultWithout = encoderNoBaseline.Encode(coordinates.AsSpan()); + string resultWith = encoderWithBaseline.Encode(coordinates); + string resultWithout = encoderNoBaseline.Encode(coordinates); // Assert — the two encodings must differ because the first lat delta changes Assert.AreNotEqual(resultWithout, resultWith); @@ -211,7 +211,7 @@ public void Encode_RoundTrip_Produces_Original_Coordinates() { (double Lat, double Lon)[] original = [.. StaticValueProvider.Valid.GetCoordinates()]; // Act - string encoded = encoder.Encode(original.AsSpan()); + string encoded = encoder.Encode(original); (double Lat, double Lon)[] decoded = [.. decoder.Decode(encoded)]; // Assert @@ -237,8 +237,8 @@ public void Encode_Chunked_With_Null_Options_Produces_Same_Result_As_Standard() (double, double)[] coordinates = [(38.5, -120.2), (40.7, -120.95), (43.252, -126.453)]; // Act - string standard = encoder.Encode(coordinates.AsSpan()); - string chunked = encoder.Encode(coordinates.AsSpan(), null, CancellationToken.None); + string standard = encoder.Encode(coordinates); + string chunked = encoder.Encode(coordinates, null, CancellationToken.None); // Assert Assert.AreEqual(standard, chunked); @@ -256,9 +256,9 @@ public void Encode_Chunked_With_Previous_Seeds_Delta_Baseline() { (double Lat, double Lon)[] chunkB = [(43.252, -126.453)]; // Act — encode chunk B starting from zero (standard) and from chunkA's last point - string standardB = encoder.Encode(chunkB.AsSpan()); + string standardB = encoder.Encode(chunkB); string chunkedB = encoder.Encode( - chunkB.AsSpan(), + chunkB, new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]), CancellationToken.None); @@ -296,14 +296,14 @@ public void Encode_Chunked_Concatenated_Decodes_To_Full_Sequence() { (double Lat, double Lon)[] chunkB = all[2..]; // Act - string polylineA = encoder.Encode(chunkA.AsSpan()); + string polylineA = encoder.Encode(chunkA); string polylineB = encoder.Encode( - chunkB.AsSpan(), + chunkB, new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]), CancellationToken.None); string concatenated = polylineA + polylineB; - string fullEncoding = encoder.Encode(all.AsSpan()); + string fullEncoding = encoder.Encode(all); (double Lat, double Lon)[] decodedConcatenated = [.. decoder.Decode(concatenated)]; (double Lat, double Lon)[] decodedFull = [.. decoder.Decode(fullEncoding)]; @@ -332,7 +332,7 @@ public void Encode_Chunked_With_Empty_Span_Throws_ArgumentException() { // Act & Assert Assert.ThrowsExactly( () => encoder.Encode( - ReadOnlySpan<(double, double)>.Empty, + Array.Empty<(double, double)>(), new PolylineEncodingOptions<(double Lat, double Lon)>(), CancellationToken.None)); } @@ -351,6 +351,6 @@ public void Encode_Chunked_With_Pre_Cancelled_Token_Throws_OperationCanceledExce // Act & Assert Assert.ThrowsExactly( - () => encoder.Encode(coordinates.AsSpan(), null, cts.Token)); + () => encoder.Encode(coordinates, null, cts.Token)); } } diff --git a/tests/PolylineAlgorithm.Tests/Extensions/PolylineEncoderExtensionsTests.cs b/tests/PolylineAlgorithm.Tests/Extensions/PolylineEncoderExtensionsTests.cs index 08cee491..06316c54 100644 --- a/tests/PolylineAlgorithm.Tests/Extensions/PolylineEncoderExtensionsTests.cs +++ b/tests/PolylineAlgorithm.Tests/Extensions/PolylineEncoderExtensionsTests.cs @@ -36,8 +36,7 @@ public sealed class PolylineEncoderExtensionsTests { /// [TestMethod] public void Encode_With_Array_Null_Encoder_Throws_ArgumentNullException() { - // Arrange — call the extension method explicitly because IPolylineEncoder.Encode(ReadOnlySpan) - // would be preferred over the extension when calling through method syntax with an array argument. + // Arrange — call the extension method explicitly to target the array overload. IPolylineEncoder<(double, double), string>? encoder = null; (double, double)[] coordinates = [(0.0, 0.0)]; From ed3d34461d823537b0c798c8e304b5b09a50b5f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:18:36 +0000 Subject: [PATCH 2/3] perf: stack-allocate scratch spans and grow output buffer on demand in PolylineEncoder Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/9ad635cf-efe4-4650-86cc-0083af3375f7 Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com> --- src/PolylineAlgorithm/PolylineEncoder.cs | 152 +++++++---------------- 1 file changed, 43 insertions(+), 109 deletions(-) diff --git a/src/PolylineAlgorithm/PolylineEncoder.cs b/src/PolylineAlgorithm/PolylineEncoder.cs index 8ade0e9a..918205eb 100644 --- a/src/PolylineAlgorithm/PolylineEncoder.cs +++ b/src/PolylineAlgorithm/PolylineEncoder.cs @@ -7,13 +7,10 @@ namespace PolylineAlgorithm; using Microsoft.Extensions.Logging; using PolylineAlgorithm.Abstraction; -using PolylineAlgorithm.Internal; using PolylineAlgorithm.Internal.Diagnostics; using System; using System.Buffers; using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Threading; /// @@ -67,74 +64,18 @@ public PolylineEncoder(PolylineOptions options) { /// /// Thrown when is empty. /// - /// - /// Thrown when the internal encoding buffer cannot accommodate the encoded value. - /// /// /// Thrown when is canceled. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Null is verified before use via ExceptionGuard.ThrowArgumentNull, which is annotated [DoesNotReturn]. CA1062 does not recognise custom [DoesNotReturn] helpers as null guards.")] public TPolyline Encode(IEnumerable coordinates, CancellationToken cancellationToken = default) { - const string OperationName = nameof(Encode); - - _logger.LogOperationStartedDebug(OperationName); + _logger.LogOperationStartedDebug(nameof(Encode)); if (coordinates is null) { ExceptionGuard.ThrowArgumentNull(nameof(coordinates)); } - IReadOnlyList items = coordinates as IReadOnlyList ?? [.. coordinates]; - - if (items.Count < 1) { - _logger.LogOperationFailedDebug(OperationName); - _logger.LogEmptyArgumentWarning(nameof(coordinates)); - ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates)); - } - - int width = _formatter.Width; - int length = GetMaxBufferLength(items.Count, width); - - char[]? temp = length <= _options.StackAllocLimit - ? null - : ArrayPool.Shared.Rent(length); - - Span buffer = temp is null ? stackalloc char[length] : temp.AsSpan(0, length); - - int position = 0; - long[] previous = new long[width]; - long[] values = new long[width]; - - SeedPrevious(previous, null); - - try { - for (int i = 0; i < items.Count; i++) { - cancellationToken.ThrowIfCancellationRequested(); - - _formatter.GetValues(items[i], values.AsSpan()); - - for (int j = 0; j < width; j++) { - long current = values[j]; - long delta = current - previous[j]; - previous[j] = current; - - if (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) { - _logger.LogOperationFailedDebug(OperationName); - _logger.LogCannotWriteValueToBufferWarning(position, i); - ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer(); - } - } - } - - // Convert to string inside the try block so the buffer is still valid. - string encodedResult = buffer[..position].ToString(); - - _logger.LogOperationFinishedDebug(OperationName); - - return _formatter.Write(encodedResult.AsMemory()); - } finally { - if (temp is not null) { - ArrayPool.Shared.Return(temp); - } - } + return EncodeCore(coordinates, null, cancellationToken); } /// @@ -160,96 +101,89 @@ public TPolyline Encode(IEnumerable coordinates, CancellationToken cance /// /// Thrown when is empty. /// - /// - /// Thrown when the internal encoding buffer cannot accommodate the encoded value. - /// /// /// Thrown when is canceled. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Null is verified before use via ExceptionGuard.ThrowArgumentNull, which is annotated [DoesNotReturn]. CA1062 does not recognise custom [DoesNotReturn] helpers as null guards.")] public TPolyline Encode(IEnumerable coordinates, PolylineEncodingOptions? options, CancellationToken cancellationToken) { - const string OperationName = nameof(Encode); - - _logger.LogOperationStartedDebug(OperationName); + _logger.LogOperationStartedDebug(nameof(Encode)); if (coordinates is null) { ExceptionGuard.ThrowArgumentNull(nameof(coordinates)); } - IReadOnlyList items = coordinates as IReadOnlyList ?? [.. coordinates]; + return EncodeCore(coordinates, options, cancellationToken); + } - if (items.Count < 1) { - _logger.LogOperationFailedDebug(OperationName); - _logger.LogEmptyArgumentWarning(nameof(coordinates)); - ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates)); - } + private TPolyline EncodeCore(IEnumerable coordinates, PolylineEncodingOptions? options, CancellationToken cancellationToken) { + const string OperationName = nameof(Encode); + const int MaxStackWidth = 8; int width = _formatter.Width; - int length = GetMaxBufferLength(items.Count, width); - char[]? temp = length <= _options.StackAllocLimit - ? null - : ArrayPool.Shared.Rent(length); + Span previous = width <= MaxStackWidth ? stackalloc long[MaxStackWidth] : new long[width]; + Span values = width <= MaxStackWidth ? stackalloc long[MaxStackWidth] : new long[width]; + previous = previous[..width]; + values = values[..width]; - Span buffer = temp is null ? stackalloc char[length] : temp.AsSpan(0, length); + SeedPrevious(previous, options); - int position = 0; - long[] previous = new long[width]; - long[] values = new long[width]; + int stackLimit = _options.StackAllocLimit; + Span buffer = stackalloc char[stackLimit]; + char[]? rented = null; - SeedPrevious(previous, options); + int position = 0; + bool hasItems = false; try { - for (int i = 0; i < items.Count; i++) { + foreach (TValue item in coordinates) { cancellationToken.ThrowIfCancellationRequested(); - - _formatter.GetValues(items[i], values.AsSpan()); + hasItems = true; + _formatter.GetValues(item, values); for (int j = 0; j < width; j++) { long current = values[j]; long delta = current - previous[j]; previous[j] = current; - if (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) { - _logger.LogOperationFailedDebug(OperationName); - _logger.LogCannotWriteValueToBufferWarning(position, i); - ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer(); + while (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) { + int newSize = rented is null ? stackLimit * 2 : rented.Length * 2; + char[] newRented = ArrayPool.Shared.Rent(newSize); + buffer[..position].CopyTo(newRented); + if (rented is not null) { + ArrayPool.Shared.Return(rented); + } + rented = newRented; + buffer = rented.AsSpan(); } } } + if (!hasItems) { + _logger.LogOperationFailedDebug(OperationName); + _logger.LogEmptyArgumentWarning(nameof(coordinates)); + ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates)); + } + string encodedResult = buffer[..position].ToString(); _logger.LogOperationFinishedDebug(OperationName); return _formatter.Write(encodedResult.AsMemory()); } finally { - if (temp is not null) { - ArrayPool.Shared.Return(temp); + if (rented is not null) { + ArrayPool.Shared.Return(rented); } } } - private void SeedPrevious(long[] previous, PolylineEncodingOptions? options) { - int width = _formatter.Width; - + private void SeedPrevious(Span previous, PolylineEncodingOptions? options) { if (options is { HasPrevious: true }) { - _formatter.GetValues(options.Previous, previous.AsSpan()); + _formatter.GetValues(options.Previous, previous); } else { - for (int j = 0; j < width; j++) { + for (int j = 0; j < previous.Length; j++) { previous[j] = _formatter.GetBaseline(j); } } } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetMaxBufferLength(int count, int valuesPerItem) { - Debug.Assert(count > 0, "Count must be greater than zero."); - Debug.Assert(valuesPerItem > 0, "Values per item must be greater than zero."); - - int requestedBufferLength = count * valuesPerItem * Defaults.Polyline.Block.Length.Max; - - Debug.Assert(requestedBufferLength > 0, "Requested buffer length must be greater than zero."); - - return requestedBufferLength; - } } From b8d9904647c549127a19ac27c7e03dd6ad25f2e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:21:29 +0000 Subject: [PATCH 3/3] style: rename hasItems to anyItemProcessed in EncodeCore Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/9ad635cf-efe4-4650-86cc-0083af3375f7 Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com> --- src/PolylineAlgorithm/PolylineEncoder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PolylineAlgorithm/PolylineEncoder.cs b/src/PolylineAlgorithm/PolylineEncoder.cs index 918205eb..12f18987 100644 --- a/src/PolylineAlgorithm/PolylineEncoder.cs +++ b/src/PolylineAlgorithm/PolylineEncoder.cs @@ -133,12 +133,12 @@ private TPolyline EncodeCore(IEnumerable coordinates, PolylineEncodingOp char[]? rented = null; int position = 0; - bool hasItems = false; + bool anyItemProcessed = false; try { foreach (TValue item in coordinates) { cancellationToken.ThrowIfCancellationRequested(); - hasItems = true; + anyItemProcessed = true; _formatter.GetValues(item, values); for (int j = 0; j < width; j++) { @@ -159,7 +159,7 @@ private TPolyline EncodeCore(IEnumerable coordinates, PolylineEncodingOp } } - if (!hasItems) { + if (!anyItemProcessed) { _logger.LogOperationFailedDebug(OperationName); _logger.LogEmptyArgumentWarning(nameof(coordinates)); ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates));