fix(patterns): count daily habits by local day, not UTC day - #56
Open
Sarcastic-Soul wants to merge 1 commit into
Open
fix(patterns): count daily habits by local day, not UTC day#56Sarcastic-Soul wants to merge 1 commit into
Sarcastic-Soul wants to merge 1 commit into
Conversation
daily_habits grouped clicks with SQLite's date(), which buckets the stored UTC timestamps by UTC calendar day, while the rest of the package is local-day based. For a user far enough from UTC a single local day straddles a UTC boundary and was counted as two -- and since the habit threshold is days >= 2, that reported a habit out of activity that never repeated, contradicting the module's stated guarantee. Bucket in Python from parse_epoch + local_day_string instead, matching how coverage() already derives local time and keeping the result independent of the SQLite process timezone. The per-day click floor the SQL HAVING enforced moves to MIN_CLICKS_PER_DAY, and rows whose timestamp will not parse are now skipped rather than bucketed at epoch 0. Adds tests/test_patterns.py (the module had no direct coverage). Fixes nossa-y#51 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #51
The bug
daily_habitswas the one place in the package that used SQLite'sdate()on the stored UTC timestamps:That buckets by UTC calendar day. Everything else —
local_day_string,local_day_window_utc,fmt_local_hm,coverage()'s hour histogram — works in the user's local day.For a user far enough from UTC, one local day straddles a UTC boundary and gets counted as two.
Why it matters
The threshold for a habit is
if h["days"] >= 2. So this doesn't just inflate a number — it manufactures a habit out of activity that only ever happened once, which is exactly what the module docstring promises can't happen:Concretely, in
Asia/Kolkata(UTC+5:30), six clicks on a single local day:'Standup' on 2 days, 6x totalLate-night and early-morning users are hit hardest, since that's when a local day crosses UTC. Users near UTC+0 are unaffected — probably why it went unnoticed.
The change
Select raw timestamps instead of pre-grouping in SQL, then bucket by local day in Python:
This reuses the existing
_timehelpers rather than reaching for SQLite's'localtime'modifier, so the answer doesn't depend on the SQLite process timezone and matches howcoverage()already derives local time.Three things move with it:
HAVING cnt >= 3enforced is now the named constantMIN_CLICKS_PER_DAY = 3, applied in Python. Same value, same effect.2026-07-08T99:99:99passes it and reaches the parser. Without a guard those rows would all bucket onto the same 1970 local day and invent an extra day for the habit.ORDER BY element_name, daygave the stable sort an alphabetical tiebreaker; the sort key is now explicitly(-total, name), preserving that exactly.Row volume is a little higher than the pre-aggregated query (one row per click rather than one per day/element), bounded by the same window and click filters — this detector already sits alongside
action_sequences, which pulls up to 20k raw click rows.Tests
patterns.pyhad no direct test coverage, so this addstests/test_patterns.pywith alocal_timezone()context manager that pins and restoresTZ(skipped wheretime.tzset()is unavailable, i.e. not on the Linux/macOS CI matrix):test_one_local_day_is_not_a_habit— the reproduction above (fails onmain)test_habit_detection_is_timezone_relative— same capture data, one habit underTZ=UTCand none underTZ=Asia/Kolkata, pinning down that "a day" means the user's day (fails onmain)test_daily_habits_skips_malformed_timestamps— unparseable rows dropped, not bucketed at epoch 0 (fails onmain)test_two_local_days_is_a_habit— a genuine two-local-day habit is still reported, with the right count (passes onmain, guards against over-narrowing)test_per_day_click_floor_still_applies— a day below the click floor doesn't count (passes onmain)Full suite: 112 passed (107 before, +5).
Scope
daily_habitsonly. The other five detectors, and thedays >= 2/MIN_FREQUENCYthresholds themselves, are untouched.