Dan's Tech (and more) Blog
Ruby's Partition Method: It Does What You Think.

Ruby’s Partition Method: It Does What You Think.

October 03, 2020 ( last updated : October 03, 2020 )
ruby


Ruby has a neat function in Enumerable called partition that can split the values in the enumerable into two partitions based arbitrary logic.

My problem

I had an array of ActiveRecord models:

class Section < ActiveRecord::Base
  # TABLE public.sections (
  # id integer NOT NULL,
  # section_type character varying, NOT NULL,
  # created_at timestamp without time zone NOT NULL,
  # updated_at timestamp without time zone NOT NULL
  # )
  has_many :fields
end

class Field < ActiveRecord::Base
  # TABLE public.fields (
  # id integer NOT NULL,
  # created_at timestamp without time zone NOT NULL,
  # updated_at timestamp without time zone NOT NULL
  # )
  belongs_to :section
end

That were being serialized based on a bunch of logic earlier in the application:

class SerializerClass
  module_function

  def serialize_fields(fields)
    sorted_fields = special_sort(fields)
  end
end

I was tasked with placing any fields belonging to sections of type last at the end of the serialized list regardless of where they were sorted previously. Also, if there were multiple fields related to sections of type last, I needed to ensure they stayed in the order they were sorted into previously.

This might be better illustrated by a simplified example: say we wanted to place all names starting with the letter A a the end of this list, ensuring they were in the same order given to us:

# Given Input
['Billy', 'Alex', 'Sanjay', 'Anwar', 'Dan']
# Desired output:
['Billy', 'Sanjay', 'Dan', 'Alex', 'Anwar']

Unfortunately ruby delete_if doesn’t return the objects removed, so we need to find them, delete them then put them back on at the end.

class SerializerClass
  module_function

  def serialize_fields(fields)
    sorted_fields = special_sort(fields)
    last_fields = sorted_fields.find_by { |field| field.section.section_type == 'last' }

    # Required because .delete(*[]) returns an ArgumentError:
    if last_fields.count > 0
      sorted_fields = sorted_fields.delete(*last_fields)
      sorted_fields = sorted_fields.append(*last_fields)
    end
    sorted_fields
  end
end

This isn’t great, but it worked until we could get more explicit ordering in the up-stream logic.

A few weeks later I was told we need to add ANOTHER bit of logic putting fields belonging to any penultimate sections after everything except those last fields…. Also we didn’t have time to add the logic upstream where it really belonged.

This gets gross kinda quickly:

class SerializerClass
  module_function

  def serialize_fields(fields)
    sorted_fields = special_sort(fields)
    last_fields = sorted_fields.select { |field| field.section.section_type == 'last' }
    penultimate_fields = sorted_fields.select { |field| field.section.section_type == 'penultimate' }

    if last_fields.any?
      sorted_fields = sorted_fields.delete(*last_fields)

      if penultimate_fields.any?
        sorted_fields = sorted_fields.delete(*penultimate_fields)
        # Writing this now I'm realizing I could have done array addition at the end instead of using
        # append, but I like my partition solution better anyway 
        sorted_fields = sorted_fields.append(*penultimate_fields)
      end

      sorted_fields = sorted_fields.append(*last_fields)
    end
    sorted_fields
  end
end

So I decided to check if Array or Enumerable had methods I could leverage.

My Solution

After reading the docs a bit, I found partition which returns an array of arrays, the first containing everything that returned true and the second array containing everything else:

[1, 2, 3, 4, 5, 6].partition { |num| num % 2 == 0 }
# => [[2, 4, 6], [1, 3, 5]]

You can also assign multiple variables at the same time like so:

even, odd = [1, 2, 3, 4, 5, 6].partition { |num| num % 2 == 0 }

p even
# => [2, 4, 6]
p odd
# => [1, 3, 5]

Re-writing the code above with partition we can say:

class SerializerClass
  module_function

  def serialize_fields(fields)
    sorted_fields = special_sort(fields)
    last_fields, other_fields = sorted_fields.partition { |field| field.section.section_type == 'last' }
    penultimate_fields, other_fields = other_fields.partition { |field| field.section.section_type == 'penultimate' }

    other_fields + penultimate_fields + last_fields
  end
end

Which is less destructive, feels a lot more straight-forward and gives allows us to have a more declarative ordering statement at the end.