Displaying articles with tag

Il mio primo Ruby Quiz

Posted by paolo, Thu Feb 08 19:17:00 UTC 2007

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:

in lcd.rb:
1
2
3
4
5
class Lcd
  def Lcd.process number
    '' # do nothing righ now
  end
end
in lcd_test.rb:
1
2
3
4
require 'test/unit'
require 'lcd'
class LcdTest < Test::Unit::TestCase
end
in RakeFile:
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
 -
| |

| |
 - 
e:
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.rb
1
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
in lcd_number.rb:
1
2
3
4
5
def LcdNumber.even_line bit
  line = ' ' 
  line << (bit.to_b ? '-':' ')
  line << ' '
end
Aveto notato il metodo 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) :

in lcd_number_test.rb:
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?).

Decido di seguire questa strategia: mi faccio un array che conterrà le righe di tutti i numeri in input per avere qualcosa di questo tipo (immaginate che debba stampare 123):
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
 -
|
 -
| |
 -
oppure usare lo switch -s:
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.

6 comments | Filed Under: Linguaggio | Tags: