feat(activity): computation and range syntax in the core #305

Merged
kfickel merged 4 commits from 279_activity-computation-and-range into main 2026-10-20 13:05:59 +02:00
Collaborator

Closes #279 (part of #277).

The core now computes everything a host shows of an activity command for a range of days. Nothing prints it or draws it yet (#280–#282).

Range (commands/activity/range.rs, pure)

  • RangeSpec::parse: all, 2026, 2026-10, 2026-10-16, 2026-10-03..2026-10-17 (a reversed range is an error), last N days / last 1 day. Case and surrounding spaces don't matter. Any other text gives an error that lists the accepted forms. Display writes it back in the same form.
  • resolve(today, start) -> DayRange { from, to, unit }. A year, a month or a range of days keeps all of its days, so a calendar shows the whole month. Only all is cut, to run from the first tracked day to today.
  • label (All time, Oct 2026, 3–17 Oct, 28 Sep–4 Oct, 28 Dec 2025–3 Jan 2026), step(±n), can_step, spec, quick_picks(today), and Shape::of (Day / Calendar up to 42 days / Grid).

Computation (commands/activity/compute.rs)

  • Each series finds its outermost matches. They are keyed by the shard, so a shard in two series is one Match holding both series.
  • The day is the local date in the repository timezone. Days are Tracked, Future or Untracked (before start, or before the first match).
  • Each DayCell has, per series, the count and the first and last time; also the number of distinct shards, the Classification, the series it is drawn in (split / first / most, a tie goes to the first) and its level (count: 1–4+; fixed: 4).
  • Stats holds the inclusive and the exclusive day counts together, plus per_week and percent. With 0 tracked days everything is 0, never a division by zero.
  • The matches are newest first. PageRequest / Page handle paging.

Facade

  • api::activity(store, project, stream, key, range, now, page) -> ActivityReport: range, label, shape, start, whether it can step back and forward, the series (name and colour), exclude_mixed, the days, the stats, a page of ActivityShards with their shard_summary, and the shared shards.
  • api::activity_range(text).
  • Errors: ConfigError for an unknown key or a command of another type (…is not an activity command). A new CoreError::InvalidRange covers a text that is no range.

Deviations from the plan

  • api::activity takes a StreamStore. shard_summary needs the text of the file, so only the files of the requested page are read. The timezone comes from the project, not from a separate argument.
  • No new UnknownCommand / NotAnActivityCommand variants: the facade's existing ConfigError wording is used, as for every other command type.
  • The level of a mixed day counts its distinct shards, so a shard in two series counts once.
  • InvalidRange crosses the bindings as StreamdException.InvalidRange, with its arm in the app's reason(). The records of the report come with #283.

Tests

These cover the sample of the screenshots (15 shards, 11 of 17 days active, 6.2 a week, Run 7 including 2 mixed, Climbing 6, Mixed 2, None 6), the exclusive mode, start cutting days, a shard in two series, a sub-heading counted once, the most tie, fixed, a day at midnight across Berlin's DST change, and every range form, label, step, clamp and quick pick, plus paging. Facade tests on a MemoryStore cover the summaries, paging and errors.

nix flake check passes. nix build .#streamd-app builds with its tests.

Docs: R36 (range and computation), README (Activity), AGENTS.md (the module and the facade).

Closes #279 (part of #277). The core now computes everything a host shows of an activity command for a range of days. Nothing prints it or draws it yet (#280–#282). ## Range (`commands/activity/range.rs`, pure) - `RangeSpec::parse`: `all`, `2026`, `2026-10`, `2026-10-16`, `2026-10-03..2026-10-17` (a reversed range is an error), `last N days` / `last 1 day`. Case and surrounding spaces don't matter. Any other text gives an error that lists the accepted forms. `Display` writes it back in the same form. - `resolve(today, start) -> DayRange { from, to, unit }`. A year, a month or a range of days keeps all of its days, so a calendar shows the whole month. Only `all` is cut, to run from the first tracked day to today. - `label` (`All time`, `Oct 2026`, `3–17 Oct`, `28 Sep–4 Oct`, `28 Dec 2025–3 Jan 2026`), `step(±n)`, `can_step`, `spec`, `quick_picks(today)`, and `Shape::of` (`Day` / `Calendar` up to 42 days / `Grid`). ## Computation (`commands/activity/compute.rs`) - Each series finds its outermost matches. They are keyed by the shard, so a shard in two series is one `Match` holding both series. - The day is the local date in the repository timezone. Days are `Tracked`, `Future` or `Untracked` (before `start`, or before the first match). - Each `DayCell` has, per series, the count and the first and last time; also the number of distinct shards, the `Classification`, the series it is `drawn` in (`split` / `first` / `most`, a tie goes to the first) and its `level` (`count`: 1–4+; `fixed`: 4). - `Stats` holds the inclusive and the exclusive day counts together, plus `per_week` and `percent`. With 0 tracked days everything is 0, never a division by zero. - The matches are newest first. `PageRequest` / `Page` handle paging. ## Facade - `api::activity(store, project, stream, key, range, now, page) -> ActivityReport`: range, label, shape, start, whether it can step back and forward, the series (name and colour), `exclude_mixed`, the days, the stats, a page of `ActivityShard`s with their `shard_summary`, and the `shared` shards. - `api::activity_range(text)`. - Errors: `ConfigError` for an unknown key or a command of another type (``…is not an activity command``). A new `CoreError::InvalidRange` covers a text that is no range. ## Deviations from the plan - `api::activity` takes a `StreamStore`. `shard_summary` needs the text of the file, so only the files of the requested page are read. The timezone comes from the project, not from a separate argument. - No new `UnknownCommand` / `NotAnActivityCommand` variants: the facade's existing `ConfigError` wording is used, as for every other command type. - The level of a mixed day counts its distinct shards, so a shard in two series counts once. - `InvalidRange` crosses the bindings as `StreamdException.InvalidRange`, with its arm in the app's `reason()`. The records of the report come with #283. ## Tests These cover the sample of the screenshots (15 shards, 11 of 17 days active, 6.2 a week, Run 7 including 2 mixed, Climbing 6, Mixed 2, None 6), the exclusive mode, `start` cutting days, a shard in two series, a sub-heading counted once, the `most` tie, `fixed`, a day at midnight across Berlin's DST change, and every range form, label, step, clamp and quick pick, plus paging. Facade tests on a `MemoryStore` cover the summaries, paging and errors. `nix flake check` passes. `nix build .#streamd-app` builds with its tests. Docs: R36 (range and computation), README (Activity), AGENTS.md (the module and the facade).
docs(activity): describe the range syntax and the computation
All checks were successful
Continuous Integration / Conventional Commits (push) Successful in 18s
Continuous Integration / Lint, Check & Test (push) Successful in 6m30s
Continuous Integration / Build Package (push) Successful in 13m32s
60f93b5086
kfickel deleted branch 279_activity-computation-and-range 2026-10-20 13:05:59 +02:00
Sign in to join this conversation.
No reviewers
No labels
android
cli
epic
planned
tui
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
kfickel/streamd!305
No description provided.