Set unit for number_to_currency with a user setting? - ruby-on-rails

I want to allow users to change the currency unit throughout their account.
The obvious way to do it is to pass the unit parameter to number_to_currency, but given number_to_currency is used hundreds of times throughout the app, it seems a little repetitive to do that.
So is there some way to change what unit is used for all instances of number_to_currency based on a setting stored in the database for each user?

Sounds to me like you need some sort of global function / variable to define the symbol
I would do it like this:
#app/helpers/application_helper.rb
def unit
User.find(current_user.id).select(:currency_type) #I don't know how your units are stored - you may need logic to return the correctly formatted unit
end
This will allow you to call: <%= number_to_currency, unit: unit %>
Overriding Helper Method
number_to_currency is literally just a helper itself, which means you can append options on the fly:
Original
# File actionpack/lib/action_view/helpers/number_helper.rb, line 106
def number_to_currency(number, options = {})
return unless number
options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
wrap_with_output_safety_handling(number, options.delete(:raise)) {
ActiveSupport::NumberHelper.number_to_currency(number, options)
}
end
Amended
#app/helpers/application_herlper.rb
def number_to_currency(number, options ={})
unit = User.find(current_user.id).select(:currency_type)
options[:unit] = unit unless options[:unit].present?
super
end

You could pass the currency as an option to the number_to_currency method as shown:
number_to_currency(1234567890.506, locale: :fr)
In that case you would need to replace :fr to whatever points to the user's setting and to create such a locale with such options:
number:
currency:
format:
unit: '€'
format: '%n %u'
separator: ","
delimiter: "."
precision: 2
significant: false
strip_insignificant_zeros: false
Or you set the unit in another way:
number_to_currency(1234567890.50, unit: "£", format: "%u %n")
=> "£ 1.234.567.890,50"
Hope this helps you.

Related

Rails helper pass negative symbol as argument in self method

In my Rails 6, Ruby 2.7 app I'm using ActionView::Helpers::NumberHelper and number_to_currency method. Everything works fine but in one place I need to have negative amount instead of positive. To do so I created two methods:
formatters/document_amount_formatter.rb
module Formatters
# Formats the amount in a suitable form to be used in PDF creator.
class DocumentAmountFormatter
extend ActionView::Helpers::NumberHelper
# The method to call.
#
# #return [String]
def self.call(amount)
number_to_currency(amount.to_f, delimiter: '.', separator: ',', format: '%n €')
end
def self.negative_amount(amount)
number_to_currency(-amount.to_f, delimiter: '.', separator: ',', format: '%n €')
end
end
end
Both works well:
Formatters::CashbookDocumentAmountFormatter.call(cash_transactions.first.gross_amount)
=> "100,00 €"
Formatters::CashbookDocumentAmountFormatter.negative_amount(cash_transactions.first.gross_amount)
=> "-100,00 €"
But I'm not so sure if this is a good approach, tbh the code seems to be smelly. Is it possible to change those two methods into one? How to pass '-' or '+' as an argument inside of one of these methods?
Call call from within negative_amount.
def self.negative_amount(amount)
call(-amount)
end
The next question being, why have this method at all? The caller can write formatter.call(-amount) just as easily and more obviously.
Note that you probably shouldn't hard-code currency formatting and instead make use if internationalization.

prompt option not working with select_tag

