Quelle est la meilleure méthode pour gérer la monnaie / l'argent?

323

Je travaille sur un système de panier d'achat très basique.

J'ai une table itemsqui a une colonne pricede type integer.

J'ai du mal à afficher la valeur du prix dans mes vues pour les prix qui incluent à la fois les euros et les cents. Suis-je en train de manquer quelque chose d'évident en ce qui concerne la gestion des devises dans le cadre Rails?

Barry Gallagher
la source
si quelqu'un utilise sql, alors DECIMAL(19, 4) c'est un choix populaire, cochez cette case, vérifiez également ici Formats de devise mondiale pour décider du nombre de décimales à utiliser, espérons que cela aide.
shaijut

Réponses:

495

Vous voudrez probablement utiliser un DECIMALtype dans votre base de données. Dans votre migration, faites quelque chose comme ceci:

# 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

Dans Rails, le :decimaltype est renvoyé en tant que BigDecimal, ce qui est idéal pour le calcul du prix.

Si vous insistez pour utiliser des entiers, vous devrez convertir manuellement vers et depuis BigDecimals partout, ce qui deviendra probablement juste une douleur.

Comme indiqué par mcl, pour imprimer le prix, utilisez:

number_to_currency(price, :unit => "€")
#=> €1,234.01
molf
la source
13
Utilisez l'assistant number_to_currency, plus d'informations sur api.rubyonrails.org/classes/ActionView/Helpers/…
mlibby
48
En fait, il est beaucoup plus sûr et plus facile d'utiliser un entier en combinaison avec actes_as_dollars. Avez-vous déjà été mordu par une comparaison à virgule flottante? Sinon, n'en faites pas votre première expérience. :) Avec actes_as_dollars, vous mettez des choses au format 12.34, elles sont stockées sous 1234 et elles sortent sous 12.34.
Sarah Mei
50
@Sarah Mei: BigDecimals + le format de colonne décimal évite précisément cela.
molf
114
Il est important de ne pas copier cette réponse à l'aveugle - la précision 8, l'échelle 2 vous donne une valeur maximale de 999 999,99 . Si vous avez besoin d'un nombre supérieur à un million, augmentez la précision!
Jon Cairns
22
Il est également important de ne pas utiliser aveuglément une échelle de 2 si vous manipulez des devises différentes - certaines devises nord-africaines et arabes comme le rial omanais ou le dinar tunisien ont une échelle de 3, donc la précision 8, échelle 3, est plus appropriée là-bas. .
Beat Richartz
117

Voici une approche fine et simple qui exploite composed_of(une partie d'ActiveRecord, en utilisant le modèle ValueObject) et la gemme Money

Tu auras besoin

  • Le joyau de l'argent (version 4.1.0)
  • Un modèle, par exemple Product
  • Une integercolonne dans votre modèle (et base de données), par exemple:price

Écrivez ceci dans votre product.rbdossier:

class Product > ActiveRecord::Base

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

Ce que vous obtiendrez:

  • Sans aucun changement supplémentaire, tous vos formulaires afficheront des dollars et des cents, mais la représentation interne n'est toujours que des cents. Les formulaires accepteront des valeurs telles que "12 034,95 $" et les convertiront pour vous. Il n'est pas nécessaire d'ajouter des gestionnaires ou des attributs supplémentaires à votre modèle ou des aides dans votre vue.
  • product.price = "$12.00" convertit automatiquement en classe Money
  • product.price.to_s affiche un nombre au format décimal ("1234.00")
  • product.price.format affiche une chaîne correctement formatée pour la devise
  • Si vous devez envoyer des cents (à une passerelle de paiement qui veut des sous), product.price.cents.to_s
  • Conversion de devises gratuite
Ken Mayer
la source
14
J'adore cette approche. Mais veuillez noter: assurez-vous que votre migration pour le «prix» dans cet exemple n'autorise pas les valeurs nulles et par défaut à 0 de peur de devenir fou en essayant de comprendre pourquoi cela ne fonctionne pas.
Cory
3
J'ai trouvé que le joyau money_column (extrait de Shopify) était très simple à utiliser ... plus facile que le joyau de l'argent, si vous n'avez pas besoin de conversion de devises.
talyric
7
Il convient de noter pour tous ceux qui utilisent la gemme Money que l'équipe principale de Rails discute de la dépréciation et de la suppression de "compose_of" du cadre. Je soupçonne que la gemme sera mise à jour pour gérer cela si cela se produit, mais si vous regardez Rails 4.0, vous devez être conscient de cette possibilité
Peer Allan
1
En ce qui concerne le commentaire de @ PeerAllan sur la suppression d' composed_of ici, vous trouverez plus de détails à ce sujet ainsi qu'une implémentation alternative.
HerbCSO
3
De plus, c'est vraiment très pratique en utilisant le joyau rails-argent .
fotanus
25

La pratique courante pour la gestion des devises consiste à utiliser le type décimal. Voici un exemple simple de "Développement Web Agile avec Rails"

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

Cela vous permettra de gérer des prix de -999 999,99 à 999 999,99.
Vous pouvez également inclure une validation dans vos articles comme

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

pour vérifier vos valeurs.

alex.zherdev
la source
1
Cette solution vous permet également d'utiliser la somme SQL et vos amis.
Larry K
4
Pourriez-vous éventuellement faire: valide: prix,: présence => vrai,: numéricité => {: Greater_than => 0}
Galaxy
9

Si vous utilisez Postgres (et puisque nous sommes en 2017 maintenant), vous voudrez peut-être :moneyessayer leur type de colonne.

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

Utilisez la gemme de l'argent-rails . Il gère bien l'argent et les devises dans votre modèle et dispose également d'un tas d'aides pour formater vos prix.

Troggy
la source
Ouais, je suis d'accord avec ça. Généralement, je gère l'argent en le stockant en cents (entier) et en utilisant un joyau comme actes-comme-argent ou argent (porte-monnaie) pour gérer les données en mémoire. La manipulation en nombres entiers empêche ces erreurs d'arrondi désagréables. Par exemple 0,2 * 3 => 0,6000000000000001 Ceci, bien sûr, ne fonctionne que si vous n'avez pas besoin de gérer des fractions de cent.
Chad M
C'est très bien si vous utilisez des rails. Déposez-le et ne vous inquiétez pas des problèmes avec une colonne décimale. Si vous l'utilisez avec une vue, cette réponse peut également être utile: stackoverflow.com/questions/18898947/…
mooreds
6

Juste une petite mise à jour et une cohésion de toutes les réponses pour certains aspirants juniors / débutants en développement RoR qui viendront sûrement ici pour quelques explications.

Travailler avec de l'argent

Utilisez-le :decimalpour stocker de l'argent dans la base de données, comme l'a suggéré @molf (et ce que mon entreprise utilise comme référence pour travailler avec de l'argent).

# 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

Quelques points:

  • :decimalva être utilisé comme BigDecimalce qui résout beaucoup de problèmes.

  • precisionet scaledoit être ajusté en fonction de ce que vous représentez

    • Si vous travaillez avec la réception et l'envoi de paiements, precision: 8et scale: 2vous donne 999,999.99le montant le plus élevé, ce qui est bien dans 90% des cas.

    • Si vous devez représenter la valeur d'une propriété ou d'une voiture rare, vous devez utiliser une valeur supérieure precision.

    • Si vous travaillez avec des coordonnées (longitude et latitude), vous aurez sûrement besoin d'un plus haut scale.

Comment générer une migration

Pour générer la migration avec le contenu ci-dessus, exécutez dans le terminal:

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

ou

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

comme expliqué dans ce billet de blog .

Formatage des devises

KISS les bibliothèques supplémentaires au revoir et utilisez des aides intégrées. Utilisez number_to_currencycomme @molf et @facundofarias suggéré.

Pour jouer avec number_to_currencyaide dans la console Rails, envoyer un appel à la ActiveSupportde NumberHelperclasse afin d'accéder à l'aide.

Par exemple:

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

donne la sortie suivante

2500000,61

Vérifiez l'autre optionsde l' assistant number_to_currency .

Où le mettre

Vous pouvez le mettre dans une application d'assistance et l'utiliser à l'intérieur des vues pour n'importe quel montant.

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

Ou vous pouvez le mettre dans le Itemmodèle en tant que méthode d'instance et l'appeler là où vous devez formater le prix (dans les vues ou les aides).

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

Et, un exemple comment j'utilise l' number_to_currencyintérieur d'un contrôleur (remarquez l' negative_formatoption, utilisée pour représenter les remboursements)

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

À l'aide d' attributs virtuels (lien vers Railscast révisé (payant)), vous pouvez stocker vos price_in_cents dans une colonne entière et ajouter un attribut virtuel price_in_dollars dans votre modèle de produit en tant que getter et 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

Source: RailsCasts # 016: Attributs virtuels : attributs virtuels sont un moyen propre pour ajouter des champs de formulaire qui ne correspondent pas directement à la base de données. Ici, je montre comment gérer les validations, les associations, etc.

Thomas Klemm
la source
1
cela laisse 200,0 un chiffre
ajbraus
2

Certainement des entiers .

Et même si BigDecimal existe techniquement, vous 1.5aurez toujours un flottant pur en Ruby.

discutable
la source
2

Si quelqu'un utilise Sequel, la migration ressemblerait à ceci:

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

en quelque sorte Sequel ignore: précision et: échelle

(Version de la suite: suite (3.39.0, 3.38.0))

jethroo
la source
2

Mes API sous-jacentes utilisaient toutes des cents pour représenter l'argent, et je ne voulais pas changer cela. Je ne travaillais pas non plus avec de grosses sommes d'argent. Je viens donc de mettre cela dans une méthode d'aide:

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

Cela convertit l'entier en une chaîne avec au moins trois chiffres (en ajoutant des zéros au début si nécessaire), puis insère une virgule décimale avant les deux derniers chiffres, sans jamais utiliser a Float. De là, vous pouvez ajouter les symboles monétaires appropriés à votre cas d'utilisation.

C'est définitivement rapide et sale, mais parfois c'est très bien!

Brent Royal-Gordon
la source
Je ne peux pas croire que personne ne vous ait voté. C'était la seule chose qui a fonctionné pour obtenir mon objet Money sous une forme telle qu'une API puisse le prendre. (Décimal)
Code-MonKy
2

Je l'utilise de cette façon:

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

Bien sûr, le symbole monétaire, la précision, le format, etc. dépendent de chaque devise.

facundofarias
la source
1

Vous pouvez transmettre certaines options à number_to_currency(un assistant de vue Rails 4 standard):

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

Tel que publié par Dylan Markow

blnc
la source
0

Code simple pour Ruby & Rails

<%= number_to_currency(1234567890.50) %>

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