<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sugar Sense</title>
    <description>The latest articles on DEV Community by Sugar Sense (@sugarsense).</description>
    <link>https://dev.to/sugarsense</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4067778%2F06bda92c-d1ab-443a-89c1-092727995b89.png</url>
      <title>DEV Community: Sugar Sense</title>
      <link>https://dev.to/sugarsense</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sugarsense"/>
    <language>en</language>
    <item>
      <title>Glucose math looks trivial until you ship it: five rules from a production CGM app</title>
      <dc:creator>Sugar Sense</dc:creator>
      <pubDate>Fri, 07 Aug 2026 15:47:08 +0000</pubDate>
      <link>https://dev.to/sugarsense/glucose-math-looks-trivial-until-you-ship-it-five-rules-from-a-production-cgm-app-2ia0</link>
      <guid>https://dev.to/sugarsense/glucose-math-looks-trivial-until-you-ship-it-five-rules-from-a-production-cgm-app-2ia0</guid>
      <description>&lt;p&gt;We build &lt;a href="https://sugarsense.io" rel="noopener noreferrer"&gt;Sugar Sense&lt;/a&gt;, a CGM companion app that connects to&lt;br&gt;
FreeStyle Libre, Dexcom and &lt;a href="https://nightscout.github.io/" rel="noopener noreferrer"&gt;Nightscout&lt;/a&gt; and shows glucose&lt;br&gt;
on phones, watches, a browser toolbar and a Windows system tray. Over the last two years our&lt;br&gt;
team has re-implemented the same tiny pile of glucose display logic on every one of those&lt;br&gt;
surfaces, and the math that fits on a napkin still found a way to go subtly wrong on each&lt;br&gt;
new port.&lt;/p&gt;

&lt;p&gt;So we extracted the rules into two zero-dependency open source packages,&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/sugarsense-glucose-units" rel="noopener noreferrer"&gt;sugarsense-glucose-units&lt;/a&gt; on npm and&lt;br&gt;
&lt;a href="https://rubygems.org/gems/sugarsense-units" rel="noopener noreferrer"&gt;sugarsense-units&lt;/a&gt; on RubyGems, and wrote the&lt;br&gt;
conventions down. Here are the five rules, so you can steal them even if you never install&lt;br&gt;
anything.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Pick one canonical unit and convert only at display
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mgdlToMmol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formatGlucose&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sugarsense-glucose-units&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;mgdlToMmol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// 5.55&lt;/span&gt;
&lt;span class="nf"&gt;formatGlucose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mmol&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// '6.7'&lt;/span&gt;
&lt;span class="nf"&gt;formatGlucose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mgdl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// '120'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment two components each keep their own copy of the value in their preferred unit, they&lt;br&gt;
drift. We learned this on a widget that rounded before converting.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Decide what happens exactly at the boundary
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;classifyZone&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sugarsense-glucose-units&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;classifyZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 'low'&lt;/span&gt;
&lt;span class="nf"&gt;classifyZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 'inRange'&lt;/span&gt;
&lt;span class="nf"&gt;classifyZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 'high'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Treat trend as a code, not a string
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trendArrow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trendName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sugarsense-glucose-units&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;trendArrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// '↘'&lt;/span&gt;
&lt;span class="nf"&gt;trendName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 'falling'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. A glucose value without freshness is a lie
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isStale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sugarsense-glucose-units&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;isStale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readingTimestampMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true when older than 15 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. One threshold set drives everything
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;timeInRange&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sugarsense-glucose-units&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;timeInRange&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// { veryLow: 16.7, low: 16.7, inRange: 33.3, high: 16.7, veryHigh: 16.7 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The packages
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/sugarsense-glucose-units" rel="noopener noreferrer"&gt;sugarsense-glucose-units&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RubyGems: &lt;a href="https://rubygems.org/gems/sugarsense-units" rel="noopener noreferrer"&gt;sugarsense-units&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/sugarsense/sugarsense-oss" rel="noopener noreferrer"&gt;github.com/sugarsense/sugarsense-oss&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>ruby</category>
    </item>
  </channel>
</rss>
