LCD Numbers (#14)

This week's quiz is to write a program that displays LCD style numbers at adjustable sizes.

The digits to be displayed will be passed as an argument to the program. Size should be controlled with the command-line option -s follow up by a positive integer. The default value for -s is 2.

For example, if your program is called with:

> lcd.rb 012345

The correct display is:

-- -- -- --
| | | | | | | |
| | | | | | | |
-- -- -- --
| | | | | | |
| | | | | | |
-- -- -- --

And for:

> lcd.rb -s 1 6789

- - - -
| | | | | |
- - -
| | | | | |
- - -

Note the single column of space between digits in both examples. For other values of -s, simply lengthen the - and | bars.

Quiz Summary

Clearly this problem isn't too difficult. As we've now seen, it can be solved in under 300 bytes. However, this classic challenge does address topics like scaling and joining multiline data that are applicable to many areas of computer programming. That always makes for an interesting quiz in my book.

There were three main strategies used for solving the problem. Some used a template approach, where you have some kind of text representation of your number at a scale of one. Two might look like this, for example:

[ " - ",
" |",
" - ",
"| ",
" - " ]

Scaling that to any size is a two-fold process. First, you need to stretch it horizontally. The easy way to do that is to grab the second character of each string (a "-" or a " ") and repeat it -s times:

ruby
digit.each { |row| row[1, 1] = row[1, 1] * scale }

After that, the digit needs to be scaled vertically. That's pretty easy to do while printing it out, if you want. Just print any line containing a "|" -s times:

ruby
digit.each do |row|
if row =~ /\|/
scale.times { puts row }
else
puts row
end
end

The second strategy used was to treat each digit as a series of segments that can be "on" or "off". The numbers easily break down into seven positions:

6
5 4
3
2 1
0

Using that map, we can convert the two above to a binary digit, and some did:

ruby
0b1011101

Expansion of these representations is handled much as it was in the first strategy above.

With either method, you will need to join the scaled digits together for output. This is basically a two dimensional join() problem. Building a routine like that is simple using either Array.zip() or Array.transpose(). (See the submissions for numerous examples of this.)

The third strategy caught my eye, so I'll examine it here. Let's look at the primary class of Dale Martenson's solution:

ruby
class LCD
attr_accessor( :size, :spacing )

#
# This hash is used to define the segment display for the
# given digit. Each entry in the array is associated with
# the following states:
#
# HORIZONTAL
# VERTICAL
# HORIZONTAL
# VERTICAL
# HORIZONTAL
# DONE
#
# The HORIZONTAL state produces a single horizontal line. There
# are two types:
#
# 0 - skip, no line necessary, just space fill
# 1 - line required of given size
#
# The VERTICAL state produces a either a single right side line,
# a single left side line or a both lines.
#
# 0 - skip, no line necessary, just space fill
# 1 - single right side line
# 2 - single left side line
# 3 - both lines
#
# The DONE state terminates the state machine. This is not needed
# as part of the data array.
#
@@lcdDisplayData = {
"0" => [ 1, 3, 0, 3, 1 ],
"1" => [ 0, 1, 0, 1, 0 ],
"2" => [ 1, 1, 1, 2, 1 ],
"3" => [ 1, 1, 1, 1, 1 ],
"4" => [ 0, 3, 1, 1, 0 ],
"5" => [ 1, 2, 1, 1, 1 ],
"6" => [ 1, 2, 1, 3, 1 ],
"7" => [ 1, 1, 0, 1, 0 ],
"8" => [ 1, 3, 1, 3, 1 ],
"9" => [ 1, 3, 1, 1, 1 ]
}

@@lcdStates = [
"HORIZONTAL",
"VERTICAL",
"HORIZONTAL",
"VERTICAL",
"HORIZONTAL",
"DONE"
]

def initialize( size=1, spacing=1 )
@size = size
@spacing = spacing
end

def display( digits )
states = @@lcdStates.reverse
0.upto(@@lcdStates.length) do |i|
case states.pop
when "HORIZONTAL"
line = ""
digits.each_byte do |b|
line += horizontal_segment(
@@lcdDisplayData[b.chr][i]
)
end
print line + "\n"
when "VERTICAL"
1.upto(@size) do |j|
line = ""
digits.each_byte do |b|
line += vertical_segment(
@@lcdDisplayData[b.chr][i]
)
end
print line + "\n"
end
when "DONE"
break
end
end
end

def horizontal_segment( type )
case type
when 1
return " " + ("-" * @size) + " " + (" " * @spacing)
else
return " " + (" " * @size) + " " + (" " * @spacing)
end
end

def vertical_segment( type )
case type
when 1
return " " + (" " * @size) + "|" + (" " * @spacing)
when 2
return "|" + (" " * @size) + " " + (" " * @spacing)
when 3
return "|" + (" " * @size) + "|" + (" " * @spacing)
else
return " " + (" " * @size) + " " + (" " * @spacing)
end
end
end

The comment above gives you a nice clue to what is going on here. The class represents a state machine. For the needed size (set in initialize()), that class walks a series of states (defined in @@lcdStates). At each state, horizontal and vertical segments are built as needed (with horizontal_segment() and vertical_segment()).

The process I've just described is run through display(), the primary interface method. You pass it a string of digits, it walks each state, and generates segments as needed.

One nice aspect of this approach is that it's easy to handle output one line at a time and display() does this. The top line of all digits, generated by the first "HORIZONTAL" state, is printed as soon as it's built, as is each state that follows. This resource friendly system could scale well to much larger inputs.

The rest of Dale's code (not shown), is just option parsing and the single call to display().

My usual thanks to all who played with the quiz. More thanks to the "golfers" who taught me some interesting tricks about their sport and one more thank you to why the lucky stiff who notes our playing around in his blog, RedHanded.

Tomorrow, you'll think up the quiz and your computer will solve it for you...