1 2 3 4 5 6 |
$ lcd.rb 012 - - | | | | - | | | | - - |

Il quiz prevede anche un parametro in input (-s) che indica la dimensione dei numeri, -s sta per size. quindi:
1 2 3 4 5 6 7 8 |
$ lcd.rb -s 2 012 -- -- | | | | | | | | -- | | | | | | | | -- -- |
Per semplicità io partirò con un default size di 1, aggiungiendo il supporto al size in seguito.
Partiamo creando la classe principale Lcd, la relativa classe di test LcdTest e il RakeFile per lanciare i nostri test:
1 2 3 4 5 |
class Lcd def Lcd.process number '' # do nothing righ now end end |
1 2 3 4 |
require 'test/unit' require 'lcd' class LcdTest < Test::Unit::TestCase end |
1 2 3 4 5 6 7 8 9 10 11 12 |
require 'rake' require 'rake/testtask' desc 'Default: run unit tests.' task :default => :test desc 'Test the lcd' Rake::TestTask.new(:test) do |t| t.libs << '.' t.pattern = '*_test.rb' t.verbose = true end |
Ora per lanciare i test basta scrivere rake dalla shell e, siccome non ci sono test, avremo un messaggio d’errore. In verità per lanciare i test non mi serve fare un RakeFile perchè potrei direttamente lanciare lcd_test.rb. Il fatto di avere rake mi torna utile quando voglio lanciare più di un file di test contemporaneamente, quindi il mio approccio, quando avrò più di una classe di test, sarà indicativamente: lancio il test singolo con xxx_test.rb per testare la classe xxx e lancio rake quando voglio testare tutto.
Iniziamo con lo scrivere 2 test a caso, uno per il numero ‘0’ e uno per il numero ‘5’ in modo da avere qualcosa su cui provare la nostra classe Lcd:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class LcdTest < Test::Unit::TestCase def setup @lcd0 = File.new('0.txt').read @lcd5 = File.new('5.txt').read end def test0 assert_equal(@lcd0, Lcd.process(0)) end def test5 assert_equal(@lcd5, Lcd.process(5)) end end |
I file 0.txt e 5.txt contengono rispettivamente:
1 2 3 4 5 |
- | | | | - |
1 2 3 4 5 |
- | - | - |
Tenere i casi di test fuori da lcd_test.rb mi permette di tenere la classe pulita. Ovviamente il test ora fallisce perchè non c’è l’implementazione del metodo Lcd.process(...).
Iniziamo a pensare a come risolvere il problema. I display LCD sono composti da 7 segmenti che illuminandosi o rimanendo spenti producono i vari numeri.

Quindi decido di implementare la rappresentazione di ogni numero con una maschera binaria di 7 bit che mi identificano se i vari led sono accesi(1) o spenti(0):
1 2 3 4 5 6 7 8 9 10 11 |
n. 01234567 (led) 0 => 1110111 1 => 0010010 2 => 1011101 3 => 1011011 4 => 0111010 5 => 1101011 6 => 1101111 7 => 1010010 8 => 1111111 9 => 1111011 |
ovvero in Ruby:
1 2 3 4 5 6 7 8 9 10 11 12 |
MASKS ={ '0' => 0b1110111, '1' => 0b0010010, '2' => 0b1011101, '3' => 0b1011011, '4' => 0b0111010, '5' => 0b1101011, '6' => 0b1101111, '7' => 0b1010010, '8' => 0b1111111, '9' => 0b1111011 } |
Non so bene perchè ma mi viene spontaneo wrappare i numeri Lcd e le maschere in un’oggetto separato:
in lcd_number.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class LcdNumber attr_reader :value, :mask MASKS ={'0' => 0b1110111, '1' => 0b0010010, '2' => 0b1011101, '3' => 0b1011011, '4' => 0b0111010, '5' => 0b1101011, '6' => 0b1101111, '7' => 0b1010010, '8' => 0b1111111, '9' => 0b1111011} def initialize value @value = value @mask = MASKS[value.to_s] end end |
e relativo test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class LcdNumberTest < Test::Unit::TestCase def setup; @one = LcdNumber.new 1 @two = LcdNumber.new 2 end def test_mask assert_equal 0b0010010, @one.mask assert_equal 0b1011101, @two.mask end def test_value assert_equal 1, @one.value assert_equal 2, @two.value end end |
Oddio! Appena inizio a parlare di maschere di bit la gente tipicamente inizia ad allarmarsi… di sicuro le operazioni bitwise non sono tra le più semplici e piacevoli da maneggiare. Non vi preoccupate non faremo niente di tutto questo. A noi in realtà server solo un modo per dire “questo led è acceso, questo no, questo, si…”.
In pratica avrei potuto fare un array di booleani per ogni numero, tipo:'0' => [true, true, true, false, true, true, true] |
oppure usare dei simboli, tipo:
'0' => [:on, :on, :on, :off, :on, :on, :on] |
...ma sinceramente la notazione binaria in questo caso mi sembra più compatta e più leggibile, quindi portate pazienza e non preoccupatevi perchè non userò operatori strano o shift di varia natura.
Ok. L’Hash MASKS viene utilizzata solo all’inizializzazione degli oggetti LcdNumber, dopo di che ogni istanza avrà i field value e mask a disposizione.
Su queste basi iniziamo a pensare come generare l’output. Siccome ho considerato la rappresentazione in Lcd come un’insieme di 7 led che si accendono e si spengono, posso ora incasellare questi led in righe diverse, la riga zero contiene il led 0, la riga 1 contiene i led 1 e 2, la riga 2 contiene il led 4 e così via.
E’ facile notare che ci sono 2 tipologie divese di righe, quelle che contengono un solo segmento e quelle che ne contengono due, ovvero righe even e odd (dato dall’indice di riga, 0 è even, 1 è odd).

