Skip to content

Fix off-by-one in stats period start date and day count #619

@MtlPhil

Description

@MtlPhil

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions