处理货币/金钱的最佳方法是什么?


323

我正在开发一个非常基本的购物车系统。

我有一个表items,该表具有pricetype 列integer

我无法在包含欧元和美分的价格视图中显示价格值。就在Rails框架中处理货币而言,我是否缺少明显的东西?


如果有人使用sql,则DECIMAL(19, 4) 是一个流行的选择,请在检查并在此处检查世界货币格式以确定要使用的小数位数,希望有所帮助。
shaijut

Answers:


495

您可能需要DECIMAL在数据库中使用一种类型。在迁移中,请执行以下操作:

# 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

在Rails中,:decimal类型返回为BigDecimal,这对于价格计算非常有用。

如果您坚持使用整数,则必须在BigDecimal任何地方手动与s进行相互转换,这可能会很痛苦。

正如mcl指出的那样,要打印价格,请使用:

number_to_currency(price, :unit => "€")
#=> €1,234.01

13
使用number_to_currency帮助程序,有关更多信息,请访问api.rubyonrails.org/classes/ActionView/Helpers/…–
mlibby

48
实际上,将整数与acts_as_dollars结合使用更安全,更容易。您是否曾经被浮点比较所困扰?如果没有,请不要将其作为您的初次体验。:)使用acts_as_dollars,您将内容以12.34格式放置,存储为1234,结果显示为12.34。
莎拉·梅

50
@Sarah Mei:BigDecimals +十进制列格式可以避免这种情况。
molf

114
重要的是不要盲目地复制此答案- 精度8,小数位数2为您提供最大值999,999.99。如果您需要大于一百万的数字,请提高精度!
乔恩·凯恩斯

22
同样重要的是,如果您要处理不同的货币,不仅要盲目使用2的标度–一些北非和阿拉伯货币(例如阿曼里亚尔或突尼斯第纳尔)的标度为3,因此精度8的标度3更适合。
Beat Richartz

117

这是一种很好的简单方法 composed_of(使用ValueObject模式的ActiveRecord的一部分)和Money gem

你需要

  • 金钱的宝石(版本4.1.0)
  • 例如模型 Product
  • 例如integer,模型(和数据库)中的列:price

将此写入您的product.rb文件中:

class Product > ActiveRecord::Base

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

您会得到:

  • 如果不做任何额外的更改,您的所有表格都将显示美元和美分,但是内部表示形式仍然仅仅是美分。表单将接受“ $ 12,034.95”之类的值并为您转换。无需在模型中添加额外的处理程序或属性,也无需在视图中添加帮助程序。
  • product.price = "$12.00" 自动转换为Money类
  • product.price.to_s 显示十进制格式的数字(“ 1234.00”)
  • product.price.format 显示正确格式的货币字符串
  • 如果您需要发送美分(到需要几美分的付款网关), product.price.cents.to_s
  • 免费货币换算

14
我喜欢这种方法。但是请注意:请确保在此示例中,您为“价格”进行的迁移不允许为空,并且默认为0,以免您疯狂地尝试找出为什么这种方法无效。
科里(Cory)

3
我发现money_column宝石 (从Shopify提取)非常容易使用...如果您不需要货币换算,则比money宝石更容易使用。
talyric,2011年

7
对于所有使用Money gem的人来说,应该注意的是,Rails核心团队正在讨论弃用和从框架中删除“ composed_of”的问题。我怀疑如果发生这种情况,gem将被更新以处理此问题,但是如果您正在使用Rails 4.0,则应该意识到这种可能性
Peer Allan'Oct

1
关于@PeerAllan关于删除composed_of 此处的评论,对此有更详细的介绍以及其他实现。
HerbCSO

3
此外,使用rails-money宝石确实很容易。
fotanus 2014年

25

处理货币的常见做法是使用十进制类型。这是“使用Rails进行敏捷Web开发”中的一个简单示例

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

这将使您能够处理-999,999.99至999,999.99之间的价格。
您可能还希望在商品中加入验证,例如

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

理智地检查您的价值观。


1
该解决方案还使您能够使用SQL sum和朋友。
拉里·K

