Skip to content

[major] Make SignificantNumber a value type - #88

Merged
matt-edmondson merged 1 commit into
mainfrom
significant-number-value-type
Sep 15, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
significant-number-value-type

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Summary

SignificantNumber builds against ktsu.PreciseNumber 2.x again. PreciseNumber 2.0 became a readonly record struct, and a struct can't be inherited, so record SignificantNumber : PreciseNumber no longer compiles against it. SignificantNumber is now a readonly record struct that holds a PreciseNumber and implements INumber<SignificantNumber> itself. Generic math conversion (T.CreateChecked and friends) works, and default(SignificantNumber) is a valid zero.

This is a major version, because every consumer that relied on SignificantNumber being a class or a PreciseNumber is affected. docs/migration-guide-2.0.md covers each change.

Changes

  • Composition instead of inheritance. The value lives in Value. A SignificantNumber converts to PreciseNumber implicitly, and the reverse is explicit because it opts into the significant figure rules. FromPreciseNumber and ToPreciseNumber() do the same as named methods.
  • Members that used to be inherited are declared on the struct and delegate to Value with the same return types: Exponent, Significand, SignificantDigits, Abs(), Round, ReduceSignificance, Clamp, Squared, Cubed, To<T>, the ToString overloads, TryFormat, and CompareTo(object). NegativeOne, E, Pi, and Tau are now typed as SignificantNumber, and there are static Max, Min, Clamp, and Round.
  • Conversions. TryConvertFrom* and TryConvertTo* delegate to PreciseNumber, and handle SignificantNumber and PreciseNumber first because PreciseNumber doesn't recognize SignificantNumber.
  • Rule helpers (LowestDecimalDigits, LowestSignificantDigits, and the check for -1, 0, or 1) are private statics, because PreciseNumber 2.0 made its versions internal.
  • Stack overflow removed. The explicit IUtf8SpanFormattable.TryFormat called itself. It's gone, and INumberBase's default implementation does the work.
  • Package. ktsu.PreciseNumber is pinned to 2.0.3, which includes the conversion and rounding fixes from PreciseNumber chore: update ktsu.Sdk to 2.21.1 [patch] #72.

Breaking changes

  • It's a struct, so it can't be null or derived from.
  • The protected constructors and the CreateFromComponents overload that takes sanitize are removed.
  • CompareTo(SignificantNumber?) is now CompareTo(SignificantNumber).
  • The four TryParse overloads and TryConvertFrom* use out SignificantNumber, and TryParse yields zero instead of null on failure.
  • Assigning a PreciseNumber to a SignificantNumber needs an explicit cast.
  • ToSignificantNumber() returns a copy instead of the same instance.
  • Static members inherited from PreciseNumber that aren't redeclared, such as MakeCommonized, are no longer available.

Testing

  • 95 tests pass on net10.0, up from 75 on main. The 20 new ones in SignificantNumberValueTypeTests cover default, both conversions with PreciseNumber, CreateChecked, CreateSaturating, and CreateTruncating from generic code, a generic Sum<T>, the static helpers, the TryParse failure path, CompareTo(object), and UTF-8 formatting.
  • Existing tests changed only where the value type forces it: IsNull on a failed parse became equality with Zero, AreSame became a value check, the unsanitized CreateFromComponents test is gone with the overload, and IsNotNull on Exp became a range check.
  • ToSignificantNumberWithDigits_FromSignificantNumber_ReducesSignificance reduces 123.456 to three digits and expects 123. It fails on PreciseNumber 2.0.1, which added 55… instead of 50… when rounding, and passes on 2.0.3.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K5Bk9UjGdGUtC5C6qK5ZxD

PreciseNumber 2.0 is a readonly record struct, and a struct can't be
inherited, so SignificantNumber can no longer derive from it. It is now a
readonly record struct that holds a PreciseNumber in its Value property and
implements INumber<SignificantNumber> itself.

The significant figure rules are unchanged. The helpers they relied on from
the base type (LowestDecimalDigits, LowestSignificantDigits, and the check
for operands of exactly -1, 0, or 1) are private statics here, because
PreciseNumber 2.0 made them internal. The members callers used to inherit,
such as Exponent, Significand, Round, ReduceSignificance, To<T>, and the
ToString overloads, are declared on the struct and delegate to the held
value with the same return types.

default(SignificantNumber) is zero, and a test pins it. A SignificantNumber
converts to a PreciseNumber implicitly, and the reverse is explicit because
it opts into the significant figure rules. TryParse now yields zero instead
of null on failure, and the TryConvertFrom and TryConvertTo members delegate
to PreciseNumber, so CreateChecked, CreateSaturating, and CreateTruncating
work in generic code, including to and from PreciseNumber.

This also removes the explicit IUtf8SpanFormattable.TryFormat, which called
itself and overflowed the stack. The implementation INumberBase provides
takes its place.

docs/migration-guide-2.0.md lists every breaking change.

Claude-Session: https://claude.ai/code/session_01K5Bk9UjGdGUtC5C6qK5ZxD
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
63.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@matt-edmondson
matt-edmondson merged commit 26f2209 into main Sep 15, 2026
12 of 13 checks passed
@matt-edmondson
matt-edmondson deleted the significant-number-value-type branch September 15, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant