Skip to content

formula-cpp 0.4.0

Choose a tag to compare

@christianparpart christianparpart released this 05 Oct 05:29
6ef3b19

formula-cpp 0.4.0

The fourth release of formula-cpp, a header-only C++23 library for traceable formulas. You write a formula once
in C++. Dimensions are checked at compile time, units are converted exactly, and evaluation runs in exact rational
arithmetic. The same formula renders as documentation and traces step by step into an audit trail.

This release widens exact numbers to 128 bits and makes every number the library writes name its unit:

  • Wider exact numbers. Rational stores a new formula::Int128, so realistic laboratory statistics that
    overflowed 64 bits now answer. The logarithm and exponential take every argument a Rational holds.
  • A unit on every number. Every value a trace, a rendered formula, number_text or std::format writes now says
    which unit its number is in.

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), including an UndefinedBehaviorSanitizer build;
    • Clang on macOS.

What is new

  • 128-bit exact numbers.

    • formula::Int128: a signed 128-bit integer with one API on every compiler. GCC and Clang use the
      compiler's own 128-bit integer, and cl and clang-cl use portable constexpr code. It converts to no
      built-in integer, implicitly or explicitly. to_int64() and to_uint64() say when a value does not fit, and
      std::format writes it in decimal.

    • Rational stores its numerator and denominator in Int128. These now answer where they overflowed:

      • the sample variance of masses read to 6 decimal places of a gram;
      • rejection by standard deviations at that resolution;
      • a cylinder's strength at every diameter measured.

      The numeric headroom page measures how much room each fixture leaves.

  • Logarithms and exponentials take every argument a Rational holds.

    • The kernels behind rounded_ln, rounded_log10 and rounded_exp used to refuse arguments wider than 64 bits.
      ln 2^70 is now 48.5203 at 4 places.
    • rounded_exp answers up to 88.7, beyond which no result fits a Rational: e^45 to 18 places, e^88 to whole
      units.
    • The exponential computes with 192 fraction bits, so a result as wide as a Rational is still decided.
  • A unit on every value a trace writes.

    • Borrowed units. A computed value reads in its operand's unit where that is safe: the outlier-rejection limit
      #2 * #3 = 1239/500000 is now 1239/500 g. Otherwise it reads in the coherent unit, spelt from its base units:
      427/125000000 kg^2, 60000000 kg/(m s^2).
    • Inverse units. A unit with nothing above the slash is written with negative exponents, so 20000/413 kg^-1
      cannot be read as a fraction divided again.
    • Precision limits. A precision limit's first pass reads in its level's unit, as its second pass already
      did.
  • Units with no symbol. A dimensioned unit with no symbol is named by its size wherever a number refers to it:

    • a rounding reads round(#1, to 2 dp of 1/1000 kg), where it read round(#1, to 2 dp);
    • numeric(x, in 1/1000 kg) names the unit the same way;
    • render(), number_text and std::format write a value declared in such a unit in the coherent unit, as a
      trace does: 3/1000 kg, not a bare 3. This covers constants, table rows and bands, and limits too.

    In LaTeX the size is grouped, \operatorname{round}_{2\,(1/1000\,\mathrm{kg})}, and powers are raised:
    \mathrm{kg}^{-1}.

  • Diagnostics. explain and checked_explain given a series refuse it in the library's words, and point at
    explain_series instead of failing with "no matching function".

Upgrading from 0.3.0

A minor release may break the public API before 1.0, and this one does. The CHANGELOG lists every change under
Changed. These are the ones most likely to need an edit:

  • A dimensionless unit with a scale or an offset and no symbol no longer compiles. That covers a quantity's
    unit, a constant's, a rounding's, and a table's key or result. Such a unit printed one half in hundredths as a bare
    50. Give the unit a symbol (%, ppm, or your own), or declare the quantity in scale 1. The refusal reads
    formula: a dimensionless unit with a scale must have a symbol.
  • Rational::Int is Int128, and a Rational is 32 bytes.
    • Code that stored numerator() or denominator() in a built-in integer must narrow with to_int64(), which
      returns std::optional.
    • Rational's converting constructor takes every built-in integer of at most 64 bits except bool, and refuses
      a wider one.
    • band(Rational, Rational) and breakpoint(Rational) refuse a value that does not fit their 64-bit fields.
  • Trace texts changed. Text that a test pins changes wherever a trace showed a dimensioned value bare, an
    inverse unit as 1/kg, or a rounding in a unit with no symbol.
    • Step::unit now holds the unit a computed step borrowed.
    • Step gains lookupKeyHigh: a sample-size lookup's count can exceed 2^64 - 1.
  • Number text in a unit with no symbol. number_text and std::format of a Measured value in a dimensioned
    unit with no symbol write it in the coherent unit, 3/1000 kg or 0.003 kg, at that unit's places, where they
    wrote a bare 3.