Skip to content

Commit 7852986

Browse files
Copilotpetesramek
andauthored
feat: inline DecodeIterator into Decode, encode/decode timestamps as Unix seconds
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/07ca1843-2d5d-44b9-a4d4-ab03b37d1877 Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent d9d5d57 commit 7852986

3 files changed

Lines changed: 50 additions & 34 deletions

File tree

samples/PolylineAlgorithm.SensorData.Sample/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@
3434
Console.WriteLine($"Encoded polyline: {encoded}");
3535
Console.WriteLine();
3636

37-
// Decode (timestamps are not encoded, so they will be default)
37+
// Decode (timestamps and temperatures are both recovered)
3838
IEnumerable<SensorReading> decoded = decoder.Decode(encoded);
3939

40-
Console.WriteLine("Decoded temperatures:");
40+
Console.WriteLine("Decoded readings:");
4141
foreach (SensorReading r in decoded)
4242
{
43-
Console.WriteLine($" {r.Temperature:F1} °C");
43+
Console.WriteLine($" [{r.Timestamp:HH:mm:ss}] {r.Temperature:F1} °C");
4444
}

samples/PolylineAlgorithm.SensorData.Sample/SensorDataDecoder.cs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,12 @@ namespace PolylineAlgorithm.SensorData.Sample;
1919
/// scalar type, following the same structural pattern as <see cref="AbstractPolylineDecoder{TPolyline,TCoordinate}"/>.
2020
/// </para>
2121
/// <para>
22-
/// Because sensor data is one-dimensional (a single temperature per reading), the base class designed
23-
/// for two-dimensional coordinate pairs is not used. Instead, <see cref="PolylineEncoding"/> static
24-
/// helpers are called directly to read delta-encoded characters and denormalise the recovered values.
25-
/// </para>
26-
/// <para>
27-
/// Timestamps cannot be recovered from the encoded string.
28-
/// The decoded <see cref="SensorReading"/> instances will have <see cref="SensorReading.Timestamp"/>
29-
/// set to <see langword="default"/>.
22+
/// Each encoded pair consists of a delta-compressed Unix timestamp (seconds since Unix epoch, precision 0)
23+
/// followed by a delta-compressed temperature value (at <see cref="PolylineEncodingOptions.Precision"/>).
24+
/// Both are recovered and used to reconstruct the original <see cref="SensorReading"/>.
3025
/// </para>
3126
/// </remarks>
27+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Sonar", "S4456:Parameter validation in yielding methods should be wrapped", Justification = "Inlined by design to demonstrate a simple iterator without a wrapper method.")]
3228
internal sealed class SensorDataDecoder : IPolylineDecoder<string, SensorReading> {
3329
/// <summary>
3430
/// Initializes a new instance of the <see cref="SensorDataDecoder"/> class with default encoding options.
@@ -68,8 +64,8 @@ public SensorDataDecoder(PolylineEncodingOptions options) {
6864
/// </param>
6965
/// <returns>
7066
/// An <see cref="IEnumerable{T}"/> of <see cref="SensorReading"/> whose
71-
/// <see cref="SensorReading.Temperature"/> values are recovered from the encoded string.
72-
/// <see cref="SensorReading.Timestamp"/> will be <see langword="default"/> for every element.
67+
/// <see cref="SensorReading.Timestamp"/> and <see cref="SensorReading.Temperature"/> values
68+
/// are recovered from the encoded string.
7369
/// </returns>
7470
/// <exception cref="ArgumentNullException">
7571
/// Thrown when <paramref name="polyline"/> is <see langword="null"/>.
@@ -87,23 +83,25 @@ public IEnumerable<SensorReading> Decode(string polyline, CancellationToken canc
8783
throw new ArgumentException("Encoded polyline must not be empty.", nameof(polyline));
8884
}
8985

90-
return DecodeIterator(polyline.AsMemory(), cancellationToken);
91-
}
92-
93-
private IEnumerable<SensorReading> DecodeIterator(ReadOnlyMemory<char> memory, CancellationToken cancellationToken) {
86+
ReadOnlyMemory<char> memory = polyline.AsMemory();
9487
int position = 0;
95-
int accumulated = 0;
88+
// Mirror the encoder's base epoch so the first delta decodes back to the correct Unix seconds.
89+
int accumulatedTimestamp = SensorDataEncoder.TimestampBaseEpochSeconds;
90+
int accumulatedTemperature = 0;
9691

9792
while (position < memory.Length) {
9893
cancellationToken.ThrowIfCancellationRequested();
9994

100-
if (!PolylineEncoding.TryReadValue(ref accumulated, memory, ref position)) {
95+
// Read Unix timestamp delta (precision 0) then temperature delta.
96+
if (!PolylineEncoding.TryReadValue(ref accumulatedTimestamp, memory, ref position)
97+
|| !PolylineEncoding.TryReadValue(ref accumulatedTemperature, memory, ref position)) {
10198
yield break;
10299
}
103100

104-
double temperature = PolylineEncoding.Denormalize(accumulated, Options.Precision);
101+
long unixSeconds = (long)PolylineEncoding.Denormalize(accumulatedTimestamp, precision: 0);
102+
double temperature = PolylineEncoding.Denormalize(accumulatedTemperature, Options.Precision);
105103

106-
yield return new SensorReading(default, temperature);
104+
yield return new SensorReading(DateTimeOffset.FromUnixTimeSeconds(unixSeconds), temperature);
107105
}
108106
}
109107
}

samples/PolylineAlgorithm.SensorData.Sample/SensorDataEncoder.cs

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,29 @@ namespace PolylineAlgorithm.SensorData.Sample;
1111

1212
/// <summary>
1313
/// Encodes a sequence of <see cref="SensorReading"/> values into a compact polyline string
14-
/// using the polyline delta-encoding algorithm applied to the <see cref="SensorReading.Temperature"/> field.
14+
/// using the polyline delta-encoding algorithm applied to both the <see cref="SensorReading.Timestamp"/>
15+
/// and <see cref="SensorReading.Temperature"/> fields.
1516
/// </summary>
1617
/// <remarks>
1718
/// <para>
1819
/// This class demonstrates implementing <see cref="IPolylineEncoder{TValue,TPolyline}"/> for a custom
1920
/// scalar type, following the same structural pattern as <see cref="AbstractPolylineEncoder{TCoordinate,TPolyline}"/>.
2021
/// </para>
2122
/// <para>
22-
/// Because sensor readings carry only a single numeric dimension (temperature), the base class designed
23-
/// for two-dimensional coordinate pairs is not used. Instead, <see cref="PolylineEncoding"/> static
23+
/// Because sensor readings carry two numeric dimensions (timestamp and temperature), the base class designed
24+
/// for geographic coordinate pairs is not used. Instead, <see cref="PolylineEncoding"/> static
2425
/// helpers are called directly to perform normalisation, delta computation, and character-level encoding.
2526
/// </para>
2627
/// <para>
27-
/// Only <see cref="SensorReading.Temperature"/> values are encoded. Timestamps are not encoded and
28-
/// will not be recovered on decoding.
28+
/// Each reading is encoded as a pair of delta-compressed values:
29+
/// the Unix timestamp in seconds (precision 0) followed by the temperature (at <see cref="PolylineEncodingOptions.Precision"/>).
2930
/// </para>
3031
/// </remarks>
3132
internal sealed class SensorDataEncoder : IPolylineEncoder<SensorReading, string> {
33+
// 2020-01-01 00:00:00 UTC in Unix seconds. Used as the delta-encoding base for timestamps
34+
// so that the first absolute delta stays within the int32 safe range of the polyline algorithm.
35+
internal const int TimestampBaseEpochSeconds = 1_577_836_800;
36+
3237
/// <summary>
3338
/// Initializes a new instance of the <see cref="SensorDataEncoder"/> class with default encoding options.
3439
/// </summary>
@@ -59,14 +64,15 @@ public SensorDataEncoder(PolylineEncodingOptions options) {
5964
/// Encodes a sequence of <see cref="SensorReading"/> values into a polyline string.
6065
/// </summary>
6166
/// <param name="coordinates">
62-
/// The sensor readings whose <see cref="SensorReading.Temperature"/> values are to be encoded.
67+
/// The sensor readings to encode. Each reading contributes a delta-compressed Unix timestamp
68+
/// (seconds since Unix epoch, precision 0) and a delta-compressed temperature value.
6369
/// Must contain at least one element.
6470
/// </param>
6571
/// <param name="cancellationToken">
6672
/// A <see cref="CancellationToken"/> that can be used to cancel the encoding operation.
6773
/// </param>
6874
/// <returns>
69-
/// A polyline-encoded string representing the delta-compressed temperature series.
75+
/// A polyline-encoded string representing the delta-compressed timestamp and temperature series.
7076
/// </returns>
7177
/// <exception cref="ArgumentException">
7278
/// Thrown when <paramref name="coordinates"/> is empty.
@@ -83,9 +89,14 @@ public string Encode(ReadOnlySpan<SensorReading> coordinates, CancellationToken
8389
// using the polyline algorithm (ceil(32 bits / 5 bits per chunk) + sign bit = 7).
8490
const int MaxEncodedCharsPerValue = 7;
8591

86-
int previousNormalized = 0;
92+
// Each reading encodes two values: Unix timestamp (precision 0) + temperature.
93+
// The polyline algorithm uses signed int32 internally, limiting safe absolute values to ~1.07B.
94+
// Current Unix time in seconds (~1.74B) exceeds this. We therefore delta-encode relative to
95+
// 2020-01-01 00:00:00 UTC (= 1 577 836 800 s), keeping the initial delta well within range.
96+
int previousTimestampNormalized = TimestampBaseEpochSeconds;
97+
int previousTemperatureNormalized = 0;
8798
int position = 0;
88-
int length = coordinates.Length * MaxEncodedCharsPerValue;
99+
int length = coordinates.Length * 2 * MaxEncodedCharsPerValue;
89100

90101
char[]? temp = length <= Options.StackAllocLimit
91102
? null
@@ -97,14 +108,21 @@ public string Encode(ReadOnlySpan<SensorReading> coordinates, CancellationToken
97108
for (int i = 0; i < coordinates.Length; i++) {
98109
cancellationToken.ThrowIfCancellationRequested();
99110

100-
int normalized = PolylineEncoding.Normalize(coordinates[i].Temperature, Options.Precision);
101-
int delta = normalized - previousNormalized;
111+
// Encode Unix timestamp in whole seconds (precision 0).
112+
int normalizedTimestamp = PolylineEncoding.Normalize((double)coordinates[i].Timestamp.ToUnixTimeSeconds(), precision: 0);
113+
int timestampDelta = normalizedTimestamp - previousTimestampNormalized;
114+
115+
// Encode temperature at the configured precision.
116+
int normalizedTemperature = PolylineEncoding.Normalize(coordinates[i].Temperature, Options.Precision);
117+
int temperatureDelta = normalizedTemperature - previousTemperatureNormalized;
102118

103-
if (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) {
119+
if (!PolylineEncoding.TryWriteValue(timestampDelta, buffer, ref position)
120+
|| !PolylineEncoding.TryWriteValue(temperatureDelta, buffer, ref position)) {
104121
throw new InvalidOperationException("Encoding buffer is too small to hold the encoded value.");
105122
}
106123

107-
previousNormalized = normalized;
124+
previousTimestampNormalized = normalizedTimestamp;
125+
previousTemperatureNormalized = normalizedTemperature;
108126
}
109127

110128
return buffer[..position].ToString();

0 commit comments

Comments
 (0)