How to pluralize "There is/are N object/objects"? - ruby-on-rails

Pluralizing a single word is simple:
pluralize(#total_users, "user")
But what if I want to print "There is/are N user/users":
There are 0 users
There is 1 user
There are 2 users
, i.e., how to pluralize a sentence?

You can add a custom inflection for it. By default, Rails will add an inflections.rb to config/initializers. There you can add:
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular "is", "are"
end
You will then be able to use pluralize(#total_users, "is") to return is/are using the same rules as user/users.
EDIT: You clarified the question on how to pluralize a sentence. This is much more difficult to do generically, but if you want to do it, you'll have to dive into NLP.
As the comment suggests, you could do something with I18n if you just want to do it with a few sentences, you could build something like this:
def pluralize_sentence(count, i18n_id, plural_i18n_id = nil)
if count == 1
I18n.t(i18n_id, :count => count)
else
I18n.t(plural_i18n_id || (i18n_id + "_plural"), :count => count)
end
end
pluralize_sentence(#total_users, "user_count")
And in config/locales/en.yml:
en:
user_count: "There is %{count} user."
user_count_plural: "There are %{count} users."

This is probably best covered by the Rails i18n pluralization features. Adapted from http://guides.rubyonrails.org/i18n.html#pluralization
I18n.backend.store_translations :en, :user_msg => {
:one => 'There is 1 user',
:other => 'There are %{count} users'
}
I18n.translate :user_msg, :count => 2
# => 'There are 2 users'

I think the first part of Martin Gordon's answer is pretty good.
Alternatively, it's kind of messy but you can always just write the logic yourself:
"There #{#users.size == 1 ? 'is' : 'are'} #{#users.size} user#{'s' unless #users.size == 1}."

UPDATE to code: I no longer use the inflections route as stated in #Martin Gordon's answer. For some reason it would cause other non-related functions to error. I did extensive tests to confirm, though could not track down a reason why. So, below is now what I use and it works.
There are many ways to do this. This is how I did it using Rails 6.0.3.4 and Ruby 2.7.1.
I wanted to pluralize this sentence:
Singular: There is 1 private group
Plural: There are 2 private groups
What I did is I went to application_helper.rb and added this code:
def pluralize_private_statement(list, word)
num_in_list = list.count
is_or_are = num_in_list == 1 ? 'is' : 'are'
return "There " + is_or_are + " " + num_in_list.to_s + " private " + word.pluralize(num_in_list)
end
Now, all I have to use in my view is:
<%= pluralize_private_statement(private_groups, "group") %>
# private_groups = 2
# -> There are 2 private groups
What the code in application_helper.rb does is first create a variable for the number of items in the list passed and store it in num_in_list. Then it creates a second varible checking if the num_in_list is equal to 1 and if so returns 'is' otherwise it returns 'are'. Then, it returns the sentence that is constructed with the information obtained.
The first part of the sentence is a simple string, then the is_or_are variable which holds either 'is' or 'are' as explained above. Then it adds a space with the number of list items, converted from an integer to a string, followed by the 'private' word. Then it adds the pluralization of the word passed to the initial function; but, only returns the singular/plural word without a number attached as pluralize(#total_users, "is") would do.
Here is how you could use it for your specific question.
First, add this to your application_helper.rb file:
def pluralize_sentence(list, word)
num_in_list = list.count
is_or_are = num_in_list == 1 ? 'is' : 'are'
return "There " + is_or_are + " " + num_in_list.to_s + " " + word.pluralize(num_in_list)
end
Lastly, you can use this code wherever you wish to have the pluralized sentence:
<%= pluralize_sentence(#total_users, "user") %>
Happy Coding!

Related

Truncate sentences in rails?

I've got a helper that I'm using to truncate strings in Rails, and it works great when I truncate sentences that end in periods. How should I modify the code to also truncate sentences when they end in question marks or exclamation points?
def smart_truncate(s, opts = {})
opts = {:words => 12}.merge(opts)
if opts[:sentences]
return s.split(/\.(\s|$)+/).reject{ |s| s.strip.empty? }[0, opts[:sentences]].map{|s| s.strip}.join('. ') + '...'
end
a = s.split(/\s/) # or /[ ]+/ to only split on spaces
n = opts[:words]
a[0...n].join(' ') + (a.size > n ? '... (more)' : '')
end
Thanks!!!
You have the truncate method
'Once upon a time in a world far far away'.truncate(27, separator: /\s/, ommission: "....")
which will return "Once upon a time in a..."
And if you need to truncate by number of words instead then use the newly introduced truncate_words (since Rails 4.2.2)
'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)')
which returns
"And they found that many... (continued)"

Capitalize First Letter of all Words and Keep Already Capitalized

Using rails 4, and having trouble finding documentation on this. I would like to capitalize the first letter of each word in a string but keep already capitalized letters.
I would like the following outputs:
how far is McDonald's from here? => How Far Is McDonald's From Here?
MDMA is also known as molly => MDMA Is Also Known As Molly
i drive a BMW => I Drive A BMW
I thought .titleize would do it, but that will turn BMW into Bmw. Thank you for any help.
You can try the following:
a.split.map{|x| x.slice(0, 1).capitalize + x.slice(1..-1)}.join(' ')
# or
a.split.map{|x| x[0].upcase + x[1..-1]}.join(' ')
#=> ["MDMA Is Also Known As Molly",
"How Far Is McDonald's From Here?",
"I Drive A BMW"]
Demonstration
You can do a custom method like this:
string = "your string IS here"
output = []
string.split(' ').each do |word|
if word =~ /[A-Z]/
output << word
else
output << word.capitalize
end
end
output.join(' ')
Of course, this will not change a word like "tEST" or "tEst" because it has at least one capital letter in it.
to capitalize only the first letter while preserving existing capitalization:
your_string.then { |s| s[0].upcase + s[1..-1] }

Ruby regex puncuation

I am having trouble writing this so that it will take a sentence as an argument and perform the translation on each word without affecting the punctuation.
I'd also like to continue using the partition method.
It would be nice if I could have it keep a quote together as well, such as:
"I said this", I said.
would be:
"I aidsay histay", I said.
def convert_sentence_pig_latin(sentence)
p split_sentence = sentence.split(/\W/)
pig_latin_sentence = []
split_sentence.each do |word|
if word.match(/^[^aeiou]+/x)
pig_latin_sentence << word.partition(/^[^aeiou]+/x)[2] + word.partition(/^[^aeiou]+/x)[1] + "ay"
else
pig_latin_sentence << word
end
end
rejoined_pig_sentence = pig_latin_sentence.join(" ").downcase + "."
p rejoined_pig_sentence.capitalize
end
convert_sentence_pig_latin("Mary had a little lamb.")
Your main problem is that [^aeiou] matches every character outside that range, including spaces, commas, quotation marks, etc.
If I were you, I'd use a positive match for consonants, ie. [b-df-hj-np-tv-z] I would also put that regex in a variable, so you're not having to repeat it three times.
Also, in case you're interested, there's a way to make your convert_sentence_pig_latin method a single gsub and it will do the whole sentence in one pass.
Update
...because you asked...
sentence.gsub( /\b([b-df-hj-np-tv-z])(\w+)/i ) { "#{$2}#{$1}ay" }
# iterate over and replace regexp matches using gsub
def convert_sentence_pig_latin2(sentence)
r = /^[^aeiou]+/i
sentence.gsub(/"([^"]*)"/m) {|x| x.gsub(/\w+/) {|y| y =~ r ? "#{y.partition(r)[2]}#{y.partition(r)[1]}ay" : y}}
end
puts convert_sentence_pig_latin2('"I said this", I said.')
# define instance method: String#to_pl
class String
R = Regexp.new '^[^aeiou]+', true # => /^[^aeiou]+/i
def to_pl
self.gsub(/"([^"]*)"/m) {|x| x.gsub(/\w+/) {|y| y =~ R ? "#{y.partition(R)[2]}#{y.partition(R)[1]}ay" : y}}
end
end
puts '"I said this", I said.'.to_pl
sources:
http://www.ruby-doc.org/core-2.1.0/Regexp.html
http://ruby-doc.org/core-2.0/String.html#method-i-gsub

How can I get will_paginate to correctly display the number of entries?

WillPaginate has a page_entries_info view helper to output text like "Displaying contracts 1 - 35 of 4825 in total".
However, I'm finding that when I try to use it like this...
= page_entries_info #contracts
It outputs...
Displaying Contract 1 - 35 of 4825 in total
(It outputs the singular name of the model, rather than pluralized, all lower case.)
Do I need to feed it some other param?
I tried page_entries_info #contracts, :model => Contract but got the same result.
I'm using version 3.0.3 -- the current version.
Incidentally, can someone point me to the API docs for WillPaginate?
will_paginate API docs: https://github.com/mislav/will_paginate/wiki/API-documentation
Short answer
You can specify the model option as a string, which will be correctly pluralized.
page_entries_info #contracts, :model => 'contract'
# Displaying contracts 1 - 35 of 4825 in total
Longer answer
The will_paginate docs suggest that you use the i18n mechanism for customizing output. This is kind of a pain since AFAICT you have to write out singular and plural form for all of your models in the config/locales/*.yml files (e.g., en.yml), and the %{foo}-style syntax doesn't seem to be ERB, but just placeholders, so you can't do things like %{foo.downcase}.
If you write your own helper, you get complete control over the output. For example:
def my_page_info(collection)
model_class = collection.first.class
if model_class.respond_to? :model_name
model = model_class.model_name.human.downcase
models = model.pluralize
if collection.total_entries == 0
"No #{models} found."
elsif collection.total_entries == 1
"Found one #{model}."
elsif collection.total_pages == 1
"Displaying all #{collection.total_entries} #{models}"
else
"Displaying #{models} #{collection.offset + 1} " +
"to #{collection.offset + collection.length} " +
"of #{number_with_delimiter collection.total_entries} in total"
end
end
end
# Displaying contracts 1 - 35 of 4,825 in total

Overloading ActiveSupport's default to_sentence behaviour

ActiveSupport offers the nice method to_sentence. Thus,
require 'active_support'
[1,2,3].to_sentence # gives "1, 2, and 3"
[1,2,3].to_sentence(:last_word_connector => ' and ') # gives "1, 2 and 3"
it's good that you can change the last word connector, because I prefer not to have the extra comma. but it takes so much extra text: 44 characters instead of 11!
the question: what's the most ruby-like way to change the default value of :last_word_connector to ' and '?
Well, it's localizable so you could just specify a default 'en' value of ' and ' for support.array.last_word_connector
See:
from: conversion.rb
def to_sentence(options = {})
...
default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale])
...
end
Step by step guide:
First, Create a rails project
rails i18n
Next, edit your en.yml file: vim config/locales/en.yml
en:
support:
array:
last_word_connector: " and "
Finally, it works:
Loading development environment (Rails 2.3.3)
>> [1,2,3].to_sentence
=> "1, 2 and 3"
As an answer to how to override a method in general, a post here gives a nice way of doing it. It doesn't suffer from the same problems as the alias technique, as there isn't a leftover "old" method.
Here how you could use that technique with your original problem (tested with ruby 1.9)
class Array
old_to_sentence = instance_method(:to_sentence)
define_method(:to_sentence) { |options = {}|
options[:last_word_connector] ||= " and "
old_to_sentence.bind(self).call(options)
}
end
You might also want read up on UnboundMethod if the above code is confusing. Note that old_to_sentence goes out of scope after the end statement, so it isn't a problem for future uses of Array.
class Array
alias_method :old_to_sentence, :to_sentence
def to_sentence(args={})
a = {:last_word_connector => ' and '}
a.update(args) if args
old_to_sentence(a)
end
end

Resources