sugarsense-units 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dcc1213237aae5f99e43cf1f88dc91a40e62e8e285227223961022d23863c13b
4
+ data.tar.gz: 5ef263aaa45acd74d33487b2a7657e702c566e4b6f208df7464953bf8426aa27
5
+ SHA512:
6
+ metadata.gz: 6d7833ba0a82dc6e8a2761807dae113954a856edf8855240176e0b9ae436c7577602a81d1bbdb6fe8652ebf9796718797f8f2b51cea3a84f0c9c9c6ba2d0fbf0
7
+ data.tar.gz: 7bbe2fb05bbf457f2a62d902235e5422ff0188c6dd9b9d64991b71564b0105bf8a2d066753e3db0ff8834cd5b05106d0596bcfeb7c9b65924bddc88dea830016
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sugar Sense (https://sugarsense.io)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # sugarsense-units
2
+
3
+ Blood glucose unit conversion (mg/dL to mmol/L and back), CGM trend arrows, glucose zone classification, time in range and GMI helpers for Ruby. No dependencies.
4
+
5
+ Extracted from [Sugar Sense](https://sugarsense.io), a CGM monitoring app for iOS, Apple Watch and Android that connects to FreeStyle Libre, Dexcom and [Nightscout](https://nightscout.github.io/) and shares glucose with family in real time. These are the exact conventions the app uses in production, published so other diabetes tools can reuse them instead of re-deriving the math.
6
+
7
+ > This library is informational only. It contains no insulin dosing logic and gives no medical advice.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ gem install sugarsense-units
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ require "sugarsense-units"
19
+
20
+ SugarSense::Units.mgdl_to_mmol(100) # => 5.55
21
+ SugarSense::Units.format_glucose(120, :mmol) # => "6.7" (display form, 1 decimal)
22
+ SugarSense::Units.trend_arrow(2) # => "↘"
23
+ SugarSense::Units.classify_zone(65) # => :low
24
+ SugarSense::Units.gmi(154) # => 6.99 (approximates lab A1C, %)
25
+
26
+ SugarSense::Units.time_in_range([50, 60, 100, 150, 200, 300])
27
+ # => { total: 6,
28
+ # counts: { very_low: 1, low: 1, in_range: 2, high: 1, very_high: 1 },
29
+ # percent: { very_low: 16.67, low: 16.67, in_range: 33.33, high: 16.67, very_high: 16.67 } }
30
+ ```
31
+
32
+ ## Conventions
33
+
34
+ These match the Sugar Sense production apps and backend:
35
+
36
+ - All math is done in **mg/dL**; mmol/L is a display conversion (factor `0.0555`, shown with 1 decimal).
37
+ - **Trend codes** are integers 1 to 5: `1 ↓ falling fast, 2 ↘ falling, 3 → steady, 4 ↗ rising, 5 ↑ rising fast` (the same scheme Dexcom, Libre and Nightscout data can be normalized to).
38
+ - **Zones**: `:very_low, :low, :in_range, :high, :very_high`, with boundaries inclusive on the out-of-range side (a value exactly at the low limit is `:low`, exactly at the high limit is `:high`).
39
+ - **Default thresholds** are `55 / 70 / 180 / 250` mg/dL (`DEFAULT_THRESHOLDS`), aligned with ADA guidance. Every method accepts custom thresholds.
40
+
41
+ ## About Sugar Sense
42
+
43
+ [Sugar Sense](https://sugarsense.io) turns CGM data into real-time monitoring for the whole family: glucose on the lock screen and Apple Watch, low and high alerts, an automated emergency call on urgent lows, and a Care Circle so loved ones can follow along. There is also a [web dashboard](https://app.sugarsense.io) and a free [download page](https://sugarsense.io/download).
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SugarSense
4
+ module Units
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "units/version"
4
+
5
+ # Glucose unit and zone helpers, extracted from Sugar Sense (https://sugarsense.io).
6
+ # All internal math is mg/dL; mmol/L exists only as a display conversion.
7
+ # This library is informational only and contains no dosing logic.
8
+ module SugarSense
9
+ module Units
10
+ MMOL_PER_MGDL = 0.0555
11
+
12
+ # ADA-aligned defaults used by Sugar Sense for new accounts (mg/dL).
13
+ DEFAULT_THRESHOLDS = { very_low: 55, low: 70, high: 180, very_high: 250 }.freeze
14
+
15
+ # CGM trend codes: 1 falling fast, 2 falling, 3 steady, 4 rising, 5 rising fast.
16
+ TREND_ARROWS = { 1 => "↓", 2 => "↘", 3 => "→", 4 => "↗", 5 => "↑" }.freeze
17
+ TREND_NAMES = {
18
+ 1 => "falling_fast", 2 => "falling", 3 => "steady", 4 => "rising", 5 => "rising_fast"
19
+ }.freeze
20
+
21
+ module_function
22
+
23
+ # mg/dL -> mmol/L (unrounded float). Use format_glucose for the display form.
24
+ def mgdl_to_mmol(mgdl)
25
+ Float(mgdl) * MMOL_PER_MGDL
26
+ end
27
+
28
+ # mmol/L -> mg/dL, rounded to the nearest whole number.
29
+ def mmol_to_mgdl(mmol)
30
+ (Float(mmol) / MMOL_PER_MGDL).round
31
+ end
32
+
33
+ # Display formatting identical to the Sugar Sense apps:
34
+ # mmol/L is shown with exactly 1 decimal, mg/dL as a whole number.
35
+ # Rounding goes through Rational so the exact value of the IEEE double is
36
+ # rounded, matching JavaScript's toFixed(1) (100 mg/dL displays as "5.5");
37
+ # Ruby's own %.1f and Float#round(1) both give "5.6" for the same double.
38
+ def format_glucose(mgdl, unit = :mgdl)
39
+ case unit
40
+ when :mmol
41
+ mmol = Float(mgdl) * MMOL_PER_MGDL
42
+ format("%.1f", (Rational(mmol) * 10).round / 10.0)
43
+ when :mgdl then Float(mgdl).round.to_s
44
+ else raise ArgumentError, "unit must be :mgdl or :mmol"
45
+ end
46
+ end
47
+
48
+ def trend_arrow(code)
49
+ TREND_ARROWS.fetch(code, "")
50
+ end
51
+
52
+ def trend_name(code)
53
+ TREND_NAMES.fetch(code, "")
54
+ end
55
+
56
+ # Zone boundaries are inclusive on the out-of-range side:
57
+ # :very_low value <= very_low
58
+ # :low very_low < value <= low
59
+ # :in_range low < value < high
60
+ # :high high <= value < very_high
61
+ # :very_high value >= very_high
62
+ def classify_zone(mgdl, thresholds = DEFAULT_THRESHOLDS)
63
+ v = Float(mgdl)
64
+ t = validate_thresholds(thresholds)
65
+ return :very_high if v >= t[:very_high]
66
+ return :very_low if v <= t[:very_low]
67
+ return :high if v >= t[:high]
68
+ return :low if v <= t[:low]
69
+
70
+ :in_range
71
+ end
72
+
73
+ # Five-band distribution over an array of mg/dL values.
74
+ # Returns { total:, counts: {zone => n}, percent: {zone => Float} }.
75
+ def time_in_range(readings, thresholds = DEFAULT_THRESHOLDS)
76
+ t = validate_thresholds(thresholds)
77
+ counts = { very_low: 0, low: 0, in_range: 0, high: 0, very_high: 0 }
78
+ readings.each { |v| counts[classify_zone(v, t)] += 1 }
79
+ total = readings.length
80
+ percent = counts.transform_values { |c| total.zero? ? 0.0 : c * 100.0 / total }
81
+ { total: total, counts: counts, percent: percent }
82
+ end
83
+
84
+ # Glucose Management Indicator (approximates lab A1C, %) from mean mg/dL.
85
+ # International consensus formula (Bergenstal et al., 2018).
86
+ def gmi(mean_mgdl)
87
+ 3.31 + 0.02392 * Float(mean_mgdl)
88
+ end
89
+
90
+ def validate_thresholds(t)
91
+ unless t[:very_low] < t[:low] && t[:low] < t[:high] && t[:high] < t[:very_high]
92
+ raise ArgumentError, "thresholds must satisfy very_low < low < high < very_high"
93
+ end
94
+
95
+ t
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sugarsense/units"
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sugarsense-units
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sugar Sense
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-01 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'Glucose helpers extracted from the Sugar Sense CGM monitoring app: mg/dL
14
+ to mmol/L conversion, trend arrow codes, glucose zone classification, time in range
15
+ distribution and the GMI formula. Informational only, contains no dosing logic.'
16
+ email:
17
+ - info@sugarsense.io
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE
23
+ - README.md
24
+ - lib/sugarsense-units.rb
25
+ - lib/sugarsense/units.rb
26
+ - lib/sugarsense/units/version.rb
27
+ homepage: https://sugarsense.io
28
+ licenses:
29
+ - MIT
30
+ metadata:
31
+ homepage_uri: https://sugarsense.io
32
+ documentation_uri: https://www.rubydoc.info/gems/sugarsense-units
33
+ rubygems_mfa_required: 'true'
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '2.5'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 3.0.3.1
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: Blood glucose unit conversion, CGM trend arrows, zone classification and
53
+ time in range helpers.
54
+ test_files: []