Ruby on rails tests - ruby-on-rails

In controller I have got this:
def output
if params[:find]
result
elsif params[:print_db]
print_db
end
end
And the test doesn't want to enter in params[:find] condition.
Test controller:
test "should get 5 5 1 for view with 5 5 1 3 3" do
get :output, {str: "5 5 1 3 3 5"}, :params => 'find'
assert_equal assigns[:result], "5, 5, 1"
end
Please, help me!

It seems like you want to have both: A params[:str] and a params[:find] parameter.
Change:
get :output, {str: "5 5 1 3 3 5"}, :params => 'find'
to:
get :output, str: '5 5 1 3 3 5', find: true
Because your version passes the :params => 'find' part into the session variable not into the params. You might want to read in the Rails' guide about what parameters the get method accepts.

Try this:
get :output, { str: "5 5 1 3 3 5", find: true }
In your request you send only the str with the value "5 5 1 3 3 5" and no find parameter. You have to put all parameters into the first hash.

Related

Ruby debugger: weird sort of caching

I'm using Ruby debugger gem in my RoR project. Ruby 1.9.3 and Rails 3.2
Debugger works fine by a sort of weird file caching occurs.
I mean:
First example
[4, 13] in /.../app/controllers/fork_controller.rb
4 def index
5
6 debugger
7 a = "hello"
8
=> 9 puts "#{a}"
10
11 end
12
13 def requesting
(rdb:4) eval a
"hello"
(rdb:4) continue
Then I change var "a" to "hello2"
def index
debugger
a = "hello2"
puts "#{a}"
end
this is the result:
[4, 13] in /.../app/controllers/fork_controller.rb
4 def index
5
6 debugger
7 a = "hello"
8
=> 9 puts "#{a}"
10
11 end
12
13 def requesting
(rdb:4) eval a
"hello2"
(rdb:4) continue
So ... the variable evaluation is correct "hello2" instead of "hello" but ... the "breakpoint view" show the first variable content "hello".
What is wrong?
Thanks in advance.

Best practices for hard-coded strings

I am writing a ruby script where I read commands from command line and check if they are correct. If not I show the proportionate error.
My code looks like this:
if command == 0
puts "error one #{command}"
elsif command == 1
puts "other error two #{command}"
...
end
I have a lot of different error-strings and they have ruby code in it.
I was thinking to create a hash but I can not add ruby code in the error-string.
Is there any better way to manage (hard-coded) error-strings?
If the code is always going to be at the end, then this might work:
Errors = {
0 => "error one",
1 => "other error two",
}.freeze
# later...
command = 1
puts "#{Errors.fetch(command)} #{command}"
#=> other error two 1
Otherwise, you can add a custom placeholder and later substitute in the error code:
Errors = {
0 => "error %{code} one",
1 => "%{code} other error two",
}.freeze
def error_str_for_code(code)
Errors.fetch(code) % { code: code.to_s }
end
# later...
command = 1
puts error_str_for_code(command)
#=> 1 other error two

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);

Resources