Skip to content

Commit ee09102

Browse files
Copilotpetesramek
andauthored
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>
1 parent 0b16dcd commit ee09102

6 files changed

Lines changed: 785 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//
2+
// Copyright © Pete Sramek. All rights reserved.
3+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace PolylineAlgorithm;
7+
8+
using PolylineAlgorithm.Internal;
9+
using System;
10+
using System.Collections.Generic;
11+
12+
/// <summary>
13+
/// Provides a fluent builder for constructing a <see cref="PolylineFormatter{T}"/>.
14+
/// </summary>
15+
/// <typeparam name="T">The source object type from which column values are extracted.</typeparam>
16+
/// <remarks>
17+
/// <para>
18+
/// Use <see cref="Create"/> to obtain an instance, call <see cref="AddValue"/> once per column,
19+
/// optionally chain <see cref="SetBaseline"/> to specify an epoch for the most-recently added column,
20+
/// then call <see cref="Build"/> to produce the immutable <see cref="PolylineFormatter{T}"/>.
21+
/// </para>
22+
/// <para>
23+
/// The builder is the <em>only</em> way to create a <see cref="PolylineFormatter{T}"/> — its
24+
/// constructor is internal.
25+
/// </para>
26+
/// </remarks>
27+
public sealed class FormatterBuilder<T> {
28+
private readonly List<FormatterRule<T>> _rules = [];
29+
private readonly HashSet<string> _names = new(StringComparer.Ordinal);
30+
31+
private FormatterBuilder() { }
32+
33+
/// <summary>
34+
/// Creates a new <see cref="FormatterBuilder{T}"/> instance.
35+
/// </summary>
36+
/// <returns>A fresh <see cref="FormatterBuilder{T}"/> with no rules.</returns>
37+
public static FormatterBuilder<T> Create() => new();
38+
39+
/// <summary>
40+
/// Adds a column with the specified value selector and precision.
41+
/// </summary>
42+
/// <param name="name">
43+
/// A unique, non-null, non-empty name that identifies the column. Used for diagnostics only.
44+
/// </param>
45+
/// <param name="selector">
46+
/// A delegate that extracts the column's raw <see cref="double"/> value from an item of type
47+
/// <typeparamref name="T"/>.
48+
/// </param>
49+
/// <param name="precision">
50+
/// The number of decimal places to preserve. Each extracted value is multiplied by
51+
/// 10^<paramref name="precision"/> before encoding. Defaults to 5.
52+
/// </param>
53+
/// <returns>The current <see cref="FormatterBuilder{T}"/> instance for method chaining.</returns>
54+
/// <exception cref="ArgumentNullException">
55+
/// Thrown when <paramref name="name"/> or <paramref name="selector"/> is <see langword="null"/>.
56+
/// </exception>
57+
/// <exception cref="ArgumentException">
58+
/// Thrown when <paramref name="name"/> is empty, or a rule with the same name already exists.
59+
/// </exception>
60+
public FormatterBuilder<T> AddValue(string name, Func<T, double> selector, uint precision = 5) {
61+
if (name is null) {
62+
throw new ArgumentNullException(nameof(name));
63+
}
64+
65+
if (name.Length == 0) {
66+
throw new ArgumentException("Name cannot be empty.", nameof(name));
67+
}
68+
69+
if (selector is null) {
70+
throw new ArgumentNullException(nameof(selector));
71+
}
72+
73+
if (!_names.Add(name)) {
74+
throw new ArgumentException($"A rule with the name '{name}' has already been added.", nameof(name));
75+
}
76+
77+
_rules.Add(new FormatterRule<T>(name, (long)Pow10.GetFactor(precision), selector));
78+
79+
return this;
80+
}
81+
82+
/// <summary>
83+
/// Sets a baseline (epoch) on the most-recently added column.
84+
/// During encoding the baseline is subtracted from the first item's scaled column value,
85+
/// keeping the initial delta small when the absolute first value is large.
86+
/// </summary>
87+
/// <param name="baseline">The baseline value to apply to the first item's column value.</param>
88+
/// <returns>The current <see cref="FormatterBuilder{T}"/> instance for method chaining.</returns>
89+
/// <exception cref="InvalidOperationException">
90+
/// Thrown when no rules have been added yet. Call <see cref="AddValue"/> before <see cref="SetBaseline"/>.
91+
/// </exception>
92+
public FormatterBuilder<T> SetBaseline(long baseline) {
93+
if (_rules.Count == 0) {
94+
throw new InvalidOperationException("Cannot set a baseline when no rules have been added. Call AddValue first.");
95+
}
96+
97+
var last = _rules[^1];
98+
_rules[^1] = new FormatterRule<T>(last.Name, last.Factor, last.Select, baseline);
99+
100+
return this;
101+
}
102+
103+
/// <summary>
104+
/// Bakes all added rules into a sealed, immutable <see cref="PolylineFormatter{T}"/>.
105+
/// </summary>
106+
/// <returns>
107+
/// An immutable <see cref="PolylineFormatter{T}"/> whose rules can no longer be changed.
108+
/// </returns>
109+
/// <exception cref="InvalidOperationException">
110+
/// Thrown when no rules have been added.
111+
/// </exception>
112+
public PolylineFormatter<T> Build() {
113+
if (_rules.Count == 0) {
114+
throw new InvalidOperationException("At least one rule must be added before calling Build.");
115+
}
116+
117+
return new PolylineFormatter<T>(_rules.ToArray());
118+
}
119+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// Copyright © Pete Sramek. All rights reserved.
3+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace PolylineAlgorithm.Internal;
7+
8+
using System;
9+
10+
/// <summary>
11+
/// Represents a single column rule baked into a <see cref="PolylineFormatter{T}"/>.
12+
/// Stores the pre-calculated factor and an optional baseline alongside the user-supplied value selector.
13+
/// </summary>
14+
/// <typeparam name="T">The source object type from which the column value is extracted.</typeparam>
15+
internal sealed class FormatterRule<T> {
16+
/// <summary>
17+
/// Initializes a new instance of <see cref="FormatterRule{T}"/>.
18+
/// </summary>
19+
/// <param name="name">The column name used for diagnostics.</param>
20+
/// <param name="factor">The pre-calculated scaling factor (10^precision).</param>
21+
/// <param name="select">The delegate that extracts the raw value from an item.</param>
22+
/// <param name="baseline">The optional baseline value applied to the first item only.</param>
23+
internal FormatterRule(string name, long factor, Func<T, double> select, long? baseline = null) {
24+
Name = name;
25+
Factor = factor;
26+
Select = select;
27+
Baseline = baseline;
28+
}
29+
30+
/// <summary>
31+
/// Gets the column name. Used for diagnostics and duplicate-name detection only.
32+
/// </summary>
33+
internal string Name { get; }
34+
35+
/// <summary>
36+
/// Gets the pre-calculated scaling factor (10^precision).
37+
/// Stored as <see cref="long"/> so that <c>(long)(value * Factor)</c> stays in 64-bit arithmetic
38+
/// throughout the encoding hot loop without additional casting.
39+
/// </summary>
40+
internal long Factor { get; }
41+
42+
/// <summary>
43+
/// Gets the optional baseline (epoch). When set, the encoder subtracts this value from the
44+
/// first point's scaled column value to keep the initial delta small.
45+
/// </summary>
46+
internal long? Baseline { get; }
47+
48+
/// <summary>
49+
/// Gets the delegate that extracts the column's raw <see cref="double"/> value from an item of type
50+
/// <typeparamref name="T"/>. Stored as a concrete delegate so the JIT can inline the call site.
51+
/// </summary>
52+
internal Func<T, double> Select { get; }
53+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// Copyright © Pete Sramek. All rights reserved.
3+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace PolylineAlgorithm;
7+
8+
using PolylineAlgorithm.Internal;
9+
using System;
10+
using System.Runtime.CompilerServices;
11+
12+
/// <summary>
13+
/// Provides an immutable, sealed rule engine that describes how to extract and scale values from
14+
/// an object of type <typeparamref name="T"/> for polyline encoding.
15+
/// </summary>
16+
/// <typeparam name="T">The source object type from which column values are extracted.</typeparam>
17+
/// <remarks>
18+
/// <para>
19+
/// Instances of this class are constructed exclusively through <see cref="FormatterBuilder{T}"/>.
20+
/// </para>
21+
/// <para>
22+
/// The <see langword="sealed"/> modifier allows the JIT to devirtualise and inline calls to
23+
/// <see cref="GetValues"/>, eliminating vtable dispatch in the encoding hot loop.
24+
/// </para>
25+
/// </remarks>
26+
public sealed class PolylineFormatter<T> {
27+
private readonly FormatterRule<T>[] _rules;
28+
29+
/// <summary>
30+
/// Initializes a new instance of <see cref="PolylineFormatter{T}"/> with the baked rules.
31+
/// This constructor is intentionally internal; use <see cref="FormatterBuilder{T}"/> to create instances.
32+
/// </summary>
33+
/// <param name="rules">The pre-calculated rules array produced by the builder.</param>
34+
internal PolylineFormatter(FormatterRule<T>[] rules) {
35+
_rules = rules;
36+
Width = rules.Length;
37+
HasBaselines = Array.Exists(rules, static r => r.Baseline.HasValue);
38+
}
39+
40+
/// <summary>
41+
/// Gets the number of columns (values per item).
42+
/// This is the required length of the <see cref="Span{T}"/> passed to <see cref="GetValues"/>.
43+
/// </summary>
44+
public int Width { get; }
45+
46+
/// <summary>
47+
/// Gets a value indicating whether any column has a baseline defined.
48+
/// When <see langword="false"/> the encoder can skip the baseline-subtraction branch entirely,
49+
/// keeping the common-case encoding path branch-free.
50+
/// </summary>
51+
public bool HasBaselines { get; }
52+
53+
/// <summary>
54+
/// Extracts and scales all column values from <paramref name="item"/> into the <paramref name="values"/> span.
55+
/// Called once per item in the encoding hot loop. This method performs no heap allocation;
56+
/// the caller is responsible for providing and owning the output buffer.
57+
/// </summary>
58+
/// <param name="item">The source item from which column values are extracted.</param>
59+
/// <param name="values">
60+
/// Output buffer that receives the scaled values.
61+
/// Its length must equal <see cref="Width"/>.
62+
/// </param>
63+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
64+
public void GetValues(T item, Span<long> values) {
65+
var rules = _rules; // local copy avoids repeated bounds check on the field
66+
for (var i = 0; i < rules.Length; i++) {
67+
ref var rule = ref rules[i];
68+
values[i] = (long)(rule.Select(item) * rule.Factor);
69+
}
70+
}
71+
72+
/// <summary>
73+
/// Returns the baseline for the column at <paramref name="index"/>, or <c>0</c> if none is configured.
74+
/// The encoder subtracts this value from the first item's scaled column value during encoding.
75+
/// </summary>
76+
/// <param name="index">The zero-based column index. Must be in the range <c>[0, Width)</c>.</param>
77+
/// <returns>The baseline value, or <c>0</c> when no baseline has been defined for the column.</returns>
78+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
79+
public long GetBaseline(int index) => _rules[index].Baseline ?? 0L;
80+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// Copyright © Pete Sramek. All rights reserved.
3+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace PolylineAlgorithm;
7+
8+
using System;
9+
10+
/// <summary>
11+
/// Provides configuration for a <see cref="PolylineFormatter{T}"/>-driven encoding operation.
12+
/// </summary>
13+
/// <typeparam name="T">The source object type from which column values are extracted.</typeparam>
14+
/// <remarks>
15+
/// Combines a <see cref="PolylineFormatter{T}"/> (which defines the column schema and scaling rules)
16+
/// with a <see cref="PolylineEncodingOptions"/> (which controls buffer sizes, precision, and logging).
17+
/// </remarks>
18+
public sealed class PolylineOptions<T> {
19+
/// <summary>
20+
/// Initializes a new instance of <see cref="PolylineOptions{T}"/>.
21+
/// </summary>
22+
/// <param name="formatter">
23+
/// The sealed formatter that defines the column schema. Must not be <see langword="null"/>.
24+
/// </param>
25+
/// <param name="encoding">
26+
/// The encoding options that control buffer sizes, precision, and logging.
27+
/// Pass <see langword="null"/> to use default options.
28+
/// </param>
29+
/// <exception cref="ArgumentNullException">
30+
/// Thrown when <paramref name="formatter"/> is <see langword="null"/>.
31+
/// </exception>
32+
public PolylineOptions(PolylineFormatter<T> formatter, PolylineEncodingOptions? encoding = null) {
33+
if (formatter is null) {
34+
throw new ArgumentNullException(nameof(formatter));
35+
}
36+
37+
Formatter = formatter;
38+
Encoding = encoding ?? new PolylineEncodingOptions();
39+
}
40+
41+
/// <summary>
42+
/// Gets the sealed formatter that defines the column schema and scaling rules.
43+
/// </summary>
44+
public PolylineFormatter<T> Formatter { get; }
45+
46+
/// <summary>
47+
/// Gets the encoding options that control buffer sizes, precision, and logging.
48+
/// </summary>
49+
public PolylineEncodingOptions Encoding { get; }
50+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,15 @@
11
#nullable enable
2+
PolylineAlgorithm.FormatterBuilder<T>
3+
PolylineAlgorithm.FormatterBuilder<T>.AddValue(string! name, System.Func<T, double>! selector, uint precision = 5) -> PolylineAlgorithm.FormatterBuilder<T>!
4+
PolylineAlgorithm.FormatterBuilder<T>.Build() -> PolylineAlgorithm.PolylineFormatter<T>!
5+
PolylineAlgorithm.FormatterBuilder<T>.SetBaseline(long baseline) -> PolylineAlgorithm.FormatterBuilder<T>!
6+
PolylineAlgorithm.PolylineFormatter<T>
7+
PolylineAlgorithm.PolylineFormatter<T>.GetBaseline(int index) -> long
8+
PolylineAlgorithm.PolylineFormatter<T>.GetValues(T item, System.Span<long> values) -> void
9+
PolylineAlgorithm.PolylineFormatter<T>.HasBaselines.get -> bool
10+
PolylineAlgorithm.PolylineFormatter<T>.Width.get -> int
11+
PolylineAlgorithm.PolylineOptions<T>
12+
PolylineAlgorithm.PolylineOptions<T>.Encoding.get -> PolylineAlgorithm.PolylineEncodingOptions!
13+
PolylineAlgorithm.PolylineOptions<T>.Formatter.get -> PolylineAlgorithm.PolylineFormatter<T>!
14+
PolylineAlgorithm.PolylineOptions<T>.PolylineOptions(PolylineAlgorithm.PolylineFormatter<T>! formatter, PolylineAlgorithm.PolylineEncodingOptions? encoding = null) -> void
15+
static PolylineAlgorithm.FormatterBuilder<T>.Create() -> PolylineAlgorithm.FormatterBuilder<T>!

0 commit comments

Comments
 (0)