I'm trying to build in a calendar system into my app. The problem I'm currently on is once a month schedule. Basically if a person schedules on the second Wednesday of the month I want to automatically update the schedule to the second Wednesday of the next month. I've tried just adding + 1.month but that doesn't achieve exactly what I want. Because it moves it to the next month date not day of the week. Does anyone know how I can achieve this. Here is what I'm currently doing that is not right.
def call
schedule.update!(
end_at: end_at_next_month,
start_at: start_at_next_month,
status: "monthly",
arranged: true
)
end
private
def schedule
context.schedule
end
def end_at_next_month
schedule.end_at + 1.month
end
def start_at_next_month
schedule.start_at + 1.month
end
end
require 'date'
def date_of_nth_wday(month, year, wday, nth)
base_date = Date.new(year, month)
base_wday = base_date.wday
ret_date = base_date +
( wday > base_wday ? wday - base_wday : 7 - base_wday + wday) +
7*(nth-1)
ret_date.month == month ? ret_date : nil
end
Obtain the date of the second Sunday of August, 2017
date_of_nth_wday(8, 2017, 0, 2)
#=> #<Date: 2017-08-13 ((2457979j,0s,0n),+0s,2299161j)>
Obtain the date of the third Friday of August, 2017
date_of_nth_wday(8, 2017, 5, 3)
#=> #<Date: 2017-08-18 ((2457984j,0s,0n),+0s,2299161j)>
Obtain the date of the fifth Monday of August, 2017
date_of_nth_wday(8, 2017, 1, 5)
#=> nil (there are only 4 Mondays in August, 2017)
You can try something like this:
DAYS_MAPPING = { 0=>"Sunday",
1=>"Monday",
2=>"Tuesday",
3=>"Wednesday",
4=>"Thursday",
5=>"Friday",
6=>"Saturday" }
# Returns week of month in integer
# for example: 1 for matching day of 1st week of the month
def get_week_number(date)
week_number = date.strftime('%U').to_i - date.beginning_of_month.strftime('%U').to_i
offset = (Date.parse(DAYS_MAPPING[date.wday]) < date.beginning_of_month) ? 0 : 1
week_number + offset
end
def specific_day_of_next_month(schedule)
# wday => day of week in integer
# for example: 0 for Sunday; 1 for Monday & so on ...
wday = schedule.wday
week_number = get_week_number(schedule)
next_month = schedule + 1.month
days = (next_month.beginning_of_month..next_month.end_of_month).select { |d| d.wday == wday }
days[week_number-1]
end
Example 1:
schedule = Date.today
# => Sat, 01 Jul 2017 (First Saturday of July)
specific_day_of_next_month(schedule)
# => Sat, 05 Aug 2017 (First Saturday of August)
Example 2:
schedule = Date.new(2017, 7, 10)
# => Mon, 10 Jul 2017 (Second Monday of July)
specific_day_of_next_month(schedule)
# => Mon, 14 Aug 2017 (Second Monday of August)
Your title is different to your explanation in the text. With my example you don't get the last friday, but you could select the 1,2,3,4,5th wednesday (or any other day) of a month.
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
#2nd monday (weekday 1) in july
p Date.new_by_mday(2017,7,1,2)
If you look for a gem: date_tools supports ths function.
Related
I am trying to write a method that takes todays date and if the provided date falls in between DST dates for 2018 (DST is true if date is 03/11/18 2:00am and 11/04/18 2:00am), then it would return the date of the next respective DST change.
I actually have no idea how to approach this other than taking the provided date and writing a case statement around it that iterates through the provided date's given year. and each when holds a different year
# method to take todays date and if the date falls in between DST dates,
# then the method will return the date of the next DST change
def dst_date_change(date)
return case
when date.include? ='2018'
if (date > Time.parse('March 11, 2018 2:00am') && (date < Time.parse('November 4, 2018 2:00am'))
end
when date.include? ='2019'
if
end
when date.include? ='2020'
if
end
when date.include? ='2020'
if
end
else
this is what i currently have. clearly unfinished..
I have interpreted the question as follows: "Given a date (possibly today's date), if at no time on that day it is DST, return nil. Otherwise, return the next date (possibly the same date) that begins as DST and ends as non-DST".
require 'date'
def next_dst_date_change(base_date)
base_date_time = base_date.to_time
next_date_time = (base_date+1).to_time
if base_date_time.dst? || next_date_time.dst?
base_date + 1.step.find { |n| (base_date + n).to_time.dst? == false } - 1
end
end
See Date#to_time, Time#dst?, Numeric#step and Enumerable#find.
Examples
d0 = Date.today
#=> #<Date: 2018-09-24 ((2458386j,0s,0n),+0s,2299161j)>
next_dst_date_change(d0)
#=> #<Date: 2018-11-04 ((2458427j,0s,0n),+0s,2299161j)>
d1 = Date.new(2018, 11, 04)
#=> #<Date: 2018-11-04 ((2458427j,0s,0n),+0s,2299161j)>
next_dst_date_change(d0)
#=> #<Date: 2018-11-04 ((2458427j,0s,0n),+0s,2299161j)>
d2 = d0 + 90
#=> #<Date: 2018-12-23 ((2458476j,0s,0n),+0s,2299161j)>
next_dst_date_change(d2)
#=> nil
d3 = d0 + 365
#=> #<Date: 2019-09-24 ((2458751j,0s,0n),+0s,2299161j)>
next_dst_date_change(d3)
#=> #<Date: 2019-11-03 ((2458791j,0s,0n),+0s,2299161j)>
d4 = Date.new(2018, 3, 10)
#=> #<Date: 2018-03-10 ((2458188j,0s,0n),+0s,2299161j)>
next_dst_date_change(d4)
#=> nil
d5 = Date.new(2018, 3, 11)
next_dst_date_change(d5)
#=> #<Date: 2018-11-04 ((2458427j,0s,0n),+0s,2299161j)>
To speed things up one could do a binary search to determine the next DST-to-non_DST change date, using Range#bsearch. I've assumed that DST ends some time before December 1 of each year.
def next_dst_date_change(base_date)
base_date_time = base_date.to_time
next_date_time = (base_date+1).to_time
if base_date_time.dst? || next_date_time.dst?
diff = (Date.new(base_date.year, 12, 1) - base_date).to_i
base_date + (1..diff).bsearch { |n| !(base_date + n).to_time.dst? } - 1
end
end
I have ISO 8601 compliant date strings like "2016" or "2016-09" representing year or months. How can I get start end dates from this.
for example:
2016 -> ["2016-01-01", "2016-12-31"]
2016-09 -> ["2016-09-01", "2016-09-30"]
Thank you
Try this
require 'date'
def iso8601_range(str)
parts = str.scan(/\d+/).map(&:to_i)
date = Date.new(*parts)
case parts.size
when 1
date .. date.next_year - 1
when 2
date .. date.next_month - 1
else
date .. date
end
end
iso8601_range('2016') # => 2016-01-01..2016-12-31
iso8601_range('2016-09') # => 2016-09-01..2016-09-30
iso8601_range('2016-09-20') # => 2016-09-20..2016-09-20
If you are cool with using send you can replace the case statement with
date .. date.send([:next_year,:next_month,:next_day][parts.size - 1]) - 1
require 'date'
def create_start_end(string)
year, month = string.split('-').map(&:to_i)
if month && !month.zero?
[Date.new(year, month, 1).to_s, Date.new(year, month, -1).to_s]
else
[Date.new(year, 1, 1).to_s, Date.new(year, 12, -1).to_s]
end
end
create_start_end('2016')
#=> ["2016-01-01", "2016-12-31"]
create_start_end('2016-01')
#=> ["2016-01-01", "2016-01-31"]
create_start_end('2016-09')
#=> ["2016-09-01", "2016-09-30"]
One more solution in according to #AndreyDeineko :)
require 'date'
def create_date date
date = date.split('-').map(&:to_i)
[Date.new(*date, 1, 1), Date.new(*date, -1, -1)].map(&:to_s)
end
The only thing that is wrong in this code is the return :P
How would you display how many Friday 13ths there in a year?
def unlucky_days(year)
require 'date'
start_date = Date.new(year)
end_date = Date.new(year+1)
my_fridays = [4]
thirteen = "13"
result = (start_date..end_date).to_a.select {|k| my_fridays.include?(k.wday) && thirteen.include?(k.strftime('%d'))}
result.length
end
I'd write:
require 'date'
(1..12).count { |month| Date.new(year, month, 13).friday? }
+1 to #MarkReed's comments. Also, why call .to_a on a range, and why use variables when the Date class in Ruby already has methods like .day and .friday? Here is how I would do it:
def unlucky_days(year)
s = Date.new(year, 1, 1)
e = Date.new(year, 12, 31)
((s...e).select {|d| d.friday? && d.day == 13 }).count
end
Your code is wrong on a few points.
Friday is weekday number 5, not 4.
Why [4].include?(n) instead of just n==4?
"13".include?("#{n}") is not just strange but incorrect, since it returns true for 1 and 3 as well as 13.
You can go cut down on the level of brute force by just looking at the twelve 13ths and counting how many are Fridays, rather than looking at all 365 or 366 days and seeing which of them are both 13th's and Fridays, as in #tokland's answer, reproduced here:
def unlucky_days(year)
(1..12).count { |month| Date.new(year, month, 13).friday? }
end
Or, since there are only 14 possibilities, you could also just use a prebuilt table:
# number of Friday the 13ths in a given year is given by
# UnluckyDays[weekday of Jan 1][0 if common, 1 if leap]
UnluckyDays = [ [2,3], [2,2], [2,1], [1,2], [3,2], [1,1], [1,1] ]
def unlucky_days(year)
UnluckyDays[Date.new(year,1,1).wday][Date.leap?(year) ? 1 : 0 ]
end
This is a variant of #Tokland's answer.
require 'date'
def count_em(year)
d = Date.new(year, 1, 13) << 1
12.times.count { (d >>= 1).friday? }
end
(2010..2016).each { |y| puts "%d Friday the 13ths in %s" % [count_em(y), y] }
# 1 Friday the 13ths in 2010
# 1 Friday the 13ths in 2011
# 3 Friday the 13ths in 2012
# 2 Friday the 13ths in 2013
# 1 Friday the 13ths in 2014
# 3 Friday the 13ths in 2015
# 1 Friday the 13ths in 2016
If this calculation (or one like it) were done often and performance was important, two hashes could be constructed, one for leap years, the other for non-leap years, with keys the day of week on which the first day of the year falls and the values the number of Friday the 13ths in such years.
I'm trying to build reporting for retail sales in a Rails app. I'm trying to write a Gem which can find the start and end dates of a retail month/quarter/season in a Retail Calendar
I'm having difficulty in determining the start of the year.
Ex: in 2014, the year starts on 2nd Feb. For 2015, it's 1st Feb and 2016 is 31st January.
Has anyone encountered such a requirement in Ruby or any other language.
require 'date'
# Class for 5-4-4, 4-5-4, and 4-4-5 calendars
class Calendar13
def initialize (type = 454, yr_end_mo = 1)
case type
when 544, 454, 445 then #type = type
else raise ArgumentError, "Bad calendar type #{type}"
end
if (1..12) === yr_end_mo then #yr_end_mo = yr_end_mo
else raise ArgumentError, "Bad year-end month #{yr_end_mo}"
end
end
# Return the ending date for a particular year
def end_of_year (year)
year += 1 unless #yr_end_mo == 12
year_end = Date.new year, #yr_end_mo, -1
wday = (year_end.wday + 1) % 7 # Saturday-origin day-of-week
# Advance or retreat to closest Saturday
if wday > 3 then year_end += 7 - wday
elsif wday > 0 then year_end -= wday
end
year_end
end
# Return starting date for a particular year
def start_of_year (year); end_of_year(year - 1) + 1; end
# Return starting date for a particular month
def start_of_month (year, month)
start = start_of_year(year) + ((month - 1) / 3).to_i * 91
case #type * 10 + (month - 1) % 3
when 4451, 4541 then start += 28
when 5441 then start += 35
when 4452 then start += 56
when 4542, 5442 then start += 63
end
start
end
# Return the ending date for a particular month
def end_of_month (year, month)
if month == 12 then end_of_year year
else start_of_month(year, month + 1) - 1
end
end
# Return the starting date for a particular quarter
def start_of_quarter (year, quarter)
start_of_month year, (quarter - 1) * 3 + 1
end
# Return the ending date for a particular quarter
def end_of_quarter (year, quarter)
if quarter == 4 then end_of_year year
else start_of_quarter(year, quarter + 1) - 1
end
end
# Return the number of weeks in a particular year
def weeks_in_year (year)
((start_of_year(year + 1) - start_of_year(year)) / 7).to_i
end
end
See also http://www.smythretail.com/general-retailing/how-to-set-up-a-4-5-4-calendar/.
I want to find the first weekday of the month after the 1st which also happens to fall on a weekday in Ruby, something like the following:
Min ( DayOfMonth in (2...8) && DayOfWeek in ( monday ... friday ) )
How can I state this appropriately?
We can simply start at the second day of the desired month, and keep going to the next day until we find a weekday. This will require 2 increments at most, since the worst case is when day 2 is a Saturday and 3 a Sunday. Our target then would be the 4th day. If Sunday is the 2nd day, our target is 3, and otherwise the desired day is the 2nd of the month. My first_wday method returns a Date object as well. You can use .day to get the day of the month, or .wday to get the day of the week on it.
require 'date'
def first_wday(date)
d = Date.new(date.year, date.month, 2)
d += 1 while d.wday == 6 || d.wday == 0 # Saturday or Sunday
d
end
(1..12).each do |month|
date = Date.new(2014, month, 1)
wday = first_wday(date)
puts wday.strftime('%9B %Y: %-d (%A)')
end
# January 2014: 2 (Thursday)
# February 2014: 3 (Monday)
# March 2014: 3 (Monday)
# April 2014: 2 (Wednesday)
# May 2014: 2 (Friday)
# June 2014: 2 (Monday)
# July 2014: 2 (Wednesday)
# August 2014: 4 (Monday)
# September 2014: 2 (Tuesday)
# October 2014: 2 (Thursday)
# November 2014: 3 (Monday)
# December 2014: 2 (Tuesday)
This would be one way of doing it.
Code
require 'date'
d = Date.today
d = (d-d.day+2).next_month
(d..d+2).find { |i| (1..5).cover?(i.wday) }.strftime("%m-%d-%Y %a")
Explanation
d = Date.today #=> #<Date:2014-04-28((2456776j,0s,0n),+0s,2299161j)>
d = (d-d.day+2) #=> #<Date:2014-04-02((2456750j,0s,0n),+0s,2299161j)>
d = d.next_month #=> #<Date:2014-05-02((2456780j,0s,0n),+0s,2299161j)>
d.month #=> 5
d.year #=> 2014
d.wday #=> 5 (Friday)
At least one of the first three days of the month is a weekday, so we need only consider the range (d..d+2). The first value of (d..d+2) passed to the block is d, so:
i = d #=> #<Date: 2014-05-02 ((2456780j,0s,0n),+0s,2299161j)>
i.wday #=> 5
As Date's week starts with 0 on Sunday, the five week days are 1..5:
(1..5).cover?(i.wday) #=> true
(1..5).cover?(4) #=> true
So
i.strftime("%m-%d-%Y %a") #=> "05-02-2014 Fri"
is returned.