kip
In 2021 I started a new library called Tempo with the objective of modelling time as a set of intervals - not as instants. In 2022 I gave a talk on it at ElixirConf - its on YouTube. If you haven’t seen it you might find it a bit of fun.
After that, I made a lot of promises and didn’t keep any of them. Localize (ex_cldr), ExMoney, Image, Color, Unicode, Astro - there was always some other project to help me avoid the hard R&D work necessary to move Tempo along. Turns out modelling the complexities of time as interval sets and then implementing the set operators wasn’t so easy (for me).
Slow forward 5 years and here we are.
Tempo is coming (it will be ex_tempo on hex - I was too slow!). I’ll launch it on hex,pm this Thursday. 100% commitment on that.
Modelling time as an interval unlocks some very cool patterns and removes the cause of whole classes of bugs.
Along the way the only fully ISO8601 part 1 and part 2 compliant parser came into being (really, I purchased the ISO standards and went looking for prior art. Couldn’t find any in any language).
Importing iCalendar events as interval sets was straight forward thanks to the iCal library.
And lots of other fun features you’ll see on Thursday!
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #blog-post
- #ai
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming












Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
aseigo
So glad you are finding ICal useful!
And Ive been watching Tempo, too!
Very excited to see it hit hex …
Oh, it’s not you. Time is a demon. One of the “final boss” topics of devel … ha!
nathanl
@kip Woo, very exciting! That talk was amazing when I saw it back in 2022, and I’m really glad the ideas are coming to fruition.
kip
Tempo v0.2 — time as an interval, now with set operations and now on hex.
When we left off Tempo at ElixirConf '22 we demonstrated that a unified time type — a type that treats every temporal value as a bounded interval on a shared time line — removes a whole class of foot-guns that standard date libraries share. “End of day” ambiguity. Off-by-one at midnight. Last day of month. Cross-calendar comparisons. The awkward shuffle between
Date,Time,NaiveDateTime,DateTime.The basics still hold:
Tempo v0.2 — the other half of the thesis
Once every value is a bounded interval, set operations on time follow naturally — union, intersection, difference, symmetric difference, complement, and the corresponding predicates (
overlaps?,within?,disjoint?,adjacent?). v0.2 ships all of it, across zones, across calendars, across resolutions, with metadata that survives the operation.Which turns a surprising number of hard questions into short programs.
How many workdays in June 2026, in Australia, net of public holidays?
Swap
:AUfor:SAand you get the Saudi working week instead — Sunday to Thursday, with Saudi holidays. CLDR data is baked in; the set algebra is the same.When are Bruce and Shiela both booked next month?
Meet Bruce and Shiela. Their two
.icsfiles are in the repo (demo/calendars/). Loading them:Every RRULE in those feeds — the standups, the yoga classes, the partners’ meetings — is fully expanded into concrete day-resolution intervals with the event’s summary, location, and attendee metadata attached. Bruce has 83 events; Shiela has 67.
No custom scheduling engine. No hand-rolled sweep-line. Just intervals on a time line and set algebra — the same operators you’d reach for if you were intersecting two sets of integers, applied to two sets of
[from, to)moments.Q3 Mondays, excluding public holidays
~o"1K"is ISO 8601-2 selection syntax — day-of-week selector. If you’re wondering what else you can shove in there, the built-in visualizer has a syntax-reference sidebar that explains every form; paste anything into the input and watch it decompose into coloured segments with plain-English labels.When was I both in Japan and enrolled at university?
Two intervals, two calendars, one question:
Does this dig layer overlap with that dynasty?
The approximate-end qualification qualifier ships through.
In this release
Set operators
Tempo.union/2,intersection/2,difference/2,complement/2,symmetric_difference/2.Predicates
overlaps?,within?,disjoint?,adjacent?,Tempo.Interval.compare/2.Tempo.select/2The composition primitive. Narrow any base span by an integer-list, range, Tempo projection, day-of-week pattern, or function. Negative indices count from the end. Returns an IntervalSet that plugs straight into the set ops.
Tempo.workdays/1,Tempo.weekend/1.These are Territory-aware, CLDR-backed.
workdays(t) ++ weekend(t)partitions the seven days of the week.iCalendar import
BY*rule,BYSETPOS,WKST,RDATE,EXDATE. thanks to the wonderful iCal.DST-aware enumeration
Gap hours are skipped; fold hours are emitted twice with distinct offsets. The wall clock is authoritative; UTC projects on demand. No silent drift when Tzdata ships a rule change for a future zone.
Leap-second metadata
spans_leap_second?/1,leap_seconds_spanned/1, and opt-in leap-aware duration on intervals.23:59:60is accepted only on those dates).IXDTF (RFC 9557)
[u-ca=hebrew]), and arbitrary tagged suffixes parsed and round-tripped.ISO 8601 Parts 1 & 2 + EDTF Levels 0–2
100% of the
unt-libraries/edtf-validatecorpus passes.Archaeological masks, uncertain/approximate qualifications, long years with exponents (
Y17E8), significant-digits notation — all parseable and queryable.Web visualizer
Tempo.Visualizer.Standalone.start(port: 4001)and paste any ISO 8601 / EDTF / IXDTF string. Every character is coloured by role (numbers, literals, qualifiers, syntax, separators), each component gets its own labelled box with a plain-English description, and a permanent sidebar explains the syntax with copy-pasteable examples.The visualizer is most definitely still a work in progress.
Thanks
Tempo stands on the shoulders of Calendrical, Localize, Tzdata, Astro, and ical — each of which does the hard calendrical / locale / zone / recurrence work that Tempo’s set algebra builds on.
jswanner
Amazing work, @kip!
christhekeele
This looks amazing!
I noticed in the documentation you cite
tsrange/tsmultirangeas part of your inspiration, my first question on hearing about Tempo was if support for corresponding postgresEcto.Types would be viable forTempo/`Tempo.Interval/Tempo.IntervalSetstructs.A couple of notes on easily-remediable just-released issues:
Installation issue
This does not work as advertised:
It fails with
I would have the
defmodule Tempo.Visualizerbe wrapped in the usualif Code.ensure_loaded?(Plug)check and mentionPlugbeing optional too in the documentation.This works as expected:
Sigil issue
I found I have to add this line for most examples to work, but needing it is not documented anywhere (that I could find):
I notice you use
import Tempo.Sigilto make this work in ex. doctests. I would recommend making ause Tempo.Sigil__using__macro to auto-import just the sigils (not the bare import, as you’ve put extra functions in that module). I would also mention using it at the start of the README and each individual guide, and even consider adding it as the first line of all doctest examples (and removing it from the doctest setup) as well for rigor. Make it impossible for someone to copy-paste some example code and experience an error!(Actually while the public API is still fresh and malleable, I’d recommend naming the module in a pluralized form called
Tempo.Sigilswith just the sigils and put the helper non-sigil function somewhere else so that module could be pleasantly imported asimport Tempo.Sigils. And then still create a__using__macro forTempoitself to do the sigil import, so you can install that callback hook in more apps early on if you ever decide to do other similar things that would benefit from it. But that’s a more opinionated take.)Source code linking issue
You have not pushed the
v0.2.0git tag to github, so source code links from the exdocs do not work, ex: tempo/lib/tempo.ex at v0.2.0 · kipcole9/tempo · GitHubchristhekeele
Another thought while the API is malleable: I might consider renaming the
Tempo/Tempo.Intervalcompare/2function.Providing a
compare/2function that returns:lt | :eq | :gtis the standard signature for custom sorting of structural types in the ecosystem and plays nicely withEnum.sort/2(module variation) . Providing a similar function for your types with different return values is surprising: you probably want the function to work with sort/2 or be named something different. Additionally, any future work (conventions, libraries, stdlib) to make developer-defined types work with the comparison operators will almost certainly hinge upon structs with modules with functions with this signature.So I’d avoid “squatting” on the conventional name with a non-idiomatic return type. Perhaps
Tempo.relation/2instead? This would be more harmonious withTempo.IntervalSet.relation_matrix/2anyhow. And, if you find that you can condense the Allen relations to some reasonable approximation of a one-size-fits-all:lt | :eq | :gt(which I am uncertain of), wrapTempo.relation/2into acompare/2function for that purpose.Library users can always invoke
Enum.sort/2(functional variation) with an anonymous function that resolves theTempo.relation/2into:lt | :eq | :gtas appropriate for their domain.christhekeele
Worth pointing out you depict the internal representation of your structs in the Holidays guide > Expressing the Q3 window.
I definitely recommend making sure at no point in your documentation do you recommend or demonstrate constructing or pattern matching via your structs fields directly; always prefer a sigil/functional construction interface in case your struct internal representation changes (they’re very public private fields, library struct fields).
This famously caused a lot more pain than needed when
Range’s internal representation changed. If there isn’t a clean, example-friendly functional API to build your structs the way your examples require, that’s a sign that a new construction helper function would be valuable. But def try to avoid leaking internals so people following your examples in an earlier version don’t have their code break on upgrade!In the same vein, I’d make sure that your code comments hide this structure, like using
~o"2026Y7M8D/2026Y7M9D"as the display oftargetat this point in the guide.kip
Couldn’t agree more. I thought I had eradicated all of those so definitely a lack of precision on my part. During development the API surface evolved in part to avoid this precise point.
Thanks too for all the great feedback, truly appreciated. I will get a new version out in the next few hours addressing all your very valuable points.
kip
Done.
On it, good callout.
Agreed, will do.
I definitely have that objective but I haven’t got a clear strategy in mind yet since not all intervals map directly given the variable resolution of a tempo interval. Suggestions most welcome.
I’ll make sure it’s fully optional. It should be, so definitely a bug. Is supposed to require Bandit and Plug as a compilation guard on the module.
And lots of doc improvements you kindly pointed on. Sleep and coffee and then back on to it…
christhekeele
The best solution that springs to mind is having an Ecto parameterized type with a resolution config.
On cast from Tempo → type, you’d want some method of either raising if given more precision, lossily discarding extra precision, some sort of rounding strategy, or configuration to select between the three. Similarly on load from database → type.
I’ll probably play with this some in a side project that’s been wanting something like Tempo when I find the time, if you don’t beat me to it!