This project is archived and is in readonly mode.

#879 ✓hold
Jon Leighton

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

Reported by Jon Leighton | August 21st, 2008 @ 05:12 PM | in 2.x

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?

Comments and changes to this ticket

  • Robby Russell

    Robby Russell October 8th, 2008 @ 10:03 PM

    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

  • Dan Barry

    Dan Barry October 13th, 2008 @ 03:04 AM

    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.

  • Dan Barry

    Dan Barry October 13th, 2008 @ 04:15 AM

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

  • Pratik

    Pratik January 18th, 2009 @ 06:54 AM

    • Milestone cleared.
    • Assigned user set to “Geoff Buesing”
  • Geoff Buesing

    Geoff Buesing January 19th, 2009 @ 03:37 PM

    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.

  • Geoff Buesing

    Geoff Buesing January 19th, 2009 @ 03:40 PM

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

  • Geoff Buesing

    Geoff Buesing January 19th, 2009 @ 03:51 PM

    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.

  • Dan Barry

    Dan Barry January 19th, 2009 @ 05:54 PM

    @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#/.

  • Dan Barry

    Dan Barry January 19th, 2009 @ 05:59 PM

    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.

  • Jon Leighton

    Jon Leighton January 19th, 2009 @ 07:29 PM

    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!

  • Jon Leighton

    Jon Leighton January 19th, 2009 @ 07:31 PM

    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
    
  • Dan Barry

    Dan Barry January 19th, 2009 @ 08:40 PM

    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.

  • DHH

    DHH February 7th, 2009 @ 01:13 PM

    • Milestone set to 2.x
  • Geoff Buesing

    Geoff Buesing February 8th, 2009 @ 09:53 PM

    • 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.

  • Levin Alexander

    Levin Alexander February 8th, 2009 @ 10:04 PM

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

  • ssupreme11

    ssupreme11 May 10th, 2011 @ 10:26 PM

    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

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

<h2 style="font-size: 14px">Tickets have moved to Github</h2>

The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>

Referenced by

Pages