Why does Date.new not call initialize? - ruby

I want to create a subclass of Date.
A normal, healthy, young rubyist, unscarred by the idiosyncrasy of Date's implementation would go about this in the following manner:
require 'date'
class MyDate < Date
def initialize(year, month, day)
#original_month = month
#original_day = day
# Christmas comes early!
super(year, 12, 25)
end
end
And proceed to use it in the most expected manner...
require 'my_date'
mdt = MyDate.new(2012, 1, 28)
puts mdt.to_s
... only to be double-crossed by the fact, that the Date::new method is actually an alias to Date::civil, which doesn't ever call initialize. In this case, the last piece of code prints "2012-01-28" instead of the expected "2012-12-25".
Dear Ruby-community, wtf is this?
Is there some very good reason for aliasing new, so that it ignores initialize, and as a result, any common sense and regard for the client's programmer's mental health?

You define initialize, but you create the new instance with new. new returns a new instance of the class, not the result of initialize.
You may do:
require 'date'
class MyDate < Date
def self.new(year, month, day)
#original_month = month
#original_day = day
# Christmas comes early!
super(year, 12, 25)
end
end
mdt = MyDate.new(2012, 1, 28)
puts mdt.to_s
Remark:
#original_month and #original_day are not available in this solution. The following solution extends Date, so you can access the original month and day. For normal dates, the values will be nil.
require 'date'
class Date
attr_accessor :original_month
attr_accessor :original_day
end
class MyDate < Date
def self.new(year, month, day)
# Christmas comes early!
date = super(year, 12, 25)
date.original_month = month
date.original_day = day
date
end
end
mdt = MyDate.new(2012, 1, 28)
puts mdt.to_s
puts mdt.original_month
But I would recommend:
require 'date'
class MyDate < Date
def self.create(year, month, day)
#original_month = month
#original_day = day
# Christmas comes early!
new(year, 12, 25)
end
end
mdt = MyDate.create(2012, 1, 28)
puts mdt.to_s
or
require 'date'
class Date
def this_year_christmas
# Christmas comes early!
self.class.new(year, 12, 28)
end
end
mdt = Date.new(2012, 1, 28).this_year_christmas
puts mdt.to_s

Related

Looking for an efficient way to detect birthday ranges using Ruby

