Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

There was a problem

You must be a member of this account.

This project is archived and is in readonly mode.

Finding the days/weeks/months/years between two dates

#879

I recently needed to be able to calculate (in integers) the days, weeks, months and years between two dates, and to do it precisely (i.e. taking account of different lengths of months and years).

I couldn't find a way to do this with anything in active support or elsewhere, so I wrote my own extensions to the Date class. (See the attached file.)

If I were to work up a patch (with tests of course), would this be something you would like to add to active support?

Reported by Jon Leighton · August 21st, 2008 @ 05:12 PM

State: hold
Milestone: 2.x
Assigned to: Geoff Buesing Geoff Buesing
Importance: none

Activity

  1. Robby Russell
    Robby Russell

    Thanks for sharing this. I've needed something like this af ew times and just tossed this into lib/ for now. Would support having something like this in Rails.

    +1

    October 8th, 2008 @ 10:03 PM

  2. Dan Barry
    Dan Barry

    I've attached a patch that could add this to ActiveSupport.

    I don't see a way to multiply a duration by an integer, so I can't apply an optimization similar to http://github.com/danbarry/recur..., making the runtime of this function increase linearly compared to the duration instead of logarithmically.

    I'm going to take a little time to look at how Duration works and possibly add multiplication by an integer to it. If someone knows of a fast way to multiply a duration by an integer that I overlooked, please tell me so I don't reinvent the wheel.

    October 13th, 2008 @ 03:04 AM

  3. Dan Barry
    Dan Barry

    I've added * and / to duration, so I was able to optimize these calculations. A new patch has been attached with the optimizations.

    October 13th, 2008 @ 04:15 AM

  4. Pratik
    Pratik
    • Milestone cleared.
    • Assigned user set to Geoff Buesing

    January 18th, 2009 @ 06:54 AM

  5. Geoff Buesing
    Geoff Buesing

    Seems like this could be useful stuff. But I think the idea needs to start off in a plugin first, so that we'll better understand the common use cases that we can potentially extract into core.

    Some questions I have:

    1. should these methods return absolute values, or should they return negative values if the start is later than the finish (like Time#-)? I notice that Jon's patch returns negative values in this case, whereas Dan's patch returns absolute values, which suggests that we don't have common use cases here.

    2. Are months and years treated as fixed values (i.e., always 30 days and 365 days, respectively?) Assuming this, would we get incorrect results for #months_between and #years_between if we're dealing with larger distances between start and finish dates?

    3. What use cases do these methods address that distance_of_time_in_words doesn't already address?

    4. Should we restrict these calculations to just Date objects (as is the case with both of these patches? Seems like they'd be useful and applicable to Time objects as well.

    5. What about #hours_between, #minutes_between, #seconds_between? Dan's patch accepts any arbitrary Duration, which seems to indicate that you could pass in durations of less than a day, but you get an error when you try do do this.

    January 19th, 2009 @ 03:37 PM

  6. Geoff Buesing
    Geoff Buesing

    Also: @Dan Barry, just curious, where does the mysterious 512 multiplier in your patch derive from? :-)

    January 19th, 2009 @ 03:40 PM

  7. Geoff Buesing
    Geoff Buesing

    Also just noticed the Duration * and / methods in @Dan Barry's patch -- I think that that could be extracted into its own patch -- would be nice if

    
    5.days * 3
    

    returned a Duration of 15.days, just like

    
    5.days + 5.days + 5.days
    

    does now.

    January 19th, 2009 @ 03:51 PM

  8. Dan Barry
    Dan Barry

    @Geoff

    1. I'm not sure if this should return negative or absolute values. To me, "between two dates" sounds like it would be absolute, but "between the start date and end date" sounds like it would include negative values.

    2. Months and years are not treated as fixed values.

    3. This would be applicable to times as well. That raises the question of whether the function go in Date, Time, DateTime, or Duration. I'm thinking the latter, but can't come up with a good name for the function. Duration.num_between ?

    4. What error did you get? I wrote

    
      def test_durations_between_with_minutes
        t = Time.now
        assert_equal 2, Date.durations_between(t, t + 2.minutes, 1.minute)
      end
    

    which passed.

    1. I picked 512 as the starting value since finding the number of days between two dates that are ~2 years apart was the use case with the largest distance that I'd expect to see. If someone can think of a different way to optimize this (preferably that's also more legible and easy to comprehend), that would be awesome.

    I've attached the patch for just Duration#* and Duration#/.

    January 19th, 2009 @ 05:54 PM

  9. Dan Barry
    Dan Barry

    The numbering on my comments should 1, 2, 4, 5, 6, but the markdown interpreter converted them to an ol and they got reordered 1, 2, 3, 4, 1. So, my 3rd response is in reference to your 4th question, and my 4th to your 5th.

    January 19th, 2009 @ 05:59 PM

  10. Jon Leighton
    Jon Leighton

    I think it should return negative values and be clear about this in the documentation. I agree with Dan that's it's a question of perception, but returning an absolute value removes information whereas a negative value can always be made absolute later on.

    What about introducing a new kind of Duration which knows about its start and end dates? I don't know about a name... maybe TimeSpan? Although perhaps "time span" is just a synonym for "duration" anyway. AnchoredDuration? (I am trying to think of a way to express the fact that this duration does not exist in isolation, but is "fixed" to the time line.)

    Anyway, with this new class, you could do something like:

    @ActiveSupport::AnchoredDuration.new(Time.now, Time.now + 3.months).years # => 0.25@

    In terms of applicability in light of duration_of_time_in_words, personally I used it to find recurring dates. If people say "every 3rd of the month" they really mean on the 3rd day of every month rather than a start_date + x * 30. So this code was useful for that.

    I agree that Duration#* and Duration#/ is a useful addition in isolation.

    I would also add that I think the durations_between method is too clever really. If I recall correctly, when I originally implemented this I tried something abstract like that but concluded it was better to have separate code for each "time unit", especially given my code will run in constant time. Without thinking about it too hard, it seems like it would be harder to make the durations_between method run in constant time.

    Also, I haven't entirely wrapped my head around the code in durations_between, but it does seem that the 512 thing causes problems:

    @@@>> durations_between(Time.now, Time.now + 1.day, 1.year) RangeError: time + 16157491200.000000 out of Time range

    from /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/core_ext/time/calculations.rb:268:in `plus_without_duration'
    from /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/core_ext/time/calculations.rb:268:in `+'
    from ./test.rb:6:in `durations_between'
    from (irb):11@@@
    
    

    This is because the code essentially does check = start + 512.years initially. (Maybe you are on a 64 bit machine Dan?)

    Also, there is a rounding error, presumably from the division in the last line:

    @@@durations_between(Time.now, Time.now + 1.day, 1.day) => 1.00000000003472@@@

    Sorry for this slightly convoluted response, hopefully it makes (some) sense!

    January 19th, 2009 @ 07:29 PM

  11. Jon Leighton
    Jon Leighton

    Crap, I should really read the formatting help before guessing things. A preview would be nice too. Here are those three pieces of code for readability's sake:

    
    ActiveSupport::AnchoredDuration.new(Time.now, Time.now + 3.months).years # => 0.25
    
    
    >> durations_between(Time.now, Time.now + 1.day, 1.year)
    RangeError: time + 16157491200.000000 out of Time range
    	from /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/core_ext/time/calculations.rb:268:in `plus_without_duration'
    	from /usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/core_ext/time/calculations.rb:268:in `+'
    	from ./test.rb:6:in `durations_between'
    	from (irb):11
    
    
    >> durations_between(Time.now, Time.now + 1.day, 1.day)
    => 1.00000000003472
    

    January 19th, 2009 @ 07:31 PM

  12. Dan Barry
    Dan Barry

    AnchoredDuration sounds like a great idea. It could have built in #years, #months, etc., and also allow division by a Duration. So, you could do

    
    ActiveSupport::AnchoredDuration.new(Time.now, Time.now + 1.year) / 6.months # => 2
    

    years would be equivalent to /(1.year), but would be slightly more legible.

    January 19th, 2009 @ 08:40 PM

  13. DHH
    DHH
    • Milestone set to 2.x

    February 7th, 2009 @ 01:13 PM

  14. Geoff Buesing
    Geoff Buesing
    • State changed from new to hold

    A lot of different ideas in this thread. I don't think we know enough about the common use cases to extract anything into core yet -- best to try some of these ideas out in plugins first.

    AnchoredDuration is an interesting idea -- it sounds a lot like a Range; maybe you could subclass Range, and get some functionality for free (like the ability to pass it in to an ActiveRecord conditions hash)?

    @Dan -- your Duration division and multiplication patch could have its own ticket. The only problem I see with it is, we've deprecated Durations of fractional months and years, so we'd have to figure out what to do when we multiply a month/year Duration by a fractional arg, or divide it in a way that returns a remainder. Guess we could fall back on the existing behavior of returning seconds? Or we could disallow certain operations with month and year Durations.

    February 8th, 2009 @ 09:53 PM

  15. Levin Alexander
    Levin Alexander

    @geoff, I just made a ticket for that: #1916

    February 8th, 2009 @ 10:04 PM

  16. ssupreme11
    ssupreme11

    Its my first time to visit this site and as I was exploring I cant believe that this site was made up of a very informative articles that you should try to have compliment with so as what I am doing now I really love to look forward with more interesting information on this site..

    Regards,
    Phd Dissertation

    May 10th, 2011 @ 10:26 PM