Skip to content

formula-cpp 0.3.0

Choose a tag to compare

@christianparpart christianparpart released this 30 Sep 23:07
7d720b5

formula-cpp 0.3.0

The third release of formula-cpp, a header-only C++23 library for traceable formulas: written
once in C++, with dimensions checked at compile time and units converted exactly, evaluated in exact
rational arithmetic, rendered as documentation, and traced step by step into an audit trail.

This release gives a shorter spelling to everything the examples repeated, and takes no spelling
away: exact decimal literals, plain numbers in series, one call that reads a result's number or
returns a trace, std::format for results, a rounding named once, and a formula bound to its result
quantity. Every example is rewritten with them.

Documentation, with the guides, a gallery and the API reference:
https://lastrada-software.github.io/formula-cpp/

Requirements

  • C++23, CMake 3.23 or newer, and GCC 14 or newer.
  • CI builds and tests every change on MSVC cl and clang-cl (Windows), Clang and GCC 14
    (Linux, with an UndefinedBehaviorSanitizer build) and Clang on macOS.

What is new

  • Literals and inputs: 27.3_r is the exact Rational 273/10, where 27.3 is the double
    nearest it. A series takes plain numbers and not_measured beside measured values:
    measured_series<Determination>(41.3_r, 43.9_r, formula::not_measured, 45.7_r).
    breakpoint(12.7_r) and band(83.7_r, 97.3_r) take exact numbers too.
  • Reading results: number_of(x) is the number a result holds, or nothing, as a
    std::optional<Rational>, for a Measured, an Outcome, a checked_evaluate result and the
    other result types: number_of(checked_evaluate<Q>(...)) == 0.5_r. A Measured also has throwing
    twins, convert_to<R>, round_to_declared and within_bounds, for callers who would only
    rethrow.
  • Formatting: with <formula-cpp/format.hpp>, std::format("{}", outcome) writes an Outcome,
    a Unit, a Dimension and every enumeration that has a describe(). describe now covers
    ConstraintOutcomeKind, RetryEnd, ValueSource, OutcomeKind and FailureSite.
    symbol_of<Q>() needs no vocabulary, and render(x, options) no placeholder one:
    std::println("{} = {} ({})", formula::symbol_of<WaterCementRatio>(), *result, result->source()).
  • Traces: traced(evaluation) runs any evaluation that takes a sink and returns the outcome with
    its trace. explain_method<Tag>, explain_check_method, explain_check, explain_check_all,
    explain_conformity, explain_curve and explain_rejection are the traced twins of their verbs, and
    trace_of<Q>(expression, env) is the trace of an evaluation in one call, whether it succeeds or
    fails: render_trace(trace_of(mixCost, inputs), { .maxSteps = 20 }).
  • A rounding named once: DecimalRounding and SignificantRounding hold a unit, the places or
    digits, and a mode, where the three were repeated at every use.
    declared_rounding(unit, mode) takes the places the unit declares:
    constexpr formula::DecimalRounding wholeCents = formula::declared_rounding(EuroCent, formula::RoundingMode::HalfAwayFromZero);
    once, then rounded<wholeCents>(var<Subtotal> + var<Vat>).
  • Bound formulas: yields<Q>(expression) names a formula's result quantity once, where the
    formula is written: constexpr auto circularArea = yields<Area>(pi * pow<2>(var<Diameter>) / 4);
    then evaluate(circularArea, environment). Q is still checked against the dimension the
    expression computes, never deduced. Misusing a bound formula draws one formula: message, not an
    overload list. That covers a bound series or rejection handed to a verb that answers with one value,
    and a bound formula used as an operand or compared.
  • The examples themselves: 18 example programs, 4705 → 4402 lines; uses of a private rat()
    helper 155 → 0; printf 361 → 0 (now std::println); measurement().value() chains 60 → 0;
    RecordingSink in hand-built traces 23 → 0.

Upgrading from 0.2.0

A minor release may break the public API before 1.0, and this one does in a few places. Each change
is listed under Changed in the CHANGELOG. The ones most likely to need an edit:

  • Argument-dependent lookup. An unqualified call with this library's types as arguments now also
    finds the functions this release adds, among them describe, number_of, convert_to,
    round_to_declared, within_bounds, traced, trace_of, the explain_* twins, yields and
    declared_rounding. A consumer's function template of the same name and shape, such as a
    template <typename R, typename Q> Measured<R> convert_to(Measured<Q>) helper, is displaced
    silently: the library's is more constrained and is chosen, so the helper no longer runs, and
    where it returned an absent value on failure the library's convert_to throws
    ArithmeticException. A function of another shape, such as a non-template
    describe(ConstraintOutcomeKind) or a template taking its argument by value, makes the call
    ambiguous. A non-template taking exactly a library template's parameter types is preferred and
    keeps working. Rename the helper, remove it, or call it by a qualified name such as ::convert_to.
    And since _r is in an inline namespace of formula, using namespace formula; brings it into
    scope, where a consumer's own _r is ambiguous with it.
  • checked_convert_to across dimensions is now a compile error. Converting, say, a volume into
    a mass, or euros into yen, used to compile and return ArithmeticError::DomainError at run time.
    The message names the two quantities. A conversion within one dimension is unchanged.
  • A floating-point breakpoint or band argument is refused. breakpoint(1.5) used to be
    truncated silently to breakpoint(1). It now fails to compile, with a message that names the exact
    spelling for that call, such as 12.7_r, Rational { 127, 10 } or band(12.7_r, 17.3_r).
  • std::formatter specialisations may collide with yours. <formula-cpp/format.hpp> now
    specialises it for Outcome<Q>, Unit, Dimension and every enumeration that has a
    describe(). A program that defines its own for one of these now defines it twice, and a generic
    formatter for every enumeration is ambiguous for them; remove it and use the library's.
  • GCC 14 is required. Older GCC is not supported.
  • The with_rounding refusal text changed. With no citation it now opens
    formula: with_rounding<...>() was given no citation, for either spelling.

Using it

Install it with CMake, then consume it with find_package:

cmake -S . -B build -DFORMULA_INSTALL=ON -DCMAKE_INSTALL_PREFIX=/your/prefix
cmake --install build
find_package(formula-cpp 0.3 CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE formula-cpp::formula-cpp)

The package's compatibility is SameMinorVersion, so a request for 0.3 is not satisfied by 0.2 or
by 0.4. Because the library is header-only, copying include/ into your tree also works.

Known limits

  • No stable API before 1.0. A minor release may change the public API. Every change is recorded
    in the CHANGELOG.
  • 64-bit rational headroom (#1):
    the exact route of some realistic laboratory statistics still overflows 64 bits. It reports an
    overflow; it never gives a wrong number. The least-squares fits answer at a declared precision
    where their exact route overflows, within the width of their wider integers. The guide
    "Numeric headroom" measures where each computation stops.
  • Units of single-value arithmetic in a trace
    (#2): a computed single value, such
    as a product or a quotient, is still shown in its coherent SI value with no unit symbol.

The full list of changes is in CHANGELOG.md.