I am trying to do some date math based on the week number of a given year. For example:
date = Date.today # Monday, March 5, 2012
puts date.cwyear # 2012
puts date.cweek # 10 (10th week of 2012)
Now that I know what the current week is, I want to figure out what the next week and previous week are. I need to take the year (2012) and the week number (10) and turn it back into a date object so I can calculate the value for the next/previous week. How can I do this?
You want Date.commercial:
require 'date'
now = Date.today #=> 2012-03-05
monday_next_week = Date.commercial(now.cwyear,now.cweek+1) #=> 2012-03-12
next_sunday_or_today = monday_next_week - 1 #=> 2012-03-11
Note that weeks start on Monday, so if you are on a Sunday and ask for next monday - 1 you'll get the same day.
Note also that if you don't want Mondays you can also specify the day number in the method:
thursday_next_week = Date.commercial(now.cwyear,now.cweek+1,4) #=> 2012-03-15
Calculating on a day basis is pretty simple with Date objects. If you just want to get the previous / next week from a given Date object use the following:
date = Date.today
previous_week = (date - 7).cweek
next_week = (date + 7).cweek
In ActiveSupport you have helper to convert Fixnum to time http://as.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Numeric/Time.html use:
date = Date.today
week_ago = date - 1.week
next_week = date + 1.week
I have created some methods to get week number of a given date
something like this:
def self.get_week(date)
year = date.year
first_monday_of_the_year = self.get_first_monday_of_the_year(year)
# The first days of January may belong to the previous year!
if date < first_monday_of_the_year
year -= 1
first_monday_of_the_year = self.get_first_monday_of_the_year(year)
end
day_difference = date - first_monday_of_the_year
week = (day_difference / 7).to_i + 1
week
end
def self.get_monday_of_year_week(year, week)
d = self.get_first_monday_of_the_year(year)
d + ((week - 1) * 7).days
end
def self.get_first_monday_of_the_year(year)
d = Date.new(year, 1, 7) # 7 Jan is always in the first week
self.get_monday_of_week(d)
end
def self.get_monday_of_week(date)
wday = (date.wday + 6) % 7
date - wday.days
end
Assuming you mean "a given week number in the current year", you can do the following:
2.weeks.since(Time.gm(Time.now.year))
=> Fri Jan 15 00:00:00 UTC 2010
Substitute (week_number - 1) for the 1 in the above, and you'll get a date in the desired week.
Related
In Ruby, I'd like to be able to do something like this:
days_of_week = %i(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
today = :Sunday
today = today + 1
if today > :Saturday
today = :Sunday
end
This gives me
'undefined method '+' for :Sunday:Symbol (NoMethodError)
Can I define a method somehow?
I've looked at various stack overflow questions on enums, and didn't see what I'm looking for, but it's a large volume of information to sort through.
This code, as it is, does not work in ruby (as you noticed). Symbol :Sunday does not know that it's placed in an array, so adding a 1 to it does not make any sense.
You can compare symbols like that, but the results will surprise you. :Wednesday is greater than :Thursday, for example.
Why not just operate on indexes in the array. With integer indexes you can increment them and compare with expected results. Could look like this, for example:
today = days_of_week.index(:Friday)
puts "tomorrow is #{days_of_week[today + 1]}" # >> tomorrow is Saturday
This gives me undefined method `+' for :Sunday:Symbol (NoMethodError)
Can I define a method somehow?
In Ruby, you can open existing classes and add methods to them. Although it's not recommended to patch classes you don't own β let alone core classes β it's absolutely possible:
class Symbol
WEEKDAYS = %i(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
def +(offset)
wday = WEEKDAYS.index(self)
return unless wday
WEEKDAYS[(wday + offset) % WEEKDAYS.length]
end
end
:Sunday + 2
#=> :Tuesday
A less invasive approach is to define a custom class:
class WeekDay
attr_reader :name, :wday
def initialize(name, wday)
#name = name
#wday = wday
end
alias to_s name
alias inspect name
end
In addition to a name, each instance also knows its wday (0-6) which will simplify the lookup later on (so we don't have to scan the array to find the day's index).
For each day of the week I'd define a constant and also put all days in an array: (you could also use a loop and define them via const_set, I prefer to set them explicitly)
class WeekDay
module DayConstants
Sunday = WeekDay.new('Sunday', 0)
Monday = WeekDay.new('Monday', 1)
Tuesday = WeekDay.new('Tuesday', 2)
Wednesday = WeekDay.new('Wednesday', 3)
Thursday = WeekDay.new('Thursday', 4)
Friday = WeekDay.new('Friday', 5)
Saturday = WeekDay.new('Saturday', 6)
end
include DayConstants
WEEKDAYS = [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday].freeze
def +(offset)
WEEKDAYS[(wday + offset) % WEEKDAYS.length]
end
end
You might have wondered why I put the day constants in a separate module. It's so you can easily include them in your current namespace: (this is mostly cosmetic β you could just as well omit the include and write out today = WeekDay::Sunday)
include WeekDay::DayConstants
today = Sunday
today += 1
today #=> Monday
today += 4
today #=> Friday
today += 2
today #=> Sunday
To add comparison operators, you can include Comparable and implement <=>:
class WeekDay
include Comparable
def <=>(other)
wday <=> other.wday if other.is_a?(WeekDay)
end
# ...
end
Friday > Wednesday
#=> true
Wednesday.between?(Monday, Friday)
#=> true
I think I would create an index with as many entries as necessary to describe what you are trying to do.
Here is a hash of hashes to hold the index and next/prev logical day:
days_of_week = %i(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
idx=Hash.new {|h, k| h[k] = {} }
days_of_week.each_with_index{ |e,i|
idx[e][:idx]=i
idx[e][:next_day]=
(i+1)==days_of_week.length ? days_of_week[0] : days_of_week[i+1]
idx[e][:prev_day]=
i==0 ? days_of_week[-1] : days_of_week[i-1]
}
# {:Sunday=>{:idx=>0, :next_day=>:Monday, :prev_day=>:Saturday}, :Monday=>{:idx=>1, :next_day=>:Tuesday, :prev_day=>:Sunday}, :Tuesday=>{:idx=>2, :next_day=>:Wednesday, :prev_day=>:Monday}, :Wednesday=>{:idx=>3, :next_day=>:Thursday, :prev_day=>:Tuesday}, :Thursday=>{:idx=>4, :next_day=>:Friday, :prev_day=>:Wednesday}, :Friday=>{:idx=>5, :next_day=>:Saturday, :prev_day=>:Thursday}, :Saturday=>{:idx=>6, :next_day=>:Sunday, :prev_day=>:Friday}}
Then you can get the next day or previous day:
idx[:Tuesday][:next_day]
# Wednesday
idx[:Saturday][:next_day]
# Wraps around to Sunday
You can create a compare:
def cmp(x,y, idx)
idx[x][:idx]<=>idx[y][:idx]
end
So that :Wednesday is properly less than :Thursday
cmp(:Wednesday, :Thursday, idx)
-1
Or sort another array of symbols into the order of the index:
%i(Saturday Sunday Tuesday Thursday Wednesday Friday Monday).
sort_by{ |day| idx[day][:idx] }
# [:Sunday, :Monday, :Tuesday, :Wednesday, :Thursday, :Friday, :Saturday]
Those types of function can be added to a class to represent the type of symbols you want to deal with.
How is it possible to find the date based on the current year's day?
For example, you have the year's day 235, which date does it corresponds to?
For example, you can find a year's day as follows:
Date.today.yday
#=> 126 (for May 6, 2021)
How to achieve the opposite?
Date.some_method(126)
#=> 2021-05-06
I tried as follows:
now = Date.today
#=> Thu, 06 May 2021
start_year = now.beginning_of_year
#=> Fri, 01 Jan 2021
start.advance(days: (start.yday + 126))
#=> Sat, 08 May 2021
Why there is a difference of 2 days?
This should work (pure ruby):
def day_of_year(num)
Date.new(Date.today.year) + num - 1
end
day_of_year(1) #=> 01/01/2021
day_of_year(235) #=> 23/08/2021
Another option:
def day_of_year(num)
Date.strptime(num.to_s, "%j")
end
day_of_year(1) #=> 01/01/2021
day_of_year(235) #=> 23/08/2021
Note: #steenslag posted the actually correct answer - please go and upvote.
Date.ordinal gives you the date, given the year and the number. You can even specify a negative number, which would calculate the date counting backwards from the end of the year. doc
Try This.
Date.today - (Date.today.yday - 126)
I'm using Chronic to get the last Sunday of the month of any given year. It will gladly give me the nβth Sunday, but not the last.
This works, but is not what I need:
Chronic.parse('4th sunday in march', :now => Time.local(2015,1,1))
This is what I need, but doesn't work:
Chronic.parse('last sunday in march', :now => Time.local(2015,1,1))
Is there any way around this apparent limitation?
UPDATE: I'm upvoting the two answers below because they're both good, but I've already implemented this in "pure Ruby" (in 2 lines of code, besides the require 'date' line), but I'm trying to demonstrate to management that Ruby is the right language to use to replace a Java codebase that is going away (and which had dozens of lines of code to compute this), and I told one manager that I probably could do it in one line of Ruby, and it would be readable and easy to maintain.
I am not sure about Chronic (I haven't heared about it before), but we can implement this in pure ruby :)
##
# returns a Date object being the last sunday of the given month/year
# month: integer between 1 and 12
def last_sunday(month,year)
# get the last day of the month
date = Date.new year, month, -1
#subtract number of days we are ahead of sunday
date -= date.wday
end
The last_sunday method can be used like this:
last_sunday 07, 2013
#=> #<Date: 2013-07-28 ((2456502j,0s,0n),+0s,2299161j)>
Reading the update in your question, I tried to come up with another answer using only a single line of ruby code (without using gems). How about this one?
##
# returns a Date object being the last sunday of the given month/year
# month: integer between 1 and 12
def last_sunday(month,year)
# get the last day of the month, go back until we have a sunday
Date.new(year, month, -1).downto(0).find(&:sunday?)
end
last_sunday 07, 2013
#=> #<Date: 2013-07-28 ((2456502j,0s,0n),+0s,2299161j)>
What about
Chronic.parse('last sunday', now: Chronic.parse('last day of march'))
This works, and is as readable as I can get:
Chronic.parse('last sunday', now: Date.new(year,3,31))
Thanks to Ismael Abreu for the idea to just parse 'last sunday' and control the rest via the :now option.
UPDATE: Please also upvote Ismael's answer.
It's a bit ugly, but you could simply try the 5th or 4th in that order:
d = [5,4].each do |i|
try = Chronic.parse("#{i}th sunday in march", :now => Time.local(2015,1,1))
break try unless try.nil?
end
=> Sun Mar 29 12:30:00 +0100 2015
d = [5,4].each do |i|
try = Chronic.parse("#{i}th sunday in april", :now => Time.local(2015,1,1))
break try unless try.nil?
end
=> Sun Apr 26 12:00:00 +0100 2015
My code:
require 'Date'
s = "I'm going away on Oct 2, 2012th"
puts Date.parse(s)
=> 2012-10-02
I want to delete the date from my strings, that I found with Date.parse(s). The problem is, I know that there is a date, but not how it was written in the string. I know that Date.parse found it and converted "2012-10-02" to a new format.
Here is a quick and dirty solution. The function date_string
returns just the portion of the string containing the date
found by parse.
require 'date'
DATE_ERROR = -1
# If the string doesn't contain a date, it raises an
# exception. This little helper routine catches the
# exception.
def get_date(s)
date = 0
begin
date = Date.parse(s)
rescue
date = DATE_ERROR
end
date
end
# Returns just the part of the string containing the date
def date_string(s)
# First, find the date contained in the string
date = get_date(s)
return "" if date == DATE_ERROR
# Repeatedly chop off characters from the front to find the
# start of the date
first = 1
while date == get_date(s[first..-1])
first += 1
end
# Repeatedly chop off characters from the end to find the
# end of the date
last = s.length - 2
while date == get_date(s[0..last])
last -= 1
end
#Return just the date
s[first - 1..last + 1]
end
puts date_string("I'm going away on Oct 2, 2012th")
puts date_string("I'm going away on 10/2/12 and not coming back")
puts date_string("10 Nov 1999")
puts date_string("I see no date here")
This outputs:
Oct 2, 2012
10/2/12
10 Nov 1999
So you could do something like:
s = "I'm going away on Oct 2, 2012th"
datestr = date_string(s)
s.gsub!(datestr, "")
puts s
Date doesn't appear to be able to tell you where it found the date. You're probably going to have to write your own custom date finder.
I trying to do a each on a Date interval with Rails 3.2.. something like this:
(1.months.ago.to_date..5.months.from_now.to_date).step(1.month).each do |date|
puts date.strftime('%m/%Y')
end
But, the step(1.month) does not work.. seems like it get the first month (ex: today is august, it will return jully) and does not iterate the other months..
Is there a way to do that?
Thanks
You are using Date as your iteration base, and 1.month translates (behind the scenes) into seconds I believe.
When you add to the Date object, it's in days, thus:
Date.today + 1 would be tomorrow
Thus, in your example, you are trying to step 2592000 days.
What you probably want is something more like:
(1.months.ago.to_date..5.months.from_now.to_date).step(30).each { |date| puts date.strftime('%m/%Y') }
If you are looking for the iterator to be smart enough to know how many days are in each month when you are "stepping" that's not going to happen. You will need to roll that on your own.
You can intelligently iterate through months by using the >> operator, so:
date = Date.today
while date < 5.months.from_now.to_date do
puts date.strftime('%m/%Y')
date = date>>1
end
how about this:
current_date, end_date = Date.today, 5.monthes.from_now.to_date
while current_date <= end_date
puts current_date
current_date = current_date.next_month
end