Sort specific items of an array first - ruby-on-rails

I have a ruby array that looks something like this:
my_array = ['mushroom', 'beef', 'fish', 'chicken', 'tofu', 'lamb']
I want to sort the array so that 'chicken' and 'beef' are the first two items, then the remaining items are sorted alphabetically. How would I go about doing this?

irb> my_array.sort_by { |e| [ e == 'chicken' ? 0 : e == 'beef' ? 1 : 2, e ] }
#=> ["chicken", "beef", "fish", "lamb", "mushroom", "tofu"]
This will create a sorting key for each element of the array, and then sort the array elements by their sorting keys. Since the sorting key is an array, it compares by position, so [0, 'chicken'] < [1, 'beef'] < [2, 'apple' ] < [2, 'banana'].
If you don't know what elements you wanted sorted to the front until runtime, you can still use this trick:
irb> promotables = [ 'chicken', 'beef' ]
#=> [ 'chicken', 'beef' ]
irb> my_array.sort_by { |e| [ promotables.index(e) || promotables.size, e ] }
#=> ["chicken", "beef", "fish", "lamb", "mushroom", "tofu"]
irb> promotables = [ 'tofu', 'mushroom' ]
#=> [ 'tofu', 'mushroom' ]
irb> my_array.sort_by { |e| [ promotables.index(e) || promotables.size, e ] }
#=> [ "tofu", "mushroom", "beef", "chicken", "fish", "lamb"]

Mine's a lot more generic and more useful if you get your data only at runtime.
my_array = ['mushroom', 'beef', 'fish', 'chicken', 'tofu', 'lamb']
starters = ['chicken', 'beef']
starters + (my_array.sort - starters)
# => ["chicken", "beef" "fish", "lamb", "mushroom", "tofu"]

Could just do
firsts = ["chicken", "beef"]
[*firsts, *(my_array.sort - firsts)]
#=> ["chicken", "beef", "fish", "lamb", "mushroom", "tofu"]

Related

Efficient way to subtract arrays and get index of resulting subarray

Lets say I have the following arrays:
arr1 = [
['a', 'b'],
['c', 'd'],
['e', 'f']
]
arr2 = [
['g', 'h'],
['i', 'k'],
['a', 'b']
]
I want to find the elements in arr1 that do not exist in arr2 and the index of the elements in arr1
I can do this with the following but this isn't very efficient and is not a pretty solution. There can also be many elements in the arrays so this does not scale. Is there a better way to do this?
diff = arr1 - arr2
diff_with_index = diff.map { |x| { index: arr1.index(x), values: x } }
print diff_with_index
# [{:index=>1, :values=>["c", "d"]}, {:index=>2, :values=>["e", "f"]}]
When you have to do multiple include? checks, the most efficient way is to turn one of the lists into a set or hash beforehand, so you can have O(1) lookup time, so something like this:
require 'set'
arr2_set = Set.new(arr2)
arr1.each_index.select { |idx| !arr2_set.include?(arr1[idx]) }
Here is one way.
i1 = (0..arr1.size-1).to_a
#=> [0, 1, 2]
h1 = arr1.zip(i1).to_h
#=> {["a", "b"]=>0, ["c", "d"]=>1, ["e", "f"]=>2}
i1 - arr2.map { |a| h1[a] }
#=> [1, 2]
Note that
arr2.map { |a| h1[a] }
#=> [nil, nil, 0]

How to clone array of hashes and add key value using each loop

