ActiveRecord parse string to datetime? - ruby-on-rails

If I pass a String into Datetime column while creating new AR object, it will be automatically parse:
1.9.2p290 :011 > Movie.new(:release_date=>"21-Nov-1990")
=> #<Movie id: nil, release_date: "1990-11-21 00:00:00", created_at: nil, updated_at: nil>
How does Rails, or ActiveRecord, do this magic? Which method does it use?

Rails adds a to_date method to String. Its source is simple:
# File activesupport/lib/active_support/core_ext/string/conversions.rb, line 42
def to_date
return nil if self.blank?
::Date.new(*::Date._parse(self, false).values_at(:year, :mon, :mday))
end
Date._parse is native to Ruby (the same method is called by Date.parse) and it's where the real work is done.
It first uses a regular expression to remove extraneous symbols from the string, then passes it to other methods like _parse_eu, _parse_iso, _parse_dot and so on. Each of these uses its own regular expressions and other methods to see if it's a date that it understands and extract the meaningful information from it. Once one of them "works" (i.e. returns true), the rest are skipped. Finally, back in _parse, the extracted information is used to build a date and time, doing a little more work to figure out things like checking for the day of the week and whether a year value of "12" should mean 1912 or 2012.
The docs call this a heuristic method, which could be taken to mean it throws a bunch of possibilities at the wall to see what sticks. It's pretty poorly-documented but works remarkably well.

There's also to_datetime if you need the time.

You probably want to use Date.strptime(str).

As other comments and the documentation suggests, String#to_datetime "Converts a string to a DateTime value.":
"1-1-2012".to_datetime # => Sun, 01 Jan 2012 00:00:00 +0000
"01/01/2012 23:59:59".to_datetime # => Sun, 01 Jan 2012 23:59:59 +0000
"2012-12-13 12:50".to_datetime # => Thu, 13 Dec 2012 12:50:00 +0000
"12/13/2012".to_datetime # => ArgumentError: invalid date

Related

Update time attribute in Rails, SUPER easy, i'm being dense

I have the following RentalRequest object:
#<RentalRequest id: 3, pickup_start: "2015-03-16 21:00:00", pickup_end: "2015-03-16 23:00:00">
I'd like to update pickup_start and pickup_end to be on 2015-03-17 instead. When I run:
RentalRequest.find(3).update_attribute(pickup_end: "2015-03-17 23:00:00")
ArgumentError: wrong number of arguments (1 for 2)
Not sure what other argument I need to provide.
If it helps, here's the type and output of the attribute by itself
RentalRequest.find(3).pickup_end
=> Mon, 16 Mar 2015 23:00:00 UTC +00:00
RentalRequest.find(3).pickup_end.class
=> ActiveSupport::TimeWithZone
Look at the docs for update_attribute, see how it is expecting name, value as the arguments?
You're passing name: value which is a hash, a single argument of a hash. That's why you're getting the error about wrong number of arguments, the method is expecting 2 but you're providing only 1.
You can either change your code to call: update_attribute( :pickup_end, "2015-03-etc..") or you can use the update_attributes method instead, which takes a hash. I'd recommend the latter, because then you can update both attributes at once, in the same SQL statement, and in the same DB transaction:
update_attributes pickup_end: "2015...etc", pickup_start: "2015...etc"

Submitting dates in US format: mm/dd/yyyy

I can get Rails to display mm/dd/yyyy using the following initializer:
date_format.rb
Date::DATE_FORMATS[:default]="%m/%d/%Y"
or l18n using this answer
But this does not change the way dates are submitted. When submitted:
10/02/2014
Will be parsed as
#entry.photo_date
Mon, 10 Feb 2014
I'm looking for a way to submit a date in mm/dd/yyyy format.
I'd like to be able to do this centrally, so I do not have to repeat code throughout the application
In modern ruby (ie. with prepend) you can insert your own type casting in front of Rails's. You'll want to do this for whatever other date/time formats you're using. Here's the code for Date, just stick this in an config/initializers/typecasts.rb or somewhere:
module Typecasting
module Date
def cast_value v
::Date.strptime v, "%m/%d/%Y" rescue super
end
end
::ActiveRecord::Type::Date.prepend Date
end
Rails will try the American format and fall back to using the builtin method if that didn't work.
Try this:
irb(main):001:0> today = Date.today
=> Tue, 06 Jan 2015
irb(main):003:0> today.strftime("%m/%d/%Y")
=> "01/06/2015"
So in your case, #entry.photo_date.strftime("%m/%d/%Y") should work.

How can I use US-style dates in Rails using Ruby 1.9?

