Surprisingly valid Ruby syntax: % everywhere - ruby

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.

Related

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| |
# +-----------------+-----------------+-----------------+

Why is a statement like 1 + n *= 3 allowed in Ruby?

The precedence tables in many Ruby documentations out there list binary arithmetic operations as having higher precedence than their corresponding compound assignment operators. This leads me to believe that code like this shouldn't be valid Ruby code, yet it is.
1 + age *= 2
If the precedence rules were correct, I'd expect that the above code would be parenthesized like this:
((1 + age) *= 2) #ERROR: Doesn't compile
But it doesn't.
So what gives?
Checking ruby -y output, you can see exactly what is happening. Given the source of 1 + age *= 2, the output suggests this happens (simplified):
tINTEGER found, recognised as simple_numeric, which is a numeric, which is a literal, which is a primary. Knowing that + comes next, primary is recognised as arg.
+ found. Can't deal yet.
tIDENTIFIER found. Knowing that next token is tOP_ASGN (operator-assignment), tIDENTIFIER is recognised as user_variable, and then as var_lhs.
tOP_ASGN found. Can't deal yet.
tINTEGER found. Same as last one, it is ultimately recognised as primary. Knowing that next token is \n, primary is recognised as arg.
At this moment we have arg + var_lhs tOP_ASGN arg on stack. In this context, we recognise the last arg as arg_rhs. We can now pop var_lhs tOP_ASGN arg_rhs from stack and recognise it as arg, with stack ending up as arg + arg, which can be reduced to arg.
arg is then recognised as expr, stmt, top_stmt, top_stmts. \n is recognised as term, then terms, then opt_terms. top_stmts opt_terms are recognised as top_compstmt, and ultimately program.
On the other hand, given the source 1 + age * 2, this happens:
tINTEGER found, recognised as simple_numeric, which is a numeric, which is a literal, which is a primary. Knowing that + comes next, primary is recognised as arg.
+ found. Can't deal yet.
tIDENTIFIER found. Knowing that next token is *, tIDENTIFIER is recognised as user_variable, then var_ref, then primary, and arg.
* found. Can't deal yet.
tINTEGER found. Same as last one, it is ultimately recognised as primary. Knowing that next token is \n, primary is recognised as arg.
The stack is now arg + arg * arg. arg * arg can be reduced to arg, and the resultant arg + arg can also be reduced to arg.
arg is then recognised as expr, stmt, top_stmt, top_stmts. \n is recognised as term, then terms, then opt_terms. top_stmts opt_terms are recognised as top_compstmt, and ultimately program.
What's the critical difference? In the first piece of code, age (a tIDENTIFIER) is recognised as var_lhs (left-hand-side of assignment), but in the second one, it's var_ref (a variable reference). Why? Because Bison is a LALR(1) parser, meaning that it has one-token look-ahead. So age is var_lhs because Ruby saw tOP_ASGN coming up; and it was var_ref when it saw *. This comes about because Ruby knows (using the huge state transition table that Bison generates) that one specific production is impossible. Specifically, at this time, the stack is arg + tIDENTIFIER, and next token is *=. If tIDENTIFIER is recognised as var_ref (which leads up to arg), and arg + arg reduced to arg, then there is no rule that starts with arg tOP_ASGN; thus, tIDENTIFIER cannot be allowed to become var_ref, and we look at the next matching rule (the var_lhs one).
So Aleksei is partly right in that there is some truth to "when it sees a syntax error, it tries another way", but it is limited to one token into future, and the "attempt" is just a lookup in the state table. Ruby is incapable of complex repair strategies we humans use to understand sentences like "the horse raced past the barn fell", where we happily parse till the last word, then reevaluate the whole sentence when the first parse turns out impossible.
tl;dr: The precedence table is not exactly correct. There is no place in Ruby source where it exists; rather, it is the result of the interplay of various parsing rules. Many of the precedence rules break in when left-hand-side of an assignment is introduced.
The simplified answer is. You can only assign a value to a variable, not to an expression. Therefore the order is 1 + (age *= 2). The precedence only comes into play if multiple options are possible. For example age *= 2 + 1 can be seen as (age *= 2) + 1 or age *= (2 + 1), since multiple options are possible and the + has a higher precedence than *=, age *= (2 + 1) is used.
NB This answer should not be marked as solving the issue. See the answer by #Amadan for the correct explanation.
I am not sure what “many Ruby documentations” you mentioned, here is the official one.
Ruby parser does its best to understand and successfully parse the input; when it sees a syntax error, it tries another way. That said, syntax errors have greater precedence compared to all operator precedence rules.
Since LHO must be variable, it starts with an assignment. Here is the case when the parsing can be done with a default precedence order and + is done before *=:
age = 2
age *= age + 1
#⇒ 6
Ruby has 3 phases before your code is actually executed.
Tokenize -> Parse -> Compile
Let's look at the AST(Abstract Syntax Tree) Ruby generates which is the parse phase.
# # NODE_SCOPE (line: 1, location: (1,0)-(1,12))
# | # new scope
# | # format: [nd_tbl]: local table, [nd_args]: arguments, [nd_body]: body
# +- nd_tbl (local table): :age
# +- nd_args (arguments):
# | (null node)
# +- nd_body (body):
# # NODE_OPCALL (line: 1, location: (1,0)-(1,12))*
# | # method invocation
# | # format: [nd_recv] [nd_mid] [nd_args]
# | # example: foo + bar
# +- nd_mid (method id): :+
# +- nd_recv (receiver):
# | # NODE_LIT (line: 1, location: (1,0)-(1,1))
# | | # literal
# | | # format: [nd_lit]
# | | # example: 1, /foo/
# | +- nd_lit (literal): 1
# +- nd_args (arguments):
# # NODE_ARRAY (line: 1, location: (1,4)-(1,12))
# | # array constructor
# | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
# | # example: [1, 2, 3]
# +- nd_alen (length): 1
# +- nd_head (element):
# | # NODE_DASGN_CURR (line: 1, location: (1,4)-(1,12))
# | | # dynamic variable assignment (in current scope)
# | | # format: [nd_vid](current dvar) = [nd_value]
# | | # example: 1.times { x = foo }
# | +- nd_vid (local variable): :age
# | +- nd_value (rvalue):
# | # NODE_CALL (line: 1, location: (1,4)-(1,12))
# | | # method invocation
# | | # format: [nd_recv].[nd_mid]([nd_args])
# | | # example: obj.foo(1)
# | +- nd_mid (method id): :*
# | +- nd_recv (receiver):
# | | # NODE_DVAR (line: 1, location: (1,4)-(1,7))
# | | | # dynamic variable reference
# | | | # format: [nd_vid](dvar)
# | | | # example: 1.times { x = 1; x }
# | | +- nd_vid (local variable): :age
# | +- nd_args (arguments):
# | # NODE_ARRAY (line: 1, location: (1,11)-(1,12))
# | | # array constructor
# | | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
# | | # example: [1, 2, 3]
# | +- nd_alen (length): 1
# | +- nd_head (element):
# | | # NODE_LIT (line: 1, location: (1,11)-(1,12))
# | | | # literal
# | | | # format: [nd_lit]
# | | | # example: 1, /foo/
# | | +- nd_lit (literal): 2
# | +- nd_next (next element):
# | (null node)
# +- nd_next (next element):
# (null node)
As you can see # +- nd_mid (method id): :+ where 1 is treated as the receiver and everything on the right as arguments. Now, it goes further and does its best to evaluate the arguments.
To further support Aleksei's great answer. The # NODE_DASGN_CURR (line: 1, location: (1,4)-(1,12)) is the assignment on age as a local variable as it decodes it as age = age * 2, which is why +- nd_mid (method id): :* is treated as the operation on age as the receiver and 2 as its argument.
Now when it goes on to compile it tries as operation: age * 2 where age is nil because it already parsed it as a local variable with no pre-assigned value, raises exception undefined method '*' for nil:NilClass (NoMethodError).
It works the way it did is cause any operation on the receiver must have an evaluated argument from the RHO.

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?

