How to convert "3.days" into the string "3 days" in Rails? - ruby-on-rails

I need to convert a simple "3.days" string into something that prints "3 days" on screen that will use the appropriate locale depending on the language to print the word.
I guess there's probably an easy way to do this on Rails that I can't seem to find.

Call inspect on the result. It's been overwritten to return the string "3 days":
Loading development environment (Rails 3.2.6)
irb(main):001:0> 3.days.inspect
=> "3 days"
Behind the scenes, 3.days is just returning the number of seconds in that time period:
irb(main):001:0> puts 3.days
259200
If you're storing that integer value, you can go back to 3 you started with by dividing the number by the number of seconds in one day:
num_days = 3.days / 1.day
puts "#{num_days} days" # 3 days

If you have the input as a string and the output needs to be a string too, one solution would be to use gsub to replace '.' with ' '
1.9.3p194 :004 > '3.days'.gsub('.', ' ')
=> "3 days"

Related

Display duration in a human readable format such as "X hours, Y minutes"

I am using Rails 4, Ruby 2.1 with PostgreSQL.
I have a database field called duration which is an interval data type.
When pulling out the data in this column it returns in the format of hh:mm:ss, e.g. 01:30:00.
I am trying to figure out a way to display this as 1 hour, 30 minutes.
Other examples:
02:00:00 to 2 hours
02:15:00 to 2 hours, 15 minutes
02:01:00 to 2 hours, 1 minute
Just use duration + inspect
seconds = 86400 + 3600 + 15
ActiveSupport::Duration.build(seconds).inspect
=> "1 day, 1 hour, and 15.0 seconds"
Or a it can be a little be customized
ActiveSupport::Duration.build(seconds).parts.map do |key, value|
[value.to_i, key].join
end.join(' ')
=> "1days 1hours 15seconds"
P.S.
You can get seconds with
1.day.to_i
=> 86400
Time can be parsed only in ISO8601 format
ActiveSupport::Duration.parse("PT2H15M").inspect
=> "2 hours and 15 minutes"
I would start with something like this:
def duration_of_interval_in_words(interval)
hours, minutes, seconds = interval.split(':').map(&:to_i)
[].tap do |parts|
parts << "#{hours} hour".pluralize(hours) unless hours.zero?
parts << "#{minutes} minute".pluralize(minutes) unless minutes.zero?
parts << "#{seconds} hour".pluralize(seconds) unless seconds.zero?
end.join(', ')
end
duration_of_interval_in_words('02:00:00')
# => '2 hours'
duration_of_interval_in_words('02:01:00')
# => '2 hours, 1 minute'
duration_of_interval_in_words('02:15:00')
# => '2 hours, 15 minutes'
See also
ActionView::Helpers::DateHelper distance_of_time_in_words (and related)
e.g.
0 <-> 29 secs # => less than a minute
30 secs <-> 1 min, 29 secs # => 1 minute
1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
... etc
https://apidock.com/rails/ActionView/Helpers/DateHelper/distance_of_time_in_words
Perhaps not appropriate to include in a model validation error? (which is my use case)
You can try following method to display such as:
minutes_to_human(45) # 45 minutes
minutes_to_human(120) # 2 hours
minutes_to_human(75) # 2.5 hours
minutes_to_human(75) # 1.15 hours
def minutes_to_human(minutes)
result = {}
hours = minutes / 60
result[:hours] = hours if hours.positive?
result[:minutes] = ((minutes * 60) - (hours * 60 * 60)) / 60 if minutes % 60 != 0
result[:minutes] /= 60.0 if result.key?(:hours) && result.key?(:minutes)
return I18n.t('helper.minutes_to_human.hours_minutes', time: (result[:hours] + result[:minutes]).round(2)) if result.key?(:hours) && result.key?(:minutes)
return I18n.t('helper.minutes_to_human.hours', count: result[:hours]) if result.key?(:hours)
return I18n.t('helper.minutes_to_human.minutes', count: result[:minutes].round) if result.key?(:minutes)
''
end
Translations:
en:
helper:
minutes_to_human:
minutes:
zero: '%{count} minute'
one: '%{count} minute'
other: '%{count} minutes'
hours:
one: '%{count} hour'
other: '%{count} hours'
hours_minutes: '%{time} hours'
This worked for me:
irb(main):030:0> def humanized_duration(seconds)
irb(main):031:1> ActiveSupport::Duration.build(seconds).parts.except(:seconds).reduce("") do |output, (key, val)|
irb(main):032:2* output+= "#{val}#{key.to_s.first} "
irb(main):033:2> end.strip
irb(main):034:1> end
=> :humanized_duration
irb(main):035:0> humanized_duration(920)
=> "15m"
irb(main):036:0> humanized_duration(3920)
=> "1h 5m"
irb(main):037:0> humanized_duration(6920)
=> "1h 55m"
irb(main):038:0> humanized_duration(10800)
=> "3h"
You can change the format you want the resulting string to be inside the reduce. I like the 'h' and 'm' for hours and minutes. And I excluded the seconds from the duration parts since that wasn't important for my usage of it.
Here is a locale-aware helper method which builds upon MasonMc's answer.
# app/helpers/date_time_helper.rb
module DateTimeHelper
def humanized_duration(duration)
ActiveSupport::Duration.build(duration).parts.except(:seconds).collect do |key, val|
t(:"datetime.distance_in_words.x_#{key}", count: val)
end.join(', ')
end
end
You can also replace join(', ') with to_sentence if it reads better, or get fancy and allow passing a locale, like distance_of_time_in_words.
Gotcha
Rather counter-intuitively, x_hours is absent from Rails' default locale file because distance_of_time_in_words doesn't use it.
You'll need to add it yourself, even if using the rails-i18n gem.
# config/locales/en.datetime.yml
en:
datetime:
distance_in_words:
x_hours:
one: "one hour"
other: "%{count} hours"
Here's the output:
humanized_duration(100)
# => '1 minute'
humanized_duration(12.34.hours)
# => '12 hours, 20 minutes'
humanized_duration(42.hours)
# => '1 day, 18 hours, 25 minutes'
I find that duration.inspect serve the purpose pretty well.
> 10.years.inspect
=> "10 years"