I'm in the U.S., and we usually format dates as "month/day/year". I'm trying to make sure that my Rails app, using Ruby 1.9, assumes this format everywhere, and works the way it did under Ruby 1.8.
I know that lots of people have this issue, so I'd like to create a definitive guide here.
Specifically:
'04/01/2011' is April 1, 2011, not Jan 4, 2011.
'4/1/2011' is also April 1, 2011 - the leading zeros should not be necessary.
How can I do this?
Here's what I have so far.
Controlling Date#to_s behavior
I have this line in application.rb:
# Format our dates like "12/25/2011'
Date::DATE_FORMATS[:default] = '%m/%d/%Y'
This ensures that if I do the following:
d = Date.new(2011,4,1)
d.to_s
... I get "04/01/2011", not "2011-04-01".
Controlling String#to_date behavior
ActiveSupport's String#to_date method currently looks like this (source):
def to_date
return nil if self.blank?
::Date.new(*::Date._parse(self, false).values_at(:year, :mon, :mday))
end
(In case you don't follow that, the second line creates a new date, passing in year, month and day, in that order. The way it gets the year, month and day values is by using Date._parse, which parses a string and somehow decides what those values are, then returns a hash. .values_at pulls the values out of that hash in the order Date.new wants them.)
Since I know that I will normally pass in strings like "04/01/2011" or "4/1/2011", I can fix this by monkeypatching it like this:
class String
# Keep a pointer to ActiveSupport's String#to_date
alias_method :old_to_date, :to_date
# Redefine it as follows
def to_date
return nil if self.blank?
begin
# Start by assuming the values are in this order, separated by /
month, day, year = self.split('/').map(&:to_i)
::Date.new(year, month, day)
rescue
# If this fails - like for "April 4, 2011" - fall back to original behavior
begin
old_to_date
rescue NoMethodError => e
# Stupid, unhelpful error from the bowels of Ruby date-parsing code
if e.message == "undefined method `<' for nil:NilClass"
raise InvalidDateError.new("#{self} is not a valid date")
else
raise e
end
end
end
end
end
class InvalidDateError < StandardError; end;
This solution makes my tests pass, but is it crazy? Am I just missing a configuration option somewhere, or is there some other, easier solution?
Are there any other date-parsing cases I'm not covering?
Gem: ruby-american_date
This gem was created since I asked this question. I'm now using it and have been pleased.
https://github.com/jeremyevans/ruby-american_date
Date.strptime is probably what you're looking for in ruby 1.9.
You're probably stuck monkeypatching it onto string.to_date for now, but strptime is the best solution for parsing dates from strings in ruby 1.9.
Also, the formats are symmetric with strftime as far as I know.
you can use rails-i18n gem or just copy the en-US.yml and set your default locale "en-US" in config/application.rb
For parsing US-style dates, you could use:
Date.strptime(date_string, '%m/%d/%Y')
In console:
> Date.strptime('04/01/2011', '%m/%d/%Y')
=> Fri, 01 Apr 2011
> Date.strptime('4/1/2011', '%m/%d/%Y')
=> Fri, 01 Apr 2011
Use REE? :D
Seriously though. If this is a small app you have complete control over or you are standardizing on that date format, monkey patching for a project is totally reasonable. You just need to make sure all your inputs come in with the correct format, be it via API or website.
Instead of using to_s for Date instances, get in the habit of using strftime. It takes a format string that gives you complete control over the date format.
Edit:
strptime gives you full control over the parsing by specifying a format string as well. You can use the same format string in both methods.
Another option is Chronic - http://chronic.rubyforge.org/
You just need to set the endian preference to force only MM/DD/YYYY date format:
Chronic::DEFAULT_OPTIONS[ :endian_precedence ] = [ :middle ]
However the default for Chronic is the out-of-order US date format anyway!

How to display the time in user's timezone

I am using rails 3.0.5 and I have created_at and updated_at stored in UTC. Now I want to display the created_at time in users' timezone. I believe it is possible to pick user's timezone from the browser and then convert time to user's timezone.
I am sure rails will have a gem/plugin to take care of something like this. Is there something?
Rails by default converts every date into UTC before storing the value into the database. This means that, regardless the server timezone, you always have UTC dates in your database.
In order to convert the dates into your user's timezone you have basically two possibilities:
server-side approach
client-side approach
Server-side approach
If your site allows registration, you can store the user timezone as user preference. In the user table, store the user timezone. Then create a custom helper you can use to format any date/time into the proper timezone using the in_time_zone method.
> t = Time.current
# => Mon, 23 Dec 2013 18:25:55 UTC +00:00
> t.zone
# => "UTC"
> t.in_time_zone("CET")
# => Mon, 23 Dec 2013 19:25:55 CET +01:00
Your helper may looks like
def format_time(time, timezone)
time.in_time_zone(timezone)
end
I normally also like to output a standard format, using the I18n.l helper
def format_time(time, timezone)
I18n.l time.to_time.in_time_zone(timezone), format: :long
end
Client-side approach
If your site has no registration or you don't want to ask your users for their timezone or you simply want to use the user system timezone, then you can use JavaScript.
My suggestion is to create a custom helper that will print out every time in a proper way so that you can create a generic JavaScript function to convert the values.
def format_time(time, timezone)
time = time.to_time
content_tag(:span, I18n.l(time, format: :long), data: { timezone: timezone, time: time.iso8601 })
end
Now, create a JavaScript function that is performed on DOM load and that will select all the HTML tags with data-time attribute. Loop them and update the value inside the span tag with the proper time in the given timezone.
A simple jQuery example would be
$(function() {
$("span[data-time]").each(function() {
// get the value from data-time and format according to data-timezone
// write the content back into the span tag
});
});
I'm not posting the full code here, since there are plenty of JavaScript time formatters available with a simple search. Here's a few possible solutions
Convert date to another timezone in JavaScript
Convert date in local timezone using javascript
There is a nice gem by Basecamp called local_time for client side rendering - https://github.com/basecamp/local_time. It's great for applications where user is not signed in and it's caching friendly.
You can add this to your application controller to convert all times to the User's timezone:
class ApplicationController < ActionController::Base
around_filter :user_time_zone, :if => :current_user
def user_time_zone(&block)
Time.use_zone(current_user.timezone_name, &block)
end
end
You just have to capture the user's timezone
Assuming that the value you want displayed is coming from the database, :ie started_at and is (as is the default) stored in UTC.
If you have the user's timezone as an offset you can also localize the time by doing:
started_at.in_time_zone(-2)
=> Mon, 24 Feb 2014 23:07:56 GST -02:00
Which then can be munged in all sorts of way to get the parts you want:
started_at.in_time_zone(-2).yesterday
=> Sun, 23 Feb 2014 23:07:56 GST -02:00
started_at.in_time_zone(-2) + 3.days
=> Thu, 27 Feb 2014 23:07:56 GST -02:00
http://api.rubyonrails.org/classes/Time.html#method-c-use_zone
This is what you're looking for :
Time.use_zone(#user.timezone) do
blah blah blah
end
If you'd like to convert your date to a specific Timezone:
deadline.in_time_zone(time_zone)
Here deadline is a date.
In addition, you can find Universal time through your local machine Timezone plus local time and vice verse, like in Karachi - +05:00, you can simply add it to value in Universal time to find time in your time zone or get Universal time from your local time by subtraction of Timezone difference (05:00 in our case) from your local time
My jquery is a rusty, so it took me a little while to figure out how to implement the client-side approach of the accepted answer above.
Here's my solution:
HTML:
<span data-time="<%= last_message_at %>"> </span>
Jquery/Javascript:
<script type="text/javascript">
$(document).ready($(function() {
$("span[data-time]").each(function() {
var timestamp = $(this).attr("data-time");
var localeTimestamp = new Date(timestamp).toLocaleString();
$(this).html(localeTimestamp);
});
}));
</script>
You might want to take a look at these links in addition to the one Ben posted:
http://railscasts.com/episodes/106-time-zones-in-rails-2-1

How convert a string into a ActiveSupport::TimeWithZone?

I need to update a field (called updated_at). The field in MySQL is of type datetime, and the class is ActiveSupport::TimeWithZone. But the dates are strings like "10/17/2008". I used "10/17/2008".to_date (And I intend .to_time and .to_datetime), and even if in console the ActiveRecord class save successfully, the field in the database still is the current date.
OK.. let's take them one at the time.
First, it is not recommended to set a field name updated_at, since this is a "magic" field that is automatically populated by Rails.
If you want to disable this functionality, you may:
class Foo < ActiveRecord::Base
self.record_timestamps = false
end
in your class, but this will also disable created_at fields.
The best option is to add a new field (e.g. my_updated_at) as date in the database, and then Rails will automatically handle conversions, meaning that the next snippet will work:
Foo.new({:my_updated_at => "10/17/2008"})
Second, the answer on how to parse a string to ActiveSupport::TimeWithZone is:
ActiveSupport::TimeZone['UTC'].parse("10/17/2008")
but I don't think this will help you (of course, change UTC with your current date/time).
Simply
date_as_string = "2008-10-17"
ActiveSupport::TimeZone['UTC'].parse(date_as_string)
# => Fri, 17 Oct 2008 00:00:00 UTC +00:00
And just to confirm that it worked..
ActiveSupport::TimeZone['UTC'].parse(date_as_string).class
# => ActiveSupport::TimeWithZone

Resources