Calculations and worksheets, decimal display with std::format, and application-declared base dimensions - #6
Merged
Merged
Conversation
added 30 commits
September 29, 2026 09:10
The units an energy calculation needs, and a second affine temperature unit, with the tests and documentation that list shipped units. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…enheit An energy calculation needs the units an electricity bill is written in, and none of them existed: there was an energy dimension but no power one, no watt or kilowatt-hour, and degrees Celsius was the only affine unit. dim::Power is energy per time. unit::Watt and unit::Kilowatt measure it; unit::WattHour (3600 J) and unit::KilowattHour (3600000 J) measure energy, so a power times a time converts into kilowatt-hours exactly and a power cannot be added to an energy. unit::Fahrenheit is a 5/9 K degree with an offset of 45967/180 K, exact in both directions: 32 degrees is 273.15 K, 212 degrees is 100 degrees Celsius, and 98.6 degrees is exactly 37. Nothing outside unit.hpp and dimension.hpp changes: every offset rule keys on a non-zero offset, not on Celsius. The tests pin the new units in the shipped table, the symbol list and the round-trip pairs, convert them in constant expressions, evaluate a heater into kilowatt-hours and a Fahrenheit reading into Celsius, and pin the trace lines of a kilowatt-hour product and of a series of Fahrenheit readings. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The comment on Celsius said a difference of one degree is a difference of one kelvin. That is true of the physics and false of this library, which converts points and never differences: one degree Celsius converts to 274.15 kelvin, as checked_convert's own comment says two hundred lines below. It now says what the library does. The comment on Fahrenheit and the changelog entry called every conversion "exact". The factors are exact rationals and nothing rounds, but checked_convert returns an error rather than a value when an intermediate does not fit, and 100 degrees Fahrenheit, exactly 340/9 degrees Celsius, is not a terminating decimal. Both now say "no conversion rounds" and show the fraction. The comment on checked_convert names Fahrenheit beside Celsius as the units that need an offset. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The guide still counted fifty-one units, listed no power dimension, and said this library ships one affine unit. It now counts fifty-six, names Power among the derived dimensions, explains the watt-hour family (a kilowatt-hour is exactly 3600000 J, and a power is a different dimension from an energy), and gives degrees Fahrenheit its own rows in the affine section: a point converts affinely, a difference does not, and 100 degF is 340/9 degC because converting divides by 9, kept as a fraction rather than a rounded 37.78 that would not round-trip. The expressions guide's "ships one" becomes two. The two citations that named test line numbers, both already stale, now quote the assertions they mean, so they cannot go stale by an edit above them. The dimensions example prints and checks -40 degF = -40 degC, 100 degF = 340/9 degC and 1 kWh = 3600000 J, and the guide's output blocks are copied from a real run. Its overflow-census row changes (3600000 needs 22 bits), so the census page is regenerated on cl; its remark that clang and gcc read one bit fewer for this example no longer holds, measured with g++ 13.3, g++ 14, clang++ 20 and 22 and clang-cl, which all print the same row as cl now, and is narrowed to the expressions example, which still does. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Comments and guides written in English mixed decimal commas ("273,15 K",
"0,45 m3", "1,25 -> 1,3") with decimal points, which reads as a list to anyone
used to the point and made the two conventions look like two different numbers.
Every decimal in the guides, the examples and the header comments now uses a
point. Diagnostics, code and thousands separators are untouched: the compiler
positions quoted in the lookup-table guide and "89,300 N" in the headroom page
are not decimals.
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Several guides pointed at the project's build stages ("phase 4's quantity
type", "a later phase's constraints", "the phase-11 spike") which mean nothing
to a reader of the library. Each now names the feature or links the guide.
Two of the sentences were also stale, not only labelled: expressions.md said
nothing produces a verdict or an invalidation yet, when outlier rejection and
bounded retry report a verdict through Outcome, and statistics.md said reading
another test's record and retrying were still to come, when they are the
records and retry guides.
The headroom page's remark about clang and gcc now says what was measured on the
expressions example: not one bit fewer but one bit more headroom (clang-cl and
g++ print 1/2/0/61 where cl prints 3/2/0/60, so two numerator bits fewer).
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The Celsius comment said this library does not convert differences. It is checked_convert that does not: a trace deliberately shows the spread of two readings in kelvin. The comment now says so. The dimensions example called kelvin one of "the scales the library has" while describing the two affine ones, and said 100 degF "is a fraction of a degree". It now says both affine scales, and that the number of degrees Celsius is a fraction. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The comments in three test files still wrote decimals with a comma ("273,15 K",
"98,6 degF", "0,18 m3", "3,04e9"), which the guides and headers no longer do.
Only comments change: no test logic and no expected value or string is touched.
Commas that are not decimals stay: index pairs such as (0,1), interval
endpoints such as [13.9,27.1), the template arguments in "RequireTagDeclaredOnce<1,3,",
and the thousands separator in "200,000".
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Let an application declare base dimensions the SI does not have, so that euros cannot be added to a bare ratio, euros and yen never convert into each other, and kWh times EUR/kWh is EUR. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A Dimension is about to carry base dimensions that an application names itself, and each of those names is a Symbol -- the same 16-byte structural string a Unit uses for its symbol. dimension.hpp cannot include unit.hpp to reach it, since unit.hpp includes dimension.hpp, so the type moves down a level. Moved verbatim from unit.hpp: SymbolCapacity, Symbol, detail::formula_unit_symbol_too_long, symbol(), view() and the deleted view(Symbol&&). unit.hpp includes dimension.hpp, so every existing spelling keeps compiling unchanged; unit.hpp drops <cstddef> and <cstdlib>, which only the moved code used, and dimension.hpp gains them with <string_view>. Two comments pointed at the old home: band.hpp cited the Bounds struct by a unit.hpp line number that no longer holds it, and now names Bounds instead; measured.hpp's cross-reference to view(Symbol&&) now says dimension.hpp. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A Dimension held the seven SI exponents and nothing else, so an
application that measures money had to call a currency dimensionless --
and the dimension system then let euros be added to a bare ratio. A
Dimension now also holds up to four base dimensions the application names
itself, and base_dimension("EUR") makes one. Each name is a dimension of
its own: euros and yen never convert into each other, since an exchange
rate is data, a quantity in yen per euro, and energy times euros per
energy is euros.
The operators keep the named bases canonical -- sorted by name, packed,
every unused slot equal to NamedBase {} -- by merging the two operands'
lists in one pass, so equal dimensions stay equal objects and therefore
one template argument, in every translation unit. A product that would
need a fifth base does not compile and names
formula_dimension_has_too_many_named_bases; base_dimension refuses an
empty name, one of 16 bytes or more, one that is not a letter followed by
letters or digits, and the symbol of an SI base unit, each naming its
rule. It is consteval, so a bad name can never reach run time.
Two g++ defects shape the code, both measured on g++ 13.3 and 14.2 and
absent on cl, clang-cl and clang++. With namedBases default-initialised
as {}, a constant evaluation that copies a dimension and writes the
copy's last slot corrupts the original, so the four elements are spelled
out. And a slot built from a const local keeps the const in its value
and becomes a different template argument, so the merge builds each slot
from a non-const one. The dimension tests fail on g++ if either is undone.
Dimension grows from 56 to 152 bytes, Unit from 152 to 248 and
Step<Rational> from 1008 to 1296 on 64-bit builds, and the pin on a
step's size moves with it. At that size g++ warns about a range loop
that copies a Dimension per iteration, so the one in opaque.hpp binds a
reference instead. RequireSameDimension's message now says the named
bases follow the seven exponents in the vectors it points at.
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
power and nth_root build their named-base slots themselves rather than through the merge, and their tests compared values only. Equality cannot see the g++ fault the merge's comment describes, where a slot built from a const local becomes a different template argument: rewriting either loop that way passed every test while making power(EUR, -1) a different type from Scalar / EUR on g++ 13.3 and 14.2. Both now have type-identity checks, including one over two named bases, and each fails on both g++ versions when its loop is rewritten that way. The division's capacity guard was unpinned, since both negative cases for too many named bases multiply. A new case divides four bases by a fifth; without the guard that quotient compiles and silently drops the fifth base. Comments: the merge's example of cancelling before counting is now one that fits only because of the cancellation; the note on the non-const local says what was measured rather than more; the EXPECT_COUNT explanation in test/CMakeLists.txt covers only the base-name cases it is about; and a comment moved into dimension.hpp no longer refers to dimension.hpp from inside it. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A dimension can now carry base dimensions an application names itself, and two parts of the trace predate them. coherent_unit_text, which spells the unit an opaque output is shown in when no input's unit fits, knew only the seven SI units, so a tariff in euros per joule would have read s^2/(m^2 kg). It now spells each named base by its name, ahead of the SI units on its side of the slash -- EUR s^2/(m^2 kg), 1/JPY, EUR/JPY, EUR^(1/2) -- and escapes each name as author text, since a hand-filled Dimension can hold any bytes. A dimension without named bases is spelt exactly as before. unit_quotient divided two units' dimensions with operator/, at run time, where that operator's guard against a fifth named base ends the program. It now merges them with detail::merged_dimension and offers no quotient when they do not fit, as it already offers none when a symbol or a magnitude would not fit; its one caller, opaque_output_unit, then falls back to the coherent unit. The places that ask whether a dimension is a pure number need no change, because a currency is not one. Two tests pin that: a critical value's count in euros is refused as not dimensionless, where a currency declared as a bare ratio compiled; and a series of prices scaled by a ratio reads in euros, where a bare-ratio currency lost its unit. The comments on coherent() and on Unit now say that the coherent unit is the SI unit times one of each named base, and the note in merged_dimension names exactly the const local that was measured unaffected. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
unit_quotient's comment said two units carrying more than four named bases between them have no quotient a Dimension can hold. That is false: a name both units carry cancels before the count, so a credit, a bonus and a coupon over a credit, a stamp and a token -- five names between them -- has a quotient with four. The comment now states the rule merged_dimension applies, and the unit_quotient test pins that case beside the one that does not fit. escaped_author_text's comment listed every place that applies it and ended "nowhere else"; coherent_unit_text, which now escapes a named base's name, joins the list. unit.hpp's file comment now says the coherent unit is the SI unit times one of each named base, as the other comments do, the changelog says how a trace spells a named base, and the new negative case's registration is set off by a blank line like its neighbours. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
unit_quotient's comment said a quotient is refused when it needs more than four named bases once the names both units carry have cancelled. A shared name cancels only when its exponents do: credits squared over credits leave credits, so that quotient keeps five names and is refused. The comment now says a shared name counts once, or not at all when its exponents cancel, and the unit_quotient test pins the squared case beside the one that cancels. escaped_author_text's comment said it is applied by step_line to a step's fields and nowhere else, but several lines escape author text the step does not hold: a rejection's verdict label, an opaque operation's and its outputs' names, a retry's verdict label and a named base's name. It now names those, and says every author text a trace line states is escaped on the way from step_line. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Named base dimensions exist so that money is checked like any other quantity. These tests follow a currency through every layer, with units of the tests' own, since the library ships none: euros, a cent as a hundredth of a euro, yen, and a tariff in euros per kilowatt-hour over the library's own unit::KilowattHour. Units: 250 EUR is exactly 25000 ct and back; euros never convert to yen, refused as a domain error both in a constant expression and at run time; yen round to whole yen and euros to the cent. A measurement in cents converts to euros exactly, and one in euros is refused for a quantity in yen, with or without a value. Formulas: 150 kWh at 3/10 EUR/kWh is exactly 45 EUR, the product's dimension being euros while the formula compiles; 10 EUR + 250 ct is 25/2 EUR; and 100 EUR at an exchange rate of 16235/100 JPY/EUR, supplied as a quantity like any other input, is 16235 JPY. A tariff unit spelt three ways links across two translation units. Four cases must not compile: a price plus a pure number, euros plus yen, a conversion from euros to yen, and a sum of euros evaluated into a quantity in yen. In each, the two sides' SI exponents are the same, so only the named base dimension explains the refusal; each is refused once. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… a slot built apart The tariff unit's three spellings all went through the merge, so a difference between slots the merge builds and slots built any other way -- the g++ 13.3 and 14.2 fault merged_dimension's comment describes -- would have split none of them. The definition in unit_cross_tu_b.cpp now combines two equal names in the merge and then raises the result to the first power, which builds every slot again outside it. With the merge's local made const again, both g++ versions then fail to link the call, the declaration's mangled name holding an `Exponent const`; clang++ and the unchanged code link. The test's comment says what fails where: a call spelt as another type does not compile, a definition spelt as one does not link. unit_quotient's comment also says that an exponent overflow is not judged: merged_dimension combines exponents through reduced, as the division operator always did, and its guard still ends the program for units whose exponents approach 2^31. Smaller: the evaluate test's comment claims a wrong factor on one of the two units is caught, not on both; the dimension constants read EuroAmount and YenAmount; and a comment in trace_render.hpp is wrapped evenly. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The composition example had to declare its euro as a bare ratio and say
so: the dimension system "will not stop you adding euros to a bare
ratio". The euro is now declared with base_dimension("EUR"), the caveat
is gone, and a static_assert states that its dimension is not the
scalar one, pointing at the negative test that pins the refusal of a
price plus a pure number. The example's arithmetic, output and trace are
unchanged byte for byte, so the guide that quotes them stays true.
The dimensions and units example prints a dimension's named bases after
its seven SI exponents, and gains a section on money: a tariff in euros
per energy (L^-2 M^-1 T^2 EUR^1), which times an energy is euros; 250
EUR is 25000 ct and converts back exactly; and 250 EUR to JPY is
refused as a domain error, since an exchange rate is data. Each printed
claim is also checked, and the checks join the example's summary. Its
earlier output is unchanged, and neither example's overflow census moved.
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
unit_quotient's comment, as "test(unit): make the tariff's identity across translation units catch a slot built apart" worded it, said an exponent overflow is reachable only by units whose dimensions carry exponents near 2^31. That is wrong: reduced also refuses a reduced denominator above INT32_MAX, and adding or subtracting two exponents multiplies their denominators. A length to the 1/46349 over a length to the -1/46351 needs 92700/2148322499, and unit_quotient ends the program on it -- measured on cl with a throwaway program, which printed its line before the call and nothing after. The comment now says the guard fires when a combined exponent, in lowest terms, would not fit std::int32_t: a very large numerator, or two denominators whose product passes 2^31. Smaller wording: the tariff's cross-translation-unit spelling reads "euros times euros, over euros times energy"; the dimensions example says the unit named after a base has magnitude one; and the too-many negative case's quantities are EuroAmount and YenAmount. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
A Dimension now holds up to four base dimensions the application names itself, and the guides still described it as the seven SI exponents alone. The dimensions guide gains a section on them, told through money: why a currency is not a bare number; base_dimension and identity by name, byte for byte, with the convention that makes it work -- the unit named after a base has magnitude one, a cent a hundredth; one base per currency, so euros never convert into yen and an exchange rate is data, a quantity in yen per euro; the name rules and the guard each one names; the capacity, counted after cancellation; and how a name is written in a coherent unit. Its output blocks are the dimensions example's own money lines, from a run. The same guide says where named bases appear in a dimension mismatch's diagnostic -- by name on g++, as character codes on cl, clang-cl and clang++, measured on all four -- replaces the hypothetical "if Dimension ever grew an eighth base quantity" with what happened when it did, and states the coherent unit, not the coherent SI unit, in the Units table. Limits gains the capacity, the name limits and the guard names. The expressions guide says the addition check refuses euros plus a number and euros plus yen, pointing at the two negative tests that pin it, and that the composed cost formula's price is in a dimension of its own. The series guide's coherent unit covers a named base, and the README and the documentation index mention base dimensions such as money. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The comments and guides called the unit every value is carried in "the coherent SI unit". For a dimension with a named base there is no such unit: euros per kilowatt-hour are carried in EUR s^2/(m^2 kg), the SI unit times one euro. Where a statement holds for any dimension -- a leaf converted on the way in, the value a step stores, a method's answer, an opaque operation's inputs, a series' elements -- it now says "the coherent unit", which evaluate.hpp, unit.hpp and the dimensions guide define as the SI unit times one of each named base. Left as they were, because "coherent SI" is still exact there: the comments on Metre, Pascal and the other coherent SI units in unit.hpp; prose about one physical quantity -- kelvin in the expressions guide, metres in the methods guide, kg^2 and Pa in the numeric-headroom guide, the SI units the dimensions guide lists; and the tests, which state it of physical dimensions only. The tracing guide's quotation of Step::unit's comment had drifted from the header before this change: it lacked the clauses on an overridden constant and on rounding steps, and it left out the paragraph on NumericValue while presenting the rest as consecutive. It is now the header's lines as they stand. The base_dimension and coherent() comments no longer say the unit named after a base "is one of it"; they say it has magnitude one, by convention. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ings The README and the documentation index listed named base dimensions as shipped, beside text saying 0.1.0 is usable for what is listed as shipped, while the changelog has them under Unreleased. The dimensions row is back to what 0.1.0 shipped, and a row of its own says named base dimensions come in the next release. The index also said series and statistics were planned, though 0.1.0 shipped them; its table now lists the same shipped rows as the README, which match 0.1.0's own. Wordings: - the dimensions guide gives the character codes a name is printed as in prose, since cl and clang space them differently; - a computed step does carry a unit, the coherent unit of its dimension, only no symbol of its own, and the expressions guide now says that; - each money addition fails to compile with exactly one error, which is what the two negative tests pin, not merely "is refused once"; - unit_quotient's comment said two denominators whose product passes 2^31 overflow, which reads as sufficient and is off by one: the product must still exceed INT32_MAX once in lowest terms; - the tracing guide's quotation of Step::unit ends where the paragraph it discusses ends, so it stays verbatim without bringing in NumericValueNode and sourceUnit, which the guide does not explain. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Quantities bound to the expressions that calculate them, a dependency graph checked at compile time, and a worksheet that recalculates only what a change reaches, with what-if copies, overrides and a derivation per named value. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
QuantityList and JoinQuantities lived inside the overlay header, where only the overlays could reach them. Code that needs to reason about which quantities an expression reads has to build, de-duplicate, subtract and index such lists too, and should not have to include the overlays to do it. Move both into detail/type_list.hpp, which overlay.hpp now includes, and add the list utilities that code needs: UniqueQuantities (each quantity once, in order of first appearance), QuantitiesWithout (a list minus another, order and repeats kept), quantity_count_v, quantity_index_v and QuantityAt. quantity_index_v yields the first position of a quantity, and the length of the list for one that is not listed, as Environment's own search for an entry does. A caller that must refuse an unlisted quantity compares with quantity_count_v and words its own refusal, so one mistake draws one message rather than a generic one on top of it. The overlay's local alias for how a derived quantity's definition is rewritten is renamed DefinitionRewrite: it names the rewrite rather than the definition, and it frees `Definition` for a public type of that name. No behaviour changes; every utility is covered by compile-time tests next to the existing index_in_tuple ones. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… at run time The variable evaluator decided where a read's value came from by the environment's static is_entered<Q>, and read the value through get<Q>(), which cannot carry a failure. An environment that works some of its values out from others must be able to say at run time that a value was calculated, and to let a calculation that failed fail the read, so that the failure reaches the formula as any operand's failure does. Two optional hooks, both backward compatible: - source_of<Q>() returning exactly ValueSource is now preferred over is_entered<Q>, for a variable and for the entry an overlay's constant or derived quantity replaced. Environment's answer agrees with is_entered for every quantity it holds, so no existing trace changes. - checked_get<Q>() returning exactly std::expected<Measured<Q>, ArithmeticError> is read instead of get<Q>() when present. A failed read is recorded at the variable's own step, after its source, and its parent relays it; an untaken when() branch never reads it. Only that exact return type is taken for the hook, so an unrelated member of the same name is never read by accident. The environments a precision limit, a rejection and a retry evaluate in forward each hook only when the environment they wrap has it: forwarding source_of unconditionally would make a consumer's environment without it fail to compile inside a traced precision limit. A variable whose value was calculated renders ", calculated", and one with no value "(no value)", since "(not measured)" would be false of it. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…cover Two constraints on the run-time source hook were not pinned by any test. A retry's attempt environment forwards source_of only when the environment it wraps has one. Every retry test wrapped an environment that does, so deleting that constraint passed the suite while breaking a consumer's explain_retry over an environment with only get and is_entered. A test now retries over such an environment: the attempt environment has neither hook, and every read's source is the one is_entered gives. The hook is taken only when source_of returns exactly ValueSource. The one lookalike tested returned an int, which a convertible_to check rejects as well, so loosening the check passed the suite. Two more lookalikes answer Derived -- one through a type that converts to ValueSource, one through a reference -- against an is_entered that says otherwise, and the trace keeps is_entered's answer for both. The concept's comment now says a reference or a converting type is not the hook, as its sibling's does. Environment::source_of's comment now says that an entered series is ManuallyEntered too, and that an overlay's replaced entry asks it as well as a variable does. The bound environment's comment no longer leans on how its member list was found; it lists what it forwards and states what it does not: get_observations, so raw observations read inside a precision limit's limit expression do not compile. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…, known at compile time Composing formulas today means embedding one expression tree in another: nothing names the quantity a sub-formula calculates, and nothing can say which values depend on which. Binding each quantity to the expression that calculates it is the first step towards a calculation whose dependency graph is known, and checked, before anything runs. define<Q>(expression) returns a Definition<Q, Expr>, in the new header calculation.hpp, which formula.hpp includes. Its reads -- each quantity the expression reads, once, in the order first read -- are worked out at compile time by walking the expression through the children every node kind already lists for a precision limit's checks, so a node kind the library ships is seen, or refused, in one place for both. A precision limit's limit expression is walked as well as its level, since both are evaluated against the same environment. A when() lists its condition and both branches: what it may read. An overlay's derived quantity reads what its definition reads, and a fixed constant reads nothing; the placeholders a construct binds and a retry's context read nothing either. A definition is refused where it is written, with one message each, when its expression measures a dimension other than the quantity's, is a series, reads a quantity as a series or as raw observations, reads from another record, or holds a node kind of a consumer's own that the walk cannot see inside -- a quantity read there would be missing from the graph and read out of date. A library kind without an entry of its own is refused in the existing words for that omission, and the consumer's-kind message does not follow it. A node refused already reads nothing and is asked nothing, so no second message follows a first. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…istry covers The definition's dimension check is not asked of an expression refused already, whose dimension is a stand-in. The one test of such an expression had the defined quantity's dimension, so deleting that gate passed the suite; it now defines a share by what arithmetic over a retry of a mass leaves. The negative cases of a series read, an observations read, a record read and an unseen node kind asked the definition whether it was valid, which walks the expression by itself; each now only calls define, so define alone must refuse. The case of a library kind without a children entry used what arithmetic over a retry leaves, which would stop meaning anything if that kind gained an entry; it now declares a node kind of its own in the library's namespace and reaches it through define. The header comment claimed a children entry for every node kind the library ships; three have none, and it now names them. It said a retry's context reads nothing from the environment, which attempt_input does inside a retry; it now says why the walk may list it as reading nothing: no definition can hold a retry, and outside one the evaluator refuses it. A single value made of the remaining series kinds -- a negation, a rounding per element, a running total, a splice of two curves -- is now walked too. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…eries over it A definition says which quantity an expression calculates and what it reads. Put several together and the question becomes which values depend on which -- what must be calculated first, what a changed input reaches -- and whether they can be calculated at all: a quantity defined twice, a definition that reads itself, or definitions that read one another in a circle have no answer, and should be refused where they are written rather than discovered when evaluated. calculation(define<A>(...), ...) builds a Calculation. Its inputs are what its definitions read and none of them defines, in the order first read; its graph -- what each quantity reads, what reads it, and both through any chain of reads -- is worked out at compile time over one 64-bit row per quantity, every algorithm at most N^2 word operations: 4,096 at 64 quantities, which a test of 64 builds with cl 19.51, g++ 13.3 and 14.2, and clang 20.1.8. Its dependency order lists the inputs, then each defined quantity once everything it reads comes before it, taking the one given first whenever several could come next: definitions given in dependency order keep their order, and others are reordered. dependencies_of, dependents_of, upstream_of, affected_by, inputs_of and calculation_order answer the symbols concerned, in dependency order, as the vocabulary they are given writes them; depends_on answers whether one quantity depends on another, in a constant expression. Refused where the calculation is written, one message each and each check asked only once those before it hold: an argument that is not a definition, no argument, a quantity defined twice, more than 64 quantities, a definition reading what it defines, and a cycle, all of whose quantities one message names. A calculation holding a definition refused where it was written is not judged further, and its queries answer nothing and say nothing; a query about a quantity the calculation neither defines nor reads is refused. The empty answer is a namespace-scope constant rather than an array value-initialised in a function template: cl does that through a helper of its own that declares a local named i, and a consumer's global of that name then drew warning C4459 through this header. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ches A calculation knows which of its values depends on which, but nothing yet holds the values: evaluating each definition by hand means evaluating a shared value once for every value that reads it, and after one input changes, evaluating everything again, although most of it cannot have changed. worksheet(calculation, environment(...)) builds a Worksheet, which takes the inputs from the environment and calculates each defined quantity when it is first asked for. calculate<Q>() answers an Outcome<Q>, throwing for a failed calculation, and checked_calculate<Q>() the std::expected; either takes several quantities, answering a tuple, or their variables: auto [total, vat] = sheet.calculate(var<Total>, var<Vat>). Each definition is evaluated by checked_evaluate, as it would be on its own, against a view of the worksheet that shows it only the quantities it declares it reads: a calculated value is read as an input is, its source Derived, a failed one fails the read as a failed operand would -- except on a when() branch not taken -- and a read the view does not show fails to compile rather than read a value not brought up to date. set(...) changes inputs, or overrides a calculated value by hand with entered(...); clear_override<Q>() drops an override; with(...) answers a changed copy. A change marks what it reaches, and nothing is calculated until something is asked. A value the change reached is calculated again only if something it reads changed since it was last brought up to date, and otherwise reused; a value calculated again to the same answer, from the same source, counts as unchanged, so what reads it is reused in turn. On the household bill, twice the fridge's power for half the hours calculates two values again and reuses the eight after them. recomputed() and reused() count both. The calculation's queries take a worksheet too. Tests compare every value of a worksheet changed step by step with one made from scratch, over four sequences of changes, and each value with checked_evaluate of its definitions inlined by hand, over seven sets of inputs; a small worksheet sets and recalculates inside a constant expression. Refused where it is written, one message each: an environment with no entry for an input -- asked only once every entry is accepted, so that an entry naming the wrong quantity draws one message, not two -- with an entry the calculation neither reads nor defines, with a series, or with a calculated quantity given as a measurement rather than entered; set() naming a quantity twice, and only then one the calculation does not hold, a series, or a calculated quantity given as a measurement; asking about a quantity the calculation does not hold; and clear_override of an input. A worksheet of a calculation refused already says nothing more. The CHANGELOG no longer says calculation_order lists the inputs: it lists the defined quantities, and inputs_of the inputs. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
added 7 commits
September 29, 2026 19:00
…r claims shown
The guide's comparison example was wrong under its own rule: 12.04 % is
an exact decimal, so it would never read ≈12.0 %. The second specimen
is now 289/24 %, which the trace's input line rounds to ≈12 % while the
comparison states 289/24 % against at most 12 %, violated.
"What goes wrong" named three guards but said when only one fires. A
table now gives each guard the specs that fire it and what to write
instead, including the one whose name misleads: places_out_of_range
for {:~HalfEven} on a unit declaring more than 18 decimals.
Claims the example stated but never showed are now its output: the
dish's mass is its weighings' sum times a typed 1/3, which the rounding
style still writes 1/3 in the trace and in the formula's text; the
padded trace keeps the weighings' second decimal; and a rejection of the
weighings shows document() writing its typed limit, 1/30 of the pass's
mean, as typed. A derived quantity's derivation, which needs a method
and an overlay, is linked to the guide that shows one rather than
built here.
Also:
- the snippets define what they use: the trace, w and notMeasured, the
measured values of the reference, and rat();
- the vformat snippet shows its catch, and print_spelled shows view();
- a paragraph on what each surface does with a number it cannot spell;
- links for checked_round, declared decimals and the vocabulary;
- typed_number_style in place of a spelling that kept the padding;
- the intro, a sentence that misparsed, and a computed-unit claim that
a series' sum contradicted, corrected;
- the reference measures a wet mass as wetMass, and its temperature
and grain size are plainly invented;
- the grammar's fill is one Unicode scalar value, in the guide and in
both specialisations, and its sample is the example's 157.4 g;
- format's @throws says only a value with no exact decimal of at most
18 places is rounded, and so can overflow;
- the README and docs index status tables list decimals and std::format
as next release.
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
… guard's line Three places the display guide and README quote could drift unnoticed: - The guide's spec table states a call and its output in each row, beside a checked block holding the same calls. CheckGuideOutput.cmake now takes CALL_TABLES: a table row whose last two cells are a `std::format(` call and its output must match a line the example prints. docs.display-output passes it; no other guide is judged so. - README's std::format block is fenced as text, and docs.readme-display-output judges README's text blocks against the display example. - CheckDocumentedDiagnostics.cmake gains a rule for MSVC's `note: see usage of '<name>'`: the header line it quotes must call `<name>(`. The guide's quote of the rounding-mode guard is pinned to the call, where before only a drift onto a blank or comment line failed. Each was shown to fail on a deliberately wrong line and pass once it was restored. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The display guide says {:~HalfEven} on a Measured whose unit declares
decimals outside -18 to 18 is refused as places out of range. Only the
upper side was tested through std::format. A unit declaring -19
decimals is now refused with the same message; without the lower check
it would throw later, as a value that cannot be spelled.
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…base A computed value is stated in the coherent unit of its dimension, and a dimension may now carry a named base such as money, whose coherent unit is no SI unit. The display guide said "coherent SI unit" of any computed value; it now says "coherent unit", as the rest of the documentation does. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
render_derivation took TraceRenderOptions but wrote every number as a fraction, whatever options.numbers said: its steps and input lines were rendered in NumberStyle::fraction(), and a header's value through fraction_text. It now honours the style exactly as render_trace does: - each step and input line is written as render_trace writes it in the style; - an input with no step of its own falls back to the value its block holds, spelled the same way; - each header's value is spelled as a trace line states a value in its unit -- rounded and marked, padded, or exact -- and exact where the block's root states a typed number, so that the header and the root's line never disagree; one the style cannot spell there reads "(not shown: ...)"; - each header's definition is rendered through the vocabulary carrying the style, so its typed numbers are exact and never padded, as in any rendered formula. A calculation's render and document already took RenderOptions through the generic overloads; tests now pin that their definitions' typed numbers follow it, and are never rounded or padded. describe_graph and to_dot print no number. Tests render a derivation in fractions, in exact decimals and rounded, where the header, the steps and the inputs agree and the typed 1/3 is never rounded; a header over a typed root; an input's fallback line; and the household bill rounded and padded. Undoing any one of the five threads fails a test. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…write instead
- CheckGuideOutput.cmake's CALL_TABLES counted the spec table's rows
but did not pin them: a row whose Output cell lost its backticks, or
whose Example cell stopped being a code span, dropped out of the check
silently. A table row that names std::format( in any other form is
now refused.
- CheckDocumentedDiagnostics.cmake's "see usage of" rule accepted a
comment line that names the call. The line must now call it in code.
- The guide's write-time refusal of {:~Mode} said why a value is refused
but not what to write instead. No spec rounds such a value to tens or
thousands: {:~.0HalfEven} rounds it to whole units, since 0 to 18
places are spelled by long division, which cannot overflow; {:/}
writes it exactly; or the std::format_error can be caught. A format
test pins {:~.0HalfEven} of from_double_exact(0.1) at -3 decimals as
≈0.
- The comparison example says why ≈12 % shows no decimal: 12.0, its zero
trimmed.
- The example's rejection, 130 columns wide, is wrapped under 125, in
the example and in the guide's quote of it.
Each check was shown to fail on a deliberately wrong line: the spec
table's row with its Output cell unquoted, the quoted guard call turned
into a comment, and the new format test expecting ≈1.
Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
render_derivation stated a typed value exactly in its own block's header and root, but a block reading it -- a Variable step whose source is Derived -- styled it as any computed value: under a rounding style the typed 2/3 read "2/3" in its block and "≈0.667" two lines away. The step alone cannot say which block it read: a vocabulary may write two quantities alike, and Step, whose size is pinned, holds no quantity. explain_worksheet now evaluates each definition through a view that notes every read's slot at the index of the step reading it -- a node reads the worksheet just before its own step is recorded -- and records them in the new WorksheetEntry::readSlots, one per step. render_derivation decides which blocks hold a typed value, walking them from the last so that every value a block reads is decided before it, and states a read of such a block, and anything that passes it on, exactly: on the reading line and in the header of a block that only passes it on. Also: - the header's "(not shown: ...)" branch, where a style cannot spell a value in its declared unit, is tested, under a padding and a rounding style, with the root spelling the value in metres and fractions unchanged; - the three places that spelled "(not shown: ...)", and two more, share one helper, not_shown_text; - the header's comment and the display guide say what is true: the header and a line reading its value agree, while the block's own root agrees on whether the value is rounded, not always on its unit or padding; - the vocabulary's comment says render_derivation wraps it on every call; - the input fallback is tested in kilowatt-hours under a padding style; - render(calc) and document(calc) are tested with a typed number in a unit that declares decimals, unpadded under a padding style; - the derivation tests' fixtures move into the file's one namespace. Each piece was undone in turn, and each failed a test: the typed read, the noting of reads, the "(not shown: ...)" branch, the fallback's unit and the typed style of a formula's numbers. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
added 3 commits
September 29, 2026 20:46
…nificant digit A value in a unit nobody declared -- a computed step, stated in the coherent unit of its dimension -- is rounded at that unit's default 3 places. For a price per energy that unit is euros per joule, and every such step read "≈0": marked, so not false, but saying nothing. A price of 3401/9300 EUR/kWh is 3401/33480000000 there. checked_shown_text now extends the places, where they round a value other than zero to "≈0", to the value's first significant digit, up to 18, and rounds there in the style's own mode: 3401/33480000000 reads "≈0.0000001", and 1/11250000 "≈0.00000009" rather than the "≈0.0000001" it rounds up to one place sooner. A value with no digit within 18 places still reads "≈0", and a unit someone declared keeps its declared places. Trace lines and a derivation's headers both spell through it. Tests pin the tariff in euros per joule and in a pure number, a negative value, the value that rounds up a place early, and what does not change: a value showing a digit at 3 places, a rounding the mode takes away from zero, a value beyond 18 places, a declared unit and the exact style. A derivation working a price out of a cost and an energy reads its division step as "≈0.00000009". Without the extension, or rounding at the first place the style leaves something at, both tests fail. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…example docs/dimensions.md quotes examples/dimensions_and_units.cpp's code and output, the money section's included, but nothing checked either: its output blocks were fenced as plain blocks, which CheckGuideOutput does not judge, and no test named the guide. Its nine output blocks are now fenced as text, and the two code blocks that are not from the example -- the computed mass rounded to its unit's precision, and exponent(1, 0), which must not compile -- carry the "not from the example" marker. docs.dimensions-output and docs.dimensions-snippets judge the guide as the statistics guide's tests judge theirs: nine blocks of 24 output lines, and four code blocks, all real. Each test was shown to fail on a deliberately wrong line -- 25000 ct quoted as 2500 ct, and a tariff per power rather than per energy -- and to pass once it was restored. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
formula::exact_decimal(Rational) returned a value's exact decimal as a std::optional<NumberText>, while NumberStyle::exact_decimal(padding) returns a style: one name, two meanings. The function is renamed exact_decimal_text, beside its siblings fraction_text, decimal_text and number_text, while it is still unreleased. Its callers, its comments, the tests and the consumer-globals probe follow, and the CHANGELOG now names it and has_exact_decimal. NumberStyle::exact_decimal keeps its name. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
christianparpart
marked this pull request as ready for review
September 29, 2026 19:09
added 15 commits
September 29, 2026 21:17
…t_decimal_text The public formula::exact_decimal_text(Rational) forwarded to formula::detail::exact_decimal_text(Rational, int), an overload in another namespace under the same name: one name for the public answer and for the helper that also pads to a minimum number of places. The helper is now detail::exact_decimal_digits, and its three callers -- exact_decimal_text, checked_decimal_text's path for whole tens and hundreds, and checked_number_text -- follow. Nothing public changes. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ery node that reads render_derivation states a typed value exactly where another block reads it, by the slot WorksheetEntry::readSlots notes for the reading step. That rests on one invariant: only a Variable step's entry is read, and the variable evaluator reads its value just before it records its own step. Nothing tested either half over the nodes that read a worksheet, and dropping the Variable-kind check in reads_typed_block left every test passing, although a derived quantity standing for a typed value then read as typed. A new calculation holds every node that reads the worksheet: a typed factor, a share whose derived factor stands for it but computes 1/3, when() taken both ways, a precision limit whose level reads the factor, a documented value, a rounding, and a share over a fixed factor. One test checks each step of each block: a Variable step notes its own quantity's slot, an overlay's step its own quantity's slot or none, and every other step none; and that the calculation holds each of those node kinds. A second renders the share under a rounding style: the derived factor's line reads "k = #3 = ≈0.333 [derived by ...]", while the typed factor read beside it stays "2/3". The derivation text and render_derivation's comments said a block's header and its root agree on whether the value is rounded. They agree only on whether it is typed: a computed root states the value in the coherent unit, so it may differ from the header in unit, padding and decimals, and one may read "≈" where the other does not -- 1 u, in a unit of a third of a metre, is ≈0.333 at its root. docs/display.md now says so, its paragraph reflowed; NotedWorksheetView's comment states the invariant above instead of a broader one; and readSlots' doc says to keep it in step with trace when editing either. Dropping the Variable-kind check fails the rendering test on "k = #3 = 1/3"; noting a read one step late fails the invariant test; asking the calculation for a node kind it does not hold fails the check on kinds. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…-digit rounding docs/dimensions.md's code and output are checked against its example, but its introduction said only that the output was copied. It now says what docs.dimensions-output and docs.dimensions-snippets check, as the statistics and display guides do, and what the "not from the example" marker means. The declared-precision block was marked although it is the example's own three lines plus a "// 450/7 kg" comment, which the output block below it prints anyway. The comment is gone and so is the marker, so the snippet test now checks it. Only the block showing that formula::exponent(1, 0) does not compile stays marked. The test of a rounding in a unit nobody declared gains five cases: - 1/10000001 at half-even: its first digit is at 8 places, where it rounds up, and reads "≈0.0000001", not a padded "≈0.00000010"; - 1/11250000 at floor: "≈0.00000008"; - the tariff at floor: "≈0.0000001"; - the negative tariff at ceiling: "≈-0.0000001"; - 1/300000000000000000 at half-even: a digit at the 18th place, "≈0.000000000000000003". docs/display.md's "keeps those 3 places" now says "(bar one exception, below)", which the next section explains. A CHANGELOG line of 127 columns is reflowed. Each control failed once and passed when undone: - the carry's expected text written padded; - the extended rounding padded; - the search stopping short of the 18th place; - the comment put back in the unmarked block. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…o calculations A calculation and a worksheet had an API and tests, but no program and no page a newcomer could read them from. examples/electricity_bill.cpp works a household's monthly bill through them, in the library's power and energy units and with currencies of its own, each a base dimension: the calculation and its graph, described, queried and drawn for Graphviz; a first run and four changes, with how many values each recalculated and reused -- 15 and 0, 5 and 0, none, 3 and 0, and 2 and 8 where the fridge's energy a day comes out the same; a derivation of that energy after the change; a what-if copy; a value typed in by hand and cleared; and a division by zero reaching the value that reads it. It checks every number against an exact one, and its pass expression pins the counts. docs/calculations.md is the guide: why a calculation rather than a composed formula, definitions and the graph known at compile time, worksheets, asking, changes and early cutoff, what-if copies, derivations and why they are never stale, overrides, failure and absence, each refusal with the message a real compile printed, and the limits. Its code blocks are checked against the example's source. It is linked from the composition section, the navigation, the index and the README, whose status table lists calculations for the next release. The numeric-headroom page gains the example's row. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…re declares A derivation test's comment wrote twelve hours as "43,200 s", which reads as a decimal comma to half of this library's readers; it now reads "43200 s". The shared bill fixture's comment said its money was dimensionless "as examples/composition.cpp explains", which that example no longer does: it now says the fixture keeps its own units and a bare number for money, and that the electricity bill example states the same bill in the library's units, with currencies of their own. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ailure on a calculation of its own The electricity bill had grown two values, a price paid per kilowatt-hour and its gross figure, only so that a division by zero had something to reach. They made its graph, its description and its DOT differ from the bill of ten inputs and fifteen calculated values the example states. The bill is back to those fifteen. The failure is shown on a second, small calculation in the same example: a cost shared among the people who live there, each share in whole cents. Shared by three, 98.00 EUR is 32.67 EUR each; shared by nobody, the share divides by zero and the share in cents, which reads it, fails with it, both checked and thrown, with the derivation saying so. The guide's section on failure quotes that calculation, and the pass expression pins its line. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…inned The electricity bill printed its numbers through a helper of its own, and its guide quoted none of its output, so nothing checked that what the page says the program prints is what it prints. The bill now spells its numbers as the library does. Each step's line is a std::format of the measured values -- the total padded to the cents it is rounded to, 98.00 EUR, the net draw as its exact decimal, 279 kWh -- and the calculation and both derivations are written in exact decimals: self_used = solar * 0.8, and the fridge at 0.4 kW and 400 W after the change that left its energy a day at 4.8 kWh. The guide quotes the program's output at each step: the calculation written out, the queries, the graph described, the DOT's opening and close, every step's line with its counts, the what-if copy, the value typed in by hand and its clearing, both derivations and the failure. docs.calculations-output checks each quoted block against the program's real output, and the pass expression now pins the four totals as well as the counts. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The status tables of the README and the documentation's index list what the next release brings -- named base dimensions, decimals, calculations -- but not the power and energy units and Fahrenheit, which it brings too. Both tables now have that row. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…nly said The derivation the guide shows is now the daily load's, which the fridge's change reused rather than calculated again: it reads the fridge's current 400 W and no 200 W, and recording it moves neither counter. The example checks all three, and the pass expression pins the daily load's first line right after its title. A bracket argument cannot spell a line break, so the expression is built with one spelled as CR? LF. The guide gains a section on the headers and names the example uses and one on its quantities; says what `counted` and `step` are, that `.2HalfAwayFromZero` rounds and pads and that std::format requires a mode; states the order the queries and describe_graph name values in; and shows the documentation page's symbol table, the grid cost's derivation with the value typed in and the step-limit footer, the bill's total read by the second calculation, a failure not calculated again, and an input nobody counted. It quotes three more refusals -- the same quantity set twice, a quantity set that the calculation does not hold, and a query about one -- and names the rest. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
`set` already had an rvalue form that answers the worksheet, changed, so that `worksheet(...).set(...)` can be kept or asked at once. `clear_override` now has one too, so a chain that sets an override can also clear it, and `std::move(sheet).clear_override<Q>()` hands the worksheet on. A test clears an override in such a chain and on a moved-from worksheet, and pins that the net draw and the five values built on it are calculated again. Returning the worksheet without clearing it fails that test. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…orksheet The consumer-globals probe now instantiates `render` and `document` with a `RenderOptions`, and a worksheet over quantities in a currency of the consumer's own -- a named base dimension -- asked, derived in exact decimals, formatted, and with an override cleared on a worksheet about to be discarded. The probe checks 80 properties, up from 78. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…t surface Nothing tested a calculation over a named base dimension. One case now calculates a charge, a total and a rate in an invented currency and its price per kilowatt-hour, and pins the rate's derivation in exact decimals, `describe_graph`, the documentation page's formula and each row's unit and definition, and `std::format` and `number_text` of values in euros and in euros per kilowatt-hour. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…claimed The second calculation now takes the bill's total through `checked_convert_to<SharedCost>` rather than re-wrapping its raw value: the value is converted exactly into the input's unit, an absent total stays absent, and a total of another dimension is refused. The guide says why the re-wrapped number would be wrong once the units differ. The example now also prints `inputs_of`, `calculation_order` and `dependencies_of<GridCost>`, and shows a value that fails again with the same error counting as unchanged: a new cost, still shared by nobody, calculates the share again and reuses the share in cents. The pass expression pins that line. It no longer names `calculation.hpp`, which `formula.hpp` brings. The guide defines inputs and calculated values where quantities are introduced, says what `Quantity`'s first argument is, says where the blocks it does not quote sit in the daily load's derivation, quotes how the failure's counter is read, and points the `when()` claims at the tests that pin them. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
…ry text function An unqualified call of the new text functions, or of `define`, `calculation` or `worksheet`, now also finds the library's function by argument-dependent lookup, which can make a consumer's own helper of the same name ambiguous. That breaks source code, so it moves from the number-text entry to "Changed", naming every function it applies to. The number-text entry now also names `fraction_text`, the checked forms `checked_number_text` and `checked_decimal_text`, and the constants `ApproximationMarker` and `NotMeasuredText`. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
The display guide names a worksheet's derivation twice and a calculation's rendering once. Each now links the section of the calculations guide that explains it. Signed-off-by: Christian Parpart <c.parpart@lastrada.net>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This brings the capabilities of the latest calculation-engine prototype into formula-cpp, built the formula-cpp way: exact rationals, dimensions checked at compile time, errors as values, and every result traceable.
What it adds
Calculations and worksheets (
calculation.hpp, guide:docs/calculations.md)define<Q>(expression)binds a quantity to its formula, andcalculation(...)groups the definitions. The dependency graph is known at compile time:static_assert(depends_on<Total, FridgeW>(bill)), plusdependencies_of,dependents_of,upstream_of,affected_by,inputs_ofandcalculation_order. Each mistake (a cycle, a self-reference, a duplicate, a dimension mismatch, …) gives oneformula:compile error.worksheet(calc, environment(...))holds a calculation's values and computes each one only when asked.calculatereturns one result or several, in a checked or a throwing form.setrecalculates only what a change reaches, and a value that recalculates to the same answer stops the change there (early cutoff).with()makes a what-if copy.entered()value overrides a calculated one by hand.recomputed()andreused()count the work.explain_worksheetandrender_derivationgive one derivation block per named value. A derivation is re-run against the worksheet's current values, so it is never stale, even after an early cutoff.render(calc),describe_graph,to_dot(Graphviz) anddocument(calc)write the calculation as text.examples/electricity_bill.cppis the prototype's household bill: 10 inputs and 15 calculated values, declaring its own currencies. It prints the prototype's totals and counts (118.26 / 95.02 / 98.00 / 85.14 EUR; recomputed 15/0, 5/0, 0/0, 3/0, 2/8; a what-if copy recomputes 9). After the fridge swap its derivation showsfridge_w = 400 W, where the prototype showed a stale value.Decimal display and
std::format(number_text.hpp,format.hpp, guide:docs/display.md)NumberStylecovers fractions (the default, unchanged), exact decimals, and approximations. An approximation must name a rounding mode, and it always carries≈.TraceRenderOptions::numbers),render()anddocument()(RenderOptions), and derivations all take the style. A number the author typed, and both sides of a stated comparison, are never approximated.number_text,decimal_textandexact_decimal_textneed neither allocation nor<format>, and work instatic_assert.typed_number_style(vocabulary)is the style a consumer's ownrender_nodeuses for a number the author typed.≈0for a nonzero value, it shows its first significant digit (up to 18 places) instead, still marked≈.#include <formula-cpp/format.hpp>(opt-in) addsstd::formatterforRationalandMeasured<Q>:{},{:/},{:.2HalfEven},{:~.3HalfEven}, fill, align and width in code points. A bad spec in a literal format string is a compile error that names the problem; throughstd::vformatit throwsstd::format_error("formula: …"). The guide's spec table, README's sample and the quoted compile error's source line are checked against the code in CI.Application-declared base dimensions (
base_dimension, guide:docs/dimensions.md)formula::base_dimension("EUR")declares a dimension the SI does not have. EUR + a ratio, or EUR + JPY, fails to compile, and kWh × EUR/kWh = EUR. A dimension carries up to four such bases.Units:
Watt,Kilowatt,WattHour,KilowattHour(withdim::Power) andFahrenheit.Cost of named base dimensions
Measured on cl and clang-cl, before (
ccb7796) and after:sizeof(Dimension)sizeof(Unit)sizeof(Step<Rational>)formula-cpp-testsbuild, clformula-cpp-testsbuild, clang-clTrace-heavy object files grow by about 25 %. Most of that is longer mangled names, because a unit's template argument now spells four name slots. On the heaviest test object, twice as many symbols exceed cl's 4096-character limit and are hashed.
Compatibility
docs/gallery.mdis untouched (only comments in its generator changed).detail::number_text(Rational)is removed, so no surface prints aRationalwithout choosing a style.number_text,fraction_text,define,worksheet, …) by argument-dependent lookup, so a consumer's own helper of the same name may become ambiguous; the CHANGELOG lists this under Changed.docs/index.mdlist this work as "next release".Verification
mkdocs build --strict.docs.*-output/docs.*-snippetstests, now includingdocs/dimensions.md.Follow-ups (not in this PR)
{:~Mode}on a value with a very large denominator, in a unit declaring negative decimals, refuses (formula: … overflow) even where the rounded result is 0. It is a refusal, never a wrong number.