Get the first occurrence of a specific day after a given date - ruby

This thread provides an answer to find the first occurrence of a day (e.g. Thursday) after today's date.
I would like to get a more general function which gives the first occurrence of a day (e.g. Thursday) after any given date (e.g. November 1st, 2017).
The method would therefore take 2 arguments: the Date (either as a String, or as a Date object) and the day (as a String).
One way I found is to check each day after with the thursday? method (e.g. my_date_object.thursday?) but you would need a switch / if-else statement to check which day to check against, which makes the method rather bulky, as seen below.
def get_next_day_after_date(date, day)
days_in_week = 7
week = []
days_in_week.times { |day_after| week << (date + day_after) }
if day.casecmp('Monday').zero?
week.select(&:monday?)
elsif day.casecmp('Tuesday').zero?
week.select(&:tuesday?)
elsif day.casecmp('Wednesday').zero?
week.select(&:wednesday?)
# same code for Thursday to Sunday
end
end
Is there a cleaner way to achieve this in pure Ruby? (no use of active_support or rails-specific methods).

require "date"
def get_next_day_after_day(date, day)
date + (Date.strptime(day, "%A") - date) % 7
end

Related

Delphi week number function based on system start of week

The DateUtils.WeekOfTheYear function is great but it uses the ISO 8601 standard definition of a week. That is, a week is considered to start on a Monday and end on a Sunday. I need a similar function that determines the week number based on the system setting for the start of the week. Or at least for either a sunday or monday start of week like MySQL's week function. Anyone have such a function?
ISO-8601 includes a great deal more than just the first day of the week in its specifications for these calculations. There are also rules which determine the first week in the year, for example.
It is not clear whether what you are looking for is a function to replicate the ISO-8601 calculation with these rules otherwise intact and solely varying the first day of the week, or a direct equivalent of the WEEK() function of MySQL, or something else only similar (and not fully defined).
Worth noting is that the MySQL WEEK() function accepts a parameter which does not determine an arbitrary day marking the start of the week, rather it indicates whether either of Sunday or Monday is to be used along with changing a number of other rules that determine the calculated result.
By contrast, the system setting for first day of the week on Windows itself can be ANY day of the week that the user wishes - Mon, Tue, Wed, Thu, Fri, Sat or Sun.
The implementation I provide below is a simple calculation (some might call it naive) which simply returns a value 0..53 based on the number of week periods, or part periods, elapsed between a date specified and the start of the year in which that date occurs.
The week in which 1st of Jan occurs for the year containing the specified date is deemed to be week 0.
Therefore if the 1st of Jan occurs on a Sunday and the "start of week" is defined as Monday then:
Sun, 01-Jan = Week 0
Mon, 02-Jan = Week 1
..
Sun, 08-Jan = Week 1
Mon, 09-Jan = Week 2
..
etc
The Implementation
I have split the implementation into two distinct parts.
The first (WeeksSince01Jan) accepts a date and a parameter indicating the day of week to be considered the first day of the week in the calculation.
This parameter takes a TSYSDayOfWeek value - an enum arranged such that the ordinal values for each day correspond to the values used in system locale settings for the days of the week. (The value returned by the RTL DayOfWeek function uses different values, defined in this code as TRTLDayOfWeek).
The second part of the implementation provides a LocaleWeeksSince01Jan, to demonstrate obtaining the locale defined first day of week for the current user. This is then simply passed thru to a call to WeeksSince01Jan.
type
TSYSDayOfWeek = (sdMon, sdTue, sdWed, sdThu, sdFri, sdSat, sdSun);
TRTLDayOfWeek = 1..7; // Sun -> Sat
function WeeksSince01Jan(const aDate: TDateTime;
const aFirstDayOfWeek: TSYSDayOfWeek): Word;
const
LOCALE_DOW : array[TRTLDayOfWeek] of TSYSDayOfWeek = (sdSun, sdMon, sdTue, sdWed, sdThu, sdFri, sdSat);
var
y, m, d: Word;
dayOfYearStart: TSYSDayOfWeek;
dtYearStart: TDateTime;
dtStartOfFirstWeekInYear: TDateTime;
iAdjust: Integer;
begin
// Get the date for the first day of the year and determine which
// day of the week (Mon-Fri) that was
DecodeDate(aDate, y, m, d);
dtYearStart := EncodeDate(y, 1, 1);
dayOfYearStart := LOCALE_DOW[DayOfWeek(dtYearStart)];
// Week calculation is simply the number of 7 day periods
// elapsed since the start of the year to the specified date,
// adjusted to reflect any 'offset' to the specified first day of week.
iAdjust := Ord(dayOfYearStart) - Ord(aFirstDayOfWeek);
result := (((Trunc(aDate) + iAdjust) - Trunc(dtYearStart)) div 7);
end;
function LocaleWeeksSince01Jan(const aDate: TDateTime): Word;
var
localeValue: array[0..1] of Char;
firstDayOfWeek: TSYSDayOfWeek;
begin
// Get the system defined first day of the week
GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_IFIRSTDAYOFWEEK, localeValue, SizeOf(localeValue));
firstDayOfWeek := TSYSDayOfWeek(Ord(localeValue[0]) - Ord('0'));
result := WeeksSince01Jan(aDate, firstDayOfWeek);
end;
If you have more complex rules to determine the 0th or 1st week of a year based on numbers of days in that week etc, then you will need to modify this implementation accordingly. There is no attempt to accommodate such needs in the current implementation.
For Testing
The code below may be used as the basis for testing the output (using the system defined first day of the week):
const
YEAR = 2012;
var
d: Integer;
dt: TDateTime;
wk: Word;
begin
List.Items.Clear;
dt := EncodeDate(YEAR, 1, 1) - 7;
for d := 1 to 380 do
begin
dt := dt + 1;
wk := LocaleWeeksSince01Jan(dt);
List.Items.Add(Format('%s, %s = week %d', [ShortDayNames[DayOfWeek(dt)],
DateToStr(dt),
wk]));
end;
Where List is a reference to a TListbox.
Change the value of YEAR to produce a range of results that cover all dates in the specified year +/- an additional 7/8 days, to illustrate the change in result at year end of the preceding and succeeding years.
NOTE: 2012 is a year which demonstrates the possibility of returning dates in that year covering the full range of potential results, 0..53.
If you are only interested in week starting on Sunday instead on Monday you can simply substract 1 day from your DateTime value before feeding it to DateUtils.WeekOfTheYear function.
EDIT:
Response to David Heffernan comment:
Imagine what happens when you subtract 1 from January 1st
It depends on which day is on January 1st
From Embarcadero documentation: http://docwiki.embarcadero.com/Libraries/XE8/en/System.DateUtils.WeekOfTheYear
AYear returns the year in which the specified week occurs. Note that
this may not be the same as the year of AValue. This is because the
first week of a year is defined as the first week with four or more
days in that year. This means that, if the first calendar day of the
year is a Friday, Saturday, or Sunday, then for the first three, two,
or one days of the calendar year, WeekOfTheYear returns the last week
of the previous year. Similarly, if the last calendar day of the year
is a Monday, Tuesday, or Wednesday, then for the last one, two, or
three days of the calendar year, WeekOfTheYear returns 1 (the first
week of the next calendar year).
So if the week starts with Sunday instead of Monday then it means that week start and end days are simply shifted by one day backward.
So for such occasions it is best to use over-ridden version with additional variable parameter to which the year that this week belongs to is stored.
I've combined Deltics' great code with SilverWarior's simple idea to create a WeekOfYear function that handles the system week start day.
type
TSYSDayOfWeek = (sdMon, sdTue, sdWed, sdThu, sdFri, sdSat, sdSun);
function LocaleWeekOfTheYear(dte: TDateTime): word;
var
localeValue: array[0..1] of Char;
firstDayOfWeek: TSYSDayOfWeek;
yearOld,yearNew: word;
dteNew: TDateTime;
begin
// Get the system defined first day of the week
GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_IFIRSTDAYOFWEEK, localeValue, SizeOf(localeValue));
firstDayOfWeek := TSYSDayOfWeek(Ord(localeValue[0]) - Ord('0'));
yearOld:= Year(dte);
if (firstDayOfWeek=sdSun) then
dteNew:= dte-1
else
dteNew:= dte+Ord(firstDayOfWeek);
yearNew:= Year(dteNew);
if (yearOld=yearNew) then
dte:= dteNew;
Result:= WeekOfTheYear(dte);
end;
To make the first day of the week is Saturday, I use to add 2 to the value of Now date.
As example WeekOf(Now + 2) : makes the first day is Saturday,
WeekOf(Now + 2) : makes the first day is Sunday,
WeekOf(Now + 0) : makes the first day is Monday,

