Class: Sortsmith::Sorter
- Inherits:
-
Object
- Object
- Sortsmith::Sorter
- Defined in:
- lib/sortsmith/sorter.rb
Overview
A chainable sorting interface that provides a fluent API for complex sorting operations.
The Sorter class allows you to build sorting pipelines by chaining extractors, modifiers, and ordering methods before executing the sort with a terminator method. This creates readable, expressive sorting code that handles edge cases gracefully.
Constant Summary collapse
- INDIFFERENT_KEYS_TRANSFORM =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Transformation proc for converting hash keys to symbols for indifferent access.
Used internally when the
indifferent: trueoption is specified in #dig. This enables consistent key lookup across hashes with mixed symbol/string keys. ->(item) { item.transform_keys(&:to_sym) }
- DELEGATED_METHODS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
List of enumerable methods that are delegated to the sorted result.
These methods allow seamless chaining from sorting operations to common array operations without breaking the fluent interface. Each delegated method executes the sort pipeline first, then applies the requested operation to the sorted result.
%i[first last take drop each map select [] size count length].freeze
Instance Method Summary collapse
-
#[](index) ⇒ Object, Array
Access elements by index in the sorted result.
-
#asc ⇒ Sorter
Sort in ascending order (default behavior).
-
#count {|Object| ... } ⇒ Integer
Count elements in the sorted result, optionally with a condition.
-
#desc ⇒ Sorter
Sort in descending order.
-
#dig(*identifiers, indifferent: false) ⇒ Sorter
(also: #key, #field)
Extract values from objects using hash keys or object methods.
-
#downcase ⇒ Sorter
(also: #insensitive, #case_insensitive)
Transform extracted values to lowercase for comparison.
-
#drop(n) ⇒ Array
Drop the first n elements from the sorted result.
-
#each {|Object| ... } ⇒ Array, Enumerator
Iterate over the sorted result.
-
#extract(field, *positional, **keyword) ⇒ Sorter
Universal extraction method that intelligently chooses the appropriate extraction strategy.
-
#first(n = 1) ⇒ Object, Array
Get the first n elements from the sorted result.
-
#initialize(input) ⇒ Sorter
constructor
Initialize a new Sorter instance.
-
#last(n = 1) ⇒ Object, Array
Get the last n elements from the sorted result.
-
#length ⇒ Integer
Get the number of elements in the sorted result (alias for size).
-
#map {|Object| ... } ⇒ Array, Enumerator
Transform each element of the sorted result.
-
#method(method_name, *positional, **keyword) ⇒ Sorter
(also: #attribute)
Extract values by calling methods on objects with optional arguments.
-
#nil_first ⇒ Sorter
Position nil values at the beginning of sort results.
-
#nil_last ⇒ Sorter
Position nil values at the end of sort results (explicit default).
-
#reverse ⇒ Array
Shorthand for adding desc and executing sort.
-
#reverse! ⇒ Array
Shorthand for adding desc and executing sort!.
-
#select {|Object| ... } ⇒ Array, Enumerator
Filter the sorted result.
-
#size ⇒ Integer
Get the number of elements in the sorted result.
-
#sort ⇒ Array
(also: #to_a)
Execute the sort pipeline and return a new sorted array.
-
#sort! ⇒ Array
(also: #to_a!)
Execute the sort pipeline and mutate the original array in place.
-
#take(n) ⇒ Array
Take the first n elements from the sorted result.
-
#upcase ⇒ Sorter
Transform extracted values to uppercase for comparison.
Constructor Details
#initialize(input) ⇒ Sorter
Initialize a new Sorter instance.
Creates a new chainable sorter for the given collection. Typically called
automatically when using collection.sort_by without a block.
98 99 100 101 102 103 104 |
# File 'lib/sortsmith/sorter.rb', line 98 def initialize(input) @input = input @extractors = [] @modifiers = [] @descending = false @nil_first = false end |
Instance Method Details
#[](index) ⇒ Object, Array
Access elements by index in the sorted result
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#asc ⇒ Sorter
Sort in ascending order (default behavior).
This is typically unnecessary as ascending is the default sort direction, but can be useful for explicit clarity or resetting direction after previous desc calls in a chain.
404 405 406 407 |
# File 'lib/sortsmith/sorter.rb', line 404 def asc @descending = false self end |
#count {|Object| ... } ⇒ Integer
Count elements in the sorted result, optionally with a condition
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#desc ⇒ Sorter
Sort in descending order.
Reverses the final sort order after all comparisons are complete. Can be chained with other modifiers and will apply to the final result.
425 426 427 428 |
# File 'lib/sortsmith/sorter.rb', line 425 def desc @descending = true self end |
#dig(*identifiers, indifferent: false) ⇒ Sorter Also known as: key, field
Extract values from objects using hash keys or object methods.
The workhorse method for value extraction. Works with hashes, structs, and any object that responds to the given identifiers. Supports nested digging with multiple arguments and handles mixed key types gracefully.
When extracting from objects that don't respond to the specified keys/methods, returns an empty string to preserve the original ordering rather than causing comparison errors.
150 151 152 153 154 155 156 157 158 |
# File 'lib/sortsmith/sorter.rb', line 150 def dig(*identifiers, indifferent: false) if indifferent identifiers = identifiers.map(&:to_sym) before_extract = INDIFFERENT_KEYS_TRANSFORM end @extractors << {method: :dig, positional: identifiers, before_extract:} self end |
#downcase ⇒ Sorter Also known as: insensitive, case_insensitive
Transform extracted values to lowercase for comparison.
Only affects values that respond to #downcase (typically strings). Non-string values are converted to strings first, ensuring consistent behavior across mixed data types.
329 330 331 332 |
# File 'lib/sortsmith/sorter.rb', line 329 def downcase @modifiers << {method: :downcase} self end |
#drop(n) ⇒ Array
Drop the first n elements from the sorted result
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#each {|Object| ... } ⇒ Array, Enumerator
Iterate over the sorted result
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#extract(field, *positional, **keyword) ⇒ Sorter
Universal extraction method that intelligently chooses the appropriate extraction strategy.
This method serves as the smart dispatcher for value extraction, automatically detecting
whether the input collection contains hash-like objects (that respond to dig) or
regular objects (that need method calls). It provides a unified interface regardless
of the underlying data structure.
When field is nil, returns self without adding any extractors to the pipeline,
allowing for graceful handling of dynamic field selection scenarios.
293 294 295 296 297 298 299 300 301 |
# File 'lib/sortsmith/sorter.rb', line 293 def extract(field, *positional, **keyword) return self if field.nil? if @input.first.respond_to?(:dig) dig(field, **keyword) else method(field, *positional, **keyword) end end |
#first(n = 1) ⇒ Object, Array
Get the first n elements from the sorted result
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#last(n = 1) ⇒ Object, Array
Get the last n elements from the sorted result
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#length ⇒ Integer
Get the number of elements in the sorted result (alias for size)
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#map {|Object| ... } ⇒ Array, Enumerator
Transform each element of the sorted result
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#method(method_name, *positional, **keyword) ⇒ Sorter Also known as: attribute
Extract values by calling methods on objects with optional arguments.
Enables chainable sorting by calling methods on each object in the collection. Supports method calls with both positional and keyword arguments. When objects don't respond to the specified method, returns an empty string to preserve original ordering.
This is particularly useful for custom objects, calculated values, or methods that require parameters.
229 230 231 232 |
# File 'lib/sortsmith/sorter.rb', line 229 def method(method_name, *positional, **keyword) @extractors << {method: method_name, positional:, keyword:} self end |
#nil_first ⇒ Sorter
Position nil values at the beginning of sort results.
By default, nil values sort last (matching SQL/database conventions). This modifier overrides that behavior to place nils first, regardless of sort direction (asc/desc).
The nil positioning is independent of asc/desc modifiers, meaning nil_first with desc will show nils first, then non-nil values in descending order.
463 464 465 466 |
# File 'lib/sortsmith/sorter.rb', line 463 def nil_first @nil_first = true self end |
#nil_last ⇒ Sorter
Position nil values at the end of sort results (explicit default).
This is the default behavior, but can be used for explicitness or to override a previous nil_first call in a chain. Nil values will appear last regardless of sort direction (asc/desc).
497 498 499 500 |
# File 'lib/sortsmith/sorter.rb', line 497 def nil_last @nil_first = false self end |
#reverse ⇒ Array
Shorthand for adding desc and executing sort.
Equivalent to calling .desc.sort but more concise and expressive.
Useful when you know you want descending order and don't need other
modifiers.
640 641 642 |
# File 'lib/sortsmith/sorter.rb', line 640 def reverse desc.sort end |
#reverse! ⇒ Array
Shorthand for adding desc and executing sort!.
Equivalent to calling .desc.sort! but more concise. Mutates the
original array and returns it in descending order.
655 656 657 |
# File 'lib/sortsmith/sorter.rb', line 655 def reverse! desc.sort! end |
#select {|Object| ... } ⇒ Array, Enumerator
Filter the sorted result
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#size ⇒ Integer
Get the number of elements in the sorted result
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#sort ⇒ Array Also known as: to_a
Execute the sort pipeline and return a new sorted array.
Applies all chained extraction, transformation, and ordering steps to produce the final sorted result. The original collection remains unchanged.
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 |
# File 'lib/sortsmith/sorter.rb', line 522 def sort # Schwartzian Transform: extract once (O(n)), sort (O(n log n)), map back (O(n)) # Step 1: Pair each item with its extracted sort value pairs = @input.map { |item| [extract_and_transform(item), item] } # Step 2: Partition nils so we can use C-level sort_by on non-nil values nil_pairs, non_nil_pairs = pairs.partition { |v, _| v.nil? } # Step 3: Sort non-nil values using Ruby's native sort_by (runs in C) begin non_nil_pairs.sort_by! { |v, _| v } rescue ArgumentError # Re-raise with a more helpful error message # Find the first pair of incomparable values for the message non_nil_pairs.each_cons(2) do |(val_a, _), (val_b, _)| result = val_a <=> val_b next unless result.nil? # <=> returned nil - incomparable types raise ArgumentError, <<~ERROR Cannot compare values during sort - the values are incomparable types. This usually means your extraction returned mixed types or you're missing an extraction method. Comparing: #{val_a.inspect} (#{val_a.class}) <=> #{val_b.inspect} (#{val_b.class}) ERROR rescue ArgumentError # <=> raised instead of returning nil raise ArgumentError, <<~ERROR Cannot compare values during sort - the <=> operator raised an exception. This usually means the class doesn't implement <=>, has a buggy implementation, or you're missing an extraction method. Comparing: #{val_a.inspect} (#{val_a.class}) <=> #{val_b.inspect} (#{val_b.class}) ERROR end end non_nil_pairs.reverse! if @descending # Step 4: Combine based on nil positioning result = @nil_first ? nil_pairs.concat(non_nil_pairs) : non_nil_pairs.concat(nil_pairs) # Step 5: Strip the sort values, keeping only the original items result.map! { |_, item| item } end |
#sort! ⇒ Array Also known as: to_a!
Execute the sort pipeline and mutate the original array in place.
Same as #sort but modifies the original array instead of creating a new one. Returns the mutated array for chaining. Use when memory efficiency is important and you don't need to preserve the original order.
603 604 605 606 |
# File 'lib/sortsmith/sorter.rb', line 603 def sort! sorted = sort @input.replace(sorted) end |
#take(n) ⇒ Array
Take the first n elements from the sorted result
740 741 742 743 744 |
# File 'lib/sortsmith/sorter.rb', line 740 DELEGATED_METHODS.each do |method_name| define_method(method_name) do |*args, &block| to_a.public_send(method_name, *args, &block) end end |
#upcase ⇒ Sorter
Transform extracted values to uppercase for comparison.
Only affects values that respond to #upcase (typically strings). Non-string values are converted to strings first, ensuring consistent behavior across mixed data types.
380 381 382 383 |
# File 'lib/sortsmith/sorter.rb', line 380 def upcase @modifiers << {method: :upcase} self end |