How do I calculate the next annual occurrence of a date? - ruby-on-rails

Given a Ruby date, does a one liner exist for calculating the next anniversary of that date?
For example, if the date is May 01, 2011 the next anniversary would be May 01, 2012, however if it is December 01, 2011, the next anniversary is December 01, 2011 (as that date hasn't yet arrived).

If you date variable is an instance of Date then you can use >>:
Return a new Date object that is n months later than the current one.
So you could do this:
one_year_later = date >> 12
The same approach applies to DateTime. If all you have is a string, then you can use the parse method:
next_year = Date.parse('May 01, 2011') >> 12
next_year_string = (Date.parse('May 01, 2011') >> 12).to_s
IMHO you're better off using the date libraries (Date and DateTime) as much as possible but you can use the Rails extensions (such as 1.year) if you know that Rails will always be around or you don't mind manually pulling in active_support as needed.

An excellent gem exists for doing this called recurrence. You can checkout the source code or some samples:
https://github.com/fnando/recurrence
http://blog.plataformatec.com.br/tag/recurrence/
For example, if you have a date set you could try:
date = ...
recurrence = Recurrence.new(every: :year, on: [date.month, date.day])
puts recurrence.next

You can do it using Ruby's Date class:
the_date = Date.parse('jan 1, 2011')
(the_date < Date.today) ? the_date + 365 : the_date # => Sun, 01 Jan 2012
the_date = Date.parse('dec 31, 2011')
(the_date < Date.today) ? the_date.next_year : the_date # => Sat, 31 Dec 2011
Or, for convenience use ActiveSupport's Date class extensions:
require 'active_support/core_ext/date/calculations'
the_date = Date.parse('jan 1, 2011')
(the_date < Date.today) ? the_date.next_year : the_date # => Sun, 01 Jan 2012
the_date = Date.parse('dec 31, 2011')
(the_date < Date.today) ? the_date.next_year : the_date # => Sat, 31 Dec 2011

Try this:
def next_anniversary(d)
Date.today > d ? 1.year.from_now(d) : d
end

Pulling in a gem just to do this is overkill.
your_date > Date.today ? your_date : your_date >> 12

Related

Convert Time.zone.now to number OLE

I need convert the DateTime to OLE, Actually I have this code
def self.convert_time(t=42941.6102054745)
Time.at((t - 25569) * 86400).utc
end
but this converts to DateTime now I want this solution :
def self.date_time_ole(dt= Time.zone.now)
# her convert to number ole from datetime
end
could you please help me ?
Ruby's Date class subtraction returns number of days between 2 dates, so you just need to subtract your "custom epoch" (December 30, 1899 at midnight) from the other date:
ole_epoch = DateTime.new(1899, 12, 30, 0, 0, 0)
now = DateTime.now # => 2017-08-18 21:03:28 UTC
(now - ole_epoch).to_f # => 42965.87741847492
and that should be OLE and you can also just add it to go the other way:
ole_epoch + 42965.87741847492 # => Fri, 18 Aug 2017 21:03:28 +0000

rails helper function to return the most recent date

I need to Write rails helper method to return the most recent date.
So far this is my method
def latest_date(value_dates)
value_dates.each |value_date| do
my_dates << value_date
end
I need to sort the above array and return just the latest date.
The date is in the following format:
2012-10-10T22:11:52.000Z
Is there a sort method for date?
The .max method will do it for you ;)
> [Date.today, (Date.today + 2.days) ].max
#=> Fri, 05 Jul 2013
Documentation about it (Ruby 2.0):
http://ruby-doc.org/core-2.0/Enumerable.html#method-i-max
You may need to parse the data into Dates, if they are strings, you can use:
dates = ["2012-10-10T22:11:52.000Z", "2012-11-10T22:11:52.000Z", "2013-10-10T22:11:52.000Z"]
dates = dates.map{ |date_str| Date.parse(date_str) }
dates.max #=> returns the maximum date of the Array
See in my irb console (Ruby 1.9.3):
> dates = ["2012-10-10T22:11:52.000Z", "2012-11-10T22:11:52.000Z", "2013-10-10T22:11:52.000Z"]
#=> ["2012-10-10T22:11:52.000Z", "2012-11-10T22:11:52.000Z", "2013-10-10T22:11:52.000Z"]
> dates = dates.map{ |date_str| Date.parse(date_str) }
#=> [Wed, 10 Oct 2012, Sat, 10 Nov 2012, Thu, 10 Oct 2013]
> dates.max
#=> Thu, 10 Oct 2013
(use DateTime.parse(date_str) if you want to keep the Time also)