Rails shorter "time_ago_in_words"

Is there a different time calculation in rails besides "time_ago_in_words"?? I want to be able to use just 'h' for hours 'd' days 'm' for months... ex. 3d, or 4h, or 5m
My code now...
<%= time_ago_in_words(feed_item.created_at) %> ago.
The components that make up this string can be localised, and are in the datetime.distance_in_words namespace
For example stick
en:
datetime:
distance_in_words:
x_minutes:
one: "1m"
other: "%{count}m"
And rails will say 10m instead of 10 minutes. Repeat as needed for hours, seconds days etc. you can check locales/en.yml in action_view for all the keys.
If you only want the short format you could create a pseudo locale that only used those keys and use it like so
time_ago_in_words created_at, false, :locale => :en_abbrev
In the newer versions of rails, you can specify a scope as an option for the method like so:
time_ago_in_words(company.updated_at, scope: 'datetime.distance_in_words.abbrv')
Then you just need to have your regular i18n file structured like so:
en:
datetime:
distance_in_words:
abbrv:
about_x_hours:
one: ~ 1h
other: ~ %{count}h
about_x_months:
one: ~ 1mo
other: ~ %{count}mo
about_x_years:
one: ~ 1y
other: ~ %{count}y
almost_x_years:
...
This information is also available in the documentation:
https://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-distance_of_time_in_words
Here's some other ways to do it. You run the method like normal, and then you use gsub on the string.
Chained
string.gsub(/ mi.*/, 'm')
.gsub(/ h.*/, 'h')
.gsub(/ d.*/, 'd')
.gsub(/ mo.*/, 'mo')
.gsub(/ y.*/, 'y')
Hash
string.gsub(/ .+/, {
' minute'=> 'm', ' minutes'=>'m',
' hour' => 'h', ' hours' => 'h',
' day' => 'd', ' days' => 'd',
' month' => 'mo', ' months' => 'mo',
' year' => 'y', ' years' => 'y'
})
Block
string.gsub(/ .+/) { |x| x[/mo/] ? 'mo' : x[1] }
They all do the same except for when the string is "less than a minute". The chained solution returns "less than a minute". The hash solution returns "less". The block solution returns "lesst".
Just change the locale file for this one case
en:
datetime:
distance_in_words:
less_than_x_minutes:
one: '<1m'
Or add a return to clause at the top of your method
def my_method(string)
return '<1m' if string == 'less than a minute'
# code
end
Note: Does not include solutions for include_seconds: true option.

