How to sum (aggregate) from a `Time` attribute

I’ve been struggling to come up with an Ash aggregation or calculation that works on a Time attribute. The attribute in question:

    attribute :duration, :time do
      allow_nil? false
      public? true
    end

What I had thought would be a super-simple sum aggregation ended up… not working at all:

     * ** (Protocol.UndefinedError) protocol Enumerable not implemented for type Integer. This protocol is implemented for the following type(s): DBConnection.PrepareStream, DBConnection.Stream, Date.Range, Ecto.Adapters.SQL.Stream, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, Iter, Jason.OrderedObject, LazyHTML, List, Map, MapSet, Phoenix.LiveView.LiveStream, Postgrex.Stream, Range, Req.Response.Async, Rewrite, Stream, StreamData

Which led me down a path of a custom implementation, which became messy. Then I switched to a calculation. My calculation isn’t working yet, but it’s getting long and complicated and just seems like there should be a better (simpler) way.

I keep wondering if I’m missing something in the basic aggregate DSL syntax.

I can paste in my calculation if anyone wants to see it but at the moment it’s a mess. Hoping someone just points me at a better solution. :slight_smile:

Can you share the stack trace of that error? And show your sum aggregate?

Without more context, it’s little hard to understand how you would expect to sum :time. It represents a fixed point in time, whereas :duration is an abstract amount of time. :thinking:

I didn’t even notice :laughing:. Very good point.

Time more directly maps to what I’m measuring, which is an activity (period of work) represented as hours and minutes (HH:MM). That makes it very easy to render in the UX, which is (basically) a HH : MM “spinner” control. Plus, :duration Ash handling is a bit opaque but, it seems to wrap seconds (which would be harder for me to work with, since I’m thinking in “hours & minutes”) and also :time, which ends up being an unnecessary abstraction… Maybe I don’t understand :duration well enough.

Well… solved. :slight_smile: I was down a bad rabbit hole.

I realized my entire approach was just stupid.

The error was a silly mistake. The calculation was returning a single value – not a list containing the result. Oops. So that solved the calculation.

As for the other problem… After going down the rabbit hole a bit, I realized I have all the Activities in memory. Why not just reduce in the UX (silly to use Ash if they are all in memory, I guess)… So, I ended up with:

  def sum_activity_duration(activities) do
    activities
      |> Enum.reduce(~T[00:00:00], fn activity, acc ->
        seconds = activity.duration.hour * 3600 + activity.duration.minute * 60 + activity.duration.second
        # Add total seconds to the accumulator for display in UX `Time` control (HH:MM)
        Time.add(acc, seconds, :second)
      end)
  end

I may have to take a look at Duration. But since the UX thinks in “HH:MM” and works nicely with Time I’m not sure it would be a win.

:thinking: might be worth checking what happens if your times sum up to greater than 24 hours :wink:

2 Likes