diff --git a/summary/types/glucose.go b/summary/types/glucose.go index eaa4c6765..06e9fd1c0 100644 --- a/summary/types/glucose.go +++ b/summary/types/glucose.go @@ -153,15 +153,10 @@ func (r *Range) UpdateTotal(record Glucose) { // CombineVariance Implemented using https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Parallel_algorithm func (r *Range) CombineVariance(new *Range) float64 { - // Exit early for No-Op case - if r.Variance == 0 && new.Variance == 0 { - return 0 - } - - // Return new if existing is 0 - if r.Variance == 0 { - return new.Variance - } + // [BACK-4044] Only return early if either range has 0 records. Otherwise + // proceed with variance calculation even if one range has no variance in + // order to properly handle the case where multiple ranges only have one + // record each. // if we have no values in either bucket, this will result in NaN, and cant be added anyway, return what we started with if r.Records == 0 || new.Records == 0 { diff --git a/summary/types/glucose_test.go b/summary/types/glucose_test.go index f0dfbb4ec..e23bee43d 100644 --- a/summary/types/glucose_test.go +++ b/summary/types/glucose_test.go @@ -354,8 +354,10 @@ var _ = Describe("Glucose", func() { // expect percent cleared, we don't handle percent on add Expect(firstRange.Percent).To(BeZero()) }) + }) - It("range.Add without minutes", func() { + Describe("range.Add without minutes", func() { + It("multiple samples per range", func() { firstRange := Range{ Glucose: 1, Minutes: 0, @@ -384,6 +386,46 @@ var _ = Describe("Glucose", func() { // expect percent cleared, we don't handle percent on add Expect(firstRange.Percent).To(BeZero()) }) + + It("one sample per range", func() { + // Test to cover [BACK-4044] + ranges := []Range{ + { + Glucose: 10, + Minutes: 0, + Records: 1, + }, + { + Glucose: 15, + Minutes: 0, + Records: 1, + }, + { + Glucose: 8, + Minutes: 0, + Records: 1, + }, + { + Glucose: 11, + Minutes: 0, + Records: 1, + }, + } + rootRange := Range{} + vals := []float64{} + var runningTotal float64 + for i, rng := range ranges { + rootRange.Add(&rng) + runningTotal += rng.Glucose + mean := runningTotal / float64(i+1) + vals = append(vals, rng.Glucose) + variance := CalculateVariance(vals, mean) + Expect(rootRange.Variance).To(Equal(variance)) + if i > 0 { + Expect(rootRange.Variance).To(BeNumerically(">", math.SmallestNonzeroFloat64)) + } + } + }) }) Context("Ranges", func() { @@ -1056,6 +1098,79 @@ var _ = Describe("Glucose", func() { Expect(period.Max).To(Equal(InTargetBloodGlucose)) }) + It("Add four single record buckets to a period", func() { + datumTime := bucketTime.Add(5 * time.Minute) + period = GlucosePeriod{} + + bucketOne := NewBucket[*GlucoseBucket](userId, bucketTime, SummaryTypeBGM) + err = bucketOne.Update(NewSelfMonitoredGlucoseWithValue(datumTime, 5.0)) + Expect(err).ToNot(HaveOccurred()) + + bucketTwo := NewBucket[*GlucoseBucket](userId, bucketTime.Add(time.Hour), SummaryTypeBGM) + err = bucketTwo.Update(NewSelfMonitoredGlucoseWithValue(datumTime.Add(time.Hour), 5.0)) + Expect(err).ToNot(HaveOccurred()) + + bucketThree := NewBucket[*GlucoseBucket](userId, bucketTime.Add(26*time.Hour), SummaryTypeBGM) + err = bucketThree.Update(NewSelfMonitoredGlucoseWithValue(datumTime.Add(26*time.Hour), 11.0)) + Expect(err).ToNot(HaveOccurred()) + + bucketFour := NewBucket[*GlucoseBucket](userId, bucketTime.Add(27*time.Hour), SummaryTypeBGM) + err = bucketFour.Update(NewSelfMonitoredGlucoseWithValue(datumTime.Add(27*time.Hour), 11.0)) + Expect(err).ToNot(HaveOccurred()) + + err = period.Update(bucketOne) + Expect(err).ToNot(HaveOccurred()) + Expect(period.Target.Records).To(Equal(1)) + Expect(period.HoursWithData).To(Equal(1)) + Expect(period.DaysWithData).To(Equal(1)) + Expect(period.Min).To(Equal(5.0)) + Expect(period.Max).To(Equal(5.0)) + + err = period.Update(bucketTwo) + Expect(err).ToNot(HaveOccurred()) + Expect(period.Target.Records).To(Equal(2)) + Expect(period.HoursWithData).To(Equal(2)) + Expect(period.DaysWithData).To(Equal(1)) + Expect(period.Min).To(Equal(5.0)) + Expect(period.Max).To(Equal(5.0)) + + err = period.Update(bucketThree) + Expect(err).ToNot(HaveOccurred()) + Expect(period.Target.Records).To(Equal(2)) + Expect(period.HoursWithData).To(Equal(3)) + Expect(period.DaysWithData).To(Equal(2)) + Expect(period.Min).To(Equal(5.0)) + Expect(period.Max).To(Equal(11.0)) + + err = period.Update(bucketFour) + Expect(err).ToNot(HaveOccurred()) + Expect(period.Target.Records).To(Equal(2)) + Expect(period.HoursWithData).To(Equal(4)) + Expect(period.DaysWithData).To(Equal(2)) + Expect(period.Min).To(Equal(5.0)) + Expect(period.Max).To(Equal(11.0)) + + periodDays := 7 + period.Finalize(periodDays) + + Expect(period.AnyLow.Percent).To(Equal(0.0)) + Expect(period.AnyHigh.Percent).To(Equal(0.5)) + Expect(period.Target.Percent).To(Equal(0.5)) + Expect(period.Low.Percent).To(Equal(0.0)) + Expect(period.High.Percent).To(Equal(0.5)) + Expect(period.VeryLow.Percent).To(Equal(0.0)) + Expect(period.ExtremeHigh.Percent).To(Equal(0.0)) + + avgGlucose := (5.0 + 5.0 + 11.0 + 11.0) / 4.0 + Expect(period.AverageDailyRecords).To(Equal(4.0 / float64(periodDays))) + Expect(period.AverageGlucose).To(Equal(avgGlucose)) + + expectedStdev := ((5-avgGlucose)*(5-avgGlucose) + (5-avgGlucose)*(5-avgGlucose) + (11-avgGlucose)*(11-avgGlucose) + (11-avgGlucose)*(11-avgGlucose)) / 4.0 / 3.0 + expectedCV := expectedStdev / avgGlucose + Expect(period.StandardDeviation).To(Equal(expectedStdev)) + Expect(period.CoefficientOfVariation).To(Equal(expectedCV)) + }) + It("Finalize a 1d period", func() { period = GlucosePeriod{} buckets := CreateGlucoseBuckets(bucketTime, 24, 12, true)