Related
Need to example how to calculate the count of days in a period splited by month.
For example:
Wed, 25 Nov 2020 : Tue, 15 Dec 2020 => [6 (nov), 15(dec)]
Thank you!
This would be a job for tally_by, but that is not added to Ruby (yet?).
tally works too:
require 'date'
range = Date.parse("Wed, 25 Nov 2020") .. Date.parse("Tue, 15 Dec 2020")
p month_counts = range.map{|d| Date::ABBR_MONTHNAMES[d.month] }.tally
# => {"Nov"=>6, "Dec"=>15}
date1 = Date.new(2020, 11, 25)
date2 = Date.new(2020, 12, 15)
(date1..date2).group_by { |date| [date.year, date.month] }
.map { |(year, month), dates| ["#{year}/#{month}", dates.length] }
=> [["2020/11", 6], ["2020/12", 15]]
What about the interval is so long that you have same months but of different years? I've added years because of this case.
This works in pure ruby too, you just need require 'date'
Code
require 'date'
def count_days_by_month(str)
Range.new(*str.split(/ +: +/).
map { |s| Date.strptime(s, '%a, %d %b %Y') }).
slice_when { |d1,d2| d1.month != d2.month }.
with_object({}) do |a,h|
day1 = a.first
h[[day1.year, Date::ABBR_MONTHNAMES[day1.month]]] = a.size
end
end
See Range::new, Date::strptime and Enumerable#slice_when.
Examples
count_days_by_month "Wed, 25 Nov 2020 : Tue, 15 Dec 2020"
#=> {[2020, "Nov"]=>6, [2020, "Dec"]=>15}
count_days_by_month "Wed, 25 Nov 2020 : Tue, 15 Dec 2021"
#=> {[2020, "Nov"]=>6, [2020, "Dec"]=>31, [2021, "Jan"]=>31,
# ...
# [2021, "Nov"]=>30, [2021, "Dec"]=>15}
Explanation
For the first example the steps are as follows.
str = "Wed, 25 Nov 2020 : Tue, 15 Dec 2020"
b = str.split(/ +: +/)
#=> ["Wed, 25 Nov 2020", "Tue, 15 Dec 2020"]
c = b.map { |s| Date.strptime(s, '%a, %d %b %Y') }
#=> [#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-12-15 ((2459199j,0s,0n),+0s,2299161j)>]
d = Range.new(*c)
#=> #<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>..
# #<Date: 2020-12-15 ((2459199j,0s,0n),+0s,2299161j)>
e = d.slice_when { |d1,d2| d1.month != d2.month }
#=> #<Enumerator: #<Enumerator::Generator:0x00007fb1058abb10>:each>
We can see the elements generated by this enumerator by converting it to an array.
e.to_a
#=> [[#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-11-26 ((2459180j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-11-30 ((2459184j,0s,0n),+0s,2299161j)>],
# [#<Date: 2020-12-01 ((2459185j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-12-02 ((2459186j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-12-15 ((2459199j,0s,0n),+0s,2299161j)>]]
Continuing,
f = e.with_object({})
#=> #<Enumerator: #<Enumerator: #<Enumerator::Generator:0x00007fb1058abb10>
# :each>:with_object({})>
f.each do |a,h|
day1 = a.first
h[[day1.year, Date::ABBR_MONTHNAMES[day1.month]]] = a.size
end
#=> {[2020, "Nov"]=>6, [2020, "Dec"]=>15}
The first element generated by f and passed to the block, and the block variables are assign values by the rules of array decomposition:
a,h = f.next
#=> [[#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-11-26 ((2459180j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-11-30 ((2459184j,0s,0n),+0s,2299161j)>],
# {}]
a #=> [#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-11-26 ((2459180j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-11-30 ((2459184j,0s,0n),+0s,2299161j)>],
h #=> {}
Key-value pairs will be added to h over the course of the calculations. See Enumerator#next. The block calculation is now performed.
day1 = a.first
#=> #<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>
g = day1.year
#=> 2020
i = day1.month
#=> 11
j = Date::ABBR_MONTHNAMES[day1.month]
#=> "Nov"
k = a.size
#=> 6
h[[g,j]] = k
#=> 6
resulting in:
h #=> {[2020, "Nov"]=>6}
The remaining steps are similar.
I would start by breaking the whole period into periods for each month.
Since Ruby has ranges, I'd write a helper method that takes a date range and yields month-ranges:
def each_month(range)
return enum_for(__method__, range) unless block_given?
date = range.begin.beginning_of_month
loop do
from = date.clamp(range)
to = (date.end_of_month).clamp(range)
yield from..to
date = date.next_month
break unless range.cover?(date)
end
end
clamp ensures that the range's bounds are taken into account when calculating each month's range. For Ruby version prior to 2.7 you have to pass the bounds separately:
from = date.clamp(range.begin, range.end)
to = (date.end_of_month).clamp(range.begin, range.end)
Example usage:
from = '25 Nov 2020'.to_date
to = '13 Jan 2021'.to_date
each_month(from..to).to_a
#=> [
# Wed, 25 Nov 2020..Mon, 30 Nov 2020
# Tue, 01 Dec 2020..Thu, 31 Dec 2020
# Fri, 01 Jan 2021..Wed, 13 Jan 2021
# ]
Now all we need is a way to count the days in each month-range: (e.g. via jd)
def days(range)
range.end.jd - range.begin.jd + 1
end
and some formatting:
each_month(from..to).map { |r| format('%d (%s)', days(r), r.begin.strftime('%b')) }
#=> ["6 (Nov)", "31 (Dec)", "13 (Jan)"]
i have this dates
"25th April 2019 01:01 AM"
"11th May 2019 07:28 AM"
"26th August 2019 11:07 AM"
"31st July 2019 01:26 PM"
ETC...
my try
timeStr = strings.Replace(timeStr,"th","",1)
timeStr = strings.Replace(timeStr,"st","",1)
timeStr = strings.Replace(timeStr,"rd","",1)
timeStr = strings.Replace(timeStr,"nd","",1)
time.Parse("2 January 2006 15:04 PM",timeStr)
but this is wrong as it can remove characters from the month
Can use a regexp to do such kind of things.
re := regexp.MustCompile(`^(\d{1,2})(th|st|rd|nd)`)
re.ReplaceAllString("31st July 2019 01:26 PM", "$1")
How about this?
if d := timeStr[1]; d >= '0' && d <= '9' {
// 2-digit day
timeStr = timeStr[:2] + timeStr[4:]
} else {
// 1-digit day
timeStr = timeStr[:1] + timeStr[3:]
}
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.
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"]
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"]
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.
I need to convert the following raw string (date range) into ruby datetime datetype.
How to finish it on Rails ?
raw string
"2014 April/July 24-1"
convert to ruby datetime variable
start_date = 2014-04-24
end_date = 2014-07-01
raw string
"2015 April 06-20"
convert to ruby datetime variable
start_date = 2015-04-06
end_date = 2015-04-20
This may help
# In order to generate
# year = 2014
# months = "April/July"
# days = "24-1"
/(?<year>\d{4})\s*(?<months>\w+\/\w+)\s*(?<days>\d{1,2}\-\d{1,2})/ =~ "2014 April/July 24-1"
date1 = "#{year} #{months.split('/')[0]} #{days.split('-')[0]}"
date2 = "#{year} #{months.split('/')[1]} #{days.split('-')[1]}"
start_date = Date.strptime(date1, "%Y %b %d") #=> Thu, 24 Apr 2014
end_date = Date.strptime(date2, "%Y %b %d") #=> Tue, 01 Jul 2014
This snippet of code determines the date for the nth weekday of a given month.
Example: for the 2nd Tuesday of December 2013:
>> nth_weekday(2013,11,2,2)
=> Tue Nov 12 00:00:00 UTC 2013
Last Sunday of December 2013:
>> nth_weekday(2013,12,'last',0)
=> Sun Dec 29 00:00:00 UTC 2013
I was not able to find working code for this question so I'm sharing my own.
If you are using Rails, you can do this.
def nth_weekday(year, month, n, wday)
first_day = DateTime.new(year, month, 1)
arr = (first_day..(first_day.end_of_month)).to_a.select {|d| d.wday == wday }
n == 'last' ? arr.last : arr[n - 1]
end
> n = nth_weekday(2013,11,2,2)
# => Tue, 12 Nov 2013 00:00:00 +0000
You can use:
require 'date'
class Date
#~ class DateError < ArgumentError; end
#Get the third monday in march 2008: new_by_mday( 2008, 3, 1, 3)
#
#Based on http://forum.ruby-portal.de/viewtopic.php?f=1&t=6157
def self.new_by_mday(year, month, weekday, nr)
raise( ArgumentError, "No number for weekday/nr") unless weekday.respond_to?(:between?) and nr.respond_to?(:between?)
raise( ArgumentError, "Number not in Range 1..5: #{nr}") unless nr.between?(1,5)
raise( ArgumentError, "Weekday not between 0 (Sunday)and 6 (Saturday): #{nr}") unless weekday.between?(0,6)
day = (weekday-Date.new(year, month, 1).wday)%7 + (nr-1)*7 + 1
if nr == 5
lastday = (Date.new(year, (month)%12+1, 1)-1).day # each december has the same no. of days
raise "There are not 5 weekdays with number #{weekday} in month #{month}" if day > lastday
end
Date.new(year, month, day)
end
end
p Date.new_by_mday(2013,11,2,2)
This is also available in a gem:
gem "date_tools", "~> 0.1.0"
require 'date_tools/date_creator'
p Date.new_by_mday(2013,11,2,2)
# nth can be 1..4 or 'last'
def nth_weekday(year,month,nth,week_day)
first_date = Time.gm(year,month,1)
last_date = month < 12 ? Time.gm(year,month+1)-1.day : Time.gm(year+1,1)-1.day
date = nil
if nth.class == Fixnum and nth > 0 and nth < 5
date = first_date
nth_counter = 0
while date <= last_date
nth_counter += 1 if date.wday == week_day
nth_counter == nth ? break : date += 1.day
end
elsif nth == 'last'
date = last_date
while date >= first_date
date.wday == week_day ? break : date -= 1.day
end
else
raise 'Error: nth_weekday called with out of range parameters'
end
return date
end