Ruby / Rails - Change the timezone of a Time, without changing the value

I have a record foo in the database which has :start_time and :timezone attributes.
The :start_time is a Time in UTC - 2001-01-01 14:20:00, for example.
The :timezone is a string - America/New_York, for example.
I want to create a new Time object with the value of :start_time but whose timezone is specified by :timezone. I do not want to load the :start_time and then convert to :timezone, because Rails will be clever and update the time from UTC to be consistent with that timezone.
Currently,
t = foo.start_time
=> 2000-01-01 14:20:00 UTC
t.zone
=> "UTC"
t.in_time_zone("America/New_York")
=> Sat, 01 Jan 2000 09:20:00 EST -05:00
Instead, I want to see
=> Sat, 01 Jan 2000 14:20:00 EST -05:00
ie. I want to do:
t
=> 2000-01-01 14:20:00 UTC
t.zone = "America/New_York"
=> "America/New_York"
t
=> 2000-01-01 14:20:00 EST
Sounds like you want something along the lines of
ActiveSupport::TimeZone.new('America/New_York').local_to_utc(t)
This says convert this local time (using the zone) to utc. If you have Time.zone set then you can of course to
Time.zone.local_to_utc(t)
This won't use the timezone attached to t - it assumes that it's local to the time zone you are converting from.
One edge case to guard against here is DST transitions: the local time you specify may not exist or may be ambiguous.
I've just faced the same problem and here is what I'm going to do:
t = t.asctime.in_time_zone("America/New_York")
Here is the documentation on asctime
If you're using Rails, here is another method along the lines of Eric Walsh's answer:
def set_in_timezone(time, zone)
Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end
You need to add the time offset to your time after you convert it.
The easiest way to do this is:
t = Foo.start_time.in_time_zone("America/New_York")
t -= t.utc_offset
I am not sure why you would want to do this, though it is probably best to actually work with times the way they are built. I guess some background on why you need to shift time and timezones would be helpful.
Actually, I think you need to subtract the offset after you convert it, as in:
1.9.3p194 :042 > utc_time = Time.now.utc
=> 2013-05-29 16:37:36 UTC
1.9.3p194 :043 > local_time = utc_time.in_time_zone('America/New_York')
=> Wed, 29 May 2013 12:37:36 EDT -04:00
1.9.3p194 :044 > desired_time = local_time-local_time.utc_offset
=> Wed, 29 May 2013 16:37:36 EDT -04:00
Depends on where you are going to use this Time.
When your time is an attribute
If time is used as an attribute, you can use the same date_time_attribute gem:
class Task
include DateTimeAttribute
date_time_attribute :due_at
end
task = Task.new
task.due_at_time_zone = 'Moscow'
task.due_at # => Mon, 03 Feb 2013 22:00:00 MSK +04:00
task.due_at_time_zone = 'London'
task.due_at # => Mon, 03 Feb 2013 22:00:00 GMT +00:00
When you set a separate variable
Use the same date_time_attribute gem:
my_date_time = DateTimeAttribute::Container.new(Time.zone.now)
my_date_time.date_time # => 2001-02-03 22:00:00 KRAT +0700
my_date_time.time_zone = 'Moscow'
my_date_time.date_time # => 2001-02-03 22:00:00 MSK +0400
Here's another version that worked better for me than the current answers:
now = Time.now
# => 2020-04-15 12:07:10 +0200
now.strftime("%F %T.%N").in_time_zone("Europe/London")
# => Wed, 15 Apr 2020 12:07:10 BST +01:00
It carries over nanoseconds using "%N". If you desire another precision, see this strftime reference.
The question's about Rails but it seems, like me, not everyone here is on the ActiveSupport train, so yet another option:
irb(main):001:0> require "time"
=> true
irb(main):003:0> require "tzinfo"
=> true
irb(main):004:0> t = Time.parse("2000-01-01 14:20:00 UTC")
=> 2000-01-01 14:20:00 UTC
irb(main):005:0> tz = TZInfo::Timezone.get("America/New_York")
=> #<TZInfo::DataTimezone: America/New_York>
irb(main):008:0> utc = tz.local_to_utc(t)
=> 2000-01-01 19:20:00 UTC
irb(main):009:0> tz.utc_to_local(utc)
=> 2000-01-01 14:20:00 -0500
irb(main):010:0>
local_to_utc not doing the opposite of utc_to_local might look like a bug but it is at least documented: https://github.com/tzinfo/tzinfo says:
The offset of the time is ignored - it is treated as if it were a local time for the time zone
I managed to do this by calling change with the desired time zone:
>> t = Time.current.in_time_zone('America/New_York')
=> Mon, 08 Aug 2022 12:04:36.934007000 EDT -04:00
>> t.change(zone: 'Etc/UTC')
=> Mon, 08 Aug 2022 12:04:36.934007000 UTC +00:00
https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html#method-i-change
def relative_time_in_time_zone(time, zone)
DateTime.parse(time.strftime("%d %b %Y %H:%M:%S #{time.in_time_zone(zone).formatted_offset}"))
end
Quick little function I came up with to solve the job. If someone has a more efficient way of doing this please post it!
I spent significant time struggling with TimeZones as well, and after tinkering with Ruby 1.9.3 realized that you don't need to convert to a named timezone symbol before converting:
my_time = Time.now
west_coast_time = my_time.in_time_zone(-8) # Pacific Standard Time
east_coast_time = my_time.in_time_zone(-5) # Eastern Standard Time
What this implies is that you can focus on getting the appropriate time setup first in the region you want, the way you would think about it (at least in my head I partition it this way), and then convert at the end to the zone you want to verify your business logic with.
This also works for Ruby 2.3.1.
I have created few helper methods one of which just does the same thing as is asked by the original author of the post at Ruby / Rails - Change the timezone of a Time, without changing the value.
Also I have documented few peculiarities I observed and also these helpers contains methods to completely ignore automatic day-light savings applicable while time-conversions which is not available out-of-the-box in Rails framework:
def utc_offset_of_given_time(time, ignore_dst: false)
# Correcting the utc_offset below
utc_offset = time.utc_offset
if !!ignore_dst && time.dst?
utc_offset_ignoring_dst = utc_offset - 3600 # 3600 seconds = 1 hour
utc_offset = utc_offset_ignoring_dst
end
utc_offset
end
def utc_offset_of_given_time_ignoring_dst(time)
utc_offset_of_given_time(time, ignore_dst: true)
end
def change_offset_in_given_time_to_given_utc_offset(time, utc_offset)
formatted_utc_offset = ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, false)
# change method accepts :offset option only on DateTime instances.
# and also offset option works only when given formatted utc_offset
# like -0500. If giving it number of seconds like -18000 it is not
# taken into account. This is not mentioned clearly in the documentation
# , though.
# Hence the conversion to DateTime instance first using to_datetime.
datetime_with_changed_offset = time.to_datetime.change(offset: formatted_utc_offset)
Time.parse(datetime_with_changed_offset.to_s)
end
def ignore_dst_in_given_time(time)
return time unless time.dst?
utc_offset = time.utc_offset
if utc_offset < 0
dst_ignored_time = time - 1.hour
elsif utc_offset > 0
dst_ignored_time = time + 1.hour
end
utc_offset_ignoring_dst = utc_offset_of_given_time_ignoring_dst(time)
dst_ignored_time_with_corrected_offset =
change_offset_in_given_time_to_given_utc_offset(dst_ignored_time, utc_offset_ignoring_dst)
# A special case for time in timezones observing DST and which are
# ahead of UTC. For e.g. Tehran city whose timezone is Iran Standard Time
# and which observes DST and which is UTC +03:30. But when DST is active
# it becomes UTC +04:30. Thus when a IRDT (Iran Daylight Saving Time)
# is given to this method say '05-04-2016 4:00pm' then this will convert
# it to '05-04-2016 5:00pm' and update its offset to +0330 which is incorrect.
# The updated UTC offset is correct but the hour should retain as 4.
if utc_offset > 0
dst_ignored_time_with_corrected_offset -= 1.hour
end
dst_ignored_time_with_corrected_offset
end
Examples which can be tried on rails console or a ruby script after wrapping the above methods in a class or module:
dd1 = '05-04-2016 4:00pm'
dd2 = '07-11-2016 4:00pm'
utc_zone = ActiveSupport::TimeZone['UTC']
est_zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
tehran_zone = ActiveSupport::TimeZone['Tehran']
utc_dd1 = utc_zone.parse(dd1)
est_dd1 = est_zone.parse(dd1)
tehran_dd1 = tehran_zone.parse(dd1)
utc_dd1.dst?
est_dd1.dst?
tehran_dd1.dst?
ignore_dst = true
utc_to_est_time = utc_dd1.in_time_zone(est_zone.name)
if utc_to_est_time.dst? && !!ignore_dst
utc_to_est_time = ignore_dst_in_given_time(utc_to_est_time)
end
puts utc_to_est_time
Hope this helps.
This worked well for me
date = '23/11/2020'
time = '08:00'
h, m = time.split(':')
timezone = 'Europe/London'
date.to_datetime.in_time_zone(timezone).change(hour: h, min: m)
This changes the timezone to 'EST' without changing the time:
time = DateTime.current
Time.find_zone("EST").local(
time.year,
time.month,
time.day,
time.hour,
time.min,
time.sec,
)

