AbM
AbM

Reputation: 7779

Grouping an array by comparing 2 adjacent elements

I have an array of objects and I would like to group them based on the difference between the attributes of 2 adjacent elements. The array is already sorted by that attribute. For instance:

Original array:

array = [a, b, c, d, e]

and

a.attribute = 1
b.attribute = 3
c.attribute = 6
d.attribute = 9
e.attribute = 10 

If I want to group the elements such that the difference between the attributes of 2 adjacent elements are less or equal than 2, the result should look like so:

END RESULT

result_array = [[a, b], [c], [d, e]]

WHAT I HAVE

def group_elements_by_difference(array, difference)
    result_array = []
    subgroup = []
    last_element_attribute = array.first.attribute
    array.each do |element|
      if element.attribute <= (last_element_attribute + difference)
        subgroup << element
      else
        #add the subgroup to the result_array
        result_array << subgroup
        subgroup = []
        subgroup << element
      end
      #update last_element_attribute
      last_element_attribute = element.attribute
    end
    result_array << subgroup
end

QUESTION

Is there a built in function in Ruby 1.9.3, such as group_by that could replace my group_elements_by_difference?

Upvotes: 3

Views: 763

Answers (3)

steenslag
steenslag

Reputation: 80065

array = [1, 3, 6, 9, 10]
prev = array[0]
p array.slice_before{|el| prev,el = el,prev; prev-el > 2}.to_a

# => [[1, 3], [6], [9, 10]]

Upvotes: 1

Ju Liu
Ju Liu

Reputation: 3999

Following Jan Dvorak's suggestion, this solution uses slice_before and a hash to keep the state:

class GroupByAdjacentDifference < Struct.new(:data)
  def group_by(difference)
    initial = { prev: data.first }

    data.slice_before(initial) do |item, state|
      prev, state[:prev] = state[:prev], item
      value_for(item) - value_for(prev) > difference
    end.to_a
  end

  def value_for(elem)
    elem.attribute
  end
end

require 'rspec/autorun'

describe GroupByAdjacentDifference do

  let(:a) { double("a", attribute: 1) }
  let(:b) { double("b", attribute: 3) }
  let(:c) { double("c", attribute: 6) }
  let(:d) { double("d", attribute: 9) }
  let(:e) { double("e", attribute: 10) }

  let(:data) { [a, b, c, d, e] }
  let(:service) { described_class.new(data) }

  context "#group_by" do
    it "groups data by calculating adjacent difference" do
      expect(service.group_by(2)).to eq([[a, b], [c], [d, e]])
    end
  end
end

which gives

$ ruby group_by_adjacent_difference.rb
.

Finished in 0.0048 seconds
1 example, 0 failures

In alternative, local variables could also be used to keep state, although I find it a bit harder to read:

class GroupByAdjacentDifference < Struct.new(:data)
  def group_by(difference)
    tmp = data.first

    data.slice_before do |item|
      tmp, prev = item, tmp
      value_for(item) - value_for(prev) > difference
    end.to_a
  end

  def value_for(elem)
    elem.attribute
  end
end

Upvotes: 4

sawa
sawa

Reputation: 168101

The following uses numerals directly, but the algorithm should be the same as when you do it with attributes. It assumes that all numerals are greater than 0. If not, then replace it with something that works.

array = [1, 3, 6, 9, 10]

[0, *array].each_cons(2).slice_before{|k, l| l - k > 2}.map{|a| a.map(&:last)}
# => [[1, 3], [6], [9, 10]]

With attributes, do l.attribute, etc., and replace 0 with a dummy element whose attribute is 0.

Upvotes: 5

Related Questions