Ruby Inside
Published in

Ruby Inside

Introducing Priora: An Object Prioritization Utility Gem for Ruby

https://pixabay.com/en/stones-pebbles-stack-pile-zen-801756/
class Post
attr_reader :author, :like_count, :is_sponsored

def initialize(author:, like_count:, is_sponsored:)
@author = author
@like_count = like_count
@is_sponsored = is_sponsored
end
end

Comes in Priora

Since this was not the first time I’ve stumbled upon such a problem, I thought it would be nice to have a utility library that helped in getting a collection prioritized. While sorting as a concept is well-known thanks to computer science studies and algorithms courses, prioritization is not a synonym for it, as it lies one abstract level above it; prioritization is the usage of sorting for applicable purposes (“business logic”) in a way that arranges the most important objects in a collection to come up first. I looked for an existing solution over RubyGems and the web but didn’t find what I was looking for, so I decided to write it down myself, and came up with Priora.

Priora in Action

For demonstration purposes, let’s get us three Post instances with distinct attributes:

low_like_count_sponsored = Post.new(author: 'Jay C.',
like_count: 10, is_sponsored: true)
high_like_count_unsponsored = Post.new(author: 'Aaron R.',
like_count: 90, is_sponsored: false)
high_like_count_sponsored = Post.new(author: 'Don Y.',
like_count: 90, is_sponsored: true)
unprioritized_array = [high_like_count_unsponsored, low_like_count_sponsored, high_like_count_sponsored]prioritized_array =  [high_like_count_sponsored, high_like_count_unsponsored, low_like_count_sponsored]Priora.prioritize(unprioritized_array, by: [:like_count, :is_sponsored]) == prioritized_array
=> true
https://pixabay.com/en/ropes-ship-barge-sea-nautical-2151683/

Fixed Class Priorities

In case we can commit to the prioritization between Post objects — i.e., we do not need the flexibility of changing the priorities each time (which was not the case for my colleague) — we can include the Priora module in our class and declare the fixed priorities using the prioritize_by class macro and gain shorter invocation later on. Our class would then read like this:

class Post
include Priora
prioritize_by :like_count, :is_sponsored
attr_reader :author, :like_count, :is_sponsored def initialize(author:, like_count:, is_sponsored:)
@author = author
@like_count = like_count
@is_sponsored = is_sponsored
end
end
Priora.prioritize(unprioritized_array) == prioritized_array
=> true

Advantages Over Using Custom Enumerable#sort

One might come up with the following snippet as an equivalent solution:

unprioritized_array.sort { |a, b|
[a.like_count, a.is_sponsored ? 1 : 0] <=>
[b.like_count, b.is_sponsored ? 1 : 0] }.reverse == prioritized_array
=> true
  • It is more verbose and prone to errors.
  • It declares the prioritization logic twice. We cannot use the lighter sort_by since booleans are not sortable out-of-the-box.
  • It handles the conversion of a boolean value (true / false) into a sortable value (1 / 0) inline, thus mixing levels of abstractions and confusing the potential reader.

Reverse Sorting, Extended: An Agenda

As mentioned before, Priora is based on the presumption that when we talk about a prioritized collection, we often refer to the outcome of sorting it and then reversing the result. That is because we naturally think about sorting in an ascending fashion, from small to large, while when we talk about “priorities”, or “top priorities”, we usually think of the largest items appearing up first. When these items are data objects, it is up to us to define what makes one object larger than another.

Directional Priorities

Obviously, this is not always true and some prioritization processes should give precedence to smaller items first; Priora supports this as well. You may change the prioritization direction for a specific priority:

Priora.prioritize(unprioritized_array, by: [[like_count: :asc], :is_sponsored])
=> [low_like_count_sponsored, high_like_count_sponsored, high_like_count_unsponsored]

Implicit Conversions

As noted, in order to offer a friendly, domain-logic-focused API, Priora takes care of converting non-sortable values, such as true, false or nil, into sortable values. By default, it assumes that true is larger than false and that nil evaluates to 0.

Priora.configuration.add_conversion_lambda(String, lambda { |value| value.length })
Priora.configuration.remove_conversion_lambda(String)

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store