Dates and times

VERSION a9b38ce
Dates and times

A small calculator that doubles as a tour of the date and datetime variable types (see SPEC.md's "Date and datetime variables" and "Date pickers"). Two forms set the values; the blocks under them are autorun, so pressing Apply recalculates right away.

VariableTypeWhat it shows
START_DAY, END_DAYdatea calendar day, YYYY-MM-DD — the browser's date picker, or Ctrl-O in the terminal
OPENS, CLOSESdatetimean instant with its UTC offset, 2026-10-25T09:00:00+03:00

Both can be typed by hand or picked from a calendar. Either way a block only ever receives the canonical form, so it can be taken apart with plain shell parameter expansion — no date command, which differs between macOS and Linux. A value that is not a real day (2026-02-30) or a datetime without an offset is rejected, in the form, on the command line (--set START_DAY=2026-02-30) and in the cache.

Days between two dates

Calendar days from START_DAY to END_DAY (the second day is not counted; add one for an inclusive range). The two dates need not be in order.

field var="START_DAY" label="From"
field var="END_DAY" label="To"

The day number is the proleptic Gregorian "days since 1970-01-01" count, so leap years and month lengths come out right.

set -euo pipefail

# Days since 1970-01-01 of a canonical YYYY-MM-DD.
days_since_epoch() {
  local y=$((10#${1%%-*})) rest=${1#*-}
  local m=$((10#${rest%%-*})) d=$((10#${rest#*-}))
  (( m <= 2 )) && y=$((y - 1))
  local era=$(( (y >= 0 ? y : y - 399) / 400 ))
  local yoe=$(( y - era * 400 ))
  local doy=$(( (153 * ((m + 9) % 12) + 2) / 5 + d - 1 ))
  local doe=$(( yoe * 365 + yoe / 4 - yoe / 100 + doy ))
  echo $(( era * 146097 + doe - 719468 ))
}

diff=$(( $(days_since_epoch "$END_DAY") - $(days_since_epoch "$START_DAY") ))
abs=${diff#-}
if (( diff >= 0 )); then
  echo "$START_DAY → $END_DAY: $diff calendar days"
else
  echo "$END_DAY is $abs calendar days before $START_DAY"
fi
echo "= $(( abs / 7 )) weeks and $(( abs % 7 )) days; $(( abs + 1 )) if both end days are counted"
Hours between two moments

Elapsed time from OPENS to CLOSES. Each value carries its own offset, so the two may be in different time zones — the defaults are +03:00 and +01:00, and the answer is still the real elapsed time. A field in the terminal form accepts 2026-10-25 09:00 and attaches your local offset; the web form has an offset box next to the date and time.

field var="OPENS" label="Opens"
field var="CLOSES" label="Closes"
set -euo pipefail

days_since_epoch() {
  local y=$((10#${1%%-*})) rest=${1#*-}
  local m=$((10#${rest%%-*})) d=$((10#${rest#*-}))
  (( m <= 2 )) && y=$((y - 1))
  local era=$(( (y >= 0 ? y : y - 399) / 400 ))
  local yoe=$(( y - era * 400 ))
  local doy=$(( (153 * ((m + 9) % 12) + 2) / 5 + d - 1 ))
  local doe=$(( yoe * 365 + yoe / 4 - yoe / 100 + doy ))
  echo $(( era * 146097 + doe - 719468 ))
}

# Unix seconds of a canonical RFC3339 value (fractional seconds ignored).
unix_seconds() {
  local date=${1%%T*} rest=${1#*T}
  local clock=${rest:0:8} offset=0
  case $rest in
    *Z) ;;
    *) local tz=${rest: -6} sign=1
       [[ $tz == -* ]] && sign=-1
       offset=$(( sign * (10#${tz:1:2} * 3600 + 10#${tz:4:2} * 60) )) ;;
  esac
  echo $(( $(days_since_epoch "$date") * 86400 \
         + 10#${clock:0:2} * 3600 + 10#${clock:3:2} * 60 + 10#${clock:6:2} - offset ))
}

diff=$(( $(unix_seconds "$CLOSES") - $(unix_seconds "$OPENS") ))
abs=${diff#-}
span="$(( abs / 3600 )) h $(( abs % 3600 / 60 )) min"
if (( diff >= 0 )); then
  echo "$OPENS → $CLOSES: $span"
else
  echo "$CLOSES is $span before $OPENS"
fi
echo "= $abs seconds"