|
| 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 | +} |
0 commit comments