Currently working on an app that needs to show the operator a list of staff that have birthdays that fall in the past 7 days or the next 21 days.
We have a staff database that contains the date of births, and selecting them by date range is fairly straightforward. However, we then have to show how many days in the future/past the birthday is from today.
The code currently looks something like:
today = Date.today
dob = staff.dob
days = Date.new(today.year, dob.month, dob.day) - today
if days < 0
"#{ days.abs } days ago"
else
"in #{ days } days"
end
This all works well in nearly every case, except when it comes to late December, or very early January. Because we are using today.year as the year comparator, if the 21 days ahead spills over into the new year, I get a false "days ago" reading. e.g.
Today's date is 20th December 2015, and the staff member's birthday is on 2nd January, instead of getting "in 13 days", I am getting "352 days ago".
I know I can probably wrap the code in another if/then/else clause that checks if the current date is after December 10th, to cater for this edge case, but there is a possibility that the date ranges will change dynamically, and also, the code is starting to look messy and inelegant - not like how I would like ruby code to look.
Does anyone have a better suggestion as to how to handle this issue?
(Note: This is in a Sinatra based project, so I don't have all the ActiveSupport or Rails based magic, although I am not averse to using a gem if it will get me the results I need.)
to get the smallest days for this year, next year and last year
today = Date.today
dob = staff.dob
this_dob = Date.new(today.year, dob.month, dob.day)
next_dob = this_dob.next_year
prev_dob = this_dob.prev_year
days = [this_dob - today, next_dob - today, prev_dob - today].min_by{|i| i.to_i.abs}
if days < 0
"#{ days.abs } days ago"
else
"in #{ days } days"
end
require 'date'
class BirthDates
attr_reader :year, :month, :day
def initialize(year, month, day)
#year = year
#month = month
#day = day
end
end
def count_days(dob)
today = Date.today
this_year = today.year
days = [this_year-1, this_year, this_year+1].map { |year|
(Date.new(year, dob.month, dob.day) - today) }.min_by(&:abs)
if days < 0
"#{ -days } days ago"
elsif days > 0
"in #{ days } days"
else
"Happy Birthday!"
end
end
Let's try it. (Today is 29 Oct 15)
dob = BirthDates.new(1908, 12, 28)
count_days(dob) #=> "in 60 days"
dob = BirthDates.new(1999, 10, 27)
count_days(dob) #=> "2 days ago"
dob = BirthDates.new(2014, 10, 29)
count_days(dob) #=> "Happy Birthday"
If today were 5 Jan 16:
dob = BirthDates.new(1908, 12, 28)
count_days(dob) #=> "8 days ago"
dob = BirthDates.new(1999, 3, 27)
count_days(dob) #=> "in 82 days"

Ruby in 100 Minutes, good_morning method

I have been doing the Ruby in 100 minutes website, and encountered a problem during part 5.
I was asked to create a good_morning method that would print out a greeting such as 'Happy Monday, it's the 130 day of 2013'. Here is my current program:
class PersonalChef
def good_morning
date = Time.new
today = Time("%A")
day_of_year = Time.yday
this_year = Time("%Y")
puts "Happy " + "#{today}" + "! It is the " + "#{day_of_year}" + " day of the year" + "#{this_year}"
return self
end
def make_toast(color)
puts "Making your toast #{color}!"
return self
end
def make_milkshake(flavor)
puts "Don\'t worry boss, my #{flavor} milkshake brings all the boys to the yard!"
return self
end
def make_eggs(quantity)
puts "Making you #{quantity} eggs sir!"
return self
end
end
but when I run the program via irb (load 'personal_chef.rb', frank = PersonalChef.new, frank.make_milkshake('chocolate'), etc, everything works fine until I try to type frank.good_morning into irb, which gives the following error message
"NoMethodError: undefined method Time' for #<PersonalChef:0x00000001b61808>
from personal_chef.rb:4:ingood_morning'
from (irb):3
from /usr/bin/irb:12:in `'
I tried substituting the Date method instead of Time and still encountered the same problem.
Thanks for reading, and hopefully for your forthcoming helpful advice! If there is more information that would be helpful to solve this issue, please let me know.
The line of code
today = Time("%A")
uses Time, the class, as a method call, which explains the error. I see that you are trying to extract the day name and the day of year from a time object, but passing the format string to Time isn't the way to do it.
You are looking for the strftime method.
Example:
>> today = Time.new()
=> 2013-06-19 21:58:34 -0700
>> today.strftime("%A")
=> "Wednesday"
>> today.strftime("%j")
=> "170"
Wednesday, the 170th day of the year.

Does Ruby have an equivalent to TimeSpan in C#?

In C# there is a TimeSpan class. It represents a period of time and is returned from many date manipulation options. You can create one and add or subtract from a date etc.
In Ruby and specifically rails there seems to be lots of date and time classes but nothing that represents a span of time?
Ideally I'd like an object that I could use for outputting formatted dates easily enough using the standard date formatting options.
eg.
ts.to_format("%H%M")
Is there such a class?
Even better would be if I could do something like
ts = end_date - start_date
I am aware that subtracting of two dates results in the number of seconds separating said dates and that I could work it all out from that.
You can do something similar like this:
irb(main):001:0> require 'time' => true
irb(main):002:0> initial = Time.now => Tue Jun 19 08:19:56 -0400 2012
irb(main):003:0> later = Time.now => Tue Jun 19 08:20:05 -0400 2012
irb(main):004:0> span = later - initial => 8.393871
irb(main):005:0>
This just returns a time in seconds which isn't all that pretty to look at, you can use the strftime() function to make it look pretty:
irb(main):010:0> Time.at(span).gmtime.strftime("%H:%M:%S") => "00:00:08"
Something like this? https://github.com/abhidsm/time_diff
require 'time_diff'
time_diff_components = Time.diff(start_date_time, end_date_time)
No, it doesn't. You can just add seconds or use advance method.
end_date - start_date will have Float type
In the end I forked the suggestion in #tokland's answer. Not quite sure how to make it a proper gem but it's currently working for me:
Timespan fork of time_diff
Not yet #toxaq... but I've started something!
https://gist.github.com/thatandyrose/6180560
class TimeSpan
attr_accessor :milliseconds
def self.from_milliseconds(milliseconds)
me = TimeSpan.new
me.milliseconds = milliseconds
return me
end
def self.from_seconds(seconds)
TimeSpan.from_milliseconds(seconds.to_d * 1000)
end
def self.from_minutes(minutes)
TimeSpan.from_milliseconds(minutes.to_d * 60000)
end
def self.from_hours(hours)
TimeSpan.from_milliseconds(hours.to_d * 3600000)
end
def self.from_days(days)
TimeSpan.from_milliseconds(days.to_d * 86400000)
end
def self.from_years(years)
TimeSpan.from_days(years.to_d * 365.242)
end
def self.diff(start_date_time, end_date_time)
TimeSpan.from_seconds(end_date_time - start_date_time)
end
def seconds
self.milliseconds.to_d * 0.001
end
def minutes
self.seconds.to_d * 0.0166667
end
def hours
self.minutes.to_d * 0.0166667
end
def days
self.hours.to_d * 0.0416667
end
def years
self.days.to_d * 0.00273791
end
end

How do I add two weeks to Time.now?

How can I add two weeks to the current Time.now in Ruby? I have a small Sinatra project that uses DataMapper and before saving, I have a field populated with the current time PLUS two weeks, but is not working as needed. Any help is greatly appreciated! I get the following error:
NoMethodError at /
undefined method `weeks' for 2:Fixnum
Here is the code for the Model:
class Job
include DataMapper::Resource
property :id, Serial
property :position, String
property :location, String
property :email, String
property :phone, String
property :description, Text
property :expires_on, Date
property :status, Boolean
property :created_on, DateTime
property :updated_at, DateTime
before :save do
t = Time.now
self.expires_on = t + 2.week
self.status = '0'
end
end
You don't have such nice helpers in plain Ruby. You can add seconds:
Time.now + (2*7*24*60*60)
But, fortunately, there are many date helper libraries out there (or build your own ;) )
Ruby Date class has methods to add days and months in addition to seconds in Time.
An example:
require 'date'
t = DateTime.now
puts t # => 2011-05-06T11:42:26+03:00
# Add 14 days
puts t + 14 # => 2011-05-20T11:42:26+03:00
# Add 2 months
puts t >> 2 # => 2011-07-06T11:42:26+03:00
# And if needed, make Time object out of it
(t + 14).to_time # => 2011-05-20 11:42:26 +0300
require 'rubygems'
require 'active_support/core_ext/numeric/time'
self.expires = 2.weeks.from_now
You have to use seconds to do calculation between dates, but you can use the Time class as a helper to get the seconds from the date part elements.
Time.now + 2.week.to_i
EDIT: As mentioned by #iain you will need Active Support to accomplish usign 2.week.to_i, if you can't (or don't want to) have this dependency you can always use the + operator to add seconds to a Time instance (time + numeric → time docs here)
Time.now + (60 * 60 * 24 * 7 * 2)
I think week/weeks is defined in the active support numeric extension
$ ruby -e 'p Time.now'
2011-05-05 22:27:04 -0400
$ ruby -r active_support/core_ext/numeric -e 'p Time.now + 2.weeks'
2011-05-19 22:27:07 -0400
You can use these 3 patterns
# you have NoMethod Error undefined method
require 'active_support/all'
# Tue, 28 Nov 2017 11:46:37 +0900
Time.now + 2.weeks
# Tue, 28 Nov 2017 11:46:37 +0900
Time.now + 2.week
# Tue Nov 28 11:48:24 +0900 2017
2.weeks.from_now
<%current_time=Time.now
current_time_s=current_time.strftime('%Y-%m-%d %H:%M:%S').to_s #show currrent date time
current_time= Time.now + (60 * 60 * 24 * 7 * 250)
current_time_e=current_time.strftime('%Y-%m-%d %H:%M:%S').to_s #show datetime after week
%>
I like mine too :)
def minor?(dob)
n = DateTime.now
a = DateTime.parse(dob)
a >> 12*18 > n
end
Saves you the trouble of thinking about leap years and seconds. Just works out of the box.

