¿Cuál es el mejor método para manejar moneda / dinero?

323

Estoy trabajando en un sistema de carrito de compras muy básico.

Tengo una tabla itemsque tiene una columna pricede tipo integer.

Tengo problemas para mostrar el valor del precio en mis vistas para precios que incluyen euros y centavos. ¿Me estoy perdiendo algo obvio en lo que respecta al manejo de divisas en el marco de Rails?

Barry Gallagher
fuente
si alguien usa sql, entonces DECIMAL(19, 4) es una opción popular, marque esto y marque aquí Formatos de moneda mundial para decidir cuántos decimales usar, la esperanza ayuda.
shaijut

Respuestas:

495

Probablemente quieras usar un DECIMALtipo en tu base de datos. En su migración, haga algo como esto:

# 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

En Rails, el :decimaltipo se devuelve como BigDecimal, lo cual es excelente para el cálculo del precio.

Si insiste en usar números enteros, tendrá que convertir manualmente hacia y desde BigDecimals en todas partes, lo que probablemente se convierta en una molestia.

Como señaló mcl, para imprimir el precio, use:

number_to_currency(price, :unit => "€")
#=> €1,234.01
molf
fuente
13
Utilice el ayudante number_to_currency, más información en api.rubyonrails.org/classes/ActionView/Helpers/…
mlibby
48
En realidad, es mucho más seguro y más fácil usar un número entero en combinación con act_as_dollars. ¿Alguna vez te ha mordido la comparación de punto flotante? Si no, no hagas de esta tu primera experiencia. :) Con los actos como dólares, pones cosas en formato 12.34, se almacenan como 1234 y salen como 12.34.
Sarah Mei
50
@Sarah Mei: BigDecimals + el formato de columna decimal evita precisamente eso.
molf
114
Es importante no simplemente copiar esta respuesta a ciegas: la precisión 8, la escala 2 le da un valor máximo de 999,999.99 . Si necesita un número superior a un millón, ¡aumente la precisión!
Jon Cairns
22
También es importante no solo usar ciegamente una escala de 2 si maneja monedas diferentes: algunas monedas de África del Norte y Árabes como el Rial omaní o el Dinar tunecino tienen una escala de 3, por lo que la precisión 8 escala 3 es más apropiada allí .
Beat Richartz
117

Aquí hay un enfoque simple y fino que aprovecha composed_of(parte de ActiveRecord, usando el patrón ValueObject) y la gema Money

Necesitarás

  • The Money gem (versión 4.1.0)
  • Un modelo, por ejemplo Product
  • Una integercolumna en su modelo (y base de datos), por ejemplo:price

Escribe esto en tu product.rbarchivo:

class Product > ActiveRecord::Base

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

Lo que obtendrás:

  • Sin ningún cambio adicional, todos sus formularios mostrarán dólares y centavos, pero la representación interna sigue siendo solo centavos. Los formularios aceptarán valores como "$ 12,034.95" y lo convertirán por usted. No es necesario agregar controladores o atributos adicionales a su modelo, ni ayudantes desde su punto de vista.
  • product.price = "$12.00" se convierte automáticamente a la clase Money
  • product.price.to_s muestra un número con formato decimal ("1234.00")
  • product.price.format muestra una cadena con el formato correcto para la moneda
  • Si necesita enviar centavos (a una pasarela de pago que quiere centavos), product.price.cents.to_s
  • Conversión de divisas gratis
Ken Mayer
fuente
14
Me encanta este enfoque. Pero tenga en cuenta: asegúrese de que su migración para 'precio' en este ejemplo no permita valores nulos y predeterminados a 0 para que no se vuelva loco tratando de descubrir por qué esto no funciona.
Cory
3
Encontré que la gema money_column (extraída de Shopify) es muy sencilla de usar ... más fácil que la gema money, si no necesita conversión de moneda.
Talyric
77
Debe tenerse en cuenta para todos aquellos que usan la gema de Money que el equipo central de Rails está discutiendo la depreciación y eliminación de "composite_of" del marco. Sospecho que la gema se actualizará para manejar esto si sucede, pero si está mirando Rails 4.0, debe tener en cuenta esta posibilidad
Peer Allan el
1
En cuanto al comentario de @ PeerAllan sobre la eliminación de composed_of aquí, hay más detalles al respecto, así como una implementación alternativa.
HerbCSO
3
Además, esto es realmente razonable usar la gema del dinero de los rieles .
Fotanus 05 de
25

La práctica común para manejar la moneda es usar el tipo decimal. Aquí hay un ejemplo simple de "Desarrollo web ágil con rieles"

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

Esto le permitirá manejar precios de -999,999.99 a 999,999.99
También puede incluir una validación en sus artículos como

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

para comprobar la cordura de sus valores.

alex.zherdev
fuente
1
Esta solución también le permite usar SQL sum y amigos.
Larry K
44
¿Podría hacer: valida: price,: presencia => true,: numericality => {: greater_than => 0}
Galaxy
9

Si está utilizando Postgres (y desde que estamos en 2017 ahora) es posible que desee :moneyprobar su tipo de columna.

