diff --git a/Sources/LoopAlgorithm/LoopMath.swift b/Sources/LoopAlgorithm/LoopMath.swift index 13889fc..7fc9e48 100644 --- a/Sources/LoopAlgorithm/LoopMath.swift +++ b/Sources/LoopAlgorithm/LoopMath.swift @@ -192,19 +192,25 @@ extension GlucoseValue { let glucoseUnit = LoopUnit.milligramsPerDeciliter let velocityUnit = GlucoseEffectVelocity.perSecondUnit - // The starting rate, which we will decay to 0 over the specified duration - let intercept = rate.doubleValue(for: velocityUnit) // mg/dL/s - let decayStartDate = startDate.addingTimeInterval(delta) - let slope = -intercept / (duration - delta) // mg/dL/s/s + let firstChange = rate.doubleValue(for: velocityUnit) * delta // mg/dL/s * s = mg/dL + let secondChange = firstChange * (1 - delta / (duration - delta)) + + // Solve for f(t) = a*t^2 + b*t + c, where t is relative to self.startDate. + // f(0) = c + // f(delta) - c = firstChange = a*delta^2 + b*delta + // f(2*delta) - c = firstChange + secondChange = 4*a*delta^2 + 2*b*delta + // --> firstChange - secondChange = 2*a*delta^2 + let c = quantity.doubleValue(for: glucoseUnit) + let a = (secondChange - firstChange) / (2 * delta * delta) // mg/dL/s^2 + let b = (firstChange + secondChange - 4 * a * delta * delta) / (2 * delta) // mg/dL/s var values = [GlucoseEffect(startDate: startDate, quantity: quantity)] - var date = decayStartDate - var lastValue = quantity.doubleValue(for: glucoseUnit) + var date = startDate.addingTimeInterval(delta) repeat { - let value = lastValue + (intercept + slope * date.timeIntervalSince(decayStartDate)) * delta + let time = min(duration, date.timeIntervalSince(self.startDate)) + let value = a * time * time + b * time + c values.append(GlucoseEffect(startDate: date, quantity: LoopQuantity(unit: glucoseUnit, doubleValue: value))) - lastValue = value date = date.addingTimeInterval(delta) } while date < endDate diff --git a/Tests/LoopAlgorithmTests/Fixtures/carbs_with_isf_change_recommendation.json b/Tests/LoopAlgorithmTests/Fixtures/carbs_with_isf_change_recommendation.json index b4e70af..bde6ac6 100644 --- a/Tests/LoopAlgorithmTests/Fixtures/carbs_with_isf_change_recommendation.json +++ b/Tests/LoopAlgorithmTests/Fixtures/carbs_with_isf_change_recommendation.json @@ -1,5 +1,5 @@ { "manual" : { - "amount" : 10.546890782709953 + "amount" : 10.52269112701204 } -} +} \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/Fixtures/live_capture_predicted_glucose.json b/Tests/LoopAlgorithmTests/Fixtures/live_capture_predicted_glucose.json index b77cb55..1baaacf 100644 --- a/Tests/LoopAlgorithmTests/Fixtures/live_capture_predicted_glucose.json +++ b/Tests/LoopAlgorithmTests/Fixtures/live_capture_predicted_glucose.json @@ -10,383 +10,383 @@ "startDate" : "2023-06-23T02:40:00Z" }, { - "quantity" : 180.52987493690765, + "quantity" : 180.52784693243598, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T02:45:00Z" }, { - "quantity" : 179.77931710835796, + "quantity" : 179.77106522809387, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T02:50:00Z" }, { - "quantity" : 177.81435588000684, + "quantity" : 177.7956842526296, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T02:55:00Z" }, { - "quantity" : 175.04920382978105, + "quantity" : 175.01794458844162, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:00:00Z" }, { - "quantity" : 172.09884468881066, + "quantity" : 172.05499783350902, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:05:00Z" }, { - "quantity" : 169.0341959170697, + "quantity" : 168.97776144780588, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:10:00Z" }, { - "quantity" : 165.91852357330802, + "quantity" : 165.84950149008202, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:15:00Z" }, { - "quantity" : 162.78787379965794, + "quantity" : 162.70626410246973, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:20:00Z" }, { - "quantity" : 159.67566374385987, + "quantity" : 159.58146643270948, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:25:00Z" }, { - "quantity" : 156.6278000530812, + "quantity" : 156.5210151279686, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:30:00Z" }, { - "quantity" : 153.68497899133908, + "quantity" : 153.5656064522643, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:35:00Z" }, { - "quantity" : 150.85857622089654, + "quantity" : 150.73920368182175, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:40:00Z" }, { - "quantity" : 148.1797464838103, + "quantity" : 148.06037394473552, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:45:00Z" }, { - "quantity" : 145.67546444468488, + "quantity" : 145.5560919056101, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:50:00Z" }, { - "quantity" : 143.36889813413907, + "quantity" : 143.24952559506428, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T03:55:00Z" }, { - "quantity" : 141.27978455565565, + "quantity" : 141.16041201658086, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:00:00Z" }, { - "quantity" : 139.4249156157845, + "quantity" : 139.3055430767097, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:05:00Z" }, { - "quantity" : 137.7082164432302, + "quantity" : 137.58884390415542, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:10:00Z" }, { - "quantity" : 135.9914530272836, + "quantity" : 135.8720804882088, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:15:00Z" }, { - "quantity" : 134.2827664300858, + "quantity" : 134.163393891011, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:20:00Z" }, { - "quantity" : 132.58882252103788, + "quantity" : 132.4694499819631, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:25:00Z" }, { - "quantity" : 130.91436540926705, + "quantity" : 130.79499287019226, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:30:00Z" }, { - "quantity" : 129.26245506698106, + "quantity" : 129.14308252790627, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:35:00Z" }, { - "quantity" : 127.63445215517064, + "quantity" : 127.51507961609585, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:40:00Z" }, { - "quantity" : 126.02931442610466, + "quantity" : 125.90994188702987, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:45:00Z" }, { - "quantity" : 124.44584453318035, + "quantity" : 124.32647199410556, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:50:00Z" }, { - "quantity" : 122.88145382927624, + "quantity" : 122.76208129020145, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T04:55:00Z" }, { - "quantity" : 121.33291804466413, + "quantity" : 121.21354550558934, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:00:00Z" }, { - "quantity" : 119.79660318395023, + "quantity" : 119.67723064487544, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:05:00Z" }, { - "quantity" : 118.26822621269756, + "quantity" : 118.14885367362277, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:10:00Z" }, { - "quantity" : 116.74288846240054, + "quantity" : 116.62351592332575, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:15:00Z" }, { - "quantity" : 115.21516364934988, + "quantity" : 115.09579111027509, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:20:00Z" }, { - "quantity" : 113.67917795139525, + "quantity" : 113.55980541232046, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:25:00Z" }, { - "quantity" : 112.12868274578355, + "quantity" : 112.00931020670876, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:30:00Z" }, { - "quantity" : 110.55712056957398, + "quantity" : 110.4377480304992, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:35:00Z" }, { - "quantity" : 108.95768482515078, + "quantity" : 108.83831228607599, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:40:00Z" }, { - "quantity" : 107.32337371691418, + "quantity" : 107.20400117783939, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:45:00Z" }, { - "quantity" : 105.64703887119052, + "quantity" : 105.52766633211573, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:50:00Z" }, { - "quantity" : 103.92146136061618, + "quantity" : 103.80208882154139, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T05:55:00Z" }, { - "quantity" : 102.13957364029821, + "quantity" : 102.02020110122342, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:00:00Z" }, { - "quantity" : 100.29425666336888, + "quantity" : 100.1748841242941, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:05:00Z" }, { - "quantity" : 98.37810372588095, + "quantity" : 98.25873118680616, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:10:00Z" }, { - "quantity" : 96.38393930539169, + "quantity" : 96.2645667663169, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:15:00Z" }, { - "quantity" : 94.30446350902744, + "quantity" : 94.18509096995265, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:20:00Z" }, { - "quantity" : 92.24204127278486, + "quantity" : 92.12266873371007, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:25:00Z" }, { - "quantity" : 90.33818302395392, + "quantity" : 90.21881048487913, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:30:00Z" }, { - "quantity" : 88.58657375772682, + "quantity" : 88.46720121865204, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:35:00Z" }, { - "quantity" : 86.9796355549934, + "quantity" : 86.8602630159186, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:40:00Z" }, { - "quantity" : 85.50932186775859, + "quantity" : 85.3899493286838, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:45:00Z" }, { - "quantity" : 84.16822997919033, + "quantity" : 84.04885744011554, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:50:00Z" }, { - "quantity" : 82.94837192653554, + "quantity" : 82.82899938746075, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T06:55:00Z" }, { - "quantity" : 81.84224397138112, + "quantity" : 81.72287143230633, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:00:00Z" }, { - "quantity" : 80.8433012790305, + "quantity" : 80.72392873995571, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:05:00Z" }, { - "quantity" : 79.94514990703274, + "quantity" : 79.82577736795795, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:10:00Z" }, { - "quantity" : 79.1425285689858, + "quantity" : 79.02315602991101, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:15:00Z" }, { - "quantity" : 78.43073701607969, + "quantity" : 78.3113644770049, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:20:00Z" }, { - "quantity" : 77.80513210408813, + "quantity" : 77.68575956501334, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:25:00Z" }, { - "quantity" : 77.26038909817899, + "quantity" : 77.1410165591042, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:30:00Z" }, { - "quantity" : 76.79214128522554, + "quantity" : 76.67276874615075, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:35:00Z" }, { - "quantity" : 76.39636603545401, + "quantity" : 76.27699349637922, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:40:00Z" }, { - "quantity" : 76.06917517261084, + "quantity" : 75.94980263353605, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:45:00Z" }, { - "quantity" : 75.80681469169488, + "quantity" : 75.6874421526201, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:50:00Z" }, { - "quantity" : 75.60563685065486, + "quantity" : 75.48626431158007, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T07:55:00Z" }, { - "quantity" : 75.46174433219417, + "quantity" : 75.34237179311938, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:00:00Z" }, { - "quantity" : 75.3700976935867, + "quantity" : 75.25072515451191, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:05:00Z" }, { - "quantity" : 75.32563190200372, + "quantity" : 75.20625936292893, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:10:00Z" }, { - "quantity" : 75.32301505961473, + "quantity" : 75.20364252053994, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:15:00Z" }, { - "quantity" : 75.33414614640142, + "quantity" : 75.21477360732663, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:20:00Z" }, { - "quantity" : 75.34232624108009, + "quantity" : 75.2229537020053, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:25:00Z" }, { - "quantity" : 75.34805924470882, + "quantity" : 75.22868670563403, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:30:00Z" }, { - "quantity" : 75.35181912391843, + "quantity" : 75.23244658484364, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:35:00Z" }, { - "quantity" : 75.35405041818424, + "quantity" : 75.23467787910946, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:40:00Z" }, { - "quantity" : 75.35517138501669, + "quantity" : 75.2357988459419, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:45:00Z" }, { - "quantity" : 75.35557365902051, + "quantity" : 75.23620111994572, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:50:00Z" }, { - "quantity" : 75.35562264689557, + "quantity" : 75.23625010782078, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T08:55:00Z" }, { - "quantity" : 75.35562264689557, + "quantity" : 75.23625010782078, "quantityUnit" : "mg\/dL", "startDate" : "2023-06-23T09:00:00Z" } -] +] \ No newline at end of file diff --git a/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift b/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift index 1d7ae4b..47b8dd9 100644 --- a/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift +++ b/Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift @@ -76,7 +76,7 @@ final class LoopAlgorithmTests: XCTestCase { let output = LoopAlgorithm.run(input: input) XCTAssertEqual(output.activeCarbs, 50) - XCTAssertEqual(output.recommendation!.manual!.amount, 5.83, accuracy: 0.01) + XCTAssertEqual(output.recommendation!.manual!.amount, 5.86, accuracy: 0.01) } @@ -112,8 +112,8 @@ final class LoopAlgorithmTests: XCTestCase { XCTAssertEqual(outputA.effects.insulin.last?.quantity.doubleValue(for: .milligramsPerDeciliter), 0.0) XCTAssertEqual(outputB.effects.insulin.last?.quantity.doubleValue(for: .milligramsPerDeciliter), 0.0) - XCTAssertEqual(outputA.effects.retrospectiveCorrection.last?.quantity.doubleValue(for: .milligramsPerDeciliter), 165) - XCTAssertEqual(outputB.effects.retrospectiveCorrection.last?.quantity.doubleValue(for: .milligramsPerDeciliter), 165) + XCTAssertEqual(outputA.effects.retrospectiveCorrection.last?.quantity.doubleValue(for: .milligramsPerDeciliter) ?? 0, 165, accuracy: 0.05) + XCTAssertEqual(outputB.effects.retrospectiveCorrection.last?.quantity.doubleValue(for: .milligramsPerDeciliter) ?? 0, 165, accuracy: 0.05) // These tests fail, because the momentum effect is *not* time independent yet. // Even though all the input data is the same (just shifted in time), momentum effect varies in relation to how offset @@ -228,8 +228,8 @@ final class LoopAlgorithmTests: XCTestCase { let output = LoopAlgorithm.run(input: input) // Should recommend bolus to cover meal - XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 274, accuracy: 0.1) - XCTAssertEqual(output.recommendation!.manual!.amount, 1.9, accuracy: 0.01) + XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 274.14, accuracy: 0.1) + XCTAssertEqual(output.recommendation!.manual!.amount, 1.91, accuracy: 0.01) // Now check forecast if bolus recommendation is accepted and delivered. input.doses.append( @@ -259,7 +259,7 @@ final class LoopAlgorithmTests: XCTestCase { let output = LoopAlgorithm.run(input: input) // Should recommend bolus to cover meal - XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 269, accuracy: 0.1) + XCTAssertEqual(output.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 269.15, accuracy: 0.1) XCTAssertEqual(output.recommendation!.manual!.amount, 2.16, accuracy: 0.01) // Now check forecast if bolus recommendation is accepted and delivered. @@ -329,7 +329,7 @@ final class LoopAlgorithmTests: XCTestCase { var recommendedBolus = output.recommendation!.automatic?.bolusUnits var activeInsulin = output.activeInsulin! XCTAssertEqual(activeInsulin, 8.0) - XCTAssertEqual(recommendedBolus!, 1.66, accuracy: 0.01) + XCTAssertEqual(recommendedBolus!, 1.69, accuracy: 0.01) // Now try with maxBolus of 4; should not recommend any more insulin, as we're at our max iob input.maxBolus = 4 @@ -397,12 +397,12 @@ final class LoopAlgorithmTests: XCTestCase { // Without mid-absorption ISF input.useMidAbsorptionISF = false var output = LoopAlgorithm.run(input: input) - XCTAssertEqual(2.58, output.recommendation!.manual!.amount, accuracy: 0.01) + XCTAssertEqual(2.73, output.recommendation!.manual!.amount, accuracy: 0.01) // With mid-absorption ISF input.useMidAbsorptionISF = true output = LoopAlgorithm.run(input: input) - XCTAssertEqual(1.41, output.recommendation!.manual!.amount, accuracy: 0.01) + XCTAssertEqual(1.49, output.recommendation!.manual!.amount, accuracy: 0.01) } func testIncompleteISFTimelineDetected() { diff --git a/Tests/LoopAlgorithmTests/LoopMathTests.swift b/Tests/LoopAlgorithmTests/LoopMathTests.swift new file mode 100644 index 0000000..ff3bac5 --- /dev/null +++ b/Tests/LoopAlgorithmTests/LoopMathTests.swift @@ -0,0 +1,48 @@ +// +// LoopMathTests.swift +// LoopAlgorithm +// + +import XCTest +@testable import LoopAlgorithm + +class LoopMathTests: XCTestCase { + + /// `decayEffect` previously accumulated the decay step-by-step starting from + /// the simulation-grid boundary (the sample's `startDate` floored to `delta`), + /// so two samples sitting on opposite sides of a 5-minute boundary produced + /// different effect values at the same future absolute timestamp. With the + /// continuous formulation, a sub-`delta` shift in the input timestamp only + /// shifts the output series by one slot and leaves shared-timestamp values + /// effectively unchanged. + func testDecayEffectIsContinuousAcrossSimulationBoundary() { + let calendar = Calendar(identifier: .gregorian) + let alignedDate = calendar.date(from: DateComponents(year: 2024, month: 1, day: 1, hour: 10, minute: 15, second: 0))! + let shiftedDate = alignedDate.addingTimeInterval(-1e-6) + + let rate = LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: -0.5) + + let alignedSample = FixtureGlucoseSample(startDate: alignedDate, quantity: .glucose(100)) + let shiftedSample = FixtureGlucoseSample(startDate: shiftedDate, quantity: .glucose(100)) + + let alignedEffects = alignedSample.decayEffect(atRate: rate, for: .minutes(30)) + let shiftedEffects = shiftedSample.decayEffect(atRate: rate, for: .minutes(30)) + + // The shifted sample's floored start lands one `delta` earlier, so its + // series has one extra leading entry equal to the sample's value. + XCTAssertEqual(shiftedEffects.count, alignedEffects.count + 1) + XCTAssertEqual(shiftedEffects[0].quantity.doubleValue(for: .milligramsPerDeciliter), 100, accuracy: 1e-9) + + // Shared timestamps should produce shared values. + let mgdl = LoopUnit.milligramsPerDeciliter + for (index, aligned) in alignedEffects.enumerated() { + let shifted = shiftedEffects[index + 1] + XCTAssertEqual(aligned.startDate, shifted.startDate) + XCTAssertEqual( + aligned.quantity.doubleValue(for: mgdl), + shifted.quantity.doubleValue(for: mgdl), + accuracy: 1e-6 + ) + } + } +} diff --git a/Tests/LoopAlgorithmTests/Mocks/IntegralRetrospectiveCorrectionTests.swift b/Tests/LoopAlgorithmTests/Mocks/IntegralRetrospectiveCorrectionTests.swift index da43231..ccd112f 100644 --- a/Tests/LoopAlgorithmTests/Mocks/IntegralRetrospectiveCorrectionTests.swift +++ b/Tests/LoopAlgorithmTests/Mocks/IntegralRetrospectiveCorrectionTests.swift @@ -42,7 +42,7 @@ final class IntegralRetrospectiveCorrectionTests: XCTestCase { retrospectiveCorrectionGroupingInterval: LoopMath.retrospectiveCorrectionGroupingInterval ) - XCTAssertEqual(effect.last?.quantity.doubleValue(for: .milligramsPerDeciliter), 110) + XCTAssertEqual(effect.last?.quantity.doubleValue(for: .milligramsPerDeciliter) ?? 0, 110, accuracy: 0.05) XCTAssertEqual(effect.last?.startDate, dateFormatter.date(from: "2015-07-13T13:00:00")!) } }