Find the closest date from string

I'm trying to convert strings like "Sep 11, Oct 31, Feb 28" into DateTime instances as part of a screen-scraper. Using DateTime.parse() works fine apart from when the data goes across years, and it naively (and probably correctly) returns a date in the current year.
For example the following test case.
test "dateA convert next year" do
TimeService.stubs(:now).returns(Time.new(2013, 12, 30, 9, 30))
assert_equal(Date.new(2014, 1, 2), Extraction.dateA("Jan 2"))
end
I updated my method to look at what would be date with year + 1, and return the closest to 'now' - this works fine. However it feels a bit ugly, and I'm looking for a more elegant solution.
def Extraction.dateA(content)
return_value = DateTime.parse(content)
next_year = return_value.change(:year => return_value.year + 1)
now = TimeService.now.to_i
if (next_year.to_i - now).abs < (return_value.to_i - now).abs then
return_value = next_year
end
return_value
end
TimeService.now is just a utility to return current time to help with stubbing.
Excuse my ruby, I'm new to it.
I think this works as intended, allowing for closest date in previous year as well:
module Extraction
def Extraction.date_a(content)
parsed_date = DateTime.parse(content)
now = DateTime.now
dates = [ parsed_date, parsed_date.next_year, parsed_date.prev_year ]
dates.min_by { |d| ( d - now ).abs }
end
end
A few points:
Changed method name to date_a, just a Ruby convention that differs from Java.
I made use of some built-in methods next_year and prev_year on DateTime
I used a time difference metric and selected date with the minimal value of it from three candidate dates (this is what min_by does). This is simpler code than the conditional switching, especially with three dates to consider.
I forgot about min_by originally, I don't use it often, but it's a very good fit for this problem.
Note there is a pathological case - "29 Feb". If it appears correctly in text, by its nature it will define which year is valid, and it won't parse if current year is e.g. 2015.