I want to clone an array of hashes and then to clone it into more than one.
irb(main):001:0> arr = [{a: "one", b: "two"}, {a: "uno", b: "due"}, {a: "en", b: "to"}]
=> [{:a=>"one", :b=>"two"}, {:a=>"uno", :b=>"due"}, {:a=>"en", :b=>"to"}]
irb(main):002:0> arr_1 = arr.clone
=> [{:a=>"one", :b=>"two"}, {:a=>"uno", :b=>"due"}, {:a=>"en", :b=>"to"}]
irb(main):003:0> arr_2 = arr.clone
=> [{:a=>"one", :b=>"two"}, {:a=>"uno", :b=>"due"}, {:a=>"en", :b=>"to"}]
Dynamically I want to add id into the hashes.
irb(main):004:0> arr_1.each { |k| k[:id] = 1 }
=> [{:a=>"one", :b=>"two", :id=>1}, {:a=>"uno", :b=>"due", :id=>1}, {:a=>"en", :b=>"to", :id=>1}]
irb(main):005:0> arr_2.each { |k| k[:id] = 2 }
=> [{:a=>"one", :b=>"two", :id=>2}, {:a=>"uno", :b=>"due", :id=>2}, {:a=>"en", :b=>"to", :id=>2}]
But the result of arr_1's id is affected by arr_2 each loop operation which is become 2
irb(main):006:0> arr_1
=> [{:a=>"one", :b=>"two", :id=>2}, {:a=>"uno", :b=>"due", :id=>2}, {:a=>"en", :b=>"to", :id=>2}]
I have tried by using
arr_1 = arr
arr_2 = arr
but the result keeps showing the same result.
How to make the arr_1 hashes :id = 1 and arr_2 hashes :id = 2 ?
Let's see what is happening.
arr = [{a: "cat", b: "dog"}, {a: "uno", b: "due"}]
arr.object_id
#=> 4557280
arr1 = arr
arr1.object_id
#=> 4557280
As you see, the variables arr and arr1 hold the same object, because the objects have the same object id.1 Therefore, if that object is modified, arr and arr1 will still both hold that object. Let's try it.
arr[0] = {a: "cat", b: "dog"}
arr
#=> [{:a=>"cat", :b=>"dog"}, {:a=>"uno", :b=>"due"}]
arr.object_id
#=> 4557280
arr1
#=> [{:a=>"cat", :b=>"dog"}, {:a=>"uno", :b=>"due"}]
arr1.object_id
#=> 4557280
If we want to be able to modify arr in this way without it affecting arr1, we use the method Kernel#dup.
arr
#=> [{:a=>"cat", :b=>"dog"}, {:a=>"uno", :b=>"due"}]
arr1 = arr.dup
#=> [{:a=>"cat", :b=>"dog"}, {:a=>"uno", :b=>"due"}]
arr.object_id
#=> 4557280
arr1.object_id
#=> 3693480
arr.map(&:object_id)
#=> [2631980, 4557300]
arr1.map(&:object_id)
#=> [2631980, 4557300]
As you see, arr and arr1 now hold different objects. Those objects, however, are arrays whose corresponding elements (hashes) are the same objects. Let's modify one of arr's elements.
arr[1][:a] = "owl"
arr
#=> [{:a=>"cat", :b=>"dog"}, {:a=>"owl", :b=>"due"}]
arr.map(&:object_id)
#=> [2631980, 4557300]
arr still contains the same objects, but we have modified one. Let's look at arr1.
arr1
#=> [{:a=>"cat", :b=>"dog"}, {:a=>"owl", :b=>"due"}]
arr1.map(&:object_id)
#=> [2631980, 4557300]
Should we be surprised that arr1 has changed as well?
We need to dup both arr and the elements of arr.
arr = [{a: "one", b: "two"}, {a: "uno", b: "due"}]
arr1 = arr.dup.map(&:dup)
#=> [{:a=>"one", :b=>"two"}, {:a=>"uno", :b=>"due"}]
arr.object_id
#=> 4149120
arr1.object_id
#=> 4182360
arr.map(&:object_id)
#=> [4149200, 4149140]
arr1.map(&:object_id)
#=> [4182340, 4182280]
Now arr and arr1 are different objects and they contain different (hash) objects, so any change to one will not affect the other. (Try it.)
Now suppose arr were as follows.
arr = [{a: "cat", b: [1,2]}]
Let's make the copy.
arr1 = arr.dup.map(&:dup)
#=> [{:a=>"cat", :b=>[1, 2]}]
Now modify arr[0][:b].
arr[0][:b] << 3
#=> [{:a=>"cat", :b=>[1, 2, 3]}]
arr1
#=> [{:a=>"cat", :b=>[1, 2, 3]}]
Drat! arr1 changed. We can again look at object ids to see why that happened.
arr.object_id
#=> 4488500
arr1.object_id
#=> 4503140
arr.map(&:object_id)
#=> [4488520]
arr1.map(&:object_id)
#=> [4503100]
arr[0][:b].object_id
#=> 4488560
arr1[0][:b].object_id
#=> 4488560
We see that arr and arr1 are different objects and there respective hashes are the same elements, but the array is the same object for both hashes. We therefore need to do something like this:
arr1[0][:b] = arr[0][:b].dup
but that's still not good enough if arr were:
arr = [{a: "cat", b: [1,[2,3]]}]
What we need is a method that will make a deep copy. A common solution for that is to use the methods Marshal::dump and Marshal::load.
arr = [{a: "cat", b: [1,2]}]
str = Marshal.dump(arr)
#=> "\x04\b[\x06{\a:\x06aI\"\bcat\x06:\x06ET:\x06b[\ai\x06i\a"
arr1 = Marshal.load(str)
#=> [{:a=>"cat", :b=>[1, 2]}]
arr[0][:b] << 3
#=> [{:a=>"cat", :b=>[1, 2, 3]}]
arr
#=> [{:a=>"cat", :b=>[1, 2, 3]}]
arr1
#=> [{:a=>"cat", :b=>[1, 2]}]
Note we could write:
arr1 = Marshal.load(Marshal.dump(arr))
As explained in the doc, the serialization used by the Marshal methods is not necessarily the same for different Ruby versions. If, for example, dump were used to produce a string that was saved to file and later load was invoked on the contents of the file, using a different version of Ruby, the contents may not be readable. Of course that's not a problem in this application of the methods.
1. To make it easier to see differences in object id's I've only shown the last seven digits. They in all cases are preceded by the digits 4877798.

