Skip to content

Commit 9c6d9cd

Browse files
authored
Merge pull request #819 from tidepool-org/BACK-3461-trend-rate-units
[BACK-3461] Add trend rate units to cbg data type
2 parents 06a5aa0 + 87bb4e8 commit 9c6d9cd

File tree

9 files changed

+709
-162
lines changed

9 files changed

+709
-162
lines changed

data/blood/glucose/glucose.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,25 @@ const (
1010
MmolL = "mmol/L"
1111
Mmoll = "mmol/l"
1212

13+
MmolLMinute = "mmol/L/minute"
14+
1315
MgdL = "mg/dL"
1416
Mgdl = "mg/dl"
1517

18+
MgdLMinute = "mg/dL/minute"
19+
1620
MmolLMinimum float64 = 0.0
1721
MmolLMaximum float64 = 55.0
1822

23+
MmolLMinuteMinimum float64 = -5.5
24+
MmolLMinuteMaximum float64 = 5.5
25+
1926
MgdLMinimum float64 = 0.0
2027
MgdLMaximum float64 = 1000.0
2128

29+
MgdLMinuteMinimum float64 = -100.0
30+
MgdLMinuteMaximum float64 = 100.0
31+
2232
// MmolLToMgdLConversionFactor is MgdL Per MmolL.
2333
//
2434
// Reminder: The molecular mass of glucose is ≈ 180 g/mol.
@@ -34,6 +44,10 @@ func Units() []string {
3444
return []string{MmolL, Mmoll, MgdL, Mgdl}
3545
}
3646

47+
func RateUnits() []string {
48+
return []string{MmolLMinute, MgdLMinute}
49+
}
50+
3751
func ValueRangeForUnits(units *string) (float64, float64) {
3852
if units != nil {
3953
switch *units {
@@ -60,7 +74,41 @@ func NormalizeValueForUnits(value *float64, units *string) *float64 {
6074
if value != nil && units != nil {
6175
switch *units {
6276
case MgdL, Mgdl:
63-
intValue := int(*value/MmolLToMgdLConversionFactor*MmolLToMgdLPrecisionFactor + 0.5)
77+
intValue := int(*value/MmolLToMgdLConversionFactor*MmolLToMgdLPrecisionFactor + math.Copysign(0.5, *value))
78+
floatValue := float64(intValue) / MmolLToMgdLPrecisionFactor
79+
return &floatValue
80+
}
81+
}
82+
return value
83+
}
84+
85+
func ValueRangeForRateUnits(rateUnits *string) (float64, float64) {
86+
if rateUnits != nil {
87+
switch *rateUnits {
88+
case MmolLMinute:
89+
return MmolLMinuteMinimum, MmolLMinuteMaximum
90+
case MgdLMinute:
91+
return MgdLMinuteMinimum, MgdLMinuteMaximum
92+
}
93+
}
94+
return -math.MaxFloat64, math.MaxFloat64
95+
}
96+
97+
func NormalizeRateUnits(rateUnits *string) *string {
98+
if rateUnits != nil {
99+
switch *rateUnits {
100+
case MmolLMinute, MgdLMinute:
101+
return pointer.FromString(MmolLMinute)
102+
}
103+
}
104+
return rateUnits
105+
}
106+
107+
func NormalizeValueForRateUnits(value *float64, rateUnits *string) *float64 {
108+
if value != nil && rateUnits != nil {
109+
switch *rateUnits {
110+
case MgdLMinute:
111+
intValue := int(*value/MmolLToMgdLConversionFactor*MmolLToMgdLPrecisionFactor + math.Copysign(0.5, *value))
64112
floatValue := float64(intValue) / MmolLToMgdLPrecisionFactor
65113
return &floatValue
66114
}

data/blood/glucose/glucose_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ var _ = Describe("Glucose", func() {
1919
Expect(glucose.Mmoll).To(Equal("mmol/l"))
2020
})
2121

22+
It("has MmolLMinute", func() {
23+
Expect(glucose.MmolLMinute).To(Equal("mmol/L/minute"))
24+
})
25+
2226
It("has MgdL", func() {
2327
Expect(glucose.MgdL).To(Equal("mg/dL"))
2428
})
@@ -27,6 +31,10 @@ var _ = Describe("Glucose", func() {
2731
Expect(glucose.Mgdl).To(Equal("mg/dl"))
2832
})
2933

34+
It("has MgdLMinute", func() {
35+
Expect(glucose.MgdLMinute).To(Equal("mg/dL/minute"))
36+
})
37+
3038
It("has MmolLMinimum", func() {
3139
Expect(glucose.MmolLMinimum).To(Equal(0.0))
3240
})
@@ -35,6 +43,14 @@ var _ = Describe("Glucose", func() {
3543
Expect(glucose.MmolLMaximum).To(Equal(55.0))
3644
})
3745

46+
It("has MmolLMinuteMinimum", func() {
47+
Expect(glucose.MmolLMinuteMinimum).To(Equal(-5.5))
48+
})
49+
50+
It("has MmolLMinuteMaximum", func() {
51+
Expect(glucose.MmolLMinuteMaximum).To(Equal(5.5))
52+
})
53+
3854
It("has MgdLMinimum", func() {
3955
Expect(glucose.MgdLMinimum).To(Equal(0.0))
4056
})
@@ -43,6 +59,14 @@ var _ = Describe("Glucose", func() {
4359
Expect(glucose.MgdLMaximum).To(Equal(1000.0))
4460
})
4561

62+
It("has MgdLMinuteMinimum", func() {
63+
Expect(glucose.MgdLMinuteMinimum).To(Equal(-100.0))
64+
})
65+
66+
It("has MgdLMinuteMaximum", func() {
67+
Expect(glucose.MgdLMinuteMaximum).To(Equal(100.0))
68+
})
69+
4670
It("has MmolLToMgdLConversionFactor", func() {
4771
Expect(glucose.MmolLToMgdLConversionFactor).To(Equal(18.01559))
4872
})
@@ -57,6 +81,12 @@ var _ = Describe("Glucose", func() {
5781
})
5882
})
5983

84+
Context("RateUnits", func() {
85+
It("returns the expected units", func() {
86+
Expect(glucose.RateUnits()).To(ConsistOf("mmol/L/minute", "mg/dL/minute"))
87+
})
88+
})
89+
6090
DescribeTable("ValueRangeForUnits",
6191
func(units *string, expectedLower float64, expectedUpper float64) {
6292
actualLower, actualUpper := glucose.ValueRangeForUnits(units)
@@ -125,4 +155,67 @@ var _ = Describe("Glucose", func() {
125155
}
126156
})
127157
})
158+
159+
DescribeTable("ValueRangeForRateUnits",
160+
func(rateUnits *string, expectedLower float64, expectedUpper float64) {
161+
actualLower, actualUpper := glucose.ValueRangeForRateUnits(rateUnits)
162+
Expect(actualLower).To(Equal(expectedLower))
163+
Expect(actualUpper).To(Equal(expectedUpper))
164+
},
165+
Entry("returns no range for nil", nil, -math.MaxFloat64, math.MaxFloat64),
166+
Entry("returns no range for unknown units", pointer.FromString("unknown"), -math.MaxFloat64, math.MaxFloat64),
167+
Entry("returns expected range for mmol/L/minute units", pointer.FromString("mmol/L/minute"), -5.5, 5.5),
168+
Entry("returns expected range for mg/dL/minute units", pointer.FromString("mg/dL/minute"), -100.0, 100.0),
169+
)
170+
171+
DescribeTable("NormalizeRateUnits",
172+
func(rateUnits *string, expectedRateUnits *string) {
173+
actualRateUnits := glucose.NormalizeRateUnits(rateUnits)
174+
if expectedRateUnits == nil {
175+
Expect(actualRateUnits).To(BeNil())
176+
} else {
177+
Expect(actualRateUnits).ToNot(BeNil())
178+
Expect(*actualRateUnits).To(Equal(*expectedRateUnits))
179+
}
180+
},
181+
Entry("returns nil for nil", nil, nil),
182+
Entry("returns unchanged units for unknown units", pointer.FromString("unknown"), pointer.FromString("unknown")),
183+
Entry("returns mmol/L/minute for mmol/L/minute", pointer.FromString("mmol/L/minute"), pointer.FromString("mmol/L/minute")),
184+
Entry("returns mmol/L/minute for mg/dL/minute", pointer.FromString("mg/dL/minute"), pointer.FromString("mmol/L/minute")),
185+
)
186+
187+
Context("NormalizeValueForRateUnits", func() {
188+
DescribeTable("given value and units",
189+
func(value *float64, rateUnits *string, expectedValue *float64) {
190+
actualValue := glucose.NormalizeValueForRateUnits(value, rateUnits)
191+
if expectedValue == nil {
192+
Expect(actualValue).To(BeNil())
193+
} else {
194+
Expect(actualValue).ToNot(BeNil())
195+
Expect(*actualValue).To(Equal(*expectedValue))
196+
}
197+
},
198+
Entry("returns nil for nil value", nil, pointer.FromString("mmol/L/minute"), nil),
199+
Entry("returns unchanged value for nil units", pointer.FromFloat64(1.0), nil, pointer.FromFloat64(1.0)),
200+
Entry("returns unchanged value for unknown units", pointer.FromFloat64(1.0), pointer.FromString("unknown"), pointer.FromFloat64(1.0)),
201+
Entry("returns unchanged value for mmol/L/minute units", pointer.FromFloat64(1.0), pointer.FromString("mmol/L/minute"), pointer.FromFloat64(1.0)),
202+
Entry("returns converted value for mg/dL/minute units", pointer.FromFloat64(18.0), pointer.FromString("mg/dL/minute"), pointer.FromFloat64(0.99913)),
203+
)
204+
205+
It("properly normalizes a range of mmol/L/minute values", func() {
206+
for value := glucose.MmolLMinuteMinimum; value <= glucose.MmolLMinuteMaximum; value += 0.1 {
207+
normalizedValue := glucose.NormalizeValueForRateUnits(pointer.FromFloat64(float64(value)), pointer.FromString("mmol/L/minute"))
208+
Expect(normalizedValue).ToNot(BeNil())
209+
Expect(*normalizedValue).To(Equal(value))
210+
}
211+
})
212+
213+
It("properly normalizes a range of mg/dL/minute values", func() {
214+
for value := int(glucose.MgdLMinuteMinimum); value <= int(glucose.MgdLMinuteMaximum); value++ {
215+
normalizedValue := glucose.NormalizeValueForRateUnits(pointer.FromFloat64(float64(value)), pointer.FromString("mg/dL/minute"))
216+
Expect(normalizedValue).ToNot(BeNil())
217+
Expect(int(*normalizedValue*18.01559 + math.Copysign(0.5, float64(value)))).To(Equal(value))
218+
}
219+
})
220+
})
128221
})

data/blood/glucose/test/target.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,26 @@ func ExpectNormalizedValue(value *float64, expectedValue *float64, units *string
8484
}
8585
}
8686