How to loop numerically + month and day over the past X years?

I need to loop through all of the days and months for the past couple decades numerically as well as to have the name of the month and day for each date. Obviously a few series of loops can accomplish this, but I wanted to know the most concise ruby-like way to accomplish this.
Essentially I'd need output like this for each day over the past X years:
3 January 2011 and 1/3/2011
What's the cleanest approach?
Dates can work as a range, so it's fairly easy to iterate over a range. The only real trick is how to output them as a formatted string, which can be found in the Date#strftime method, which is documented here.
from_date = Date.new(2011, 1, 1)
to_date = Date.new(2011, 1, 10)
(from_date..to_date).each { |d| puts d.strftime("%-d %B %Y and %-m/%-d/%Y") }
# => 1 January 2011 and 1/1/2011
# => 2 January 2011 and 1/2/2011
# => ...
# => 9 January 2011 and 1/9/2011
# => 10 January 2011 and 1/10/2011
(Note: I recall having some bad luck a ways back with unpadded percent formats like %-d in Windows, but if the above doesn't work and you want them unpadded in that environment you can remove the dash and employ your own workarounds.)
Given start_date & end_date:
(start_date..end_date).each do |date|
# do things with date
end
as David said, this is possible because of Date#succ. You can use Date#strftime to get the date in any format you'd like.
See if you can construct a Range where the min and max are Date objects, then call .each on the range. If the Date object supports the succ method this should work.

Check if two timestamps are the same day in Ruby

I'm a bit confused between Date, Datetime, and Time in Ruby. What's more, my application is sensitive to timezones, and I'm not sure how to convert between these three while being timezone-robust.
How can I check if two unix timestamps (seconds since epoch) represent the same day? (I don't actually mind if it uses local time or UTC; while I'd prefer local time, as long as it's consistent, I can design around that).
Using the standard library, convert a Time object to a Date.
require 'date'
Time.at(x).to_date === Time.at(y).to_date
Date has the === method that will be true if two date objects represent the same day.
ActiveSupport defines nice to_date method for Time class. That's how it looks like:
class Time
def to_date
::Date.new(year, month, day)
end
end
Using it you can compare timestamps like that:
Time.at(ts1).to_date === Time.at(ts2).to_date
And here is less controversial way without extending Time class:
t1 = Time.at(ts1) # local time corresponding to given unix timestamp ts1
t2 = Time.at(ts2)
Date.new(t1.year, t1.month, t1.day) === Date.new(t2.year, t2.month, t2.day)
Time.at(ts1).day == Time.at(ts2).day && (ts1 - ts2).abs <= 86400
Or
t1 = Time.at(ts1)
t2 = Time.at(ts2)
t1.yday == t2.yday && t1.year == t2.year
In the first case we make sure that timestamps are no more than day apart (because #day returns day of month and without this additional check Apr 1 would be equal to May 1)
An alternative is to take day of year and make sure that they are of the same year.
These methods work equally well in both 1.8 and 1.9.
We can use beginning_of_day of the time and compare them:
t1.beginning_of_day == t2.beginning_of_day
This way the timezones won't be affected.

Is there a more concise way to combine many comparisons in Ruby?

I currently have this if statement:
if Time.now.day == 1 and Time.now.hour == 0 and Time.now.minute == 0 and Time.now.second == 0
Is there a more concise way to do this?
You can use Time#to_a to convert your time to an array (sec, min, hour, day, month, year, wday, yday, isdst, zone):
Time.now.to_a # => [20, 57, 16, 30, 11, 2010, 2, 334, false, "CET"]
then slice off just the part you want to match against:
Time.now.to_a[0,4] # => [20, 57, 16, 30]
Your particular check can then be made as concise as this:
if Time.now.to_a[0,4] == [0,0,0,1]
[:day,:hour,:minute,:second].map{|s|Time.now.send s}==[1,0,0,0]
The standard Ruby Time library is pretty spare, so I would simply add my own method to make this easier:
class Time
def beginning_of_month
Time.local(self.year, self.month, 1)
end
def beginning_of_month?
self == beginning_of_month
end
end
so you could then write:
if Time.now.beginning_of_month?
How about?
Time.now.strftime('%d %H:%M:%S') == '01 00:00:00'
I like this because it's very self-documenting; It's obvious you're comparing to a certain day at midnight.
Explanation: The strftime() method makes it easy to output a custom date and/or time string. This format is outputting the day of the month, hour, minute and second.
Time.now.strftime('%d %H:%M:%S') #=> "01 16:54:24"
You can compare it to a time you created:
if Time.now == Time.at(0)
or
if Time.now == Time.utc(2000,"jan",1,20,15,1)
I guess that you are trying to make something similar to cron, i.e. you are in an endless loop and checking whether time has come to perform certain action.
If that is the case, I don't think you should rely that your timestamp check will fall exactly on the first second of the day. What would happen if is delayed (for any reason), and first it fires on 23:59:59, and the next cycle happens on 00:00:01 instead of :00? You will fail to perform desired action whatsoever.
Also, you would want to include some sleep, so that your empty loop wouldn't consume all you resources while waiting.
What you could do instead, is keep the last action timestamp, and compare now with the next action timestamp, performing the action if now >= next_timestamp. Something like:
last_action_on = Time.at(0)
loop do
now = Time.now
next_action_on = Time.local(now.year, now.month, 1) # beginning of the current day
if last_action_on < next_action_on && now >= next_action_on
last_action_on = now
do_action
end
sleep 1
end
%w(day==1 hour==0 minute==0 second==0).all? { |e| eval "Time.now.#{e}" }

Resources