Find index of array in multidimensional array by string value

I need the index of an array in a multidimensional array if it contains a unique string.
array:
[
{:id=>5, :name=>"Leaf Green", :hex_value=>"047115"},
{:id=>15, :name=>"Lemon Yellow", :hex_value=>"FFF600"},
{:id=>16, :name=>"Navy", :hex_value=>"285974"}
]
If hex_value of 'FFF600' exists, return the arrays position, which in this case would be 1.
This is where I am at, but it's returning [].
index = array.each_index.select{|i| array[i] == '#FFF600'}
That's returning nil, because there's no element i (index) in the array with value #FFF600 (nor FFF600), you need to access to the hex_value key value:
p [
{:id=>5, :name=>"Leaf Green", :hex_value=>"047115"},
{:id=>15, :name=>"Lemon Yellow", :hex_value=>"FFF600"},
{:id=>16, :name=>"Navy", :hex_value=>"285974"}
].yield_self { |this| this.each_index.select { |index| this[index][:hex_value] == 'FFF600' } }
# [1]
Giving you [1], because of using select, if you want just the first occurrence, you can use find instead.
I'm using yield_self there, to avoid assigning the array to a variable. Which is equivalent to:
array = [
{:id=>5, :name=>"Leaf Green", :hex_value=>"047115"},
{:id=>15, :name=>"Lemon Yellow", :hex_value=>"FFF600"},
{:id=>16, :name=>"Navy", :hex_value=>"285974"}
]
p array.each_index.select { |index| array[index][:hex_value] == 'FFF600' }
# [1]
Being Ruby, you can use the method for that: Enumerable#find_index
p [
{:id=>5, :name=>"Leaf Green", :hex_value=>"047115"},
{:id=>15, :name=>"Lemon Yellow", :hex_value=>"FFF600"},
{:id=>16, :name=>"Navy", :hex_value=>"285974"}
].find_index { |hash| hash[:hex_value] == 'FFF600' }
# 1

How to expand elements of an array into sub-arrays?

