Knobs are a convenient physical interface for adjusting scalar values. Compared to a 10-key they save space and make value clamping less confusing1. However, they are significantly more tedious for selecting random values within the supported range.

I wanted to mitigate this problem for my desk lamp’s pomodoro timer. Having each step of the encoder wheel increment the time by a fixed interval was obviously wrong, and velocity-based solutions never feel right. Turns out, humans set timers (and generally experience time) on a logarithmic scale. The Timers app on watchOS understands this, with defaults for 1:00, 3:00, 5:00, 10:00, 15:00, 30:00, 1:00:00, and 2:00:002—a huge range represented by a tiny number of values, with a relatively small perceived loss of resolution.

Expecting to reuse the pomodoro firmware for a reminders board someday, I defined a sequence of time intervals spanning from one second to one year3 with two design goals:

  1. One turn of the knob (24 steps) changes the time interval’s order of magnitude
  2. The values are maximally round.

The following Python code generates the sequence I settled on:

def time_intervals():
    minute, hour, day = 60, 60*60, 24*60*60
    start = 1                          # From 1s
    for step, end in [
        (1, 15),                       # to 15s in steps of 1s
        (5, minute+30),                # to 1m30s in steps of 5s
        (15, 5*minute),                # to 5m in steps of 15s
        (minute, 20*minute),           # to 20m in steps of 1m
        (5*minute, hour+30*minute),    # to 1h30m in steps of 5m
        (15*minute, 5*hour),           # to 5h in steps of 15m
        (hour, day),                   # to 24h in steps of 1h
        (4*hour, 3*day),               # to 3d in steps of 4h
        (day, 14*day),                 # to 14d in steps of 1d
        (7*day, 91*day),               # to 91d in steps of 7d
    ]:
        for i in range(start, end, step):
            yield i
        start = i + step
    for i in range(0, 9+1, 1):         # to 1y in steps of "a month"
        yield (91+round(i*30.417))*day

Since the human experience of time mixes so many bases, the increments used by different intervals are round factors of the prevailing unit—seconds and minutes count by 1, 5, and 15; hours by 1 and 4; days by 1, 7, and… “a month”. Months are an objectively terrible unit of measure, with an average length of 30.417 days. One would reasonably expect a monthlong timer to expire the same time of day4 it was set, so when incrementing by month the generator rounds to the nearest day. This results in a mix of increments (either 30 or 31 days) that naturally ends at 365 days.5

  1. Also: less prone to errors! Our oven’s timers will clock out an hour when set to 1:00, but if you punch in 60? After an hour it starts over and counts down a second hour. It will also accept 3:75 (I was trying to set the temperature)—what duration that represents is anyone’s guess. 

  2. Citation. My watch is littered with custom intervals, but they also follow this distribution. 

  3. Before someone links me to Falsehoods programmers believe about time, these are time intervals that compromise precision for ergonomics! Of course years aren’t 31,536,000 seconds long, but if you’re setting a timer for “a year” it’s close enough. Days aren’t 86,400 seconds long either, but good luck convincing your kids, pets, or plants otherwise! 

  4. ish. See previous footnote. 

  5. Which is a year. See previous footnote.