An interval is something with a start and an end: a night’s sleep, a session of reading, a workout, a shift at work. In Datler it is a kind of field (Value Types), so a trackable declares one beside its other fields and each entry carries its two ends. Sleep is one interval from Bedtime to Waketime (Sleep); Read is a record of an interval, a book and the pages it covered (Reading a Book); a routine’s entry is an interval that other entries sit inside (Events).

An interval keeps its two ends and works its length out from them rather than storing a length: a length alone cannot say when something happened (“at 15:30, for 30 minutes” does not say whether 15:30 was the start or the end), and an end never logged would leave no trace. Both ends sit on one entry, so a start never has to be matched with an end logged somewhere else.

The field

An interval field (IntervalFieldSchema in packages/core/src/value-schema.ts) has:

  • Words for its two ends, start_label and end_label: Bedtime and Waketime, Start and Stop. Issues and forms use them (“Has no bedtime”).
  • Entry date, dated_by: which end dates the entry. A night belongs to the morning it ended (dated by its end, the default); a Read entry belongs to when you sat down (dated by its start).
  • Stale after, stale_after: how many hours one may stay open before it is read as never ended rather than running. 16 hours when not set.

A trackable has at most one interval field, and no other field beside it may be called start, end or duration, since those are the entry’s own. A trackable whose stored schema is the interval kind itself is read as the record with one interval field, time (asCurrentSchema).

An interval holds only its two ends. The fields logged with it, such as Read’s book and pages, are the trackable’s, beside it, so the trackable is the one container of fields and nothing that reads them needs a second path for fields inside an interval. An interval’s entry takes one note, for the whole entry; its start and its end have none of their own.

What is stored

An interval entry stores its ends at the top of value_json, as local wall-clock times (YYYY-MM-DDTHH:mm), with the values of its other fields flat beside them. intervalEntryFields in packages/core/src/interval.ts derives the rest:

  • happened_at is the dating end, or the other end where that one is missing;
  • value_number is the length in seconds, the base unit of a duration, and is empty unless both ends are there.

Because the length is a plain number in value_number, every numeric path (a Sum of a day, a chart, a derived formula) treats a finished interval as its length, and a length can be added to a number field that measures a duration. An open interval, or one with no start, has no length and drops out of every total. The length is the difference between the two wall-clock times, so a change of the clocks during the night does not change it: in bed from 23:00 to 07:00 is 8 hours either way, as people read it.

Either end may be missing, never both. An interval with only an end logged (a sleep logged on waking with no bedtime) is kept and reported as an issue (Issues); a day it falls on reads its total as at least that much (≥).

Open, closed and stale

classifyIntervals works out each entry’s status when read, never storing it:

  • closed when it has an end, or has only an end;
  • open when it has no end, is the most recently started of those with no end, and started within Stale after;
  • stale when it has no end and either a later one has started since or it has run past Stale after.

Stale after is what keeps a forgotten start from pairing with a much later end: a bedtime left open two nights ago is stale by this morning, so this morning’s Waketime logs a new sleep rather than closing that one as a 40-hour night.

This is the one rule for whether an interval still runs: classifyIntervals and openInterval take the trackable and read its Stale after themselves, so no caller can apply the default to a trackable that set its own. What runs now is openIntervals in packages/core/src/routines.ts (open, and begun by now), and every surface asks it through runsNow: the running bar, a routine’s page and its rows, the routine a new entry goes into, and the timeline.

An open interval shows in the running bar on every screen until it is stopped. A stale one is the issue Never ended, offered to be opened or deleted, and listed before every other issue. It no longer runs: it is no routine anything logged goes into unasked, and reads “no end” where its end would show, with no Stop.

A forgotten interval is what the bar is for, so the one a trackable last began, once stale, stays in it (staleIntervals): first, as a warning, saying it never ended, with End at, which asks when it did and ends it then (currentInterval in interval.ts). One left open with others begun after it, ended or not, is only an issue: a sleep from August that sixty nights followed was not forgotten last night.

A stale interval’s span ends at its start, as the timeline draws it (whenOf, which asks whether the entry runs): a span that grew by a day for every day it was forgotten would be the wrong thing to measure anything against. So a routine stale since Monday covers Monday’s start alone, and a part logged into it after that start is an issue, After its routine ended, until the routine is ended.

Logging one

The entry panel logs a new interval with its two ends on one row, start → end, and a Swap start and end button between them.

  • The moment fills one end. Which one comes from the trackable’s history (momentFills): of its latest sixty entries, counting the finished ones, if at least three end within 45 minutes of the moment’s time of day and more than twice as many end there as start there, the moment fills the end; otherwise the start. A sleep logged on waking fills Waketime; a reading session fills its start. The filled end follows the app’s moment (The Moment) until typed over.
  • The other end is empty until typed. Where its time would put the end before the start, it is taken as the day before (a start) or after (an end).
  • With only a start, the action is Open, and the panel then shows the interval running, since when, its fields so far, and Stop at the moment. With both ends it is Log.
  • Fields are filled at either end, and a required field is asked for once the interval has ended.
  • A Rundown item can ask for one end (The Rundown): Sleep’s Waketime row asks for the end and logs a sleep that ended now when none is open.

An entry already logged edits its start and end each as a time field with a smaller date field beside it (MomentFields), since it is usually the time that needs changing, not the day.

Changing the field

The words for its ends and Stale after are safe to change: they change how entries read, not what is stored. Entry date is not safe, and stays editable: changing it moves entries between days, so the form warns and, on save, rewrites the affected entries’ times (redated).

Reading one

Read as a number, an interval is its length, shown as hours and minutes (“7h 10m”), and a day of them sums by default. Its ends are readable as times of day (Time of Day): a sleep’s start reads as its bedtime, its end as its waketime. A Reading’s list steps work over intervals: coalesce joins pieces of a broken night, maxBy picks the longest, and a filter finds the one running at a clock time (Readings).

The Timeline visualization draws each day’s intervals as bars at their clock times on a 24-hour axis that starts at its Day Boundary (Time of Day), and the Calendar’s Week and Day views draw them as blocks.

Value Types · Sleep · Readings · Time of Day · Issues · Events · Entry