devo quindi predisporre dei metodi che tengano in considerazione le due tipologie e che prendano in ingresso i bit della maschera per capire se i segmenti sono accesi o spenti e disegnarli di conseguenza. Ovviamente parto scrivendo il test:
in lcd_number_test.rb:1 2 3 4 |
def test_even_line assert_equal ' - ', LcdNumber.even_line(1) assert_equal ' ', LcdNumber.even_line(0) end |
1 2 3 4 5 |
def LcdNumber.even_line bit line = ' ' line << (bit.to_b ? '-':' ') line << ' ' end |
to_b? sta per to_boolean, infatti converte 0 in false e 1 in true. Questo metodo non c’è nelle librerie base di Ruby, l’ho preso da facets. Se non avete facets installato dovete scaricarvi la libreria con:
gem install facets |
e poi dovete aggiungere in cima a lcd_number.rb la seguente riga:
require 'facet/integer/to_b' |
che aggiunge il metodo to_b alla classe Numeric.
Passiamo ora all’implementazione del metodo che stamperà le righe dispari:
in lcd_number_test.rb:1 2 3 4 5 6 |
def test_odd_line assert_equal '| |', LcdNumber.odd_line(1,1) assert_equal '| ', LcdNumber.odd_line(1,0) assert_equal ' |', LcdNumber.odd_line(0,1) assert_equal ' ', LcdNumber.odd_line(0,0) end |
e in lcd_number.rb:
1 2 3 4 |
def LcdNumber.odd_line *bits chars = bits.map {|bit| bit.to_b ? '|':' '} chars.join(' ') # insert the empty char in the middle end |
Attenzione all’ultima riga del metodo odd_line. Infatti per generare la linea prima trovo i caratteri significativi (pipe o vuoto) e poi inserisco gli spazi interni (un pò strano ma funziona :-)).
A questo punto mi serve un metodo unico in grado di stampare le linee, siano esse even o odd, del numero Lcd. In input passerò uno o due bit della maschera a seconda che sia una riga di un tipo o dell’altro… ma per capire ciò che voglio parto scrivendo il test, il metodo lo chiamerò line e prenderà in ingresso l’indice di riga che voglio stampare (ricordatevi che il numero da stampare è implicito nella variabile value dell’istanza LcdNumber) :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def test_line assert_equal ' ', @one.line(0) assert_equal ' |', @one.line(1) assert_equal ' ', @one.line(2) assert_equal ' |', @one.line(3) assert_equal ' ', @one.line(4) assert_equal ' - ', @two.line(0) assert_equal ' |', @two.line(1) assert_equal ' - ', @two.line(2) assert_equal '| ', @two.line(3) assert_equal ' - ', @two.line(4) end |
una possibile implementazione di line è:
1 2 3 4 5 |
def line n bits = bits_for_line n return LcdNumber.odd_line(*bits) if n.odd? return LcdNumber.even_line(bits) if n.even? end |
Facile no? Questo metodo non fa altro che chiamare odd_line o even_line a seconda della riga che ho chiesto e si preoccupa di passare i bit corrispondenti.
Soffermiamoci un secondo sui metodi odd? e even?: nemmeno loro sono compresi nella standard library di Ruby. Facets ci viene in aiuto di nuovo, basta aggiungere in lcd_number.rb:
require 'facet/integer/odd' |
Questa non è l’unica cosa che manca per il metodo line, infatti usa un fantomatico bits_for_line che mi dovrebbe restituire i bit relativi alla riga che sto interrogando, prendendoli dalla mask. Poco male, visto che l’implementazione è banale, in lcd_numbers.rb metto:
1 2 3 4 5 6 7 8 9 10 |
#gives me back the bits (0 or 1) of the nth line def bits_for_line n case n when 0 then @mask[6] when 1 then [@mask[5], @mask[4]] when 2 then @mask[3] when 3 then [@mask[2],@mask[1]] when 4 then @mask[0] end end |
Qui utilizzo il metodo Fixnum#[] che restituisce il bit n-esimo (0 o 1) della maschera, dove fix[0] è il bit meno significativo (quindi l’ordine è rovescio rispetto a come ho scritto io le maschere di bit).
Finita l’implementazione di bits_for_line lancio lcd_number_test.rb per verificare che line funzioni a dovere.
Adesso viene il bello, devo implementare il metodo process in modo che invochi line correttamente e faccia finalmente girare i test in lcd_test.rb (ve li ricordate quelli con lo 0 e 5?).
1 2 3 4 5 |
line[0] #=> ' - -' line[1] #=> ' | | |' line[2] #=> ' - - ' line[3] #=> ' || |' line[4] #=> ' - - ' |
quindi scorro ripetutamente i numeri in input (1, 2 e 3) per ogni riga in modo da creare le righe complete. Poi ne faccio un join mettendo un ’\n’ alla fine di ogni riga:
in lcd.rb:1 2 3 4 5 6 7 8 9 10 11 |
def process string_number lines = [] (0..4).each do |line_number| string_number.each_char do |char| lcdn = LcdNumber.new(char.to_i) lines[line_number] ||= '' lines[line_number] << lcdn.line(line_number) end end lines.join("\n") end |
Per scorrere tutti i numeri dell’ipotetica stringa in input ‘123’, mi viene nuovamente in aiuto facets con il metodo each_char.
Inutile dire che dovete aggiungere a lcd.rb:
require 'facet/string/each_char' |
Adesso posso finalmente lanciare il test lcd_test.rb e verificare che i numeri 0 e 5 vengano generati correttamente come nei file 0.txt e 5.txt.
Purtroppo non possiamo ancora provare il nostro programmino dalla shell perchè non prende il parametro in input. Poco male in fondo a lcd.rb aggiungiamo:
1 2 3 4 5 |
if ARGV[0] puts Lcd.process(ARGV[0]) else puts 'usage: lcd.rb <NUMBER>' end |
Provo i soliti 0 e 5 da command line e verifico che sia tutto ok.
Adesso è ora di aggiungere gli altri test funzionali che mancano: 1.txt, 2.txt etc e aggiungerli a lcd_test.rb. Siccome le ripetizioni non mi piacciono cerco di generalizzare i test con un bell’eval che mi fa il setup dei field con caricate le rappresentazion Lcd dei numeri da 0 a 9 (@lcd0, @lcd1..@lcd9):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class LcdTest < Test::Unit::TestCase def setup @lcd = {} (0..9).each do |n| eval "@lcd[#{n}] = File.new('#{n}.txt').read" end end def test_all @lcd.each do |n, lcd_string| assert_equal lcd_string, Lcd.process(n), "testing number #{n}" end end end |
Rilancio i test (con rake stavolta perchè devono girare sia quelli di lcd_test.rb che quelli di lcd_number_test.rb) e verifico che sia tutto a posto.
Bene, se siamo arrivati fino a qui possiamo passare ad aggiungere la gestione del size, quindi creo un file di test per size=2:
size2_6.txt contiene:1 2 3 4 5 6 7 |
-- | | -- | | | | -- |
Aggiungo il test a lcd_test.rb:
1 2 3 4 |
def test_6_size2 @size2_6 = File.new('size2_6.txt').read assert_equal @size2_6, Lcd.process(n,2) end |
e modifico la classe LcdNumber in modo da gestirlo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class LcdNumber attr_reader :value, :mask, :size ... def initialize value, size=1 @value = value @mask = MASKS[value.to_s] @size = size end def line n bits = bits_for_line n return LcdNumber.odd_line(@size, *bits) if n.odd? return LcdNumber.even_line(@size, bits) if n.even? end ... def LcdNumber.odd_line size, *bits chars = bits.map {|bit| bit.to_b ? '|':' '} chars.join(' ' * size) # insert the empty char in the middle end def LcdNumber.even_line size, bit line = ' ' line << (bit.to_b ? '-':' ') * size line << ' ' end end |
In pratica ho aggiunto un *size in ogni punto dove devo espandere orizzontalmente la stampa dei numeri, e messo un default size=1 in inizializzazione. Sono anche costretto a modificare i test perchè odd_line e even_line ora prendono size come primo parametro. Ho fatto quindi l’espansione orizzontale della stampa, allungando di size volte il carattere centrale della rappresentazione Lcd. Non dimentichiamoci però che se aumenta il size deve aumentare anche l’altezza delle righe odd. Questo lo posso fare all’interno del process in questo modo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def Lcd.process size, string_number lines = [] (0..4).each do |line_number| string_number.each_char do |char| lcdn = LcdNumber.new(char.to_i, size) lines[line_number] ||= '' lines[line_number] << lcdn.line(line_number) end end lines[1] = (lines[1].to_a*size).join("\n") lines[3] = (lines[3].to_a*size).join("\n") lines.join("\n") end |
Una volta appurato che i test di LcdNumber girano ancora modifico la classe Lcd in modo che chiami il costruttore di LcdNumber con ciò che gli arriva dal parametro -s della command line.
Azz, a dire il vero il parametro size non l’ho ancora preso dalla command line!!! Siccome sono pigro e non ho voglia di farlo da solo mi appoggio alla mitica libreria optparse (inclusa in Ruby, niente installazioni stavolta…).
in lcd.rb:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
require 'optparse' class Lcd ... end def parse_options options = {} OptionParser.new do |opts| opts.on("-s [SIZE]") do |size| options[:size] = size.to_i end end.parse! options end options = parse_options options[:size] ||= 1 #imposto un default se non specificato if ARGV[0] puts Lcd.process(options[:size], ARGV[0]) else puts 'usage: lcd.rb [-s SIZE] NUMBER' end |
A questo punto posso fare:
1 2 3 4 5 6 |
$lcd.rb 6 - | - | | - |
1 2 3 4 5 6 7 8 9 10 11 12 |
$ lcd.rb 6 -s 4 ---- | | | | ---- | | | | | | | | ---- |
Ovviamente il parametro -s con optparse lo posso mettere sia prima che dopo il mio numero da stampare, infatti lcd.rb -s 4 6 dà lo stesso risultato.
Bene, abbiamo finito, non ci rimane che giocare allegramente con il nostro nuovo display LCD!!!
Note
Ci sono grossomodo altri 2 modi di affrontare questo problema, il primo usando dei template e il secondo con una state machine, potete trovare le soluzioni migliori sul sito RubyQuiz.com. Il codice riportato sicuramente non è il migliore, se avete idee o suggerimenti, fatevi sotto! Siamo qui per migliorarci no? Trovate i sorgenti completi della mia implementazione qui: lcd.zip
L’autore
Paolo si occupa da anni di sviluppo software per il web, tiene corsi di programmazione e va in giro per le fiere di settore a tenere qualche seminario. E’ inoltre fondatore di SeeSaw che si occupa di applicazioni ruby/rails. Nel (poco) tempo libero va in moto e suona la chitarra.