ruby - can't concatenate array elements into a string? - ruby

For personal homework I am trying out a digital clock program to display 'big' numbers.
A ruby program that has parts of the clock that come from strings stored in an array and then a routine (not yet fully written) that can display any numbers as 'big numbers'.
This is to help me learn more about manipulating hashes and iterating through arrays, concatenating string, etc.
I am stuck right now with this:
all_nums=''
(0..3).each do |num|
all_nums+= (line_parts[num][0].values).to_s
#puts line_parts[num][0].values
end
puts all_nums
because I am getting back array elements that I can't convert to strings, i.e.
ruby digital_clock,rb
[" == "]["| | "]["| | "][" == "]
but when I just puts them (commented out 3rd line from the bottom) it is OK but, I get strings, i.e.
ruby digital_clock.rb
==
| |
| |
==
I want to be able to build up the string (big 0 in this case) but I can only puts out the pieces and I've been unable to assign them to a variable and print it. How could I do that?
The full program is:
top= ' == | == == | | == | == == == '
top_middle= '| | | __| __| |__| |__ |__ | |__| |__|'
bottom_middle='| | | | | | | | | | | | |'
bottom= ' == | == == | == == | == |'
number_parts=[{},{},{},{},{},{},{},{},{},{}]
line_parts=[[],[],[],[]]
['top','top_middle','bottom_middle','bottom'].each_with_index do |one_line, i|
number_parts=[{},{},{},{},{},{},{},{},{},{}]
(0..9).each do |the_num|
number_parts[the_num] = {one_line.to_sym => eval(one_line)[the_num*5,5].to_s}
end
line_parts[i]= number_parts
end
all_nums=''
(0..3).each do |num|
all_nums+= (line_parts[num][0].values).to_s
#puts line_parts[num][0].values
end
puts all_nums

I think this will fix your immediate problem -- namely, printing a big "0".
all_nums = []
(0..3).each do |num|
all_nums.push(line_parts[num][0].values)
end
puts all_nums.join("\n")
A better way to do that would be with map:
all_nums = (0..3).map { |num| line_parts[num][0].values }
puts all_nums.join("\n")
Another issue: you don't need to use eval. In fact, one almost never needs to use eval.
# Use variables rather than the strings 'top', 'top_middle', etc.
[top,top_middle,bottom_middle,bottom].each_with_index do |one_line, i|
...
# Then the eval() is unnecessary.
number_parts[the_num] = {one_line.to_sym => one_line[the_num*5,5]}
....
But you can really simplify the code along the following lines:
# Use an array, not a set of specifically named variables.
# This makes it a lot easier to iterate over the data.
rows = [
' -- | -- -- | | -- | -- -- -- ',
' | | | __| __| |__| |__ |__ | |__| |__|',
' | | | | | | | | | | | | |',
' -- | -- -- | -- -- | -- |',
]
# Create an array-of-arrays.
# num_parts[NUMBER] = [an array of four strings]
width = 5
num_parts = (0..9).map { |n| rows.map { |r| r[n * width, width] } }
# Inspect.
num_parts.each do |np|
puts
puts np.join("\n")
end
You might also take a look at the Unix command called banner, which will inspire you to make the numbers EVEN BIGGER. That would allow you to start adding curves and other design touches.

Related

Surprisingly valid Ruby syntax: % everywhere

In Ruby 2.7 and 3.1 this script does the same thing whether or not the % signs are there:
def count(str)
state = :start
tbr = []
str.each_char do
% %case state
when :start
tbr << 0
% %state = :symbol
% when :symbol
tbr << 1
% % state = :start
% end
end
tbr
end
p count("Foobar")
How is this parsed? You can add more % or remove some and it will still work, but not any combination. I found this example through trial and error.
I was teaching someone Ruby and noticed only after their script was working that they had a random % in the margin. I pushed it a little further to see how many it would accept.
Syntax
Percent String Literal
This is a Percent String Literal receiving the message %.
A Percent String Literal has the form:
% character
opening-delimiter
string content
closing-delimiter
If the opening-delimiter is one of <, [, (, or {, then the closing-delimiter must be the corresponding >, ], ), or }. Otherwise, the opening-delimiter can be any arbitrary character and the closing-delimiter must be the same character.
So,
%
(that is, % SPACE SPACE)
is a Percent String Literal with SPACE as the delimiter and no content. I.e. it is equivalent to "".
Operator Message Send a % b
a % b
is equivalent to
a.%(b)
I.e. sending the message % to the result of evaluating the expression a, passing the result of evaluating the expression b as the single argument.
Which means
% % b
is (roughly) equivalent to
"".%(b)
Argument List
So, what's b then? Well, it's the expression following the % operator (not to be confused with the % sigil of the Percent String Literal).
The entire code is (roughly) equivalent to this:
def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start
tbr << 0
"".%(state = :symbol)
""when :symbol
tbr << 1
"".%(state = :start)
""end)
end
tbr
end
p count("Foobar")
AST
You can figure this out yourself by just asking Ruby:
# ruby --dump=parsetree_with_comment test.rb
###########################################################
## Do NOT use this node dump for any purpose other than ##
## debug and research. Compatibility is not guaranteed. ##
###########################################################
# # NODE_SCOPE (id: 62, line: 1, location: (1,0)-(17,17))
# | # new scope
# | # format: [nd_tbl]: local table, [nd_args]: arguments, [nd_body]: body
# +- nd_tbl (local table): (empty)
# +- nd_args (arguments):
# | (null node)
[…]
# | | +- nd_body (body):
# | | # NODE_OPCALL (id: 48, line: 5, location: (5,0)-(12,7))*
# | | | # method invocation
# | | | # format: [nd_recv] [nd_mid] [nd_args]
# | | | # example: foo + bar
# | | +- nd_mid (method id): :%
# | | +- nd_recv (receiver):
# | | | # NODE_STR (id: 12, line: 5, location: (5,0)-(5,3))
# | | | | # string literal
# | | | | # format: [nd_lit]
# | | | | # example: 'foo'
# | | | +- nd_lit (literal): ""
# | | +- nd_args (arguments):
# | | # NODE_LIST (id: 47, line: 5, location: (5,4)-(12,7))
# | | | # list constructor
# | | | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
# | | | # example: [1, 2, 3]
# | | +- nd_alen (length): 1
# | | +- nd_head (element):
# | | | # NODE_CASE (id: 46, line: 5, location: (5,4)-(12,7))
# | | | | # case statement
# | | | | # format: case [nd_head]; [nd_body]; end
# | | | | # example: case x; when 1; foo; when 2; bar; else baz; end
# | | | +- nd_head (case expr):
# | | | | # NODE_DVAR (id: 13, line: 5, location: (5,9)-(5,14))
# | | | | | # dynamic variable reference
# | | | | | # format: [nd_vid](dvar)
# | | | | | # example: 1.times { x = 1; x }
# | | | | +- nd_vid (local variable): :state
[…]
Some of the interesting places here are the node at (id: 12, line: 5, location: (5,0)-(5,3)) which is the first string literal, and (id: 48, line: 5, location: (5,0)-(12,7)) which is the first % message send:
# | | +- nd_body (body):
# | | # NODE_OPCALL (id: 48, line: 5, location: (5,0)-(12,7))*
# | | | # method invocation
# | | | # format: [nd_recv] [nd_mid] [nd_args]
# | | | # example: foo + bar
# | | +- nd_mid (method id): :%
# | | +- nd_recv (receiver):
# | | | # NODE_STR (id: 12, line: 5, location: (5,0)-(5,3))
# | | | | # string literal
# | | | | # format: [nd_lit]
# | | | | # example: 'foo'
# | | | +- nd_lit (literal): ""
Note: this is just the simplest possible method of obtaining a parse tree, which unfortunately contains a lot of internal minutiae that are not really relevant to figuring out what is going on. There are other methods such as the parser gem or its companion ast which produce far more readable results:
# ruby-parse count.rb
(begin
(def :count
(args
(arg :str))
(begin
(lvasgn :state
(sym :start))
(lvasgn :tbr
(array))
(block
(send
(lvar :str) :each_char)
(args)
(send
(dstr) :%
(case
(lvar :state)
(when
(sym :start)
(begin
(send
(lvar :tbr) :<<
(int 0))
(send
(dstr) :%
(lvasgn :state
(sym :symbol)))
(dstr)))
(when
(sym :symbol)
(begin
(send
(lvar :tbr) :<<
(int 1))
(send
(dstr) :%
(lvasgn :state
(sym :start)))
(dstr))) nil)))
(lvar :tbr)))
(send nil :p
(send nil :count
(str "Foobar"))))
Semantics
So far, all we have talked about is the Syntax, i.e. the grammatical structure of the code. But what does it mean?
The method String#% performs String Formatting a la C's printf family of functions. However, since the format string (the receiver of the % message) is the empty string, the result of the message send is the empty string as well, since there is nothing to format.
If Ruby were a purely functional, lazy, non-strict language, the result would be equivalent to this:
def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start
tbr << 0
""
""when :symbol
tbr << 1
""
""end)
end
tbr
end
p count("Foobar")
which in turn is equivalent to this
def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start
tbr << 0
""
when :symbol
tbr << 1
""
end)
end
tbr
end
p count("Foobar")
which is equivalent to this
def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start
""
when :symbol
""
end)
end
tbr
end
p count("Foobar")
which is equivalent to this
def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start, :symbol
""
end)
end
tbr
end
p count("Foobar")
which is equivalent to this
def count(str)
state = :start
tbr = []
str.each_char do
""
end
tbr
end
p count("Foobar")
which is equivalent to this
def count(str)
state = :start
tbr = []
tbr
end
p count("Foobar")
which is equivalent to this
def count(str)
[]
end
p count("Foobar")
Clearly, that is not what is happening, and the reason is that Ruby isn't a purely functional, lazy, non-strict language. While the arguments which are passed to the % message sends are irrelevant to the result of the message send, they are nevertheless evaluated (because Ruby is strict and eager) and they have side-effects (because Ruby is not purely functional), i.e. their side-effects of re-assigning variables and mutating the tbr result array are still executed.
If this code were written in a more Ruby-like style with less mutation and fewer side-effects and instead using functional transformations, then arbitrarily replacing results with empty strings would immediately break it. The only reason there is no effect here is because the abundant use of side-effects and mutation.

How to draw a square grid of size n to console?

I need to draw a square grid of size n to the console. The grid uses - for horizontal cell boundaries, | for vertical cell boundaries, and + for corners of each cell.
As an example, a size 3 grid should look like the following:
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
I was thinking of using a double for loop with outer loop iterating through rows and inner loop iterating through cols. each iteration of inner loop would be processing an individual cell. drawing | characters doesn't seem to hard but I'm not sure how I'd go about printing the - chars above and below a cell.
You could use Integer#times and String#*:
def print_grid(n)
n.times { print "+-"*n, "+\n", "| "*n, "|\n" }
print "+-"*n, "+\n"
end
print_grid(3)
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
=> nil
Alternatively:
def print_grid(n)
puts n.times.map{ "+-"*n + "+\n" + "| "*n + "|\n" }.join + "+-"*n + "+\n"
end
For a width of 3, you have a separator-row like this:
+-+-+-+
This can be sees as either:
3 - connected by and surrounded by +
4 + connected by -
The latter is a bit easier to express in Ruby:
width = 3
Array.new(width + 1, '+').join('-')
#=> "+-+-+-+"
The same works for the cell-row:
Array.new(width + 1, '|').join(' ')
#=> "| | | |"
Vertically, you have 3 cell-rows connected by and surrounded by separator-rows. (that should ring a bell) Just like before, this can also be expressed as 4 separator-rows connected by cell-rows.
Let's store our separator-row and cell-row in variables: (we also have to append newlines)
width = 3
separator_row = Array.new(width + 1, '+').join('-') << "\n"
cell_row = Array.new(width + 1, '|').join(' ') << "\n"
And define the grid:
height = 3
grid = Array.new(height + 1, separator_row).join(cell_row)
#=> "+-+-+-+\n| | | |\n+-+-+-+\n| | | |\n+-+-+-+\n| | | |\n+-+-+-+\n"
put grid
Output:
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
Here are two ways to do that.
Determine each character depending on whether the row and column indices are even or odd
def grid(n)
sz = 2*n+1
Array.new(sz) do |i|
Array.new(sz) do |j|
if i.even?
j.even? ? '+' : '-'
else # i is odd
j.even? ? '|' : ' '
end
end.join
end
end
puts grid(3)
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
puts grid(4)
+-+-+-+-+
| | | | |
+-+-+-+-+
| | | | |
+-+-+-+-+
| | | | |
+-+-+-+-+
| | | | |
+-+-+-+-+
Use an enumerator
def pr_grid(n)
enum = [*['+','-']*n, "+", "\n", *['|',' ']*n, "|", "\n"].cycle
((2+2*n)*(1+2*n)).times { print enum.next }
end
pr_grid(3)
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
| | | |
+-+-+-+
pr_grid(4)
+-+-+-+-+
| | | | |
+-+-+-+-+
| | | | |
+-+-+-+-+
| | | | |
+-+-+-+-+
| | | | |
+-+-+-+-+
For n = 3 the steps are as follows.
a = [*['+','-']*n, "+", "\n", *['|',' ']*n, "|", "\n"]
#=> ["+", "-", "+", "-", "+", "-", "+", "\n",
# "|", " ", "|", " ", "|", " ", "|", "\n"]
enum = a.cycle
#=> #<Enumerator: ["+", "-", "+", "-", "+", "-", "+", "\n",
# "|", " ", "|", " ", "|", " ", "|", "\n"]:cycle
enum.next #=> "+"
enum.next #=> "-"
enum.next #=> "+"
enum.next #=> "-"
enum.next #=> "+"
enum.next #=> "-"
enum.next #=> "+"
enum.next #=> "\n"
enum.next #=> "|"
enum.next #=> " "
and so on.
Just for fun and just as an example, here is a list of methods (to be optimized and debugged and maybe used within a class) that you can use also for filling a table.
#cell = '|'
#line = '-'
#cross = '+'
def build_row(content)
(content.zip [#cell]* content.size).flatten.prepend(#cell).join
end
def separator_from_content(content)
content.map { |e| #line * e.size + #cross }.prepend(#cross).join
end
def adjust_content_in(lines)
width = lines.flatten.max_by(&:size).size
lines.map { |line| line.map { |e| e << ' ' * (width - e.size) } }
end
def build_table(lines)
lines = adjust_content_in(lines)
separator = separator_from_content(lines.first)
mapped = lines.map { |line| build_row(line) }.zip([separator] * (lines.size) )
return mapped.flatten.prepend(separator).join("\n")
end
def empty_squared_table(n)
lines = n.times.map { n.times.map { ' ' } }
build_table(lines)
end
So, if you want to draw an empty table, can call:
n = 3
puts empty_squared_table(n)
# +-+-+-+
# | | | |
# +-+-+-+
# | | | |
# +-+-+-+
# | | | |
# +-+-+-+
Or you can fill in some text (more lines inside a cell not supported):
lines = [['so', 'you can', 'fill'], ['a table', 'given the content', '']]
puts build_table(lines)
# +-----------------+-----------------+-----------------+
# |so |you can |fill |
# +-----------------+-----------------+-----------------+
# |a table |given the content| |
# +-----------------+-----------------+-----------------+

Ruby adding empty strings to hash for CSV spacing

I have:
hash = {"1"=>["A", "B", "C", ... "Z"], "2"=>["B", "C"], "3"=>["A", "C"]
My goal is to use hash as a source for creating a CSV with columns whose names are a letter of the alphabet and with rows hash(key) = 1,2,3 etc.
I created an array of all hash.values.unshift("")values that serve as row 1 (columns labels).
desired output:
| A | B | C | ... | Z |
1| A | B | C | ... | Z |
2| | B | C | ....... |
3| A | | C | ....... |
Creating CSV:
CSV.open("groups.csv", 'w') do |csv|
csv << row1
hash.each do |v|
csv << v.flatten
end
end
This makes the CSV look almost what I want but There is no spacing to get columns to align.
Any advice on how to make a method for modifying my hash that compares my all [A-Z] against each subsequent hash key (rows) to insert empty strings to provide spacing?
Can Class CSV do it better?
Something like this?
require 'csv'
ALPHA = ('A'..'Z').to_a.freeze
hash={"1"=>ALPHA, "2"=>["B", "C"], "3"=>["A", "C"]}
csv = CSV.generate("", col_sep: "|") do |csv|
csv << [" "] + ALPHA # header
hash.each do |k, v|
alphabet = ALPHA.map { |el| [el, 0] }.to_h
v.each { |el| alphabet[el] += 1 }
csv << [k, *alphabet.map { |k, val| val == 1 ? k : " " }]
end
end
csv.split("\n").each { |row| puts row }
output:
|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z
1|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z
2| |B|C| | | | | | | | | | | | | | | | | | | | | | |
3|A| |C| | | | | | | | | | | | | | | | | | | | | | |
If your values are truly single characters and don't need the CSV escaping, then I recommend bypassing CSV altogether and building the string in plain Ruby.
Assuming you want to align your lines correctly regardless of the number of digits in the row number (e.g. 1, 10, and 100), you can use printf style formatting to guarantee horizontal aligment (assuming your row number width never exceeds the value of ROWNUM_WIDTH).
By the way, I changed the hash's keys to integers, hope that's ok.
#!/usr/bin/env ruby
FIELDS = ('A'..'Z').to_a
DATA = { 1 => FIELDS, 2 => %w(B C), 3 => %w(A C) }
ROWNUM_WIDTH = 3
output = ' ' * ROWNUM_WIDTH + " | #{FIELDS.join(' | ')} |\n"
DATA.each do |rownum, values|
line = "%*d | " % [ROWNUM_WIDTH, rownum]
FIELDS.each do |field|
char = values.include?(field) ? field : ' '
line << "#{char} | "
end
output << line << "\n"
end
puts output
=begin
Outputs:
| A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z |
1 | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z |
2 | | B | C | | | | | | | | | | | | | | | | | | | | | | | |
3 | A | | C | | | | | | | | | | | | | | | | | | | | | | | |
=end
all = [*?A..?Z]
hash = {"1"=>[*?A..?Z], "2"=>["B", "C"], "3"=>["A", "C"]}
hash.map do |k, v|
[k, *all.map { |k| v.include?(k) ? k : ' ' }]
end.unshift([' ', *all]).
map { |row| row.join('|') }
#⇒ [
# [0] " |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z",
# [1] "1|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z",
# [2] "2| |B|C| | | | | | | | | | | | | | | | | | | | | | | ",
# [3] "3|A| |C| | | | | | | | | | | | | | | | | | | | | | | "
# ]

How to split string using regex to split between +,-,*,/ symbols?

I need to tell Ruby in regex to split before and after the + - * / symbols in my program.
Examples:
I need to turn "1+12" into [1.0, "+", 12.0]
and "6/0.25" into [6.0, "/", 0.25]
There could be cases like "3/0.125" but highly unlikely. If first two I listed above are satisfied it should be good.
On the Ruby docs, "hi mom".split(%r{\s*}) #=> ["h", "i", "m", "o", "m"]
I looked up a cheat-sheet to try to understand %r{\s*}, and I know that the stuff inside %r{} such as \s are skipped and \s means white space in regex.
'1.0+23.7'.scan(/(((\d\.?)+)|[\+\-\*\/])/)
instead of splitting, match with capture groups to parse your inputs:
(?<operand1>(?:\d+(?:\.\d+)?)|(?:\.\d+))\s*(?<operator>[+\/*-])\s*(?<operand2>(?:\d+(?:\.\d+)?)|(?:\.\d+))
explanation:
I've used named groups (?<groupName>regex) but they aren't necessary and could just be ()'s - either way, the sub-captures will still be available as 1,2,and 3. Also note the (?:regex) constructs that are for grouping only and do not "remember" anything, and won't mess up your captures)
(?:\d+(?:\.\d+)?)|(?:\.\d+)) first number: either leading digit(s) followed optionally by a decimal point and digit(s), OR a leading decimal point followed by digit(s)
\s* zero or more spaces in between
[+\/*-] operator: character class meaning a plus, division sign, minus, or multiply.
\s* zero or more spaces in between
(?:\d+(?:\.\d+)?)|(?:\.\d+) second number: same pattern as first number.
regex demo output:
I arrived a little late to this party, and found that many of the good answers had already been taken. So, I set out to expand on the theme slightly and compare the performance and robustness of each of the solutions. It seemed like a fun way to entertain myself this morning.
In addition to the 3 examples given in the question, I added test cases for each of the four operators, as well as for some new edge cases. These edge cases included handling of negative numbers and arbitrary spaces between operands, as well as how each of the algorithms handled expected failures.
The answers revolved around 3 methods: split, scan, and match. I also wrote new solutions using each of these 3 methods, specifically respecting the additional edge cases that I added to here. I ran all of the algorithms against this full set of test cases, and ended up with a table of pass/fail results.
Next, I created a benchmark that created 1,000,000 test strings that each of the solutions would be able to parse properly, and ran each solution against that sample set.
On first benchmarking, Cary Swoveland's solution performed far better than the others, but didn't pass the added test cases. I made very minor changes to his solution to produce a solution that supported both negative numbers and arbitrary spaces, and included that test as Swoveland+.
The final results printed from to the console are here (note: horizontal scroll to see all results):
| Test Case | match | match | scan | scan |partition| split | split | split | split |
| | Gaskill | sweaver | Gaskill | techbio |Swoveland| Gaskill |Swoveland|Swoveland+| Lilue |
|------------------------------------------------------------------------------------------------------|
| "1+12" | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| "6/0.25" | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| "3/0.125" | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| "30-6" | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| "3*8" | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| "20--4" | Pass | -- | Pass | -- | Pass | Pass | -- | Pass | Pass |
| "33+-9" | Pass | -- | Pass | -- | Pass | Pass | -- | Pass | Pass |
| "-12*-2" | Pass | -- | Pass | -- | Pass | Pass | -- | Pass | Pass |
| "-72/-3" | Pass | -- | Pass | -- | Pass | Pass | -- | Pass | Pass |
| "34 - 10" | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| " 15+ 9" | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| "4*6 " | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| "b+0.5" | Pass | Pass | Pass | -- | -- | -- | -- | -- | -- |
| "8---0.5" | Pass | Pass | Pass | -- | -- | -- | -- | -- | -- |
| "8+6+10" | Pass | -- | Pass | -- | -- | -- | -- | -- | -- |
| "15*x" | Pass | Pass | Pass | -- | -- | -- | -- | -- | -- |
| "1.A^ff" | Pass | Pass | Pass | -- | -- | -- | -- | -- | -- |
ruby 2.2.5p319 (2016-04-26 revision 54774) [x86_64-darwin14]
============================================================
user system total real
match (Gaskill): 4.770000 0.090000 4.860000 ( 5.214996)
match (sweaver2112): 4.640000 0.040000 4.680000 ( 4.911849)
scan (Gaskill): 7.360000 0.080000 7.440000 ( 7.719646)
scan (techbio): 12.930000 0.140000 13.070000 ( 13.791613)
partition (Swoveland): 5.390000 0.050000 5.440000 ( 5.648762)
split (Gaskill): 5.150000 0.100000 5.250000 ( 5.455094)
split (Swoveland): 3.860000 0.060000 3.920000 ( 4.040774)
split (Swoveland+): 4.240000 0.040000 4.280000 ( 4.537570)
split (Lilue): 7.540000 0.090000 7.630000 ( 8.022252)
In order to keep this post from being far too long, I've included the complete code for this test at https://gist.github.com/mgaskill/96f04e7e1f72a86446f4939ac690759a
The robustness test cases can be found in the first table above. The Swoveland+ solution is:
f,op,l = formula.split(/\b\s*([+\/*-])\s*/)
return [f.to_f, op, l.to_f]
This includes a \b metacharacter prior to splitting on an operator ensures that the previous character is a word character, giving support for negative numbers in the second operand. The \s* metacharacter expressions support arbitrary spaces between operands and operator. These changes incur less than 10% performance overhead for the additional robustness.
The solutions that I provided are here:
def match_gaskill(formula)
return [] unless (match = formula.match(/^\s*(-?\d+(?:\.\d+)?)\s*([+\/*-])\s*(-?\d+(?:\.\d+)?)\s*$/))
return [match[1].to_f, match[2], match[3].to_f]
end
def scan_gaskill(formula)
return [] unless (match = formula.scan(/^\s*(-?\d+(?:\.\d+)?)\s*([+*\/-])\s*(-?\d+(?:\.\d+)?)\s*$/))[0]
return [match[0][0].to_f, match[0][1], match[0][2].to_f]
end
def split_gaskill(formula)
match = formula.split(/(-?\d+(?:\.\d+)?)\s*([+\/*-])\s*(-?\d+(?:\.\d+)?)/)
return [match[1].to_f, match[2], match[3].to_f]
end
The match and scan solutions are very similar, but perform significantly differently, which is very interesting, because they use the exact same regex to do the work. The split solution is slightly simpler, and only splits on the entire expression, capturing each operand and the operator, separately.
Note that none of the split solutions was able to properly identify failures. Adding this support requires additional parsing of the operands, which significantly increases the overhead of the solution, typically running about 3 times slower.
For both performance and robustness, match is the clear winner. If robustness isn't a concern, but performance is, use split. On the other hand, scan provided complete robustness, but was more than 50% slower than the equivalent match solution.
Also note that using an efficient way to extract the results from the solution into the result array is as important to performance as is the algorithm chosen. The technique of capturing the results array into multiple variables (used in Woveland) outperformed the map solutions dramatically. Early testing showed that the map extraction solution more than doubled the runtimes of even the highest-performing solutions, hence the exceptionally high runtime numbers for Lilue.
I think this could be useful:
"1.2+3.453".split('+').flat_map{|elem| [elem, "+"]}[0...-1]
# => ["1.2", "+", "3.453"]
"1.2+3.453".split('+').flat_map{|elem| [elem.to_f, "+"]}[0...-1]
# => [1.2, "+", 3.453]
Obviously this work only for +. But you can change the split character.
EDIT:
This version work for every operator
"1.2+3.453".split(%r{(\+|\-|\/|\*)}).map do |x|
unless x =~ /(\+|\-|\/|\*)/ then x.to_f else x end
end
# => [1.2, "+", 3.453]
R = /
(?<=\d) # match a digit in a positive lookbehind
[^\d\.] # match any character other than a digit or period
/x # free-spacing regex definition mode
def split_it(str)
f,op,l = str.delete(' ').partition(R)
[convert(f), op, convert(l)]
end
def convert(str)
(str =~ /\./) ? str.to_f : str.to_i
end
split_it "1+12"
#=> [1, "+", 12]
split_it "3/ 5.2"
#=> [3, "/", 5.2]
split_it "-4.1 * 6"
#=> [-4.1, "*", 6]
split_it "-8/-2"
#=> [-8, "/", -2]
The regex can of course be written in the conventional way:
R = /(?<=\d)[^\d\.]/

Infinite loop nested within a block in Ruby

Here's what I'm trying to do. I'm iterating through an array of strings, each of which may contain [] multiple times. I want to use String#match as many times as necessary to process every occurrence. So I have an Array#each block, and nested within that is an infinite loop that should break only when I run out of matches for a given string.
def follow(c, dir)
case c
when "\\"
dir.rotate!
when "/"
dir.rotate!.map! {|x| -x}
end
return dir
end
def parse(ar)
ar.each_with_index { |s, i|
idx = 0
while true do
t = s.match(/\[[ ]*([a-zA-Z]+)[ ]*\]/) { |m|
idx = m.end 0
r = m[1]
s[m.begin(0)...idx] = " "*m[0].size
a = [i, idx]
dir = [0, 1]
c = ar[a[0]][a[1]]
while !c.nil? do
dir = follow c, dir
ar[a[0]][a[1]] = " "
a[0] += dir[0]; a[1] += dir[1]
c = ar[a[0]][a[1]]
if c == ">" then
ar[a[0]][a[1]+1] = r; c=nil
elsif c == "<" then
ar[a[0]][a[1]-r.size] = r; c=nil
end
end
ar[a[0]][a[1]] = " "
puts ar
}
if t == nil then break; end
end
}
parse File.new("test", "r").to_a
Contents of test:
+--------+----------+-------------+
| Colors | Foods | Countries |
+--------+----------+-------------+
| red | pizza | Switzerland |
/--> /----> | |
| |[kale]/ | hot dogs | Brazil |
| | <----------------------\ |
| | orange |[yellow]\ | [green]/ |
| +--------+--------|-+-------------+
\-------------------/
Goal:
+--------+----------+-------------+
| Colors | Foods | Countries |
+--------+----------+-------------+
| red | pizza | Switzerland |
yellow kale | |
| | hot dogs | Brazil |
| green |
| orange | | |
+--------+-------- -+-------------+
Actual output of program:
+--------+----------+-------------+
| Colors | Foods | Countries |
+--------+----------+-------------+
| red | pizza | Switzerland |
yellow kale | |
| | hot dogs | Brazil |
| <----------------------\ |
| orange | | [green]/ |
+--------+-------- -+-------------+
(Since the array is modified in place, I figure there is no need to update the match index.) The inner loop should run again for each pair of [] I find, but instead I only get one turn per array entry. (It seems that the t=match... bit isn't the problem, but I could be wrong.) How can I fix this?

Resources