I have an array of hashes as in example:
[{:number=>2131, :owner=>"Mark"},
{:number=>223, :owner=>"Mark"},
{:number=>546, :owner=>"Mark"},
{:number=>765454, :owner=>"Tom"},
{:number=>845378, :owner=>"Jack"},
{:number=>75, :owner=>"Jack"},
{:number=>2342, :owner=>"Jack"}]
How can I output number values of the owners sorted line by line to get this:
# ["Jack", "Mark", "Tom"]
75 223 765454
2342 546 -
845378 2131 -
Each column with sorted numbers belongs to an owner, each row contains numbers from each owner.
What about
a = [{:number=>2131, :owner=>"Mark"},
{:number=>223, :owner=>"Mark"},
{:number=>546, :owner=>"Mark"},
{:number=>765454, :owner=>"Tom"},
{:number=>845378, :owner=>"Jack"},
{:number=>75, :owner=>"Jack"},
{:number=>2342, :owner=>"Jack"}]
a1 = a.group_by { |h| h[:owner] }
#=> {"Mark"=>[{:number=>2131, :owner=>"Mark"}, {:number=>223, :owner=>"Mark"}, {:number=>546, :owner=>"Mark"}], "Tom"=>[{:number=>765454, :owner=>"Tom"}], "Jack"=>[{:number=>845378, :owner=>"Jack"}, {:number=>75, :owner=>"Jack"}, {:number=>2342, :owner=>"Jack"}]}
a2 = a1.map { |k, v| [k, v.sort_by { |v| v[:number] } ] }
#=> [["Mark", [{:number=>223, :owner=>"Mark"}, {:number=>546, :owner=>"Mark"}, {:number=>2131, :owner=>"Mark"}]], ["Tom", [{:number=>765454, :owner=>"Tom"}]], ["Jack", [{:number=>75, :owner=>"Jack"}, {:number=>2342, :owner=>"Jack"}, {:number=>845378, :owner=>"Jack"}]]]
a3 = a2.sort_by { |(v0)| v0 }.to_h
#=> {"Jack"=>[{:number=>75, :owner=>"Jack"}, {:number=>2342, :owner=>"Jack"}, {:number=>845378, :owner=>"Jack"}], "Mark"=>[{:number=>223, :owner=>"Mark"}, {:number=>546, :owner=>"Mark"}, {:number=>2131, :owner=>"Mark"}], "Tom"=>[{:number=>765454, :owner=>"Tom"}]}
max_values_size = a3.values.max_by { |v| v.size }.size
#=> 3
a4 = max_values_size.times.map do |i|
a3.keys.map do |k|
a3[k][i] ? a3[k][i][:number] : '-'
end
end
#=> [[75, 223, 765454], [2342, 546, "-"], [845378, 2131, "-"]]
a4.each { |v| puts v.join(' ') }
#=>
75 223 765454
2342 546 -
845378 2131 -
It should be quite self-explanatory, ask if unclear
Related
An array of hashes is given (10 elements at least):
arr = [{letter: "a", number: "1"}, {letter: "a", number: "3"}, {letter: "b", number: "4"}, {letter: "b", number: "1"}, ..., {letter: "e", number: "2"} ]
The task is to shuffle the array so that there are no adjacent elements with the same 'letter' value.
So, the result should be like the following:
[{letter: "c", number: "4"}, {letter: "a", number: "1"}, {letter: "e", number: "2"}, {letter: "b", number: "1"}, ..., {letter: "a", number: "3"} ]
What is the simplest way to do that?
=== UPDATE ===
The number of repeated letters in the array is precisely known - it's 20% of the array length.
So, the array looks like the following:
[
{letter: "a", number: "1"}, {letter: "a", number: "3"},
{letter: "b", number: "4"}, {letter: "b", number: "1"},
{letter: "c", number: "7"}, {letter: "c", number: "3"},
{letter: "d", number: "6"}, {letter: "d", number: "4"},
{letter: "e", number: "5"}, {letter: "e", number: "2"}
]
Or, its simplified version:
["a", "a", "b", "b", "c", "c", "d", "d", "e", "e"]
Or, for example, there is a simplified array containing 15 elements:
["a", "a", "a", "b", "b", "b", "c", "c", "c", "d", "d", "d", "e", "e", "e"]
The simplest way (without any random):
# Calculate letter frequency
freq = arr.group_by { |h| h[:letter] }.map { |k, v| [k, v.size] }.to_h
# Then check that the most frequent element occurs less that arr.size / 2
center = (arr.size + 1) / 2
if freq.values.max > center
# Impossible
end
# Sort array by frequency to have most frequent first.
sarr = arr.sort_by { |h| freq[h[:letter]] }.reverse
sarr[0..center-1].zip(sarr[center..-1]).flatten.compact
Your problem is a special case of this question. See my answer for the detailed explanation how this works.
We even don't need to sort by letter frequency. It's for corner cases like "abbcccc". We can solve them in another way:
# Works with correct data: most frequent letter occurs <= center times
def f(arr)
arr = arr.sort
center = (arr.size + 1) / 2
arr = arr[0..center-1].zip(arr[center..-1]).flatten.compact
double = (1..arr.size-1).find { |i| arr[i] == arr[i-1] }
double ? arr.rotate(double) : arr # fix for the corner cases
end
puts f(%w[a a a a b b c].shuffle).join
# ababaca
puts f(%w[a a b b b b c].shuffle).join
# bcbabab
puts f(%w[a b b c c c c].shuffle).join
# cacbcbc
The only non-linear part of the algorithm is arr.sort. But as you can see by the link above, we even don't need the sorting. We need letters counts, which could be found in linear time. Therefore, we can reduce the algorithm to O(n).
The number of repeated letters in the array is precisely known - it's 20% of the array length.
With this update, the algorithm is simplified to (as there are no corner cases):
sarr = arr.sort_by { |h| h[:letter] }
center = (arr.size + 1) / 2
sarr[0..center-1].zip(sarr[center..-1]).flatten.compact
The simple and maybe the less effective way could be the brute force.
So on a simplified version of the array, one can do:
ary = %w(a a c c b a s)
loop do
break if ary.shuffle!.slice_when { |a, b| a == b }.to_a.size == 1
end
Some check should be added to assure that a solution exists, to avoid infinite loop.
Other (better?) way is to shuffle then find the permutation (no infinite loop) which satisfy the condition:
ary.shuffle!
ary.permutation.find { |a| a.slice_when { |a, b| a == b }.to_a.size == 1 }
If a solution does not exist, it returns nil.
Run the the benchmark:
def looping
ary = %w(a a c c b a s)
loop do
break if ary.shuffle!.slice_when { |a, b| a == b }.to_a.size == 1
end
ary
end
def shuffle_permute
ary = %w(a a c c b a s)
ary.shuffle!
ary.permutation.lazy.find { |a| a.slice_when { |a, b| a == b }.to_a.size == 1 }
end
require 'benchmark'
n = 500
Benchmark.bm do |x|
x.report { looping }
x.report { shuffle_permute }
end
Code
def reorder(arr)
groups = arr.group_by { |h| h[:letter] }
return nil if 2 * groups.map { |_,v| v.size }.max > arr.size + 1
max_key = groups.max_by { |_,a| a.size }.first
letters = ([max_key] + (groups.keys - [max_key])).cycle
ordered = []
while ordered.size < arr.size
k = letters.next
ordered << groups[k].pop unless groups[k].empty?
end
ordered
end
nilis returned if it is not possible to rearrange the elements in such a way that g[:letter] != h[:letter] for all pairs of consecutive elements g and h.
Note that this method has near linear computational complexity, O(arr.size), "near" because hash lookups are not quite constant time.
If desired, one could call the method with arr randomized: reorder(arr.shuffle).
Example
arr = [
{ letter: "a" }, { letter: "e" }, { letter: "b" }, { letter: "b" },
{ letter: "e" }, { letter: "a" }, { letter: "a" }, { letter: "f" }
]
reorder(arr)
#=> [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
# {:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"a"}]
Proof
The assertion is that if the line
return nil if 2 * groups.map { |_,v| v.size }.max > arr.size + 1
were removed from the method the array returned by the method would have the property that for all pairs of successive elements, g, h, g[:letter] != h[:letter] if and only if
2 * groups.map { |_,v| v.size }.max <= arr.size + 1
The proof has two parts.
The above inequality holds if the method produces a valid array
Compute
max_key = groups.max_by { |_,a| a.size }.first
max_key_freq = groups.map { |_,v| v.size }.max
and assume a valid array is returned. There must be at least one element other than max_key between each successive value of max_key in that array. The number of elements of arr other than max_key must therefore be at least max_key_freq - 1, so that
max_key_freq + max_key_freq - 1 <= arr.size
Hence,
2 * max_key_freq <= arr.size + 1
which is the same as:
2 * groups.map { |_,v| v.size }.max <= arr.size + 1
The above inequality does not hold if the method produces an invalid array
Suppose ordered is returned and it contains successive elements g and h for which both g[:letter] and h[:letter] equal the same letter l.
Because of the way ordered is constructed:
groups[k] must be empty for all keys k in groups for which k != l;
f[:letter] must equal l for all elements of ordered following g (if there are any); and
l must be the first key enumerated by keys, which is a letter that appears with a frequency that is not less than that of any other letter. l has frequency groups.map { |_,v| v.size }.max.
If n = groups.keys.size there must be a non-negative integer k (loosely, the number of rounds of allocations for all keys of groups) such that the number of elements h of arr for which h[:letter] != l equals k*n and the number of elements h of arr for which h[:letter] == l is k*n + 2 + m, where m >= 0. The size of arr is therefore 2*k*n + 2 + m.
In that case,
2 * groups.map { |_,v| v.size }.max > arr.size + 1
-> 2 * (k*n + 2 + m) > (k*n + 2 + m + k*n) + 1
-> 2*k*n + 4 + 2*m > 2*k*n + 3 + m
-> (4-3) + m > 0
-> true
Explanation
For the example,
groups = arr.group_by { |h| h[:letter] }
#=> {"a"=>[{:letter=>"a"}, {:letter=>"a"}, {:letter=>"a"}],
# "e"=>[{:letter=>"e"}, {:letter=>"e"}],
# "b"=>[{:letter=>"b"}, {:letter=>"b"}],
# "f"=>[{:letter=>"f"}]}
The following tells us that a solution exists.
2 * groups.map { |_,v| v.size }.max > arr.size + 1
#=> 2 * [3, 2, 2, 1].max > 8 + 1
#=> 2 * 3 > 9
#=> 6 > 9
#=> false
Next create an enumerator letters.
max_key = groups.max_by { |_,a| a.size }.first
#=> "a"
letters = ([max_key] + (groups.keys - [max_key])).cycle
#=> #<Enumerator: ["a", "e", "b", "f"]:cycle>
The elements of letters are generated as follows.
letters.next #=> "a"
letters.next #=> "e"
letters.next #=> "b"
letters.next #=> "f"
letters.next #=> "a"
letters.next #=> "e"
... ad infinititum
See Array#cycle.
I can best explain the remaining calculations by salting the method with puts statements before running the method. Note that arr.size #=> 8.
def reorder(arr)
groups = arr.group_by { |h| h[:letter] }
puts "groups = #{groups}"
return nil if 2 * groups.map { |_,v| v.size }.max > arr.size + 1
max_key = groups.max_by { |_,a| a.size }.first
letters = ([max_key] + (groups.keys - [max_key])).cycle
ordered = []
while ordered.size < arr.size
puts "\nordered.size = #{ordered.size} < #{arr.size} = #{ordered.size < arr.size}"
k = letters.next
puts "k = #{k}"
puts "groups[#{k}].empty? = #{groups[k].empty?}"
ordered << groups[k].pop unless groups[k].empty?
puts "ordered = #{ordered}"
puts "groups = #{groups}"
end
ordered
end
reorder(arr)
#=> [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
# {:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"a"}]
The following is displayed.
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}, {:letter=>"e"}],
"b"=>[{:letter=>"b"}, {:letter=>"b"}],
"f"=>[{:letter=>"f"}]}
ordered.size = 0 < 8 = true
k = a
groups[a].empty? = false
ordered = [{:letter=>"a"}]
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}, {:letter=>"e"}],
"b"=>[{:letter=>"b"}, {:letter=>"b"}],
"f"=>[{:letter=>"f"}]}
ordered.size = 1 < 8 = true
k = e
groups[e].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}]
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}],
"b"=>[{:letter=>"b"}, {:letter=>"b"}],
"f"=>[{:letter=>"f"}]}
ordered.size = 2 < 8 = true
k = b
groups[b].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}]
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}],
"b"=>[{:letter=>"b"}],
"f"=>[{:letter=>"f"}]}
ordered.size = 3 < 8 = true
k = f
groups[f].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}]
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}], "b"=>[{:letter=>"b"}],
"f"=>[]}
ordered.size = 4 < 8 = true
k = a
groups[a].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}]
groups = {"a"=>[{:letter=>"a"}],
"e"=>[{:letter=>"e"}],
"b"=>[{:letter=>"b"}],
"f"=>[]}
ordered.size = 5 < 8 = true
k = e
groups[e].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}, {:letter=>"e"}]
groups = {"a"=>[{:letter=>"a"}],
"e"=>[],
"b"=>[{:letter=>"b"}],
"f"=>[]}
ordered.size = 6 < 8 = true
k = b
groups[b].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}]
groups = {"a"=>[{:letter=>"a"}], "e"=>[], "b"=>[], "f"=>[]}
ordered.size = 7 < 8 = true
k = f
groups[f].empty? = true
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}]
groups = {"a"=>[{:letter=>"a"}], "e"=>[], "b"=>[], "f"=>[]}
ordered.size = 7 < 8 = true
k = a
groups[a].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"a"}]
groups = {"a"=>[], "e"=>[], "b"=>[], "f"=>[]}
Refering to the revised question, if
arr = ["a", "a", "b", "b", "c", "c", "d", "d", "e", "e"]
one could simply write:
arr.each_slice(arr.index { |s| s != arr.first }.to_a.transpose.flatten
#=> ["a", "b", "c", "d", "e", "a", "b", "c", "d", "e"]
or
arr.each_slice(arr.count(arr.first)).to_a.transpose.flatten
This sounds a lot like backtracking.
I would build the "shuffled" array from left to right.
Assume that there are N elements in the array. Say that at some point during the algorithm, you have already the first k elements arranged to fulfil the condition.
Now you pick from the remaining (N-k) elements the first one, which you can append to your result array, without breaking the condition.
If you can find one, you repeat the process recursively, now having an result array of (k+1) elements.
If you can not find one, you return a failure indicator and let the caller (i.e. the previous recursion) try another choice.
I have an array of hashes:
arr_of_hsh =[
{minimum_quantity: "250", cost: "1.5600"},
{minimum_quantity: "500", cost: "1.3500"},
{minimum_quantity: "1000", cost: "1.1800"},
{minimum_quantity: "2500", cost: "1.0200"}
]
Given a certain amount, I'd like to get the hash whose value for :minimum_quantity is the closest to the amount among those less than or equal to the amount. For example, if the amount was 300, this hash would be returned:
{minimum_quantity: "250", cost: "1.5600"}
and if the amount was 1000:
{minimum_quantity: "1000", cost: "1.1800"}
Keeping your data structure the same I'd say this way
arr_of_hsh.select { |item| item[:minimum_quantity].to_i <= quantity }.
max_by { |item| item[:minimum_quantity].to_i }
Here is a single-pass solution.
def f(arr, qty)
h = arr.min_by do |h|
n = h[:minimum_quantity].to_i
n <= qty ? (qty-n) : Float::INFINITY
end
h[:minimum_quantity].to_i <= qty ? h : nil
end
(100..1100).step(100).each { |qty| puts "#{qty}: #{f(arr_of_hsh, qty)}" }
100: nil
200: nil
300: {:minimum_quantity=>"250", :cost=>"1.5600"}
400: {:minimum_quantity=>"250", :cost=>"1.5600"}
500: {:minimum_quantity=>"500", :cost=>"1.3500"}
600: {:minimum_quantity=>"500", :cost=>"1.3500"}
700: {:minimum_quantity=>"500", :cost=>"1.3500"}
800: {:minimum_quantity=>"500", :cost=>"1.3500"}
900: {:minimum_quantity=>"500", :cost=>"1.3500"}
1000: {:minimum_quantity=>"1000", :cost=>"1.1800"}
1100: {:minimum_quantity=>"1000", :cost=>"1.1800"}
If the method is to be called several times for a given arr_of_hsh, sort arr_of_hsh by decreasing value of arr_by_hsh[:minimum_quantity.to_i and then simply use Enumerable#find.
def f(arr, qty)
h = arr.find { |h| h[:minimum_quantity].to_i <= qty }
end
arr = arr_of_hsh.sort_by { |h| -h[:minimum_quantity].to_i }
#=> [{:minimum_quantity=>"2500", :cost=>"1.0200"},
# {:minimum_quantity=>"1000", :cost=>"1.1800"},
# {:minimum_quantity=>"500", :cost=>"1.3500"},
# {:minimum_quantity=>"250", :cost=>"1.5600"}]
(100..1100).step(100).each { |qty| puts "#{qty}: #{f(arr, qty)}" }
#=> (same as above)
I'd like to iterate over an entire array, starting from any position. I'm not sure if there's a way to achieve this easily in Ruby, and I couldn't find any examples in the Array or Enumerator docs.
array = [0, 1, 2, 3, 4]
array.each.starting_at(3) { |e| e }
#=> [3, 4, 0, 1, 2]
And also:
array.each.starting_at_reverse(3) { |e| e }
#=> [3, 2, 1, 0, 4]
You can use the rotate method for this. This method rotates the position of each element by n. So your examples can be done like this
array.rotate(3).each {|e| e }
and
array.reverse.rotate(1).each {|e| e}
Note: for the second method the parameter to rotate can be derived by finding the negative index of n. So for this the element at index 3 is at index -2 in a length 5 array.
You can do this with upto and downto Fixnum's methods:
array = [0, 1, 2, 3, 4]
last_index = array.size - 1
3.upto last_index do |i|
puts array[i]
end
# => 3, 4
last_index.downto 3 do |i|
puts array[i]
end
# => 4, 3
PS. as speed benchmark, iteration with rotation faster
array.rotate(3).each {|e| puts e}
benchmark:
require 'benchmark'
array = Array.new(10000000) { rand(1...9) }
last_index = array.size - 1
Benchmark.bm do |x|
x.report 'upto' do
10000.upto last_index do |index| a = array[index] + 1; end
end
x.report 'downto' do
last_index.downto 10000 do |index| a = array[index] + 1; end
end
x.report 'rotate' do
array.rotate(10000).each {|e| a = e + 1 }
end
end
# RESULTS:
# user system total real
# upto 0.680000 0.000000 0.680000 ( 0.681932)
# downto 0.680000 0.000000 0.680000 ( 0.679752)
# rotate 0.590000 0.040000 0.630000 ( 0.622901)
but, as memory benchmark, iteration by array indexes less memory hungry, especially on big array sizes:
require 'memory_profiler'
array = Array.new(10000000) { rand(1...9) }
last_index = array.size - 1
{
upto: -> { 10000.upto last_index do |index| a = array[index] + 1; end },
downto: -> { last_index.downto 10000 do |index| a = array[index] + 1; end },
rotate: -> { array.rotate(10000).each {|e| a = e + 1 } },
reverse_rotate: -> { array.reverse.rotate(10000).each {|e| a = e + 1 } }
}.each { |desc, code| puts "#{desc.to_s} => #{MemoryProfiler.report(&code).total_allocated_memsize.to_s}" }
# RESULTS (in bytes):
# upto => 0 # no additional memory allocation
# downto => 0 # no additional memory allocation
# rotate => 80000040 # implicitly copied array 1 time
# reverse_rotate => 160000080 # implicitly copied array 2 times
I have an array of names. Each letter gets a value of 1 to 26 for the alphabet. Then the letter is multiplied by its place in the list. I came up with the following algorithm:
score = 0
names.each_with_index do |name, index|
temp = 0
letters = name.to_s.scan(/(.)/)
letters.each do |letter|
temp += 1 if letter.to_s.match(/A/)
temp += 2 if letter.to_s.match(/B/)
temp += 3 if letter.to_s.match(/C/)
temp += 4 if letter.to_s.match(/D/)
temp += 5 if letter.to_s.match(/E/)
temp += 6 if letter.to_s.match(/F/)
temp += 7 if letter.to_s.match(/G/)
temp += 8 if letter.to_s.match(/H/)
temp += 9 if letter.to_s.match(/I/)
temp += 10 if letter.to_s.match(/J/)
temp += 11 if letter.to_s.match(/K/)
temp += 12 if letter.to_s.match(/L/)
temp += 13 if letter.to_s.match(/M/)
temp += 14 if letter.to_s.match(/N/)
temp += 15 if letter.to_s.match(/O/)
temp += 16 if letter.to_s.match(/P/)
temp += 17 if letter.to_s.match(/Q/)
temp += 18 if letter.to_s.match(/R/)
temp += 19 if letter.to_s.match(/S/)
temp += 20 if letter.to_s.match(/T/)
temp += 21 if letter.to_s.match(/U/)
temp += 22 if letter.to_s.match(/V/)
temp += 23 if letter.to_s.match(/W/)
temp += 24 if letter.to_s.match(/X/)
temp += 25 if letter.to_s.match(/Y/)
temp += 26 if letter.to_s.match(/Z/)
end
score += (index+1) * temp
end
puts score
This is quite slow code. I hope someone can explain me a better and faster way to accomplish this task.
This is how I would do it.
Assumptions
the only characters are upper- and lower-case letters and whitespace.
after converting all characters of each element of the array to lower case, you want the letter offsets from a for each word to be summed and then multiplied by the (base 1) position of the word in the array.
Code
def totals_by_name(names)
names.each.with_index(1).with_object({}) { |(name,i),tots|
tots[name] = i*(name.downcase.each_char.reduce(0) { |tot,c|
tot + c.ord - 'a'.ord + 1 }) }
end
def total(names)
totals_by_name(names).values.reduce(:+)
end
Example
names = %w{ Joanne Jackie Joe Jethro Jack Jill }
#=> ["Joanne", "Jackie", "Joe", "Jethro", "Jack", "Jill"]
total(names)
#=> 914
Explanation
For the method totals_by_name and the array names above:
e0 = names.each
#=> #<Enumerator: ["Joanne", "Jackie", "Joe", "Jethro", "Jack", "Jill"]:each>
We can see the values this enumerator will pass on by converting it to an array:
e0.to_a
#=> ["Joanne", "Jackie", "Joe", "Jethro", "Jack", "Jill"]
Continuing,
e1 = e0.with_index(1)
#=> #<Enumerator: #<Enumerator: ["Joanne", "Jackie", "Joe", "Jethro",
# "Jack", "Jill"]:each>:with_index(1)>
e1.to_a
#=> [["Joanne", 1], ["Jackie", 2], ["Joe", 3], ["Jethro", 4], ["Jack", 5], ["Jill", 6]]
e2 = e1.with_object({})
#=> #<Enumerator: #<Enumerator: #<Enumerator: ["Joanne", "Jackie", "Joe",
# "Jethro", "Jack", "Jill"]:each>:with_index(1)>:with_object({})>
e2.to_a
#=> [[["Joanne", 1], {}], [["Jackie", 2], {}], [["Joe", 3], {}],
# [["Jethro", 4], {}], [["Jack", 5], {}], [["Jill", 6], {}]]
We can think of e2 and e3 as compound enumerators.
Enumerator#each passes elements of e2 to the block and assigns values to the block variables. We can use Enumerator#next to see what happens:
(name,i),tots = e2.next
#=> [["Joanne", 1], {}]
name #=> "Joanne"
i #=> 1
tots #=> {}
The block calculation is:
e3 = name.downcase.each_char
#=> #<Enumerator: "joanne":each_char>
e3.to_a # have a look
#=> ["j", "o", "a", "n", "n", "e"]
e3.reduce(0) { |tot,c| tot + c.ord - 'a'.ord + 1 }
#=> 59
For
c = "j"
this block calculation is:
tot + c.ord - 'a'.ord + 1
#=> 0 + 106 - 97 + 1
#=> 10
Therefore:
tots[name] = i*(name.downcase.each_char.reduce(0) { |tot,c|
tot + c.ord - 'a'.ord + 1 })
#=> tots["Joanna"] = 1*(59)
tots
#=> {"Joanne"=>59}
Values for other names are calculated similarly. The method tots is straightforward.
I would most likely go with:
class String
def points
each_char.map.with_index do |char, index|
(char.downcase.ord - 96) * (index + 1)
end.inject(0, :+)
end
end
'Hello'.points #=> 177
Given these arrays, how do I remove three occurrences of a value while keeping the fourth or fifth in the array?
[1,5,1,1,1] # => [1,5]
[3,3,3,2,3] # => [3,2]
[3,4,5,3,3] # => [4,5]
[1,1,1,1,1] # => [1,1]
[1,2,2,4,5] # => [1,2,2,4,5]
Here's what I've tried:
array = [1,5,1,1,1]
top3 = array.select { |x| array.count(x) >= 3 }[0..2]
last2 = array - top3
This strategy (and similar) only seem to work when there are three duplicates but not four or five. Are there elegant solutions to this problem?
UPDATE: Thank you for your amazing answers. As a beginning rubyist I learned a lot just from analyzing each response. My question came from a Ruby Koan challenge for a dice program. Here's my complete solution implemented with Abdo's suggestion. I'm sure there are more efficient ways to implement the program :)
def score(dice)
a,b,c,d,e = dice
array = [a,b,c,d,e]
total = 0
triples = array.select {|x| array.count(x) >= 3}[0..2]
singles = array.group_by{|i| i}.values.map{ |a|
a.length > 2 ? a[0, a.length - 3] : a
}.inject([], :+)
# Calculate values for triples
# 1 * 3 = 1000pts
# 2 * 3 = 200pts
# 3 * 3 = 300pts
# 4 * 3 = 400pts
# 5 * 3 = 500pts
# 6 * 3 = 600pts
case triples[0]
when 1 then total += triples[0]*1000
when (2..6) then total += triples[0]*100
end
# Calculate values for singles:
# 1s = 100pts each
# 5s = 50pts each
singles.include? (1) ? singles.select {|x| x == 1 }.each {|x| total += x*100 } : total
singles.include? (5) ? singles.select {|x| x == 5 }.each {|x| total += x*10 } : total
return total
end
puts score([5,1,1, 5, 6]) # 300 points
puts score([]) # 0 points
puts score([1,1,1,5,1]) # 1150 points
puts score([2,3,4,6,2]) # 0 points
puts score([3,4,5,3,3]) # 350 points
puts score([1,5,1,2,4]) # 250 points
array = [1,5,1,1,1]
occurrence = {}
array.select do|a|
if(array.count(a) > 3)
occurrence[a] ||= []
occurrence[a] << a
occurrence[a].count > 3
else
true
end
end
PS: This solution preserves the order of the elements in the original array
Here's a faster solution when the size of the array is large:
(I avoid using count because it would loop through the array in an inner loop)
arr.inject({}) {
|h, i| h[i] ||= 0; h[i] += 1; h
}.collect_concat {|k,v| [k] * (v > 2 ? v - 3 : v) }
Here's the fruity comparison to the other working solutions:
arr = 1000.times.collect { rand(100) }.shuffle
require 'fruity'
compare do
vimsha {
occurrence = {};
arr.select do|a|
if(arr.count(a) > 3)
occurrence[a] ||= []
occurrence[a] << a
occurrence[a].count > 3
else
true
end
end
}
caryswoveland {
arr.uniq.reduce([]) {|a,e| a + [e]*((cnt=arr.count(e)) > 2 ? cnt-3 : cnt)}
}
aruprakshit {
num_to_del = arr.find { |e| arr.count(e) >= 3 }
if !num_to_del.nil?
3.times do
ind = arr.index { |e| e == num_to_del }
arr.delete_at(ind)
end
end
arr
}
# edited as suggested by #CarySwoveland
abdo {
arr.each_with_object(Hash.new {|h,k| h[k]=[]}) {|i,h| h[i] += 1
}.collect_concat { |k,v| [k] * (v > 2 ? v - 3 : v) }
}
broisatse {
arr.group_by{|i| i}.values.map{ |a|
a.length > 2 ? a[0, a.length - 3] : a
}.inject([], :+)
}
end
Here's the comparison result:
Running each test 64 times. Test will take about 48 seconds.
broisatse is faster than abdo by 30.000000000000004% ± 10.0%
abdo is faster than aruprakshit by 4x ± 1.0 (results differ: ...)
aruprakshit is similar to caryswoveland (results differ: ...)
caryswoveland is similar to vimsha (results differ: ...)
Note: I took #aruprakshit's code outside the method so we don't waste time in the method call itself.
When the array's size is increased further:
arr = 1000.times.collect { rand(1000) }.shuffle
we get:
abdo is faster than broisatse by 3x ± 1.0
broisatse is faster than aruprakshit by 6x ± 10.0
aruprakshit is faster than caryswoveland by 2x ± 1.0
caryswoveland is similar to vimsha
Another way, assuming order need not be preserved (which is consistent with a comment by the asker):
array = [1,2,4,1,2,1,2,1,1,4]
array.uniq.reduce([]) {|a,e| a + [e]*((cnt=array.count(e)) > 2 ? cnt-3 : cnt)}
#=> [1, 1, 4, 4]
Try something like:
a.group_by{|i| i}.values.map{|a| a[0, a.length % 3]}.inject([], :+)
This will remove all triplets from the array. If you want to remove only the first triplet, then do:
a.group_by{|i| i}.values.map{|a| a.length > 2 ? a[0, a.length - 3] : a }.inject([], :+)
Note: This might mess up the order of the array:
[1,2,1,2,3] #=> [1,1,2,2,3]
Let me know if you need to keep the order and, if so, which elements need to be removed if there are more than three, e.g. what should say: [1,1,2,1,1,] - [1,2] or [2,1]?
x.group_by{|i| i }.values.select{|a| a.size >= 3 }.each{|a| c=[3,a.size].min; x.delete_if{|e| a[0]==e && (c-=1)>=0 } }
It will remove the first [3,a.size].min occurrences of a[0] from the input x where a is, for example, [1,1,1,1] for x = [1,2,1,1,1]
I'd do as below :
def del_first_three(a)
num_to_del = a.find { |e| a.count(e) >= 3 }
return a if num_to_del.nil?
3.times do
ind = a.index { |e| e == num_to_del }
a.delete_at(ind)
end
a
end
del_first_three([3,4,5,3,3]) # => [4, 5]
del_first_three([1,5,1,1,1]) # => [5, 1]
del_first_three([1,2,2,4,5]) # => [1, 2, 2, 4, 5]