[Rio-pm] O programador mais fodido da cidade

breno breno em rio.pm.org
Sábado Fevereiro 11 10:04:46 PST 2012


2012/2/11 Aureliano Guedes <guedes_1000 em hotmail.com>:
> Bom dia, monges!
>
> Gostaria de saber se alguem pode me ajudar em uma questão peculiar.
>
> Eu não entendo muito de bitwise, a questão de bit e byte me confundem muito,
> mas eu queria perguntar como passar uma letra do minusculo para o maiusculo.
>

Isso tá com cara de trabalho de escola/faculdade :-)

Aureliano, a *grande* vantagem do Perl é não precisar ficar escovando
bits para trabalhar com strings. Consequentemente, a maneira mais
simples de se passar textos inteiros para maiúsculas é usar a função
"uc" do Perl:

my $maiuscula = uc $string;

Mas não foi isso que você perguntou, então vamos lá.

Perl oferece uma série de opções para manipulação binária, como os
operadores bit-a-bit ~, |, & e ^.

O bit tradicional é uma unidade básica de informação que só pode
assumir dois valores distintos, normalmente denotados como 1 e 0, e
interpretados conforme o caso: verdadeiro/falso, ligado/desligado,
etc. Na matemática popular usamos a base dez, já que cada casa pode
assumir até dez valores (0, 1, 2, 3, 4, 5, 6, 7, 8, 9). Quando somamos
1 ao número 9, essa casa volta ao 0 e adicionamos uma unidade à casa
da esquerda, certo? (por isso 9 + 1 = 10 ). Pois o mesmo acontece
quando estamos trabalhando com bits: como eles assumem dois valores
(0, 1), se somarmos 1 ao número 1, essa casa volta ao 0 e adicionamos
uma unidade à casa da esquerda. Assim, 2 é representado como "10" ("um
zero", e não "dez"). Podemos representar todos os números em qualquer
base. Veja abaixo alguns na base 2, com o equivalente em base 10 entre
parênteses:

base 2 (base 10)
0 (0)
1 (1)
10 (2)
11 (3)
100 (4)
101 (5)
110 (6)
111 (7)
1000 (8)
1001 (9)
1010 (10)
1011 (11)
1100 (12)
1101 (13)

e assim por diante. Com 1 bit podemos representar duas coisas (0, 1).
Com 2 bits, quatro coisas (00, 01, 10, 11). Com 3 bits, 8 coisas (000,
001, 010, 011, 100, 101, 110, 111). Com n bits, portanto, podemos
representar 2**n coisas.

O byte é outra unidade de informação, composta normalmente por 8 bits.
Desse modo, com 1 bytes (8 bits) podemos representar 2**8 == 256
coisas: de 00000000 a 11111111.

Existe uma diferença muito grande entre bytes e caracteres.

Tradicionalmente, escolheu-se 1 byte para representar um caractere,
especialmente no popular padrão ASCII. O problema é que há muito mais
de 256 caracteres a serem representados! Por isso, diferentes
codificações vem sendo usadas no mundo, a mais aceita hoje sendo o
utf-8, capaz de representar todos os 1.112.064 code-points
estabelecidos no padrão Unicode. Para isso, o utf-8 usa conjuntos
variáveis de 1 até 4 bytes (de 8 bits cada). No Brasil, outro encoding
popular é o iso-8859-1 (a.k.a. "latin 1"), mas este está em desuso e
vem sendo gradativamente substituido pelo utf-8.

Para mais informações sobre encoding e como fazer as coisas da forma
correta, veja o excelente artigo do Stan:

   http://sao-paulo.pm.org/artigo/2011/PERLEUNICODEENTREOUTRASCODIFICACOESDETEXTO

Para o resto dessa explicação, vou assumir que estamos falando de
ASCII tradicional, em que só alguns caracteres são representados e no
qual 1 byte == 1 caractere.

Na tabela ASCII
(https://en.wikipedia.org/wiki/Ascii#ASCII_printable_characters), os
valores que representam letras são sequenciais, de 65 ("A") até 90
("Z"), e de 97 ("a") até 122 ("z"). Ao colocarmos esses valores na
base 2, vemos que eles vão de 01000001 (65) até 01011010 (90), e de
01100001 (97) até 01111010 (122). A sua observação, portanto, está
correta, e *nessa codificação* basta trocar o bit na casa 5 (contando
da direita para a esquerda, a partir da casa 0). Mas como fazer isso?

Operações bit-a-bit:

Para manipular elementos que só podem assumir dois valores distintos
(bits), utilizamos a álgebra booleana. Nela, observamos as seguintes
operações básicas:

E (AND) - Só é "verdadeira" (1) quando os dois elementos são verdadeiros:
0 & 0 == 0
0 & 1 == 0
1 & 0 == 0
1 & 1 == 1

OU (OR) - É verdadeira se pelo menos um dos dois elementos for verdadeiro:
0 | 0 == 0
0 | 1 == 1
1 | 0 == 1
1 | 1 == 1

NÃO (NOT) - inverte o elemento:
~ 0 == 1
~ 1 == 0

Há também o "ou exclusivo" (xor), verdadeiro se somente um dos
elementos for verdadeiro:

0 ^ 0 == 0
0 ^ 1 == 1
1 ^ 0 == 1
1 ^ 1 == 0

Assim, se você quiser garantir que o bit na quinta posição está sempre
desativado, pode fazer uma operação de "E", aplicando o que é
conhecido como "máscara de bits":

my $maiuscula = chr( ord($letra) & 0b11011111 );

O que isso faz? Vamos separar em pedaços:

my $letra = 'a';
my $numero = ord $letra;
my $resultado = $numero & 0b11011111;
my $maiuscula = chr $resultado;

A primeira linha é mole: criamos a variável $letra com o caractere 'a'.

A segunda linha usa a função ord() para retornar o valor numérico (em
8 bits) do primeiro caractere da string. Em nosso caso, como só temos
1 caractere, vai retornar o valor numérico de 'a', que como vimos é 97
em decimal e 01100001 em 8 bits.

A terceira linha faz a operação. Em Perl, a base 10 é assumida, mas
podemos representar números em outras bases prefixando com 0 (para
octal), 0x (para hexadecimal) ou 0b (para binário). Observe a tabela
do "E" acima e verá que "valor & 1" é sempre o próprio valor. Isto é,
0 & 1 == 0, e 1 & 1 == 1. Assim, se sabemos que $numero tem 8 bits,
podemos fazer $numero & 0b11111111 e obter o próprio $numero. O legal
disso é que, se "virarmos" o bit que queremos para 0, ele vai ser
sempre 0, pois 0 & 0 == 0 e 1 & 0 == 0. Assim, nossa máscara de bits
assume o valor 0b11011111, e a operação vai garantir que o número
permaneça o mesmo exceto pelo bit na quinta casa, que agora é 0.

Finalmente, na quarta linha, usamos a função chr() para retornar o
caractere representado pelo número, que será o mesmo caractere ASCII
original só que sempre maiúsculo. Nesse caso, como usamos 'a', o valor
será 'A'.

Algumas observações importantes: utilizei valores em binário por conta
da sua pergunta, mas vale a pena notar que os números são os mesmos em
qualquer base. Assim, sabendo que nossa máscara 11011111 em decimal é
223, podemos reescrever a linha:

my $maiuscula = chr( ord($letra) & 0b11011111 );

como:

my $maiuscula = chr( ord($letra) & 223 );

Ou ainda, sabendo que os caracteres na tabela ASCII são sequenciais,
podemos observar que a "distância" de 'A' até 'a' são 97 - 65 == 32
caracteres. Assim, passar um caractere para maiúsculas nessa tabela é
apenas uma questão de subtrair 32 do valor numérico:

my $maiuscula = chr( ord($letra) - 32 );

O problema dessa última abordagem é que só vale se você tiver certeza
que o caractere original é minúsculo. Se passar maiúsculo, a conta dá
errado. Já as outras soluções apresentadas com "&", por tratar-se de
uma máscara, funcionam para maiúsculas e minúsculas da tabela ASCII.

Portanto, uma potencial solução para "passar todos os caracteres de
uma palavra contendo apenas a-zA-Z, sem espaços nem pontuação, em
ASCII para maiúsculas" seria:

----------------8<----------------
sub meu_uc {
    my $string = shift;
    my $maiuscula = q();
    foreach my $chr (split //, $string) {
        $maiuscula .= chr( ord($chr) & 223 );
    }
    return $maiuscula
};
---------------->8----------------

Esse tipo de coisa que me faz valorizar o Perl ainda mais. A função
'uc' é extremamente simples e faz a coisa certa até para encodings
diferentes, sem que precisemos nos preocupar.

Enfim, espero ter ajudado. Desculpe o email longo, vc disse que era
iniciante então tentei ser o mais didático e detalhado possível :-)

[]s

-b


Mais detalhes sobre a lista de discussão Rio-pm