1

I have a time period in two variables, from start_date to end_date. Is there an easy way to split it into a smaller periods, rounding the values up.

Here's an example:

I have period from 15.01.2016 to 10.06.2016. I want to split it into months, so I will have six periods:

01.01.2016 - 31.01.2016  
01.02.2016 - 31.02.2016   
01.03.2016 - 31.03.2016  
01.04.2016 - 31.04.2016  
01.05.2016 - 31.05.2016  
01.06.2016 - 31.06.2016 

I want to include the time between 01.01.2016 and 15.01.2016 regardless of the fact that it is not in the original period.

I've been looking for some ideas, but at this time it seems that the only way is using the start date and iterating using DateAndTime::Calculations to determine the borders of intervals, until hitting the end date.

2
  • Please read "How to Ask" and "minimal reproducible example". When asking it's important to show evidence of your effort. That'd be the pages you searched and why those didn't answer your question, or the minimum code that demonstrates the problem and why it doesn't do what you want. Without that it looks like you're asking us to write code for you, which isn't what Stack Overflow is for. Also, please take the time to format your question for readability. It helps us help you, and helps those searching for a similar solution. Commented Oct 10, 2016 at 17:32
  • Maybe I didn't state myself clear in the question, but I didn't want the code. I had the idea how to solve (it is in the question), but is was so primitive, that I was pretty sure that someone will have a better one (like using group_by).
    – ZebThan
    Commented Oct 11, 2016 at 7:18

3 Answers 3

9
from = Date.parse('15.01.2016')
to   = Date.parse('10.06.2016')
(from..to).group_by(&:month).map do |group|
  group.last.first.beginning_of_month..group.last.last.end_of_month
end
# => [Fri, 01 Jan 2016..Sun, 31 Jan 2016,
#  Mon, 01 Feb 2016..Mon, 29 Feb 2016,
#  Tue, 01 Mar 2016..Thu, 31 Mar 2016,
#  Fri, 01 Apr 2016..Sat, 30 Apr 2016,
#  Sun, 01 May 2016..Tue, 31 May 2016,
#  Wed, 01 Jun 2016..Thu, 30 Jun 2016]

Or, map dates to strings, if you need string representation:

(from..to).group_by(&:month).map do |group|
  "#{group.last.first.beginning_of_month} - #{group.last.last.end_of_month}"
end

# => ["2016-01-01 - 2016-01-31",
#     "2016-02-01 - 2016-02-29",
#     "2016-03-01 - 2016-03-31",
#     "2016-04-01 - 2016-04-30",
#     "2016-05-01 - 2016-05-31",
#     "2016-06-01 - 2016-06-30"]

To get precisely what you want, you can format the string representation of the dates:

(from..to).group_by(&:month).map do |group|
  "#{group.last.first.beginning_of_month.strftime('%d-%m-%Y')} - #{group.last.last.end_of_month.strftime('%d-%m-%Y')}"
end
#=> ["01-01-2016 - 31-01-2016",
#    "01-02-2016 - 29-02-2016",
#    "01-03-2016 - 31-03-2016",
#    "01-04-2016 - 30-04-2016",
#    "01-05-2016 - 31-05-2016",
#    "01-06-2016 - 30-06-2016"]

EDIT

To make sure ranges do not mix up because of different years, include it in grouping along with month:

from = Date.parse('15.01.2016')
to   = Date.parse('10.02.2017')
(from..to).group_by {|a| [a.year, a.month]}.map do |group|
  "#{group.last.first.beginning_of_month.strftime('%d-%m-%Y')} - #{group.last.last.end_of_month.strftime('%d-%m-%Y')}"
end
# => ["01-01-2016 - 31-01-2016",
#  "01-02-2016 - 29-02-2016",
#  "01-03-2016 - 31-03-2016",
#  "01-04-2016 - 30-04-2016",
#  "01-05-2016 - 31-05-2016",
#  "01-06-2016 - 30-06-2016",
#  "01-07-2016 - 31-07-2016",
#  "01-08-2016 - 31-08-2016",
#  "01-09-2016 - 30-09-2016",
#  "01-10-2016 - 31-10-2016",
#  "01-11-2016 - 30-11-2016",
#  "01-12-2016 - 31-12-2016",
#  "01-01-2017 - 31-01-2017",
#  "01-02-2017 - 28-02-2017"]
5
  • Ok, it works perfectly for months, and that makes the answer correct, so I'm marking it as it. But is it possible to group it by quarters instead of months?
    – ZebThan
    Commented Oct 10, 2016 at 12:58
  • 1
    @ZebThan yea, I think you will have to define a method that identifies date's quarter, and group based of it. Google it and you will probably find some solutions. Otherwise you can always ask a new question :) Commented Oct 10, 2016 at 13:02
  • 1
    @ZebThan standard quarters would be as simple as (month-1).divmod(3).first + 1. eg. Date.today.quarter = 4 because (10-1).divmod(3) = [3,0] then _.first = 3 and add 1 because quarter 0 is not a thing. Commented Oct 10, 2016 at 14:59
  • @AndreyDeineko - I found a flaw in your solution. If I have a period longer than a year, months (or quarters) from different years will mix up. Can you please update, so it's more like this? (from..to).group_by{|i| "#{i.month}-#{i.year}"}.map
    – ZebThan
    Commented Oct 11, 2016 at 8:50
  • @AndreyDeineko start date and end date doesn't match in the response with the input. Start date ( input - 15.01.2016, output - 01-01-2016) and End date ( input - 10.02.2017, output - 28-02-2017). How can I produce the response with matching of start and end date?
    – Galet
    Commented Feb 20, 2019 at 10:19
