ActiveRecord adding to negative date year - ruby-on-rails

I need to be able to save negative dates, but have been running into a problem: ActiveRecord is adding 1 to the year when retrieving the date from the database.
The migration uses t.date.
Here's a quick example:
I create and save a model with a date value of Date.new(-44, 3, 15).
When inserting into the database, and inside the database, the year is correct. A manual psql check confirmed this.
However, when retrieving the date from the DB using ActiveRecord, the year is -43.
I've googled and searched SO with no results. Any idea what is causing this?

http://ruby-doc.org/stdlib-1.9.3/libdoc/date/rdoc/Date.html#method-c-new
...BCE years are counted astronomically.
...the year before the year 1 is the year zero,
and the year preceding the year zero is the year -1.
Researching this I thought you had discovered a defect in Date#parse, then I thought it was working 'as designed', now I am not sure anymore :(
Date.parse('2015-06-15 CE') == Date.new(2015, 6, 15)
# => true
Date.parse('0000-01-01 CE') == Date.new(0, 1, 1)
# => true
Date.parse('0002-01-01 BCE') == Date.new(-1, 1, 1)
# => true
Date.parse('0000-01-01 CE') == Date.parse('0001-01-01 BCE')
# => true
Date.parse('0001-01-01 BCE') == Date.new(0, 1, 1)
# => true
https://en.wikipedia.org/wiki/Astronomical_year_numbering
The year 1 BC/BCE is numbered 0, the year 2 BC is numbered −1
So it appears to work 'as designed', but maybe not as expected
# The year 1 BC/BCE is numbered 0
Date.parse('0001-01-01 BCE') == Date.new(0, 1, 1)
# the year 2 BC is numbered −1
Date.parse('0002-01-01 BCE') == Date.new(-1, 1, 1)
What does this mean for you and your rails app?
User input is most likely submitted in the format '0044-03-15 BC' as a string (params), which is assigned to a model attribute.
x = Widget.create(date_column: '0044-03-15 BC')
x.date_column
=> Wed, 15 Mar -0043
x.attributes_before_type_cast['date_column']
=> "0044-03-15 BC"
x.attributes_before_type_cast['date_column'].to_date
=> Wed, 15 Mar -0043
guessing to_date is also using Date.parse
I'm really not sure how the correct value gets stored in postgres, but rails is going to use Date.parse on that value and be off by a year when displaying the users entered date (BCE only).
some possible hacky workarounds
1) Store the values as integers
class Whatever < ActiveRecord::Base
# 'date' column -> the_date stored as an integer
# override the columns 'getter' method
def the_date
the_time = Time.at(self[:the_date])
Date.new(the_time.year, the_time.month, the_time.day)
end
end
# Time#to_i - epoch time
x = Whatever.create(the_date: Time.new(-44, 3, 15, 0, 0, 0).to_i)
2) Store the year, month, day as separate database fields?
class Whatever < ActiveRecord::Base
# 'date' stored as 3 integer columns -> the_date_year, the_date_month, the_date_day
# use this in views, etc...
def the_date
Date.new(the_date_year, the_date_month, the_date_day)
end
end
hopefully somebody else has a better solution...

Related

How to test specific dates that cannot be parsed

I need to test a specific array of dates to ensure that they are in the correct format, however I cannot use 'parse' because, if I do, the dates that are incorrect are sorted out. For instance, if I have an incorrect date with a month "13", it adds another year and sets the month to 1.
My code pulls in the dates from an SQL query:
table_birth_dates = self.class.connection.execute("SELECT birth_date FROM #{temp_table_name}").values.flatten
[
[0] "1980-30-54",
[1] "1980-30-54",
[2] "2020-09-10",
[3] "1890-10-30"
]
yr = 1900
year_test = table_birth_dates.select{|d| Date.parse(d).year < yr}
This now gives me an ArgumentError: invalid date.
I thought of using:
splitted_birth_date = table_birth_dates.first.split("-")
splitted_birth_date.first.to_i > 1900?
but if I try to loop through all of my dates, I'm not able to manipulate anything via splitting:
table_birth_dates.each do |birth_date|
birth_date.split("-")
end
What can I do with this?
I need to test a specific array of dates to ensure that they are in
the correct format...
If you get an error it means that the date is incorrect, you could rescue that error and do anything you want with that date to make it valid or whatever.
table_birth_dates.each do |birth_date|
begin
if Date.parse(d).year < yr
# do smth
end
rescue ArgumentError => e
# do smth with `d`
end
end
You could combine your select and split approaches together:
table_birth_dates.select { |d| d.split('-').first.to_i < 1900 }
#=> ["1890-10-30"]

Ruby on Rails parsing time from datetime, not as string

