By: dreev

Last updated: 2019-11-20

More user-oriented version now on the blog

Beeminder users really hate it when Beeminder tells them they need to floss their teeth 0.23 times or archive 6.8 emails or whatever nonsensical / physically impossible thing. I personally shrugged that off for years. Obviously you just mentally take the ceiling of that number, right? If you have to floss 0.23 times and your possible choices are flossing zero times and one time, then the requirement to floss 0.23 times is logically equivalent to a requirement to floss exactly once.

No. Bad. Illusion of transparency! At least one user, arguably more mathy than me, says that when he first saw something like “+0.23 due by midnight” he assumed that that implied the deadline for the actual +1 was some pro-rated amount of time after midnight. More commonly, users are genuinely baffled or walk away in disgust at what they see as blatant brokenness.

So, point taken. As in, let’s get those decimal points taken out. It’s a big deal.

There’s one case where we show fractional amounts without decimal points:
goals whose datapoints and bare-min or hard-cap values are shown in HH:MM format.
That’s determined by a separate boolean goal field, `timey`

, and is shown as a checkbox in settings:

[ ] Show data in HH:MM format

If `timey = true`

then every datapoint value, bare min, hard cap, safety buffer, etc — including the `quantum`

field described below — is displayed as HH:MM.

We call goals where only whole numbers make sense *integery*.
Dealing with them correctly is a special case of conservative rounding.
If you have a hard cap of turning your phone’s screen on 4.92 more times, then Beeminder better let you know you can do it up to 4 more times, not 5.
In general, we want to round up in the case of an integery do-more goal, or down in the case of do-less.
Even more generally, we want to round in the direction of the good side of the yellow brick road.

And it’s all the same principle no matter what we’re rounding to, so behind the scenes we’re implementing this nice and generally.

In particular, in addition to `timey`

, every Beeminder goal shall have two new fields:
`quantum`

and
`quantex`

.

The `quantum`

field gives the granularity of the thing being measured.
All numbers displayed for what you need to do for the goal will be rounded conservatively to the nearest `quantum`

.
(Note: we never round the datapoint values themselves — those always keep whatever precision they were born with.)
If `quantum`

is 1 then the goal is integery.
For dollar amounts you might want a `quantum`

of 0.01.
Or 0.1 for a weight goal if your scale weighs to the nearest tenth of a kilogram.

The user-facing manifestation of `quantum`

is a field in goal settings called “precision”, defaulting to 1, with explanatory/help text as follows.

E.g., your weight has precision 0.1 if that’s what your scale measures to. Use “1” if you never want to see decimals.

In theory you could have a `quantum`

greater than 1.
We don’t know of a use case where that would be better than `quantum = 1`

so we won’t worry our pretty heads about that until we do.

A `quantum`

of zero means no rounding at all — full machine precision.
No one wants that and the UI should enforce `quantum > 0.000001`

.
But the implementation is fine with `quantum = 0`

; it just can’t be negative.

Again, if `timey`

is true then `quantum`

or “precision” in the above UI is also shown in HH:MM format.

In practice, as users
have been vociferously advocating, the overwhelming majority of goals are integery or timey-wimey.
The first obvious decision is to make integery goals
(`quantum = 1`

)
the default,
or `quantum = 1/60`

for timey-wimey goals.

The `quantex`

field is a flag indicating if the `quantum`

field was set explicitly by the user.
It’s initially false.
The first time the user submits a value for `quantum`

(AKA “precision”) in goal settings, `quantex`

is set permanently to true.
So `quantex := true`

is part of the submit action for the `quantum`

field.

Mostly we don’t want newbees to have to think about this and newbees almost always want
integery (`quantum = 1, timey = false`

) or
timey-wimey (`quantum = 1/60, timey = true`

) goals.
So that’s what `quantum`

is set to by default.

Every time a datapoint is added, if `quantex`

is false, set `quantum`

to the min of itself and
`quantize(x)`

where `x`

is the datapoint value, as a string, the way it was entered, and `quantize()`

is defined as in
conservaround.glitch.me.

For showing
bare min,
hard cap,
and safety buffer expressed in goal units,
use the `conservaround`

function as defined at
conservaround.glitch.me with the goal’s `yaw`

field as the error direction parameter.
For example, a do-more goal (good side up) will round up.

Fractional beeminding works fine. That’s when you have an inherently integery metric but you treat it as non-integery and enter fractional amounts. It’s a thing, it’s fine, none of this impacts it.

If someone enters a fraction like 1/3 as a datapoint value, that gets macro-expanded in the UI to “0.333333333333333333”. Similarly, datapoints submitted via the API could accidentally have a stupid amount of precision. Decision: Tough cookies, go fix the precision if it’s messed up. (But also try it after this is all shipped and if it’s too ugly, enforce a minimum

`quantum`

in the back-end as well as the UI.)What happens to the precision when you rescale your data and the graph? If

`quantex`

is true then just rescale`quantum`

as well. Otherwise, set`quantum = 1`

and then rescale all the datapoints, triggering an update for each of them. That sets`quantum`

however makes sense according to the`quantize`

function based on the new data. What about if rescaling yields stupidly fine precision? This we’ll callously ignore. If you’re rescaling then you can set your own dang precision.Possible Pareto-dominant proposal that avoids the precision setting: Hold off on the

`quantex`

field and the`quantum`

field is strictly inferred from datapoints. The`quantum`

field defaults to 1 (or 1/60 if timey) but if you ever enter a non-integer (non-divisible-by-1/60) datapoint it permanently reverts to the status quo where we target 4 sigfigs. Tentative decision: Given the existing half-assed integery setting, this is too hard to quite Pareto dominate the status quo. Also we’re feeling ready to just take the plunge now that this spec is a Heartbreaking Work of Staggering Genius.What if you have a totally integery do-less goal with a rate of 2/7 so the PPRs are .571429 or whatever? Do they just ruin the integeriness of your goal until you go set an explicit

`quantum=1`

? (Deleting the PPR doesn’t help since inferred quantum isn’t recomputed when datapoints are deleted.) Answer: No, because we won’t update`quantum`

based on PPRs. Those aren’t user-originating datapoints.

Thanks to Bee Soule for pretty much coauthoring this, to the daily beemail subscribers for resoundingly rejecting a version of this that twisted itself into a pretzel trying to avoid having an explicit user setting, Oliver Mayor for reminding me to consider data rescaling, and to Zedmango for setting me straight on the question of rounding datapoint values as well as help with the webcopy.