Skip to content

Commit 474fcec

Browse files
committed
Fix various patient export rounding related issues. (#258)
1 parent dd10fdc commit 474fcec

4 files changed

Lines changed: 149 additions & 68 deletions

File tree

export/export.go

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,21 @@ func (e *exporter) ToCSVRow(p *patients.ExportedPatient) []string {
5151
fmtDataSourceStatus(p.TwiistDataSource, e.params.ReportDate),
5252
fmtDataSourceLastDataDate(p.TwiistDataSource),
5353
ptime(p.CgmLastDataDate, "2006-01-02"),
54-
ppct(p.CgmActiveWearTime, 0),
54+
ppctunrounded(p.CgmActiveWearTime),
5555
pint(p.CgmDaysWithData),
5656
pint(p.CgmHoursWithData),
5757
ptomgdl(p.CgmAverageGlucose),
5858
pfloat(p.CgmGmi, 2),
5959
fmtPreferredUnits(p.CgmStdDev, e.clinic.PreferredBgUnits, 1),
6060
ppct(p.CgmCV, 0),
61-
ppct(p.CgmTimeInLevel2Hypo, 0),
62-
ppct(p.CgmTimeInLevel1Hypo, 0),
63-
ppct(p.CgmTimeInTarget, 0),
61+
ppctunrounded(p.CgmTimeInLevel2Hypo),
62+
ppctunrounded(p.CgmTimeInLevel1Hypo),
63+
ppctunrounded(p.CgmTimeInTarget),
6464
ppct(p.CgmTimeInLevel1Hyper, 0),
6565
ppct(p.CgmTimeInLevel2Hyper, 0),
6666
ptime(p.BgmLastDataDate, "2006-01-02"),
6767
ptomgdl(p.BgmAverageGlucose),
68-
pfloat(p.BgmReadingsPerDay, 0),
68+
pfloattrunc(p.BgmReadingsPerDay),
6969
pint(p.BgmTotalReadings),
7070
pint(p.BgmLowEvents),
7171
pint(p.BgmHighEvents),
@@ -251,17 +251,6 @@ func fmtClinicTime(t time.Time, clinic *clinics.Clinic) string {
251251
return t.In(loc).Format(timeFormat)
252252
}
253253

254-
func fmtClinicDate(t time.Time, clinic *clinics.Clinic) string {
255-
if clinic.Timezone == nil || *clinic.Timezone == "" {
256-
return t.Format(time.DateOnly)
257-
}
258-
loc, err := time.LoadLocation(*clinic.Timezone)
259-
if err != nil {
260-
return t.Format(time.DateOnly)
261-
}
262-
return t.In(loc).Format(time.DateOnly)
263-
}
264-
265254
func fmtBool(b bool, valIfTrue string, valIfFalse string) string {
266255
if b {
267256
return valIfTrue
@@ -316,17 +305,18 @@ func periodToDays(period string) (days int, err error) {
316305
return days, nil
317306
}
318307

319-
func fmtFloat(f float64, precision int) string {
308+
// FmtFloat returns a number as a string with precision digits to the right of the decimal point after banker's rounding.
309+
func FmtFloat(f float64, precision int) string {
320310
shift := math.Pow(10, float64(precision))
321-
return fmt.Sprintf("%v", math.RoundToEven(f*shift*100)/shift)
311+
return fmt.Sprintf("%v", math.RoundToEven(f*shift)/shift)
322312
}
323313

324314
func fmtPreferredUnits(valMmolL *float64, preferredBgUnits string, precision int) string {
325315
if valMmolL == nil {
326316
return ""
327317
}
328318
if strings.ToLower(preferredBgUnits) == "mg/dl" {
329-
return fmtFloat(toMgDl(*valMmolL), precision)
319+
return FmtFloat(toMgDl(*valMmolL), precision)
330320
}
331-
return fmtFloat(*valMmolL, precision)
321+
return FmtFloat(*valMmolL, precision)
332322
}

export/export_test.go

Lines changed: 122 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,82 @@ import (
1414
)
1515

1616
var _ = Describe("Export", func() {
17+
Describe("Format and conversion functions", func() {
18+
DescribeTable("FmtFloat",
19+
func(val float64, precision int, expected string) {
20+
formatted := export.FmtFloat(val, precision)
21+
Expect(formatted).To(Equal(expected))
22+
},
23+
Entry("0 precision", 2.45, 0, "2"),
24+
Entry("0 precision round to even downwards", 2.5, 0, "2"),
25+
Entry("0 precision round to even upwards", 3.5, 0, "4"),
26+
Entry("1 precision round upwards", 3.67, 1, "3.7"),
27+
Entry("1 precision round downwards", 3.44, 1, "3.4"),
28+
Entry("1 precision round to even upwards", 3.35, 1, "3.4"),
29+
Entry("1 precision round to even download", 8.65, 1, "8.6"),
30+
)
31+
})
32+
1733
Describe("ToCSVRow", func() {
18-
It("matches expected columns", func() {
19-
clinicianID := "a578d15f-73b6-4e25-9295-95707fca3520"
20-
patientTagID := "6a5794ac4762b6efdb48aaaa"
21-
patientTagOID, _ := primitive.ObjectIDFromHex(patientTagID)
22-
clinicID := "6a5794ac4762b6efdb48aaab"
23-
clinicOID, _ := primitive.ObjectIDFromHex(clinicID)
24-
patientID := "7ce10e4c-8930-4770-8323-c8305db09c61"
25-
reportDate := time.Date(2026, time.July, 13, 19, 0, 0, 0, time.UTC)
34+
var clinicianID string
35+
var patientTagID string
36+
var patientTagOID primitive.ObjectID
37+
var clinicID string
38+
var clinicOID primitive.ObjectID
39+
var patientID string
40+
var reportDate time.Time
41+
var inactiveDataSource patients.DataSource
42+
var expiredDataSource patients.DataSource
43+
var connectedDataSource patients.DataSource
44+
var patientTags []clinics.PatientTag
45+
var cs []*clinicians.Clinician
46+
var params patients.ExportParams
47+
var patient *patients.ExportedPatient
48+
49+
BeforeEach(func() {
50+
clinicianID = "a578d15f-73b6-4e25-9295-95707fca3520"
51+
patientTagID = "6a5794ac4762b6efdb48aaaa"
52+
patientTagOID, _ = primitive.ObjectIDFromHex(patientTagID)
53+
clinicID = "6a5794ac4762b6efdb48aaab"
54+
clinicOID, _ = primitive.ObjectIDFromHex(clinicID)
55+
patientID = "7ce10e4c-8930-4770-8323-c8305db09c61"
56+
reportDate = time.Date(2026, time.July, 13, 19, 0, 0, 0, time.UTC)
2657

27-
inactiveDataSource := patients.DataSource{
58+
inactiveDataSource = patients.DataSource{
2859
ProviderName: "dexcom",
2960
State: "connected",
3061
LatestDataTime: timep(time.Date(2026, time.July, 9, 0, 0, 0, 0, time.UTC)),
3162
}
32-
expiredDataSource := patients.DataSource{
63+
expiredDataSource = patients.DataSource{
3364
ExpirationTime: timep(time.Date(2025, time.January, 2, 3, 0, 0, 0, time.UTC)),
3465
ProviderName: "abbott",
3566
State: "pending",
3667
}
37-
connectedDataSource := patients.DataSource{
68+
connectedDataSource = patients.DataSource{
3869
ProviderName: "twiist",
3970
State: "connected",
4071
LatestDataTime: timep(time.Date(2026, time.July, 12, 20, 0, 0, 0, time.UTC)),
4172
}
42-
patientTags := []clinics.PatientTag{
73+
patientTags = []clinics.PatientTag{
4374
{
4475
Id: &patientTagOID,
4576
Name: "Some Tag",
4677
Patients: 1,
4778
},
4879
}
49-
cs := []*clinicians.Clinician{
80+
cs = []*clinicians.Clinician{
5081
{
5182
UserId: &clinicianID,
5283
Name: strp("Some Clinician"),
5384
},
5485
}
55-
clinic := &clinics.Clinic{
56-
Id: &clinicOID,
57-
PatientTags: patientTags,
58-
}
59-
params := patients.ExportParams{
86+
params = patients.ExportParams{
6087
Period: "1d",
6188
ExporterClinicianID: clinicianID,
6289
WorkspaceID: clinicID,
6390
ReportDate: reportDate,
6491
}
65-
patient := &patients.ExportedPatient{
92+
patient = &patients.ExportedPatient{
6693
FullName: strp("Some Patient"),
6794
UserId: &patientID,
6895
MRN: strp("123456789"),
@@ -85,6 +112,7 @@ var _ = Describe("Export", func() {
85112
CgmDaysWithData: intp(13),
86113
CgmHoursWithData: intp(238),
87114
CgmAverageGlucose: floatp(5.131),
115+
CgmStdDev: floatp(2.3),
88116
CgmTimeInLevel2Hypo: floatp(0.02134),
89117
CgmTimeInLevel1Hypo: floatp(0.06013),
90118
CgmTimeInTarget: floatp(0.82333),
@@ -95,7 +123,14 @@ var _ = Describe("Export", func() {
95123
BgmLowEvents: intp(3),
96124
BgmHighEvents: intp(1),
97125
}
126+
})
98127

128+
It("matches preferred units of mmol/L", func() {
129+
clinic := &clinics.Clinic{
130+
Id: &clinicOID,
131+
PatientTags: patientTags,
132+
PreferredBgUnits: "mmol/L",
133+
}
99134
e, err := export.NewPatientExportClinic(clinic, cs, nil, params)
100135
Expect(err).ToNot(HaveOccurred())
101136
row := e.ToCSVRow(patient)
@@ -118,25 +153,76 @@ var _ = Describe("Export", func() {
118153
"",
119154
"connected",
120155
"2026-07-12",
121-
"2026-07-13",
122-
"71",
123-
"13",
124-
"238",
125-
"92",
126-
"",
127-
"",
128-
"",
129-
"2",
130-
"6",
131-
"82",
156+
"2026-07-13", // cgm last data date
157+
"71.428", // cgm active wear time
158+
"13", // cgm days w/ data
159+
"238", // cgm hours w/ data
160+
"92", // avg glucose mg/dL
161+
"", // cgm gmi %
162+
"2.3", // cgm stdev in clnic preferred units
163+
"", // cbm cv %
164+
"2.1340000000000003", // time in level 2 hypo %
165+
"6.013", // time in level 1 hypo %
166+
"82.333", // cgm time in target %
167+
"", // cgm time in level 1 hyper %
168+
"", // cgm time in level 2 hyper %
169+
"2026-07-10", // bgm last data date
170+
"112", // bgm avg glucose mg/dL
171+
"2", // bgm readings / day
172+
"5", // bgm total readings
173+
"3", // bgm # low events
174+
"1", // bgm # high events
175+
}
176+
Expect(row).To(Equal(expectedRow))
177+
})
178+
179+
It("matches preferred units of mg/dL", func() {
180+
clinic := &clinics.Clinic{
181+
Id: &clinicOID,
182+
PatientTags: patientTags,
183+
PreferredBgUnits: "mg/dL",
184+
}
185+
e, err := export.NewPatientExportClinic(clinic, cs, nil, params)
186+
Expect(err).ToNot(HaveOccurred())
187+
row := e.ToCSVRow(patient)
188+
expectedRow := []string{
189+
"Some Patient",
190+
patientID,
191+
"123456789",
192+
"2000-01-02",
193+
"patient@tidepool.org",
194+
"Claimed",
195+
"2003-04-05",
196+
"Some Clinician",
197+
"Site1,Site2",
198+
`Some Tag,`,
132199
"",
200+
"type1",
201+
"inactive",
202+
"2026-07-09",
203+
"expired",
133204
"",
134-
"2026-07-10",
135-
"112",
136-
"2",
137-
"5",
138-
"3",
139-
"1",
205+
"connected",
206+
"2026-07-12",
207+
"2026-07-13", // cgm last data date
208+
"71.428", // cgm active wear time
209+
"13", // cgm days w/ data
210+
"238", // cgm hours w/ data
211+
"92", // avg glucose mg/dL
212+
"", // cgm gmi %
213+
"41.4", // cgm stdev in clnic preferred units
214+
"", // cbm cv %
215+
"2.1340000000000003", // time in level 2 hypo %
216+
"6.013", // time in level 1 hypo %
217+
"82.333", // cgm time in target %
218+
"", // cgm time in level 1 hyper %
219+
"", // cgm time in level 2 hyper %
220+
"2026-07-10", // bgm last data date
221+
"112", // bgm avg glucose mg/dL
222+
"2", // bgm readings / day
223+
"5", // bgm total readings
224+
"3", // bgm # low events
225+
"1", // bgm # high events
140226
}
141227
Expect(row).To(Equal(expectedRow))
142228
})

export/pointer.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ func pfloat(f *float64, precision int) string {
1919
return fmt.Sprintf("%v", math.RoundToEven(*f*shift)/shift)
2020
}
2121

22+
func pfloattrunc(f *float64) string {
23+
if f == nil {
24+
return ""
25+
}
26+
return fmt.Sprintf("%v", int(*f))
27+
}
28+
29+
func ppctunrounded(f *float64) string {
30+
if f == nil {
31+
return ""
32+
}
33+
return fmt.Sprintf("%v", *f*100)
34+
}
35+
2236
func pint(i *int) string {
2337
if i == nil {
2438
return ""
@@ -34,6 +48,7 @@ func ptomgdl(valMmolL *float64) string {
3448
return pfloat(&val, 0)
3549
}
3650

51+
// ppct takes a number ≤ 1.0 and returns a number as a string ≤ 100 with precision digits to the right of the decimal point after banker's rounding.
3752
func ppct(f *float64, precision int) string {
3853
if f == nil {
3954
return ""
@@ -48,12 +63,6 @@ func ptime(t *time.Time, layout string) string {
4863
}
4964
return t.Format(layout)
5065
}
51-
func ptimed(t *time.Time, layout, defaultVal string) string {
52-
if t == nil || t.IsZero() {
53-
return defaultVal
54-
}
55-
return t.Format(layout)
56-
}
5766

5867
func pstr(p *string) string {
5968
if p == nil {
@@ -70,10 +79,6 @@ func pstrd(p *string, defaultVal string) string {
7079
return *p
7180
}
7281

73-
func strp(s string) *string {
74-
return &s
75-
}
76-
7782
func toMgDl(valMmolL float64) float64 {
7883
intValue := int(valMmolL*MmolLToMgdLConversionFactor*MmolLToMgdLPrecisionFactor + 0.5)
7984
return float64(intValue) / MmolLToMgdLPrecisionFactor

patients/repository/repository.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,8 +1146,8 @@ func (r *repository) ListExportedPatients(ctx context.Context, params patients.E
11461146
},
11471147
},
11481148
"bgmTotalReadings": bgmPathPrefix + ".totalRecords",
1149-
"bgmLowEvents": bgmPathPrefix + ".timeInLowRecords",
1150-
"bgmHighEvents": bgmPathPrefix + ".timeInHighRecords",
1149+
"bgmLowEvents": bgmPathPrefix + ".timeInVeryLowRecords", // BGM Low Events are a count of readings below 54 mg/dL hence using the very low threshold count
1150+
"bgmHighEvents": bgmPathPrefix + ".timeInVeryHighRecords", // BGM High Events are a count of readings above 250 mg/dL.
11511151
}},
11521152
}
11531153

0 commit comments

Comments
 (0)