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.
Related
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.
I need to generate next month date from today. Should I manuallt check if month == 12 and add +1 to d.year or there is easy solution?
Time class did strange math:
>> d = Time.now
=> 2015-12-03 15:15:36 +0300
>> d.month
=> 12
>> d.month.next
=> 13
Date has a method next_month:
require 'date'
Date.today #=> #<Date: 2015-12-03 ((2457360j,0s,0n),+0s,2299161j)>
Date.today.next_month #=> #<Date: 2016-01-03 ((2457391j,0s,0n),+0s,2299161j)>
Date.today.next_month.month #=> 1
To convert a Time instance to a Date, use to_date.
Yes add+1
but do (this month + 1)%12
if ( thisMonth == 12)
nextMonth = 1;
else
nextMonth = thisMonth+1;
This is just to give an idea
I did not see for any edge cases yet
there are two commonly used possible solutions:
1: Date.today+1.month OR for month only (Date.today+1.month).month
2: Date.today.next_month OR for month only Date.today.next_month.month
In ruby, how can I get every 14th day of the year, going backwards and forwards from a date.
So consider I'm billed for 2 weeks of recycling on today, 6-16-2015. How can I get an array of every recycling billing day this year based on that date.
Date has a step method:
require 'date'
d = Date.strptime("6-16-2015", '%m-%d-%Y') # strange date format
end_year = Date.new(d.year, -1, -1)
p d.step(end_year, 14).to_a
# =>[#<Date: 2015-06-16 ((2457190j,0s,0n),+0s,2299161j)>, #<Date: 2015-06-30 ((2457204j,0s,0n),+0s,2299161j)>, ...
# Going backward:
begin_year = Date.new(d.year, 1, 1)
p d.step(begin_year,-14).to_a
# =>[#<Date: 2015-06-16 ((2457190j,0s,0n),+0s,2299161j)>, #<Date: 2015-06-02 ((2457176j,0s,0n),+0s,2299161j)>,...
A more descriptive and easy to understand solution:
require 'date'
current_date = Date.parse "16-june-15"
start_date = Date.parse '1-jan-15'
end_date = Date.parse '31-dec-15'
interval = 14
result = current_date.step(start_date, -interval).to_a
result.sort!.pop
result += current_date.step(end_date, interval).to_a
You could do that as follows:
require 'date'
date_str = "6-16-2015"
d = Date.strptime(date_str, '%m-%d-%Y')
f = Date.new(d.year)
((f + (f-d).abs % 14)..Date.new(d.year,-1,-1)).step(14).to_a
#=> [#<Date: 2015-01-13 ((2457036j,0s,0n),+0s,2299161j)>,
# #<Date: 2015-01-27 ((2457050j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2015-06-16 ((2457190j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2015-12-29 ((2457386j,0s,0n),+0s,2299161j)>]
Based on the second sentence of your question, I assume you simply want an array of all dates in the given year that are two-weeks apart and include the given day.
I attempted a mathy modulus biased approach which turned out unexpectedly confusing.
require 'date'
a_recycle_date_string = "6-17-2015"
interval = 14
a_recycle_date = Date.strptime(a_recycle_date_string, '%m-%d-%Y')
current_year = a_recycle_date.year
end_of_year = Date.new(current_year, -1, -1)
# Find out which index of the first interval's days is the first recycle day
# of the year the (1 indexed)
remainder = (a_recycle_date.yday) % interval
# => 0
# make sure remainder 0 is treated as interval-1 so it doesn't louse
# the equation up
n_days_from_first_recycling_yday_of_year = (remainder - 1) % interval
first_recycle_date_this_year = Date.new(current_year,
1,
1 + n_days_from_first_recycling_yday_of_year)
first_recycle_date_this_year.step(end_of_year, interval).to_a
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.
I have two DateTime objects (date0 and date1).
I want to know how many Mondays are between [date0..date1]
What is the best way to do this in Ruby?
I'd do it differently, using Date#upto and Array#count :
require 'date'
date1 = Date.parse "01/01/2014"
date2 = Date.parse "01/03/2014"
date1.upto(date2).count(&:monday?) # => 8
Use Date#monday?
[1] pry(main)> require 'date'
=> true
[2] pry(main)> date1 = "01/01/2014"
=> "01/01/2014"
[3] pry(main)> date2 = "01/03/2014"
=> "01/03/2014"
(Date.parse(date1)..Date.parse(date2)).select { |x| x.monday? }
=> [#<Date: 2014-01-06 ((2456664j,0s,0n),+0s,2299161j)>,
#<Date: 2014-01-13 ((2456671j,0s,0n),+0s,2299161j)>,
#<Date: 2014-01-20 ((2456678j,0s,0n),+0s,2299161j)>,
#<Date: 2014-01-27 ((2456685j,0s,0n),+0s,2299161j)>,
#<Date: 2014-02-03 ((2456692j,0s,0n),+0s,2299161j)>,
#<Date: 2014-02-10 ((2456699j,0s,0n),+0s,2299161j)>,
#<Date: 2014-02-17 ((2456706j,0s,0n),+0s,2299161j)>,
#<Date: 2014-02-24 ((2456713j,0s,0n),+0s,2299161j)>]
EDIT
The above gives the list of Mondays as date objects but if you need their count then it is:
mondays = (Date.parse(date1)..Date.parse(date2)).select { |x| x.monday? }
mondays.count
=> 8
Array#count
For DateTime logic remains same
[10] pry(main)> date1 = "01/01/2014"
=> "01/01/2014"
[11] pry(main)> date2 = "01/03/2014"
=> "01/03/2014"
[12] pry(main)> (DateTime.parse(date1)..DateTime.parse(date2)).select { |x| x.monday? }.size
=> 8
#Matt showed a O(1) solution, here's my re-implementation with a little more explanation:
Basically, in order to get the number of mondays (or any other day) between two dates, we want to count the number of weeks from the start_date till the end_date and get rid of a couple exceptions (I'll explain below).
Img0: Days of the week, 0 denoting Sunday.
Getting the number of weeks is similar to getting the number of rows in the table above. The only difference is that we'll have an offset, which is start_date's wday.
number_of_rows = (day_diff + start_date.wday) / 7 + 1
We add 1 because given two valid dates, the minimum number of rows is 1.
Next, if our start_date's wday is greater than the day of week (dow in code), we don't want to count that monday in the row, thus the -1. (check Img0 for illustration - the blue area is an interval of days that starts after a monday and ends before the following monday)
Likewise, if the end_date's wday is less than the day of week, we don't want to count the monday in the row.
def day_count(dow, start_date, end_date)
day_diff = (end_date - start_date).to_i
number_of_rows = (day_diff + start_date.wday) / 7 + 1
end_day_index = (day_diff + start_date.wday) % 7
# could have done end_date.wday ; using % 7 to explain
# its existence in #Matt's code
number_of_rows +
( start_date.wday > dow ? -1 : 0 ) +
( end_day_index < dow ? -1 : 0)
end
def monday_count(start_date, end_date)
day_count(1, start_date, end_date)
end
O(1) solution:
require 'date'
(1 + (date1-date0).to_i + (date0.wday+5) % 7) / 7
my_days = [1] # day of the week in 0-6. Sunday is day-of-week 0; Saturday is day-of-week 6.
result = (date0..date1).to_a.select {|k| my_days.include?(k.wday)}