I have an array
and a hash
and what I want to achieve is to sort the hash
(with each id
inside each hash) based on the sort that is in the array and if the id
doesn't exist just don't need to sort them and should not be deleted.
my_array = [4, 2, 5, 3, 1]
hash = [
{"id" => 1, "field_name" => "foo"},
{"id" => 2, "field_name" => "bar"},
{"id" => 3, "field_name" => "abc"},
{"id" => 4, "field_name" => "zsh"},
{"id" => 5, "field_name" => "kql"},
{"id" => 6, "field_name" => "plo"},
{"id" => 7, "field_name" => "cde"}
]
Needed output
[
{"id" => 4, "field_name" => "zsh"},
{"id" => 2, "field_name" => "bar"},
{"id" => 5, "field_name" => "kql"},
{"id" => 3, "field_name" => "abc"},
{"id" => 1, "field_name" => "foo"},
{"id" => 6, "field_name" => "plo"},
{"id" => 7, "field_name" => "cde"}
]
Appreciate any help and thanks in advance!
CodePudding user response:
Map Over Your ID Array to Select Matching Hashes in Order
While there may be a more elegant way to do this, I think the solution below is quite clear. It uses your Array of id elements as an Enumerable object to map over, returning the matching Hash object with a matching value for each "id"
key encountered during iteration (if found), and then removes any elements where no match was found, e.g. the Array returned by #map returns nil
.
Using Ruby 3.1.2:
id_order = [4, 2, 5, 3, 1]
array_of_hashes = [
{"id" => 1, "field_name" => "foo"},
{"id" => 2, "field_name" => "bar"},
{"id" => 3, "field_name" => "abc"},
{"id" => 4, "field_name" => "zsh"},
{"id" => 5, "field_name" => "kql"},
{"id" => 6, "field_name" => "plo"},
{"id" => 7, "field_name" => "cde"}
]
id_order.map { |id| array_of_hashes.detect { |h| h["id"] == id } }.compact
This correctly returns your ordered results as specified in the first Array:
#=>
[{"id"=>4, "field_name"=>"zsh"},
{"id"=>2, "field_name"=>"bar"},
{"id"=>5, "field_name"=>"kql"},
{"id"=>3, "field_name"=>"abc"},
{"id"=>1, "field_name"=>"foo"}]
Note that it doesn't return Hash objects for IDs 6
or 7
because they aren't present in your Array of IDs to search for. You can adjust the #map to treat those as pass-throughs (although you haven't explained how or why they should appear at the end of your resulting Array) or add them to the Array of elements to search for, which would be the preferred approach. However, since they weren't addressed in your original post, this is in fact the correct output if no matching ID is found.
CodePudding user response:
my_array = [4, 7, 5, 3, 1]
arr = [
{"id" => 1, "field_name" => "foo"},
{"id" => 2, "field_name" => "bar"},
{"id" => 3, "field_name" => "abc"},
{"id" => 4, "field_name" => "zsh"},
{"id" => 5, "field_name" => "kql"},
{"id" => 6, "field_name" => "plo"},
{"id" => 7, "field_name" => "cde"}
]
I have assumed that each element h
of arr
for which my_array.include?(h["id"]) #=> false
is to remain in its current position in arr
. I've changed my_array
to make that more clear for the example. I have also renamed hash
to arr
, as the former is perhaps not the best name for an array.
id_to_idx = arr.each_with_index.with_object({}) { |(g,i),h| h[g["id"]] = i }
#=> { 1=>0, 2=>1, 3=>2, 5=>4, 6=>5, 7=>6 }
id_to_idx.values_at(*my_array).sort.each_with_index.with_object(arr.dup) do |(idx,i),a|
a[idx] = arr[id_to_idx[my_array[i]]]
end
#=> [
# {"id" => 4, "field_name" => "zsh"},
# {"id" => 2, "field_name" => "bar"},
# {"id" => 7, "field_name" => "cde"}
# {"id" => 5, "field_name" => "kql"},
# {"id" => 3, "field_name" => "abc"},
# {"id" => 2, "field_name" => "bar"},
# {"id" => 1, "field_name" => "foo"}
# ]
(Explanation to follow.)