Qual é o melhor método de lidar com moeda / dinheiro?

323

Estou trabalhando em um sistema de carrinho de compras muito básico.

Eu tenho uma tabela itemsque tem uma coluna pricedo tipo integer.

Estou com problemas para exibir o valor do preço em minhas visualizações para preços que incluem euros e centavos. Estou perdendo algo óbvio no que diz respeito à manipulação de moeda na estrutura Rails?

Barry Gallagher
fonte
se alguém usa SQL, então DECIMAL(19, 4) é uma escolha popular verificar isso também verificar aqui Formatos Mundo moeda para decidir quantos lugares para uso decimal, esperança ajuda.
shaijut

Respostas:

495

Você provavelmente desejará usar um DECIMALtipo no seu banco de dados. Na sua migração, faça algo assim:

# precision is the total number of digits
# scale is the number of digits to the right of the decimal point
add_column :items, :price, :decimal, :precision => 8, :scale => 2

No Rails, o :decimaltipo é retornado como BigDecimal, o que é ótimo para o cálculo de preços.

Se você insistir em usar números inteiros, precisará converter manualmente de e para BigDecimals em todos os lugares, o que provavelmente se tornará um problema.

Conforme indicado pelo mcl, para imprimir o preço, use:

number_to_currency(price, :unit => "€")
#=> €1,234.01
molf
fonte
13
Use ajudante number_to_currency, mais informações em api.rubyonrails.org/classes/ActionView/Helpers/...
mlibby
48
Na verdade, é muito mais seguro e fácil usar um número inteiro em combinação com o act_as_dollars. Você já foi mordido por comparação de ponto flutuante? Caso contrário, não faça desta a sua primeira experiência. :) Com o actions_as_dollars, você coloca coisas no formato 12.34, é armazenado como 1234 e sai como 12.34.
21339 Sarah Mei
50
@ Sarah Mei: BigDecimals + formato de coluna decimal evita precisamente isso.
molf
114
É importante não copiar esta resposta cegamente - a precisão 8, escala 2 fornece um valor máximo de 999.999,99 . Se você precisar de um número maior que um milhão, aumente a precisão!
Jon Cairns
22
Também é importante não usar cegamente uma escala de 2 se você estiver lidando com moedas diferentes - algumas moedas norte-africanas e árabes como o Rial Omani ou o dinar tunisino têm uma escala de 3, portanto a precisão 8 na escala 3 é mais apropriada .
Beat Richartz
117

Aqui está uma abordagem simples e fina que utiliza composed_of(parte do ActiveRecord, usando o padrão ValueObject) e a jóia Money

Você precisará

  • A jóia do Money (versão 4.1.0)
  • Um modelo, por exemplo Product
  • Uma integercoluna no seu modelo (e banco de dados), por exemplo:price

Escreva isso no seu product.rbarquivo:

class Product > ActiveRecord::Base

  composed_of :price,
              :class_name => 'Money',
              :mapping => %w(price cents),
              :converter => Proc.new { |value| Money.new(value) }
  # ...

O que você obterá:

  • Sem nenhuma alteração extra, todos os seus formulários mostrarão dólares e centavos, mas a representação interna ainda é apenas centavos. Os formulários aceitarão valores como "US $ 12.034,95" e os converterão para você. Não há necessidade de adicionar manipuladores ou atributos extras ao seu modelo ou auxiliares na sua visualização.
  • product.price = "$12.00" converte automaticamente para a classe Money
  • product.price.to_s exibe um número no formato decimal ("1234.00")
  • product.price.format exibe uma string formatada corretamente para a moeda
  • Se você precisar enviar centavos (para um gateway de pagamento que queira moedas de um centavo), product.price.cents.to_s
  • Conversão de moeda gratuitamente
