From db610b35bf8721f0314d0b9befd2de010384a0f1 Mon Sep 17 00:00:00 2001 From: Ross Brandes Date: Tue, 22 Sep 2026 15:10:46 -0400 Subject: [PATCH] Add Put.enum for sorting by position in a caller-supplied list Closes #2. Lets callers sort discrete values (roles, statuses, tiers) by their position in an explicit order list instead of hand-writing a case/when translation table. Unmatched values sort last rather than raising, matching the gem's existing fault-tolerant design; nil values continue to follow nils_first as with every other Put method. --- CHANGELOG.md | 4 ++ README.md | 25 ++++++++++ lib/put.rb | 5 ++ lib/put/puts_thing/enum.rb | 10 ++++ test/put/puts_thing/enum_test.rb | 78 ++++++++++++++++++++++++++++++++ test/put_test.rb | 19 ++++++++ 6 files changed, 141 insertions(+) create mode 100644 lib/put/puts_thing/enum.rb create mode 100644 test/put/puts_thing/enum_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index d186d70..22f2276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [Unreleased] + +- Add Put.enum + ## [0.2.0] - 2026-09-22 - **[Breaking]** Drop support for Ruby 2.6; require Ruby >= 3.1 diff --git a/README.md b/README.md index 7fb9344..8f00a99 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,31 @@ As you might be able to guess, `Put.nils_last` puts the nils last: } # => [:every, :counts, nil] ``` +### Put.enum(value, order:, nils_first: false) + +When you're sorting by a discrete attribute — a role, a status, a tier — that +doesn't have a natural order of its own, `Put.enum` sorts a value by its +position in a list you provide, instead of you writing a `case`/`when` +translation table by hand: + +```ruby +[:director, :staff, :manager].sort_by { |role| + [Put.enum(role, order: [:staff, :manager, :director])] +} # => [:staff, :manager, :director] +``` + +A value that isn't in `order` sorts after every value that is, rather than +raising: + +```ruby +[:manager, :contractor, :staff].sort_by { |role| + [Put.enum(role, order: [:staff, :manager, :director])] +} # => [:staff, :manager, :contractor] +``` + +Like `Put.asc` and `Put.desc`, `Put.enum` also takes a `nils_first` keyword +argument (defaulting to `false`) for where `nil` values should sort. + ### Put.debug(sorting_arrays) If you see "comparison of Array with Array failed" and you don't have any idea diff --git a/lib/put.rb b/lib/put.rb index c4f3953..15d0203 100644 --- a/lib/put.rb +++ b/lib/put.rb @@ -6,6 +6,7 @@ require_relative "put/puts_thing/first" require_relative "put/puts_thing/last" require_relative "put/puts_thing/in_order" +require_relative "put/puts_thing/enum" require_relative "put/puts_thing/ascending" require_relative "put/puts_thing/descending" require_relative "put/puts_thing/nil_order" @@ -29,6 +30,10 @@ def self.asc(value, nils_first: false) PutsThing::Ascending.new(value, nils_first: nils_first) end + def self.enum(value, order:, nils_first: false) + PutsThing::Enum.new(value, order: order, nils_first: nils_first) + end + def self.nils_first(value) PutsThing::NilsFirst.new(value) end diff --git a/lib/put/puts_thing/enum.rb b/lib/put/puts_thing/enum.rb new file mode 100644 index 0000000..d745caf --- /dev/null +++ b/lib/put/puts_thing/enum.rb @@ -0,0 +1,10 @@ +module Put + class PutsThing + class Enum < InOrder + def initialize(value, order:, nils_first: false) + rank = value.nil? ? nil : (order.index(value) || Float::INFINITY) + super(rank, nils_first: nils_first) + end + end + end +end diff --git a/test/put/puts_thing/enum_test.rb b/test/put/puts_thing/enum_test.rb new file mode 100644 index 0000000..53f88cf --- /dev/null +++ b/test/put/puts_thing/enum_test.rb @@ -0,0 +1,78 @@ +require "test_helper" + +module Put + class PutsThing + class EnumTest < Minitest::Test + def test_matched_values_sort_by_order_position + result = [:director, :staff, :manager].sort_by { |role| + [Put.enum(role, order: [:staff, :manager, :director])] + } + + assert_equal [:staff, :manager, :director], result + end + + def test_unmatched_value_sorts_after_every_matched_value_and_before_nil + result = [nil, :contractor, :director, :staff].sort_by { |role| + [Put.enum(role, order: [:staff, :director])] + } + + assert_equal [:staff, :director, :contractor, nil], result + end + + def test_nil_sorts_last_by_default + result = [nil, :staff, :director].sort_by { |role| + [Put.enum(role, order: [:staff, :director])] + } + + assert_equal [:staff, :director, nil], result + end + + def test_nil_sorts_first_when_nils_first_is_true + result = [:staff, nil, :director].sort_by { |role| + [Put.enum(role, order: [:staff, :director], nils_first: true)] + } + + assert_equal [nil, :staff, :director], result + end + + def test_nil_sorts_after_matched_and_unmatched_values_by_default + result = [nil, :contractor, :director, :staff].sort_by { |role| + [Put.enum(role, order: [:staff, :director])] + } + + assert_equal [:staff, :director, :contractor, nil], result + end + + def test_nil_sorts_before_matched_and_unmatched_values_when_nils_first_is_true + result = [:staff, :contractor, nil, :director].sort_by { |role| + [Put.enum(role, order: [:staff, :director], nils_first: true)] + } + + assert_equal [nil, :staff, :director, :contractor], result + end + + def test_repeated_order_entry_ranks_by_first_occurrence + result = [:director, :staff].sort_by { |role| + [Put.enum(role, order: [:staff, :director, :staff])] + } + + assert_equal [:staff, :director], result + end + + def test_nil_entry_in_order_has_no_effect_on_ranking + result = [nil, :director, :staff].sort_by { |role| + [Put.enum(role, order: [nil, :staff, :director])] + } + + assert_equal [:staff, :director, nil], result + end + + def test_same_rank_values_compare_as_equal + a = Put.enum(:staff, order: [:staff, :director]) + b = Put.enum(:staff, order: [:staff, :director]) + + assert_equal 0, a <=> b + end + end + end +end diff --git a/test/put_test.rb b/test/put_test.rb index fa9486d..618aa56 100644 --- a/test/put_test.rb +++ b/test/put_test.rb @@ -48,6 +48,25 @@ def test_put assert_includes result.last(2), noah2 end + Employee = Struct.new(:name, :role, keyword_init: true) + + def test_put_with_enum + employees = [ + dana = Employee.new(name: "Dana", role: :director), + abe = Employee.new(name: "Abe", role: :staff), + cy = Employee.new(name: "Cy", role: :staff) + ] + + result = employees.sort_by { |employee| + [ + Put.enum(employee.role, order: [:staff, :manager, :director]), + Put.asc(employee.name) + ] + } + + assert_equal [abe, cy, dana], result + end + Bot = Struct.new(:model, :age, keyword_init: true) def test_debug