PR: #621
The stats model has a systematic off-by-one error in how the analysis window
start date is computed, causing the selected period to span one extra day and
all per-day averages to be divided by the wrong count.
Problem
When a quick-select preset (7d, 14d, 30d) is chosen, or when the stats view
opens, the end date is correctly set to the end of yesterday (the last complete
day). However, the start date is computed by subtracting N calendar days from
the start of the end day rather than N - 1, producing a window of N + 1 days.
For example, selecting "7d" with yesterday = Apr 25 produces:
- start = Apr 25 − 7 days = Apr 18
- end = Apr 25 23:59:59
- Actual range: 8 days (Apr 18–25)
The day count label compounds the issue by using an exclusive date diff
(dateComponents([.day], from: start, to: end)), so it displays "7 days"
for an 8-day window — the label and the actual range disagree in opposite
directions.
StatsDataService.updateDateRange() then computes daysToAnalyze from the
same exclusive diff, storing 7 for an 8-day range. This value is used as the
denominator in per-day averages, overstating them.
Secondary inconsistencies:
- The bolus cutoff in
SimpleStatsViewModel re-derives its own anchor from
Date() - requestedDays * 86400 instead of reading dataService.startDate,
so it can diverge from the resolved period.
avgCarbs uses dailyCarbs.count (days with at least one entry) as the
denominator rather than the full period length, inflating the average on
carb-free days.
calculateActualDaysCovered() has the same Date()-anchored re-derivation.
Proposed Fix
- Start date uses
endDay - (N - 1) so a "7d" preset spans exactly
Apr 19–Apr 25 (7 days inclusive).
daysToAnalyze is computed as daysBetween + 1 on day-start boundaries.
- Day count label adds 1 to the exclusive diff so it matches the actual window.
- Bolus cutoff and
calculateActualDaysCovered read dataService.startDate
directly.
avgCarbs denominator uses dataService.daysToAnalyze.
AggregatedStatsView.init() had a separate hardcoded value: -7 offset that
was also corrected to -(7 - 1).
Time zone note
All date arithmetic goes through dateTimeUtils.displayCalendar(), which
respects the user's configured graph time zone or the device time zone.
startOfDay(for:) and date(byAdding: .day) use calendar days rather than
fixed 86 400-second intervals, so DST transitions are handled correctly.
PR: #621
The stats model has a systematic off-by-one error in how the analysis window
start date is computed, causing the selected period to span one extra day and
all per-day averages to be divided by the wrong count.
Problem
When a quick-select preset (7d, 14d, 30d) is chosen, or when the stats view
opens, the end date is correctly set to the end of yesterday (the last complete
day). However, the start date is computed by subtracting
Ncalendar days fromthe start of the end day rather than
N - 1, producing a window ofN + 1days.For example, selecting "7d" with yesterday = Apr 25 produces:
The day count label compounds the issue by using an exclusive date diff
(
dateComponents([.day], from: start, to: end)), so it displays "7 days"for an 8-day window — the label and the actual range disagree in opposite
directions.
StatsDataService.updateDateRange()then computesdaysToAnalyzefrom thesame exclusive diff, storing 7 for an 8-day range. This value is used as the
denominator in per-day averages, overstating them.
Secondary inconsistencies:
SimpleStatsViewModelre-derives its own anchor fromDate() - requestedDays * 86400instead of readingdataService.startDate,so it can diverge from the resolved period.
avgCarbsusesdailyCarbs.count(days with at least one entry) as thedenominator rather than the full period length, inflating the average on
carb-free days.
calculateActualDaysCovered()has the sameDate()-anchored re-derivation.Proposed Fix
endDay - (N - 1)so a "7d" preset spans exactlyApr 19–Apr 25 (7 days inclusive).
daysToAnalyzeis computed asdaysBetween + 1on day-start boundaries.calculateActualDaysCoveredreaddataService.startDatedirectly.
avgCarbsdenominator usesdataService.daysToAnalyze.AggregatedStatsView.init()had a separate hardcodedvalue: -7offset thatwas also corrected to
-(7 - 1).Time zone note
All date arithmetic goes through
dateTimeUtils.displayCalendar(), whichrespects the user's configured graph time zone or the device time zone.
startOfDay(for:)anddate(byAdding: .day)use calendar days rather thanfixed 86 400-second intervals, so DST transitions are handled correctly.