add_column :products, :price, :money, default: 0
The Whiz of Oz
fuente
7

Usa gemas de rieles de dinero . Maneja muy bien el dinero y las monedas en su modelo y también tiene un montón de ayudantes para formatear sus precios.

Troggy
fuente
Sí, estoy de acuerdo con esto. En general, manejo el dinero almacenándolo como centavos (entero) y usando una gema como actúa como dinero o dinero (carriles de dinero) para manejar los datos en la memoria. Manejarlo en enteros evita esos desagradables errores de redondeo. Por ejemplo, 0.2 * 3 => 0.6000000000000001 Esto, por supuesto, solo funciona si no necesita manejar fracciones de un centavo.
Chad M
Esto es muy bueno si está utilizando rieles. Déjalo y no te preocupes por los problemas con una columna decimal. Si usa esto con una vista, esta respuesta también puede ser útil: stackoverflow.com/questions/18898947/…
amarrado el
6

Solo una pequeña actualización y una cohesión de todas las respuestas para algunos aspirantes a junior / principiantes en el desarrollo de RoR que seguramente vendrán aquí para algunas explicaciones.

Trabajando con dinero

Utilice :decimalpara almacenar dinero en el PP, como @molf sugirió (y lo que mis usos de la empresa como un estándar de oro cuando se trabaja con dinero).

# 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

Pocos puntos:

  • :decimalse usará para BigDecimalresolver muchos problemas.

  • precisiony scaledebe ajustarse, según lo que esté representando

    • Si trabaja con recibir y enviar pagos, precision: 8y scale: 2le da 999,999.99la cantidad más alta, que está bien en el 90% de los casos.

    • Si necesita representar el valor de una propiedad o un automóvil raro, debe usar uno más alto precision.

    • Si trabaja con coordenadas (longitud y latitud), seguramente necesitará una más alta scale.

Cómo generar una migración

Para generar la migración con el contenido anterior, ejecute en la terminal:

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

o

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

como se explica en esta publicación de blog .

Formato de moneda

BESE las bibliotecas adicionales adiós y use ayudantes incorporados. Use number_to_currencycomo @molf y @facundofarias sugirieron.

Para jugar con el number_to_currencyayudante en la consola de Rails, envíe una llamada a la clase ActiveSupport's NumberHelperpara acceder al ayudante.

Por ejemplo:

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

da el siguiente resultado

2500000,61

Verifique el otro número de ayudante optionsde número a moneda

Donde ponerlo

Puede ponerlo en una aplicación auxiliar y usarlo dentro de las vistas por cualquier cantidad.

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

O puede ponerlo en el Itemmodelo como un método de instancia y llamarlo donde necesite formatear el precio (en vistas o ayudantes).

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

Y, un ejemplo de cómo uso el number_to_currencycontrolador interno (observe la negative_formatopción, utilizada 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
fuente
5

Usando Atributos Virtuales (Enlace a Railscast revisado (pagado)) puede almacenar su price_in_cents en una columna de enteros y agregar un atributo virtual price_in_dollars en su modelo de producto como getter y 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

Fuente: RailsCasts # 016: Atributos virtuales : los atributos virtuales son una forma limpia de agregar campos de formulario que no se asignan directamente a la base de datos. Aquí muestro cómo manejar validaciones, asociaciones y más.

Thomas Klemm
fuente
1
esto deja 200.0 un dígito
ajbraus
2

Definitivamente enteros .

Y a pesar de que BigDecimal técnicamente existe 1.5, todavía te dará un Float puro en Ruby.

discutible
fuente
2

Si alguien usa Sequel, la migración se vería así:

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

de alguna manera Sequel ignora: precisión y escala

(Versión de la secuela: secuela (3.39.0, 3.38.0))

jethroo
fuente
2

Mis API subyacentes estaban usando centavos para representar dinero, y no quería cambiar eso. Tampoco estaba trabajando con grandes cantidades de dinero. Así que acabo de poner esto en un método auxiliar:

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

Eso convierte el número entero en una cadena con al menos tres dígitos (agregando ceros a la izquierda si es necesario), luego inserta un punto decimal antes de los dos últimos dígitos, sin usar nunca un Float . Desde allí, puede agregar los símbolos de moneda que sean apropiados para su caso de uso.

Es sin duda rápido y sucio, pero a veces eso está bien!

Brent Royal-Gordon
fuente
No puedo creer que nadie te haya votado. Esto fue lo único que funcionó para que mi objeto Money se adaptara a una forma tal que una API pueda tomarlo. (Decimal)
Code-MonKy
2

Lo estoy usando de esta manera:

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

Por supuesto, el símbolo de la moneda, la precisión, el formato, etc. dependen de cada moneda.

facundofarias
fuente
1

Puede pasar algunas opciones a number_to_currency(un asistente de vista estándar de Rails 4):

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

Publicado por Dylan Markow

blnc
fuente
0

Código simple para Ruby & Rails

<%= number_to_currency(1234567890.50) %>

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