Uncertain dates with Tempo

Copy Markdown View Source
Mix.install([
  {:ex_tempo, "~> 2.0"},
  {:tz, "~> 0.28"}
])

Calendar.put_time_zone_database(Tz.TimeZoneDatabase)

Setup

Run this cell next: every example below depends on it. It imports Tempo.Sigils, which provides the ~o sigil the examples use to write a Tempo value as ISO 8601 text — ~o"2026-06-15" is the whole day of 15 June 2026. The import brings in only sigil_o/2, no other functions.

It needs a cell of its own because Livebook compiles a whole cell before running it, so the setup cell above can't import a module that its own Mix.install/2 has yet to fetch.

import Tempo.Sigils

Real data is not precise

Historians, archivists, genealogists, and anyone parsing messy records meet dates like "sometime in the 1560s", "the 15th of some month in 1985", "1984 or 1986", "around 2000, give or take a year". Most libraries force these into a false precision. Tempo holds each one as it is written — as ISO 8601-2 / EDTF values with real semantics — and answers questions about them without pretending to know more than the data does.

Masks — unknown digits

An X is an unknown digit. ~o"156X" is some year in the 1560s — one value spanning the whole decade, iterable and comparable like any other:

Tempo.explain(~o"156X") |> IO.puts()

A mask above concrete finer units is subtler: ~o"1985-XX-15" is the 15th of some unknown month of 1985. One actual day occurred — we just don't know which of twelve candidates. Converting expands the candidate set:

{:ok, candidates} = Tempo.to_interval(~o"1985-XX-15")
Tempo.IntervalSet.count(candidates)

One-of sets — enumerated possibilities

When the record says "1984 or 1986", that's not a span — it's an epistemic choice between listed values:

Tempo.explain(~o"[1984,1986]") |> IO.puts()

Margins — plus or minus

"Around 2000, give or take a year" is a value with an uncertainty margin. The margin is not decoration — it widens every question you ask. A value of 2000±1Y could ground anywhere in 1999–2001, so overlapping 1999 is possible, and overlapping 1997 is not:

around_2000 = ~o"2000±1Y"

{
  Tempo.certainty(:overlaps, around_2000, ~o"1999"),
  Tempo.certainty(:overlaps, around_2000, ~o"1997")
}

Qualifications — uncertain and approximate

EDTF's ? (uncertain) and ~ (approximate) attach to values and travel through operations:

{~o"1984?", ~o"2004~"}

Asking questions that respect the uncertainty

Here is the point of all this representation: Tempo.certainty/3 gives three-valued answers — :certain, :possible, or :impossible — computed over every grounding the uncertain value admits.

Was some year of the 20XXs within 2000 through 2099? Every reading is:

Tempo.certainty(:within, ~o"20XX", ~o"2000Y/2099Y")

Move the window on by a year and one reading, 2000, is outside it — so the answer is no longer certain:

Tempo.certainty(:within, ~o"20XX", ~o"2001Y/2100Y")

Do two ±-qualified events overlap? Depends how far apart they are:

{
  Tempo.certainty(:overlaps, ~o"2000±1Y", ~o"2001±1Y"),
  Tempo.certainty(:overlaps, ~o"2000±1Y", ~o"2010±1Y")
}

Tempo.certainly?/3 and Tempo.possibly?/3 are the two yes-or-no forms of the same question:

{
  Tempo.certainly?(:before, ~o"2000±1Y", ~o"2010"),
  Tempo.certainly?(:before, ~o"2000±1Y", ~o"2001"),
  Tempo.possibly?(:before, ~o"2000±1Y", ~o"2001")
}

A worked example — dating an artifact

A ledger entry is dated the 15th of some month in 1985. A fire destroyed the archive on June 20, 1985. Could the entry have been written before the fire? Must it have been?

entry = ~o"1985-XX-15"
fire = ~o"1985-06-20"

{
  Tempo.certainty(:before, entry, fire),
  Tempo.possibly?(:before, entry, fire),
  Tempo.certainly?(:before, entry, fire)
}

"The entry possibly precedes the fire — the January-through-June 15ths do, the July-through-December ones don't — but not certainly. If the fire had been in 1986, every candidate would precede it:"

Tempo.certainly?(:before, entry, ~o"1986-01-01")

Where next