Representing Durations as strings

TL;DR I’d like:

  • Duration to have a formal to_formatted_s method to more finely control output,
  • methods that generate Ranges with TimeWithZones at either end (similar to all_day)
  • flexibility in one or both of the above to handle non-overlapping ranges (all_day isn’t exactly 24.hours long)
  • a method to determine the Duration of a Range.

I’ve been working on a class that is concerned with a range of time. This can either be relative to the current time (e.g. the past 1.hour), or a fixed window (Time.zone.now.all_day). In either case, I also want to represent this range of time to the user (e.g. to say In the last 1 hour or every 24 hours).

Some things I’ve come up against with this include:

  • converting between a Duration and a Range. In the same way that there is 1.hour.ago, it would be nice to have similar methods that semantically capture differences in time as ranges. Where I’ve done so manually, especially relative to the current time, I’ve needed to use then to make sure I’m working relative to the same moment in time if I’m using Time.zone.now:

    Time.zone.now.then { |time| duration.before(time).advance(subsec: FRACTION_OF_A_SECOND)..time }
    
  • converting Durations to Strings. For my use case, it’s been fine to use duration.in_hours.ceil (more on the ceil later) to capture a number of hours. But it’d be good to have all the flexibility of ActiveSupport::Duration#inspect to hand, as the logic is there to create much nicer output, potentially. For example, in the following scenario:

    ActiveSupport::Duration.build(36.hours.in_seconds).inspect
    => "1 day and 12 hours"
    

    It would be nice to be able to set some upper limit on build to make sure it doesn’t coerce what could be 24.hours into 1.day and return the string 36 hours.

  • combinations of the above. Where I’ve manually created Ranges based on Durations, e.g.:

    Time.zone.now.then { |time| duration.before(time).advance(subsec: FRACTION_OF_A_SECOND)..time }
    

    I’ve needed to do as I’ve done in that example there and advance the start or end of the range forwards or backwards by a fraction of a second so that it’s not an overlapping period of time. That is to say, Time.zone.now.then { 24.hours.after(_1).._1 } is not equivalent to all_day when called precisely at midnight, because the end of the range will be exactly midnight the next day, as opposed to a split-second before (23:59:59.999...) as we see in all_day.

    This is something that I hope methods that generate Ranges bounded by TimeWithZones would take into account and allow me not to worry about, so that I don’t need a FRACTION_OF_A_SECOND constant in my app.

I’m happy to write failing tests for this and perhaps even resolve it upstream if there’s demand.