How can I use a Date object and a string with a day name to create a new Date object?

I have a Date object
Mon, 03 Dec 2012
and I have a String that contains a day name
"Thursday"
How can I use those two objects to find a new Date object representing the day name in the same week as the original Date object? So for this example, it would be
Thu, 07 Dec 2012
In this scenario, a week goes from Monday to Sunday.
I'm using Rails 3.2.0 and Ruby 1.9.3.
If you can manage to convert "Thursday" to week day 4 -- I know you can --, you can get another date like this:
1.9.3p125 :014 > d = Date.parse "Mon, 03 Dec 2012"
=> Mon, 03 Dec 2012
1.9.3p125 :015 > Date.commercial d.cwyear, d.cweek, 4
=> Thu, 06 Dec 2012
1.9.3p125 :016 >
BTW, you can store the map from day name to number in an Array or an Hash, or use I18n.
I am sure there is a better way for doing this but you could do
def date_by_day(date, day_name)
beginning_of_week = date.beginning_of_week
7.times do |i|
_date = beginning_of_week + i.days
return _date if _date.strftime("%A") == day_name
end
end
This one will work nicely for you.
def date_by_day(date, day_name)
beginning_of_week = date.beginning_of_week
day_number = Time::DAYS_INTO_WEEK[day_name.to_sym]
return (beginnig_of_week + day_number.days)
end