Collecting objects that are not necessarly assigned to any variables/constants

Objects that are not assigned to any variable/constant disappear immediately (under normal circumstance). In the following, the string "foo" is not captured by ObjectSpace.each_object(String) in the third line:
strings = ObjectSpace.each_object(String).to_a
"foo"
ObjectSpace.each_object(String).to_a - strings # => []
Is it possible to capture objects that are not necessarly assigned to any variables/constants or part of any variables/constants? I am particularly interested in capturing strings. The relevant domain can be a file, or a block. I expect something like the following:
capture_all_strings do
...
"a"
s = "b"
#s = "c"
##s = "d"
S = "e"
%q{f}
...
end
# => ["a", "b", "c", "d", "e", "f"]
Ruby creates the string instances when parsing your file. Here's an example: the string
"aaa #{123} zzz"
is parsed as:
$ ruby --dump=parsetree -e '"aaa #{123} zzz"'
###########################################################
## Do NOT use this node dump for any purpose other than ##
## debug and research. Compatibility is not guaranteed. ##
###########################################################
# # NODE_SCOPE (line: 1)
# +- nd_tbl: (empty)
# +- nd_args:
# | (null node)
# +- nd_body:
# # NODE_DSTR (line: 1)
# +- nd_lit: "aaa "
# +- nd_next->nd_head:
# | # NODE_EVSTR (line: 1)
# | +- nd_body:
# | # NODE_LIT (line: 1)
# | +- nd_lit: 123
# +- nd_next->nd_next:
# # NODE_ARRAY (line: 1)
# +- nd_alen: 1
# +- nd_head:
# | # NODE_STR (line: 1)
# | +- nd_lit: " zzz"
# +- nd_next:
# (null node)
There are two string literals at the parser stage, "aaa " and " zzz":
# +- nd_lit: "aaa "
# ...
# | +- nd_lit: " zzz"
Inspecting ObjectSpace confirms that these strings have been instantiated:
$ ruby -e '"aaa #{123} zzz"; ObjectSpace.each_object(String) { |s| p s }' | egrep "aaa|zzz"
"\"aaa \#{123} zzz\"; ObjectSpace.each_object(String) { |s| p s }\n"
"aaa 123 zzz"
" zzz"
"aaa "
So unless you are creating a new string instance (e.g. by assigning the string literal to a variable) you can't detect the string creation. It's already there when the code is being executed.

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

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.

Resources