Projecting an N-dimensional array to 1-d - ruby

I have an n-dimensional array I'd like to display in a table. Something like this:
#data = [[1,2,3],[4,5,6],[7,8,9]]
#dimensions = [{:name => "speed", :values => [0..20,20..40,40..60]},
{:name => "distance", :values => [0..50, 50..100, 100..150]}]
And I'd like the table to end up looking like this:
speed | distance | count
0..20 | 0..50 | 1
0..20 | 50..100 | 2
0..20 | 100..150 | 3
20..40 | 0..50 | 4
20..40 | 50..100 | 5
20..40 | 100..150 | 6
40..60 | 0..50 | 7
40..60 | 50..100 | 8
40..60 | 100..150 | 9
Is there a pretty way to pull this off? I have a working solution that I'm actually kind of proud of; this post is a bit humble-brag. However, it does feel overly complicated, and there's no way I or anyone else is going to understand what's going on later.
[nil].product(*#dimensions.map do |d|
(0...d[:values].size).to_a
end).map(&:compact).map(&:flatten).each do |data_idxs|
row = data_idxs.each_with_index.map{|data_idx, dim_idx|
#dimensions[dim_idx][:values][data_idx]
}
row << data_idxs.inject(#data){|data, idx| data[idx]}
puts row.join(" |\t ")
end

What about this?
first, *rest = #dimensions.map {|d| d[:values]}
puts first
.product(*rest)
.transpose
.push(#data.flatten)
.transpose
.map {|row| row.map {|cell| cell.to_s.ljust 10}.join '|' }
.join("\n")

Bent, let me first offer a few comments on your solution. (Then I will offer an alternative approach that also uses Array#product.) Here is your code, formatted to expose the structure:
[nil].product(*#dimensions.map { |d| (0...d[:values].size).to_a })
.map(&:compact)
.map(&:flatten)
.each do |data_idxs|
row = data_idxs.each_with_index.map
{ |data_idx, dim_idx| #dimensions[dim_idx][:values][data_idx] }
row << data_idxs.inject(#data) { |data, idx| data[idx] }
puts row.join(" |\t ")
end
I find it very confusing, in part because of your reluctance to define intermediate variables. I would first compute product's argument and assign it to a variable x. I say x because it's hard to come up with a good name for it. I would then assign the results of product to another variable, like so: y = x.shift.product(x) or (if you don't want x modified) y = x.first.product(x[1..-1). This avoids the need for compact and flatten.
I find the choice of variable names confusing. The root of the problem is that #dimensions and #data both begin with d! This problem would be diminished greatly if you simply used, say, #vals instead of #data.
It would be more idiomatic to write data_idxs.each_with_index.map as data_idxs.map.with_index.
Lastly, but most important, is your decision to use indices rather than the values themselves. Don't do that. Just don't do that. Not only is this unnecessary, but it makes your code so complex that figuring it out is time-consuming and headache-producing.
Consider how easy it is to manipulate the data without any reference to indices:
vals = #dimensions.map {|h| h.values }
# [["speed", [0..20, 20..40, 40..60 ],
# ["distance", [0..50, 50..100, 100..150]]
attributes = vals.map(&:shift)
# ["speed", "distance"]
# vals => [[[0..20, 20..40, 40..60]],[[0..50, 50..100, 100..150]]]
vals = vals.flatten(1).map {|a| a.map(&:to_s)}
# [["0..20", "20..40", "40..60"],["0..50", "50..100", "100..150"]]
rows = vals.first.product(*vals[1..-1]).zip(#data.flatten).map { |a,d| a << d }
# [["0..20", "0..50", 1],["0..20", "50..100", 2],["0..20", "100..150", 3],
# ["20..40", "0..50", 4],["20..40", "50..100", 5],["20..40", "100..150", 6],
# ["40..60", "0..50", 7],["40..60", "50..100", 8],["40..60", "100..150", 9]]
I would address the problem in such a way that you could have any number of attributes (i.e., "speed", "distance",...) and the formatting would dictated by the data:
V_DIVIDER = ' | '
COUNT = 'count'
attributes = #dimensions.map {|h| h[:name]}
sd = #dimensions.map { |h| h[:values].map(&:to_s) }
fmt = sd.zip(attributes)
.map(&:flatten)
.map {|a| a.map(&:size)}
.map {|a| "%-#{a.max}s" }
attributes.zip(fmt).each { |a,f| print f % a + V_DIVIDER }
puts COUNT
prod = (sd.shift).product(*sd)
flat_data = #data.flatten
until flat_data.empty? do
prod.shift.zip(fmt).each { |d,f| print f % d + V_DIVIDER }
puts (flat_data.shift)
end
If
#dimensions = [{:name => "speed", :values => [0..20,20..40,40..60] },
{:name => "volume", :values => [0..30, 30..100, 100..1000]},
{:name => "distance", :values => [0..50, 50..100, 100..150] }]
this is displayed:
speed | volume | distance | count
0..20 | 0..30 | 0..50 | 1
0..20 | 0..30 | 50..100 | 2
0..20 | 0..30 | 100..150 | 3
0..20 | 30..100 | 0..50 | 4
0..20 | 30..100 | 50..100 | 5
0..20 | 30..100 | 100..150 | 6
0..20 | 100..1000 | 0..50 | 7
0..20 | 100..1000 | 50..100 | 8
0..20 | 100..1000 | 100..150 | 9
It works as follows (with the original value of #dimensions, having just the two attributes, "speed" and "distance"):
Attributes is a list of the attributes. Being an array, it maintains their order:
attributes = #dimensions.map {|h| h[:name]}
# => ["speed", "distance"]
We pull out the ranges from #dimensions and convert them to strings:
sd = #dimensions.map { |h| h[:values].map(&:to_s) }
# => [["0..20", "20..40", "40..60"], ["0..50", "50..100", "100..150"]]
Next we compute the string formating for all columns but the last:
fmt = sd.zip(attributes)
.map(&:flatten)
.map {|a| a.map(&:size)}
.map {|a| "%-#{a.max}s" }
# => ["%-6s", "%-8s"]
Here
sd.zip(attributes)
# => [[["0..20", "20..40", "40..60"], "speed" ],
# [["0..50", "50..100", "100..150"], "distance"]]
8 in "%-8s" equals the maximum of the length of the column label, distance (8) and the length of the longest string representation of a distance range (also 8, for "100..150"). The - in the formatting string left-adjusts the strings.
We can now print the header:
attributes.zip(fmt).each { |a,f| print f % a + V_DIVIDER }
puts COUNT
speed | distance | count
To print the remaining lines, we construct an array containing the contents of the first two columns. Each element of the array corresponds to a row of the table:
prod = (sd.shift).product(*sd)
# => ["0..20", "20..40", "40..60"].product(*[["0..50", "50..100", "100..150"]])
# => ["0..20", "20..40", "40..60"].product(["0..50", "50..100", "100..150"])
# => [["0..20", "0..50"], ["0..20", "50..100"], ["0..20", "100..150"],
# ["20..40", "0..50"], ["20..40", "50..100"], ["20..40", "100..150"],
# ["40..60", "0..50"], ["40..60", "50..100"], ["40..60", "100..150"]]
We need to flaten #data:
flat_data = #data.flatten
# => [1, 2, 3, 4, 5, 6, 7, 8, 9]
The first time through the until do loop,
r1 = prod.shift
# => ["0..20", "0..50"]
# prod now => [["0..20", "50..100"],...,["40..60", "100..150"]]
r2 = r1.zip(fmt)
# => [["0..20", "%-6s"], ["0..50", "%-8s"]]
r2.each { |d,f| print f % d + V_DIVIDER }
0..20 | 0..50 |
puts (flat_data.shift)
0..20 | 0..50 | 1
# flat_data now => [2, 3, 4, 5, 6, 7, 8, 9]

Related

Minimax algorithm in Ruby in Object-Oriented way

I am trying to implement minimax algorithm for a tic tac toe game in an object-oriented way. I was a bit stuck with how to deal with reverting the state for my board object once the algorithm determined the best move. When running the program, I have noticed that minimax method operated on the current board object which is not ideal.
I added a method to undo the move done by the minimax method: board.[]=(empty_square, Square::INITIAL_MARKER)
I have noticed the algorithm makes the wrong choice. Here, X is a player and O is a computer. If this is the state of the board:
| |
| |
| |
-----+-----+-----
| |
| X |
| |
-----+-----+-----
| |
| | O
| |
When the player X makes a move and picks square 2, minimax (computer, O) will choose 7 instead of 8 which would be a better choice:
| |
| X |
| |
-----+-----+-----
| |
| X |
| |
-----+-----+-----
| |
O | | O
| |
Due to my inexperience, I am a little bit lost on how to proceed and would appreciate any guidance!
Here is the minimax method:
def minimax
best_move = 0
score_current_move = nil
best_score = -10000 if #current_marker == COMPUTER_MARKER
best_score = 10000 if #current_marker == HUMAN_MARKER
board.unmarked_keys.each do |empty_square|
board.[]=(empty_square, #current_marker)
if board.full?
score_current_move = 0
elsif board.someone_won?
score_current_move = -1 if board.winning_marker == HUMAN_MARKER
score_current_move = 1 if board.winning_marker == COMPUTER_MARKER
else
alternate_player
score_current_move = minimax[0]
end
if ((#current_marker == COMPUTER_MARKER) && (score_current_move >= best_score))
best_score = score_current_move
best_move = empty_square
elsif ((#current_marker == HUMAN_MARKER) && (score_current_move <= best_score))
best_score = score_current_move
best_move = empty_square
end
board.[]=(empty_square, Square::INITIAL_MARKER)
end
[best_score, best_move]
end
I see no particular advantage here to defining any classes at all. There is only one board and only two players (the machine and the human) who operate quite differently.
Main method
Next I will write the main method, which depends on several helper methods, all of which could be private.
def play_game(human_moves_first = true)
raise ArgumentError unless [true, false].include?(human_moves_first)
human_marker, machine_marker =
human_moves_first ? ['X', 'O'] : ['O', 'X']
board = Array.new(9)
if human_moves_first
display(board)
human_to_move(board, 'X')
end
loop do
display(board)
play = machine_best_play(board, machine_marker)
board[play] = machine_marker
display(board)
if win?(board)
puts "Computer wins"
break
end
if tie?(board)
puts "Tie game"
break
end
human_to_move(board, human_marker)
if tie?(board)
puts "Tie game"
break
end
end
end
As you see I have provided a choice of who starts, the machine or the human.
Initially, board is an array of 9 nils.
The method simply loops until a determination is made as to whether the machine wins or there is a tie. As we know, the machine, acting logically, cannot lose. In each pass of the loop the machine makes a mark. If that results in a win or a tie the game is over; else the human is called upon to make a mark.
Before considering the method machine_best_play, let's consider a few simple helper method that are needed.
Simple helper methods
I will demonstrate these methods with board defined as follows:
board = ['X', 'O', 'X',
nil, 'O', nil,
nil, nil, 'X']
Note that while the human refers to the nine locations as 1 through 9, internally they they are represented as indices of board, 0 through 8.
Determine unmarked cells
def unmarked_cells(board)
board.each_index.select { |i| board[i].nil? }
end
unmarked_cells(board)
#=> [3, 5, 6, 7]
Ask human to make a selection
def human_to_move(board, marker)
loop do
puts "Please mark '#{marker}' in an unmarked cell"
cell = gets.chomp
if (n = Integer(cell, exception: false)) && n.between?(1, 9)
n -= 1 # convert to index in board
if board[n].nil?
board[n] = marker
break
else
puts "That cell is occupied"
end
else
puts "That is not a number between 1 and 9"
end
end
end
human_to_move(board, 'O')
Please mark an 'O' in an unmarked cell
If cell = gets.chomp #=> "6" then
board
#=> ["X", "O", "X", nil, "O", "O", nil, nil, "X"]
For the following I have set board to its original value above.
Display the board
def display(board)
board.each_slice(3).with_index do |row, idx|
puts " | |"
puts " #{row.map { |obj| obj || ' ' }.join(' | ')}"
puts " | |"
puts "-----+-----+-----" unless idx == 2
end
end
display(board)
| |
X | O | X
| |
-----+-----+-----
| |
| O |
| |
-----+-----+-----
| |
| | X
| |
Determine if the last move (by the machine or human) wins
WINNING_CELL_COMBOS = [
[0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6]
]
def win?(board)
WINNING_CELL_COMBOS.any? do |arr|
(f = arr.first) != nil && arr == [f,f,f]
end
end
win? board
#=> false
win? ['X', nil, 'O', 'nil', 'X', 'O', nil, nil, 'X']
#=> true
win? ['X', nil, 'O', 'nil', 'X', 'O', 'X', nil, 'O']
#=> true
Determine if game ends in a tie
def tie?(board)
unmarked_cells(board).empty?
end
tie?(board)
#=> false
tie? ['X', 'X', 'O', 'O', 'X', 'X', 'X', 'O', 'O']
#=> true
Note unmarked_cells.empty? can be replaced with board.all?.
Determine machine's best play using minimax algorithm
MACHINE_WINS = 0
TIE = 1
MACHINE_LOSES = 2
NEXT_MARKER = { "X"=>"O", "O"=>"X" }
def machine_best_play(board, marker)
plays = open_cells(board)
plays.min_by |play|
board_after_play = board.dup.tap { |a| a[play] = marker }
if machine_wins?(board_after_play, marker)
MACHINE_WIN
elsif plays.size == 1
TIE
else
human_worst_outcome(board_after_play, NEXT_MARKER[marker])
end
end
end
This requires two more methods.
Determine machine's best worst outcome for current state of board
def machine_worst_outcome(board, marker)
plays = open_cells(board)
plays.map |play|
board_after_play = board.dup.tap { |a| a[play] = marker }
if win?(board_after_play)
MACHINE_WINS
elsif plays.size == 1
TIE
else
human_worst_outcome(board_after_play, NEXT_MARKER[marker])
end
end.min
end
Determine human's best worst outcome for current state of board assuming
human also plays a minimax strategy
def human_worst_outcome(board, marker)
plays = open_cells(board)
plays.map |play|
board_after_play = board.dup.tap { |a| a[play] = marker }
if win?(board_after_play)
MACHINE_LOSES
elsif plays.size == 1
TIE
else
machine_worst_outcome(board_after_play, NEXT_MARKER[marker])
end
end.max
end
Notice that the human maximizes the worst outcome from the machine's perspective whereas the machine minimizes its worst outcome.
Almost there
All that remains is to quash any bugs that are present. Being short of time at the moment I will leave that to you, should you wish to do so. Feel free to edit my answer to make any corrections.

What are the values in the Array cells_per_number refering to

This was a solution to a problem on GitHub.I was looking over the solution and I was wondering what the numbers in the array are referring to
LED Clock: You are (voluntarily) in a room that is completely dark except for
the light coming from an old LED digital alarm clock. This is one of those
clocks with 4 seven segment displays using an HH:MM time format. The clock is
configured to display time in a 24 hour format and the leading digit will be
blank if not used. What is the period of time between when the room is at its
darkest to when it is at its lightest?
def compute_brightness(units)
cells_per_number = [ 6, 2, 5, 5, 4, 5, 6, 3, 7, 6 ]
units.each_with_object({}) do |t, hash|
digits = t.split('')
hash[t] = digits.map { |d| cells_per_number[d.to_i] }.reduce(:+)
end
end
The numbers refer to the number of segments that are "on" when displaying the corresponding digit. When displaying the number "0," six segments are "on" (all except the center segment), so the number at index 0 is 6. When displaying 1, only two segments are "on," so the number at index 1 is 2. You get the idea.
_ _ _
0) | | = 6 1) | = 2 2) _| = 5 3) _| = 5
|_| | |_ _|
_ _ _
4) |_| = 4 5) |_ = 5 6) |_ = 6 7) | = 3
| _| |_| |
_ _
8) |_| = 7 9) |_| = 6
|_| _|
#Jordan has answered your specific question, but the code isn't a complete solution to the stated problem. Here's a way of doing that.
lpd = { 0=>6, 1=>2, 2=>5, 3=>5, 4=>4, 5=>5, 6=>6, 7=>3, 8=>7, 9=>6 }
def min_leds(lpd, range)
leds(lpd, range).min_by(&:last).first
end
def max_leds(lpd, range)
leds(lpd, range).max_by(&:last)
end
def leds(lpd, range)
lpd.select { |k,_| range.cover?(k) }
end
darkest =
[ *[
[max_leds(lpd, (1..1)), max_leds(lpd, (0..2))],
[[0,0], max_leds(lpd, (1..9))]
].max_by { |(_,a), (_,b)| a+b },
max_leds(lpd, (0..5)),
max_leds(lpd, (0..9))
].transpose.first.join.insert(2,':')
#=> "10:08"
lightest = [0, min_leds(lpd, (1..9)),
min_leds(lpd, (0..5)),
min_leds(lpd, (0..9))
].join.insert(2,':')
#=> "0111"
To make the solution more realistic, an array (possibly empty) of the locations of the burnt-out leds should be passed to the method.

Printing a readable Matrix in Ruby

Is there a built in way of printing a readable matrix in Ruby?
For example
require 'matrix'
m1 = Matrix[[1,2], [3,4]]
print m1
and have it show
=> 1 2
3 4
in the REPL instead of:
=> Matrix[[1,2][3,4]]
The Ruby Docs for matrix make it look like that's what should show happen, but that's not what I'm seeing. I know that it would be trivial to write a function to do this, but if there is a 'right' way I'd rather learn!
You could convert it to an array:
m1.to_a.each {|r| puts r.inspect}
=> [1, 2]
[3, 4]
EDIT:
Here is a "point free" version:
puts m1.to_a.map(&:inspect)
I couldn't get it to look like the documentation so I wrote a function for you that accomplishes the same task.
require 'matrix'
m1 = Matrix[[1,2],[3,4],[5,6]]
class Matrix
def to_readable
i = 0
self.each do |number|
print number.to_s + " "
i+= 1
if i == self.column_size
print "\n"
i = 0
end
end
end
end
m1.to_readable
=> 1 2
3 4
5 6
Disclaimer: I'm the lead developer for NMatrix.
It's trivial in NMatrix. Just do matrix.pretty_print.
The columns aren't cleanly aligned, but that'd be easy to fix and we'd love any contributions to that effect.
Incidentally, nice to see a fellow VT person on here. =)
You can use the each_slice method combined with the column_size method.
m1.each_slice(m1.column_size) {|r| p r }
=> [1,2]
[3,4]
Ok, I'm a total newbie in ruby programming. I'm just making my very first incursions, but it happens I got the same problem and made this quick'n'dirty approach.
Works with the standard Matrix library and will print columns formatted with same size.
class Matrix
def to_readable
column_counter = 0
columns_arrays = []
while column_counter < self.column_size
maximum_length = 0
self.column(column_counter).each do |column_element|# Get maximal size
length = column_element.to_s.size
if length > maximal_length
maximum_length = length
end
end # now we've got the maximum size
column_array = []
self.column(column_counter).each do |column_element| # Add needed spaces to equalize each column
element_string = column_element.to_s
element_size = element_string.size
space_needed = maximal_length - element_size +1
if space_needed > 0
space_needed.times {element_string.prepend " "}
if column_counter == 0
element_string.prepend "["
else
element_string.prepend ","
end
end
column_array << element_string
end
columns_arrays << column_array # Now columns contains equal size strings
column_counter += 1
end
row_counter = 0
while row_counter < self.row_size
columns_arrays.each do |column|
element = column[row_counter]
print element #Each column yield the correspondant row in order
end
print "]\n"
row_counter += 1
end
end
end
Any correction or upgrades welcome!
This is working for me
require 'matrix'
class Matrix
def print
matrix = self.to_a
field_size = matrix.flatten.collect{|i|i.to_s.size}.max
matrix.each do |row|
puts (row.collect{|i| ' ' * (field_size - i.to_s.size) + i.to_s}).join(' ')
end
end
end
m = Matrix[[1,23,3],[123,64.5, 2],[0,0,0]]
m.print
Here is my answer:
require 'matrix'
class Matrix
def to_pretty_s
s = ""
i = 0
while i < self.column_size
s += "\n" if i != 0
j = 0
while j < self.row_size
s += ' ' if j != 0
s += self.element(i, j).to_s
j += 1
end
i += 1
end
s
end
end
m = Matrix[[0, 3], [3, 4]]
puts m # same as 'puts m.to_s'
# Matrix[[0, 3], [3, 4]]
puts m.to_pretty_s
# 0 3
# 3 4
p m.to_pretty_s
# "0 3\n3 4"
You could use Matrix#to_pretty_s to get a pretty string for format.
There is no inbuilt Ruby way of doing this. However, I have created a Module which can be included into Matrix that includes a method readable. You can find this code here, but it is also in the following code block.
require 'matrix'
module ReadableArrays
def readable(factor: 1, method: :rjust)
repr = to_a.map { |row|
row.map(&:inspect)
}
column_widths = repr.transpose.map { |col|
col.map(&:size).max + factor
}
res = ""
repr.each { |row|
row.each_with_index { |el, j|
res += el.send method, column_widths[j]
}
res += "\n"
}
res.chomp
end
end
## example usage ##
class Matrix
include ReadableArrays
end
class Array
include ReadableArrays
end
arr = [[1, 20, 3], [20, 3, 19], [-32, 3, 5]]
mat = Matrix[*arr]
p arr
#=> [[1, 20, 3], [20, 3, 19], [-2, 3, 5]]
p mat
#=> Matrix[[1, 20, 3], [20, 3, 19], [-2, 3, 5]]
puts arr.readable
#=>
# 1 20 3
# 20 3 19
# -32 3 5
puts mat.readable
#=>
# 1 20 3
# 20 3 19
# -32 3 5
puts mat.readable(method: :ljust)
#=>
# 1 20 3
# 20 3 19
# -32 3 5
puts mat.readable(method: :center)
#=>
# 1 20 3
# 20 3 19
# -32 3 5
I had this problem just yet and haven't seen anyone posting it here, so I will put my solution if it helps someone. I know 2 for loops are not the best idea, but for smaller matrix it should be okay, and it prints beautifully and just how you want it, also without of use of require 'matrix' nor 'pp'
matrix = Array.new(numRows) { Array.new(numCols) { arrToTakeValuesFrom.sample } }
for i in 0..numRows-1 do
for j in 0..numCols-1 do
print " #{matrix[i][j]} "
end
puts ""
end

How can I merge and order multiple lists together using Ruby?

I have 2 lists that have dates and data. Each list is in the proper order as noted by the sequence number. Now I need to merge the 2 lists together and keep everything in the correct order.
For example:
List A
20101001 A data 1 seq1
20101001 A data 2 seq2
20101005 A data 3 seq3
List B
20101001 B data 1 seq1
20101003 B data 2 seq2
etc...
I need the new list to look like this:
20101001 A data 1 seq1
20101001 A data 2 seq2
20101001 B data 1 seq3
20101003 B data 2 seq4
20101005 A data 3 seq5
2 things that I thought of is merging the lists together and applying the sequence number prior to inserting them into a db or I can insert them into the db with the current sequence and pull them back out again to merge them together, but that seems like an extra step and kludgy.
Any ideas on the best way to go about this?
Assuming your lists are in Ruby Arrays, and the objects in the lists have attributes defined (such as obj.sequence_number), one way to merge and sort the lists would be:
First merge the lists as a union:
#merged_list = #list_a | #list_b
Then sort the merged_list with the appropriate sorting rule:
#merged_list.sort! {|a, b| a.date <=> b.date # or whatever your sorting rule is... }
Edit:
Once the merged array is sorted, you can re-define the sequence_number:
#merged_list.each_with_index {|obj, index| obj.sequence_number = "seq#{index+1}"}
Edit:
Same thing applies if your objects in the lists are themselves just simple arrays:
#merged_list.sort! {|a, b| a[0] <=> b[0] # or whatever your sorting rule is... }
#merged_list.each_with_index {|obj, index| obj[2] = "seq#{index+1}"}
Try this:
(listA + listB).sort!{|a, b| a.sequence_no <=> b.sequence_no}
This is an algorithm for merging an arbitrary number of sorted lists in more or less linear time:
def merge_sorted(*lists)
# the lists will be modified, so make (shallow) copies
lists = lists.map(&:dup)
result = []
loop do
# ignore lists that have been exhausted
lists = lists.reject(&:empty?)
# we're done if all lists have been exhausted
break if lists.empty?
# find the list with the smallest first element
top = lists.inject do |candidate, other|
candidate.first < other.first ? candidate : other
end
result << top.shift
end
result
end
list1 = [1, 2, 5, 6, 9]
list2 = [2, 3, 4, 11, 13]
list3 = [1, 2, 2, 2, 3]
p merge_sorted(list1, list2, list3)
# => [1, 1, 2, 2, 2, 2, 2, 3, 3, 4, 5, 6, 9, 11, 13]
For each iteration it finds the list with the smallest first element, and shifts this element off of it onto the results list. It does this until all lists are empty.
I say more or less linear time since it's actually O(n × m) where n is the number of lists and m is the total number of elements in the lists but I think this can safely be simplified to O(m) for most cases since n will be small in comparison to m.
This uses with_index which is a nice way to add an index value to an iterator:
result = (list_a + list_b).sort_by { |a| a[0 .. -2] }.map.with_index { |a, i| a[0 .. -2] + (1 + i).to_s }
puts result
# >> 20101001 A data 1 seq1
# >> 20101001 A data 2 seq2
# >> 20101001 B data 1 seq3
# >> 20101003 B data 2 seq4
# >> 20101005 A data 3 seq5
Here's some variations with benchmarks:
require 'benchmark'
list_a = [
'20101001 A data 1 seq1',
'20101001 A data 2 seq2',
'20101005 A data 3 seq3'
]
list_b = [
'20101001 B data 1 seq1',
'20101003 B data 2 seq2'
]
# #1
result = (list_a + list_b).sort_by { |a| a[0 .. -2] }.map.with_index { |a, i| a[0 .. -2] + (1 + i).to_s }
result # => ["20101001 A data 1 seq1", "20101001 A data 2 seq2", "20101001 B data 1 seq3", "20101003 B data 2 seq4", "20101005 A data 3 seq5"]
# #2
result = (list_a + list_b).map{ |r| r[0 .. -2] }.sort.map.with_index { |a, i| a + (1 + i).to_s }
result # => ["20101001 A data 1 seq1", "20101001 A data 2 seq2", "20101001 B data 1 seq3", "20101003 B data 2 seq4", "20101005 A data 3 seq5"]
# #3
i = 0
result = (list_a + list_b).map{ |r| r[0 .. -2] }.sort.map { |a| i += 1; a + i.to_s }
result # => ["20101001 A data 1 seq1", "20101001 A data 2 seq2", "20101001 B data 1 seq3", "20101003 B data 2 seq4", "20101005 A data 3 seq5"]
# #4
i = 0; result = (list_a + list_b).sort.map { |a| i += 1; a[-1] = i.to_s; a }
result # => ["20101001 A data 1 seq1", "20101001 A data 2 seq2", "20101001 B data 1 seq3", "20101003 B data 2 seq4", "20101005 A data 3 seq5"]
n = 75000
Benchmark.bm(7) do |x|
x.report('#1') { n.times { (list_a + list_b).sort_by { |a| a[0 .. -2] }.map.with_index { |a, i| a[0 .. -2] + (1 + i).to_s } } }
x.report('#2') { n.times { (list_a + list_b).map{ |r| r[0 .. -2] }.sort.map.with_index { |a, i| a + (1 + i).to_s } } }
x.report('#3') { n.times { i = 0; (list_a + list_b).map{ |r| r[0 .. -2] }.sort.map { |a| i += 1; a + i.to_s } } }
x.report('#4') { n.times { i = 0; (list_a + list_b).sort.map { |a| i += 1; a[-1] = i.to_s } } }
end
# >> user system total real
# >> #1 1.150000 0.000000 1.150000 ( 1.147090)
# >> #2 0.880000 0.000000 0.880000 ( 0.880038)
# >> #3 0.720000 0.000000 0.720000 ( 0.727135)
# >> #4 0.580000 0.000000 0.580000 ( 0.572688)
It's good to benchmark.

can't convert Array into Integer

I'm trying to iterate through an array, #chem_species = ["H2", "S", "O4"] and multiply a constant times the amount of constants present: H = 1.01 * 2, S = 32.1 * 1 and so on. The constants are of course defined within the class, before the instance method.
The code I've constructed to do this does not function:
def fw
x = #chem_species.map { |chem| chem.scan(/[A-Z]/)}
y = #chem_species.map { |chem| chem.scan({/\d+/)}
#mm = x[0] * y[0]
end
yields -> TypeError: can't convert Array into Integer
Any suggestions on how to better code this? Thank you for your insight in advance.
How about doing it all in one scan & map? The String#scan method always returns an array of the strings it matched. Look at this:
irb> "H2".scan /[A-Z]+|\d+/i
=> ["H", "2"]
So just apply that to all of your #chem_species using map:
irb> #chem_species.map! { |chem| chem.scan /[A-Z]+|\d+/i }
=> [["H", "2"], ["S"], ["O", "4"]]
OK, now map over #chem_species, converting each element symbol to the value of its constant, and each coefficient to an integer:
irb> H = 1.01
irb> S = 32.01
irb> O = 15.99
irb> #chem_species.map { |(elem, coeff)| self.class.const_get(elem) * (coeff || 1).to_i }
=> [2.02, 32.01, 63.96]
There's your molar masses!
By the way, I suggest you look up the molar masses in a single hash constant instead of multiple constants for each element. Like this:
MASSES = { :H => 1.01, :S => 32.01, :O => 15.99 }
Then that last map would go like:
#chem_species.map { |(elem, coeff)| MASSES[elem.to_sym] * (coeff || 1).to_i }
You have a syntax error in your code: Maybe it should be:
def fw
x = #chem_species.map { |chem| chem.scan(/[A-Z]/)}
y = #chem_species.map { |chem| chem.scan(/\d+/)}
#mm = x[0] * y[0]
end
Have you looked at the output of #chem_species.map { |chem| chem.scan(/[A-Z]/)} (or the second one for that matter)? It's giving you an array of arrays, so if you really wanted to stick with this approach you'd have to do x[0][0].
Instead of mapping, do each
#chem_species.each { |c| c.scan(/[A-Z]/) }
Edit: just realized that that didn't work at all how I had thought it did, my apologies on a silly answer :P
Here's a way to multiply the values once you have them. The * operator won't work on arrays.
x = [ 4, 5, 6 ]
y = [ 7, 8, 9 ]
res = []
x.zip(y) { |a,b| res.push(a*b) }
res.inject(0) { |sum, v| sum += v}
# sum => 122
Or, cutting out the middle man:
x = [ 4, 5, 6 ]
y = [ 7, 8, 9 ]
res = 0
x.zip(y) { |a,b| res += (a*b) }
# res => 122
(one-liners alert, off-topic alert)
you can parse the formula directly:
"H2SO4".scan(/([A-Z][a-z]*)(\d*)/)
# -> [["H", "2"], ["S", ""], ["O", "4"]]
calculate partial sums:
aw = { 'H' => 1.01, 'S' => 32.07, 'O' => 16.00 }
"H2SO4".scan(/([A-Z][a-z]*)(\d*)/).collect{|e,x| aw[e] * (x==""?1:x).to_i}
# -> [2.02, 32.07, 64.0]
total sum:
"H2SO4".scan(/([A-Z][a-z]*)(\d*)/).collect{|e,x| aw[e] * (x==""?1:x).to_i}.inject{|s,x| s+x}
# -> 98.09

Resources