87+
func ExpectNormalizedRateUnits(value *string, expectedValue *string) {
88+
if expectedValue != nil {
89+
gomega.Expect(value).ToNot(gomega.BeNil())
90+
gomega.Expect(*value).To(gomega.Equal(*dataBloodGlucose.NormalizeRateUnits(expectedValue)))
91+
*expectedValue = *value
92+
} else {
93+
gomega.Expect(value).To(gomega.BeNil())
94+
}
95+
}
96+
97+
func ExpectNormalizedRateValue(value *float64, expectedValue *float64, units *string) {
98+
if expectedValue != nil {
99+
gomega.Expect(value).ToNot(gomega.BeNil())
100+
gomega.Expect(*value).To(gomega.Equal(*dataBloodGlucose.NormalizeValueForRateUnits(expectedValue, units)))
101+
*expectedValue = *value
102+
} else {
103+
gomega.Expect(value).To(gomega.BeNil())
104+
}
105+
}
106+
87107
func ExpectNormalizedTarget(datum *dataBloodGlucose.Target, expectedDatum *dataBloodGlucose.Target, units *string) {
88108
gomega.Expect(datum).ToNot(gomega.BeNil())
89109
gomega.Expect(expectedDatum).ToNot(gomega.BeNil())

data/types/blood/glucose/continuous/continuous.go

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package continuous
22

33
import (
44
"github.com/tidepool-org/platform/data"
5-
"github.com/tidepool-org/platform/data/types/blood/glucose"
5+
dataBloodGlucose "github.com/tidepool-org/platform/data/blood/glucose"
6+
dataTypesBloodGlucose "github.com/tidepool-org/platform/data/types/blood/glucose"
67
"github.com/tidepool-org/platform/structure"
78
)
89

@@ -17,27 +18,34 @@ const (
1718
RapidFall = "rapidFall"
1819
RapidRise = "rapidRise"
1920

20-
TrendRateMaximum = 100
21-
TrendRateMinimum = -100
2221
SampleIntervalMinimum = 0
2322
SampleIntervalMaximum = 24 * 60 * 60 * 1000
2423
)
2524

2625
func Trends() []string {
27-
return []string{ConstantRate, SlowFall, SlowRise, ModerateFall, ModerateRise, RapidFall, RapidRise}
26+
return []string{
27+
ConstantRate,
28+
SlowFall,
29+
SlowRise,
30+
ModerateFall,
31+
ModerateRise,
32+
RapidFall,
33+
RapidRise,
34+
}
2835
}
2936

3037
type Continuous struct {
31-
glucose.Glucose `bson:",inline"`
32-
Trend *string `json:"trend,omitempty" bson:"trend,omitempty"`
33-
TrendRate *float64 `json:"trendRate,omitempty" bson:"trendRate,omitempty"`
34-
SampleInterval *int `json:"sampleInterval,omitempty" bson:"sampleInterval,omitempty"`
35-
Backfilled *bool `json:"backfilled,omitempty" bson:"backfilled,omitempty"`
38+
dataTypesBloodGlucose.Glucose `bson:",inline"`
39+
Trend *string `json:"trend,omitempty" bson:"trend,omitempty"`
40+
TrendRateUnits *string `json:"trendRateUnits,omitempty" bson:"trendRateUnits,omitempty"`
41+
TrendRate *float64 `json:"trendRate,omitempty" bson:"trendRate,omitempty"`
42+
SampleInterval *int `json:"sampleInterval,omitempty" bson:"sampleInterval,omitempty"`
43+
Backfilled *bool `json:"backfilled,omitempty" bson:"backfilled,omitempty"`
3644
}
3745

3846
func New() *Continuous {
3947
return &Continuous{
40-
Glucose: glucose.New(Type),
48+
Glucose: dataTypesBloodGlucose.New(Type),
4149
}
4250
}
4351

@@ -49,6 +57,7 @@ func (c *Continuous) Parse(parser structure.ObjectParser) {
4957
c.Glucose.Parse(parser)
5058

5159
c.Trend = parser.String("trend")
60+
c.TrendRateUnits = parser.String("trendRateUnits")
5261
c.TrendRate = parser.Float64("trendRate")
5362
c.SampleInterval = parser.Int("sampleInterval")
5463
c.Backfilled = parser.Bool("backfilled")
@@ -66,7 +75,12 @@ func (c *Continuous) Validate(validator structure.Validator) {
6675
}
6776

6877
validator.String("trend", c.Trend).OneOf(Trends()...)
69-
validator.Float64("trendRate", c.TrendRate).InRange(TrendRateMinimum, TrendRateMaximum)
78+
if trendRateUnitsValidator := validator.String("trendRateUnits", c.TrendRateUnits); c.TrendRate != nil {
79+
trendRateUnitsValidator.Exists().OneOf(dataBloodGlucose.RateUnits()...)
80+
} else {
81+
trendRateUnitsValidator.NotExists()
82+
}
83+
validator.Float64("trendRate", c.TrendRate).InRange(dataBloodGlucose.ValueRangeForRateUnits(c.TrendRateUnits))
7084
validator.Int("sampleInterval", c.SampleInterval).InRange(SampleIntervalMinimum, SampleIntervalMaximum)
7185
}
7286

@@ -76,4 +90,10 @@ func (c *Continuous) Normalize(normalizer data.Normalizer) {
7690
}
7791

7892
c.Glucose.Normalize(normalizer)
93+
94+
if normalizer.Origin() == structure.OriginExternal {
95+
rateUnits := c.TrendRateUnits
96+
c.TrendRateUnits = dataBloodGlucose.NormalizeRateUnits(rateUnits)
97+
c.TrendRate = dataBloodGlucose.NormalizeValueForRateUnits(c.TrendRate, rateUnits)
98+
}
7999
}

0 commit comments

Comments
 (0)