Rails shorter "time_ago_in_words" - ruby-on-rails

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.

Related

How do I prevent ActiveSupport using units, tens, and hundreds localisations in number_to_human?

I've set up a custom locale to get ActiveSupport to use short suffixes when calling number_to_human. Instead of number_to_human(123456) => '123.4 Thousand', it gives me number_to_human(123456) => '123.4k'.
This all works fine. What doesn't work is that while the default locale would leave smaller numbers alone (i.e. number_to_human(56) => 56), my custom locale doesn't. I've left the suffixes for units, tens, and hundreds blank, but this results in number_to_human(52) => '5.2' (i.e. 5.2 tens) or number_to_human(123) => '1.23' (for 1.23 hundreds).
How do I tell ActiveSupport not to use units, tens, or hundreds at all - to just leave numbers under 1000 alone?
Here's the locale file, if it helps (config/locales/en-ABBREV.yml):
en-ABBREV:
datetime:
distance_in_words:
x_seconds: '%{count}s'
x_minutes: '%{count}m'
about_x_hours: '%{count}h'
x_hours: '%{count}h'
x_days: '%{count}d'
x_weeks: '%{count}w'
about_x_months: '%{count}mo'
x_months: '%{count}mo'
x_years: '%{count}y'
number:
human:
unit: ''
ten: ''
hundred: ''
thousand: 'k'
million: 'm'
billion: 'b'
trillion: 't'
quadrillion: 'qd'
And my calls to number_to_human in the view look like this:
number_to_human #posts.count, precision: 1, significant: false, locale: 'en-ABBREV',
units: 'number.human', format: '%n%u'
Looking at the docs of that method I think you can define the unit you want to use like the following. When a key (like tens) is not included in the units then that units will just not be used.
number_to_human(
#posts.count,
format: '%n%u',
precision: 1,
significant: false
units: {
thousand: 'k',
million: 'm',
billion: 'b',
trillion: 't',
quadrillion: 'qd'
}
)

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"

How to convert "3.days" into the string "3 days" in 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"

i18n Pluralization