Ken Mayer
fonte
14
Eu amo essa abordagem. Mas observe: certifique-se de que sua migração para 'preço' neste exemplo não permita valores nulos e zero, para não ficar louco tentando descobrir por que isso não funciona.
Cory
3
Achei a jóia money_column (extraída do Shopify) muito simples de usar ... mais fácil do que a jóia do dinheiro, se você não precisar de conversão de moeda.
talyric
7
Deve-se observar para todos os que usam a gema Money que a equipe principal do Rails está discutindo a suspensão e remoção de "compos_of" da estrutura. Eu suspeito que a jóia será atualizado para lidar com isso, se acontecer, mas se você está olhando para Rails 4.0 você deve estar ciente desta possibilidade
Par Allan
1
Em relação ao comentário do @ PeerAllan sobre a remoção composed_of daqui, há mais detalhes sobre isso, além de uma implementação alternativa.
HerbCSO 13/10
3
Além disso, isso é realmente fácil usando a jóia do dinheiro dos trilhos .
Fotanus
25

A prática comum para lidar com moeda é usar o tipo decimal. Aqui está um exemplo simples de "Agile Web Development with Rails"

add_column :products, :price, :decimal, :precision => 8, :scale => 2 

Isso permitirá que você lide com preços de -999.999,99 a 999.999,99.
Você também pode incluir uma validação em itens como

def validate 
  errors.add(:price, "should be at least 0.01") if price.nil? || price < 0.01 
end 

verificar sanidade seus valores.

alex.zherdev
fonte
1
Esta solução também permite que você use soma SQL e amigos.
Larry K
4
Você poderia fazer: valida: preço,: presença => verdadeiro,: numericalidade => {: larger_than => 0}
Galaxy
9

Se você estiver usando o Postgres (e já que estamos em 2017 agora), convém :moneytentar o tipo de coluna deles .

add_column :products, :price, :money, default: 0
O Whiz de Oz
fonte
7

Use jóia de trilhos de dinheiro . Ele lida muito bem com dinheiro e moedas no seu modelo e também tem um monte de ajudantes para formatar seus preços.

Troggy
fonte
Sim, eu concordo com isso. Geralmente, eu manuseio dinheiro armazenando-o como centavos (inteiro) e usando uma gema como atos-como-dinheiro ou dinheiro (trilhos-dinheiro) para manipular os dados na memória. Manipulá-lo com números inteiros evita esses erros desagradáveis ​​de arredondamento. Por exemplo, 0,2 * 3 => 0,6000000000000001 Isso, obviamente, só funciona se você não precisar manipular frações de um centavo.
Chad M
Isso é muito bom se você estiver usando trilhos. Solte-o e não se preocupe com os problemas com uma coluna decimal. Se você usar isso com um ponto de vista, esta resposta pode ser útil também: stackoverflow.com/questions/18898947/...
mooreds
6

Apenas uma pequena atualização e uma coesão de todas as respostas para alguns aspirantes a juniores / iniciantes no desenvolvimento do RoR que certamente virão aqui para algumas explicações.

Trabalhando com dinheiro

Use :decimalpara armazenar dinheiro no banco de dados, como o @molf sugeriu (e o que minha empresa usa como padrão-ouro ao trabalhar com dinheiro).

# precision is the total number of digits
# scale is the number of digits to the right of the decimal point
add_column :items, :price, :decimal, precision: 8, scale: 2

Alguns pontos:

  • :decimalserá usado como o BigDecimalque resolve muitos problemas.

  • precisione scaledeve ser ajustado, dependendo do que você está representando

    • Se você trabalha com recebimento e envio de pagamentos precision: 8e scale: 2oferece 999,999.99o valor mais alto, o que é bom em 90% dos casos.

    • Se você precisar representar o valor de uma propriedade ou carro raro, use um valor mais alto precision.

    • Se você trabalha com coordenadas (longitude e latitude), certamente precisará de uma maior scale.

Como gerar uma migração

Para gerar a migração com o conteúdo acima, execute no terminal:

bin/rails g migration AddPriceToItems price:decimal{8-2}

ou

bin/rails g migration AddPriceToItems 'price:decimal{5,2}'

conforme explicado nesta postagem do blog .

Formatação de moeda

um beijo de despedida nas bibliotecas extras e use ajudantes internos. Use number_to_currencycomo @molf e @facundofarias sugeridos.

