DEV Community

Sugar Sense
Sugar Sense

Posted on

Glucose math looks trivial until you ship it: five rules from a production CGM app

We build Sugar Sense, a CGM companion app that connects to
FreeStyle Libre, Dexcom and Nightscout and shows glucose
on phones, watches, a browser toolbar and a Windows system tray. Over the last two years our
team has re-implemented the same tiny pile of glucose display logic on every one of those
surfaces, and the math that fits on a napkin still found a way to go subtly wrong on each
new port.

So we extracted the rules into two zero-dependency open source packages,
sugarsense-glucose-units on npm and
sugarsense-units on RubyGems, and wrote the
conventions down. Here are the five rules, so you can steal them even if you never install
anything.

1. Pick one canonical unit and convert only at display

Glucose is measured in mg/dL in some countries and mmol/L in others. Store exactly one of
them (we use mg/dL everywhere: database, API, push payloads) and convert at the last moment,
in the UI. The factor is 0.0555, and the display forms differ too: mg/dL is shown as a whole
number, mmol/L with one decimal.

import { mgdlToMmol, formatGlucose } from 'sugarsense-glucose-units';

mgdlToMmol(100);            // 5.55
formatGlucose(120, 'mmol'); // '6.7'
formatGlucose(120, 'mgdl'); // '120'
Enter fullscreen mode Exit fullscreen mode

The moment two components each keep their own copy of the value in their preferred unit, they
drift. We learned this on a widget that rounded before converting.

2. Decide what happens exactly at the boundary

Is a reading of exactly 70 mg/dL "in range" or "low"? There is no universal answer, but there
must be exactly one answer in your codebase. Our convention: a value exactly at the low limit
is low, exactly at the high limit is high, so "in range" is exclusive at both ends. If your
graph coloring and your Time in Range statistics answer this question differently, your users
will eventually screenshot a green dot that your stats page counts as low, and they will be
right to be confused.

import { classifyZone } from 'sugarsense-glucose-units';

classifyZone(70);  // 'low'
classifyZone(71);  // 'inRange'
classifyZone(180); // 'high'
Enter fullscreen mode Exit fullscreen mode

3. Treat trend as a code, not a string

CGM vendors report the trend (falling fast, falling, steady, rising, rising fast) in
different shapes. Normalize them to one small integer scheme at the edge, ours is 1 to 5,
and render arrows from that code. Handle unknown values explicitly instead of letting an
unexpected vendor string leak into the UI.

import { trendArrow, trendName } from 'sugarsense-glucose-units';

trendArrow(2); // '↘'
trendName(2);  // 'falling'
Enter fullscreen mode Exit fullscreen mode

4. A glucose value without freshness is a lie

Every reading is a value plus a timestamp, and the timestamp is the safety-critical half. Our
rule: anything older than 15 minutes renders as "no data", on every surface, no exceptions. A
monitoring app that confidently shows a number from 40 minutes ago is worse than an empty
one, because someone may act on it.

import { isStale } from 'sugarsense-glucose-units';

isStale(readingTimestampMs); // true when older than 15 minutes
Enter fullscreen mode Exit fullscreen mode

5. One threshold set drives everything

Alerts, graph colors and Time in Range must all read the same four numbers. Ours default to
55 / 70 / 180 / 250 mg/dL (aligned with ADA guidance) and every function accepts custom
thresholds, but there is deliberately no separate "graph range" concept. The day you let a
chart carry its own target band, rule 2 breaks silently.

import { timeInRange } from 'sugarsense-glucose-units';

timeInRange([50, 60, 100, 150, 200, 300]).percent;
// { veryLow: 16.7, low: 16.7, inRange: 33.3, high: 16.7, veryHigh: 16.7 }
Enter fullscreen mode Exit fullscreen mode

The packages

Both libraries are MIT, have zero dependencies, and ship the exact conventions our production
apps use. The npm package ships CommonJS, ESM and TypeScript types; the gem mirrors the same
API for Ruby. By design they contain no insulin dosing logic and give no medical advice: they
are display and statistics math only.

If you are building anything CGM-adjacent (a Nightscout plugin, a dashboard, a watch face),
take the five rules even if you skip the packages. Issues and pull requests are welcome on
the mirror.

Top comments (1)

Collapse
 
swapnoneel123 profile image
Swapnoneel Saha

the canonical unit, boundary rule, trend code, and freshness check form a clear contract. i would make the threshold set versioned and include the rule version in stored summaries, so a later settings change does not rewrite the meaning of old reports. add property tests for unit round trips, exact boundaries, stale timestamps, and unknown trend values across both packages. that can keep the ports aligned.