Skip to content

feat(runtime): add an exact Decimal to the expression vocabulary - #270

Open
AmeinEskinder wants to merge 1 commit into
tokio-rs:mainfrom
AmeinEskinder:feat/decimal-surrogate
Open

feat(runtime): add an exact Decimal to the expression vocabulary#270
AmeinEskinder wants to merge 1 commit into
tokio-rs:mainfrom
AmeinEskinder:feat/decimal-surrogate

Conversation

@AmeinEskinder

Copy link
Copy Markdown
Contributor

Offered with a caveat up front: this adds a type to the shared vocabulary, which is a bigger ask than the other patches I've sent and a legitimate thing to say no to. It comes out of a real application rather than a wish, but the design opinion in it is mine, not yours.

The problem

Every number in a runtime expression is an f64, matching JavaScript. That is the right default and the wrong type for money. $(price.get() > limit) on a binary float is a defect waiting for the right input, and the failure is quiet — the comparison is simply wrong for some values and fine for the ones you tested.

What this adds

Decimal holds a decimal as its digits and compares it as digits, on both sides:

  • Comparison is numeric, not lexicographic: 1.5 equals 1.50, 10 is greater than 9, -0 equals 0.
  • is_zero, is_negative, to_string.
  • &str::to_decimal_or_zero() reads one out of an input field, yielding zero for the empty or half-typed value an input holds for most of its life rather than panicking mid keystroke.

There is deliberately no arithmetic. Comparing money in the browser is useful and safe; computing money that then gets stored belongs on the server. Leaving the operators out is what keeps that line visible in the type rather than in a comment — and it is also what keeps this small, since exact decimal arithmetic is a much larger thing to own.

Keeping the two sides honest

The Rust and TypeScript comparators are independent implementations of the same string algorithm, which is exactly the setup that drifts. They are pinned by a shared comparison table asserted on both sides:

["1.5", "1.50", "eq"], ["10", "9", "gt"], ["0.1", "0.11", "lt"],
["-5", "-4", "lt"],    ["-1", "0.5", "lt"], ["1234.50", "999.99", "gt"]

A change to one that the other does not make fails a test rather than producing a hydration mismatch.

Checks

cargo fmt --all --check, cargo clippy -p topcoat-runtime --all-targets --all-features (0 warnings), cargo test -p topcoat-runtime -p topcoat-runtime-macro -p topcoat-view-macro, yarn build, yarn test. dist/index.js is rebuilt, since the Rust side would otherwise expose a type the browser cannot hydrate.

If you'd rather not

Reasonable outcomes I'd be happy with: take to_decimal_or_zero and the type but rename it; take the idea and implement it your way; or decline it and I'll keep it downstream. I'd rather have the position recorded than the patch merged.

Every number in a runtime expression is an `f64`, matching JavaScript. That
is the right default and the wrong type for money: `0.1 + 0.2` is the
canonical example, and a price compared as a binary float is a defect
waiting for the right input.

`Decimal` is a decimal held as its digits and compared as digits, on both
sides. Comparison is numeric rather than lexicographic, so `1.5` equals
`1.50` and `10` is greater than `9`, and `-0` equals `0`. It carries
`is_zero`, `is_negative`, and `to_string`, and `&str::to_decimal_or_zero`
reads one out of an input field, yielding zero for the empty or half-typed
value an input holds for most of its life rather than panicking mid
keystroke.

It has no arithmetic, deliberately. Comparing money in the browser is
useful and safe; computing money that then gets stored belongs on the
server, and leaving the operators out is what keeps that line visible.

The two implementations are held together by a shared comparison table
asserted on both sides, so a change to one that the other does not make
fails rather than drifts.
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