Format the date using Ruby on Rails

The flickr api provides a posted date as unix timestamp one: "The posted date is always passed around as a unix timestamp, which is an unsigned integer specifying the number of seconds since Jan 1st 1970 GMT."
For example, here is the date '1100897479'. How do I format it using Ruby on Rails?
Once you have parsed the timestamp string and have a time object (see other answers for details), you can use Time.to_formatted_s from Rails. It has several formats built in that you can specify with symbols.
Quote:
time = Time.now # => Thu Jan 18 06:10:17 CST 2007
time.to_formatted_s(:time) # => "06:10"
time.to_s(:time) # => "06:10"
time.to_formatted_s(:db) # => "2007-01-18 06:10:17"
time.to_formatted_s(:number) # => "20070118061017"
time.to_formatted_s(:short) # => "18 Jan 06:10"
time.to_formatted_s(:long) # => "January 18, 2007 06:10"
time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10"
time.to_formatted_s(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600"
(Time.to_s is an alias)
You can also define your own formats - usually in an initializer (Thanks to Dave Newton for pointing this out). This is how it's done:
# config/initializers/time_formats.rb
Time::DATE_FORMATS[:month_and_year] = "%B %Y"
Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }
Here's my go at answering this,
so first you will need to convert the timestamp to an actual Ruby Date/Time. If you receive it just as a string or int from facebook, you will need to do something like this:
my_date = Time.at(timestamp_from_facebook.to_i)
OK, so now assuming you already have your date object...
to_formatted_s is a handy Ruby function that turns dates into formatted strings.
Here are some examples of its usage:
time = Time.now # => Thu Jan 18 06:10:17 CST 2007
time.to_formatted_s(:time) # => "06:10"
time.to_s(:time) # => "06:10"
time.to_formatted_s(:db) # => "2007-01-18 06:10:17"
time.to_formatted_s(:number) # => "20070118061017"
time.to_formatted_s(:short) # => "18 Jan 06:10"
time.to_formatted_s(:long) # => "January 18, 2007 06:10"
time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10"
time.to_formatted_s(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600"
As you can see: :db, :number, :short ... are custom date formats.
To add your own custom format, you can create this file: config/initializers/time_formats.rb and add your own formats there, for example here's one:
Date::DATE_FORMATS[:month_day_comma_year] = "%B %e, %Y" # January 28, 2015
Where :month_day_comma_year is your format's name (you can change this to anything you want), and where %B %e, %Y is unix date format.
Here's a quick cheatsheet on unix date syntax, so you can quickly setup your custom format:
From http://linux.die.net/man/3/strftime
%a - The abbreviated weekday name (``Sun'')
%A - The full weekday name (``Sunday'')
%b - The abbreviated month name (``Jan'')
%B - The full month name (``January'')
%c - The preferred local date and time representation
%d - Day of the month (01..31)
%e - Day of the month without leading 0 (1..31)
%g - Year in YY (00-99)
%H - Hour of the day, 24-hour clock (00..23)
%I - Hour of the day, 12-hour clock (01..12)
%j - Day of the year (001..366)
%m - Month of the year (01..12)
%M - Minute of the hour (00..59)
%p - Meridian indicator (``AM'' or ``PM'')
%S - Second of the minute (00..60)
%U - Week number of the current year,
starting with the first Sunday as the first
day of the first week (00..53)
%W - Week number of the current year,
starting with the first Monday as the first
day of the first week (00..53)
%w - Day of the week (Sunday is 0, 0..6)
%x - Preferred representation for the date alone, no time
%X - Preferred representation for the time alone, no date
%y - Year without a century (00..99)
%Y - Year with century
%Z - Time zone name
%% - Literal ``%'' character
t = Time.now
t.strftime("Printed on %m/%d/%Y") #=> "Printed on 04/09/2003"
t.strftime("at %I:%M%p") #=> "at 08:56AM"
Hope this helped you.
I've also made a github gist of this little guide, in case anyone prefers.
Easiest is to use strftime (docs).
If it's for use on the view side, better to wrap it in a helper, though.
#CMW's answer is bang on the money. I've added this answer as an example of how to configure an initializer so that both Date and Time objects get the formatting
config/initializers/time_formats.rb
date_formats = {
concise: '%d-%b-%Y' # 13-Jan-2014
}
Time::DATE_FORMATS.merge! date_formats
Date::DATE_FORMATS.merge! date_formats
Also the following two commands will iterate through all the DATE_FORMATS in your current environment, and display today's date and time in each format:
Date::DATE_FORMATS.keys.each{|k| puts [k,Date.today.to_formatted_s(k)].join(':- ')}
Time::DATE_FORMATS.keys.each{|k| puts [k,Time.now.to_formatted_s(k)].join(':- ')}
Have a look at localize, or l
eg:
l Time.at(1100897479)
First you will need to convert the timestamp to an actual Ruby Date/Time.
If you receive it just as a string or int from facebook, you will need to do something like this:
my_date = Time.at(timestamp_from_facebook.to_i)
Then to format it nicely in the view, you can just use to_s (for the default formatting):
<%= my_date.to_s %>
Note that if you don't put to_s, it will still be called by default if you use it in a view or in a string e.g. the following will also call to_s on the date:
<%= "Here is a date: #{my_date}" %>
or if you want the date formatted in a specific way (eg using "d/m/Y") - you can use strftime as outlined in the other answer.
Since the timestamps are seconds since the UNIX epoch, you can use DateTime.strptime ("string parse time") with the correct specifier:
Date.strptime('1100897479', '%s')
#=> #<Date: 2004-11-19 ((2453329j,0s,0n),+0s,2299161j)>
Date.strptime('1100897479', '%s').to_s
#=> "2004-11-19"
DateTime.strptime('1100897479', '%s')
#=> #<DateTime: 2004-11-19T20:51:19+00:00 ((2453329j,75079s,0n),+0s,2299161j)>
DateTime.strptime('1100897479', '%s').to_s
#=> "2004-11-19T20:51:19+00:00"
Note that you have to require 'date' for that to work, then you can call it either as Date.strptime (if you only care about the date) or DateTime.strptime (if you want date and time). If you need different formatting, you can call DateTime#strftime (look at strftime.net if you have a hard time with the format strings) on it or use one of the built-in methods like rfc822.

Convert Ruby Date to Integer

How can I convert a Ruby Date to an integer?
t = Time.now
# => 2010-12-20 11:20:31 -0700
# Seconds since epoch
t.to_i
#=> 1292869231
require 'date'
d = Date.today
#=> #<Date: 2010-12-20 (4911101/2,0,2299161)>
epoch = Date.new(1970,1,1)
#=> #<Date: 1970-01-01 (4881175/2,0,2299161)>
d - epoch
#=> (14963/1)
# Days since epoch
(d - epoch).to_i
#=> 14963
# Seconds since epoch
d.to_time.to_i
#=> 1292828400
Date cannot directly become an integer. Ex:
$ Date.today
=> #<Date: 2017-12-29 ((2458117j,0s,0n),+0s,2299161j)>
$ Date.today.to_i
=> NoMethodError: undefined method 'to_i' for #<Date: 2017-12-29 ((2458117j,0s,0n),+0s,2299161j)>
Your options are either to turn the Date into a time then an Int which will give you the seconds since epoch:
$ Date.today.to_time.to_i
=> 1514523600
Or come up with some other number you want like days since epoch:
$ Date.today.to_time.to_i / (60 * 60 * 24) ### Number of seconds in a day
=> 17529 ### Number of days since epoch
Time.now.to_i
returns seconds since epoch format
Solution for Ruby 1.8 when you have an arbitrary DateTime object:
1.8.7-p374 :001 > require 'date'
=> true
1.8.7-p374 :002 > DateTime.new(2012, 1, 15).strftime('%s')
=> "1326585600"
I had to do it recently and took some time to figure it out but that is how I came across a solution and it may give you some ideas:
require 'date'
today = Date.today
year = today.year
month = today.mon
day = day.mday
year = year.to_s
month = month.to_s
day = day.to_s
if month.length <2
month = "0" + month
end
if day.length <2
day = "0" + day
end
today = year + month + day
today = today.to_i
puts today
At the date of this post, It will put 20191205.
In case the month or day is less than 2 digits it will add a 0 on the left.
I did like this because I had to compare the current date whit some data that came from a DB in this format and as an integer. I hope it helps you.

Ruby (with Rails) convert a string of time into seconds?

So, I've got a string of time... something along the lines of
'4 hours'
'48 hours'
'3 days'
'15 minutes'
I would like to convert those all into seconds. For '4 hours', this works fine
Time.parse('4 hours').to_i - Time.parse('0 hours').to_i
=> 14400 # 4 hours in seconds, yay
However, this doesn't work for 48 hours (outside of range error). It also does not work for 3 days (no information error), etc.
Is there a simple way to convert these strings into seconds?
What you're asking Ruby to do with Time.parse is determine a time of day. That's not what you are wanting. All of the libraries I can think of are similar in this aspect: they are interested in absolute times, not lengths of time.
To convert your strings into time formats that we can work with, I recommend using Chronic (gem install chronic). To convert to seconds, we can do everything relative to the current time, then subtract that time to get the absolute number of seconds, as desired.
def seconds_in(time)
now = Time.now
Chronic.parse("#{time} from now", :now => now) - now
end
seconds_in '48 hours' # => 172,800.0
seconds_in '15 minutes' # => 900.0
seconds_in 'a lifetime' # NoMethodError, not 42 ;)
A couple quick notes:
The from now is is why Chronic is needed — it handles natural language input.
We're specifying now to be safe from a case where Time.now changes from the time that Chronic does it's magic and the time we subtract it from the result. It might not occur ever, but better safe than sorry here I think.
'48 hours'.match(/^(\d+) (minutes|hours|days)$/) ? $1.to_i.send($2) : 'Unknown'
=> 172800 seconds
4.hours => 14400 seconds
4.hours.to_i 14400
4.hours - 0.hours => 14400 seconds
def string_to_seconds string
string.split(' ')[0].to_i.send(string.split(' ')[1]).to_i
end
This helper method will only work if the time is in the format of number[space]hour(s)/minute(s)/second(s)
Chronic will work, but Chronic Duration is a better fit.
It can parse a string and give you seconds.
require "chronic_duration"
ChronicDuration::parse('15 minutes')
# or
ChronicDuration::parse('4 hours')
http://everydayrails.com/2010/08/11/ruby-date-time-parsing-chronic.html
I'm sure you would get some good work out of chronic gem.
Also, here is some good to know info about dates/times in ruby
>> strings = ['4 hours', '48 hours', '3 days', '15 minutes', '2 months', '5 years', '2 decades']
=> ["4 hours", "48 hours", "3 days", "15 minutes", "2 months", "5 years", "2 decades"]
>> ints = strings.collect{|s| eval "#{s.gsub(/\s+/,".")}.to_i" rescue "Error"}
=> [14400, 172800, 259200, 900, 5184000, 157788000, "Error"]

Resources