1

This approach avoids the need to examine every day in the date range and returns an array of strings in the desired format.

Date#>> shifts the date forward by the number of months given by the argument. Here I've shifted dates by one month (e.g., Date.parse("14-01-2016") >> 1 #=> #<Date: 2016-02-14...>). If the last day of the following month is greater than the last day of the given month, the last day of the following month is returned (e.g., Date.parse("30-01-2016") >> 1 #=> #<Date: 2016-02-29...>).

Code

require 'datetime'

def convert(sdate, edate)
  dfirst  = Date.strptime(sdate, "%d.%m.%Y")
  dlast   = Date.strptime(edate, "%d.%m.%Y")
  nmonths = 12*dlast.year + dlast.month - 12*dfirst.year - dfirst.month + 1
  dlast   = dfirst.end_of_month
  nmonths.times.map { |i|       
    "%s - %s" % [(dfirst >> i).strftime("01.%m.%Y"), (dlast >> i).strftime("%d.%m.%Y")]}
end

Example

convert "15.01.2016", "10.06.2016"
  #=> ["01.01.2016 - 31.01.2016", "01.02.2016 - 29.02.2016",
  #    "01.03.2016 - 31.03.2016", "01.04.2016 - 30.04.2016",
  #    "01.05.2016 - 31.05.2016", "01.06.2016 - 30.06.2016"]

Explanation

The steps are as follows.

sdate = "15.01.2016"
edate = "10.06.2016"

dfirst  = Date.strptime(sdate, "%d.%m.%Y")
  #=> #<Date: 2016-01-15 ((2457403j,0s,0n),+0s,2299161j)> 
dlast   = Date.strptime(edate, "%d.%m.%Y")
  #=> #<Date: 2016-06-10 ((2457550j,0s,0n),+0s,2299161j)> 
nmonths = 12*dlast.year + dlast.month - 12*dfirst.year - dfirst.month + 1
  #=>       12*24192    +     6       -   12*24192     -      1       + 1
  #=> 6 

Change dlast to last day of first month.

dlast   = d.first.end_of_month
  #=> #<Date: 2016-01-31 ((2457419j,0s,0n),+0s,2299161j)>

Map each month to the desired format.

nmonths.times.map { |i|       
  "%s - %s" % [(dfirst >> i).strftime("01.%m.%Y"), (dlast >> i).strftime("%d.%m.%Y")]}
  #=> ["01.01.2016 - 31.01.2016", "01.02.2016 - 29.02.2016",
  #    "01.03.2016 - 31.03.2016", "01.04.2016 - 30.04.2016",
  #    "01.05.2016 - 31.05.2016", "01.06.2016 - 30.06.2016"]
1
  • The calculation of nmonths is pure Ruby. There may be an easier way to compute that in Rails, but I don't know Rails (except for a few things like beginning_of_month and end_of_month. Commented Oct 10, 2016 at 20:21
0

As mentioned in this thread: Is there a standard for inclusive/exclusive ends of time intervals? We should prefer inclusive start dates and exclusive end dates.

We can split a DateRange into days, weeks, months or years with the following tail recursive method:

  class DateRange
    def initialize(since_date, until_date)
      @since_date = since_date
      @until_date = until_date
    end

    def split_to(timespan)
      return [] if @since_date == @until_date
      end_date = [@since_date.send("next_#{timespan.to_s.singularize}"), @until_date].min
      [DateRange.new(@since_date, end_date)] + DateRange.new(end_date, @until_date).split_to(timespan)
    end
  end

This method takes :days, :weeks, :months or :years as a parameter. Notice that we rely on activesupport.

Not the answer you're looking for? Browse other questions tagged or ask your own question.