If there is value in marital_status then prompt should not be displayed but in my case it's displaying. My code is mentioned below. Please help.
= select_tag( 'request[marital_status]',
options_for_select(marital_status_options,
#employee.marital_status.try(:upcase)), prompt: "Select Marital Status", id: 'employee_marital_status', disabled: #request.submitted?)
In employee_helper.rb
def marital_status_options
Employee::MaritalStatus::ALL.zip(Employee::MaritalStatus::ALL)
end
In employee model
module MaritalStatus
MARRIED = 'MARRIED'
SINGLE = 'SINGLE'
DIVORCED = 'DIVORCED'
ALL = [MARRIED, SINGLE, DIVORCED]
end
You're very close. The problem here likely stems from your marital_status_options method: this will simply return DIVORCED as it evaluates to the last line due to your assignment.
Therefore, you might find the value is selected if your instance contains 'DIVORCED', though not either of the other values; your instance's value needs to match one of these for it to be selected instead of the prompt.
You likely want to change this:
def marital_status_options
MARRIED = 'MARRIED' # this will be evaluated first
SINGLE = 'SINGLE' # then this
DIVORCED = 'DIVORCED' # finally, this will be evaluated and returned as 'DIVORCED'
end
To an array, either:
def marital_status_options
['MARRIED', 'SINGLE', 'DIVORCED']
end
Or, to present the options as lowercase but keep uppercase values in the db:
def marital_status_options
[['Married', 'MARRIED'], ['Single', 'SINGLE'], ['Divorced', 'DIVORCED']]
end
Take a look at the docs on options_for_select and you'll see who they can be setup.
Further down the line, you might want to consider switching to enums - these are very handy for managing selections such as these, and auto generate methods such as Employee.married, employee.divorced?, and so forth.
As someone else has mentioned, it's best practice to store data such as this in the relevant model, though I'd argue these should be stored as a constant as they won't be changing. So one of the following:
# employee.rb
MARITAL_STATUSES = ['MARRIED', 'SINGLE', 'DIVORCED'].freeze
# or
MARITAL_STATUSES = [['Married', 'MARRIED'], ['Single', 'SINGLE'], ['Divorced', 'DIVORCED']].freeze
= select_tag('request[marital_status]',
options_for_select(Employee::MARITAL_STATUSES,
#employee.marital_status.try(:upcase)),
prompt: "Select Marital Status",
id: 'employee_marital_status',
disabled: #request.submitted?)
Hope that helps - let me know if you've any questions or need anything else.
The format and usage is correct. Kindly verify if #employee.marital_status.try(:upcase) exactly matches one of the marital_status_options provided here.
That looks like a probable case for such a behaviour.
Also, the first argument expected in select_tag needs to in an appropriate format, in this case, an array of strings.
Hence, your method marital_status_options should return an array of options to be used for dropdown.
def marital_status_options
['MARRIED', 'SINGLE', 'DIVORCED']
end
= select_tag "request[marital_status]", options_for_select(Employee.marital_status_options,
#employee.marital_status.try(:upcase)), :include_blank => '--Select Marital Status--', id: id: 'employee_marital_status', disabled: #request.submitted?
It's a good practice to define marital_status_options(Business logic) inside model: -
Assuming that it's Employee model
def self.marital_status_options
[
["MARRIED","MARRIED"],
["SINGLE","SINGLE"],
["DIVORCED", "DIVORCED"]
]
end
Reason that it's not selecting your default marital_status is because if #employee.marital_status.try(:upcase) will not match any of the marital_status_options, it will show your prompt option, so check it carefully that if #employee.marital_status.try(:upcase) matches any of the given options of select tag's option.

Is there a method like "ILIKE" I can use in a Rails conditional statement?

I want to perform an action if a string is contained, non-case-sensitively, in another string.
So my if statement would look something like this:
#a = "search"
if #a ILIKE "fullSearch"
#do stuff
end
You can use the include? method. So in this case:
#var = 'Search'
if var.include? 'ear'
#action
end
Remember include? method is case-sensitive. So if you use something like include? 'sea' it would return false. You may want to do a downcase before calling include?()
#var = 'Search'
if var.downcase.include? 'sea'
#action
end
Hope that helped.
There are many ways to get there. Here are three:
'Foo'.downcase.include?('f') # => true
'Foo'.downcase['f'] # => "f"
Those are documented in the String documentation which you need to become very familiar with if you're going to program in Ruby.
'Foo'[/f/i] # => "F"
This is a mix of String's [] slice shortcut and regular expressions. I'd recommend one of the first two because they're faster, but for thoroughness I added it because people like hitting things with the regex hammer. Regexp contains documentation for /f/i.
You'll notice that they return different things. Ruby considers anything other than false or nil as true, AKA "truthiness", so all three are returning a true value, and, as a result you could use them in conditional tests.
You can use a regexp with i option. i for insensitive I think.
a = "fullSearch"
a =~ /search/i
=> 4
a =~ /search/
=> nil
Or you could downcase your string and check if it's present in the other
a = "fullSearch"
a.downcase.include?('search')
=> true

number_to_human with currency

I am using number_to_human to print 4 Million. I was wondering if there is a method that will add the $ to the front of the number?
I run into issues when I have a negative number.
If I just throw a $ in front I will get $-4 Million they want -$4 Million
It's builtin Rails:
number_to_currency(number_to_human(-4000000))
# => "-$4 Million"
The big advantage in using convention is that, when you need i18n, you just have to pass the locale:
number_to_currency(number_to_human(-4000000), locale: 'en-EU')
# => "-4 Million €"
Its interesting thing and If It would help to anybody:
How to Present thousands as "K", millions as "M" in ruby for that,
just follow the steps
Include NumberHelper in controller at top
include ActionView::Helpers::NumberHelper
Use this number_to_human Helper and pass the value, This will convert the value
<%= number_to_human(5223654) %>
Paste this code in en.yml to get it working
en:
number:
human:
decimal_units:
format: "%n%u"
units:
unit: ""
thousand: K
million: M
billion: B
trillion: T
quadrillion: Q
There must be better ways. however, here is a quick hack
amount = -4000000
if amount < 0
human = "-$#{number_to_human(amount.abs)}"
else
human = "$#{number_to_human(amount)}"
end
#apneadiving shows the real simple ways.

Rails I18n: calling different logic based on locale

I have a custom helper method which outputs the percentage saved. For example it will calculate the discount for an item and output "20 percent off".
I am localizing the site to Chinese, and in Chinese the same discount is expressed differently. "20% off" is expressed as "8 Cut" or "80% of original price". Since these two expression are quite different, I think I need to write two versions of the helper method.
Currently I wrote it like this, checking locale in the helper itself:
def percent_off(discount, locale=I18n.locale)
if not locale.to_s.include?('zh')
n = ((1 - (discount.preferential_price / discount.original_price)) * 100) .to_i
"#{n}% off"
else
# Chinese
n = ((discount.preferential_price / discount.original_price) * 10) .to_i
"#{n} cut"
end
end
Is there a better way to do this?
You might want to refactor your code so that the calculation of the number used to represent the discount is separated from the selection/creation of your localized message.
Here is an idea along those lines:
In your en.yml:
# value for "n% off"
discount_msg: "%{n}% off!"
In your zh.yml:
# value for "(100-n)% of original price"
discount_msg: "%{n}% of original price (in Chinese)"
Next refactor the percent_off helper method so that it only calculates the correct value discount value depending upon the implied locale:
def percent_off(discount)
n = ((1 - (discount.preferential_price / discount.original_price)) * 100) .to_i
if I18n.locale.to_s.include?('zh')
n = 100 - n
end
n
end
Then, you could invoke the above like this:
I18n.t('discount_msg', :n=>percent_off(discount))
The only bad thing gere in your helper I can see is that you write percent_off discount, but it's not evident what it'll return ( so I would create 2 different helper methods here.
Is I noted using locale check in views doesn't look pretty when you view blocks became completely different for different translations, so I've created an i18n helper method for that purpose:
module ApplicationHelper
def translate_to(locales, &blk)
locales = [locales.to_s] if !locales.is_a?(Array)
yield if locales.include?(I18n.locale.to_s)
end
alias :tto :translate_to
end
and in views:
<% tto :en do %>
<% # some en-translated routine %>
<% end %>
<% tto :zh do %>
<% # zh-translated routine %>
<% end %>
Not sure it's a best way to manage translated blocks but I found it useful ))

Resources