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.
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.
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.