I have a huge array
huge = 1000
huge_array = (1..huge).to_a
How to best "expand" this array so that each element becomes a sub-array of format [original_element, "default value"], preferably in a memory-friendly way (without an explicit #map loop?)
expanded_huge_array = huge_array.some_magic
#=> [[1, "default value"],[2, "default value"], ... [1000, "default value"]]
huge_array.zip(['default value'] * huge_array.size)
BTW, you might simulate this behaviour with Hash with default:
arr = Hash.new { |h, key| huge_array.include?(key) ? [key, 'default value'] : nil }
arr[1]
#⇒ [1, 'default value']
arr[10000]
#⇒ nil
Try Array#product:
Returns an array of all combinations of elements from all arrays.
>> [1,2,3].product(["a"])
=> [[1, "a"], [2, "a"], [3, "a"]]

How can you sort an array in Ruby starting at a specific letter, say letter f?

I have a text array.
text_array = ["bob", "alice", "dave", "carol", "frank", "eve", "jordan", "isaac", "harry", "george"]
text_array = text_array.sort would give us a sorted array.
However, I want a sorted array with f as the first letter for our order, and e as the last.
So the end result should be...
text_array = ["frank", "george", "harry", "isaac", "jordan", "alice", "bob", "carol", "dave", "eve"]
What would be the best way to accomplish this?
Try this:
result = (text_array.select{ |v| v =~ /^[f-z]/ }.sort + text_array.select{ |v| v =~ /^[a-e]/ }.sort).flatten
It's not the prettiest but it will get the job done.
Edit per comment. Making a more general piece of code:
before = []
after = []
text_array.sort.each do |t|
if t > term
after << t
else
before << t
end
end
return (after + before).flatten
This code assumes that term is whatever you want to divide the array. And if an array value equals term, it will be at the end.
You can do that using a hash:
alpha = ('a'..'z').to_a
#=> ["a", "b", "c",..."x", "y", "z"]
reordered = alpha.rotate(5)
#=> ["f", "g",..."z", "a",...,"e"]
h = reordered.zip(alpha).to_h
# => {"f"=>"a", "g"=>"b",..., "z"=>"u", "a"=>"v",..., e"=>"z"}
text_array.sort_by { |w| w.gsub(/./,h) }
#=> ["frank", "george", "harry", "isaac", "jordan",
# "alice", "bob", "carol", "dave", "eve"]
A variant of this is:
a_to_z = alpha.join
#=> "abcdefghijklmnopqrstuvwxyz"
f_to_e = reordered.join
#=> "fghijklmnopqrstuvwxyzabcde"
text_array.sort_by { |w| w.tr(f_to_e, a_to_z) }
#=> ["frank", "george", "harry", "isaac", "jordan",
# "alice", "bob", "carol", "dave", "eve"]
I think the easiest would be to rotate the sorted array:
text_array.rotate(offset) if offset = text_array.find_index { |e| e.size > 0 and e[0] == 'f' }
Combining Ryan K's answer and my previous answer, this is a one-liner you can use without any regex:
text_array = text_array.sort!.select {|x| x.first >= "f"} + text_array.select {|x| x.first < "f"}
If I got your question right, it looks like you want to create sorted list with biased predefined patterns.
ie. let's say you want to define specific pattern of text which can completely change the sorting sequence for the array element.
Here is my proposal, you can get better code out of this, but my tired brain got it for now -
an_array = ["bob", "alice", "dave", "carol", "frank", "eve", "jordan", "isaac", "harry", "george"]
# Define your patterns with scores so that the sorting result can vary accordingly
# It's full fledged Regex so you can put any kind of regex you want.
patterns = {
/^f/ => 100,
/^e/ => -100,
/^g/ => 60,
/^j/ => 40
}
# Sort the array with our preferred sequence
sorted_array = an_array.sort do |left, right|
# Find score for the left string
left_score = patterns.find{ |p, s| left.match(p) }
left_score = left_score ? left_score.last : 0
# Find the score for the right string
right_score = patterns.find{ |p, s| right.match(p) }
right_score = right_score ? right_score.last : 0
# Create the comparision score to prepare the right order
# 1 means replace with right and -1 means replace with left
# and 0 means remain unchanged
score = if right_score > left_score
1
elsif left_score > right_score
-1
else
0
end
# For debugging purpose, I added few verbose data
puts "L#{left_score}, R:#{right_score}: #{left}, #{right} => #{score}"
score
end
# Original array
puts an_array.join(', ')
# Biased array
puts sorted_array.join(', ')

Resources