sinais, passo 3: gerar a lista de caracteres
Nosso objetivo agora é produzir a saída do programa, uma listagem com este formato:
U+0041→ A→ LATIN CAPITAL LETTER A
U+0042→ B→ LATIN CAPITAL LETTER B
U+0043→ C→ LATIN CAPITAL LETTER C
(acrescentei setas → para indicar as tabulações, que aparecerão no código como \t)
A maneira mais simples de conferir a saída padrão de um programa em Go é usar um exemplo: um tipo especial de teste, feito com uma função nomeada com o prefixo Example. Vamos criá-la no arquivo de testes runefinder_test.go assim:
func ExampleListar() { // ➊
texto := strings.NewReader(linhas3Da43) // ➋
Listar(texto, "MARK") // ➌
// Output: U+003F ? QUESTION MARK
}
Observe:
➊ O nome da função precisa começar com Example. Em seguida obrigatoriamente vem o nome da função a ser testada, por isso: ExampleListar.
➋ Nossa função Listar receberá um argumento io.Reader (ao final, será o arquivo UnicodeData.txt). Para testar, construímos um buffer de leitura strings.Reader a partir da constante linhas3Da43, cujo conteúdo veremos abaixo. Não esqueça de importar o pacote strings.
➌ Aqui invocamos a função a testar, passando o buffer e a consulta, "MARK". O comentário na linha final da função ExampleListar define o resultado esperado. O sistema de testes vai comparar o texto gerado pela função Listar na saída padrão com o que vier após a string "Output: " no comentário.
Neste exemplo, a função Listar produz uma listagem delimitada por tabs, portanto o comentário // Output: precisa ser escrito com tabs entre os campos U+003F, ? e QUESTION MARK. Se você colocar espaços em vez de tabs entre esses campos, o teste não passará.
A constante linhas3Da43 que usamos nesse teste é definida assim, no topo do arquivo runefinder_test.go:
const linhas3Da43 = `
003D;EQUALS SIGN;Sm;0;ON;;;;;N;;;;;
003E;GREATER-THAN SIGN;Sm;0;ON;;;;;Y;;;;;
003F;QUESTION MARK;Po;0;ON;;;;;N;;;;;
0040;COMMERCIAL AT;Po;0;ON;;;;;N;;;;;
0041;LATIN CAPITAL LETTER A;Lu;0;L;;;;;N;;;;0061;
0042;LATIN CAPITAL LETTER B;Lu;0;L;;;;;N;;;;0062;
0043;LATIN CAPITAL LETTER C;Lu;0;L;;;;;N;;;;0063;
`
Note que usamos o sinal de crase (grave accent, Unicode U+0060) para delimitar uma string que tem múltiplas linhas. As quebras de linha farão parte do valor da constante.
Seguindo a filosofia do TDD, vamos rodar os testes:
$ go test
# github.com/ThoughtWorksInc/sinais
./runefinder_test.go:33: undefined: Listar
FAIL github.com/ThoughtWorksInc/sinais [build failed]
Claro que falhou porque ainda não escrevemos a função Listar. Vamos começar implementando essa função da forma mais simples possível, apenas para fazer o teste passar:
func Listar(texto io.Reader, consulta string) { // ➊
runa, nome := '?', "QUESTION MARK" // ➋
fmt.Printf("U+%04X\t%[1]c\t%s\n", runa, nome) // ➌
}
Passo a passo:
➊ Listar recebe um texto do tipo io.Reader e uma consulta do tipo string.
➋ Por enquanto vamos chumbar aqui os valores esperados por nosso teste.
➌ Aqui usamos fmt.Printf com três verbos de formatação indicados por %, explicados a seguir. Note os tabs \t e a quebra de linha \n para formatar a saída.
Verbos de formatação que usamos:
%04Xpara exibir um inteiro (o valor deruna) em formato hexadeximal com 4 casas, preenchendo com zeros à esquerda, mostrando os dígitos deAaFem maiúsculas.%cé para exibir uma runa como caractere; o modificador[1]é para indicar que queremos usar o argumento 1 (runa) novamente nesta posição, e não o argumento seguinte (nome). Assim geramos três campos na saída usando apenas dois argumentos, porque usamosrunaduas vezes.%spara exibir um valorstring.
Na realidade, io.Reader é uma interface, o que significa que nossa função Listar aceita como primeiro argumento qualquer objeto que implemente o método Read conforme a documentação. Isso facilita os testes: podemos passar um buffer em vez de um arquivo para testar a função Listar. Em geral, é uma boa ideia escrever funções que aceitam interfaces como argumento, porque isso dá mais flexibilidae para quem vai usar nossa API.
Para usar fmt.Printf e io.Reader na função Listar, temos que acrescentar os pacotes fmt e io à declaração import no arquivo runefinder.go, mantendo a ordem alfabética como pede a boa educação na comunidade Go:
import (
"fmt"
"io"
"strconv"
"strings"
)
Essa implementação marota de Listar é suficiente para fazer passar o teste ExampleListar. Veja o resultado de go test com a opção -v (saída verbosa):
$ go test -v
=== RUN TestAnalisarLinha
--- PASS: TestAnalisarLinha (0.00s)
=== RUN ExampleListar
--- PASS: ExampleListar (0.00s)
PASS
ok github.com/ThoughtWorksInc/sinais 0.001s
Agora vamos codar a lógica da função Listar.
Implementando Listar de verdade
Antes de mais nada, criamos outro teste para expor o problema da nossa função Listar, que simplesmente ignora os argumentos passados. Usaremos outra função exemplo:
func ExampleListar_doisResultados() { // ➊
texto := strings.NewReader(linhas3Da43)
Listar(texto, "SIGN") // ➋
// Output:
// U+003D = EQUALS SIGN
// U+003E > GREATER-THAN SIGN
}
➊ Como já vimos, a função exemplo sempre tem o prefixo Example seguido do nome da função a ser testada. Para fazer mais de um exemplo testando a mesma função Listar, coloque um _ seguido de uma descrição iniciando com caixa baixa (obrigatoriamente). Por isso temos: ExampleListar_doisResultados.
➋ Agora vamos testar a palavra "SIGN", que ocorre duas vezes em nosso texto de testes. Para verificar saídas de várias linhas, escreva Output: na primeira linha do comentário, e coloque as linhas esperadas nos comentários seguintes, sempre colocando um espaço após o sinal //. Esse espaço será ignorado no teste.
Note que a saída esperada neste teste é formatada em três colunas separadas por tab.
Se rodarmos os testes agora, veremos isto:
$ go test
--- FAIL: ExampleListar_doisResultados (0.00s)
got:
U+003F ? QUESTION MARK
want:
U+003D = EQUALS SIGN
U+003E > GREATER-THAN SIGN
FAIL
exit status 1
FAIL github.com/ThoughtWorksInc/sinais 0.001s
A palavra got: (recebido) indica a saída que foi produzida, e want: (desejado), a saída que era esperada. Naturalmente o teste não passou porque nossa função Listar mostra sempre o mesmo resultado. Vamos consertar isso, com este código que traz várias novidades:
// Listar exibe na saída padrão o código, a runa e o nome dos caracteres Unicode
// cujo nome contém o texto da consulta // ➊
func Listar(texto io.Reader, consulta string) {
varredor := bufio.NewScanner(texto) // ➋
for varredor.Scan() { // ➌
linha := varredor.Text() // ➍
if strings.TrimSpace(linha) == "" { // ➎
continue
}
runa, nome := AnalisarLinha(linha) // ➏
if strings.Contains(nome, consulta) { // ➐
fmt.Printf("U+%04X\t%[1]c\t%s\n", runa, nome)
}
}
}
O que temos de novo:
➊ Por convenção, funções exportadas (públicas) devem ser documentadas com um comentário logo acima. Mais detalhes sobre essa convenção na seção Documentando funções, no final deste passo.
➋ Para percorrer um io.Reader linha-a-linha, usamos a função bufio.NewScanner, que devolve um objeto que implementa a interface Scanner (documentação).
➌ Um dos métodos do tipo Scanner é Scan: ele avança o Scanner até a próxima quebra de linha, e devolve true enquanto existir texto para ler, e enquanto não ocorrer um erro. Aqui usamos o resultado de Scan como condição para um laço for.
➍ Cada vez que invocamos Scan podemos usar o método Text() para obter a linha que acabou de ser lida.
➎ Aqui retiramos os caracteres brancos (whitespace) à esquerda e à direita da linha; se o resultado for uma string vazia, usamos continue para iniciar a próxima volta do laço porque não há o que fazer.
➏ Passamos a linha para a função AnalisarLinha, que devolve a runa e seu nome.
➐ Se o nome contém a string consulta, então geramos uma linha na saída, no formato que já vimos anteriormente.
Duas observações sobre a linha ➌:
- Não existe
whilena linguagem Go; a instruçãoforpode ser usada comowhiledessa forma:for «condição» {...}. - Um
Scannerpode ser configurado para iterar peloReaderde outras formas, além de linha-a-linha. Veja a documentação do métodoScanner.Splite das funçõesScanLines,ScanRunes,ScanWordseScanBytes.
Para que a função Listar funcione, precisamos importar o pacote bufio. A declaração import ficará assim:
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
)
Note a ordem alfabética dos pacotes. O utilitário go fmt ordena os pacotes automaticamente, e o goimports insere/remove pacotes da declaração import automaticamente para satisfazer o compilador. No editor Atom, o pacote go-plus executa estes e outros utilitários de verificação de código toda vez que eu salvo o arquivo, além de rodar os testes.
Com isso, todos os testes passam novamente:
$ go test -v
=== RUN TestAnalisarLinha
--- PASS: TestAnalisarLinha (0.00s)
=== RUN ExampleListar
--- PASS: ExampleListar (0.00s)
=== RUN ExampleListar_doisResultados
--- PASS: ExampleListar_doisResultados (0.00s)
PASS
ok github.com/ThoughtWorksInc/sinais 0.001s
Documentando funções
Por convenção, funções exportadas (públicas) dever ser precedidas de um comentário, como vimos no exemplo da função Listar. Eis o cabeçalho desta função:
// Listar exibe na saída padrão o código, a runa e o nome dos caracteres Unicode
// cujo nome contém o texto da consulta // <1>
func Listar(texto io.Reader, consulta string) {
O comentário deve começar com uma frase completa iniciando com o nome da função e terminando com ponto final. A ferramenta golint aponta a falta de tais comentários.
O comando go doc exibe as assinaturas das funções de um pacote:
$ go doc
package runefinder // import "github.com/ThoughtWorksInc/sinais"
func AnalisarLinha(linha string) (rune, string)
func Listar(texto io.Reader, consulta string)
E se você informar o nome de uma função, go doc mostra sua assinatura e documentação, assim:
$ go doc listar
func Listar(texto io.Reader, consulta string)
Listar exibe na saída padrão o código, a runa e o nome dos caracteres
Unicode cujo nome contém o texto da consulta
Com isso terminamos o passo 3. Estamos próximos do MVP: o produto mínimo viável é explicado no Passo 4.