61

Let’s say I have this array with shipments ids.

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

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

Array of invoices with shipment id's

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

[#<Invoice id: 98, shipment_id: 2>, #<Invoice id: 99, shipment_id: 3>]
  • Invoices belongs to Shipment.
  • Shipment has one Invoice.
  • So the invoices table has a column of shipment_id.

To create an invoice, I click on New Invoice, then there is a select menu with Shipments, so I can choose "which shipment am i creating the invoice for". So I only want to display a list of shipments that an invoice hasn't been created for.

So I need an array of Shipments that don't have an Invoice yet. In the example above, the answer would be 1, 4, 5.

Giacomo1968
  • 25,759
  • 11
  • 71
  • 103
leonel
  • 10,106
  • 21
  • 85
  • 129
  • 1
    1, 4, 5 is not a list of ids of invoices with no_shipment_id. – Robin Dec 26 '11 at 23:28
  • Sorry, corrected question. Thanks for considering it. – leonel Dec 26 '11 at 23:44
  • 2
    possible duplicate of [Finding all records without associated ones](http://stackoverflow.com/questions/1314408/finding-all-records-without-associated-ones) – Ryan Bigg Dec 27 '11 at 00:08
  • As of Ruby 2.6 you can use `difference`. [More info here](https://stackoverflow.com/questions/8639857/rails-3-how-to-get-the-difference-between-two-arrays/53342467#answer-53342467). – SRack Nov 16 '18 at 17:10
  • @SRack is the link you provided correct? – stevec Dec 22 '18 at 12:35
  • @user5783745 - it was to an answer, which I deleted as it was getting unexplained downvotes. I've undeleted for you, so the link in the comment above should work again. Feel free to give it a thumb if you find it useful :) – SRack Dec 27 '18 at 10:28

9 Answers9

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

a - b | b - a # => [6, 8, 1, 3]
Kyle Decot
  • 20,715
  • 39
  • 142
  • 263
46

First you would get a list of shipping_id's that appear in invoices:

ids = i.map{|x| x.shipment_id}

Then 'reject' them from your original array:

s.reject{|x| ids.include? x.id}

Note: remember that reject returns a new array, use reject! if you want to change the original array

pguardiario
  • 53,827
  • 19
  • 119
  • 159
  • If you are using Rails 3.2.1+ and ActiveRecord you should use pluck: `ids = i.pluck(:id)` – Lewis Buckley Feb 14 '14 at 11:15
  • 5
    This is exponentially slower than just doing `x-i`. The larger the arrays the slower it gets. Here's a benchmark I wrote comparing the two methods http://runnable.com/U5Y8g_nsUQokbzNl/benchmark-ruby-array-diff-methods – Ryan Jun 09 '14 at 23:05
  • @Ryan - yes, but that's not the same thing. – pguardiario Jun 10 '14 at 06:59
26

Use substitute sign

irb(main):001:0> [1, 2, 3, 2, 6, 7] - [2, 1]
=> [3, 6, 7]
denis.peplin
  • 9,585
  • 3
  • 48
  • 55
  • 4
    This : `[2, 1] - [1, 2, 3, 2, 6, 7]` returns `[]`. So it makes me curious how you could get the difference from two dynamic arrays regardless of their order. – Trip Feb 09 '14 at 06:31
  • 14
    @Trip to answer your question, you could do something like this... `(a-b) + (b-a)` where you get the unique values in both arrays, then combine those values together into a single array. – Ryan Jun 09 '14 at 22:51
  • @Ryan Thank you for shedding some sanity on this comment thread. – Joshua Pinter Sep 10 '14 at 00:13
16

Ruby 2.6 is introducing Array.difference:

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

So in the case given here:

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

Seems a nice elegant solution to the problem. I've been a keen follower of a - b | b - a, though it can be tricky to recall at times.

This certainly takes care of that.

SRack
  • 11,495
  • 5
  • 47
  • 60
  • 2
    I don't get the expected result. It works the same as `a - b` for me. – Mosselman Sep 16 '19 at 14:41
  • 3
    This works great. Except, it's important to note that the order of the Arrays (i.e. which one you call `.difference` on) plays a big role in the result. – Joshua Pinter Nov 17 '19 at 05:28
  • Difference only compares the receiver, it wont return values that are in the 2nd array but not in the first `[1,2,3].difference([4,5,6]) #=> [1,2,3]` it's equivalent to `a-b` – Rob Jun 21 '23 at 11:11
11

Pure ruby solution is

(a + b) - (a & b)

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

Where a + b will produce a union between two arrays
And a & b return intersection
And union - intersection will return difference

Sixty4Bit
  • 12,852
  • 13
  • 48
  • 62
zhisme
  • 2,368
  • 2
  • 19
  • 28
5

The previous answer here from pgquardiario only included a one directional difference. If you want the difference from both arrays (as in they both have a unique item) then try something like the following.

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
6ft Dan
  • 2,365
  • 1
  • 33
  • 46
5

This should do it in one ActiveRecord query

Shipment.where(["id NOT IN (?)", Invoice.select(:shipment_id)]).select(:id)

And it outputs the SQL

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

In Rails 4+ you can do the following

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

And it outputs the SQL

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

And instead of select(:id) I recommend the ids method.

Shipment.where.not(id: Invoice.select(:shipment_id).distinct).ids
6ft Dan
  • 2,365
  • 1
  • 33
  • 46
2

When dealing with arrays of Strings, it can be useful to keep the differences grouped together.

In which case, we can use Array#zip to group the elements together and then use a block to decide what to do with the grouped elements (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"]
# ]
pdobb
  • 17,688
  • 5
  • 59
  • 74
0
s.select{|x| !ids.include? x.id}
Zoe
  • 27,060
  • 21
  • 118
  • 148
Fez Abbas
  • 107
  • 1
  • 10
  • 1
    This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/low-quality-posts/22313950) – Sergio Feb 26 '19 at 18:32
  • It's the select version of the accepted answer. The opposite of using reject. I just don't have enough stack overflow rep to comment on the answer itself – Fez Abbas Feb 27 '19 at 16:21
  • I imagine this has been downvoted / flagged as it doesn't offer any context (e.g. what are `ids` here?), explanation and is not much different to an existing answer. There's a guide to putting together a solid answer [here](https://stackoverflow.com/help/how-to-answer) if it's useful @FezAbbas :) – SRack Oct 03 '19 at 15:54