From ee09102723c00a888c1db28b5b497e1c02abde2e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 11:30:22 +0000
Subject: [PATCH 01/22] feat: add PolylineFormatter, FormatterBuilder,
PolylineOptions and FormatterRule
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/16db7628-2cc2-4c51-982b-0264a04d7157
Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
---
src/PolylineAlgorithm/FormatterBuilder.cs | 119 +++++
.../Internal/FormatterRule.cs | 53 ++
src/PolylineAlgorithm/PolylineFormatter.cs | 80 +++
src/PolylineAlgorithm/PolylineOptions.cs | 50 ++
src/PolylineAlgorithm/PublicAPI.Unshipped.txt | 14 +
.../PolylineFormatterTests.cs | 469 ++++++++++++++++++
6 files changed, 785 insertions(+)
create mode 100644 src/PolylineAlgorithm/FormatterBuilder.cs
create mode 100644 src/PolylineAlgorithm/Internal/FormatterRule.cs
create mode 100644 src/PolylineAlgorithm/PolylineFormatter.cs
create mode 100644 src/PolylineAlgorithm/PolylineOptions.cs
create mode 100644 tests/PolylineAlgorithm.Tests/PolylineFormatterTests.cs
diff --git a/src/PolylineAlgorithm/FormatterBuilder.cs b/src/PolylineAlgorithm/FormatterBuilder.cs
new file mode 100644
index 00000000..26021eaf
--- /dev/null
+++ b/src/PolylineAlgorithm/FormatterBuilder.cs
@@ -0,0 +1,119 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm;
+
+using PolylineAlgorithm.Internal;
+using System;
+using System.Collections.Generic;
+
+///
+/// Provides a fluent builder for constructing a .
+///
+/// The source object type from which column values are extracted.
+///
+///
+/// Use to obtain an instance, call once per column,
+/// optionally chain to specify an epoch for the most-recently added column,
+/// then call to produce the immutable .
+///
+///
+/// The builder is the only way to create a — its
+/// constructor is internal.
+///
+///
+public sealed class FormatterBuilder {
+ private readonly List> _rules = [];
+ private readonly HashSet _names = new(StringComparer.Ordinal);
+
+ private FormatterBuilder() { }
+
+ ///
+ /// Creates a new instance.
+ ///
+ /// A fresh with no rules.
+ public static FormatterBuilder Create() => new();
+
+ ///
+ /// Adds a column with the specified value selector and precision.
+ ///
+ ///
+ /// A unique, non-null, non-empty name that identifies the column. Used for diagnostics only.
+ ///
+ ///
+ /// A delegate that extracts the column's raw value from an item of type
+ /// .
+ ///
+ ///
+ /// The number of decimal places to preserve. Each extracted value is multiplied by
+ /// 10^ before encoding. Defaults to 5.
+ ///
+ /// The current instance for method chaining.
+ ///
+ /// Thrown when or is .
+ ///
+ ///
+ /// Thrown when is empty, or a rule with the same name already exists.
+ ///
+ public FormatterBuilder AddValue(string name, Func selector, uint precision = 5) {
+ if (name is null) {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (name.Length == 0) {
+ throw new ArgumentException("Name cannot be empty.", nameof(name));
+ }
+
+ if (selector is null) {
+ throw new ArgumentNullException(nameof(selector));
+ }
+
+ if (!_names.Add(name)) {
+ throw new ArgumentException($"A rule with the name '{name}' has already been added.", nameof(name));
+ }
+
+ _rules.Add(new FormatterRule(name, (long)Pow10.GetFactor(precision), selector));
+
+ return this;
+ }
+
+ ///
+ /// Sets a baseline (epoch) on the most-recently added column.
+ /// During encoding the baseline is subtracted from the first item's scaled column value,
+ /// keeping the initial delta small when the absolute first value is large.
+ ///
+ /// The baseline value to apply to the first item's column value.
+ /// The current instance for method chaining.
+ ///
+ /// Thrown when no rules have been added yet. Call before .
+ ///
+ public FormatterBuilder SetBaseline(long baseline) {
+ if (_rules.Count == 0) {
+ throw new InvalidOperationException("Cannot set a baseline when no rules have been added. Call AddValue first.");
+ }
+
+ var last = _rules[^1];
+ _rules[^1] = new FormatterRule(last.Name, last.Factor, last.Select, baseline);
+
+ return this;
+ }
+
+ ///
+ /// Bakes all added rules into a sealed, immutable .
+ ///
+ ///
+ /// An immutable whose rules can no longer be changed.
+ ///
+ ///
+ /// Thrown when no rules have been added.
+ ///
+ public PolylineFormatter Build() {
+ if (_rules.Count == 0) {
+ throw new InvalidOperationException("At least one rule must be added before calling Build.");
+ }
+
+ return new PolylineFormatter(_rules.ToArray());
+ }
+}
diff --git a/src/PolylineAlgorithm/Internal/FormatterRule.cs b/src/PolylineAlgorithm/Internal/FormatterRule.cs
new file mode 100644
index 00000000..9d5a4ab8
--- /dev/null
+++ b/src/PolylineAlgorithm/Internal/FormatterRule.cs
@@ -0,0 +1,53 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm.Internal;
+
+using System;
+
+///
+/// Represents a single column rule baked into a .
+/// Stores the pre-calculated factor and an optional baseline alongside the user-supplied value selector.
+///
+/// The source object type from which the column value is extracted.
+internal sealed class FormatterRule {
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The column name used for diagnostics.
+ /// The pre-calculated scaling factor (10^precision).
+ /// The delegate that extracts the raw value from an item.
+ /// The optional baseline value applied to the first item only.
+ internal FormatterRule(string name, long factor, Func select, long? baseline = null) {
+ Name = name;
+ Factor = factor;
+ Select = select;
+ Baseline = baseline;
+ }
+
+ ///
+ /// Gets the column name. Used for diagnostics and duplicate-name detection only.
+ ///
+ internal string Name { get; }
+
+ ///
+ /// Gets the pre-calculated scaling factor (10^precision).
+ /// Stored as so that (long)(value * Factor) stays in 64-bit arithmetic
+ /// throughout the encoding hot loop without additional casting.
+ ///
+ internal long Factor { get; }
+
+ ///
+ /// Gets the optional baseline (epoch). When set, the encoder subtracts this value from the
+ /// first point's scaled column value to keep the initial delta small.
+ ///
+ internal long? Baseline { get; }
+
+ ///
+ /// Gets the delegate that extracts the column's raw value from an item of type
+ /// . Stored as a concrete delegate so the JIT can inline the call site.
+ ///
+ internal Func Select { get; }
+}
diff --git a/src/PolylineAlgorithm/PolylineFormatter.cs b/src/PolylineAlgorithm/PolylineFormatter.cs
new file mode 100644
index 00000000..b290115c
--- /dev/null
+++ b/src/PolylineAlgorithm/PolylineFormatter.cs
@@ -0,0 +1,80 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm;
+
+using PolylineAlgorithm.Internal;
+using System;
+using System.Runtime.CompilerServices;
+
+///
+/// Provides an immutable, sealed rule engine that describes how to extract and scale values from
+/// an object of type for polyline encoding.
+///
+/// The source object type from which column values are extracted.
+///
+///
+/// Instances of this class are constructed exclusively through .
+///
+///
+/// The modifier allows the JIT to devirtualise and inline calls to
+/// , eliminating vtable dispatch in the encoding hot loop.
+///
+///
+public sealed class PolylineFormatter {
+ private readonly FormatterRule[] _rules;
+
+ ///
+ /// Initializes a new instance of with the baked rules.
+ /// This constructor is intentionally internal; use to create instances.
+ ///
+ /// The pre-calculated rules array produced by the builder.
+ internal PolylineFormatter(FormatterRule[] rules) {
+ _rules = rules;
+ Width = rules.Length;
+ HasBaselines = Array.Exists(rules, static r => r.Baseline.HasValue);
+ }
+
+ ///
+ /// Gets the number of columns (values per item).
+ /// This is the required length of the passed to .
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets a value indicating whether any column has a baseline defined.
+ /// When the encoder can skip the baseline-subtraction branch entirely,
+ /// keeping the common-case encoding path branch-free.
+ ///
+ public bool HasBaselines { get; }
+
+ ///
+ /// Extracts and scales all column values from into the span.
+ /// Called once per item in the encoding hot loop. This method performs no heap allocation;
+ /// the caller is responsible for providing and owning the output buffer.
+ ///
+ /// The source item from which column values are extracted.
+ ///
+ /// Output buffer that receives the scaled values.
+ /// Its length must equal .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void GetValues(T item, Span values) {
+ var rules = _rules; // local copy avoids repeated bounds check on the field
+ for (var i = 0; i < rules.Length; i++) {
+ ref var rule = ref rules[i];
+ values[i] = (long)(rule.Select(item) * rule.Factor);
+ }
+ }
+
+ ///
+ /// Returns the baseline for the column at , or 0 if none is configured.
+ /// The encoder subtracts this value from the first item's scaled column value during encoding.
+ ///
+ /// The zero-based column index. Must be in the range [0, Width).
+ /// The baseline value, or 0 when no baseline has been defined for the column.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public long GetBaseline(int index) => _rules[index].Baseline ?? 0L;
+}
diff --git a/src/PolylineAlgorithm/PolylineOptions.cs b/src/PolylineAlgorithm/PolylineOptions.cs
new file mode 100644
index 00000000..f5c38ff7
--- /dev/null
+++ b/src/PolylineAlgorithm/PolylineOptions.cs
@@ -0,0 +1,50 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm;
+
+using System;
+
+///
+/// Provides configuration for a -driven encoding operation.
+///
+/// The source object type from which column values are extracted.
+///
+/// Combines a (which defines the column schema and scaling rules)
+/// with a (which controls buffer sizes, precision, and logging).
+///
+public sealed class PolylineOptions {
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ /// The sealed formatter that defines the column schema. Must not be .
+ ///
+ ///
+ /// The encoding options that control buffer sizes, precision, and logging.
+ /// Pass to use default options.
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ public PolylineOptions(PolylineFormatter formatter, PolylineEncodingOptions? encoding = null) {
+ if (formatter is null) {
+ throw new ArgumentNullException(nameof(formatter));
+ }
+
+ Formatter = formatter;
+ Encoding = encoding ?? new PolylineEncodingOptions();
+ }
+
+ ///
+ /// Gets the sealed formatter that defines the column schema and scaling rules.
+ ///
+ public PolylineFormatter Formatter { get; }
+
+ ///
+ /// Gets the encoding options that control buffer sizes, precision, and logging.
+ ///
+ public PolylineEncodingOptions Encoding { get; }
+}
diff --git a/src/PolylineAlgorithm/PublicAPI.Unshipped.txt b/src/PolylineAlgorithm/PublicAPI.Unshipped.txt
index 7dc5c581..32a0147c 100644
--- a/src/PolylineAlgorithm/PublicAPI.Unshipped.txt
+++ b/src/PolylineAlgorithm/PublicAPI.Unshipped.txt
@@ -1 +1,15 @@
#nullable enable
+PolylineAlgorithm.FormatterBuilder
+PolylineAlgorithm.FormatterBuilder.AddValue(string! name, System.Func! selector, uint precision = 5) -> PolylineAlgorithm.FormatterBuilder!
+PolylineAlgorithm.FormatterBuilder.Build() -> PolylineAlgorithm.PolylineFormatter!
+PolylineAlgorithm.FormatterBuilder.SetBaseline(long baseline) -> PolylineAlgorithm.FormatterBuilder!
+PolylineAlgorithm.PolylineFormatter
+PolylineAlgorithm.PolylineFormatter.GetBaseline(int index) -> long
+PolylineAlgorithm.PolylineFormatter.GetValues(T item, System.Span values) -> void
+PolylineAlgorithm.PolylineFormatter.HasBaselines.get -> bool
+PolylineAlgorithm.PolylineFormatter.Width.get -> int
+PolylineAlgorithm.PolylineOptions
+PolylineAlgorithm.PolylineOptions.Encoding.get -> PolylineAlgorithm.PolylineEncodingOptions!
+PolylineAlgorithm.PolylineOptions.Formatter.get -> PolylineAlgorithm.PolylineFormatter!
+PolylineAlgorithm.PolylineOptions.PolylineOptions(PolylineAlgorithm.PolylineFormatter! formatter, PolylineAlgorithm.PolylineEncodingOptions? encoding = null) -> void
+static PolylineAlgorithm.FormatterBuilder.Create() -> PolylineAlgorithm.FormatterBuilder!
diff --git a/tests/PolylineAlgorithm.Tests/PolylineFormatterTests.cs b/tests/PolylineAlgorithm.Tests/PolylineFormatterTests.cs
new file mode 100644
index 00000000..5102e23f
--- /dev/null
+++ b/tests/PolylineAlgorithm.Tests/PolylineFormatterTests.cs
@@ -0,0 +1,469 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm.Tests;
+
+using PolylineAlgorithm;
+using System;
+
+///
+/// Tests for , ,
+/// and .
+///
+[TestClass]
+public sealed class PolylineFormatterTests {
+ // ---------------------------------------------------------------------------
+ // FormatterBuilder.Create
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void Create_Returns_New_Builder() {
+ // Act
+ FormatterBuilder<(double X, double Y)> result = FormatterBuilder<(double X, double Y)>.Create();
+
+ // Assert
+ Assert.IsNotNull(result);
+ }
+
+ [TestMethod]
+ public void Create_With_Multiple_Invocations_Returns_Different_Instances() {
+ // Act
+ FormatterBuilder<(double X, double Y)> first = FormatterBuilder<(double X, double Y)>.Create();
+ FormatterBuilder<(double X, double Y)> second = FormatterBuilder<(double X, double Y)>.Create();
+
+ // Assert
+ Assert.AreNotSame(first, second);
+ }
+
+ // ---------------------------------------------------------------------------
+ // FormatterBuilder.AddValue — argument validation
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void AddValue_With_Null_Name_Throws_ArgumentNullException() {
+ // Arrange
+ FormatterBuilder<(double X, double Y)> builder = FormatterBuilder<(double X, double Y)>.Create();
+
+ // Act & Assert
+ ArgumentNullException ex = Assert.ThrowsExactly(
+ () => builder.AddValue(null!, static t => t.X));
+ Assert.AreEqual("name", ex.ParamName);
+ }
+
+ [TestMethod]
+ public void AddValue_With_Empty_Name_Throws_ArgumentException() {
+ // Arrange
+ FormatterBuilder<(double X, double Y)> builder = FormatterBuilder<(double X, double Y)>.Create();
+
+ // Act & Assert
+ ArgumentException ex = Assert.ThrowsExactly(
+ () => builder.AddValue(string.Empty, static t => t.X));
+ Assert.AreEqual("name", ex.ParamName);
+ }
+
+ [TestMethod]
+ public void AddValue_With_Null_Selector_Throws_ArgumentNullException() {
+ // Arrange
+ FormatterBuilder<(double X, double Y)> builder = FormatterBuilder<(double X, double Y)>.Create();
+
+ // Act & Assert
+ ArgumentNullException ex = Assert.ThrowsExactly(
+ () => builder.AddValue("X", null!));
+ Assert.AreEqual("selector", ex.ParamName);
+ }
+
+ [TestMethod]
+ public void AddValue_With_Duplicate_Name_Throws_ArgumentException() {
+ // Arrange
+ FormatterBuilder<(double X, double Y)> builder = FormatterBuilder<(double X, double Y)>.Create();
+ builder.AddValue("X", static t => t.X);
+
+ // Act & Assert
+ ArgumentException ex = Assert.ThrowsExactly(
+ () => builder.AddValue("X", static t => t.Y));
+ Assert.AreEqual("name", ex.ParamName);
+ }
+
+ // ---------------------------------------------------------------------------
+ // FormatterBuilder.AddValue — happy path & chaining
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void AddValue_Returns_Same_Builder_For_Method_Chaining() {
+ // Arrange
+ FormatterBuilder<(double X, double Y)> builder = FormatterBuilder<(double X, double Y)>.Create();
+
+ // Act
+ FormatterBuilder<(double X, double Y)> result = builder.AddValue("X", static t => t.X);
+
+ // Assert
+ Assert.AreSame(builder, result);
+ }
+
+ [TestMethod]
+ public void AddValue_With_Different_Names_Succeeds() {
+ // Arrange & Act
+ PolylineFormatter<(double X, double Y)> formatter = FormatterBuilder<(double X, double Y)>.Create()
+ .AddValue("X", static t => t.X)
+ .AddValue("Y", static t => t.Y)
+ .Build();
+
+ // Assert
+ Assert.AreEqual(2, formatter.Width);
+ }
+
+ // ---------------------------------------------------------------------------
+ // FormatterBuilder.SetBaseline — argument validation
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void SetBaseline_With_No_Rules_Throws_InvalidOperationException() {
+ // Arrange
+ FormatterBuilder<(double X, double Y)> builder = FormatterBuilder<(double X, double Y)>.Create();
+
+ // Act & Assert
+ Assert.ThrowsExactly(() => builder.SetBaseline(1000L));
+ }
+
+ // ---------------------------------------------------------------------------
+ // FormatterBuilder.SetBaseline — happy path
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void SetBaseline_Returns_Same_Builder_For_Method_Chaining() {
+ // Arrange
+ FormatterBuilder<(double X, double Y)> builder = FormatterBuilder<(double X, double Y)>.Create()
+ .AddValue("X", static t => t.X);
+
+ // Act
+ FormatterBuilder<(double X, double Y)> result = builder.SetBaseline(100L);
+
+ // Assert
+ Assert.AreSame(builder, result);
+ }
+
+ [TestMethod]
+ public void SetBaseline_Applies_Only_To_Last_Added_Rule() {
+ // Arrange & Act
+ PolylineFormatter<(double X, double Y)> formatter = FormatterBuilder<(double X, double Y)>.Create()
+ .AddValue("X", static t => t.X)
+ .AddValue("Y", static t => t.Y)
+ .SetBaseline(500L)
+ .Build();
+
+ // Assert — only Y (index 1) has a baseline; X (index 0) returns 0
+ Assert.AreEqual(0L, formatter.GetBaseline(0));
+ Assert.AreEqual(500L, formatter.GetBaseline(1));
+ }
+
+ [TestMethod]
+ public void SetBaseline_Can_Be_Called_On_Each_Rule() {
+ // Arrange & Act
+ PolylineFormatter<(double X, double Y)> formatter = FormatterBuilder<(double X, double Y)>.Create()
+ .AddValue("X", static t => t.X).SetBaseline(100L)
+ .AddValue("Y", static t => t.Y).SetBaseline(200L)
+ .Build();
+
+ // Assert
+ Assert.AreEqual(100L, formatter.GetBaseline(0));
+ Assert.AreEqual(200L, formatter.GetBaseline(1));
+ }
+
+ // ---------------------------------------------------------------------------
+ // FormatterBuilder.Build — validation
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void Build_With_No_Rules_Throws_InvalidOperationException() {
+ // Arrange
+ FormatterBuilder<(double X, double Y)> builder = FormatterBuilder<(double X, double Y)>.Create();
+
+ // Act & Assert
+ Assert.ThrowsExactly(() => builder.Build());
+ }
+
+ [TestMethod]
+ public void Build_With_Multiple_Invocations_Returns_Different_Instances() {
+ // Arrange
+ FormatterBuilder<(double X, double Y)> builder = FormatterBuilder<(double X, double Y)>.Create()
+ .AddValue("X", static t => t.X);
+
+ // Act
+ PolylineFormatter<(double X, double Y)> first = builder.Build();
+ PolylineFormatter<(double X, double Y)> second = builder.Build();
+
+ // Assert
+ Assert.AreNotSame(first, second);
+ }
+
+ // ---------------------------------------------------------------------------
+ // PolylineFormatter.Width
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void Width_Equals_Number_Of_Added_Rules() {
+ // Arrange & Act
+ PolylineFormatter<(double X, double Y, double Z)> formatter = FormatterBuilder<(double X, double Y, double Z)>.Create()
+ .AddValue("X", static t => t.X)
+ .AddValue("Y", static t => t.Y)
+ .AddValue("Z", static t => t.Z)
+ .Build();
+
+ // Assert
+ Assert.AreEqual(3, formatter.Width);
+ }
+
+ [TestMethod]
+ public void Width_Is_One_For_Single_Rule() {
+ // Arrange & Act
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v)
+ .Build();
+
+ // Assert
+ Assert.AreEqual(1, formatter.Width);
+ }
+
+ // ---------------------------------------------------------------------------
+ // PolylineFormatter.HasBaselines
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void HasBaselines_Is_False_When_No_Baselines_Are_Set() {
+ // Arrange & Act
+ PolylineFormatter<(double X, double Y)> formatter = FormatterBuilder<(double X, double Y)>.Create()
+ .AddValue("X", static t => t.X)
+ .AddValue("Y", static t => t.Y)
+ .Build();
+
+ // Assert
+ Assert.IsFalse(formatter.HasBaselines);
+ }
+
+ [TestMethod]
+ public void HasBaselines_Is_True_When_Any_Baseline_Is_Set() {
+ // Arrange & Act
+ PolylineFormatter<(double X, double Y)> formatter = FormatterBuilder<(double X, double Y)>.Create()
+ .AddValue("X", static t => t.X)
+ .AddValue("Y", static t => t.Y).SetBaseline(100L)
+ .Build();
+
+ // Assert
+ Assert.IsTrue(formatter.HasBaselines);
+ }
+
+ [TestMethod]
+ public void HasBaselines_Is_True_When_All_Baselines_Are_Set() {
+ // Arrange & Act
+ PolylineFormatter<(double X, double Y)> formatter = FormatterBuilder<(double X, double Y)>.Create()
+ .AddValue("X", static t => t.X).SetBaseline(10L)
+ .AddValue("Y", static t => t.Y).SetBaseline(20L)
+ .Build();
+
+ // Assert
+ Assert.IsTrue(formatter.HasBaselines);
+ }
+
+ // ---------------------------------------------------------------------------
+ // PolylineFormatter.GetBaseline
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void GetBaseline_Returns_Zero_When_No_Baseline_Configured() {
+ // Arrange
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v)
+ .Build();
+
+ // Act
+ long result = formatter.GetBaseline(0);
+
+ // Assert
+ Assert.AreEqual(0L, result);
+ }
+
+ [TestMethod]
+ public void GetBaseline_Returns_Configured_Baseline() {
+ // Arrange
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v)
+ .SetBaseline(42L)
+ .Build();
+
+ // Act
+ long result = formatter.GetBaseline(0);
+
+ // Assert
+ Assert.AreEqual(42L, result);
+ }
+
+ [TestMethod]
+ public void GetBaseline_Returns_Negative_Baseline() {
+ // Arrange
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v)
+ .SetBaseline(-1000L)
+ .Build();
+
+ // Act
+ long result = formatter.GetBaseline(0);
+
+ // Assert
+ Assert.AreEqual(-1000L, result);
+ }
+
+ // ---------------------------------------------------------------------------
+ // PolylineFormatter.GetValues
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void GetValues_Scales_Single_Column_By_Factor() {
+ // Arrange — precision 5 → factor = 100000; use a value exact in double arithmetic
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v, precision: 5)
+ .Build();
+
+ Span output = stackalloc long[1];
+
+ // Act — 38.5 * 100000 = 3850000 (exactly representable)
+ formatter.GetValues(38.5, output);
+
+ // Assert
+ Assert.AreEqual(3850000L, output[0]);
+ }
+
+ [TestMethod]
+ public void GetValues_Scales_Multiple_Columns_Independently() {
+ // Arrange
+ PolylineFormatter<(double Lat, double Lon)> formatter =
+ FormatterBuilder<(double Lat, double Lon)>.Create()
+ .AddValue("Lat", static t => t.Lat, precision: 5)
+ .AddValue("Lon", static t => t.Lon, precision: 5)
+ .Build();
+
+ Span output = stackalloc long[2];
+
+ // Act — 38.5 * 100000 = 3850000; -120.25 * 100000 = -12025000 (both exact in double)
+ formatter.GetValues((38.5, -120.25), output);
+
+ // Assert
+ Assert.AreEqual(3850000L, output[0]);
+ Assert.AreEqual(-12025000L, output[1]);
+ }
+
+ [TestMethod]
+ public void GetValues_With_Zero_Returns_Zero() {
+ // Arrange
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v, precision: 5)
+ .Build();
+
+ Span output = stackalloc long[1];
+
+ // Act
+ formatter.GetValues(0.0, output);
+
+ // Assert
+ Assert.AreEqual(0L, output[0]);
+ }
+
+ [TestMethod]
+ public void GetValues_With_Negative_Value_Returns_Negative_Scaled_Long() {
+ // Arrange
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v, precision: 5)
+ .Build();
+
+ Span output = stackalloc long[1];
+
+ // Act
+ formatter.GetValues(-90.0, output);
+
+ // Assert
+ Assert.AreEqual(-9000000L, output[0]);
+ }
+
+ [TestMethod]
+ public void GetValues_With_Custom_Precision_Scales_Correctly() {
+ // Arrange — precision 3 → factor = 1000
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v, precision: 3)
+ .Build();
+
+ Span output = stackalloc long[1];
+
+ // Act
+ formatter.GetValues(1.5, output);
+
+ // Assert — 1.5 * 1000 = 1500
+ Assert.AreEqual(1500L, output[0]);
+ }
+
+ // ---------------------------------------------------------------------------
+ // PolylineOptions constructor validation
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void PolylineOptions_With_Null_Formatter_Throws_ArgumentNullException() {
+ // Act & Assert
+ ArgumentNullException ex = Assert.ThrowsExactly(
+ () => _ = new PolylineOptions(null!));
+ Assert.AreEqual("formatter", ex.ParamName);
+ }
+
+ // ---------------------------------------------------------------------------
+ // PolylineOptions properties
+ // ---------------------------------------------------------------------------
+
+ [TestMethod]
+ public void PolylineOptions_Stores_Formatter() {
+ // Arrange
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v)
+ .Build();
+
+ // Act
+ PolylineOptions options = new(formatter);
+
+ // Assert
+ Assert.AreSame(formatter, options.Formatter);
+ }
+
+ [TestMethod]
+ public void PolylineOptions_With_Null_Encoding_Uses_Default_Options() {
+ // Arrange
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v)
+ .Build();
+
+ // Act
+ PolylineOptions options = new(formatter, null);
+
+ // Assert
+ Assert.IsNotNull(options.Encoding);
+ Assert.AreEqual(5u, options.Encoding.Precision);
+ Assert.AreEqual(512, options.Encoding.StackAllocLimit);
+ }
+
+ [TestMethod]
+ public void PolylineOptions_Stores_Custom_Encoding_Options() {
+ // Arrange
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v)
+ .Build();
+ PolylineEncodingOptions encoding = PolylineEncodingOptionsBuilder.Create()
+ .WithPrecision(7)
+ .WithStackAllocLimit(1024)
+ .Build();
+
+ // Act
+ PolylineOptions options = new(formatter, encoding);
+
+ // Assert
+ Assert.AreSame(encoding, options.Encoding);
+ Assert.AreEqual(7u, options.Encoding.Precision);
+ Assert.AreEqual(1024, options.Encoding.StackAllocLimit);
+ }
+}
From 2ebebaf79436221c26f79c1181333558af118b6c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 11:36:03 +0000
Subject: [PATCH 02/22] fix: add GetValues buffer-length validation and bounds
documentation
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/16db7628-2cc2-4c51-982b-0264a04d7157
Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
---
src/PolylineAlgorithm/PolylineFormatter.cs | 14 ++++++++-
.../PolylineFormatterTests.cs | 31 +++++++++++++++++++
2 files changed, 44 insertions(+), 1 deletion(-)
diff --git a/src/PolylineAlgorithm/PolylineFormatter.cs b/src/PolylineAlgorithm/PolylineFormatter.cs
index b290115c..3a073dfd 100644
--- a/src/PolylineAlgorithm/PolylineFormatter.cs
+++ b/src/PolylineAlgorithm/PolylineFormatter.cs
@@ -60,8 +60,17 @@ internal PolylineFormatter(FormatterRule[] rules) {
/// Output buffer that receives the scaled values.
/// Its length must equal .
///
+ ///
+ /// Thrown when .Length does not equal .
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void GetValues(T item, Span values) {
+ if (values.Length != Width) {
+ throw new ArgumentException(
+ $"Buffer length {values.Length} does not match the formatter width {Width}.",
+ nameof(values));
+ }
+
var rules = _rules; // local copy avoids repeated bounds check on the field
for (var i = 0; i < rules.Length; i++) {
ref var rule = ref rules[i];
@@ -73,7 +82,10 @@ public void GetValues(T item, Span values) {
/// Returns the baseline for the column at , or 0 if none is configured.
/// The encoder subtracts this value from the first item's scaled column value during encoding.
///
- /// The zero-based column index. Must be in the range [0, Width).
+ ///
+ /// The zero-based column index. Must be in the range [0, ).
+ /// An is thrown if the index is out of range.
+ ///
/// The baseline value, or 0 when no baseline has been defined for the column.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long GetBaseline(int index) => _rules[index].Baseline ?? 0L;
diff --git a/tests/PolylineAlgorithm.Tests/PolylineFormatterTests.cs b/tests/PolylineAlgorithm.Tests/PolylineFormatterTests.cs
index 5102e23f..3d2fdacb 100644
--- a/tests/PolylineAlgorithm.Tests/PolylineFormatterTests.cs
+++ b/tests/PolylineAlgorithm.Tests/PolylineFormatterTests.cs
@@ -353,6 +353,37 @@ public void GetValues_Scales_Multiple_Columns_Independently() {
Assert.AreEqual(-12025000L, output[1]);
}
+ [TestMethod]
+ public void GetValues_With_Wrong_Buffer_Length_Throws_ArgumentException() {
+ // Arrange — formatter has Width = 2 but buffer has length 1
+ PolylineFormatter<(double X, double Y)> formatter = FormatterBuilder<(double X, double Y)>.Create()
+ .AddValue("X", static t => t.X)
+ .AddValue("Y", static t => t.Y)
+ .Build();
+
+ long[] tooShort = new long[1];
+
+ // Act & Assert
+ ArgumentException ex = Assert.ThrowsExactly(
+ () => formatter.GetValues((1.0, 2.0), tooShort.AsSpan()));
+ Assert.AreEqual("values", ex.ParamName);
+ }
+
+ [TestMethod]
+ public void GetValues_With_Oversized_Buffer_Throws_ArgumentException() {
+ // Arrange — formatter has Width = 1 but buffer has length 3
+ PolylineFormatter formatter = FormatterBuilder.Create()
+ .AddValue("Value", static v => v)
+ .Build();
+
+ long[] tooLong = new long[3];
+
+ // Act & Assert
+ ArgumentException ex = Assert.ThrowsExactly(
+ () => formatter.GetValues(1.0, tooLong.AsSpan()));
+ Assert.AreEqual("values", ex.ParamName);
+ }
+
[TestMethod]
public void GetValues_With_Zero_Returns_Zero() {
// Arrange
From 7ea10cf32682addb53d38a02e827ce884171d253 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 14:23:37 +0000
Subject: [PATCH 03/22] Add formatter interfaces, PolylineValueFormatter,
PolylineFormatter static class, update base classes and options
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/66b5b819-3735-47b2-a2ec-372ae483e46b
Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
---
.../Abstraction/AbstractPolylineDecoder.cs | 132 ++++++++++++---
.../Abstraction/AbstractPolylineEncoder.cs | 157 +++++++++++++++---
.../Abstraction/IPolylineFormatter.cs | 42 +++++
.../Abstraction/IPolylineValueFormatter.cs | 55 ++++++
src/PolylineAlgorithm/FormatterBuilder.cs | 39 ++++-
.../Internal/DelegatePolylineFormatter.cs | 30 ++++
src/PolylineAlgorithm/PolylineFormatter.cs | 99 +++++------
src/PolylineAlgorithm/PolylineItemFactory.cs | 20 +++
src/PolylineAlgorithm/PolylineOptions.cs | 54 ++++--
.../PolylineValueFormatter.cs | 130 +++++++++++++++
src/PolylineAlgorithm/PublicAPI.Unshipped.txt | 38 +++--
11 files changed, 662 insertions(+), 134 deletions(-)
create mode 100644 src/PolylineAlgorithm/Abstraction/IPolylineFormatter.cs
create mode 100644 src/PolylineAlgorithm/Abstraction/IPolylineValueFormatter.cs
create mode 100644 src/PolylineAlgorithm/Internal/DelegatePolylineFormatter.cs
create mode 100644 src/PolylineAlgorithm/PolylineItemFactory.cs
create mode 100644 src/PolylineAlgorithm/PolylineValueFormatter.cs
diff --git a/src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs b/src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs
index 1059abb9..7971957a 100644
--- a/src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs
+++ b/src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs
@@ -14,13 +14,24 @@ namespace PolylineAlgorithm.Abstraction;
/// Provides a base implementation for decoding encoded polyline strings into sequences of geographic coordinates.
///
///
-/// Derive from this class to implement a decoder for a specific polyline type. Override
-/// and to provide type-specific behavior.
+///
+/// Formatter-based use (no subclassing required):
+/// Supply a via the
+///
+/// constructor. The formatters handle all type-specific concerns; override nothing.
+///
+///
+/// Legacy override-based use:
+/// Derive from this class and override and
+/// to provide type-specific behaviour. These overrides take priority over any registered formatter.
+///
///
/// The type that represents the encoded polyline input.
/// The type that represents a decoded geographic coordinate.
-public abstract class AbstractPolylineDecoder : IPolylineDecoder {
+public class AbstractPolylineDecoder : IPolylineDecoder {
private readonly ILogger> _logger;
+ private readonly IPolylineValueFormatter? _valueFormatter;
+ private readonly IPolylineFormatter? _polylineFormatter;
///
/// Initializes a new instance of the class with default encoding options.
@@ -48,6 +59,35 @@ protected AbstractPolylineDecoder(PolylineEncodingOptions options) {
.CreateLogger>();
}
+ ///
+ /// Initializes a new instance of the class
+ /// using the supplied .
+ ///
+ ///
+ /// Use this constructor when you want formatter-driven decoding without subclassing.
+ /// The and hooks are not called;
+ /// all type-specific logic is delegated to the formatters.
+ ///
+ ///
+ /// A that carries both the value formatter and
+ /// the polyline formatter together with the underlying .
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ public AbstractPolylineDecoder(PolylineOptions options) {
+ if (options is null) {
+ ExceptionGuard.ThrowArgumentNull(nameof(options));
+ }
+
+ Options = options.Encoding;
+ _polylineFormatter = options.PolylineFormatter;
+ _valueFormatter = options.ValueFormatter;
+ _logger = Options
+ .LoggerFactory
+ .CreateLogger>();
+ }
+
///
/// Gets the encoding options used by this polyline decoder.
///
@@ -78,6 +118,7 @@ protected AbstractPolylineDecoder(PolylineEncodingOptions options) {
///
/// Thrown when is canceled during decoding.
///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0051:Method is too long", Justification = "Method contains two path implementations.")]
public IEnumerable Decode(TPolyline polyline, CancellationToken cancellationToken = default) {
const string OperationName = nameof(Decode);
@@ -85,36 +126,67 @@ public IEnumerable Decode(TPolyline polyline, CancellationToken can
ValidateNullPolyline(polyline, _logger);
- ReadOnlyMemory sequence = GetReadOnlyMemory(in polyline);
+ ReadOnlyMemory sequence = (_valueFormatter is not null && _polylineFormatter is not null)
+ ? _polylineFormatter.Read(polyline)
+ : GetReadOnlyMemory(in polyline);
ValidateSequence(sequence, _logger);
ValidateFormat(sequence, _logger);
int position = 0;
- int encodedLatitude = 0;
- int encodedLongitude = 0;
- try {
- while (position < sequence.Length) {
- cancellationToken.ThrowIfCancellationRequested();
+ if (_valueFormatter is not null && _polylineFormatter is not null) {
+ int width = _valueFormatter.Width;
+ int[] accumulated = new int[width];
+ long[] longValues = new long[width];
+
+ try {
+ while (position < sequence.Length) {
+ cancellationToken.ThrowIfCancellationRequested();
- if (!PolylineEncoding.TryReadValue(ref encodedLatitude, sequence, ref position)
- || !PolylineEncoding.TryReadValue(ref encodedLongitude, sequence, ref position)) {
- _logger?.LogOperationFailedDebug(OperationName);
- _logger?.LogInvalidPolylineWarning(position);
+ for (int j = 0; j < width; j++) {
+ if (!PolylineEncoding.TryReadValue(ref accumulated[j], sequence, ref position)) {
+ _logger?.LogOperationFailedDebug(OperationName);
+ _logger?.LogInvalidPolylineWarning(position);
+ ExceptionGuard.ThrowInvalidPolylineFormat(position);
+ }
+ }
- ExceptionGuard.ThrowInvalidPolylineFormat(position);
+ for (int j = 0; j < width; j++) {
+ longValues[j] = accumulated[j];
+ }
+
+ yield return _valueFormatter.CreateItem(longValues.AsSpan());
}
+ } finally {
+ _logger?.LogOperationFinishedDebug(OperationName);
+ }
+ } else {
+ int encodedLatitude = 0;
+ int encodedLongitude = 0;
+
+ try {
+ while (position < sequence.Length) {
+ cancellationToken.ThrowIfCancellationRequested();
- double decodedLatitude = PolylineEncoding.Denormalize(encodedLatitude, Options.Precision);
- double decodedLongitude = PolylineEncoding.Denormalize(encodedLongitude, Options.Precision);
+ if (!PolylineEncoding.TryReadValue(ref encodedLatitude, sequence, ref position)
+ || !PolylineEncoding.TryReadValue(ref encodedLongitude, sequence, ref position)) {
+ _logger?.LogOperationFailedDebug(OperationName);
+ _logger?.LogInvalidPolylineWarning(position);
- _logger?.LogDecodedCoordinateDebug(decodedLatitude, decodedLongitude, position);
+ ExceptionGuard.ThrowInvalidPolylineFormat(position);
+ }
- yield return CreateCoordinate(decodedLatitude, decodedLongitude);
+ double decodedLatitude = PolylineEncoding.Denormalize(encodedLatitude, Options.Precision);
+ double decodedLongitude = PolylineEncoding.Denormalize(encodedLongitude, Options.Precision);
+
+ _logger?.LogDecodedCoordinateDebug(decodedLatitude, decodedLongitude, position);
+
+ yield return CreateCoordinate(decodedLatitude, decodedLongitude);
+ }
+ } finally {
+ _logger?.LogOperationFinishedDebug(OperationName);
}
- } finally {
- _logger?.LogOperationFinishedDebug(OperationName);
}
}
@@ -184,8 +256,16 @@ protected virtual void ValidateFormat(ReadOnlyMemory sequence, ILogger? lo
///
/// A of representing the encoded polyline characters.
///
+ ///
+ /// Thrown by the default implementation when no polyline formatter is registered and the method
+ /// has not been overridden in a derived class.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected abstract ReadOnlyMemory GetReadOnlyMemory(in TPolyline polyline);
+ protected virtual ReadOnlyMemory GetReadOnlyMemory(in TPolyline polyline) {
+ throw new NotSupportedException(
+ $"Override {nameof(GetReadOnlyMemory)} in a derived class, or provide a " +
+ $"{nameof(PolylineOptions)} with a polyline formatter.");
+ }
///
/// Creates a instance from the specified latitude and longitude values.
@@ -199,6 +279,14 @@ protected virtual void ValidateFormat(ReadOnlyMemory sequence, ILogger? lo
///
/// A instance representing the specified geographic coordinate.
///
+ ///
+ /// Thrown by the default implementation when no value formatter is registered and the method
+ /// has not been overridden in a derived class.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected abstract TCoordinate CreateCoordinate(double latitude, double longitude);
+ protected virtual TCoordinate CreateCoordinate(double latitude, double longitude) {
+ throw new NotSupportedException(
+ $"Override {nameof(CreateCoordinate)} in a derived class, or provide a " +
+ $"{nameof(PolylineOptions)} with a value formatter.");
+ }
}
diff --git a/src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs b/src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs
index 1bcdb0ee..3d6a169f 100644
--- a/src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs
+++ b/src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs
@@ -19,13 +19,25 @@ namespace PolylineAlgorithm.Abstraction;
/// Provides a base implementation for encoding sequences of geographic coordinates into encoded polyline strings.
///
///
-/// Derive from this class to implement an encoder for a specific coordinate and polyline type. Override
-/// , , and to provide type-specific behavior.
+///
+/// Formatter-based use (no subclassing required):
+/// Supply a via the
+///
+/// constructor. The formatter handles all type-specific concerns; override nothing.
+///
+///
+/// Legacy override-based use:
+/// Derive from this class and override , ,
+/// and to provide type-specific behaviour. These overrides take
+/// priority over any registered formatter.
+///
///
/// The type that represents a geographic coordinate to encode.
/// The type that represents the encoded polyline output.
-public abstract class AbstractPolylineEncoder : IPolylineEncoder {
+public class AbstractPolylineEncoder : IPolylineEncoder {
private readonly ILogger> _logger;
+ private readonly IPolylineValueFormatter? _valueFormatter;
+ private readonly IPolylineFormatter? _polylineFormatter;
///
/// Initializes a new instance of the class with default encoding options.
@@ -51,6 +63,35 @@ protected AbstractPolylineEncoder(PolylineEncodingOptions options) {
.CreateLogger>();
}
+ ///
+ /// Initializes a new instance of the class
+ /// using the supplied .
+ ///
+ ///
+ /// Use this constructor when you want formatter-driven encoding without subclassing.
+ /// The , , and hooks
+ /// are not called; all type-specific logic is delegated to the formatters.
+ ///
+ ///
+ /// A that carries both the value formatter and
+ /// the polyline formatter together with the underlying .
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ public AbstractPolylineEncoder(PolylineOptions options) {
+ if (options is null) {
+ ExceptionGuard.ThrowArgumentNull(nameof(options));
+ }
+
+ Options = options.Encoding;
+ _valueFormatter = options.ValueFormatter;
+ _polylineFormatter = options.PolylineFormatter;
+ _logger = Options
+ .LoggerFactory
+ .CreateLogger>();
+ }
+
///
/// Gets the encoding options used by this polyline encoder.
///
@@ -88,11 +129,15 @@ public TPolyline Encode(ReadOnlySpan coordinates, CancellationToken
ValidateEmptyCoordinates(ref coordinates, _logger);
+ if (_valueFormatter is not null && _polylineFormatter is not null) {
+ return EncodeWithFormatter(coordinates, cancellationToken);
+ }
+
CoordinateDelta delta = new();
int position = 0;
int consumed = 0;
- int length = GetMaxBufferLength(coordinates.Length);
+ int length = GetMaxBufferLength(coordinates.Length, 2);
char[]? temp = length <= Options.StackAllocLimit
? null
@@ -140,17 +185,6 @@ public TPolyline Encode(ReadOnlySpan coordinates, CancellationToken
return CreatePolyline(encodedResult.AsMemory());
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- static int GetMaxBufferLength(int count) {
- Debug.Assert(count > 0, "Count must be greater than zero.");
-
- int requestedBufferLength = count * 2 * Defaults.Polyline.Block.Length.Max;
-
- Debug.Assert(requestedBufferLength > 0, "Requested buffer length must be greater than zero.");
-
- return requestedBufferLength;
- }
-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void ValidateEmptyCoordinates(ref ReadOnlySpan coordinates, ILogger logger) {
if (coordinates.Length < 1) {
@@ -164,6 +198,56 @@ static void ValidateEmptyCoordinates(ref ReadOnlySpan coordinates,
}
}
+ ///
+ /// Encodes coordinates using the registered value and polyline formatters.
+ ///
+ private TPolyline EncodeWithFormatter(ReadOnlySpan coordinates, CancellationToken cancellationToken) {
+ const string OperationName = nameof(Encode);
+ int width = _valueFormatter!.Width;
+ int length = GetMaxBufferLength(coordinates.Length, 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;
+ int[] previous = new int[width];
+ long[] values = new long[width];
+ string encodedResult;
+
+ try {
+ for (var i = 0; i < coordinates.Length; i++) {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ _valueFormatter.GetValues(coordinates[i], values.AsSpan());
+
+ for (int j = 0; j < width; j++) {
+ int current = (int)values[j];
+ int delta = current - previous[j];
+ previous[j] = current;
+
+ if (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) {
+ _logger.LogOperationFailedDebug(OperationName);
+ _logger.LogCannotWriteValueToBufferWarning(position, i);
+ ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer();
+ }
+ }
+ }
+
+ encodedResult = buffer[..position].ToString();
+ } finally {
+ if (temp is not null) {
+ ArrayPool.Shared.Return(temp);
+ }
+ }
+
+ _logger.LogOperationFinishedDebug(OperationName);
+
+ return _polylineFormatter!.Write(encodedResult.AsMemory());
+ }
+
///
/// Creates a polyline instance from the provided read-only sequence of characters.
///
@@ -171,8 +255,16 @@ static void ValidateEmptyCoordinates(ref ReadOnlySpan coordinates,
///
/// An instance of representing the encoded polyline.
///
+ ///
+ /// Thrown by the default implementation when no polyline formatter is registered and the method
+ /// has not been overridden in a derived class.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected abstract TPolyline CreatePolyline(ReadOnlyMemory polyline);
+ protected virtual TPolyline CreatePolyline(ReadOnlyMemory polyline) {
+ throw new NotSupportedException(
+ $"Override {nameof(CreatePolyline)} in a derived class, or provide a " +
+ $"{nameof(PolylineOptions)} with a polyline formatter.");
+ }
///
/// Extracts the longitude value from the specified coordinate.
@@ -181,8 +273,16 @@ static void ValidateEmptyCoordinates(ref ReadOnlySpan coordinates,
///
/// The longitude value as a .
///
+ ///
+ /// Thrown by the default implementation when no value formatter is registered and the method
+ /// has not been overridden in a derived class.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected abstract double GetLongitude(TCoordinate current);
+ protected virtual double GetLongitude(TCoordinate current) {
+ throw new NotSupportedException(
+ $"Override {nameof(GetLatitude)} and {nameof(GetLongitude)} in a derived class, or " +
+ $"provide a {nameof(PolylineOptions)} with a value formatter.");
+ }
///
/// Extracts the latitude value from the specified coordinate.
@@ -191,7 +291,26 @@ static void ValidateEmptyCoordinates(ref ReadOnlySpan coordinates,
///
/// The latitude value as a .
///
+ ///
+ /// Thrown by the default implementation when no value formatter is registered and the method
+ /// has not been overridden in a derived class.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected abstract double GetLatitude(TCoordinate current);
-}
+ protected virtual double GetLatitude(TCoordinate current) {
+ throw new NotSupportedException(
+ $"Override {nameof(GetLatitude)} and {nameof(GetLongitude)} in a derived class, or " +
+ $"provide a {nameof(PolylineOptions)} with a value formatter.");
+ }
+
+ [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;
+ }
+}
diff --git a/src/PolylineAlgorithm/Abstraction/IPolylineFormatter.cs b/src/PolylineAlgorithm/Abstraction/IPolylineFormatter.cs
new file mode 100644
index 00000000..c7ba59e4
--- /dev/null
+++ b/src/PolylineAlgorithm/Abstraction/IPolylineFormatter.cs
@@ -0,0 +1,42 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm.Abstraction;
+
+using System;
+
+///
+/// Defines how to produce a from an encoded character buffer (output/write
+/// direction), and how to extract that buffer back from a (input/read
+/// direction).
+///
+/// The polyline surface type — for example or
+/// of .
+///
+///
+/// This interface is the polyline-surface counterpart to .
+/// The engine exclusively works with of internally.
+/// The formatter is the only code that touches .
+///
+///
+/// Use , , or
+/// to obtain a ready-made implementation.
+///
+///
+public interface IPolylineFormatter {
+ ///
+ /// Creates a from the encoded character buffer produced by the encoder.
+ ///
+ /// The encoded polyline as a read-only span of characters.
+ /// A wrapping or derived from .
+ TPolyline Write(ReadOnlyMemory encoded);
+
+ ///
+ /// Extracts the character buffer from a for the decoder to read.
+ ///
+ /// The polyline to read from.
+ /// A of representing the encoded characters.
+ ReadOnlyMemory Read(TPolyline polyline);
+}
diff --git a/src/PolylineAlgorithm/Abstraction/IPolylineValueFormatter.cs b/src/PolylineAlgorithm/Abstraction/IPolylineValueFormatter.cs
new file mode 100644
index 00000000..01b28ba2
--- /dev/null
+++ b/src/PolylineAlgorithm/Abstraction/IPolylineValueFormatter.cs
@@ -0,0 +1,55 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm.Abstraction;
+
+using System;
+
+///
+/// Defines how to extract scaled numeric values from a during encoding, and
+/// how to reconstruct a from those values during decoding.
+///
+/// The coordinate or item type that the formatter understands.
+///
+///
+/// This interface is the coordinate-side counterpart to .
+/// Together they allow the engine base classes to be used directly — without subclassing — by supplying
+/// both formatters via .
+///
+///
+/// Use to build a that
+/// already implements this interface.
+///
+///
+public interface IPolylineValueFormatter {
+ ///
+ /// Extracts and scales all column values from into the span.
+ /// Called once per item in the encoding loop.
+ ///
+ /// The source item from which column values are extracted.
+ ///
+ /// Output buffer that receives the scaled integer values. Its length must equal the number of columns
+ /// defined by this formatter.
+ ///
+ void GetValues(TValue item, Span values);
+
+ ///
+ /// Reconstructs a from the given scaled integer values.
+ /// Called once per decoded item in the decoding loop.
+ ///
+ ///
+ /// The accumulated scaled integer values decoded from the polyline. Each element corresponds to
+ /// the same column position as in .
+ ///
+ /// A reconstructed from .
+ TValue CreateItem(ReadOnlySpan values);
+
+ ///
+ /// Gets the number of values (columns) per encoded item.
+ /// This is the required length of the buffer passed to and
+ /// the length of the span received in .
+ ///
+ int Width { get; }
+}
diff --git a/src/PolylineAlgorithm/FormatterBuilder.cs b/src/PolylineAlgorithm/FormatterBuilder.cs
index 26021eaf..9a491c02 100644
--- a/src/PolylineAlgorithm/FormatterBuilder.cs
+++ b/src/PolylineAlgorithm/FormatterBuilder.cs
@@ -10,23 +10,25 @@ namespace PolylineAlgorithm;
using System.Collections.Generic;
///
-/// Provides a fluent builder for constructing a .
+/// Provides a fluent builder for constructing a .
///
/// The source object type from which column values are extracted.
///
///
/// Use to obtain an instance, call once per column,
/// optionally chain to specify an epoch for the most-recently added column,
-/// then call to produce the immutable .
+/// optionally chain to register a factory for the decoding direction,
+/// then call to produce the immutable .
///
///
-/// The builder is the only way to create a — its
+/// The builder is the only way to create a — its
/// constructor is internal.
///
///
public sealed class FormatterBuilder {
private readonly List> _rules = [];
private readonly HashSet _names = new(StringComparer.Ordinal);
+ private PolylineItemFactory? _create;
private FormatterBuilder() { }
@@ -101,19 +103,42 @@ public FormatterBuilder SetBaseline(long baseline) {
}
///
- /// Bakes all added rules into a sealed, immutable .
+ /// Registers a factory delegate used to reconstruct a from scaled values
+ /// during decoding. This enables the decoding direction of .
+ ///
+ ///
+ /// A delegate that accepts the scaled integer values decoded from the polyline and returns a
+ /// . The span length always equals the number of columns added via
+ /// .
+ ///
+ /// The current instance for method chaining.
+ ///
+ /// Thrown when is .
+ ///
+ public FormatterBuilder WithCreate(PolylineItemFactory create) {
+ if (create is null) {
+ throw new ArgumentNullException(nameof(create));
+ }
+
+ _create = create;
+
+ return this;
+ }
+
+ ///
+ /// Bakes all added rules into a sealed, immutable .
///
///
- /// An immutable whose rules can no longer be changed.
+ /// An immutable whose rules can no longer be changed.
///
///
/// Thrown when no rules have been added.
///
- public PolylineFormatter Build() {
+ public PolylineValueFormatter Build() {
if (_rules.Count == 0) {
throw new InvalidOperationException("At least one rule must be added before calling Build.");
}
- return new PolylineFormatter(_rules.ToArray());
+ return new PolylineValueFormatter(_rules.ToArray(), _create);
}
}
diff --git a/src/PolylineAlgorithm/Internal/DelegatePolylineFormatter.cs b/src/PolylineAlgorithm/Internal/DelegatePolylineFormatter.cs
new file mode 100644
index 00000000..82d44b39
--- /dev/null
+++ b/src/PolylineAlgorithm/Internal/DelegatePolylineFormatter.cs
@@ -0,0 +1,30 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm.Internal;
+
+using PolylineAlgorithm.Abstraction;
+using System;
+
+///
+/// A sealed delegate-backed implementation of .
+/// Produced by and the built-in factory properties.
+///
+/// The polyline surface type.
+internal sealed class DelegatePolylineFormatter : IPolylineFormatter {
+ private readonly Func, T> _write;
+ private readonly Func> _read;
+
+ internal DelegatePolylineFormatter(Func, T> write, Func> read) {
+ _write = write;
+ _read = read;
+ }
+
+ ///
+ public T Write(ReadOnlyMemory encoded) => _write(encoded);
+
+ ///
+ public ReadOnlyMemory Read(T polyline) => _read(polyline);
+}
diff --git a/src/PolylineAlgorithm/PolylineFormatter.cs b/src/PolylineAlgorithm/PolylineFormatter.cs
index 3a073dfd..d2be5198 100644
--- a/src/PolylineAlgorithm/PolylineFormatter.cs
+++ b/src/PolylineAlgorithm/PolylineFormatter.cs
@@ -5,88 +5,65 @@
namespace PolylineAlgorithm;
+using PolylineAlgorithm.Abstraction;
using PolylineAlgorithm.Internal;
using System;
-using System.Runtime.CompilerServices;
///
-/// Provides an immutable, sealed rule engine that describes how to extract and scale values from
-/// an object of type for polyline encoding.
+/// Provides static factory methods and ready-made instances of
+/// for the most common polyline surface types.
///
-/// The source object type from which column values are extracted.
///
///
-/// Instances of this class are constructed exclusively through .
-///
-///
-/// The modifier allows the JIT to devirtualise and inline calls to
-/// , eliminating vtable dispatch in the encoding hot loop.
+/// Use or for the two most common cases.
+/// Call to build a custom formatter from a pair of delegates.
///
///
-public sealed class PolylineFormatter {
- private readonly FormatterRule[] _rules;
-
- ///
- /// Initializes a new instance of with the baked rules.
- /// This constructor is intentionally internal; use to create instances.
- ///
- /// The pre-calculated rules array produced by the builder.
- internal PolylineFormatter(FormatterRule[] rules) {
- _rules = rules;
- Width = rules.Length;
- HasBaselines = Array.Exists(rules, static r => r.Baseline.HasValue);
- }
-
+public static class PolylineFormatter {
///
- /// Gets the number of columns (values per item).
- /// This is the required length of the passed to .
+ /// Gets a formatter that produces a from the encoded char buffer and reads
+ /// the buffer back via .
///
- public int Width { get; }
+ public static IPolylineFormatter ForString { get; } =
+ new DelegatePolylineFormatter(
+ static mem => new string(mem.Span),
+ static s => s.AsMemory());
///
- /// Gets a value indicating whether any column has a baseline defined.
- /// When the encoder can skip the baseline-subtraction branch entirely,
- /// keeping the common-case encoding path branch-free.
+ /// Gets a pass-through formatter for of .
+ /// Both Write and Read are identity operations.
///
- public bool HasBaselines { get; }
+ public static IPolylineFormatter> ForMemory { get; } =
+ new DelegatePolylineFormatter>(
+ static mem => mem,
+ static mem => mem);
///
- /// Extracts and scales all column values from into the span.
- /// Called once per item in the encoding hot loop. This method performs no heap allocation;
- /// the caller is responsible for providing and owning the output buffer.
+ /// Creates a custom from a pair of delegates.
///
- /// The source item from which column values are extracted.
- ///
- /// Output buffer that receives the scaled values.
- /// Its length must equal .
+ /// The polyline surface type.
+ ///
+ /// Converts the encoded of produced by the encoder
+ /// into a .
///
- ///
- /// Thrown when .Length does not equal .
+ ///
+ /// Extracts the encoded character buffer from a for the decoder to consume.
+ ///
+ /// A sealed backed by the supplied delegates.
+ ///
+ /// Thrown when or is .
///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void GetValues(T item, Span values) {
- if (values.Length != Width) {
- throw new ArgumentException(
- $"Buffer length {values.Length} does not match the formatter width {Width}.",
- nameof(values));
+ public static IPolylineFormatter Create(
+ Func, T> write,
+ Func> read) {
+ if (write is null) {
+ throw new ArgumentNullException(nameof(write));
}
- var rules = _rules; // local copy avoids repeated bounds check on the field
- for (var i = 0; i < rules.Length; i++) {
- ref var rule = ref rules[i];
- values[i] = (long)(rule.Select(item) * rule.Factor);
+ if (read is null) {
+ throw new ArgumentNullException(nameof(read));
}
- }
- ///
- /// Returns the baseline for the column at , or 0 if none is configured.
- /// The encoder subtracts this value from the first item's scaled column value during encoding.
- ///
- ///
- /// The zero-based column index. Must be in the range [0, ).
- /// An is thrown if the index is out of range.
- ///
- /// The baseline value, or 0 when no baseline has been defined for the column.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public long GetBaseline(int index) => _rules[index].Baseline ?? 0L;
+ return new DelegatePolylineFormatter(write, read);
+ }
}
diff --git a/src/PolylineAlgorithm/PolylineItemFactory.cs b/src/PolylineAlgorithm/PolylineItemFactory.cs
new file mode 100644
index 00000000..f977133e
--- /dev/null
+++ b/src/PolylineAlgorithm/PolylineItemFactory.cs
@@ -0,0 +1,20 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm;
+
+using System;
+
+///
+/// Represents a factory method that reconstructs a item from an array of
+/// scaled integer values decoded from a polyline.
+///
+/// The coordinate or item type to create.
+///
+/// The scaled integer values accumulated from the polyline decoder. Each element corresponds to one
+/// column as defined by the that built the associated formatter.
+///
+/// A instance reconstructed from .
+public delegate T PolylineItemFactory(ReadOnlySpan values);
diff --git a/src/PolylineAlgorithm/PolylineOptions.cs b/src/PolylineAlgorithm/PolylineOptions.cs
index f5c38ff7..d0189797 100644
--- a/src/PolylineAlgorithm/PolylineOptions.cs
+++ b/src/PolylineAlgorithm/PolylineOptions.cs
@@ -5,43 +5,67 @@
namespace PolylineAlgorithm;
+using PolylineAlgorithm.Abstraction;
using System;
///
-/// Provides configuration for a -driven encoding operation.
+/// Provides unified configuration for a formatter-driven encoding or decoding operation.
///
-/// The source object type from which column values are extracted.
+/// The coordinate or item type understood by the value formatter.
+/// The polyline surface type understood by the polyline formatter.
///
-/// Combines a (which defines the column schema and scaling rules)
-/// with a (which controls buffer sizes, precision, and logging).
+/// Combines an (which defines the column schema, scaling
+/// rules, and item factory) with an (which converts between
+/// the raw character buffer and the surface type) and a (which
+/// controls buffer sizes, precision for legacy paths, and logging).
///
-public sealed class PolylineOptions {
+public sealed class PolylineOptions {
///
- /// Initializes a new instance of .
+ /// Initializes a new instance of .
///
- ///
- /// The sealed formatter that defines the column schema. Must not be .
+ ///
+ /// The formatter that defines the column schema, scaling rules, and item factory. Must not be
+ /// .
+ ///
+ ///
+ /// The formatter that converts between the raw character buffer and
+ /// . Must not be .
///
///
/// The encoding options that control buffer sizes, precision, and logging.
/// Pass to use default options.
///
///
- /// Thrown when is .
+ /// Thrown when or is
+ /// .
///
- public PolylineOptions(PolylineFormatter formatter, PolylineEncodingOptions? encoding = null) {
- if (formatter is null) {
- throw new ArgumentNullException(nameof(formatter));
+ public PolylineOptions(
+ IPolylineValueFormatter valueFormatter,
+ IPolylineFormatter polylineFormatter,
+ PolylineEncodingOptions? encoding = null) {
+ if (valueFormatter is null) {
+ throw new ArgumentNullException(nameof(valueFormatter));
+ }
+
+ if (polylineFormatter is null) {
+ throw new ArgumentNullException(nameof(polylineFormatter));
}
- Formatter = formatter;
+ ValueFormatter = valueFormatter;
+ PolylineFormatter = polylineFormatter;
Encoding = encoding ?? new PolylineEncodingOptions();
}
///
- /// Gets the sealed formatter that defines the column schema and scaling rules.
+ /// Gets the formatter that defines the column schema, scaling rules, and item factory.
+ ///
+ public IPolylineValueFormatter ValueFormatter { get; }
+
+ ///
+ /// Gets the formatter that converts between the raw character buffer and
+ /// .
///
- public PolylineFormatter Formatter { get; }
+ public IPolylineFormatter PolylineFormatter { get; }
///
/// Gets the encoding options that control buffer sizes, precision, and logging.
diff --git a/src/PolylineAlgorithm/PolylineValueFormatter.cs b/src/PolylineAlgorithm/PolylineValueFormatter.cs
new file mode 100644
index 00000000..3c24701c
--- /dev/null
+++ b/src/PolylineAlgorithm/PolylineValueFormatter.cs
@@ -0,0 +1,130 @@
+//
+// Copyright © Pete Sramek. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace PolylineAlgorithm;
+
+using PolylineAlgorithm.Abstraction;
+using PolylineAlgorithm.Internal;
+using System;
+using System.Runtime.CompilerServices;
+
+///
+/// Provides an immutable, sealed rule engine that describes how to extract and scale values from
+/// an object of type for polyline encoding, and how to reconstruct an
+/// object of type from scaled values during decoding.
+///
+/// The source object type from which column values are extracted or to which they are reconstructed.
+///
+///
+/// Instances of this class are constructed exclusively through .
+///
+///
+/// The modifier allows the JIT to devirtualise and inline calls to
+/// and , eliminating vtable dispatch in the
+/// encoding/decoding hot loop.
+///
+///
+public sealed class PolylineValueFormatter : IPolylineValueFormatter {
+ private readonly FormatterRule[] _rules;
+ private readonly PolylineItemFactory? _create;
+
+ ///
+ /// Initializes a new instance of with the baked rules.
+ /// This constructor is intentionally internal; use to create instances.
+ ///
+ /// The pre-calculated rules array produced by the builder.
+ ///
+ /// An optional factory delegate that reconstructs a from scaled values.
+ /// Required for the decoding direction; if , throws.
+ ///
+ internal PolylineValueFormatter(FormatterRule[] rules, PolylineItemFactory? create = null) {
+ _rules = rules;
+ _create = create;
+ Width = rules.Length;
+ HasBaselines = Array.Exists(rules, static r => r.Baseline.HasValue);
+ }
+
+ ///
+ /// Gets the number of columns (values per item).
+ /// This is the required length of the passed to
+ /// and the length of the span received by .
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets a value indicating whether any column has a baseline defined.
+ /// When the encoder can skip the baseline-subtraction branch entirely,
+ /// keeping the common-case encoding path branch-free.
+ ///
+ public bool HasBaselines { get; }
+
+ ///
+ /// Gets a value indicating whether a factory delegate was supplied at build time.
+ /// When , calling throws an
+ /// .
+ ///
+ public bool CanCreateItem => _create is not null;
+
+ ///
+ /// Extracts and scales all column values from into the span.
+ /// Called once per item in the encoding hot loop. This method performs no heap allocation;
+ /// the caller is responsible for providing and owning the output buffer.
+ ///
+ /// The source item from which column values are extracted.
+ ///
+ /// Output buffer that receives the scaled values.
+ /// Its length must equal .
+ ///
+ ///
+ /// Thrown when .Length does not equal .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void GetValues(T item, Span values) {
+ if (values.Length != Width) {
+ throw new ArgumentException(
+ $"Buffer length {values.Length} does not match the formatter width {Width}.",
+ nameof(values));
+ }
+
+ var rules = _rules; // local copy avoids repeated bounds check on the field
+ for (var i = 0; i < rules.Length; i++) {
+ ref var rule = ref rules[i];
+ values[i] = (long)(rule.Select(item) * rule.Factor);
+ }
+ }
+
+ ///
+ /// Reconstructs a from the given scaled integer values.
+ ///
+ ///
+ /// The accumulated scaled integer values decoded from the polyline.
+ ///
+ /// A reconstructed from .
+ ///
+ /// Thrown when no factory delegate was supplied via .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public T CreateItem(ReadOnlySpan values) {
+ if (_create is null) {
+ throw new InvalidOperationException(
+ $"Cannot reconstruct an item because no factory was registered. " +
+ $"Call {nameof(FormatterBuilder)}.{nameof(FormatterBuilder.WithCreate)} before building.");
+ }
+
+ return _create(values);
+ }
+
+ ///
+ /// Returns the baseline for the column at , or 0 if none is configured.
+ /// The encoder subtracts this value from the first item's scaled column value during encoding.
+ ///
+ ///
+ /// The zero-based column index. Must be in the range [0, ).
+ /// An is thrown if the index is out of range.
+ ///
+ /// The baseline value, or 0 when no baseline has been defined for the column.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public long GetBaseline(int index) => _rules[index].Baseline ?? 0L;
+}
diff --git a/src/PolylineAlgorithm/PublicAPI.Unshipped.txt b/src/PolylineAlgorithm/PublicAPI.Unshipped.txt
index 32a0147c..8f358994 100644
--- a/src/PolylineAlgorithm/PublicAPI.Unshipped.txt
+++ b/src/PolylineAlgorithm/PublicAPI.Unshipped.txt
@@ -1,15 +1,33 @@
#nullable enable
PolylineAlgorithm.FormatterBuilder
PolylineAlgorithm.FormatterBuilder.AddValue(string! name, System.Func! selector, uint precision = 5) -> PolylineAlgorithm.FormatterBuilder!
-PolylineAlgorithm.FormatterBuilder.Build() -> PolylineAlgorithm.PolylineFormatter!
+PolylineAlgorithm.FormatterBuilder.Build() -> PolylineAlgorithm.PolylineValueFormatter!
PolylineAlgorithm.FormatterBuilder.SetBaseline(long baseline) -> PolylineAlgorithm.FormatterBuilder!
-PolylineAlgorithm.PolylineFormatter
-PolylineAlgorithm.PolylineFormatter.GetBaseline(int index) -> long
-PolylineAlgorithm.PolylineFormatter.GetValues(T item, System.Span values) -> void
-PolylineAlgorithm.PolylineFormatter.HasBaselines.get -> bool
-PolylineAlgorithm.PolylineFormatter.Width.get -> int
-PolylineAlgorithm.PolylineOptions
-PolylineAlgorithm.PolylineOptions.Encoding.get -> PolylineAlgorithm.PolylineEncodingOptions!
-PolylineAlgorithm.PolylineOptions.Formatter.get -> PolylineAlgorithm.PolylineFormatter!
-PolylineAlgorithm.PolylineOptions.PolylineOptions(PolylineAlgorithm.PolylineFormatter! formatter, PolylineAlgorithm.PolylineEncodingOptions? encoding = null) -> void
+PolylineAlgorithm.FormatterBuilder.WithCreate(PolylineAlgorithm.PolylineItemFactory! create) -> PolylineAlgorithm.FormatterBuilder!
+PolylineAlgorithm.Abstraction.IPolylineFormatter
+PolylineAlgorithm.Abstraction.IPolylineFormatter.Read(TPolyline polyline) -> System.ReadOnlyMemory
+PolylineAlgorithm.Abstraction.IPolylineFormatter.Write(System.ReadOnlyMemory encoded) -> TPolyline
+PolylineAlgorithm.Abstraction.IPolylineValueFormatter
+PolylineAlgorithm.Abstraction.IPolylineValueFormatter.CreateItem(System.ReadOnlySpan values) -> TValue
+PolylineAlgorithm.Abstraction.IPolylineValueFormatter.GetValues(TValue item, System.Span values) -> void
+PolylineAlgorithm.Abstraction.IPolylineValueFormatter.Width.get -> int
+PolylineAlgorithm.PolylineFormatter
+static PolylineAlgorithm.PolylineFormatter.ForMemory.get -> PolylineAlgorithm.Abstraction.IPolylineFormatter>!
+static PolylineAlgorithm.PolylineFormatter.ForString.get -> PolylineAlgorithm.Abstraction.IPolylineFormatter!
+static PolylineAlgorithm.PolylineFormatter.Create(System.Func, T>! write, System.Func>! read) -> PolylineAlgorithm.Abstraction.IPolylineFormatter!
+PolylineAlgorithm.PolylineItemFactory
+PolylineAlgorithm.PolylineValueFormatter
+PolylineAlgorithm.PolylineValueFormatter.CanCreateItem.get -> bool
+PolylineAlgorithm.PolylineValueFormatter.CreateItem(System.ReadOnlySpan