moneybird / active-date-range

Powerful DateRanges for Ruby and ActiveSupport
MIT License
50 stars 0 forks source link

Inspiration #4

Open mollerhoj opened 3 years ago

mollerhoj commented 3 years ago

Hey, I just stumbed upon this. We have a very similar class in our code base - It might be helpful to look at some of the ideas we've been discussing:

I hope we can get this into ActiveSupport so we don't have to come up with our own classes all the time ;-)

module Umr
  class DateRange
    attr_reader :since_date # Inclusive
    attr_reader :until_date # Exclusive

    def initialize(since_date, until_date)
      raise "Invalid date range: #{since_date} ; #{until_date}" if since_date > until_date

      @since_date = since_date
      @until_date = until_date
    end

    def inclusive_since_date
      since_date
    end

    def exclusive_until_date
      until_date
    end

    def exclusive_since_date
      since_date.yesterday
    end

    def inclusive_until_date
      [until_date.yesterday, since_date].max
    end

    def inspect
      "#<Umr::DateRange #{since_date}, #{until_date}>"
    end

    def self.parse(since_date_str, until_date_str)
      new(Date.parse(since_date_str), Date.parse(until_date_str))
    end

    # TODO Change to take singular argument
    def split_to(timespan)
      return [] if @since_date == @until_date
      unit = timespan.to_s.singularize
      end_date = [@since_date.send("next_#{unit}").send("at_beginning_of_#{unit}").to_date, @until_date].min
      [DateRange.new(@since_date, end_date)] + DateRange.new(end_date, @until_date).split_to(timespan)
    end

    def split_by(duration)
      return [] if @since_date == @until_date
      end_date = [@since_date + duration, @until_date].min
      [DateRange.new(@since_date, end_date)] + DateRange.new(end_date, @until_date).split_by(duration)
    end

    def in_days
      split_to('days')
    end

    def in_months
      split_to('months')
    end

    def in_weeks
      split_to('weeks')
    end

    def in_years
      split_to('years')
    end

    def size
      (until_date - since_date).to_i.days
    end

    def ==(other)
      @since_date == other.since_date && @until_date == other.until_date
    end

    def cover?(date)
      (since_date..until_date).cover?(date)
    end

    def intersection
    end

    def subtraction
    end

    def merge
    end
  end
end
edwinv commented 3 years ago

Sorry for my late reply, somehow I didn't get an email from this topic.

I can see why [A, B) is appealing. We decided to go with [A, B] because this is how databases handle Ruby ranges by default. where(date: range) will become WHERE data BETWEEN begin AND end and is inclusive.

    1. I'm not sure about the 2012-02-01..2012-03-01 format. Is .. standard? We've been using self.parse(start, end) instead.

The .. is standard for Ruby ranges:

> (1..2).to_s
 => "1..2" 
    1. split_to is great - it looks like you've done the same thing with in_groups_of. Keep that name, it's better.

👍

    1. Support for finding theintersection of two date ranges, as well as subtracting two date ranges could also be useful.

Great ideas, will work in those!

    1. Finally, you might want to consider allowing open ended date ranges. This is something we are about to implement. Having a notion of "All future days from this date" or "All past days from this date" can be very useful.

I've actually been working on this, you can see the progress in this PR: #5

I hope we can get this into ActiveSupport so we don't have to come up with our own classes all the time ;-)

We did the same and came up with a gem. I'm not sure how useful the DateRange is for every Rails app to make it into Rails Core. Have been discussing this with Rails core members.

edwinv commented 3 years ago

In #6 you can find the intersection support. Subtracting is a bit more difficult, will look into that later!

mollerhoj commented 3 years ago

Great! We'll work on replacing our own code with this gem, let's hope it becomes somewhat of a standard!

edwinv commented 3 years ago

Can you maybe explain a bit further what the use case is for subtracting date ranges? There are many edge cases that might make this function not behave like expected. It might be a challenge to make sure these edge cases are handled correctly in the caller of the subtraction method.

mollerhoj commented 3 years ago

It's useful in scenarios where we want to do calculations on date ranges as if they were sets. Operations such as union, intersection, subtraction, can be used to answer questions such as:

For union and intersection, I would expect a single range to be returned. For subtraction, guess an array of either 0, 1 or 2 ranges would make sense?

mollerhoj commented 3 years ago

I'd suggest supporting different settings for inclusive/exclusive bounds: See postgres documentation: https://www.postgresql.org/docs/9.3/rangetypes.html

Should also affect to_s and inspect by printing either (),[],[) or (]