Para jogar com number_to_currencyajudante no console do Rails, enviar uma chamada para o ActiveSupport's NumberHelperclasse, a fim de acessar o ajudante.

Por exemplo:

ActiveSupport::NumberHelper.number_to_currency(2_500_000.61, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")

dá a seguinte saída

2500000,61

Verifique o outro auxiliar optionsde number_to_currency .

Onde colocá-lo

Você pode colocá-lo em um auxiliar de aplicativo e usá-lo dentro de visualizações para qualquer quantia.

module ApplicationHelper    
  def format_currency(amount)
    number_to_currency(amount, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
  end
end

Ou você pode colocá-lo no Itemmodelo como um método de instância e chamá-lo onde precisar formatar o preço (em visualizações ou auxiliares).

class Item < ActiveRecord::Base
  def format_price
    number_to_currency(price, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
  end
end

E, um exemplo de como eu uso o number_to_currencycontrolador interno (observe a negative_formatopção usada para representar reembolsos)

def refund_information
  amount_formatted = 
    ActionController::Base.helpers.number_to_currency(@refund.amount, negative_format: '(%u%n)')
  {
    # ...
    amount_formatted: amount_formatted,
    # ...
  }
end
Zlatko Alomerovic
fonte
5

Usando Atributos Virtuais (Link para o Railscast revisado (pago)), você pode armazenar seus price_in_cents em uma coluna inteira e adicionar um atributo virtual price_in_dollars em seu modelo de produto como getter e setter.

# Add a price_in_cents integer column
$ rails g migration add_price_in_cents_to_products price_in_cents:integer

# Use virtual attributes in your Product model
# app/models/product.rb

def price_in_dollars
  price_in_cents.to_d/100 if price_in_cents
end

def price_in_dollars=(dollars)
  self.price_in_cents = dollars.to_d*100 if dollars.present?
end

Fonte: RailsCasts # 016: Atributos virtuais : os atributos virtuais são uma maneira limpa de adicionar campos de formulário que não são mapeados diretamente no banco de dados. Aqui eu mostro como lidar com validações, associações e muito mais.

Thomas Klemm
fonte
1
Isso deixa 200,0 um dígito
ajbraus
2

Definitivamente inteiros .

E mesmo que o BigDecimal exista tecnicamente, 1.5você ainda terá um Float puro em Ruby.

discutível
fonte
2

Se alguém estiver usando o Sequel, a migração será semelhante a:

add_column :products, :price, "decimal(8,2)"

de alguma maneira Sequel ignora: precisão e: escala

(Versão da sequela: sequela (3.39.0, 3.38.0))

jethroo
fonte
2

Minhas APIs subjacentes estavam usando centavos para representar dinheiro, e eu não queria mudar isso. Também não estava trabalhando com grandes quantias de dinheiro. Então, eu apenas coloquei isso em um método auxiliar:

sprintf("%03d", amount).insert(-3, ".")

Isso converte o número inteiro em uma string com pelo menos três dígitos (adicionando zeros à esquerda, se necessário) e, em seguida, insere um ponto decimal antes dos dois últimos dígitos, nunca usando a Float. A partir daí, você pode adicionar quaisquer símbolos monetários adequados ao seu caso de uso.

É definitivamente rápido e sujo, mas às vezes tudo bem!

Brent Royal-Gordon
fonte
Não posso acreditar que ninguém te votou. Essa foi a única coisa que funcionou para colocar meu objeto Money de forma agradável, de forma que uma API pudesse aceitá-lo. (Decimal)
Code-MonKy
2

Estou usando desta maneira:

number_to_currency(amount, unit: '€', precision: 2, format: "%u %n")

É claro que o símbolo da moeda, a precisão, o formato etc. dependem de cada moeda.

facundofarias
fonte
1

Você pode passar algumas opções para number_to_currency(um auxiliar de exibição padrão do Rails 4):

number_to_currency(12.0, :precision => 2)
# => "$12.00"

Como postado por Dylan Markow

blnc
fonte
0

Código simples para Ruby & Rails

<%= number_to_currency(1234567890.50) %>

OUT PUT => $1,234,567,890.50
Dinesh Vaitage
fonte