I'm trying to extract the time component from a DateTime object (which is represented as "at" in my example). How do I do this, I am absolutely stumped? (I don't want to parse it to a string with strftime as i did here):
#session_date.at.strftime("%H:%M")
I would really like to return the hours and minutes as a Time object.
Is there a specific reason you want a Time object?
Just so we're clear, the Time class in Ruby isn't just "DateTime without the date." As "What's the difference between DateTime and Time in Ruby?" explains, "Time is a wrapper around POSIX-standard time_t, or seconds since January 1, 1970." Like DateTime, a Time object still has year, month, and day, so you don't really gain anything by using Time instead. There's not really a way to represent just hour and minute using either Time or DateTime.
The best you could do with Time, I think, would be this:
date_time = DateTime.now
seconds = date_time.hour * 60 * 60 + date_time.minute * 60
time = Time.at(seconds)
# => 1970-01-01 09:58
...but then you still have to call time.hour and time.min to get at the hour and minute.
If you're just looking for a lightweight data structure to represent an hour and minute pair, though, you might as well just roll your own:
HourAndMinute = Struct.new(:hour, :minute) do
def self.from_datetime(date_time)
new(date_time.hour, date_time.minute)
end
end
hm = HourAndMinute.from_datetime(DateTime.now)
# => #<struct HourAndMinute hour=15, minute=58>
hm.to_h
# => { :hour => 15, :minute => 58 }
hm.to_a
# => [ 15, 58 ]
Edit re:
I have a variable that stores an appointment -- this variable is a DateTime object. I have two table fields that store the start and end times of a location. I need to check if the time scheduled for that appointment lies between the start and end times.
Ah, it seems you had a bit of a XY problem. This makes a lot more sense now.
Absent any more information, I'm going to assume your "fields that store the start and end times of a location" are MySQL TIME columns called start_time and end_time. Given MySQL TIME columns, Rails casts the values to Time objects with the date component set to 1/1/2000. So if your database has the values start_time = '09:00' and end_time = '17:00', Rails will give you Time objects like this:
start_time = Time.new(2000, 1, 1, 9, 0) # => 2000-01-01 09:00:00 ...
end_time = Time.new(2000, 1, 1, 17, 0) # => 2000-01-01 17:00:00 ...
Now you say your appointment time is a DateTime, so let's call it appointment_datetime and suppose it's at 10:30am tomorrow:
appointment_datetime = DateTime.new(2014, 11, 18, 10, 30) # => 2014-11-18 10:30:00 ...
So now to rephrase your question: How do we tell if the time part of appointment_datetime is between the time part of start_time and end_time. The answer is, we need to either change the date part of start_time and end_time to match the date part of appointment_datetime, or the other way around. Since it's easier to change one thing than two, let's do it the other way around and change appointment_datetime to match start_time and end_time (and, since those two are Time objects, we'll create a Time object):
appointment_time = DateTime.new(2000, 1, 1, appointment_datetime.hour, appointment_datetime.minute)
# => 2000-01-01 10:30:00 ...
Now we can compare them directly:
if appointment_time >= start_time && appointment_time <= end_time
puts "Appointment time is good!"
end
# Or, more succinctly:
if (start_time..end_time).cover?(appointment_time)
puts "Appointment time is good!"
end
You would, of course, want to wrap all of this up in a method, perhaps in your Location model (which, again, I'm assuming has start_time and end_time attributes):
class Location < ActiveRecord::Base
# ...
def appointment_time_good?(appointment_datetime)
appointment_time = DateTime.new(2000, 1, 1,
appointment_datetime.hour, appointment_datetime.minute)
(start_time..end_time).cover?(appointment_time)
end
end
location = Location.find(12) # => #<Location id: 12, ...>
location.appointment_time_good?(appointment_time) # => true
I hope that's helpful!
P.S. Another way to implement this would be to ditch the date/time objects entirely and do a straight numeric comparison:
def appointment_time_good?(appointment_datetime)
appointment_hour_min = [ appointment_datetime.hour, appointment_datetime.minute ]
appointment_hour_min >= [ start_time.hour, start_time.min ]
&& appointment_hour_min <= [ end_time.hour, end_time.min ]
end
If you have a DateTime object:
date_time = DateTime.now
date_time.hour
# => 16
date_time.minute
# => 1
If you are looking for the Time since now in words (which is common in Rails apps), then this may be a good read: http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-time_ago_in_words

Set a datetime for next or previous sunday at specific time

I have an app where there is always a current contest (defined by start_date and end_date datetime). I have the following code in the application_controller.rb as a before_filter.
def load_contest
#contest_last = Contest.last
#contest_last.present? ? #contest_leftover = (#contest_last.end_date.utc - Time.now.utc).to_i : #contest_leftover = 0
if #contest_last.nil?
Contest.create(:start_date => Time.now.utc, :end_date => Time.now.utc + 10.minutes)
elsif #contest_leftover < 0
#winner = Organization.order('votes_count DESC').first
#contest_last.update_attributes!(:organization_id => #winner.id, :winner_votes => #winner.votes_count) if #winner.present?
Organization.update_all(:votes_count => 0)
Contest.create(:start_date => #contest_last.end_date.utc, :end_date => Time.now.utc + 10.minutes)
end
end
My questions:
1) I would like to change the :end_date to something that signifies next Sunday at a certain time (eg. next Sunday at 8pm). Similarly, I could then set the :start_date to to the previous Sunday at a certain time. I saw that there is a sunday() class (http://api.rubyonrails.org/classes/Time.html#method-i-sunday), but not sure how to specify a certain time on that day.
2) For this situation of always wanting the current contest, is there a better way of loading it in the app? Would caching it be better and then reloading if a new one is created? Not sure how this would be done, but seems to be more efficient.
Thanks!
Start with this snippet. In rails, given a date/time object d, the expression
d - wday.day
Gives the last Sunday not after that date. i.e. if the day itself is a Sunday the result will be d.
Then to get the next sunday:
sunday1 = d - wday.day
sunday2 = sunday1 + 7.day
And to set the times for both:
sunday1 = (d - wday.day).change(:hour => 8,:min => 30)
sunday2 = (sunday1 + 7.day).change(:hour => 20)
No need for any external modules. Rails (but not core ruby!) has all the date/time manipulation functionality you need.
Note how the change function works: if you pass it :hour only, it will reset the minute and second to 0. If you pass it :hour and :minute, it will reset only the second. See the docs for details.
One last thing: be mindful of what time zone you are using.

Thinking Sphinx with a date range

I am implementing a full text search API for my rails apps, and so far have been having great success with Thinking Sphinx.
I now want to implement a date range search, and keep getting the "bad value for range" error.
Here is a snippet of the controller code, and i'm a bit stuck on what to do next.
#search_options = { :page => params[:page], :per_page => params[:per_page]||50 }
unless params[:since].blank?
# make sure date is in specified format - YYYY-MM-DD
d = nil
begin
d = DateTime.strptime(params[:since], '%Y-%m-%d')
rescue
raise ArgumentError, "Value for since parameter is not a valid date - please use format YYYY-MM-DD"
end
#search_options.merge!(:with => {:post_date => d..Time.now.utc})
end
logger.info #search_options
#posts = Post.search(params[:q], #search_options)
When I have a look at the log, I am seeing this bit which seems to imply the date hasn't been converted into the same time format as the Time.now.utc.
withpost_date2010-05-25T00:00:00+00:00..Tue Jun 01 17:45:13 UTC 2010
Any ideas? Basically I am trying to have the API request pass in a "since" date to see all posts after a certain date. I am specifying that the date should be in the YYYY-MM-DD format.
Thanks for your help.
Chris
EDIT: I just changed the date parameters merge statement to this
#search_options.merge!(:with => {:post_date => d.to_date..DateTime.now})
and now I get this error
undefined method `to_i' for Tue, 25 May 2010:Date
So obviously there is something still not setup right...
lets say d = "2010-12-10"
:post_date => (d.to_time.to_i..Time.now.to_i) would have gotten you there. I just did this in my project and it works great
I finally solved this, but it takes a slightly different approach but it works fine.
I was trying to put the date-range search inside a sphinx_scope (in the model) or as a :condition or :with (in the controller). This did not work, so instead I had to implement it inside the define_index in the model.
So what I did was put a check in the define_index to see if a record fell within a date range, the date range being defined by some SQL code, as shown below. In this case, I wanted to see if "start_date" fell within a date between now and 30 days ago, and an "end_date" fell within today and 30 days from now.
If the dates fell within the ranges, the code below causes the :live to be 0 or 1, depending on whether it falls outside or inside the date ranges (respectively):
define index do
# fields:
...
# attributes:
has "CASE WHEN start_date > DATE_ADD(NOW(), INTERVAL -30 DAY) AND end_date < DATE_ADD(NOW(), INTERVAL 30 DAY) THEN 1 ELSE 0 END", :type => :integer, :as => :live
...
# delta:
...
end
Then in your controller, all you have to do is check if :live => 1 to obtain all records that have start_dates and end_dates within the date ranges.
I used a sphinx_scope like this:
sphinx_scope(:live) {
{ :with => { :live => 1 } }
}
and then in my controller:
#models = Model.live.search(...)
To make sure it works well, you of course need to implement frequent reindexing to make sure the index is up to date, i.e. the correct records are :live => 1 or 0!
Anyway, this is probably a bit late for you now, but I implemented it and it works like a charm!!!
Wouldn't it work if you replaced
d = DateTime.strptime(params[:since], '%Y-%m-%d')
by
Time.parse(params[:since]).strftime("%Y-%m-%d")
(It seems the first one doesn't return a date in the expected format)

Best way to create random DateTime in Rails

What is the best way to generate a random DateTime in Ruby/Rails? Trying to create a nice seeds.rb file. Going to use it like so:
Foo.create(name: Faker::Lorem.words, description: Faker::Lorem.sentence, start_date: Random.date)
Here is how to create a date in the last 10 years:
rand(10.years).ago
You can also get a date in the future:
rand(10.years).from_now
Update – Rails 4.1+
Rails 4.1 has deprecated the implicit conversion from Numeric => seconds when you call .ago, which the above code depends on. See Rails PR #12389 for more information about this. To avoid a deprecation warning in Rails 4.1 you need to do an explicit conversion to seconds, like so:
rand(10.years).seconds.ago
Here are set of methods for generating a random integer, amount, time/datetime within a range.
def rand_int(from, to)
rand_in_range(from, to).to_i
end
def rand_price(from, to)
rand_in_range(from, to).round(2)
end
def rand_time(from, to=Time.now)
Time.at(rand_in_range(from.to_f, to.to_f))
end
def rand_in_range(from, to)
rand * (to - from) + from
end
Now you can make the following calls.
rand_int(60, 75)
# => 61
rand_price(10, 100)
# => 43.84
rand_time(2.days.ago)
# => Mon Mar 08 21:11:56 -0800 2010
I prefer use (1..500).to_a.rand.days.ago
You are using Faker; why not use one of the methods provided by Faker::Date?
# Random date between dates
# Keyword arguments: from, to
Faker::Date.between(from: 2.days.ago, to: Date.today) #=> "Wed, 24 Sep 2014"
# Random date between dates except for certain date
# Keyword arguments: from, to, excepted
Faker::Date.between_except(from: 1.year.ago, to: 1.year.from_now, excepted: Date.today) #=> "Wed, 24 Sep 2014"
# Random date in the future (up to maximum of N days)
# Keyword arguments: days
Faker::Date.forward(days: 23) # => "Fri, 03 Oct 2014"
# Random date in the past (up to maximum of N days)
# Keyword arguments: days
Faker::Date.backward(days: 14) #=> "Fri, 19 Sep 2014"
# Random birthday date (maximum age between 18 and 65)
# Keyword arguments: min_age, max_age
Faker::Date.birthday(min_age: 18, max_age: 65) #=> "Mar, 28 Mar 1986"
# Random date in current year
Faker::Date.in_date_period #=> #<Date: 2019-09-01>
# Random date for range of year 2018 and month 2
# Keyword arguments: year, month
Faker::Date.in_date_period(year: 2018, month: 2) #=> #<Date: 2018-02-26>
# Random date for range of current year and month 2
# Keyword arguments: month
Faker::Date.in_date_period(month: 2) #=> #<Date: 2019-02-26>
current Faker version: 2.11.0
Here is how to create a date in this month:
day = 1.times.map{ 0+Random.rand(30) }.join.to_i
rand(day.days).ago
Another approach using DateTime's advance
def rand_date
# return a random date within 100 days of today in both past and future directions.
n = rand(-100..100)
Date.today.advance(days: n)
end
This is what I use:
# get random DateTime in last 3 weeks
DateTime.now - (rand * 21)
other way:
(10..20).to_a.sample.years.ago
I haven't tried this myself but you could create a random integer between two dates using the number of seconds since epoch. For example, to get a random date for the last week.
end = Time.now
start = (end - 1.week).to_i
random_date = Time.at(rand(end.to_i - start)) + start
Of course you end up with a Time object instead of a DateTime but I'm sure you can covert from here.
As I already mentioned in another question I think the following code-snippet is more consisent regarding the data-types of the parameters and of the value to be returned. Stackoverflow: How to generate a random date in Ruby?
Anyway this uses the rand() method's internal logic what is the random Date or random Time within a span. Maybe someone has a more efficient way to set the default-parameter to (Time.now.to_date) of the method random_date, so it doesn't need this typecasting.
def random_time from = Time.at(0.0), to = Time.now
rand(from..to)
end
# works quite similar to date :)
def random_date from = Date.new(1970), to = Time.now.to_date
rand(from..to)
end
Edit: this code won't work before ruby v1.9.3
You can pass Time Range to rand
rand(10.weeks.ago..1.day.ago)
Output Example:
=> Fri, 10 Jan 2020 10:28:52 WIB +07:00
Without user faker (cause I'm using an old version of ruby):
Time.zone.now - rand(16..35.years) - rand(1..31).days
My 'ish' gem provides a nice way of handling this:
# plus/minus 5 min of input date
Time.now.ish
# override that time range like this
Time.now.ish(:offset => 1.year)
https://github.com/spilliton/ish

Resources