How do I convert an XML body to a hash in Ruby?
I have an XML body which I'd like to parse into a hash
<soap:Body>
<TimesInMyDAY>
<TIME_DATA>
<StartTime>2010-11-10T09:00:00</StartTime>
<EndTime>2010-11-10T09:20:00</EndTime>
</TIME_DATA>
<TIME_DATA>
<StartTime>2010-11-10T09:20:00</StartTime>
<EndTime>2010-11-10T09:40:00</EndTime>
</TIME_DATA>
<TIME_DATA>
<StartTime>2010-11-10T09:40:00</StartTime>
<EndTime>2010-11-10T10:00:00</EndTime>
</TIME_DATA>
<TIME_DATA>
<StartTime>2010-11-10T10:00:00</StartTime>
<EndTime>2010-11-10T10:20:00</EndTime>
</TIME_DATA>
<TIME_DATA>
<StartTime>2010-11-10T10:40:00</StartTime>
<EndTime>2010-11-10T11:00:00</EndTime>
</TIME_DATA>
</TimesInMyDAY>
</soap:Body>
I'd like to convert it into a hash like this:
{ :times_in_my_day => {
:time_data = > [
{:start_time=>"2010-11-10T09:00:00", :end_time => "2010-11-10T09:20:00" },
{:start_time=>"2010-11-10T09:20:00", :end_time => "2010-11-10T09:40:00" },
{:start_time=>"2010-11-10T09:40:00", :end_time => "2010-11-10T10:00:00" },
{:start_time=>"2010-11-10T10:00:00", :end_time => "2010-11-10T10:20:00" },
{:start_time=>"2010-11-10T10:40:00", :end_time => "2010-11-10T11:00:00" }
]
}
}
Ideally, the tags would convert to snake_case symbols and become keys within the hash.
Also, the datetimes are missing their time zone offsets. They are in the local time zone (not UTC). So I'd like to parse it to show the local offset and then convert the xml datetime strings into Rails DateTime objects. The resulting array would be something like:
{ :times_in_my_day => {
:time_data = > [
{:start_time=>Wed Nov 10 09:00:00 -0800 2010, :end_time => Wed Nov 10 9:20:00 -0800 2010 },
{:start_time=>Wed Nov 10 09:20:00 -0800 2010, :end_time => Wed Nov 10 9:40:00 -0800 2010 },
{:start_time=>Wed Nov 10 09:40:00 -0800 2010, :end_time => Wed Nov 10 10:00:00 -0800 2010 },
{:start_time=>Wed Nov 10 10:00:00 -0800 2010, :end_time => Wed Nov 10 10:20:00 -0800 2010 },
{:start_time=>Wed Nov 10 10:40:00 -0800 2010, :end_time => Wed Nov 10 11:00:00 -0800 2010 }
]
}
}
I was able to convert a single datetime with the parse and in_time_zone methods this way:
Time.parse(xml_datetime).in_time_zone(current_user.time_zone)
But I'm not quite sure the best way to parse the times while converting the XML into a hash.
I'd appreciate any advice. Thanks!
Edit
The code for converting the datetime string into a Rails DateTime object is wrong. That will parse the xml datetime string to the system's timezone offset and then convert that time to the user's timezone. The correct code is:
Time.zone.parse(xml_datetime)
If the user has a different time zone other than the system, this will add the user's time zone offset to the original datetime string. There's a Railscast on how to enable user timezone preferences here: http://railscasts.com/episodes/106-time-zones-in-rails-2-1.
Hash.from_xml(xml) is simple way to solve this. Its activesupport method
I used to use XML::Simple in Perl because parsing XML using Perl was a PITA.
When I switched to Ruby I ended up using Nokogiri, and found it to be very easy to use for parsing HTML and XML. It's so easy that I think in terms of CSS or XPath selectors and don't miss a XML-to-hash converter.
require 'ap'
require 'date'
require 'time'
require 'nokogiri'
xml = %{
<soap:Body>
<TimesInMyDAY>
<TIME_DATA>
<StartTime>2010-11-10T09:00:00</StartTime>
<EndTime>2010-11-10T09:20:00</EndTime>
</TIME_DATA>
<TIME_DATA>
<StartTime>2010-11-10T09:20:00</StartTime>
<EndTime>2010-11-10T09:40:00</EndTime>
</TIME_DATA>
<TIME_DATA>
<StartTime>2010-11-10T09:40:00</StartTime>
<EndTime>2010-11-10T10:00:00</EndTime>
</TIME_DATA>
<TIME_DATA>
<StartTime>2010-11-10T10:00:00</StartTime>
<EndTime>2010-11-10T10:20:00</EndTime>
</TIME_DATA>
<TIME_DATA>
<StartTime>2010-11-10T10:40:00</StartTime>
<EndTime>2010-11-10T11:00:00</EndTime>
</TIME_DATA>
</TimesInMyDAY>
</soap:Body>
}
time_data = []
doc = Nokogiri::XML(xml)
doc.search('//TIME_DATA').each do |t|
start_time = t.at('StartTime').inner_text
end_time = t.at('EndTime').inner_text
time_data << {
:start_time => DateTime.parse(start_time),
:end_time => Time.parse(end_time)
}
end
puts time_data.first[:start_time].class
puts time_data.first[:end_time].class
ap time_data[0, 2]
with the output looking like:
DateTime
Time
[
[0] {
:start_time => #<DateTime: 2010-11-10T09:00:00+00:00 (19644087/8,0/1,2299161)>,
:end_time => 2010-11-10 09:20:00 -0700
},
[1] {
:start_time => #<DateTime: 2010-11-10T09:20:00+00:00 (22099598/9,0/1,2299161)>,
:end_time => 2010-11-10 09:40:00 -0700
}
]
The time values are deliberately parsed into DateTime and Time objects to show that either could be used.
ActiveSupport adds a Hash.from_xml, which does the conversion in a single call. Described in another question: https://stackoverflow.com/a/7488299/937595
Example:
require 'open-uri'
remote_xml_file = "https://www.example.com/some_file.xml"
data = Hash.from_xml(open(remote_xml_file))
The original question was asked some time ago, but I found a simpler solution than using Nokogiri and searching for specific names in the XML.
Nori.parse(your_xml) will parse the XML into a hash and the keys will have the same names as your XML items.
If you don't mind using a gem, crack does a pretty good job at this.
Crack does the XML to hash processing, then you can loop over the resulting hash to normalize the datetimes.
edit
Using REXML, you could try the following (should be close to working, but I do not have access to a terminal so it may need some tweaking):
require 'rexml/document'
arr = []
doc = REXML::XPath.first(REXML::Document.new(xml), "//soap:Body/TimesInMyDAY").text
REXML::XPath.each(doc, "//TIME_DATA") do |el|
start = REXML::XPath.first(el, "//StartTime").text
end = REXML::XPath.first(el, "//EndTime").text
arr.push({:start_time => Time.parse(start).in_time_zone(current_user.time_zone), :end_time => Time.parse(end).in_time_zone(current_user.time_zone)})
end
hash = { :times_in_my_day => { :time_data => arr } }
Of course, this assumes the structure is ALWAYS the same, and that the example you posted was not contrived for simplicity sake (as examples often are).
Related
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,
)
Hi I have application running on Ruby 1.8.7 (i am presently not in a position to update this to 1.9.2) and Rails 3.0.9 running mongoid 2.2.2.
I have a couple of named scopes which I have defined as follows
scope :for_pickups, where(:state => "accepted")
scope :confirmed_shipments, where(:state => "confirmed", :created_at.lt => Date.today - 1.day).desc(:created_at).limit(10)
scope :undelivered_shipments, Proc.new{ |start_date_utc, end_date_utc|
start_date_utc = (Date.today - 7.days).to_time.utc if !start_date_utc.present?
end_date_utc = Date.today.to_time.utc if !end_date_utc.present?
any_of({:created_at.gte => start_date_utc, :created_at.lte => end_date_utc},
{:pickup_date.gte => start_date_utc, :pickup_date.lte => end_date_utc},
{:desired_arrival_date.gte => start_date_utc, :desired_arrival_date.lte => end_date_utc}).
where(:state.in => ["accepted"], :state.nin => ["delivered"]).without(:carrier_digest, :carrier_label_image_base64, :carrier_label_html_base64)
}
scope :search, Proc.new{|search_params|
regexp = Regexp.new(/.*#{search_params.strip}.*/i, true)
where(:golfer_name => regexp)
}
This seems to work well and return the expected values when I use them on their own but when I use scope chaining it gets messed up.
For instance when I run Model.undelivered_shipments.search("sid") i get an array which has :ids as the first parameter
[:ids, #[{:created_at=>{"$gte"=>Thu Sep 27 18:30:00 UTC 2012, "$lte"=>Thu Oct 04 18:30:00 UTC 2012}}, {:pickup_date=>{"$gte"=>Thu Sep 27 18:30:00 UTC 2012, "$lte"=>Thu Oct 04 18:30:00 UTC 2012}}, {:desired_arrival_date=>{"$gte"=>Thu Sep 27 18:30:00 UTC 2012, "$lte"=>Thu Oct 04 18:30:00 UTC 2012}}], :_id=>"sid", :state=>{"$nin"=>["delivered"], "$in"=>["accepted"]}},
options: {:fields=>{:carrier_digest=>0, :carrier_label_html_base64=>0, :carrier_label_image_base64=>0}},
class: Shipment,
embedded: false>
]
Not sure why it returns this and why mongoid criteria defines :_id => 'sid' However when i run the search before the undelivered_shipments method it generates the criteria correctly.
It works if run it with other scopes like for_pickups.search etc but not with the undelivered shipments. I understand I am missing something but I've been through the docs and I can't find anything that really helps. I would really appreciate any help on this
Thanks in advance.
Wed Sep 22 13:15:02 -0400 2010 to this format 2010-08-23 13:15:02 -0400
The left is Time.now
The right is 30.days.ago =\
You can use the to_s(:db) method in Time class to convert it to a database-friendly format.
Time.now.to_s(:db) # => "2010-09-22 17:50:41"
If you really need the time zone offset info, you could add a custom format to Time::DATE_FORMATS, e.g.
Time::DATE_FORMATS[:db_with_zone_offset] = lambda { |time|
time.strftime("%Y-%m-%d %H:%M:%S #{time.formatted_offset(false)}")
}
after which you can simply call
Time.now.to_s(:db_with_zone_offset) => # "2010-09-22 17:48:21 +0000"
Both are different data types.
>> Time.now.class
=> Time
>> 30.days.ago.class
=> ActiveSupport::TimeWithZone
use the strftime method to format it.
If you want to have format in database format, then you can use:
Time.now
=> Wed Sep 22 19:54:24 +0200 2010
Time.now.to_s(:db)
=> "2010-09-22 19:54:48"
Time.now.utc.to_s(:db)
=> "2010-09-22 17:55:16"
I am a RoR newbie. I tried a lot of things, finally came to following:
<td>
<%= Date.strptime(request.baseline_start_date, "%Y-%M-%D %H:%M:%S %Z").strftime("%M/%D/%Y")%>
</td>
But this is also giving me an error:
$_ value need to be String (nil given)
But I know that request.baseline_start_date gives me value (tried printing it separately). I don't know which one it is saying as nil given.
Any suggestions on how I can achieve format conversion?
In Rails you can use the to_time function on a string to convert it into a Date object:
'2012-11-14 14:27:46'.to_time.strftime('%B %e at %l:%M %p')
#=> "November 14 at 2:27 PM"
For a handy, interactive reference guide, refer to http://www.foragoodstrftime.com/
Date.strptime(
"2009-04-24 18:33:41 UTC",
"%Y-%m-%d %H:%M:%S %Z"
).strftime("%m/%d/%Y")
# => "04/24/2009"
I think maybe you just got the capitalization on your format strings wrong.
Check the active support documentation and examples at:
http://apidock.com/rails/ActiveSupport/CoreExtensions/DateTime/Conversions/to_formatted_s
Examples
datetime = DateTime.civil(2007, 12, 4, 0, 0, 0, 0) # => Tue, 04 Dec 2007 00:00:00 +0000
datetime.to_formatted_s(:db) # => "2007-12-04 00:00:00"
datetime.to_s(:db) # => "2007-12-04 00:00:00"
datetime.to_s(:number) # => "20071204000000"
datetime.to_formatted_s(:short) # => "04 Dec 00:00"
datetime.to_formatted_s(:long) # => "December 04, 2007 00:00"
datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00"
datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000"
Or if you really want to customise it, define the helper like:
def custom_format(time)
Time::DATE_FORMATS[:w3cdtf] = lambda { |time| time.strftime("%Y-%m-%dT%H:%M:%S# {time.formatted_offset}") }
end
You can use the String#to_time (or Date#to_time) function in ActiveSupport to convert the string into a Time (or Date) object. Then use strftime as you have already.
Ive written a really nice gem that simplifies the whole process, and makes date formatting DRY.
Check it out at:
http://github.com/platform45/easy_dates
What I have done is add an initializer named conversions.rb in config/initializer
After that Add a line like follows:
ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.update(:<name> => '<formatting>')
From there on you can render your datetime using your format with:
dateVar.to_s(:<name>)
There is a handy list here of the formatting tokens
Thanks a lot for the reply. My problem is, the output seems to be already string and i have to convert from date in string to another format.
When I look at the date stored in database (Oracle) it is mm/dd/yy, but when i get it displayed, it adds the timestamp and timezone.
I tried setting the default in Configuration\environment.rb as
ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(
:default => '%d %b %Y'
)
But that also doesn't seem to help.
At the end, if I just get the string to convert from Timezone format to mm/dd/yyyy, that is enough.
I want to display dates in the format: short day of week, short month, day of month without leading zero but including "th", "st", "nd", or "rd" suffix.
For example, the day this question was asked would display "Thu Oct 2nd".
I'm using Ruby 1.8.7, and Time.strftime just doesn't seem to do this. I'd prefer a standard library if one exists.
Use the ordinalize method from 'active_support'.
>> time = Time.new
=> Fri Oct 03 01:24:48 +0100 2008
>> time.strftime("%a %b #{time.day.ordinalize}")
=> "Fri Oct 3rd"
Note, if you are using IRB with Ruby 2.0, you must first run:
require 'active_support/core_ext/integer/inflections'
You can use active_support's ordinalize helper method on numbers.
>> 3.ordinalize
=> "3rd"
>> 2.ordinalize
=> "2nd"
>> 1.ordinalize
=> "1st"
Taking Patrick McKenzie's answer just a bit further, you could create a new file in your config/initializers directory called date_format.rb (or whatever you want) and put this in it:
Time::DATE_FORMATS.merge!(
my_date: lambda { |time| time.strftime("%a, %b #{time.day.ordinalize}") }
)
Then in your view code you can format any date simply by assigning it your new date format:
My Date: <%= h some_date.to_s(:my_date) %>
It's simple, it works, and is easy to build on. Just add more format lines in the date_format.rb file for each of your different date formats. Here is a more fleshed out example.
Time::DATE_FORMATS.merge!(
datetime_military: '%Y-%m-%d %H:%M',
datetime: '%Y-%m-%d %I:%M%P',
time: '%I:%M%P',
time_military: '%H:%M%P',
datetime_short: '%m/%d %I:%M',
due_date: lambda { |time| time.strftime("%a, %b #{time.day.ordinalize}") }
)
>> require 'activesupport'
=> []
>> t = Time.now
=> Thu Oct 02 17:28:37 -0700 2008
>> formatted = "#{t.strftime("%a %b")} #{t.day.ordinalize}"
=> "Thu Oct 2nd"
Although Jonathan Tran did say he was looking for the abbreviated day of the week first followed by the abbreviated month, I think it might be useful for people who end up here to know that Rails has out-of-the-box support for the more commonly usable long month, ordinalized day integer, followed by the year, as in June 1st, 2018.
It can be easily achieved with:
Time.current.to_date.to_s(:long_ordinal)
=> "January 26th, 2019"
Or:
Date.current.to_s(:long_ordinal)
=> "January 26th, 2019"
You can stick to a time instance if you wish as well:
Time.current.to_s(:long_ordinal)
=> "January 26th, 2019 04:21"
You can find more formats and context on how to create a custom one in the Rails API docs.
Create your own %o format.
Initializer
config/initializers/srtftime.rb
module StrftimeOrdinal
def self.included( base )
base.class_eval do
alias_method :old_strftime, :strftime
def strftime( format )
old_strftime format.gsub( "%o", day.ordinalize )
end
end
end
end
[ Time, Date, DateTime ].each{ |c| c.send :include, StrftimeOrdinal }
Usage
Time.new( 2018, 10, 2 ).strftime( "%a %b %o" )
=> "Tue Oct 2nd"
You can use this with Date and DateTime as well:
DateTime.new( 2018, 10, 2 ).strftime( "%a %b %o" )
=> "Tue Oct 2nd"
Date.new( 2018, 10, 2 ).strftime( "%a %b %o" )
=> "Tue Oct 2nd"
I like Bartosz's answer, but hey, since this is Rails we're talking about, let's take it one step up in devious. (Edit: Although I was going to just monkeypatch the following method, turns out there is a cleaner way.)
DateTime instances have a to_formatted_s method supplied by ActiveSupport, which takes a single symbol as a parameter and, if that symbol is recognized as a valid predefined format, returns a String with the appropriate formatting.
Those symbols are defined by Time::DATE_FORMATS, which is a hash of symbols to either strings for the standard formatting function... or procs. Bwahaha.
d = DateTime.now #Examples were executed on October 3rd 2008
Time::DATE_FORMATS[:weekday_month_ordinal] =
lambda { |time| time.strftime("%a %b #{time.day.ordinalize}") }
d.to_formatted_s :weekday_month_ordinal #Fri Oct 3rd
But hey, if you can't resist the opportunity to monkeypatch, you could always give that a cleaner interface:
class DateTime
Time::DATE_FORMATS[:weekday_month_ordinal] =
lambda { |time| time.strftime("%a %b #{time.day.ordinalize}") }
def to_my_special_s
to_formatted_s :weekday_month_ordinal
end
end
DateTime.now.to_my_special_s #Fri Oct 3rd