I want to be able to translate pluralized strings in i18n in rails. A string can be :
You have 2 kids
or
You have 1 kid
I know that I can use pluralize helper method, but I want to embed this in i18n translations so that I don't have to mess up with my views at any point in the future. I read that :count is somehow used in translations for plural, but I can't find any real resources on how it gets implemented.
Notice that I know that I can pass a variable in a translation string. I also tried something like :
<%= t 'misc.kids', :kids_num => pluralize(1, 'kid') %>
Which works fine, but has a fundamental problem of the same idea. I need to specify the string 'kid' in the pluralize helper. I don't want to do that because it will lead to view problems in the future. Instead I want to keep everything in the translation and nothing in the view.
How can I do that ?
Try this:
en.yml :
en:
misc:
kids:
zero: no kids
one: 1 kid
other: %{count} kids
In a view:
You have <%= t('misc.kids', :count => 4) %>
Updated answer for languages with multiple pluralization (tested with Rails 3.0.7):
File config/initializers/pluralization.rb:
require "i18n/backend/pluralization"
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
File config/locales/plurals.rb:
{:ru =>
{ :i18n =>
{ :plural =>
{ :keys => [:one, :few, :other],
:rule => lambda { |n|
if n == 1
:one
else
if [2, 3, 4].include?(n % 10) &&
![12, 13, 14].include?(n % 100) &&
![22, 23, 24].include?(n % 100)
:few
else
:other
end
end
}
}
}
}
}
#More rules in this file: https://github.com/svenfuchs/i18n/blob/master/test/test_data/locales/plurals.rb
#(copy the file into `config/locales`)
File config/locales/en.yml:
en:
kids:
zero: en_zero
one: en_one
other: en_other
File config/locales/ru.yml:
ru:
kids:
zero: ru_zero
one: ru_one
few: ru_few
other: ru_other
Test:
$ rails c
>> I18n.translate :kids, :count => 1
=> "en_one"
>> I18n.translate :kids, :count => 3
=> "en_other"
>> I18n.locale = :ru
=> :ru
>> I18n.translate :kids, :count => 1
=> "ru_one"
>> I18n.translate :kids, :count => 3
=> "ru_few" #works! yay!
>> I18n.translate :kids, :count => 5
=> "ru_other" #works! yay!
I hope Russian-speaking Ruby on Rails programmers could find this. Just want to share my own very precise Russian pluralization formula. It based on Unicode Specs.
Here is contents of config/locales/plurals.rb file only, everything else should be done as same as in answer above.
{:ru =>
{ :i18n =>
{ :plural =>
{ :keys => [:zero, :one, :few, :many],
:rule => lambda { |n|
if n == 0
:zero
elsif
( ( n % 10 ) == 1 ) && ( ( n % 100 != 11 ) )
# 1, 21, 31, 41, 51, 61...
:one
elsif
( [2, 3, 4].include?(n % 10) \
&& ![12, 13, 14].include?(n % 100) )
# 2-4, 22-24, 32-34...
:few
elsif ( (n % 10) == 0 || \
![5, 6, 7, 8, 9].include?(n % 10) || \
![11, 12, 13, 14].include?(n % 100) )
# 0, 5-20, 25-30, 35-40...
:many
end
}
}
}
}
}
Native speakers may enjoy cases such as 111 and 121.
And here the test results:
zero: 0 запросов/куриц/яблок
one: 1 запрос/курица/яблоко
few: 3 запроса/курицы/яблока
many: 5 запросов/куриц/яблок
one: 101 запрос/курица/яблоко
few: 102 запроса/курицы/яблока
many: 105 запросов/куриц/яблок
many: 111 запросов/куриц/яблок
many: 119 запросов/куриц/яблок
one: 121 запрос/курица/яблоко
few: 122 запроса/курицы/яблока
many: 125 запросов/куриц/яблок
Thanks for initial answer!
First, remember that number of plural forms depends on language, for English there are two, for Romanian there are 3 and for Arabic there are 6 !.
If you want to be able to properly use plural forms you have to use gettext.
For Ruby and rails you should check this http://www.yotabanana.com/hiki/ruby-gettext-howto-rails.html
English
It just works out of the box
en.yml:
en:
kid:
zero: 'no kids'
one: '1 kid'
other: '%{count} kids'
Usage (you can skip I18n in a view file, of course):
> I18n.t :kid, count: 1
=> "1 kid"
> I18n.t :kid, count: 3
=> "3 kids"
Russian (and other languages with multiple plural forms)
Install rails-18n gem and add translations to your .yml files as in the example:
ru.yml:
ru:
kid:
zero: 'нет детей'
one: '%{count} ребенок'
few: '%{count} ребенка'
many: '%{count} детей'
other: 'дети'
Usage:
> I18n.t :kid, count: 0
=> "нет детей"
> I18n.t :kid, count: 1
=> "1 ребенок"
> I18n.t :kid, count: 3
=> "3 ребенка"
> I18n.t :kid, count: 5
=> "5 детей"
> I18n.t :kid, count: 21
=> "21 ребенок"
> I18n.t :kid, count: 114
=> "114 детей"
> I18n.t :kid, count: ''
=> "дети"
Rails 3 handles this robustly with CLDR consideration and count interpolation variable. See http://guides.rubyonrails.org/i18n.html#pluralization
# in view
t('actors', :count => #movie.actors.size)
# locales file, i.e. config/locales/en.yml
en:
actors:
one: Actor
other: Actors
There is actually an alternative to the cumbersome i18n approach. The solution is called Tr8n.
Your above code would simply be:
<%= tr("You have {num || kid}", num: 1) %>
That's it. No need to extract your keys from your code and maintain them in resource bundles, no need to implement pluralization rules for each language. Tr8n comes with numeric context rules for all language. It also comes with gender rules, list rules and language cases.
The full definition of the above translation key would actually look like this:
<%= tr("You have {num:number || one: kid, other: kids}", num: 1) %>
But since we want to save space and time, num is automatically mapped to numeric rules and there is no need to provide all options for the rule values. Tr8n comes with pluralizers and inflectors that will do the work for you on the fly.
The translation for your key in Russian, would simply be:
"У вас есть {num || ребенок, ребенка, детей}"
By the way, your translation would be inaccurate in languages that have gender specific rules.
For example, in Hebrew, you would actually have to specify at least 2 translations for your example, as "You" would be different based on the gender of the viewing user. Tr8n handles it very well. Here is a transliteration of Hebrew translations:
"Yesh leha yeled ahad" with {context: {viewing_user: male, num: one}}
"Yesh leha {num} yeladim" with {context: {viewing_user: male, num: other}}
"Yesh lah yeled ahad" with {context: {viewing_user: female, num: one}}
"Yesh lah {num} yeladim" with {context: {viewing_user: female, num: other}}
So your single English key, in this case, needs 4 translations. All translations are done in context - you don't have to break the sentence. Tr8n has a mechanism to map one key to multiple translations based on the language and context - all done on the fly.
One last thing. What if you had to make the count part bold? It would simply be:
<%= tr("You have [bold: {num || kid}]", num: 1, bold: "<strong>{$0}</strong>") %>
Just in case you want to redefine your "bold" later - it would be very easy - you won't have to go through all your YAML files and change them - you just do it in one place.
To learn more, please take a look here:
https://github.com/tr8n/tr8n_rails_clientsdk
Disclosure: I am the developer and the maintainer of Tr8n framework and all its libraries.
About Redmine. If you copy pluralization file rules in config/locales/ as plurals.rb and other not same as locale name (ru.rb, pl.rb .. etc) these not work.
You must rename file rules to 'locale'.rb or change method in file /lib/redmine/i18n.rb
def init_translations(locale)
locale = locale.to_s
paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale}
load_translations(paths)
translations[locale] ||= {}
end
and if you have older redmine, add
module Implementation
include ::I18n::Backend::Base
**include ::I18n::Backend::Pluralization**
I've found a very good resource with description of locales http://translate.sourceforge.net/wiki/l10n/pluralforms ( alt link ).
For example, for ukrainian, russian, belarusian and few other languages the expression will look like this plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);

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