Is it possible to create a list of months between two dates in Rails

I am trying to create a page to display a list of links for each month, grouped into years. The months need to be between two dates, Today, and The date of the first entry.
I am at a brick wall, I have no idea how to create this.
Any help would be massively appriciated
Regards
Adam
Just put what you want inside a range loop and use the Date::MONTHNAMES array like so
(date.year..laterdate.year).each do |y|
mo_start = (date.year == y) ? date.month : 1
mo_end = (laterdate.year == y) ? laterdate.month : 12
(mo_start..mo_end).each do |m|
puts Date::MONTHNAMES[m]
end
end
The following code will add a months_between instance method to the Date class
#!/usr/bin/ruby
require 'date'
class Date
def self.months_between(d1, d2)
months = []
start_date = Date.civil(d1.year, d1.month, 1)
end_date = Date.civil(d2.year, d2.month, 1)
raise ArgumentError unless d1 <= d2
while (start_date < end_date)
months << start_date
start_date = start_date >>1
end
months << end_date
end
end
This is VERY lightly tested, however it returns an Array of dates each date being the 1st day in each affected month.
I don't know if I've completely understood your problem, but some of the following might be useful. I've taken advantage of the extensions to Date provided in ActiveSupport:
d1 = Date.parse("20070617") # => Sun, 17 Jun 2007
d2 = Date.parse("20090529") #=> Fri, 29 May 2009
eom = d1.end_of_month #=> Sat, 30 Jun 2007
mth_ends = [eom] #=> [Sat, 30 Jun 2007]
while eom < d2
eom = eom.advance(:days => 1).end_of_month
mth_ends << eom
end
yrs = mth_ends.group_by{|me| me.year}
The final line uses another handy extension: Array#group_by, which does pretty much exactly what it promises.
d1.year.upto(d2.year) do |yr|
puts "#{yrs[yr].min}, #{yrs[yr].max}"
end
2007-06-30, 2007-12-31
2008-01-31, 2008-12-31
2009-01-31, 2009-05-31
I don't know if the start/end points are as desired, but you should be able to figure out what else you might need.
HTH
Use the date_helper gem which adds the months_between method to the Date class similar to Steve's answer.
xmas = Date.parse("2013-12-25")
hksar_establishment_day = Date.parse("2014-07-01")
Date.months_between(xmas,hksar_establishment_day)
=> [Sun, 01 Dec 2013, Wed, 01 Jan 2014, Sat, 01 Feb 2014, Sat, 01 Mar 2014, Tue, 01 Apr 2014, Thu, 01 May 2014, Sun, 01 Jun 2014, Tue, 01 Jul 2014]

Resources