Skip to content

Commit 794cbf7

Browse files
Copilotpetesramek
andauthored
Fix baseline not applied in EncodeWithFormatter, long→int delta arithmetic, doc comment, add formatter-path integration tests
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/f4994093-4128-4101-b5df-f6a031463712 Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent 3482dac commit 794cbf7

6 files changed

Lines changed: 238 additions & 6 deletions

File tree

src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ namespace PolylineAlgorithm.Abstraction;
1616
/// <remarks>
1717
/// <para>
1818
/// <b>Formatter-based use (no subclassing required):</b>
19-
/// Supply a <see cref="PolylineOptions{TPolyline, TCoordinate}"/> via the
20-
/// <see cref="AbstractPolylineDecoder{TPolyline, TCoordinate}(PolylineOptions{TPolyline, TCoordinate})"/>
19+
/// Supply a <see cref="PolylineOptions{TCoordinate, TPolyline}"/> via the
20+
/// <see cref="AbstractPolylineDecoder{TPolyline, TCoordinate}(PolylineOptions{TCoordinate, TPolyline})"/>
2121
/// constructor. The formatters handle all type-specific concerns; override nothing.
2222
/// </para>
2323
/// <para>

src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,22 +213,26 @@ private TPolyline EncodeWithFormatter(ReadOnlySpan<TCoordinate> coordinates, Can
213213
Span<char> buffer = temp is null ? stackalloc char[length] : temp.AsSpan(0, length);
214214

215215
int position = 0;
216-
int[] previous = new int[width];
216+
long[] previous = new long[width];
217217
long[] values = new long[width];
218218
string encodedResult;
219219

220+
for (int j = 0; j < width; j++) {
221+
previous[j] = _valueFormatter.GetBaseline(j);
222+
}
223+
220224
try {
221225
for (var i = 0; i < coordinates.Length; i++) {
222226
cancellationToken.ThrowIfCancellationRequested();
223227

224228
_valueFormatter.GetValues(coordinates[i], values.AsSpan());
225229

226230
for (int j = 0; j < width; j++) {
227-
int current = (int)values[j];
228-
int delta = current - previous[j];
231+
long current = values[j];
232+
long delta = current - previous[j];
229233
previous[j] = current;
230234

231-
if (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) {
235+
if (!PolylineEncoding.TryWriteValue((int)delta, buffer, ref position)) {
232236
_logger.LogOperationFailedDebug(OperationName);
233237
_logger.LogCannotWriteValueToBufferWarning(position, i);
234238
ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer();

src/PolylineAlgorithm/Abstraction/IPolylineValueFormatter.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,12 @@ public interface IPolylineValueFormatter<TValue> {
5252
/// the length of the span received in <see cref="CreateItem"/>.
5353
/// </summary>
5454
int Width { get; }
55+
56+
/// <summary>
57+
/// Returns the baseline (epoch) for the column at <paramref name="index"/>, or <c>0</c> if none is configured.
58+
/// The encoder subtracts this value from the first item's scaled column value to keep the initial delta small.
59+
/// </summary>
60+
/// <param name="index">The zero-based column index. Must be in the range <c>[0, <see cref="Width"/>)</c>.</param>
61+
/// <returns>The baseline value, or <c>0</c> when no baseline has been defined for the column.</returns>
62+
long GetBaseline(int index) => 0L;
5563
}

src/PolylineAlgorithm/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ PolylineAlgorithm.Abstraction.IPolylineFormatter<TPolyline>.Read(TPolyline polyl
99
PolylineAlgorithm.Abstraction.IPolylineFormatter<TPolyline>.Write(System.ReadOnlyMemory<char> encoded) -> TPolyline
1010
PolylineAlgorithm.Abstraction.IPolylineValueFormatter<TValue>
1111
PolylineAlgorithm.Abstraction.IPolylineValueFormatter<TValue>.CreateItem(System.ReadOnlySpan<long> values) -> TValue
12+
PolylineAlgorithm.Abstraction.IPolylineValueFormatter<TValue>.GetBaseline(int index) -> long
1213
PolylineAlgorithm.Abstraction.IPolylineValueFormatter<TValue>.GetValues(TValue item, System.Span<long> values) -> void
1314
PolylineAlgorithm.Abstraction.IPolylineValueFormatter<TValue>.Width.get -> int
1415
PolylineAlgorithm.PolylineFormatter

tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineDecoderTests.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace PolylineAlgorithm.Tests.Abstraction;
99
using PolylineAlgorithm.Utility;
1010
using System;
1111
using System.Collections.Generic;
12+
using PolylineAlgorithm;
1213

1314
/// <summary>
1415
/// Tests for <see cref="AbstractPolylineDecoder{TPolyline, TCoordinate}"/>.
@@ -141,4 +142,99 @@ public void Decode_With_Pre_Cancelled_Token_Throws_OperationCanceledException()
141142
// Act & Assert
142143
Assert.ThrowsExactly<OperationCanceledException>(() => decoder.Decode(polyline, cts.Token).ToList());
143144
}
145+
146+
// -------------------------------------------------------------------------
147+
// Formatter-based path (PolylineOptions<TValue, TPolyline> constructor)
148+
// -------------------------------------------------------------------------
149+
150+
/// <summary>
151+
/// Tests that the PolylineOptions constructor with null throws <see cref="ArgumentNullException"/>.
152+
/// </summary>
153+
[TestMethod]
154+
public void Constructor_With_Null_PolylineOptions_Throws_ArgumentNullException() {
155+
// Act & Assert
156+
ArgumentNullException ex = Assert.ThrowsExactly<ArgumentNullException>(
157+
() => new AbstractPolylineDecoder<string, (double, double)>((PolylineOptions<(double, double), string>)null!));
158+
Assert.AreEqual("options", ex.ParamName);
159+
}
160+
161+
/// <summary>
162+
/// Tests that the formatter-based decoder decodes the known polyline to the expected coordinates.
163+
/// </summary>
164+
[TestMethod]
165+
public void Decode_FormatterPath_With_Known_Polyline_Returns_Expected_Coordinates() {
166+
// Arrange
167+
PolylineValueFormatter<(double Latitude, double Longitude)> valueFormatter =
168+
FormatterBuilder<(double Latitude, double Longitude)>.Create()
169+
.AddValue("lat", c => c.Latitude)
170+
.AddValue("lon", c => c.Longitude)
171+
.WithCreate(static values => (values[0] / 1e5, values[1] / 1e5))
172+
.Build();
173+
174+
PolylineOptions<(double Latitude, double Longitude), string> options = new(
175+
valueFormatter,
176+
PolylineFormatter.ForString);
177+
178+
AbstractPolylineDecoder<string, (double Latitude, double Longitude)> decoder = new(options);
179+
180+
string polyline = StaticValueProvider.Valid.GetPolyline();
181+
(double Latitude, double Longitude)[] expected = [.. StaticValueProvider.Valid.GetCoordinates()];
182+
183+
// Act
184+
(double Latitude, double Longitude)[] result = [.. decoder.Decode(polyline)];
185+
186+
// Assert
187+
Assert.AreEqual(expected.Length, result.Length);
188+
for (int i = 0; i < expected.Length; i++) {
189+
Assert.AreEqual(expected[i].Latitude, result[i].Latitude, 1e-5);
190+
Assert.AreEqual(expected[i].Longitude, result[i].Longitude, 1e-5);
191+
}
192+
}
193+
194+
/// <summary>
195+
/// Tests that the formatter-based decoder throws <see cref="ArgumentNullException"/> for a null polyline.
196+
/// </summary>
197+
[TestMethod]
198+
public void Decode_FormatterPath_With_Null_Polyline_Throws_ArgumentNullException() {
199+
// Arrange
200+
PolylineValueFormatter<(double, double)> valueFormatter =
201+
FormatterBuilder<(double, double)>.Create()
202+
.AddValue("lat", c => c.Item1)
203+
.AddValue("lon", c => c.Item2)
204+
.WithCreate(static values => (values[0] / 1e5, values[1] / 1e5))
205+
.Build();
206+
207+
AbstractPolylineDecoder<string, (double, double)> decoder = new(
208+
new PolylineOptions<(double, double), string>(valueFormatter, PolylineFormatter.ForString));
209+
210+
// Act & Assert
211+
ArgumentNullException ex = Assert.ThrowsExactly<ArgumentNullException>(
212+
() => decoder.Decode(null!).ToList());
213+
Assert.AreEqual("polyline", ex.ParamName);
214+
}
215+
216+
/// <summary>
217+
/// Tests that the formatter-based decoder with a pre-cancelled token throws <see cref="OperationCanceledException"/>.
218+
/// </summary>
219+
[TestMethod]
220+
public void Decode_FormatterPath_With_Pre_Cancelled_Token_Throws_OperationCanceledException() {
221+
// Arrange
222+
PolylineValueFormatter<(double, double)> valueFormatter =
223+
FormatterBuilder<(double, double)>.Create()
224+
.AddValue("lat", c => c.Item1)
225+
.AddValue("lon", c => c.Item2)
226+
.WithCreate(static values => (values[0] / 1e5, values[1] / 1e5))
227+
.Build();
228+
229+
AbstractPolylineDecoder<string, (double, double)> decoder = new(
230+
new PolylineOptions<(double, double), string>(valueFormatter, PolylineFormatter.ForString));
231+
232+
string polyline = StaticValueProvider.Valid.GetPolyline();
233+
using CancellationTokenSource cts = new();
234+
cts.Cancel();
235+
236+
// Act & Assert
237+
Assert.ThrowsExactly<OperationCanceledException>(
238+
() => decoder.Decode(polyline, cts.Token).ToList());
239+
}
144240
}

tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineEncoderTests.cs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace PolylineAlgorithm.Tests.Abstraction;
88
using PolylineAlgorithm.Abstraction;
99
using PolylineAlgorithm.Utility;
1010
using System;
11+
using PolylineAlgorithm;
1112

1213
/// <summary>
1314
/// Tests for <see cref="AbstractPolylineEncoder{TCoordinate, TPolyline}"/>.
@@ -149,4 +150,126 @@ public void Encode_With_Small_Stack_Alloc_Limit_Uses_Heap_Allocation_And_Produce
149150
// Assert
150151
Assert.AreEqual(expected, result);
151152
}
153+
154+
// -------------------------------------------------------------------------
155+
// Formatter-based path (PolylineOptions<TValue, TPolyline> constructor)
156+
// -------------------------------------------------------------------------
157+
158+
/// <summary>
159+
/// Tests that the PolylineOptions constructor with null throws <see cref="ArgumentNullException"/>.
160+
/// </summary>
161+
[TestMethod]
162+
public void Constructor_With_Null_PolylineOptions_Throws_ArgumentNullException() {
163+
// Act & Assert
164+
ArgumentNullException ex = Assert.ThrowsExactly<ArgumentNullException>(
165+
() => new AbstractPolylineEncoder<(double, double), string>((PolylineOptions<(double, double), string>)null!));
166+
Assert.AreEqual("options", ex.ParamName);
167+
}
168+
169+
/// <summary>
170+
/// Tests that the formatter-based encoder round-trips known coordinates through the polyline wire format.
171+
/// </summary>
172+
[TestMethod]
173+
public void Encode_FormatterPath_With_Known_Coordinates_Returns_Expected_Polyline() {
174+
// Arrange
175+
PolylineValueFormatter<(double Latitude, double Longitude)> valueFormatter =
176+
FormatterBuilder<(double Latitude, double Longitude)>.Create()
177+
.AddValue("lat", c => c.Latitude)
178+
.AddValue("lon", c => c.Longitude)
179+
.Build();
180+
181+
PolylineOptions<(double Latitude, double Longitude), string> options = new(
182+
valueFormatter,
183+
PolylineFormatter.ForString);
184+
185+
AbstractPolylineEncoder<(double Latitude, double Longitude), string> encoder = new(options);
186+
187+
(double Latitude, double Longitude)[] coordinates = [.. StaticValueProvider.Valid.GetCoordinates()];
188+
string expected = StaticValueProvider.Valid.GetPolyline();
189+
190+
// Act
191+
string result = encoder.Encode(coordinates.AsSpan());
192+
193+
// Assert
194+
Assert.AreEqual(expected, result);
195+
}
196+
197+
/// <summary>
198+
/// Tests that the formatter-based encoder respects a non-zero baseline by subtracting it
199+
/// from the first item's scaled value, producing a smaller first delta.
200+
/// </summary>
201+
[TestMethod]
202+
public void Encode_FormatterPath_With_Baseline_Produces_Correct_First_Delta() {
203+
// Arrange: a single coordinate at (1.0, 2.0) with precision 5.
204+
// Without baseline, scaled values are (100000, 200000).
205+
// With baseline lat=100000, the first lat delta must be 0 (100000 - 100000).
206+
PolylineValueFormatter<(double Lat, double Lon)> valueFormatter =
207+
FormatterBuilder<(double Lat, double Lon)>.Create()
208+
.AddValue("lat", c => c.Lat)
209+
.SetBaseline(100000L) // scaled value of 1.0 at precision 5
210+
.AddValue("lon", c => c.Lon)
211+
.Build();
212+
213+
PolylineOptions<(double Lat, double Lon), string> optionsWithBaseline = new(
214+
valueFormatter,
215+
PolylineFormatter.ForString);
216+
217+
PolylineValueFormatter<(double Lat, double Lon)> valueFormatterNoBaseline =
218+
FormatterBuilder<(double Lat, double Lon)>.Create()
219+
.AddValue("lat", c => c.Lat)
220+
.AddValue("lon", c => c.Lon)
221+
.Build();
222+
223+
PolylineOptions<(double Lat, double Lon), string> optionsNoBaseline = new(
224+
valueFormatterNoBaseline,
225+
PolylineFormatter.ForString);
226+
227+
AbstractPolylineEncoder<(double Lat, double Lon), string> encoderWithBaseline = new(optionsWithBaseline);
228+
AbstractPolylineEncoder<(double Lat, double Lon), string> encoderNoBaseline = new(optionsNoBaseline);
229+
230+
(double, double)[] coordinates = [(1.0, 2.0)];
231+
232+
// Act
233+
string resultWithBaseline = encoderWithBaseline.Encode(coordinates.AsSpan());
234+
string resultNoBaseline = encoderNoBaseline.Encode(coordinates.AsSpan());
235+
236+
// Assert: baseline shifts the first delta so the results differ, and the baseline result
237+
// encodes a smaller (zero) latitude delta.
238+
Assert.AreNotEqual(resultNoBaseline, resultWithBaseline);
239+
}
240+
241+
/// <summary>
242+
/// Tests that the formatter-based encoder produces output that the formatter-based decoder can
243+
/// reconstruct exactly (full round-trip).
244+
/// </summary>
245+
[TestMethod]
246+
public void Encode_FormatterPath_RoundTrip_Produces_Original_Coordinates() {
247+
// Arrange
248+
PolylineValueFormatter<(double Latitude, double Longitude)> valueFormatter =
249+
FormatterBuilder<(double Latitude, double Longitude)>.Create()
250+
.AddValue("lat", c => c.Latitude)
251+
.AddValue("lon", c => c.Longitude)
252+
.WithCreate(static values => (values[0] / 1e5, values[1] / 1e5))
253+
.Build();
254+
255+
PolylineOptions<(double Latitude, double Longitude), string> options = new(
256+
valueFormatter,
257+
PolylineFormatter.ForString);
258+
259+
AbstractPolylineEncoder<(double Latitude, double Longitude), string> encoder = new(options);
260+
AbstractPolylineDecoder<string, (double Latitude, double Longitude)> decoder = new(options);
261+
262+
(double Latitude, double Longitude)[] original = [.. StaticValueProvider.Valid.GetCoordinates()];
263+
264+
// Act
265+
string encoded = encoder.Encode(original.AsSpan());
266+
(double Latitude, double Longitude)[] decoded = [.. decoder.Decode(encoded)];
267+
268+
// Assert
269+
Assert.AreEqual(original.Length, decoded.Length);
270+
for (int i = 0; i < original.Length; i++) {
271+
Assert.AreEqual(original[i].Latitude, decoded[i].Latitude, 1e-5);
272+
Assert.AreEqual(original[i].Longitude, decoded[i].Longitude, 1e-5);
273+
}
274+
}
152275
}

0 commit comments

Comments
 (0)