Rails 3. 如何获取两个数组之间的差异?

62

假设我有一个包含运输订单 ID 的数组。

s = Shipment.find(:all, :select => "id")

[#<Shipment id: 1>, #<Shipment id: 2>, #<Shipment id: 3>, #<Shipment id: 4>, #<Shipment id: 5>]

带有运输 ID 的发票数组

i = Invoice.find(:all, :select => "id, shipment_id")

[#<Invoice id: 98, shipment_id: 2>, #<Invoice id: 99, shipment_id: 3>]
  • 发票属于货运。
  • 货运有一个发票。
  • 因此,发票表有一个名为shipment_id的列。

要创建发票,我点击新发票,然后有一个包含货运的选择菜单,这样我就可以选择“要为哪个货运创建发票”。所以我只想显示尚未创建发票的货运列表。

所以我需要一个还没有发票的货运数组。在上面的例子中,答案将是1、4、5。


1
1、4、5不是没有shipment_id的发票ID列表。 - Robin
抱歉,问题已经更正。感谢您的考虑。 - leonel
2
可能是重复的问题:查找所有没有关联记录的记录 - Ryan Bigg
从 Ruby 2.6 开始,您可以使用 difference更多信息请参见此处 - SRack
@SRack,您提供的链接正确吗? - stevec
@user5783745 - 这是一条回答,但由于它遭到了未经解释的负评,我将其删除。现在我已经为您恢复了该回答,所以上面评论中的链接应该可以正常使用了。如果您觉得有用,请点个赞吧 :) - SRack
9个回答

175
a = [2, 4, 6, 8]
b = [1, 2, 3, 4]

a - b | b - a # => [6, 8, 1, 3]

24
这是最优雅的答案。a - b 返回在 a 中但不在 b 中的任何元素。b - a 返回在 b 中但不在 a 中的任何元素,而 | 运算符返回这两个结果中唯一的元素集合。 - galatians
喜欢这个 :) 这是一种很好的比较数组差异的方式。 - Brian Clifton
5
不错的方法,但对于重复值将无法奏效。例如,如果一个数包含在两个4中,则第二个4将不会显示出来。 - Saleh Rastani
完美的解决方案...! - nikamanish

46

首先,您需要获取出现在发票中的运输ID列表:

ids = i.map{|x| x.shipment_id}
然后从原始数组中“拒绝”它们:
s.reject{|x| ids.include? x.id}

注意: 记住,reject 返回一个新数组,如果你想改变原始数组,请使用 reject!。


如果您正在使用Rails 3.2.1+和ActiveRecord,您应该使用pluck:ids = i.pluck(:id) - Lewis Buckley
5
这比只做 x-i 慢得多,数组越大,速度越慢。这是我写的一个基准测试,比较了这两种方法的效率。链接为:http://runnable.com/U5Y8g_nsUQokbzNl/benchmark-ruby-array-diff-methods - Ryan
@Ryan - 是的,但那不是同一件事情。 - pguardiario

27

使用替代符号

irb(main):001:0> [1, 2, 3, 2, 6, 7] - [2, 1]
=> [3, 6, 7]

4
这个:[2, 1] - [1, 2, 3, 2, 6, 7] 返回 []。这让我很好奇,如何从两个动态数组中获取它们的差异,而不考虑它们的顺序。 - Trip
14
为了回答你的问题,你可以这样做...(a-b) + (b-a)。其中,你需要获取两个数组中的唯一值,然后将这些值合并到一个新的数组中。 - Ryan
@Ryan 谢谢你在这个评论区中提供一些理智的观点。 - Joshua Pinter

16

Ruby 2.6 推出了 Array.difference 方法:

[1, 1, 2, 2, 3, 3, 4, 5 ].difference([1, 2, 4]) #=> [ 3, 3, 5 ]

所以在这个案例中:

Shipment.pluck(:id).difference(Invoice.pluck(:shipment_id))

看起来是一个不错的优雅解决方案。虽然有时候回想起 a - b | b - a 可能有些棘手,但我一直是一个热心追随者。

这当然解决了这个问题。


2
我没有得到预期的结果。对我来说它的工作方式与 a - b 相同。 - Mosselman
3
这个很好用。但需要注意的是,数组的顺序(即在哪个数组上使用.difference方法)对结果有很大影响。 - Joshua Pinter
只有比较接收器的差异,不会返回第二个数组中存在但第一个数组中不存在的值。 [1,2,3].difference([4,5,6]) #=> [1,2,3] 等同于 a-b - Rob

12

纯Ruby解决方案为

(a + b) - (a & b)

([1,2,3,4] + [1,3]) - ([1,2,3,4] & [1,3])
=> [2,4]

a + b将产生两个数组的并集
a&b返回交集
以及union - intersection将返回差异


5

这里之前的回答只包括了单向差异。如果你想要两个数组之间的不同(也就是它们都有一个独特的项),那么可以尝试以下方法。

def diff(x,y)
  o = x
  x = x.reject{|a| if y.include?(a); a end }
  y = y.reject{|a| if o.include?(a); a end }
  x | y
end

5
这应该可以在一个ActiveRecord查询中完成。
Shipment.where(["id NOT IN (?)", Invoice.select(:shipment_id)]).select(:id)

它输出SQL查询语句

SELECT "shipments"."id" FROM "shipments"  WHERE (id NOT IN (SELECT "invoices"."shipment_id" FROM "invoices"))

在 Rails 4+ 版本中,你可以进行如下操作:
Shipment.where.not(id: Invoice.select(:shipment_id).distinct).select(:id)

它会输出SQL语句

SELECT "shipments"."id" FROM "shipments"  WHERE ("shipments"."id" NOT IN (SELECT DISTINCT "invoices"."shipment_id" FROM "invoices"))

我建议使用ids方法而不是select(:id)

这将返回一个只包含所选对象的ID数组。

Shipment.where.not(id: Invoice.select(:shipment_id).distinct).ids

2

当处理字符串数组时,将差异分组在一起可能很有用。

这种情况下,我们可以使用Array#zip将元素分组,然后使用块来决定如何处理分组的元素(Array)。

a = ["One", "Two",     "Three", "Four"]
b = ["One", "Not Two", "Three", "For" ]

mismatches = []
a.zip(b) do |array| 
  mismatches << array if array.first != array.last
end

mismatches
# => [
#   ["Two", "Not Two"], 
#   ["Four", "For"]
# ]

0
s.select{|x| !ids.include? x.id}

1
这并没有回答问题。一旦你拥有足够的声望,你将能够对任何帖子进行评论;相反,提供不需要提问者澄清的答案。- 来自审核 - Sergio
这是被接受答案的精选版本。与使用拒绝相反。我只是没有足够的堆栈溢出声望来评论答案本身。 - Fez Abbas
我想这个问题已经被投票降低/标记了,因为它没有提供任何上下文(例如,这里的“ids”是什么?),解释并且与现有答案没有太大区别。如果有用的话,这里有一个组成扎实答案的指南在这里 @FezAbbas :) - SRack

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