4
您是否可以做:验证:price,:presence => true,:numericality => {:greater_than => 0}
星系

9

如果您使用的是Postgres(并且现在已经是2017年了),则可以:money尝试使用其列类型。

add_column :products, :price, :money, default: 0

7

使用钱轨宝石。它可以很好地处理您模型中的金钱和货币,并且还有很多帮助您确定价格的助手。


是的,我同意这一点。通常,我通过将钱存储为美分(整数)并使用像按行为或金钱(钱轨)之类的宝石来处理内存中的数据来处理钱。以整数处理它可以防止那些讨厌的舍入错误。例如0.2 * 3 => 0.6000000000000001当然,这仅在不需要处理一分钱的分数时才有效。
乍得M

如果使用滑轨,这非常好。放入它,不用担心小数点栏的问题。如果您将其用于视图,那么此答案也可能会有所帮助:stackoverflow.com/questions/18898947/…–
停泊

6

对于RoR开发中一些有抱负的初级/初学者,只需稍作更新,并汇总所有答案,就一定会在这里进行一些解释。

用钱工作

使用:decimal存储钱DB,作为@molf建议(什么我公司使用的金标准与金钱工作时)。

# 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

几点:

  • :decimal将被用来BigDecimal解决很多问题。

  • precisionscale应根据您所代表的内容进行调整

    • 如果您使用收款和发送付款方式,precision: 8并且scale: 2给您999,999.99最高的金额,那么在90%的情况下可以。

    • 如果您需要代表房地产或稀有汽车的价值,则应使用更高的precision

    • 如果使用坐标(经度和纬度),则肯定需要更高的scale

如何产生迁移

要使用上述内容生成迁移,请在终端中运行:

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

要么

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

如本博客文章所述。

货币格式

亲吻额外的库,并使用内置的帮助器。number_to_currency按照建议使用@molf和@facundofarias。

number_to_currency在Rails控制台中使用帮助程序,请将呼叫发送到ActiveSupportNumberHelper类,以访问该帮助程序。

例如:

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

提供以下输出

2500000,61

检查其他optionsnumber_to_currency帮手。

放在哪里

您可以将其放在应用程序助手中,并在视图内部使用任意数量。

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

或者,您可以将其Item作为实例方法放入模型中,并在需要设置价格格式的地方(在视图或助手中)调用它。

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

还有一个示例,说明我如何使用number_to_currency控制器内部(注意该negative_format选项,用于表示退款)

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

5

使用虚拟属性(链接到经修订(收费)的Railscast的链接),您可以将price_in_cents存储在整数列中,并在产品模型中添加虚拟属性price_in_dollars作为获取器和设置器。

# 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

来源:RailsCasts#016:虚拟属性虚拟属性是添加不直接映射到数据库的表单域的一种干净方法。在这里,我展示了如何处理验证,关联等等。


1
留下200.0一位数字
ajbraus

2

绝对是整数

即使BigDecimal从技术上来说1.5仍然存在,仍将为您提供Ruby中的纯Float。


2

如果有人在使用Sequel,则迁移将类似于:

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

Sequel以某种方式忽略了:precision和:scale

(续集版本:续集(3.39.0,3.38.0))


2

我的基础API都使用美分来代表金钱,我不想改变这一点。我也没有花很多钱。所以我只是把它放在一个辅助方法中:

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

它将整数转换为至少包含三位数的字符串(如有必要,添加前导零),然后在最后两位数字之前插入小数点,从不使用 Float。在这里,您可以添加适用于您的用例的任何货币符号。

绝对是快速和肮脏的,但有时这只是罚款!


不能相信没有人支持你。这是唯一可以将我的Money对象很好地转换为某种形式,以便API可以使用它的方法。(十进制)
代码星期一

2

我以这种方式使用它:

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

当然,货币符号,精度,格式等取决于每种货币。


1

您可以将一些选项传递给number_to_currency(标准的Rails 4视图助手):

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

正如Dylan Markow发布的


0

Ruby&Rails的简单代码

<%= number_to_currency(1234567890.50) %>

OUT PUT => $1,234,567,890.50
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.