I've started re-familiarizing with elixir for fun. Currently, I'm playing around trying to build a little dashboard that lets me track things like my sleep and other fun stuff.

When I submit how my sleep was, I want it to disappear from the dashboard — it only really needs to show up once a day, right?

So, I got that started without too much trouble:

  def logged_sleep_today? do
    date = Date.utc_today()
    beginning_of_day_time = ~T[00:00:01.000]
    end_of_day_time = ~T[23:59:59.000]
    beg_of_day = NaiveDateTime.new!(date, beginning_of_day_time)
    end_of_day = NaiveDateTime.new!(date, end_of_day_time)

    res = Repo.all(from s in Sleep, where: s.inserted_at < ^end_of_day and s.inserted_at > ^beg_of_day)
    Enum.count(res) > 0

My above query is naive in a few ways:

  • First of all and most importantly: it doesn't handle for timezones. Elixir stores DateTime's without timezones, in UTC. That means if I log how my sleep was at 8pm during the day, it will register as a tomorrow's sleep. That doesn't help anybody.
  • I didn't know that Elixir 1.15 had come out and it has nifty functions for beginning_of_day and end_of_day
  • I probably shouldn't be using Repo.all, right? I just need one result.

I'm a little rusty. So, I did some research while I was stuck in an airport the other day and learned that with Elixir you should install a "TimeZone" database. I've been lucky and haven't had to work with timezones much in my programming career, so I'm learning things.

So here's what I know now:

  • If I log my sleep at 9pm on June 01, 2023, It will go into the database as being inserted at something like 2 or 3 am on June 02, 2023.
  • When I reload my dashboard, it will fetch this latest insertion.
  • Then, I can create a new DateTime like so: DateTime.from_naive(latest_sleep.inserted_at, "America/Toronto")
  • Which will print something like this: #DateTime<2023-07-02 03:47:05-04:00 EDT America/Toronto>. So, it's still reading out like it's at 3:47am, but (I think) the -04:00 is to say that we need to subtract four hours, thus making the insertion time 11:47:05.
  • I can get the "current time" by running DateTime.utc_now()
  • With that current time, I should convert it to my timezone, to get my "local time".
  • Originally, I was going to check to see if the sleep's inserted_at time was between 12:00:01 and 23:59:59, but really, I just need to see if the date (not the time) of my sleep matches the date it currently is (in my timezone). So, we can manually change the date data in the DateTime struct to reset it to 0. I don't like, but I find it hard to wrap my head around using NaiveDateTime's "beginningofday" function and then converting it back to a DateTime with a timezone.

Here's what I've ended up with:

  def logged_sleep_today?() do
    latest_sleep = Sleep |> last(:inserted_at) |> Repo.one()

    if latest_sleep do
      with {:ok, sleep_dt} <- DateTime.from_naive(latest_sleep.inserted_at, "America/Toronto"),
           {:ok, now} <- DateTime.now(sleep_dt.time_zone) do

        beg_of_day = %{now | hour: 0, minute: 0, second: 0}
        case DateTime.compare(sleep_dt, beg_of_day) do
          :lt -> false
          :gt -> true

It's not great, but it works. Right now I'm hard-coding in my timezone, but I'll have to think of a better way to detect that - or just manually have myself set it as a user-setting. (Although, auto-detect based on browser is perhaps possible, and would be better.)

Ok, that's it! I was really fumbling over this, and it turned out to be about 10 lines of code. Writing it out really helped me figure this one out!

Thanks for reading 👋 !