如何将BigDecimal与ActiveRecord decimal字段进行比较?

3

假设有这样一个模式:

create_table "bills", :force => true do |t|
  t.decimal  "cost", :precision => 10, :scale => 5
end

我想编写一个函数,当且仅当它是唯一的时,将新账单写入数据库。以下代码无法实现此功能:

def load_bill_unless_exists(candidate)
  incumbents = Bill.scoped.where(:cost => candidate.cost)
  candidate.save unless incumbents.exists?
end

因为现有账单和候选账单在其BigDecimal表示中具有不同的限制,所以:cost => candidate.cost测试失败。也就是说,它正在比较:

candidate: #<Bill id: nil, cost: #<BigDecimal:105e39850,'0.1670576666 6666666E4',27(27)>>

使用

incumbent: #<ServiceBill id: 198449, cost: #<BigDecimal:105e35840,'0.167057667E4',18(18)>>

注意候选者的BigDecimal表示成本时比现有者多了更多数字。

因此,问题很简单:执行此比较的正确方法是什么?我考虑过:cost => BigDecimal.new(candidate.cost.to_s, 18),但那感觉不对——例如,数字18从哪里来?

2个回答

1
如果您正在考虑的强制转换有效,那么您可能必须采用它。您使用的查询仅构建一个“WHERE cost = number”的条件语句,如果数据库无法正确比较传递的数字,则需要以不同的方式传递它。看起来是数据库在阻止您操作,而不一定是Rails内部的任何问题。
如果您不喜欢在查询中进行强制转换,您可以在模型中执行它:
def cost_with_incumbent_precision
  BigDecimal.new(cost.to_s, 18)
end

我显然不理解BigDecimal.new()中的有效数字参数。例如,BigDecimal.new('1670.5766666666666') == BigDecimal.new('1670.5766666666666', 3) => true。所以,不,像我考虑的那样转换是行不通的。 - fearless_fool
1
也许尝试使用 round 或 truncate?cost.round(18) 或 cost.truncate(18)?你的主要问题是在 SQL 语句中生成的字符串与数据库中现有表中的内容不一致。 - jdc

1

尝试使用 BigDecimal#round

def load_bill_unless_exists(candidate)
  incumbents = Bill.scoped.where(:cost => candidate.cost.round(5))
  candidate.save unless incumbents.exists?
end

来自文档:

四舍五入到最近的1(默认),将结果作为BigDecimal返回。如果指定了n并且为正数,则结果的小数部分不超过该数字。

考虑到您在模式中指定了精度为5,因此在进行比较时应该将其舍入到该精度。


啊!看起来完全正确。我会测试一下——如果它能正常工作,你就会得到勾号。 - fearless_fool
没错 - 这完美地解决了问题。我用 COST_SCALE = Bill.columns.find {|r| r.name == 'cost'}.scale 来优化你的解决方案,然后(稍后)使用 incumbents = Bill.scoped.where(:cost => candidate.cost.round(COST_SCALE) 避免在代码中使用类似“5”这样的神奇数字。 - fearless_fool
我也遇到过这样的问题,需要比较像16.94 == 16.939999999999999这样的金钱值以进行保存前的验证。然后我将其四舍五入保留2位小数,问题得到了解决。 - Amit Patel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接