get next/previous month from a Time object

I have a Time object and would like to find the next/previous month. Adding subtracting days does not work as the days per month vary.
time = Time.parse('21-12-2008 10:51 UTC')
next_month = time + 31 * 24 * 60 * 60
Incrementing the month also falls down as one would have to take care of the rolling
time = Time.parse('21-12-2008 10:51 UTC')
next_month = Time.utc(time.year, time.month+1)
time = Time.parse('01-12-2008 10:51 UTC')
previous_month = Time.utc(time.year, time.month-1)
The only thing I found working was
time = Time.parse('21-12-2008 10:51 UTC')
d = Date.new(time.year, time.month, time.day)
d >>= 1
next_month = Time.utc(d.year, d.month, d.day, time.hour, time.min, time.sec, time.usec)
Is there a more elegant way of doing this that I am not seeing?
How would you do it?
Ruby on Rails
Note: This only works in Rails (Thanks Steve!) but I'm keeping it here in case others are using Rails and wish to use these more intuitive methods.
Super simple - thank you Ruby on Rails!
Time.now + 1.month
Time.now - 1.month
Or, another option if it's in relation to the current time (Rails 3+ only).
1.month.from_now
1.month.ago
Personally I prefer using:
Time.now.beginning_of_month - 1.day # previous month
Time.now.end_of_month + 1.day # next month
It always works and is independent from the number of days in a month.
Find more info in this API doc
you can use standard class DateTime
require 'date'
dt = Time.new().to_datetime
=> #<DateTime: 2010-04-23T22:31:39+03:00 (424277622199937/172800000,1/8,2299161)>
dt2 = dt >> 1
=> #<DateTime: 2010-05-23T22:31:39+03:00 (424282806199937/172800000,1/8,2299161)>
t = dt2.to_time
=> 2010-05-23 22:31:39 +0200
There are no built-in methods on Time to do what you want in Ruby. I suggest you write methods to do this work in a module and extend the Time class to make their use simple in the rest of your code.
You can use DateTime, but the methods (<< and >>) are not named in a way that makes their purpose obvious to someone that hasn't used them before.
If you do not want to load and rely on additional libraries you can use something like:
module MonthRotator
def current_month
self.month
end
def month_away
new_month, new_year = current_month == 12 ? [1, year+1] : [(current_month + 1), year]
Time.local(new_year, new_month, day, hour, sec)
end
def month_ago
new_month, new_year = current_month == 1 ? [12, year-1] : [(current_month - 1), year]
Time.local(new_year, new_month, day, hour, sec)
end
end
class Time
include MonthRotator
end
require 'minitest/autorun'
class MonthRotatorTest < MiniTest::Unit::TestCase
describe "A month rotator Time extension" do
it 'should return a next month' do
next_month_date = Time.local(2010, 12).month_away
assert_equal next_month_date.month, 1
assert_equal next_month_date.year, 2011
end
it 'should return previous month' do
previous_month_date = Time.local(2011, 1).month_ago
assert_equal previous_month_date.month, 12
assert_equal previous_month_date.year, 2010
end
end
end
below it works
previous month:
Time.now.months_since(-1)
next month:
Time.now.months_since(1)
I just want to add my plain ruby solution for completeness
replace the format in strftime to desired output
DateTime.now.prev_month.strftime("%Y-%m-%d")
DateTime.now.next_month.strftime("%Y-%m-%d")
You can get the previous month info by this code
require 'time'
time = Time.parse('2021-09-29 12:31 UTC')
time.prev_month.strftime("%b %Y")
You can try convert to datetime.
Time gives you current date, and DateTime allows you to operate with.
Look at this:
irb(main):041:0> Time.new.strftime("%d/%m/%Y")
=> "21/05/2015"
irb(main):040:0> Time.new.to_datetime.prev_month.strftime("%d/%m/%Y")
=> "21/04/2015"
Here is a solution on plain ruby without RoR, works on old ruby versions.
t=Time.local(2000,"jan",1,20,15,1,0);
curmon=t.mon;
prevmon=(Time.local(t.year,t.mon,1,0,0,0,0)-1).mon ;
puts "#{curmon} #{prevmon}"
Some of the solutions assume rails. But, in pure ruby you can do the following
require 'date'
d = Date.now
last_month = d<<1
last_month.strftime("%Y-%m-%d")
Im using the ActiveSupport::TimeZone for this example, but just in case you are using Rails or ActiveSupport it might come in handy.
If you want the previous month you can substract 1 month
time = Time.zone.parse('21-12-2008 10:51 UTC')
time.ago(1.month)
$ irb
irb(main):001:0> time = Time.now
=> 2016-11-21 10:16:31 -0800
irb(main):002:0> year = time.year
=> 2016
irb(main):003:0> month = time.month
=> 11
irb(main):004:0> last_month = month - 1
=> 10
irb(main):005:0> puts time
2016-11-21 10:16:31 -0800
=> nil
irb(main):006:0> puts year
2016
=> nil
irb(main):007:0> puts month
11
=> nil
irb(main):008:0> puts last_month
10
=> nil

Resources