dax
dax

Reputation: 10997

Remove one object from an array with multiple matching objects

I have an array:

array = ['a', 'b', 'c', 'a', 'b', 'a', 'a']

sorted, just to make it easier to look at:

array = ['a', 'a', 'a', 'a', 'b', 'b', 'c']

I want to remove, for example, three of the a's. array.delete('a') removes every a.

The following code 'works' but I think you'll agree it's absolutely hideous.

 new_array = array.sort.join.sub!('aaa', '').split(//)

How do I do this more cleanly?

To give a bit more information on what I'm doing here, I have some strings being pushed into an array asynchronously. Those strings can be (and often are) identical to each other. If there are a certain number of those matching strings, an action is triggered, the matching object is removed (like Tetris, I guess), and the process continues.

Before the following code is run, array could be ['a', 'a', 'a', 'b']:

while array.count(product[:name]) >= product[:quantity]
  # trigger an event
  product[:quantity].times do
    array.slice!(array.index(product[:name]))
  end
end

assuming that product[:name] is a and product[:quantity] is 3, after the code above runs, array should be ['b'].

Upvotes: 3

Views: 126

Answers (3)

the Tin Man
the Tin Man

Reputation: 160551

If you want to maintain, or convert, an array so it only one instance of each element, you can use uniq or a Set instead of an array.

array = ['a', 'b', 'c', 'a', 'b', 'a', 'a']
array.uniq # => ["a", "b", "c"]

require 'set'
array.to_set # => #<Set: {"a", "b", "c"}>

A Set will automatically maintain the uniqueness of all the elements for you, which is useful if you're going to have a huge number of potentially repetitive elements and don't want to accumulate them in memory before doing a uniq on them.


@sawa mentioned that this looks like an "XY problem", and I agree.

The source of the problem is using an array instead of a hash as your basic container. An array is good when you have a queue or list of things to process in order but it's horrible when you need to keep track of the count of things, because you have to walk that array to find out how many of a certain thing you have. There are ways to coerce the information you want out of an array when you get the array as your source.

Since it looks like he identified the real problem, here are some building blocks to use around the problem.

If you have an array and want to figure out how many different elements there are, and their count:

array = ['a', 'a', 'a', 'a', 'b', 'b', 'c', 'c']
array_count = array.group_by { |i| i }.map{ |k, v| [k, v.size] }.to_h
# => {"a"=>4, "b"=>2, "c"=>2}

From that point it's easy to find out which ones exceed a certain count:

array_count.select{ |k, v| v >= 3 } # => {"a"=>4}

For a quick way to remove all elements of something from the array, after processing you can use a set "difference" operation:

array = ['a', 'a', 'a', 'a', 'b', 'b', 'c']
array -= ['a']
# => ["b", "b", "c", "c"]

or delete_if:

array.delete_if { |i| i == 'a' }
array # => ["b", "b", "c"]

Upvotes: 2

sawa
sawa

Reputation: 168101

I think you have an XY-problem. Instead of an array, you should use a hash with number of occurrences as the value.

hash = Hash.new(0)

When you want to add an entity, you should do:

hash["a"] += 1

If you want to limit the number to a certain value, say k, then do:

hash["a"] += 1 unless hash["a"] == k

Upvotes: 2

Mureinik
Mureinik

Reputation: 311393

slice may be the thing you're looking for:

3.times {array.slice!(array.index('a'))}

Upvotes: 2

Related Questions