Como Encontrar Todos os Sinônimos de um Nome (Problema Geral)

Introdução

Precisei normalizar os nomes de alguns genes humanos e inicialmente pensei que seria uma tarefa fácil, porém ao analisar as saídas, verifiquei que nem todos os nomes foram normalizados. O problema evoluiu e encontrei a solução lá na área de Teoria de Grafos.

A postagem de hoje é para resolver qualquer problema em que entidades (ex: genes) podem ser referenciadas por vários outros nomes em que tais nomes não estão agrupados de forma estruturada, mas sim espalhados por todo um banco de dados.

O problema

Suponha que você tenha essa cadeia de nomes:

A,B,C,D,E,F

O que ela diz é que A é sinônimo de B, de C, de E, etc. Também diz que B e C, B e D, etc são sinônimos. Basicamente, de forma geral, onde aparecer qualquer uma dessas letras, está se referenciando a mesma entidade. Um processo de normalização seria quando aparecer as letras {B,C,D,E,F} trocar pela a letra {A}, por exemplo.

Agora, suponha que você tenha várias dessas cadeias:

A,B,C,D,E,F
K,L,O
J,M,D
M,O,P,E

seguindo o mesmo padrão, todos os símbolos na mesma linha representam a mesma entidade. Como podemos ver, há um problema nessa abordagem. A terceira linha contém o símbolo “D”, indicando que {J,M} também referencia a mesma entidade que {A,B,C,E,F}.

O mesmo ocorre na última linha, onde aparece o símbolo E, o que acrescenta mais três nomes novos, {M,O,P}, para representar a mesma entidade. Ainda mais, o “O” aparece na segunda linha, o que faz que todos os nomes mostrados representem a mesma entidade.

Todas as linhas poderiam ser reduzidas a:

A,B,C,D,E,F,K,L,J,M,O,P

O problema está em encontrar tal redução dado que os nomes estão espalhados no arquivo de entrada.

Vantagem de nomes únicos

Uma vantagem de normalizar nomes é que se entidades se relacionam entre si (ex: um grafo), o uso de nomes únicos aumenta o número de relações (alguns vértices se tornam o mesmo), torna o grafo mais denso e, consequentemente, menor.

É só imaginar que se um grafo era formado por:

A -> X
E -> Y
P -> X

então o novo grafo será formado somente por:

A -> X
A -> Y

já que P -> X é duplicado e E tem o mesmo nome de A.

A solução ingênua

A solução ingênua seria pegar cada nome de uma linha e verificar se ele aparece no restante do arquivo. Se sim, junte as linhas em que ele aparece e recomece a busca usando outro nome porém sem acrescentar linhas já coadunadas anteriormente. Parece um problema complexo, porque realmente é, e a complexidade estoura se o arquivo consultado for grande, pois o tempo é da ordem de n^2.

A solução: Componentes Conexas

Se você observar bem, as linhas do arquivo problema é de fato uma formulação de um grafo. Todos os nomes na mesma linha ligam entre si, como também os vértices que são referenciados em outras linhas. Logo, a solução se passa na área de Teoria de Grafos.

Se tratarmos cada nome como um vértice de um grafo e as arestas representarem os vértices associados como sinônimos, então a solução é basicamente sair de um vértice e verificar quais outros vértices são alcançáveis a partir dele. Esse conjunto de vértices contém todos os sinônimos para uma mesma entidade. Ora, isso nada mais é que encontrar as componentes conexas de um grafo não direcionado [2].

Para quem faltou as aulas de Grafos ou não conhece muito sobre o assunto, uma componente conexa (CC) é um conjunto de vértices que estão conectados, isso é, para cada par (i, j) de vértices desse conjunto, você consegue sair de i e chegar em j.

Em relação ao nosso problema, o conjunto de vértices de uma CC representa todos os nomes associados a mesma entidade. Para normalizar, basta eleger um vértice da CC como nome geral e substituir todas ocorrências dos outros vértices por esse nome.

Encontrar todas as CC’s é realizar uma busca em largura a partir de um vértice e salvar os vértices alcançáveis, depois realizar a busca novamente iniciando de outro vértice e continuar assim até que todos os vértices sejam visitados.

Código

O código eu peguei pronto de [1] e está escrito em python. Fiz uma pequena modificação para ler de um arquivo fixo de entrada, chamado de entrada.txt.

#!/usr/bin/env python

# Finding connected components in a bidirectional graph.
# By Mario Vilas (mvilas at gmail dot com)
# http://breakingcode.wordpress.com/2013/04/08/finding-connected-components-in-a-graph

# [ cc.py ]
# Modificado ligeiramente para ler de um arquivo fixo: entrada.txt
# daemoniolabs.wordpress.com
# Sat Oct 15 20:26:52 BRT 2016

# The graph nodes.
class Data(object):
    def __init__(self, name):
        self.__name  = name
        self.__links = set()

    @property
    def name(self):
        return self.__name

    @property
    def links(self):
        return set(self.__links)

    def add_link(self, other):
        self.__links.add(other)
        other.__links.add(self)

# The function to look for connected components.
def connected_components(nodes):

    # List of connected components found. The order is random.
    result = []

    # Make a copy of the set, so we can modify it.
    nodes = set(nodes)

    # Iterate while we still have nodes to process.
    while nodes:

        # Get a random node and remove it from the global set.
        n = nodes.pop()

        # This set will contain the next group of nodes connected to each other.
        group = {n}

        # Build a queue with this node in it.
        queue = [n]

        # Iterate the queue.
        # When it's empty, we finished visiting a group of connected nodes.
        while queue:

            # Consume the next item from the queue.
            n = queue.pop(0)

            # Fetch the neighbors.
            neighbors = n.links

            # Remove the neighbors we already visited.
            neighbors.difference_update(group)

            # Remove the remaining nodes from the global set.
            nodes.difference_update(neighbors)

            # Add them to the group of connected nodes.
            group.update(neighbors)

            # Add them to the queue, so we visit them in the next iterations.
            queue.extend(neighbors)

        # Add the group to the list of groups.
        result.append(group)

    # Return the list of groups.
    return result

# The test code...
if __name__ == "__main__":

    f = open("entrada.txt") 
    LINHAS = f.readlines()

    # Dicionario: key (sinonimo) => value (objeto node correspondente)
    NOMES={}
    for linha in LINHAS:
        linha = linha.rstrip('\n')
        genes = linha.split(',')
        for g in genes:
            if len(g) == 0: continue
            if not NOMES.has_key(g):
                NOMES[g]=Data(g)

    for linha in LINHAS:
        linha = linha.rstrip('\n')
        n = linha.split(',')
        if len(n) > 1:
            for i in range(len(n)):
                if len(n[i]) == 0: continue
                for j in range(len(n)):
                    if len(n[j]) == 0: continue
                    if i != j:
                        a = NOMES[n[i]]
                        b = NOMES[n[j]]
                        a.add_link(b)

    nodes = NOMES.values()
    # Find all the connected components.
    number = 1
    for components in connected_components(nodes):
        names = sorted(node.name for node in components)
        names = ",".join(names)
        #print "Group #%i:%s" % (number, names)
        print names
        number += 1

    # You should now see the following output:
    # Group #1: a, b, c, d, e, f
    # Group #2: g
    # Group #3: h, i, j, k

Exemplo: Genes humanos

Vejamos um exemplo mais complexo envolvendo genes humanos. Esse é um modelo do arquivo de entrada, o entrada.txt, em que linhas são nomes/sinônimos separados por vírgula.

DPCK,EEC3,ERGIC63,ERGIC_63,GPAT,HCP5,KET,CKAP4
SHFM4,TP53CP,TP53L,P63
ELAC2,FLJ10530,HPC2,CBX4
HPC8,HSPC187,NBP16,NEC2,PC2,PCAP,ELAC2
PCSK2,PC_2,PKD2,PKD4,HPC2
GCFC2,GC_F,GCF,GUC2DL,GUCY2F,RETGC2
RETGC_2,ROSGC2,ROS_GC2,TCF9,GCF
AIE75,AIE_75,DFNB18,
HARMONIN,NYCO37,NYCO38,NY_CO_37,NY_CO_38,PDZ73
C2ORF3,CYGF,DNABF,GCF
PDZ73,PDZD7C,PDZ_73,USH1C,AIE75
TP63,TP73L,ZER6,ZNF398
CKAP4,CLIMP63,CLIMP_63,COASY,CYCSP5,D6S2650E,TP73L
PRKD2,SPC2,TRPP2,PKD2
CBX4,CSAD,CSD,DKFZP586E0820
P63,P71,P73H,P73L,PPAT,PRAT,NBP
KIAA1339,NBP,OFC8,P51,P53CP,P5_1,EEC3

Genes na mesma linha são sinônimos e um gene que aparece em duas linhas distintas torna essas linhas sinônimas. Um exemplo é a primeira e a última linha que compartilham o gene “EEC3”. Essas linhas também são sinônimas da penúltima linha porque esta e a última compartilham o gene “NPB”.

Vamos rodar o código cc.py para encontrar as componentes:

$ python cc.py
AIE75,AIE_75,DFNB18,HARMONIN,NYCO37,NYCO38,NY_CO_37,NY_CO_38,PDZ73,PDZD7C,PDZ_73,USH1C

CKAP4,CLIMP63,CLIMP_63,COASY,CYCSP5,D6S2650E,DPCK,EEC3,ERGIC63,ERGIC_63,GPAT,HCP5,KET,KIAA1339,NBP,OFC8,P51,P53CP,P5_1,P63,P71,P73H,P73L,PPAT,PRAT,SHFM4,TP53CP,TP53L,TP63,TP73L,ZER6,ZNF398

CBX4,CSAD,CSD,DKFZP586E0820,ELAC2,FLJ10530,HPC2,HPC8,HSPC187,NBP16,NEC2,PC2,PCAP,PCSK2,PC_2,PKD2,PKD4,PRKD2,SPC2,TRPP2

C2ORF3,CYGF,DNABF,GCF,GCFC2,GC_F,GUC2DL,GUCY2F,RETGC2,RETGC_2,ROSGC2,ROS_GC2,TCF9

Ele encontrou 4 componentes conexas, cada uma em uma linha. Isso indica que todos os genes na mesma linha são sinônimos. A versão em grafo seria:

Grafo dos genes sinônimos

Grafo dos genes sinônimos. Quatro componentes conexas. Gerado pelo GraphViz.

E como normalizar?

Para normalizar é preciso eleger um nome dentro de cada CC e substituir todos os outros nomes por ele. Isso vai depender do problema e de como ele é apresentado. Basicamente, rode um “sed” ou equivalente para trocar todos os nomes de um conjunto pelo nome eleito daquele conjunto. Não mostrarei tal implementação aqui, se precisar de ajuda é só postar nos comentários.

Conclusão

Um problema comum é encontrar os sinônimos para uma mesma entidade tal que os nomes estão espalhados por todo um arquivo de entrada. Vimos que para agrupar todos os sinônimos é necessário considerar o arquivo de entrada como um grafo, em que cada nome é um vértice e uma aresta indica que os vértices são sinônimos, e a solução é encontrar todas as componentes conexas desse grafo.

Referências

[1] “Find Connected Components in a Graph” by breakingcode (http://archive.is/VAcdP) (Acessado em: Outubro/2016)
http://breakingcode.wordpress.com/2013/04/08/finding-connected-components-in-a-graph

[2] Connected component (graph theory) by wikipedia (http://archive.is/OQN1h) (Acessado em: Outubro/2016)
https://en.wikipedia.org/wiki/Connected_component_%28graph_theory%29

Gerando Combinações Iterativamente Usando Pilhas em C

Introdução

Combinações são agrupamentos desordenados de elementos de um determinado conjunto. A necessidade de se gerar combinações de elementos é uma tarefa bastante comum em computação, especialmente quando se está trabalhando com algoritmos de força bruta em que os agrupamentos devem ser testados um por um a fim de se obter a solução ótima de um problema.

Frequentemente, esses agrupamentos são gerados de forma recursiva, embora ela seja mais fácil de implementar, ela fica limitada à pilha do sistema operacional, o que inviabiliza a geração de muitos agrupamentos.

Nessa postagem, iremos aprender como gerar as combinações de fora iterativa usando a TAD Pilha como estrutura de armazenamento. Se você deseja somente um código para combinar elementos de um vetor, pule para o fim da postagem.

Combinações-r

Dado um conjunto de N elementos, gerar as combinações-r dele é mostrar todas as sequências desordenadas de r elementos desse grupo. Isso é, para r=3, os agrupamentos de três elementos, {1, 2, 3} e {1, 3, 2}, representam o mesmo agrupamento, pois a ordem dos elementos não gera agrupamentos diferentes.

Em se tratando de combinações sem repetição, o valor de r deve ser menor ou igual a N. O total de agrupamentos é obtido pelo coeficiente binomial: N!/r!(N-r)!.

Ao se variar o r de 0 a N, isso é, gerar todas as combinações de qualquer tamanho, forma-se um outro conjunto, chamado de conjunto potência, que é definido como o conjunto de todos os subconjuntos de um conjunto. O tamanho do conjunto potência é 2^N.

TAD Pilha

Um Tipo Abstrato Pilha fornece duas operações básicas: Empilha() e Desempilha(). Dado uma lista de elementos, restringimos o acesso a ela impondo que um elemento só é acrescentando ou retirado de seu topo. A operação Empilha() serve para adicionar um elemento no topo e a Desempilha() retira esse elemento do topo, retornando-o.

Um esquema para se entender a pilha é lembrar da pilha de pratos de um restaurante: assim que pratos sujos vão chegando, eles são colocados (empilhados) um em cima do outro. O primeiro prato que será lavado (ou desempilhado) será o que está no topo da pilha.

Intuição

Primeiramente, vamos observar a intuição de como as combinações são geradas. Suponha, por exemplo, os seguintes elementos {1, 2, 3, 4, 5} e que queiramos gerar combinações de r=3 elementos. Os agrupamentos formados serão:

{1, 2, 3}, {1, 2, 4}, {1, 2, 5}, {1, 3, 4}, {1, 3, 5}, {1, 4, 5}
{2, 3, 4}, {2, 3, 5}, {2, 4, 5}
{3, 4, 5}

O total de combinações-r dado N elementos é 5!/2!3! = 10.

A primeira linha contém todos os agrupamentos que começam com 1, a segunda todos com 2 e a terceira com 3. Embora os agrupamentos sejam desordenados, podemos utilizar um padrão ordenado para termos uma ideia de como gerar todos eles.

A regra para obter os agrupamentos da linha 1 é: fixe dois elementos {1, 2}. Acrescente nesse conjunto todos os N-2=3 elementos restantes, cada um formando uma nova combinação: {1, 2, 3}, {1, 2, 4}, {1, 2, 5}. Como não há como acrescentar mais elementos, então trocamos o segundo elemento para o 3, e adicionamos os N-3=2 elementos restantes: {1, 3, 4}, {1, 3, 5}. Não há mais elementos, então trocamos o segundo por 4, e adicionamos os N-4=1 elementos restantes, formando {1, 4, 5}. Ao se trocar o segundo elemento por 5, não temos demais elementos a adicionar, pois N-5=0. Isso acaba as combinações da primeira linha.

E para gerar as combinações da segunda linha? É o mesmo processo, porém todas elas devem começar com o com 2. Fixando os dois primeiros elementos, {2, 3}, adicionamos os N-3=2 elementos seguintes, formando {2, 3, 4}, {2, 3, 5}. Trocamos o 3 pelo 4 e adicionamos os N-4=1 elementos restantes: {2, 4, 5}. Trocamos o 4 pelo 5 e observamos que N-5=0, então mudamos de linha.

Para a terceira linha, fixamos dois elementos: {3, 4}. Adicionamos os N-4=1 elementos restantes, formando {3, 4, 5}. Trocamos o 4 pelo 5 e vemos que não há como adicionar mais elementos, dessa maneira, trocamos de linha. O primeiro elemento será o 4 e fixando dois elementos, {4, 5} adicionamos os N-5=0 elementos restantes. Porém não temos esse elemento, e assim nada é gerado e finalizamos o algoritmo.

Generalizando

Para se elaborar um conceito geral é preciso usar os benefícios da pilha. Dada uma combinação qualquer, por exemplo, {1, 2, 3}, temos que pensar nela como os elementos de uma pilha, sendo o mais à direita, o 3, o topo. Para se gerar novas combinações, o topo é desempilhado, incrementado e o resultado empilhado. Nesse caso em particular, o 3 seria desempilhado e o 4 empilhado no lugar, formando assim o novo agrupamento {1, 2, 4}. Em alguma hora será impossível variar o topo, como em {1, 2, 5}. Nessa situação, o 5 é desempilhado mas não é incrementado. A operação de desempilhar se propaga para o 2, e o 3 é empilhado no lugar. O próximo elemento ao 3 é o 4, então ele é empilhado para formar a combinação {1, 3, 4}. Por esse simples esquema, vemos que sempre que um elemento é desempilhado, seu próximo elemento é que entrará na pilha. Caso ele não possa ser variado, a operação de desempilhar é propagada até que se encontre um possível elemento de ser variado. Se isso não ocorrer, o processo se finaliza. A imagem abaixo esquematiza isso:

Esquema Pilha

Figura 1: Esquema Pilha

As linhas em vermelho são combinações válidas. As verdes são empilhamentos feitos até que a pilha atinja um tamanho r ou até quando não há mais elementos a adicionar. As linhas em azul é o estado da pilha após um desempilhamento (último elemento impossível de ser variado).

Pseudocódigo

O pseudocódigo a seguir representa o que foi visto.

     1	   i=1
     2	   N = número de elementos
     3	   P = pilha inicialmente vazia
       
     4	   while(Pilha não vazia ou i < N)
     5	      para cada elemento c próximo ou igual a i
     6	         Empilha(P, c)
       
     7	         if tamanho P == r
     8	            { temos uma combinação válida quando
     9	            { o tamanho da Pilha for igual ao r passado }
    10	            combinacao = itens de P
       
    11	            { coloque aqui qualquer função que faça uso
    12	            { de uma combinação }
       
    13	            { desempilha o topo }
    14	            Desempilha(P)
       
    15	      { desempilha o anterior }
    16	      i = Desempilha(P) + 1

O laço interno (5-14) monta uma combinação de tamanho r e varia o elemento do topo até não poder mais. Quando isso acontecer, o Desempilha() da linha 16 é executado para se tentar variar elementos anteriores ao antigo topo. O processo termina quando o último elemento é alcançado (i == N) e a pilha está vazia.

Prova e Complexidade

Não entrarei em nenhuma prova formal para demonstrar que o algoritmo visto funciona para todos as entradas N. Como intuição de prova, veja que na Figura 1 são geradas não só os agrupamentos desejados como também outros agrupamentos menores. Isso é, o algoritmo gera todo o conjunto potência do conjunto de entrada e filtra somente os subconjuntos de tamanho r.

Sabe-se que a cardinalidade do conjunto potência é 2^N, indicando que a complexidade do algoritmo é O(2^N).

Código para combinar elementos de vetores em C

Os arquivos são (retire a extensão .docx)

eles já implementam a TAD Pilha internamente. Para testar, usaremos o seguinte exemplo:

/*
 * [exemplo.c]
 * Testa a biblioteca de gerar combinações.
 *
 * [Compilação]
 * $ gcc -o exemplo exemplo.c Comb.c
 *
 * [Uso]
 * $ ./exemplo <r> <N elementos>
 * exemplo:
 * $ ./exemplo 2 maca uva abacaxi
 *    maca        uva 
 *    maca    abacaxi 
 *     uva    abacaxi
 */

#include <stdio.h>
#include <string.h>
#include "Comb.h"

int main(int argc, char **argv) {
    struct comb *c ;
    int r = atoi(argv[1]) ;
    int i ;

    int *saida = malloc(r * sizeof(int)) ;

    /* 1) Passamos o tamanho total do vetor
     * de entrada.
     * 2) O vetor que receberá as permutações.
     * 3) O tamanho do r.
     */
    c = init_comb(argc-2, saida, r) ;

    /* Para obter todas as permutações chame proxCombinacao()
     * em um loop testando se o retorno é 1. Se zero, as
     * combinações foram todas geradas.
     */
       
    while(proxCombinacao(c) == 1) {
        /* Mostra a combinação */
        for(i=0; i < r; i++) printf("%10s ", argv[2+saida[i]]) ;
        putchar('\n') ;
    }

    return 0 ;
}

Compilando:

$ gcc -o exemplo exemplo.c Comb.c

Testando:

$ ./exemplo 4 maca uva pera abacaxi manga goiaba
      maca        uva       pera    abacaxi 
      maca        uva       pera      manga 
      maca        uva       pera     goiaba 
      maca        uva    abacaxi      manga 
      maca        uva    abacaxi     goiaba 
      maca        uva      manga     goiaba 
      maca       pera    abacaxi      manga 
      maca       pera    abacaxi     goiaba 
      maca       pera      manga     goiaba 
      maca    abacaxi      manga     goiaba 
       uva       pera    abacaxi      manga 
       uva       pera    abacaxi     goiaba 
       uva       pera      manga     goiaba 
       uva    abacaxi      manga     goiaba 
      pera    abacaxi      manga     goiaba 

Basicamente, é necessário uma variável do tipo struct comb e um vetor de inteiros de r posições. A variável do tipo struct comb é inicializada através da função init_comb() que recebe o total N de elementos,  o vetor de saída e o tamanho r das combinações. Para se obter combinações, basta chamar a função proxCombinacao() passando a variável comb como parâmetro. A função retorna 1 quando ainda há combinações para se gerar e zero quando tudo foi gerado. A construção mais comum é usar essa função como a condição de um loop.

Conclusão

O método visto gera combinações iterativamente usando a TAD Pilha como método de armazenamento. O algoritmo é bastante rápido para entradas pequenas, mas devido ao seu custo exponencial, o tempo de execução cresce abruptamente para ligeiros aumentos na entrada.

Referências

[1] Gerando Combinações Pela Contagem Binária by Daemonio (Acessado em: Abril/2014)
https://daemoniolabs.wordpress.com/2011/02/17/gerando-combinacoes-sem-repeticao-pela-contagem-binaria-em-c/

Classe Python de Números Por Extenso

Introdução

Seguindo a ideia do post [1] sobre o algoritmo geral para se escrever números em extenso, resolvi fazer a versão em python do algoritmo. Aqui no blog tem a versão em C [2] para quem desejar conhecer.

O código

Salve como dExtenso.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# [dExtenso.py]
# Implementa o algoritmo geral de escrita
# por extenso de um número qualquer.
#
# [Autor]
# Marcos Paulo Ferreira (Daemonio)
# undefinido gmail com
# https://daemoniolabs.wordpress.com
#
# Versão 1.0 by daemonio @Thu Sep 12 22:22:06 BRT 2013
# Versão 1.1 by daemonio @Sat Dec 20 23:41:50 BRST 2014
#   + consertado bug no "cem"
 
class dExtenso():
    trioextenso=()
    classextenso=()
 
    def __init__(self):
        self.trioextenso=(
                     ("dummy","um","dois","três","quatro","cinco","seis","sete",
                     "oito","nove"),
                     ("dez","onze","doze","treze","quatorze","quinze","dezesseis",
                     "dezessete","dezoito","dezenove"),
                     ("dummy","dummy","vinte","trinta","quarenta","cinquenta",
                     "sessenta","setenta","oitenta","noventa"),
                     ("dummy","cento","duzentos","trezentos","quatrocentos",
                     "quinhentos","seiscentos","setecentos","oitocentos",
                     "novecentos"))
        self.classextenso=(
                      "dummy","mil","milh","bilh","trilh","quatrilh",
                      "quintilh","sextilh","septilh","octilh",
                      "nonilh","decilh","undecilh","duodecilh",
                      "tredecilh","quatordecilh","quindecilh",
                      "sexdecilh","setedecilh","octodecilh",
                      "novedecilh","vigesilh" )
 
    def escrever_trio_extenso(self, trio):
        """
        Retorna um trio por extenso.
 
        Entrada: trio na forma de string.
 
        Retorno: trio em extenso.
        """
        saida=[]
 
        if trio == '100':
            # Erro antigo aqui. Consertado usando "return"
            return 'cem'
        elif trio == '000':
            return 'zero'
        else:
            c, d, u = trio
            c, d, u = int(c), int(d), int(u)
 
            if c != 0:
                saida.append(self.trioextenso[3][c])
            if d == 1:
                saida.append(self.trioextenso[1][u])
            else:
                if d != 0:
                    saida.append(self.trioextenso[2][d])
                if u != 0:
                    saida.append(self.trioextenso[0][u])
        return ' e '.join(saida)
 
    def nao_e_ultimo_trio(self, totalTrios, contador):
        """
        Retorna verdadeiro se o trio indicado pelo contador
        não é o último (isso é, não é o mais à direita).
 
        Entrada: Número total de trios e o contador.
 
        Retorno: Verdadeiro se o trio NÃO é o último e
        falso caso contrário.
        """
        return contador < (totalTrios - 1)
 
    def trio_a_esquerda_eq_zero(self, trioLista, contador):
        """
        Retorna verdadeiro se o trio à esquerda do trio
        indicado pelo contador é igual a zero.
 
        Entrada: Os trios em forma de Lista e o contador.
 
        Retorno: Verdadeiro se o trio à esquerda do contador
        for zero e falso caso contrário.
        """
 
        # Contador igual a zero indexa o elemento mais à direita,
        # por isso devemos acrescentar tamanho da lista.
        t = len(trioLista)-1
        return trioLista[t-contador-1] == '000'
 
    def getExtenso(self, num, quebradelinhas=0):
        """
        Algoritmo principal. Recebe um número na forma de
        string e retorna sua escrita em extenso.
 
        Entrada: Número na forma de string e uma flag que, se
        tiver o valor 0 (zero), o extenso é retornado em uma
        só linha. Um valor 1 (um) faz o extenso ser quebrado
        em várias linhas.
 
        Retorno: O número de entrada em extenso na forma de
        string.
        """
 
        # Remove os zeros iniciais e faz padding
        # para números com quantidade de algarismos
        # não múltipla de 3
        num = num.lstrip('0')
        pad = 3 - len(num)%3
        if pad < 3: num = '0'*pad + num
 
        it = iter(num)
        trioLista = [ ''.join([a,b,c]) for a, b, c in zip(it, it, it)]
 
        if len(trioLista) > len(self.classextenso):
            raise IndexError,'Número muito grande'
 
        contador=0
        saida=''
        extensofinal=''
 
        for trio in reversed(trioLista):
            trioInt=int(trio)
 
            if trioInt > 0:
                saida = self.escrever_trio_extenso(trio)
                if contador > 0:
                    saida = saida + ' ' + self.classextenso[contador]
                if contador > 1:
                    if trioInt > 1:
                        saida = saida + 'ões'
                    else:
                        saida = saida + 'ão'
                if quebradelinhas == 0:
                    if self.nao_e_ultimo_trio(len(trioLista), contador):
                        if self.trio_a_esquerda_eq_zero(trioLista, contador):
                            saida = ' e ' + saida
                        elif trioInt >= 100:
                            saida = ', ' + saida
                        else:
                            saida = ' e ' + saida
                else:
                    saida = saida + '\n'
 
                extensofinal = saida + extensofinal
            contador = contador + 1
        return extensofinal.rstrip('\n')
 
if __name__ == '__main__':
    import sys
    argc = len(sys.argv)
 
    if argc < 2:
        print '[uso] %s <numero>' % (sys.argv[0])
    else:
        n = dExtenso()
        # Valor 1 em quebradelinhas faz o extenso de
        # cada trio ser em uma linha separada.
        print n.getExtenso(sys.argv[1], quebradelinhas=0)

Uso

Você pode utilizar o código diretamente no terminal ou criar instâncias da classe no seu código python. Para rodar via terminal, digite:

$ chmod +x dExtenso.py
$ ./dExtenso 9977700
nove milhões, novecentos e setenta e sete mil, setecentos

No próprio código fonte do módulo há um exemplo da criação de uma instância. A seguir mais um exemplo:

$ python
>>>import dextenso
>>>ext = dextenso.dExtenso()
>>>print ext.getExtenso('9977700')
nove milhões, novecentos e setenta e sete mil, setecentos
>>>print ext.getExtenso('8923718789312379818792379123887172397891371', quebradelinhas=1)
oito tredecilhões
novecentos e vinte e três duodecilhões
setecentos e dezoito undecilhões
setecentos e oitenta e nove decilhões
trezentos e doze nonilhões
trezentos e setenta e nove octilhões
oitocentos e dezoito septilhões
setecentos e noventa e dois sextilhões
trezentos e setenta e nove quintilhões
cento e vinte e três quatrilhões
oitocentos e oitenta e sete trilhões
cento e setenta e dois bilhões
trezentos e noventa e sete milhões
oitocentos e noventa e um mil
trezentos e setenta e um

Passando quebradelinhas=1, cada extenso de um trio vem em uma linha separada.

Referências

Números Por Extenso: Algoritmo Geral by Daemonio (Acessado em: Setembro/2013)
https://daemoniolabs.wordpress.com/2012/06/24/numeros-por-extenso-algoritmo-geral/

Biblioteca em C: Números por Extenso by Daemonio (Acessado em: Setembro/2013)
https://daemoniolabs.wordpress.com/2012/10/20/biblioteca-em-c-numeros-por-extenso/

Calculando com o sed – Parte II

Introdução

Essa é a parte dois da série “Calculando com o sed”. Nessa parte, iremos aprender como somar e subtrair dois números inteiros.

O conteúdo não está completamente definido e por isso poderá sofrer mudanças no decorrer das postagens.

Somando algarismos

Vamos começar com o modo mais simples de somar, que é a soma de dois algarismos. Claro que existem vários algoritmos algoritmos para esse propósito, mas aqui veremos aquele usado no dc.sed [1].

Como antes, resolveremos esse problema usando lookup tables. Somando dois algarismos cuja soma é menor que dez, podemos obter valor da soma usando deslocamentos:

4+3
XXXXXXX9876543210
|________|
   +10

A lógica é inserir um número de caracteres X igual a soma dos algarismos e em seguida deslocar 10 unidades. No final, alcançaremos o valor da soma na string numérica. O caracter X é chamado de placeholder, e nesse caso, ele poderia ter qualquer valor.

Veja que esse conceito funciona para qualquer soma de algarismos cujo resultado é menor que 10:

2+6
XXXXXXXX9876543210
|________|
   + 10

1+1
XX9876543210
|________|
   + 10

0+0
9876543210
|________|
   + 10

Para a soma 0+0 nenhum caractere placeholder foi adicionado. Mais na frente, veremos como gerar esses caracteres placeholders e notaremos que não é ideal gerar duas strings vazias como na soma 0+0. Para contornar esse problema, aumentaremos o deslocamento para 11 e adicionaremos mais um caractere X:

0+0
X9876543210
|_________|
    +11

1+8
XXXXXXXXXX9876543210
|_________|
    +11

Agora vamos analisar quando a soma é maior ou igual a 10.

9+7
XXXXXXXXXXXXXXXX9876543210
|_________|
   +11

A soma deu maior que 10 e o deslocamento de 11 não foi o suficiente para se chegar na string numérica. Diante desse problema, teremos que alterar esses caracteres placeholders de modo que quando este tipo de soma ocorrer um deslocamento de 11 retorne o algarismo correto da soma.

No dc.sed os placeholders são gerados da seguinte maneira:

9+8
9876543210765432109876543210
|_________|      |
|   + 11         |
|________________|
   placeholders

ou seja, os dois algarismos são “expandidos” de forma decrescente até o zero, e no caso do primeiro algarismo, ele é incluído na sequência e o segundo não. Essa expansão garante que o placeholder indexado em uma soma maior que 10 resulte no algarismo correto do resultado. Outros exemplos:

7+6
765432105432109876543210
|_________|
    + 11

5+5
543210432109876543210
|_________|
    + 11

Repare que isso também funciona para as somas menores que 10:

4+2
43210109876543210
|_________|
    +11

Lembrando que, quando a soma é menor que 10, não importa como os placeholders são gerados, pois o valor da soma é obtido na string numérica final e não nos placeholders em si.

Somando dois números

Somar dois números é o mesmo que somar os algarismos correspondentes desses números e propagar os vai-uns que aparecem. Como primeiro exemplo, veremos como somar dois números de dois algarismos:

  87
  34
-----
  ||_ 4+7
  |_ 3+8

o que aconteceu, basicamente, foi a soma dos algarismos 7+4 e depois 3+8. A diferença agora é o tratamento do vai um, que não foi abordado anteriormente. Primeiramente, devemos saber como e quando ocorre um vai um. Um vai um ocorre sempre quando uma soma de dois algarismos é superior ou igual a 10 e um modo de se detectar isso é analisando a quantidade de caracteres da lookup table após o algarismo indexado.

7+7
7654321065432109876543210
|_________|             |
    +11   |_____________|
               >10

Se a quantidade de algarismos após o algarismo indexado for maior que 10 quer dizer que uma soma superior ou igual a 10 ocorreu, isso é, um vai um foi gerado e deve ser propagado.

No outro caso, para somas menores que 10, a quantidade de algarismos após o algarismo indexado é sempre menor que 10, pois, como sabemos, o resultado é obtido na string numérica final:

2+5
210432109876543210
|_________|      |
    +11   |______|
             <10

Quando um vai um é propagado, o resultado da soma subsequente é deslocado em uma unidade, isso é, o comportamento de um vai um é o mesmo que a adição de mais um placeholder.

  67
+ 24
-----
  ||_ 7+4
  |   |_ 7654321032109876543210
  |      |_________|         |
  |         + 11   |___+10___|
  |_ 6+2 ____________________|
     |  |
     |  v
     |_ 16543210109876543210
        |_________|
        |   + 11
        v
      vai um

resultado: 91

Na soma 7+4 sabemos que o resultado é 11 e que após o deslocamento, temos mais de dez caracteres, o que indica a presença de um. A propagação do vai um ocorre obtendo-se o décimo caractere após o deslocamento e o acrescentando como placeholder na soma subsequente. No esquema acima esse caractere é o 1 e veja como ele entra à esquerda da soma 6+2 para acrescentar ao resultado uma unidade.

Esse conceito se espande para números com qualquer quantidade de algarismos:

   678
 + 084
-------
   |||_ 8+4
   ||   |_ 87654321032109876543210
   ||      |_________|         |
   ||         + 11   |___+10___|
   ||_ 7+8 ____________________|
   |   |  |
   |   |  v
   |   |_ 276543210765432109876543210
   |      |_________|         |
   |         + 11   |___+10___|
   |_ 6+0 ____________________|
      |  |
      |  v
      |_ 665432109876543210
         |_________|
             + 11

resultado: 762

Um último exemplo mostrando o caso em que uma soma não propaga vai um:

   104
 + 659
-------
   |||_ 4+9
   ||   |_ 432108765432109876543210
   ||      |_________|         |
   ||         + 11   |___+10___|
   ||_ 0+5 ____________________|
   |   |  |
   |   |  v
   |   |_ 30432109876543210
   |      |_________|         |
   |          + 11  |sem vaium|
   |_ 1+6
      |_ 105432109876543210
         |_________|
              + 11
resultado: 763

A soma 0+5 não gerou vai um porque não há mais de dez caracteres após o algarismo indexado.

Subtraindo dois algarismos

O ideal é se o processo de subtração fosse semelhante ao de adição, pois assim bastaria uma única sequência de código para realizar as duas operações. A boa notícia é que esses dois processos se assemelham bastante porém com a subtração um pouco mais trabalhosa. Veja:

4-3
XXXXXXXXX0123456789
|_________|
   + 11

Repare que a lookup table está invertida em relação à tabela de adição.

Bem, a substituição dos caracteres placeholders é um pouco tanto misteriosa:

ex: 4-6
432107890123456789
|_________|
   + 11

O primeiro algarismo é expandido que nem na adição. Já o segundo algarismo é expandido usando a sequência 0123456789 e o resultado é todos algarismos que vem logo após ele, sem incluí-lo. Em suma, o primeiro algarismo é expandido de forma decrescente entrando na sequência e o seguindo de forma crescente sem entrar na sequência.

ex:  4    -     6
9876543210 0123456789
     ^^^^^        ^^^
       |      _____|
       |     |
       v     v
      43210 789 0123456789
      |___________|
           + 11

Para se fazer 6-4=2, temos que inverter a ordem dos algarismos e fazer 4-6 no lugar. Esse método tem a peculiaridade de depender da ordem dos fatores pra se realizar a subtração. Esse detalhe será visto mais à frente quando falaremos do vai um negativo.

Outro exemplo:

ex:  3    -     3
9876543210 0123456789
      ^^^^      ^^^^^
       |      ____|
       |     |
       v     v
      3210 456789 0123456789
      |___________|
           + 11

Subtraindo dois números

Entendido a parte de subtração entre dois algarismos, agora vamos analisar a subtração entre dois números. Como sabemos, a subtração de dois números nada mais é que a subtração dos algarismos correspondentes dos números com o respectivo vai um negativo (ou borrow). Sempre ocorre um vai um negativo quando, matematicamente falando, o algarismo da direita é maior que o da esquerda, como em 4-6. O resultado é obtido após o acréscimo de dez unidades no algarismo da esquerda, como ocorre no método de subtração aprendido na escola:

  24
- 16
 ----
  ||_ 4-6 = 14-6 = 8
  |_ 2-1(-1) = 0
        ^^^^
        borrow

Então, nesse caso, quando se faz 4-6, o resultado não é -2 e sim 8, pois 14-6=8.

Como esse método considera a ordem inversa dos algarismos, um borrow é gerado se o número da esquerda é maior que o da direita.

ex: 6-4 (o mesmo que 4-6, matematicamente)
5432104567890123456789
|_________|          |
    +11   |__________|
               >10

resultado: 8

O esquema para se verificar se ocorreu borrow é o mesmo utilizado para verificar um carry: conta-se dez posições após o resultado e se existir alguma coisa lá então um vai um negativo é gerado. Entenda aqui  o motivo em que a ordem dos algarismos importa, pois devido a forma de como a expansão é feita, esse método consegue nos dizer se ocorreu ou não um vai um.

Vejamos um exemplo de subtração numérica com propagação de vai um negativo:

   247
 - 602
-------
   |||_ 7-2
   ||   |_ 6543210234567890123456789
   ||      |_________|         |
   ||         + 11   |___+10___|
   ||                          |
   ||_ 4-0 ____________________|
   |   |  |
   |   |  v
   |   |_ 5321001234567890123456789
   |      |_________|         |
   |         + 11   |___+10___|
   |                          |
   |_ 2-6 ____________________|
      |  |
      |  v
      |_ 51067890123456789
         |_________|
             + 11
|resultado| = 355

Outro exemplo:

   1099
 - 1234
-------
   |||_ 9-4
   ||   |_ 8765432104567890123456789
   ||      |_________|         |
   ||         + 11   |___+10___|
   ||                          |
   ||_ 9-3 ____________________|
   |   |  |
   |   |  v
   |   |_ 587654321034567890123456789
   |      |_________|         |
   |         + 11   |___+10___|
   |                          |
   |_ 0-2 ____________________| 
   |  |  |
   |  |  v
   |  |_ 3234567890123456789
   |     |_________|
   |        + 11
   |_ 1-1
      |_ 01234567890123456789
         |_________|
             + 11
|resultado:| 0135

Para esse método funcionar corretamente sempre devemos subtrair do menor número o maior, isso é, o menor número sempre deve ser o primeiro. Para decidirmos qual número é maior ou menor que o outro devemos ter algum outro método para realizarmos comparações. Esse é o nosso próximo assunto.

Comparação de números em sed

Para se realizar a subtração com o método visto sempre utiliza-se o menor número como primeiro fator, e para isso, é preciso um algoritmo que retorne o menor número entre dois números dados. A primeira intuição é que, retirando os zeros iniciais, o número com menor quantidade de algarismo é o menor entre eles.

<5897,~147899, \
<589,7~1478,9   |
<58,97~147,89    > desloca-se a vírgula para esquerda
<5,897~14,789   |
<,5897~1,4789  /

O processo acima encontra o menor/maior número entre dois números simplesmente verificando o número de algarismos de cada um. Se no fim do processo, que é quando a vírgula encontra o < ou o caractere ~, a vírgula estiver entre os algarismos de um número, esse número é o maior.

Esse processo é feito assim no sed:

:cmp1
s/^\(<[^,]*\)\([^,]\),\([^~]*~[^,]*\)\([^,]\),/\1,\2\3,\4/
tcmp1

Trocando em miúdos:

s/^(<[^,]*) ([^,]), ([^~]*~[^,]*) ([^,]),/ \1,\2\3,\4/
  ^^^^^^^^^^^^  ^^^^^^  ^^^^^^^^^^^^  ^^^^^   ^^^^^^^^^
      [1]      [2]         [3]         [4]      [5]

Em [1], salvamos em \1 tudo antes da vírgula do primeiro número. Em [2], um dígito do primeiro número é obtido, mas se esse “dígito” for a vírgula, o processo termina (não casa). Em [3], salvamos tudo após a vírgula do primeiro número mais tudo antes da vírgula do segundo número. Em seguida, em [4], obtemos um dígito do segundo número. Por fim, em [5], andamos com a vírgula para a esquerda nos dois números. O loop termina quando a vírgula encontrar o sinal de menor (&lt,) ou o til (~,).

Agora, vamos analisar a outra situação que é quando os números têm a mesma quantidade de algarismos. A lógica aqui é comparar somente o algarismo mais à esquerda que se difere nos números.

<4567,~4321,~
<456,7~432,1~
<45,67~43,21~
<4,567~4,321~
<,4567~,4321~

O algarismo mais à esquerda que se difere no dois números é o 5 e o seu corresponde é o 3. Como 5 > 3, concluímos que o número 4567 é maior que o 4321.

Essa comparação é feita assim:

s/$/;,0123456789/
/^<\([^~]*\)\([^~]\)[^~]*~\1\(.\)[^;]*;.*\3.*\2/ s/</>/

Se a segunda expressão regular casar, então o sinal de menor é trocado pelo de maior. O sinal no início é para indicar se o primeiro número é maior ou menor igual que o segundo.

No fim dos números, inclui-se uma lookup table que será usada para se realizar a comparação dos algarismos. Os algarismos correspondentes ficam em \3 e em \2 e se o conteúdo desses retrovisores estiverem em sequência, o primeiro número é maior, gerando uma inversão dos sinais. Como queremos o menor número sempre à esquerda, utilizamos a seguinte linha em sed para realizar essa tarefa:

s/^>\([^~]*~\)\([^~]*~\)/>\2\1/

Veja o que acontece quando fazemos 12345-67 no sedsed [2]:

PATT:<12345,~67,~$
HOLD:$
COMM::cmp1
COMM:s/^\(<[^,]*\)\([0-9]\),\([^,]*\)\([0-9]\),/\1,\2\3,\4/
PATT:<1234,5~6,7~$
HOLD:$
COMM:t cmp1
COMM:s/^\(<[^,]*\)\([0-9]\),\([^,]*\)\([0-9]\),/\1,\2\3,\4/
PATT:<123,45~,67~$
HOLD:$
COMM:t cmp1
COMM:s/^\(<[^,]*\)\([0-9]\),\([^,]*\)\([0-9]\),/\1,\2\3,\4/
PATT:<123,45~,67~$
HOLD:$
COMM:t cmp1
COMM:s/$/;,0123456789/
PATT:<123,45~,67~;,0123456789$
HOLD:$
COMM:/^<\([^~]*\)\([^~]\)[^~]*~\1\(.\)[^;]*;.*\3.*\2/ s/</>/
PATT:>123,45~,67~;,0123456789$
HOLD:$
COMM:s/^>\([^~]*~\)\([^~]*~\)/>\2\1/
PATT:>,67~123,45~;0123456789$

A primeira linha destacada representa a entrada formatada. A segunda é o fim do processo de comparação, e nesse caso, vemos que o primeiro número é maior que o segundo. Na terceira linha destacada, o sinal inicial ‘<‘ é trocado pelo ‘>’ devido ao primeiro número ser o maior entre eles. Já na última linha ocorre a troca de posições entre o maior número e o menor, sendo que o menor número sempre deve ficar à esquerda.

O script: addsub.sed

Para fixar o que aprendemos nada melhor que um script que realiza adição e subtração entre dois números. Ele foi baseado inteiramente no bc.sed[1] e demais detalhes sobre ele podem ser conferidos no código do script ou também na sua visualização pelo sedsed[2].

#!/bin/sed -f
# [addsub.sed]
# Soma e subtrai dois números usando o sed.
#
# [Autor]
# Marcos Paulo Ferreira (daemonio)
# undefinido gmail com
# https://daemoniolabs.wordpress.com/
#
# [Uso]
# $ chmod +x addsub.sed
# $ echo '1234+56789' | ./addsub.sed
#   58023.
# $ echo '1234-56789' | ./addsub.sed
#   -055555.
#
# obs: a operação deve ser sempre entre
# números inteiros positivos, ou seja,
# da forma a+b ou a-b, a>=0 e b>=0.
#
# Versão 1.0 by daemonio @ Fri Mar  1 00:59:02 BRT 2013
#

# Formata a entrada.
# <NUM1,~NUM2,~SINAL
s/\([0-9]*\)\(.\)\([0-9]*\)/<\1,~\3,~\2/

# Desloca as vírgulas a fim de se
# encontrar o menor número.
:cmp1
s/^\(<[^,]*\)\([0-9]\),\([^,]*\)\([0-9]\),/\1,\2\3,\4/
t cmp1

# Compara os números.
s/$/;,0123456789/
/^<\([^~]*\)\([^~]\)[^~]*~\1\(.\)[^;]*;.*\3.*\2/ s/</>/

# Coloca o menor número à esquerda.
s/^>\([^~]*~\)\([^~]*~\)/>\2\1/

# Formato para addsub deve ser do tipo:
# NUM1~SINAL0NUM2,.~;LOOKUP

# Acrescenta sinal de menor no resultado se
# o segundo número é maior e a operação é
# subtração.
s/\(<,[^~]*\)~\([^~]*\)~-/\1~-0\2~-/

# Retira as vírgulas.
s/..\([^~]*\)~\([^,]*\),\([^~]*\)~/\1~\2\3,.~/

# Adiciona as lookup tables de acordo com a
# operação.
s/~+;.*/~;9876543210;9876543210/
s/~-;.*/~;0123456789;9876543210/

# Realiza soma e subtração.
:addsub1
s/\(.\{0,1\}\)\(~[^,]*\)\([0-9]\)\(\.*\),\([^;]*\)\(;\([^;]*\(\3[^;]*\)\).*X*\1\(.*\)\)/\2,\4\5\9\8\7\6/
s/,\([^~]*~\).\{10\}\(.\)[^;]\{0,9\}\([^;]\{0,1\}\)[^;]*/,\2\1\3/
/^~.*~;/ !b addsub1

# Formata a saída.
:endbin
s/.\([^,]*\),\([0-9.]*\).*/\1\2/

# EOF

O script só aceita entradas do tipo a+b e a-b, sendo a e b inteiros positivos. Essa limitação é para se evitar o jogo de sinais para se decidir qual será o sinal do resultado. Veremos esse jogo de sinais no próximo tópico da série.

Vamos testar o script:

$ echo '0123456789-9876543210' | ./addsub.sed
-09753086421.
$ echo '0123456789-9876543210' | bc
-9753086421

$ echo '32232456679786978567543620757589+8796521243135231135879869069886' | ./addsub.sed
41028977922922209703423489827475.
$ echo '32232456679786978567543620757589+8796521243135231135879869069886' | bc
41028977922922209703423489827475

O script não está completo porque ainda não há uma verificação de sinais adequada e a saída não está completa. Melhoraremos esse código também no próximo tópico da série.

Conclusão

Nessa parte, vimos como somar e subtrair dois números. Vimos que esses dois processos podem ser feitos com a mesma sequência de código, o que facilita em muito a programação. No próximo tópico veremos como somar e subtrair dois números decimais (ex: 0.44 + 0.12) e estenderemos os conceitos vistos aqui para que eles se apliquem em qualquer caso.

Referências

[1] dc.sed by Greg Ubben (Acessado em: Março/2013)
http://sed.sourceforge.net/grabbag/scripts/dc.sed

[2] sedsed by Aurélio Jargas (Acessado em: Março/2013)
http://aurelio.net/projects/sedsed/

[3] Overview of How the dc.sed Script Works by Greg Ubben (Acessado em: Março/2013)
http://sed.sourceforge.net/grabbag/scripts/dc_overview.htm

Calculando com o sed – Parte I

Introdução

Durante esse novo ano produzirei uma série de postagens focando no uso do sed como calculadora. Assim veremos como realizar operações de soma, subtração e multiplicação com números decimais e inteiros tudo isso usando o bom e velho comando sed.

Como sabemos, o sed é somente um aplicativo de tratamento de texto, impossibilitando, por exemplo, de forma natural, seu uso para operações matemáticas. Porém, isso não impediu que várias pessoas encontrassem modos elegantes e inteligentes para transformarem scripts sed em potentes calculadoras (veja dc.sed em [1]).

Essa é a Parte I desse tutorial. As demais partes estarão disponíveis nos links abaixo assim que forem postadas:

O conteúdo não está completamente definido e por isso poderá sofrer mudanças no decorrer das postagens.

Soma um

Nosso primeiro objetivo é somar uma unidade a um número qualquer. O modo mais inocente de se fazer isso é:

$ cat soma1_1.sed
#!/bin/sed -f
s/8$/9/
s/7$/8/
s/6$/7/
s/5$/6/
s/4$/5/
s/3$/2/
s/1$/2/
s/0$/1/

A ordem das substituições é importante para evitar substituições em cadeia. Esse método é inocente por não abordar todos os casos. Por exemplo, ele não cobre uma soma em um número terminado em 9 porque para fazer isso, teremos que bolar um jeito para propagarmos o vai um.

Lookup tables

Lookup tables é um recurso bastante utilizado na computação em geral. Essas tabelas armazenam pré-computações que são utilizadas para aumentar o desempenho de certas operações. Para nossos objetivos, essas tabelas funcionarão como vetores que armazenarão números utilizados na operação de somar um.

Acrescentando-se a sequência ;0123456789 após um número, podemos realizar uma soma utilizando somente uma expressão regular:

$ cat soma1_2.sed
#!/bin/sed -f

s/$/;0123456789/
s/\(.\);.*\1\(.\).*/\2/

Todo aquele código em soma1_1.sed foi substituído por apenas duas linhas. Veja que o problema dos vai-uns ainda não foi resolvido.

Vamos testar:

$ chmod +x soma1_2.sed
$ echo '55' | ./soma1_2.sed
56
$ echo 2012 | ./soma1_2.sed
2013
$ echo 109020293200 | ./soma1_2.sed
109020293201

Como isso funciona? Basicamente, acrescentamos nossa lookup table no fim do número na primeira substituição. Em seguida, pegamos o último algarismo do número (antes do delimitador ponto e vírgula) e procuramos na lookup table o número que precede esse último algarismo. Esquematicamente funciona assim:

s/  \(.\)  ; .*\1  \(.\)  .*/   \2/
     ^^^      ^^^   ^^^   ^^^   ^^^
     [1]      [2]   [3]   [4]   [5]

[1] Último algarismo do número
[2] Casamos tudo até o algarismo em [1]
[3] Obtemos o algarismo que precede [1]
[4] Limpamos o restante da lookup table
[5] Substitui último algarismo+lookup table por [3]

Repare que a lookup table desaparece do resultado final porque ela foi completamente casada na substituição.

Intuição geral na Soma Um

O scripts que vimos não cobrem todos os casos por não tratar os vai-uns. Vamos analisar, através de exemplos, como funciona uma soma um em diversos números.

5 --> 6
0 --> 1
123 --> 124
675 --> 676
9 --> 10
19 --> 20
229 --> 230
9999 --> 10000

Você conseguiu perceber um padrão? Podemos ver que se um número não termina em nove, somente o último algarismo dele é incrementado. O problema é quando temos um número terminado em 9. O que acontece nesse caso é: o nove final é transformado em zero e o número anterior que é incrementado. Podemos ver isso nas 4 últimas linhas.

Porém, há ainda dois casos especiais aqui: um é quando temos mais de um 9 final, pois nesse caso o vai-um deve ser propagado em mais de uma casa. Observando a última linha do exemplo, vemos que temos que substituir todos os noves finais por zeros e incrementar o primeiro número (não 9) à esquerda dos noves. O outro caso especial é quando não temos esse número não 9 no número.

Observando padrões podemos chegar a seguinte conclusão em relação a Soma Um em um número:

Se o número NÃO termina em 9
   Incremente o último algarismo. Fazemos isso facilmente
   com lookup tables.

Se o número termina em 9
   Troque todos os 9 finais por zeros e incremente
   o primeiro número não 9 à esquerda dos 9's.

A boa notícia é que podemos englobar esses dois casos usando uma mesma expressão regular.

Montando a expressão regular

Com o que vimos já é o suficiente para montarmos nossa expressão regular final. Devido aos números que terminam com vários noves, teremos que modificar nossa lookup table:

s/$/;9876543210 99990000 999000 9900 90/

Incluímos, agora, uma quantidade fixa de noves e zeros no fim da tabela. Se um número terminar, por exemplo, com três noves, tudo que devemos fazer é encontrar os três noves na tabela e obter a mesma quantidade de zeros. Esse acréscimo na look up é uma maneira inteligente de se mapear os noves na quantidade de zeros correspondente.

Você deve ter reparado que a sequência numérica está invertida. Isso aconteceu devido a particularidade dos números que só contêm noves. A ideia é trocar a expressão .*\1 (veja soma1_2.sed) por [^1]*\(.\)\1 para garantirmos que se mesmo não havendo um número antes dos noves, o algarismo 1 seja sempre obtido.

A nova regexp, então, fica:

$ cat soma1_3.sed
#!/bin/sed -f

s/$/;9876543210 99990000 999000 9900 90/
s/\([0-8]\{0,1\}\)\(9*\);[^1]*\(.\)\1.*\2\(0*\).*/\3\4/

Testando:

$ chmod +x soma1_3.sed
$ echo '999' | ./soma1_3.sed
1000
$ echo '123499' | ./soma1_3.sed
123500

Agora, vamos analisar a última linha do código esquematicamente:

s/\([0-8]\{0,1\}\)  \(9*\) ; [^1]*\(.\)\1  .*\2\(0*\)  .* /  \3\4/
  ^^^^^^^^^^^^^^^^   ^^^^    ^^^^^^^^^^^^    ^^^^^^^^  ^^    ^^^^
      [1]             [2]       [3]            [4]     [5]    [6]

Em [1] armazenamos no retrovisor \1 o algarismo não nove mais à direita do número. Se esse número não existir, ou seja, um número composto somente de noves, o retrovisor terá valor nulo. Em seguida, obtemos os noves finais do número e os armazenamos no retrovisor \2. Mais uma vez, se os noves não existirem, o retrovisor terá valor nulo. Já em [3], guardamos no retrovisor \3 o algarismo que precede o \1. Veja que, pela montagem da expressão, se \1 for nulo, teremos em \3 o algarismo “1”, como queríamos. O próximo passo é a obtenção dos zeros equivalentes. Isso é feito em [4] onde \2 mapeia os noves na lookup table e o \(0*\) armazena no retrovisor \4 o número de zeros correspondente. Por fim, em [5] limpamos o que sobrou da lookup table e em [6] realizamos a substituição de tudo após o algarismo não nove mais à direita por \3 (algarismo incrementado) + \4 (quantidade de zeros equivalente).

O único problema dessa abordagem é que a lookup table é sempre fixa e isso determina um valor máximo de noves finais que um número poderá ter.

Aumentando a precisão da lookup table

A lookup table, como foi elaborada, sempre é fixa e limita o número de noves finais que um número deve ter. Se quisermos, por exemplo, tratar números com até 6 noves finais, teremos que fazer:

s/$/;9876543210 999999000000 9999900000 99990000 999000 9900 90/

Quanto mais noves adicionais, maior ficará a lookup table. Há um modo de aumentarmos a precisão da tabela, ainda mantendo-a fixa, em um taxa melhor que a anterior, ou seja, conseguimos uma maior precisão na entrada sem acrescentarmos muitos caracteres nela.

A ideia é um pouco matemática e você deve limitar a quantidade de noves finais dentro do código. A nova lookup table é a seguinte:

s/$/;9876543210 99999999999;9999999999900000000000/

Com essa tabela, poderemos tratar um número com no máximo 11 noves finais. Obviamente, esse limite poderá ser aumentado simplesmente aumentando os noves antes e depois do ‘;’ e também os zeros, tudo na mesma quantidade.

Como exemplo, considere um número com três noves finais. Mapeamos esses três noves na primeira sequência de noves e obtemos o restante dos noves até o segundo delimitador dois pontos. Isso é, como temos 11 noves nessa sequência, sobrarão agora somente oito (11-3=8). Em seguida, mapeamos esses oito noves na segunda sequência e contamos mais 11 caracteres para avançarmos até a sequência de zeros. A quantidade de zeros restante é a quantidade desejada para a substituição.

Esquematicamente, temos:

99999999999;9999999999900000000000
   ^^^^^^^^ ^^^^^^^^+---------+^^^
      |________|     11 chars   |__ quantidade desejada
          |
      deslocamento de 8 (11-3)

O código geral também deve ser mudado. Não darei maiores explicações, pois deixarei para o leitor tirar suas próprias conclusões. :-)

$ cat soma1_4.sed
#!/bin/sed -f

s/$/;9876543210 99999999999;9999999999900000000000/
s/\([0-8]\{0,1\}\)\(9*\);[^1]*\(.\)\1[^9]*\2\(9*\);\4.\{11\}\(0*\)/\3\5/

Precisão ilimitada

Usando-se de loops podemos alcançar uma precisão ilimitada no processo de Soma Um, e com isso somarmos praticamente qualquer número de entrada.

O que nos impedia de tratarmos qualquer número de entrada era o problema dos noves finais. Sendo assim, trataremos esse problema de outra maneira: trocaremos os noves finais por ‘@’ e, em seguida, incrementaremos o algarismo antes do ‘@’, e assim, no fim do processo, fazemos a troca de ‘@’ por ‘0’.

Em código, fica assim:

$ cat soma1_5.sed
#!/bin/sed -f

:loop ; s/9\b/@/ ; tloop

s/$/;9876543210/
s/\([0-8]\{0,1\}\)\(@*\);[^1]*\(.\)\1.*/\3\2/

y/@/0/

O loop inicial troca os noves finais por @. A lookup table de incremento é adicionada logo em seguida e o número antes do @ é incrementado. Por fim, trocamos os ‘@’s por ‘0’s.

Testando:

$ chmod +x soma1_5.sed
$ echo '19999999999999' | ./soma1_5.sed
20000000000000
$ echo '9999995999999999999' | ./soma1_5.sed 
9999996000000000000

Conclusão

Essa é a Parte I da série ensinando técnicas de cálculo com o SED. As demais partes serão linkadas com essa assim que forem postadas.

Referências

[1] dc.sed – an arbitrary precision RPN calculator by Greg Ubben (Acessado em: Janeiro/2013)
http://sed.sourceforge.net/grabbag/scripts/dc.sed

[2] greg_add.txt by Greg Ubben (Acessado em: Janeiro/2013)
http://sed.sourceforge.net/grabbag/tutorials/greg_add.txt

[3] Lookup Tables & Incrementando em sed by Thobias Salazar Trevisan (Acessado em: Janeiro/2013)
http://thobias.org/doc/lookup_tables_sed.html

Quebrando A Senha De Arquivos Rar Por Brute Force

Introdução

Em um post anterior do blog [1], eu mostrei como quebrar senhas de arquivos ZIP por força bruta (ataque de dicionário), utilizando somente o programa unzip. Hoje irei mostrar o mesmo procedimento, porém para arquivos RAR e utilizando o unrar.

O método

Como já abordei esse assunto em [1], neste post serei bem breve. Primeiramente, vamos observar a man page do comando unrar e verificar se há opções úteis para nosso objetivo:

$ unrar --help
UNRAR 4.20 beta 3 freeware      Copyright (c) 1993-2012 Alexander Roshal

Usage:     unrar <command> -<switch 1> -<switch N> <archive> <files...>
               <@listfiles...> <path_to_extract\>

<Commands>
  ...
  t             Test archive files
  ...

<Switches>
  ...
  id[c,d,p,q]   Disable messages
  ...
  p[password]   Set password
  ...

Temos a opção t para testar um arquivo, a opção p para passar a senha do rar e as flags cdpq que habilitam/desabilitam certas mensagens do programa unrar.

Vejamos alguns exemplos:

# Compactando arquivo qualquer
$ rar -p a arquivocomsenha.rar /etc/passwd
12345        # Senha: 12345

# Descompactando com senha inválida
$ unrar -idq -psenhainvalida t arquivocomsenha.rar
CRC failed in the encrypted file etc/passwd. Corrupt file or wrong password.

# Descompactando com senha correta
$ unrar -idq -p12345 t arquivocomsenha.rar
$

Os resultados nos mostram que: ao se utilizar uma senha incorreta, o unrar retorna “CRC failed in the…” em stderr. Por outro lado, ele retorna vazio quando a senha está correta.

Assim, temos nosso algoritmo:

1) Se há palavras no dicionário para ler:
   Leia uma palavra do dicionário de palavras para a variável senhateste.
2) Execute: unrar -idq -psenhateste t arquivocomsenha.rar.
3) Verifique se a saída é vazia.
   Se sim, a senha do arquivo é senhateste e finalize o processo.
4) Volte para o passo 1.

Isto é o suficiente para montarmos nosso shell script para quebrar a senha de um arquivo RAR por força bruta.

O script: quebrarar.sh

#!/bin/bash
# [quebrarar.sh]
# Quebra senha de arquivos rar pelo método
# da força bruta.
#
# [Autor]
# Marcos Paulo Ferreira (Daemonio)
# undefinido gmail com
# https://daemoniolabs.wordpress.com
#
# [Uso]
# $ ./quebrarar.sh arquivocomsenha.rar wordlist.txt
# ....
# ....
# Senha: "senha"
#
# Versão 1.0, by daemonio @ Sun Jul 29 19:57:33 BRT 2012
#

# Arquivo rar com senha
ARQUIVORAR=

# Dicionário de senhas
WORDLIST=

# Recebe cada senha do dicionário
senhateste=

# Função de ajuda.
function show_help {
echo 'quebrarar.sh - by daemonio'
echo '[uso] $ ./quebrarar.sh arquivocomsenha.rar wordlist.txt'
exit 1
}

# Função chamada quando se interrompe
# o script com um ctrl+c. Essa função mostra
# a senha usada no momento da interrupção.
function pegarctrlc {
echo "[+] Processo abortado. Senha atual: $senhateste"
exit 1
}

# Instala um signal handler pro ctrl+c
trap pegarctrlc SIGINT

# Obtém os parâmetros.
ARQUIVORAR="$1"
WORDLIST="$2"

# Flag. Tem valor 1 se a senha foi quebrada e
# 0 caso contrário.
SENHAQUEBRADA=0

# Testa os parâmetros.
[ -e "$ARQUIVORAR" ] && [ -e "$WORDLIST" ] || show_help

# Processo de quebragem.
echo '[+] Espere. Quebrando a senha...'
while read senhateste
do
    SAIDA=$(unrar -idq -p"$senhateste" t "$ARQUIVORAR" 2>&1)
    # Se a variável estiver vazia é porque a senha foi quebrada.
    [ -z "$SAIDA" ] && SENHAQUEBRADA=1 && break
done < "$WORDLIST"

# Informa se a senha foi quebrada ou não.
if [ "$SENHAQUEBRADA" = '1' ]
then
    echo '[+] Senha quebrada: '$senhateste
else
    echo '[+] Senha NAO quebrada. Tente outra lista de palavras.'
fi

#EOF

Salve o código como quebrarar.sh e dê permissão de execução:

$ chmod +x quebrarar.sh

Para executar o script, passe o arquivo rar como primeiro parâmetro e a lista de palavras como segundo:

$ ./quebrarar.sh arquivocomsenha.rar senhas.txt
[+] Espere. Quebrando a senha...
[+] Senha quebrada: 12345

Podemos aumentar a eficiência desse script utilizando o xargs para executá-lo em várias threads. O conceito por trás do xargs pode ser visto em [2] e também em [1].

A seguir o código para a versão paralelizável desse script:

#!/bin/bash
# [xquebrarar.sh]
# Quebra senha de arquivos rar pelo método
# da força bruta.
#
# [Autor]
# Marcos Paulo Ferreira (Daemonio)
# undefinido gmail com
# https://daemoniolabs.wordpress.com
#
# [Uso]
# $ ./xquebrarar.sh arquivocomsenha.rar senha1, senha2, ..., senhaN
# ....
# ....
# Senha: "senha"
#
# Versão 1.0, by daemonio @ Sun Jul 29 20:30:49 BRT 2012
#

# Função de ajuda.
function show_help {
echo 'xquebrarar.sh - by daemonio'
echo '[uso] $ ./quebrarar.sh arquivocomsenha.rar senha1, senha2, ..., senhaN'
exit 1
}

# Obtém os parâmetros.
ARQUIVORAR="$1"

# Flag. Tem valor 1 se a senha foi quebrada e
# 0 caso contrário.
SENHAQUEBRADA=0

# Testa os parâmetros.
[ -e "$ARQUIVORAR" ] || show_help

# Desloca os parâmetros.
shift

# Lê as senhas da linha de comando.
for senhateste in $*
do
    SAIDA=$(unrar -idq -p"$senhateste" t "$ARQUIVORAR" 2>&1)
    # Se a variável estiver vazia é porque a senha foi quebrada.
    [ -z "$SAIDA" ] && SENHAQUEBRADA=1 && break
done

# Informa se a senha foi quebrada ou não.
if [ "$SENHAQUEBRADA" = '1' ]
then
    echo '[+] Senha quebrada: '$senhateste
    # Executando esse script no xargs precisamos matar
    # o próprio xargs (processo pai) para finalizar as
    # outras threads.
    kill $PPID
fi

#EOF

Salve o código como xquebrarar.sh e dê permissão de execução:

$ chmod +x xquebrarar.sh

Em seguida execute-o pelo xargs: (obs: 10 threads)

$ xargs -a senhas.txt -n 20 -P 10 ./xquebrarar.sh arquivocomsenha.rar
[+] Senha quebrada: 12345
Terminated

A versão paralelizável é mais rápida do que a sequencial, e isso já era de se esperar. Os tempos de execução para duas versões são semelhantes àqueles vistos em [1]. Para o ataque ser eficiente é necessário ter uma boa lista de senhas.

Referências

[1] Quebrando a Senha de Arquivos ZIP Por Brute Force by Daemonio (Acessado em: Julho/2012)
https://daemoniolabs.wordpress.com/2012/05/27/quebrando-a-senha-de-arquivos-zip-por-brute-force/

[2] Paralelismo com o xargs by Daemonio (Acessado em: Julho/2012)
https://daemoniolabs.wordpress.com/2012/04/07/paralelismo-com-o-xargs/

Conversão de base numérica

Introdução

É muito comum precisarmos converter um número em uma base numérica para outra, principalmente quando a conversão envolve as bases 10, 2 e 16. Muitas linguagens de programação oferecem funções ou métodos para realizar essa tarefa de conversão, porém é sempre útil entendê-la e implementá-la manualmente.

Hoje veremos alguns algoritmos para converter bases numéricas.

O que é base numérica?

Uma base numérica é um conjunto finito de símbolos usados para representar quantidades numéricas. A base mais comum é a base 10, que nos fornece 10 símbolos para serem usados nessa representação. A conversão entre bases numéricas é a passagem de uma base para outra, ou seja, muda-se somente a representação simbólica, preservando o valor quantitativo.

Os símbolos usados nas bases numéricas são os algarismos arábicos que todos nós já conhecemos: 0, 1, 2, …, 9. Para bases maiores que 10, as letras do alfabeto também são usadas. Bases entre 11 e 26 usam as letras do alfabeto maiúsculo, e as bases acima de 26 utilizam também o alfabeto minúsculo. Observe que podemos trocar os algarismos arábicos por outros símbolos, inclusive strings, resultando naquele princípio de gerar permutações de strings, visto no post [2].

Um detalhe: base64

Base64 é um algoritmo utilizado, principalmente, para transformar dados binários em texto. Esse algoritmo não é simplesmente transformar para a base 64 como alguns devem pensar. Ele, na verdade, é um conjunto de padrões que visa mapear qualquer stream de bits nos 64 caracteres de textos mais usuais (isto é: [a-zA-Z0-9+/]). Veja mais sobre base64 em [3].

Convertendo da base n para a base m

A seguir veremos 3 procedimentos comuns para a conversão entre bases.

1) Processo de contagem

Todo mundo sabe que depois do “8” vem o “9” e depois do “9” vem o “10”. A disposição de todas permutações dos símbolos de uma base em uma ordem específica é o que denominamos de processo de contagem. Esse processo nada mais é que pegar a representação para a quantidade zero e avançar a quantidade em uma unidade e obter a representação equivalente.

Para a base 4, os primeiros 17 termos da contagem são:

0, 1, 2, 3, 10, 11, 12, 13, 20, 21, 22, 23, 30, 31, 32, 33, 100, etc

Se fizermos o processo de contagem simultaneamente em outras bases, os termos na mesma posição representam a mesma quantidade, isto é:

Processo de contagem

Processo de contagem

Logo, podemos ver que 13(4), 7(10) e 111(2) representam a mesma quantidade, e assim, para converter, por exemplo, um numeral na base 4 para a base 2, basta olharmos os termos correspondentes.

Obviamente o processo de contagem não é muito viável, pois para números grandes teremos que gerar sequências enormes. Porém, esse método é muito útil para gerar permutações. Se trocarmos os símbolos 0, 1, 2 e 3 da base 4, por a, b, c e d, e considerando o zero à esquerda, teremos:

aa, ab, ac, ad, ba, bb, bc, bd, ca, cb, cc, cd, da, db, dc, dd, baa, etc
\___________________________ ________________________________/
                            v
            Permutações dois a dois de a, b, c, d

O processo de contagem nada mais é que a permutação-r dos símbolos da bases. Esse conceito para gerar permutações já foi exemplificado em [2].

2) Método da expansão

Esse método utiliza a definição de um número em sua forma polinomial:

abcdef = a*(B^5) + b*(B^4) + c*(B^3) + d*(B^2) + e*(B^1) + f*(B^0)

sendo B, a base atual do número abcdef.

A intuição disso é que um símbolo na posição d só é mudado quando os símbolos nas posições e e f alcançam o limite B de símbolos. Para um símbolo em e variar é necessário que o símbolo em f ultrapasse o limite B de símbolos, daí vem o e*B. O mesmo acontece em d, só que agora com um grau de dependência maior: para se mudar um símbolo em d, é necessário que o símbolo em e chegue ao limite B, porém para que cada símbolo em e seja mudado, é necessário que o símbolo em f chegue também ao limite B. Daí chegamos em d*B^2, pois para cada símbolo em e, um total de B símbolos em f foram usados, resultando em B^2. Esse processo se repete para as outras posições até que o resultado final do polinômio seja igual a quantidade de símbolos usados.

Exemplo:

111001(2) = (1*2^5 + 1*2^4 + 1*2^3 + 0*2^2 + 0*2^1 + 1*2^0)(10) 
          = (32   +  16   +   8   +   0   +  0    +  1)(10)
          = 57(10)

O resultado já saiu na base 10, porque resolvi as operações usando aritmética decimal. Se resolvermos essas operações em uma base m, o resultado final também estará na base m. Veja:

57(10) = (5*10^1 + 7*10^0)(10)
       = (101*1010^1 + 111*1010^0)(2) <- símbolos da base 2
       = (110010 + 111)(2)
       = 111001(2)                    <- resultado na base 2

A desvantagem desse método de conversão é que ele exige que as operações matemáticas sejam feitas em cima da base destino. Se a base destino for a decimal, fica fácil a conversão, porém, para outras bases, o processo não parece muito intuitivo, como vimos no exemplo acima.

3) Método da divisão

Convertendo 13(10) para binário, obtemos a representação 1101(2). Para alcançar esse resultado, seguimos o procedimento: divida 13 por 2. Obtenha o quociente 6 e o resto 1. Continue esse processo, mas utilizando o último quociente como dividendo. Pare somente quando o quociente for zero. Os restos gerados, se anotados de forma contrária, nos darão a representação do 13(10) em binário.

13 / 2 = 6     resto 1
6  / 2 = 3     resto 0
3  / 2 = 1     resto 1
1  / 2 = 0     resto 1

Logo, 13(10) = 1101(2)

Veja que esse método funciona para qualquer base:

35(10) / 3 = 11    resto 2
11     / 3 = 3     resto 2
3      / 3 = 1     resto 0
1      / 3 = 0     resto 1

Logo: 35(10) = 1022(3)

Em ambos exemplos utilizei a base 10 como base fonte. O motivo disso é que a operação de divisão nessa base é muito mais intuitiva que em qualquer outra.

A prova que isso funciona é mostrada formalmente na referência [6]. Basicamente ela nos diz o seguinte: suponha Ai um inteiro, Ai+1 o quociente da divisão, ri o resto da divisão para algum i>=0 e t a base destino. Podemos escrever Ai como:

Ai = (Ai+1)*t + ri

dividindo por t:

(Ai)/t = Ai+1 + (ri)/t

Sendo A0 o inteiro que queremos converter e t a base destino. Pelo método da expansão, temos:

Prova método da divisão

Prova método da divisão

Em seguida aplicamos em A1 o mesmo que fizemos em A0 e a cada iteração obteremos um resto ri = bi, onde bi é o símbolo na base t correspondente à posição i do resultado. A expansão e o restante da prova podem ser vistos na referência [6].

Base 10 como intermediária

Em geral, evitamos realizar operações com bases não muito usuais (ex: base 7). O ideal, então, é utilizar a base 10 sempre que possível em algum lugar no processo de conversão.

Isso é, para converter uma base n para uma base m, é sempre mais fácil fazer, primeiramente, a conversão da base n para base 10 pelo método da expansão, e em seguida transformar da base 10 para a base m pelo método da divisão.

1465(8) = (1*8^3 + 4*8^2 + 6*8^1 5*8^0)(10)
        = (512 + 256 + 48 + 5)(10)
        = 821(10)
        = 821(10) / 3 = 273  resto 2
        = 273     / 3 = 91   resto 0
        = 91      / 3 = 30   resto 1
        = 30      / 3 = 10   resto 0
        = 10      / 3 = 3    resto 1
        = 3       / 3 = 1    resto 0
        = 1       / 3 = 0    resto 1

Logo, 1465(8) = 1010102(3)

Conversão de base no Linux

No Linux temos vários modos de se converter bases numéricas. Programas ou bult-ins como bc, dc, printf, $(()) podem ser usados para essa tarefa. Aqui no blog tem um bom material sobre isso na referência [7].

Implementação

Para finalizar o assunto discutido nesse post, colocarei um código em C implementando funções para o método da expansão e para o método da divisão.

/*
 * [dbase.c]
 * Converte um número da base N
 * para a base M, indicadas como
 * parâmetro.
 *
 * [Autor]
 * Marcos Paulo Ferreira (daemonio)
 * undefinido gmail com
 * https://daemoniolabs.wordpress.com
 *
 * [Compilação]
 * $ gcc -o dbase dbase.c
 *
 * [Uso]
 * $ ./dbase <numero> <basen> <basem>
 *
 * ex:
 *   $ ./dbase 1100110101010 2 16
 *   19AA
 *   $ ./dbase 19AA 16 2
 *   1100110101010
 *   $ ./dbase EBACOCA 16 29
 *   C1BCLP
 *
 * [Observações]
 * - Esse código deve ser usado somente para
 *   propósitos educacionais. Nenhuma checagem
 *   por erros foi feita para não aumentar o
 *   tamanho do código.
 *
 * Versão 1.0, by daemonio @ Wed Jul 18 16:08:38 BRT 2012
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char *alfabeto = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ;

/* Retorna o valor absoluto de cada caractere
 * no alfabeto. */
long int index_alfabeto(char c) {
    char *p ;

    if((p=strchr(alfabeto, c))) {
        return (long int)(p - alfabeto);
    } else {
        return (long int)NULL ;
    }
}

/* Utiliza o método da expansão para converter
 * um número na base N para a base 10.
 * Entradas: Uma string numérica na base N.
 *           Base numérica do 1o parâmetro.
 * Saída   : Saída inteira do resultado.
 */

long int baseNtobase10(char *strnum, int basen) {
    long int outnum = 0;
    long int exp = 1;
    long int index;

    index = strlen(strnum) - 1;

    if(index <= 0) return -1 ;

    while(index >= 0) {
        outnum += index_alfabeto(strnum[index])*exp;
        index-- ; exp = exp * basen ;
    }

    return outnum ;
}

/* Utiliza o método da divisão para converter
 * um número na base 10 para a base N.
 * Entradas: Buffer para armazenar o resultado.
 *           Inteiro na base 10.
 *           Base de saída.
 * Saída   : String numérica na base M.
 */
void base10tobaseM(char *str, long int inpnum, int basem) {
    int istr = 0;

    /* Número zero. */
    if(inpnum == 0) {
        str[0]='0'; istr=1;
    }

    /* Mapeia o resto da divisão no alfabeto e
     * faz do quociente o novo dividendo. */
    while(inpnum) {
        str[istr++]=alfabeto[inpnum%basem];
        inpnum /= basem;
    }
    str[istr]=0 ;

    /* Inverte a string. */
    if(istr>0) {
        char t ;
        long int i=0 ;

        istr-- ;
        while(i <= istr/2) {
            t = str[istr-i] ;
            str[istr-i] = str[i];
            str[i++] = t;
        }
    }
}

int main(int argc, char **argv) {
    char saida[100] ;
    long int numbase10 ;

    if(argc != 4) {
        printf("[uso] ./dbase <num> <baseN> <baseM>\n") ;
        return -1 ;
    }

    /* Os parâmetros são passados diretamente, deveria
     * ter uma checagem de erro antes. */
    numbase10 = baseNtobase10(argv[1], atoi(argv[2])) ;
    base10tobaseM(saida, numbase10, atoi(argv[3])) ;

    /* Saída. */
    puts(saida) ;

    return 0;
}

Exemplo de execução

$ gcc -o dbase dbase.c

# Da base 2 para base 10
$ ./dbase 101101101010 2 10
2922

# Da base 4 para base 2
$ ./dbase 103320222 4 2
10011111000101010

# Da base 5 para base 18
$ ./dbase 4443302111 5 18
52EE31

# Da base 16 para base 32
$ ./dbase BEBA 16 32
1FLQ

Referências

[1] Conversão de base numérica by wikipedia (Acessado em: Julho/2012)

http://pt.wikipedia.org/wiki/Convers%C3%A3o_de_base_num%C3%A9rica

[2] Gerando Permutações-r Com Repetição em C, by Daemonio (Acessado em: Julho/2012)

https://daemoniolabs.wordpress.com/2011/02/11/gerando-permutacoes-r-com-repeticao-em-c/

[3] Codificação base64 em C by Daemonio (Acessado em: Julho/2012)

https://daemoniolabs.wordpress.com/2011/09/09/codificacao-base64-em-c/

[4] Algarismo, by wikipedia (Acessado em: Julho/2012)

http://pt.wikipedia.org/wiki/Algarismo

[5] Numeração Decimal by Escola Kids (Acessado em: Julho/2012)

http://www.escolakids.com/numeracao-decimal-1.htm

[6] Conversion of Number Representations by Lionel E. Deimel (Acessado em: Julho/2012)

http://www.deimel.org/comp_sci/conversion.htm

[7] Mudança de base no Linux by Daemonio (Acessado em: Julho/2012)

https://daemoniolabs.wordpress.com/2011/09/15/mudanca-de-base-no-linux/

Números Por Extenso: Algoritmo Geral

Introdução

Há algum tempo eu fiz um script em SED que escrevia por extenso um dado número inteiro. Para criar esse script, tive que quebrar um pouco a cabeça, devido a limitação do SED em não fornecer operações aritméticas, mas somente operações em string. Acabou que “criei” um método geral de conversão, que é muito semelhante ao que mostrarei hoje. A vantagem desse método é que ele trata o número de entrada como uma string, nos permitindo converter números extremamente grandes.

Hoje aprenderemos um algoritmo que tem por finalidade escrever por extenso um dado número de entrada.

Primeira parte: um trio por extenso

Primeiramente, vamos analisar como escrever por extenso um número menor que 1000, ou seja, todos os números na faixa [000, 999]. Como não há como gerar os valores por extenso por meio de algoritmos, o único jeito de recuperar esses valores é armazendo-os em algum lugar, tipo uma matriz. Em C, essa matriz é similar a:

/* A palavra dummy é para preencher uma posição
para que cada linha tenha sempre 10 colunas. */
matrizextenso[4][10] = {
{"dummy", "um", "dois", "três", ... , "nove"},
{"dez", "onze, "doze", "treze", ...., "dezenove",
{"dummy", "dummy", "vinte", "trinta", ..., "noventa"},
{"dummy", "cento", "duzentos", "trezentos", ..., "novecentos"}
}

Usando essa matriz, podemos facilmente escrever por extenso um número de três algarismos. Considere o seguinte número: 456. Escrevemos esse número por extenso somente acessando a matriz:

ex: 456
matrizextenso[0][6] = seis
matrizextenso[2][5] = cinquenta
matrizextenso[3][4] = quatrocentos

Observe que os algarismos do número indexam as colunas da matriz enquanto que as linhas são indexadas “manualmente”.

Para números que tem o ‘1’ na casas das dezenas, a indexação é ligeiramente diferente. Considere o número 712:

ex: 712
matrizextenso[1][2] = doze
matrizextenso[3][4] = setecentos

A diferença é que o acesso pelo índice 1 substitui os acessos pelos índices 0 e 2.

Para números com algarismos zeros, a indexação naquela posição não deve ser feita.

ex: 103
matrizextenso[0][3] = três
matrizextenso[3][1] = cento

O índice 2 não foi usado devido o número conter ‘0’ nas casas das dezenas. Outro exemplo:

ex: 002
matrizextenso[0][2] = dois

Infelizmente temos algumas exceções que devem ser tratadas separadamente. São elas: o número 100 (cem) e o 000 (zero).

Com essas regras, podemos montar um pequeno algoritmo para escrever um número de três dígitos por extenso:

definição: escrever_trio_extenso
A entrada é um número da forma: cdu (as letras representam cada algarismo)
A saída é guardada na variável saída

Se trio == "100" : saída = "cem"
senão se trio == "000" : saída = "zero"
senão:
     se c != '0' : saída = matrizextenso[3][c]
     se d == '1':
        saída = saída + matrizextenso[1][u]
     senão:
        se d != '0' : saída = saída + matrizextenso[2][d]
        se u != '0' : saída = saída + matrizextenso[0][u]
     saída = adicionar_conjuncao_e_(saída)

Por fim, devemos adicionar a conjunção ‘e’, como em vinte ‘e’ três. Esse processo não é tão simples quanto parece, principalmente em linguagens que não suportam muitas operações em strings (ex: C). Aqui usei a função adicionar_conjuncao_e() que faz esse papel e sua implementação será ocultada.

Exemplo:

número: 406
Seguindo o algoritmo:

saída = matrizextenso[3][4]
      = quatrocentos

saída = saída + matrizextenso[0][6]
      = quatrocentos seis

saída = adicionar_conjuncao_e(saída)
      = quatrocentos e seis

Qualquer número por extenso: um esboço

Um número grande por extenso nada mais é que conjuntos de três algarismos por extenso. Isso é, para obter esse número maior por extenso, basta dividi-lo em grupos de três e transformar cada grupo separadamente. Veja:

número: 123456789

Divida o número em trios:
  789 : setecentos e oitenta e nove
  456 : quatrocentos de cinquenta e seis
  123 : cento e vinte e três

A saída final consiste na adição das classes (próximo tópico) e da conjunção ‘e’ ou vírgula. É bem mais prático considerar a divisão em trios de modo invertido, como mostra o exemplo, por facilitar as operações.

Segunda Parte: classes

O que chamo de classe aqui é o nome dado a cada elemento do conjunto formado pelas palavras: mil, milhão, bilhão, trilhão, etc. Mais uma vez, como não há como gerar o nome dessas classes dinamicamente, teremos que armazená-las em algum lugar, como um vetor.

*classextenso[] = {"dummy", "mil", "milh", "bilh", "trilh", ...., "vigesilh"}

Os nomes dessas classes foram obtidos da referência [1].

Os nomes das classes estão sem variação em número (plural e singular) para não precisarmos criar um vetor para classes no singular e outro no plural. O próximo passo do algoritmo será como decidir quando usar singular ou plural.

Antes de tudo, vejamos um exemplo:

número: 123 456 789 012 345
 345 : trezentos e quarenta e cinco       0
 012 : doze                               1
 789 : setecentos e oitenta e nove        2
 456 : quatrocentos e cinquenta e seis    3
 123 : cento e vinte e três               4

O número na última coluna nada mais é que um contador de posições. Esse número indexará no vetor de classes qual classe será usada naquele momento. Isso é, se usarmos esse número como índice no vetor classextenso, obteremos a classe desejada. Assim:

número: 123 456 789 012 345

 345 : trezentos e quarenta e cinco       
 012 : doze                               classextenso[1] : mil
 789 : setecentos e oitenta e nove        classextenso[2] : milh
 456 : quatrocentos e cinquenta e seis    classextenso[3] : bilh
 123 : cento e vinte e três               classextenso[4] : trilh

OBS: Para contador igual a zero, nenhuma classe é mapeada (posição “dummy” do vetor).

Um algoritmo simples para obter as classes é:

Para cada trio do número em ordem inversa:
   contador = 0
   Se contador > 0 e trio > 0
      saída = classextenso[contador]
   contador = contador + 1

Terceira parte: plural ou singular

Algo que dificulta em muito a escrita por extenso é a variação de número das classes, pois ora ou outra temos que decidir se usamos, por exemplo, “milhões” ou “milhão”. Em geral, decidir entre esses dois casos é simples: se o trio em questão for igual a ‘1’ usa-se singular, caso contrário usamos plural. Observe que só colocamos plural para classes acima de mil, pois a classe mil sempre vem no singular.

Com isso, para decidirmos se usamos plural ou singular, fazemos:

Para cada trio do número em ordem inversa:
   contador = 0
   Se contador > 0 e trio > 0
      saída = classextenso[contador]
      se contador > 1
         se trio > 1 : saída = saída + "ões"
         senão       : saída = saída + "ão"
   contador = contador + 1

O algoritmo nos diz que só acessamos as classes se o contador for maior que 0, e adicionamos plural ou singular se o contador for maior que 1 (classes acima de mil).

Exemplos:

número: 1789415234001
001 : um                            0
234 : duzentos e trinta e quatro    1 : classextenso[1] = mil
415 : quatrocentos e quinze         2 : classextenso[2] = milh + "ões" (415 != 1)
789 : setecentos e oitenta e nove   3 : classextenso[3] = bilh + "ões" (789 != 1)
001 : um                            4 : classextenso[4] = trilh + "ão" (001 == 1)

Juntando tudo, temos:

um
duzentos e trinta e quatro mil
quatrocentos e quinze milhões
setecentos e oitenta e nove bilhões
um trilhão

Agora só basta juntar as linhas para formar a saída completa. Essa junção é feita pelo conectivo ‘e’ ou pela vírgula, e isso nos leva ao próximo passo.

Quarta Parte: junção dos extensos

De acordo com o gerador de extensos em [2], temos dois modos de juntar os extensos dos trios. Uma é usando ‘e’ e outra, a vírgula.

Basicamente, para cada trio não nulo de um número (exceto o último) e começando do trio mais à direita: se o trio à esquerda for o “000”, usamos o ‘e’. Se o trio dado for menor que 100 também usamos o ‘e’. Caso contrário, para trios maiores ou iguais a 100, usamos a ‘,’.

Espero não ter complicado muito. A seguir um exemplo para esclarecer essa regra:

Exemplo:

número: 1000420055333

001 000 420 055 333
   |   |   |   |
   |   |   |   v
   |   |   |  ',', pois 055 != 0 e 333 >= 100 
   |   |   v           
   |   |  'e', pois 420 != 0 e 055 < 100 
   |   v
   |  'e', pois 000 == 000
   v
  'e', pois 001 != 000 e 001 < 100

Último passo: algoritmo geral

Agora vamos juntar tudo que vimos e elaborar um algoritmo geral para a escrita por extenso.

saída=
extensofinal=
contador=0

Para cada trio do número em ordem inversa:
   se trio > 0
      saída = escrever_trio_extenso(trio)
      se contador > 0
         saída = saída + " " + classextenso[contador]
         se contador > 1:
            se trio > 1 : saída = saída + "ões"
            senão : saída = saída + "ão"
      se nao_e_ultimo_trio()
         se trio_a_esquerda_eq_zero()
            saída = " e " + saida
         senão se trio >= 100
            saída = ", " + saída
         senão
            saída = " e " + saída
      extensofinal = saída + extensofinal
   contador = contador + 1

imprima(extensofinal)

Note que há três funções nesse algoritmo. A escrever_trio_extenso() é a função definida para escrever um trio por extenso. A função nao_e_ultimo_trio() retorna verdadeiro se o trio analisado não é o último trio do número – o trio mais à esquerda. A outra função, trio_a_esquerda_eq_zero(), retorna verdadeiro se o trio à esquerda do trio atual não for zero. Essas duas funções só existem devido a regra de junção de extensos e suas implementações serão ocultadas aqui.

Esse algoritmo pode ser implementado na maioria das linguagens de programação, especialmente àquelas que fornecem um alto poder de tratamento de strings (ex: regex). Para algumas linguagens, algumas restrições devem ser acrescentadas no algoritmo, por exemplo em C, em que o acesso ao vetor classsextenso deve ser controlado para não gerar erro de leitura em memórias não acessíveis.

O script: dextenso.sh

A seguir um shell script que utiliza o algoritmo anterior para escrever um número de entrada por extenso.

#!/bin/bash
# [dextenso.sh]
# Retorna um número de entrada por extenso.
#
# [Autor]
# Marcos Paulo Ferreira (Daemonio)
# undefinido gmail com
# https://daemoniolabs.wordpress.com
#
# [Codificação]
# Script : UTF-8
# Saída  : UTF-8
#
# [Uso]
# $ chmod +x dextenso.sh
# $ ./dextenso.sh <numero>
#
# ex:
# $ ./dextenso.sh 48000069
# quarenta e oito milhões e sessenta e nove
#
# Versão 1.0, by daemonio @ Sun Jun 24 15:19:30 BRT 2012
#

#
# Variáveis globais
#

# Array com os números em extenso.
# OBS: A palavra dummy é só um marcador de
# lugar que assegura uma indexação correta.
trioextenso=( "dummy" "um" "dois" "três" "quatro" "cinco" "seis" "sete"
              "oito" "nove"
              "dez" "onze" "doze" "treze" "quatorze" "quinze" "dezesseis"
              "dezessete" "dezoito" "dezenove"
              "dummy" "dummy" "vinte" "trinta" "quarenta" "cinquenta"
              "sessenta" "setenta" "oitenta" "noventa"
              "dummy" "cento" "duzentos" "trezentos" "quatrocentos"
              "quinhentos" "seiscentos" "setecentos" "oitocentos"
              "novecentos" )

# Array com as classes em extenso.
classextenso=( "dummy" "mil" "milh" "bilh" "trilh" "quatrilh"
               "quintilh" "sextilh" "septilh" "octilh"
               "nonilh" "decilh" "undecilh" "duodecilh"
               "tredecilh" "quatordecilh" "quindecilh"
               "sexdecilh" "setedecilh" "octodecilh"
               "novedecilh" "vigesilh" )

#
# Funções
#

# Escreve um trio por extenso.
function escrever_trio_extenso {
local trio=$1
local saida=
local u= d= c= t=

# Obtém cada algarismo do trio.
c=${trio:0:1}; d=${trio:1:1}; u=${trio:2:1}

case "$trio" in
    100)
        saida='cem'
        ;;
    [0-9]1[0-9]) # Números da forma: x1x.
        t=$((10+$u)); saida=${trioextenso[$t]}
        [ "$c" != '0' ] && t=$((30+$c)) && saida=${trioextenso[$t]}' '$saida
        ;;
    *) # Qualquer número, exceto 100 e os da forma x1x.
        [ "$u" != '0' ] && saida=${trioextenso[$u]}
        [ "$d" != '0' ] && t=$((20+$d)) && saida=${trioextenso[$t]}' '$saida
        [ "$c" != '0' ] && t=$((30+$c)) && saida=${trioextenso[$t]}' '$saida
        ;;
esac

# Adiciona a conjução 'e'. Basicamente insere
# a string " e " no lugar dos espaços que não
# estejam no início ou no fim.
saida=$(sed 's/^ *//;s/ *$//;s/ / e /g'<<<"$saida")
echo "$saida"
}

# Retorna verdadeiro se o trio dado
# não é o último. Para isso, ela recebe
# o número dado no primeiro parâmetro e
# o contador no segundo.
function nao_e_ultimo_trio {
local numero=$1
local contador=$2

(( $contador < ((${#numero}/3)-1) ))
}

# Retorna verdadeiro se o trio à esquerda
# do trio dado não é zero. Para isso, ela
# recebe o número dado no primeiro
# parâmetro e o contador no segundo.
function trio_esquerda_eq_zero {
local numero=$1
local contador=$2
local t=$(( ${#numero} - 3- 3*(contador+1)))

(( 10#${numero:$t:3} == 0 ))
}

# Função principal. Ela representa o algoritmo final
# visto no post.
function dextenso {
local numero=$1
local contador=0
local saida=
local extensofinal=
local t=

# Retira os zeros iniciais
numero=$(sed 's/^0*//'<<<$numero)

# Número zero.
[ -z $numero ] && echo zero && return 0

# Padding para que a quantidade de algarismos desse
# número seja múltipla de três.
t=$((3-${#numero}%3))
[ $t != 3 ] && numero=$(echo '000' | cut -c1-$t)$numero

# Para cada trio na ordem inversa do número...
for trio in $(echo "$numero" | rev | sed 's/.../&\n/g')
do
    # Obtém a ordem normal do trio usando o comando
    # rev novamente.
    trio=$(rev<<< "$trio")

    if [ "$trio" -ne 0 ]
    then
        # Extenso de um trio.
        saida=$(escrever_trio_extenso $trio)

        # Classes + plural.
        if [ "$contador" -gt 0 ]
        then
            saida="$saida"' '"${classextenso[$contador]}"
            if [ "$contador" -gt 1 ]
            then
                [ "$trio" -gt 1 ] && saida="$saida"'ões' || saida="$saida"'ão'
            fi
        fi

        # Junção.
        if nao_e_ultimo_trio $numero $contador
        then
            if trio_esquerda_eq_zero $numero $contador
            then
                saida=" e "$saida
            elif [ $trio -ge 100 ]; then
                saida=", "$saida
            else
                saida=" e "$saida
            fi
        fi

        # Monta a saída.
        extensofinal="${saida}${extensofinal}"
    fi

    let contador++
done

# Fim do algoritmo. Mostra a saída.
echo "$extensofinal"
}

function show_help {
echo 'dextenso - by daemonio'
echo '[uso] ./dextenso.sh <numero>'
echo
exit 1
}

#
# Main
#

# Checa parâmetros.
[ -z "$1" ] && show_help

# Retorna '2' se o parâmetro passado
# não é um número.
[[ "$1" =~ ^[0-9]*$ ]] || exit 2

# Chama a função, passando o número de
# entrada como parâmetro.
dextenso $1

#EOF

Exemplos:

$ chmod +x dextenso.sh
$ ./dextenso.sh 78991100101
setenta e oito bilhões, novecentos e noventa e um milhões, cem mil, cento e um

$ ./dextenso.sh 1000450002578
um trilhão e quatrocentos e cinquenta milhões e dois mil, quinhentos e setenta e oito

$ ./dextenso.sh 1989 
um mil, novecentos e oitenta e nove

Conclusão

A vantagem do método apresentado é que ele considera o número de entrada como uma string, e graças a isso, não estamos limitados às restrições que determinas linguagens impõem no tratamento de números, como tamanho máximo para não ocorrer overflow.
Vale ressaltar mais uma vez que o método apresentado foi inteiramente baseado na saída do gerador do link [2], e é provável que eu tenha deixado alguma coisa passar (ou me baseado na referência errada). Se você encontrou algum erro na saída do script, por favor o relate nos comentários.

Referências

[1] Escalas curta e longa by wipedia (Acessado em: Junho/2012)
http://pt.wikipedia.org/wiki/Escalas_curta_e_longa

[2] Calculadora de Números Decimais por Extenso by Matemática Didática (Acessado em: Junho/2012)
http://www.matematicadidatica.com.br/CalculadoraNumerosDecimaisPorExtenso.aspx

Solução Iterativa Para o Problema Da Torre De Hanói

Introdução

A resolução do quebra-cabeça Torre de Hanói é um problema conhecido na área da computação. A sua solução mais comum é um dos exemplos clássicos de recursão e bastante usada didaticamente para exemplificar algoritmos recursivos.

Nesse post de hoje iremos aprender como montar uma solução iterativa para o problema das torres. Essa solução parece ser a mais eficiente até hoje [7], sendo bastante rápida e concisa, porém um pouco “misteriosa” por envolver manipulação de bits.

Definição

Torre de Hanói

Torre de Hanói

Torre De Hanói é um quebra-cabeça criado pelo matemático francês Edourd Lucas em 1883. Esse quebra-cabeça consiste de 3 hastes e n discos, inicialmente dispostos em ordem decrescente de tamanho na haste esquerda. O objetivo desse jogo é transferir os n discos da haste inicial para a haste mais a direita, seguindo as regras:

  • Um disco deve ser movido de cada vez.
  • Cada movimento consiste em retirar um disco do topo de uma haste e passar para o topo de outra haste.
  • Um disco maior não deve ficar por cima de um disco menor.

Versões Online

Antes de mais nada, gostaria de dizer que há várias versões online da Torre de Hanói. A referência [2] contém uma dessas versões.

No decorrer desse tutorial sempre que possível tire suas dúvidas nas versões online.

Solução mínima geral [3]

Dado um número n de discos, é possível encontrar matematicamente o menor número de movimentos a serem feitos para solucionar o problema das torres. Obviamente você pode gastar um número maior de movimentos que o mínimo, porém é sempre útil encontrarmos uma solução mínima para um determinado problema.

O algoritmo abaixo, se seguido, garantirá a solução ótima, ou seja, o menor número de movimentos.

  1. Mova o menor disco para a haste não recentemente visitada. Se for o primeiro movimento do jogo, mova o disco menor para a haste destino se o número de discos for ímpar, caso contrário, mova o disco menor para a haste auxiliar.
  2. Mova o disco disponível (nesse ponto só há um movimento “legal” a ser feito)
  3. Repita os passos 1 e 2 até resolver o problema.

Para verificar a validade desse algoritmo você pode fazer anotações para números pequenos de n (ex: n=3).

OBS: A “prova” de que esse algoritmo garante o menor número de movimentos não será vista aqui. Para mais informações consulte [3].

Versão Recursiva

A versão recursiva para esse problema é muito usada em cursos de computação como método didático para ensinar recursão. A seguir um roteiro mostrando como implementar esse problema de forma recursiva [2].

Primeiro, chamamos as três hastes origem, auxiliar e destino de ORI, AUX e DEST, respectivamente. Para resolver o problema, cedo ou tarde o disco inferior (= o primeiro de baixo pra cima) deverá ser movido de ORI para DEST. Nesse ponto, todos os discos restantes estarão empilhados em forma decrescente na haste auxiliar (veja esse fato na animação em [2]). Após mover o disco inferior de ORI para DEST, os discos restantes terão que ser movidos de AUX para DEST. Portanto, para um número n de discos, o problema parece ser resolvido se seguirmos esses passos:

  1. Mova os n-1 discos do topo de ORI para AUX, usando DEST como auxiliar.
  2. Mova o disco de baixo de ORI para DEST.
  3. Mova os n-1 discos de AUX para DEST, usando ORI como auxiliar.

Baseando-se nisso, temos uma solução recursiva para o problema:

/* [hanoirecursivo.c]
 * Solução recursiva para o problema da
 * torre de hanoi.
 * 
 * [Compilação]
 * $ gcc -o hanoirecursivo hanoirecursivo.c
 *
 * [Execução]
 * ./hanoirecursivo 3
 *
 * Sendo 3 o número de discos.
 *
 */
#include <stdio.h>
#include <stdlib.h> /* for atoi() */

void hanoi(int n, char *from, char *temp, char *to) {
    if ( n == 0 ) return ;

    hanoi(n-1, from, to, temp) ;
    printf("Mova disco %d de %s para %s\n", n, from, to) ;
    hanoi(n-1, temp, from, to) ;
}

int main(int argc, char **argv) {
    int i ;
    if ( argc > 1 )
        i = atoi(argv[1]) ;
    else
        i = 3 ; /* padrão: três discos */

    hanoi(i, "A", "B", "C") ; /* ORI, AUX, DEST */

    return 0;
}

O programa acima resolve de forma ótima o problema da torre de hanói:

$ ./hanoirecursivo 3
Mova disco 1 de A para C
Mova disco 2 de A para B
Mova disco 1 de C para B
Mova disco 3 de A para C
Mova disco 1 de B para A
Mova disco 2 de B para C
Mova disco 1 de A para C

$ ./hanoirecursivo 4
Mova disco 1 de A para B
Mova disco 2 de A para C
Mova disco 1 de B para C
Mova disco 3 de A para B
Mova disco 1 de C para A
Mova disco 2 de C para B
Mova disco 1 de A para B
Mova disco 4 de A para C
Mova disco 1 de B para C
Mova disco 2 de B para A
Mova disco 1 de C para A
Mova disco 3 de B para C
Mova disco 1 de A para B
Mova disco 2 de A para C
Mova disco 1 de B para C
$ ./hanoirecursivo 3 | wc -l
7
$ ./hanoirecursivo 4 | wc -l
15

Total de passos

De acordo com a solução recursiva, podemos montar uma equação de recorrência para esse problema:

T0 = 0
T(n) = 2*T(n-1) + 1

A função T(n) recebe um n (quantidade de discos) de entrada e retorna o número mínimo de movimentos necessários para movimentar todos os discos da haste origem até o destino.

Em [2] vemos como expandir essa equação a fim de encontrarmos uma fórmula fechada para ela:

f(n) = 2^n - 1

Sabendo disso, a quantidade mínima de movimentos para resolver o problema com n=3 discos é:

f(3) = 2^3 - 1
          = 8 - 1
          = 7

E para n=4:

f(4) = 2^4 - 1
          = 16 - 1
          = 15

Com isso comprovamos que a saída do programa hanoirecursivo foi correta (veja as duas últimas linhas da saída do programa).

OBS: A explicação das fórmulas apresentadas está além do escopo desse post. Para mais detalhes, veja referência [2].

Soluções Iterativas

Soluções iterativas são aquelas que resolvem um problema sem usar recursão, usando laços de repetição e em alguns casos, estruturas de dados, como pilha, fila, árvore, etc. No caso da torre de Hanói, a solução recursiva é muito mais intuitiva do que suas soluções iterativas, pois essas últimas necessitam de uma análise especial nas movimentações dos discos. Nesse post irei apresentar uma solução iterativa que utiliza os bits de um certo número k para gerar as movimentações. Essa solução não é única para o problema e existem outras, como podemos ver no link da wikipédia em [1].

A solução iterativa misteriosa

De acordo com [7] existe uma solução um tanto que misteriosa para resolução do problema da torre de hanói: todos os movimentos mínimos entre os discos estão “escondidos” na representação binária de um número.

O código

O seguinte código resolve o quebra-cabeça da torre de Hanói iterativamente:

/*
 * [hanoiterativo.c]
 * Resolve o quebra-cabeçaa da torre de Hanoi
 * de modo iterativo.
 *
 * [Compilação]
 * $ gcc -o hanoiterativo hanoiterativo.c
 *
 * [Execução]
 * $ ./hanoiterativo 3
 *
 * Sendo 3 o número de discos.
 */
#include <stdio.h>
#include <stdlib.h> /* for atoi() */

int main(int argc, char **argv) {
    int n = 3 ;
    int from, to, k ;

    if ( argc > 1 ) n = atoi(argv[1]) ;

    for(k=1; k < (1 << n); k++) {
        /* obtem haste origem */
        from = (k&(k-1))%3 ;

        /* obtem haste destino */
        to   = ((k|(k-1))+1)%3 ;

        /* descreve o movimento */
        printf("Mova disco de %d para %d\n", from, to) ;
    }

    return 0;
}

A saída do programa, para n=3 será:

$ ./hanoiterativo 3
Mova disco de 0 para 2
Mova disco de 0 para 1
Mova disco de 2 para 1
Mova disco de 0 para 2 (***)
Mova disco de 1 para 0
Mova disco de 1 para 2
Mova disco de 0 para 2

Considere, por enquanto, as hastes numeradas como 0, 1, 2 ao invés de “A”, “B” e “C” como abordado anteriormente.

Bem, observando o código percebemos que a mágica toda é feita usando manipulação de bits. Primeiro vemos que a condição do laço for é dada por (1<<n), que nada mais é que 2^n. Dessa forma o loop  itera por 2^n – 1 vezes, que na verdade é o número mínimo de movimentos para n discos.

Observando a saída do programa acima, percebemos que alguns padrões aparecem. Por exemplo, repare que as linhas acima e abaixo da linha central (marcada com asteriscos) são iguais, com a única diferença na ordem em que aparecem. Isso é uma consequência direta do algoritmo recursivo, pois nenhuma operação aritmética é feita nele, somente permutações das hastes.

O que as operações bit a bit do último código fazem é exatamente isso: Geram resultados que “imitam” perfeitamente os movimentos da torre de Hanói. A tabela seguinte mostra os valores para os primeiros 7 valores de k:

+---+------------------+-----------------+
|k  | (k & (k-1)) % 3  | (k | (k-1)) % 3 |
+---+------------------+-----------------+
|1  |    0 % 3 = 0     |    2 % 3 = 2    |
+---+------------------+-----------------+
|2  |    0 % 3 = 0     |    4 % 3 = 1    |
+---+------------------+-----------------+
|3  |    2 % 3 = 2     |    4 % 3 = 1    |
+---+------------------+-----------------+
|4  |    0 % 3 = 0     |    8 % 3 = 2    |
+---+------------------+-----------------+
|5  |    4 % 3 = 1     |    6 % 3 = 0    |
+---+------------------+-----------------+
|6  |    4 % 3 = 1     |    8 % 3 = 2    |
+---+------------------+-----------------+
|7  |    6 % 3 = 0     |    8 % 3 = 2    |
+---+------------------+-----------------+

Agora veja novamente a saída do programa hanoirecursivo, para n = 3:

$ ./hanoirecursivo 3 | tr ABC 012 # tr troca A por 0, B por 1 e C por 2
Mova disco 1 de 0 para 2
Mova disco 2 de 0 para 1
Mova disco 1 de 2 para 1
Mova disco 3 de 0 para 2
Mova disco 1 de 1 para 0
Mova disco 2 de 1 para 2
Mova disco 1 de 0 para 2

Repare que a sequência de movimentos (0, 2), (0, 1), (2, 1), …, (0, 2) é a mesma representada pelas expressões bit-a-bit vistas na tabela acima.

Prova

A seguir uma prova de que as expressões bit-a-bit geram, de fato, a sequência de movimentos do problema da torre de Hanói. Tudo dito aqui pode ser conferido na referência [5].

1) Nomeação das hastes

As três hastes são nomeadas como 0, 1, 2 para n ímpar e como 0, 2, 1 para n par, dessa maneira a haste final sempre será a 2 e o movimento inicial de 0 para 2. Em alguns casos, a haste do meio pode ser considerada final, em outros não, pois muitos algoritmos só consideram a haste 2 como final (vide [2]). Em geral, pense que a haste final não depende do algoritmo, mas somente dos “nomes” das próprias hastes.

2) Obtendo as hastes a partir de k

Considere k como o movimento de algum disco. Sabemos que para n=3 discos, temos no total 7 movimentos e com isso, o k varia de 1 a 7.

É fácil notar que para k ímpar o disco número 1 é que se movimenta e esse movimento é descrito pela sequência 0->2->1->0 para n ímpar e por 0->1->2->0 para n par. Em termos de k, o disco 1 se move da haste (k-1)%3 para a haste (k+1)%3, independente do n.

Para k par, podemos ter discos ímpares ou pares se movimentando. Para discos ímpares, eles sempre se movem da haste (k+2)%3 para a haste (k+4)%3. Os discos pares se movimentam em ordem inversa, ou seja, da haste (k+4)%3 para a haste (k+2)%3.

Bem, relembrando as propriedades do módulo, vemos que (k-1)%3 é igual a (k+2)%3, pois -1 e +2 representam o mesmo valor no conjunto Z3 = {0,1,2} (ambos representam o elemento +2, pois -1%3=2 e 2%3=2). Por esse mesmo motivo, constatamos que (k+1)%3 é igual a (k+4)%3.

Logo, encontramos algo mais geral que pode ser reformulado da seguinte maneira:

Para todo k: Para j ímpar, o movimento é de (k-1)%3 para (k+1)%3
             Para j par, o movimento é de (k+1)%3 para (k-1)%3

sendo j o identificador de um disco qualquer.

Podemos mostrar que essa propriedade se mantém apenas observando a saída do programa hanoirecursivo.

$ ./hanoirecursivo 3 | tr ABC 012
Mova disco 1 de 0 para 2  # k=1, j=1 (ímpar) move de (1-1)%3=0 para (1+1)%3=2
Mova disco 2 de 0 para 1  # k=2, j=2 (par)   move de (2+1)%3=0 para (2-1)%3=0
Mova disco 1 de 2 para 1  # k=3, j=1 (ímpar) move de (3-1)%3=2 para (3+1)%3=1
Mova disco 3 de 0 para 2  # k=4, j=3 (ímpar) move de (4-1)%3=0 para (4+1)%3=2
Mova disco 1 de 1 para 0  # k=5, j=1 (ímpar) move de (5-1)%3=1 para (5+1)%3=0
Mova disco 2 de 1 para 2  # k=6, j=2 (par)   move de (6+1)%3=1 para (6-1)%3=2
Mova disco 1 de 0 para 2  # k=7, j=1 (ímpar) move de (7-1)%3=0 para (7+1)%3=2

3) Relação entre k e j

Agora vamos encontrar uma relação entre k e j. Antes de tudo, tenha em mente que:

2^(2i + 1) % 3 = 2 (ou seja: 2^ímpar % 3 = 2)
2^(2i)     % 3 = 1 (ou seja: 2^par   % 3 = 1)

Assim, definimos os movimentos feitos para um dado disco j como:

Para i>=0,

Para j ímpar, ou seja j=2i - 1, temos:
     Da haste: (k-1)%3 = (k-2^(j-1))%3
     Para a haste: (k+1)%3 = (k+2^(j-1))%3

Para j par, ou seja j=2i + 2, temos:
     Da haste: (k+1)%3 = (k-2)%3 = (k-2^(j-1))%3
     Para a haste: (k-1)%3 = (k+2)%3 = (k+2^(j-1))%3

Derivamos as hastes para j ímpar impondo que 2^(j-1) %3 seja igual a 1 (2^par %3 = 1). Para j par, temos que considerar que (k+1)%3 = (k-2)%3, pois +1 e -2 tem o mesmo módulo 3. O 2, então, é substituído por 2^(j-1)%3 (2^ímpar %3 = 2), como observado nas propriedades de módulo 3.

Por fim, chegamos em algo importante: o movimento de um disco j qualquer é sempre descrito da seguinte maneira:

Para a dupla (k, j), o movimento de um disco j é descrito
da haste (k - 2^(j-1))%3 para a haste (k + 2^(j-1))%3

Os valores devem vir em dupla, pois um k define exatamente um j e vice-versa. Por exemplo, para n=3 sabemos que o disco j=3, no passo k=4 do algoritmo, se move de (4 – 2^2)%3=0 para (4 + 2^2)%3=2.

4) As operações bit-a-bit

Considere a forma binária de k e suponha um z, tal que:

z = número máximo de zeros mais à direita de k na base binária

ex: k=4=100, z=2
    k=5=101, z=0
    k=6=110, z=1

Definido z, agora vamos observar as operações lógicas vistas no código iterativo. A operação k&(k-1) zera o bit 1 menos significativo de k. Veja:

Para k=5   k&(k-1) = 0101 & 0100 = 0100 = 4
     k=12  k&(k-1) = 1100 & 1011 = 1000 = 8

Curiosamente podemos definir essa operação em termos de z, como sendo:

k&(k-1) = k - 2^z

ex: k=5=0101, z=0, k - 2^z = 5-2^0 = 5-1 = 4 = 0100
    k=12=01100, z=2, k - 2^z = 12-2^2 = 12-4 = 8 = 1000

Compare esses resultados com os anteriores. Outra curiosidade é que a operação k|(k-1) também pode ser definida em termos de z, assim:

k|(k-1) = k + 2^z - 1

Ou seja:

k=5=0101, z=0:
    k|(k-1) = 5|4 = 0101|100 = 101 = 5
    k + 2^z = 5 + 2^0 - 1 = 5
k=8=1000, z=3:
    k|(k-1) = 8|7 = 1000|0111 = 1111 = 15
    k + 2^z = 8 + 2^3 - 1 = 15

Caso tenha dúvidas, verifique você mesmo a veracidade dessas relações com alguns valores de k.

5) Movimentos a partir das operações lógicas

Esse é o último passo da prova. Aqui relacionamos os movimentos definidos em 3) com as operações lógicas em 4).

Sabemos que dado um par (k, j) podemos encontrar o movimento como sendo:
de (k - 2^(j-1)) % 3 para (k + 2^(j-1)) %3. Definindo j = z+1, temos então:

(k - 2^(j-1)) % 3 = (k - 2^z) % 3
                  = (k&(k-1)) % 3

(k + 2^(j-1)) % 3 = (k + 2^z) % 3
                  = ((k|(k-1))+1)%3
(cqd)

Com isso provamos que, dado um k, podemos encontrar “dentro de seus bits” a movimentação de um disco j. Veja que as operações bit-a-bit estão em função de k e por isso não sabemos dizer qual disco está sendo movido. Entretanto, como definimos j = z+1, podemos facilmente encontrar esse disco.

Abaixo, o código final que mostra a solução iterativa da torre de Hanói usando os conceitos demonstrados nesse post:

/*
 * [hanoiterativofinal.c]
 * Resolve o quebra-cabeça da torre de Hanói
 * de modo iterativo.
 *
 * [Compilação]
 * $ gcc -o hanoiterativofinal hanoiterativofinal.c
 *
 * [Execução]
 * $ ./hanoiterativofinal 3
 *
 * Sendo 3 o número de discos.
 */
#include <stdio.h>
#include <stdlib.h> /* for atoi() */

/* Para que a solução final esteja sempre
 * em C, devemos definir os nomes das hastes
 * de acordo com a paridade de n.
 */
char *nomehastepar[] = {"A", "C", "B"} ;
char *nomehasteimpar[] = {"A", "B", "C"} ;

/* Apontará para os nomes corretos. */
char **nomehaste ;

/* A operação de encontrar o valor de z se chama
 * `count trailing zeroes', e pode ser escrita
 * como:
 */
int ctz(int k) {
    int z = 0;

    if(k) { /* aqui k é sempre maior que 1 */
        while((k&1) == 0) {k=k>>1; z++;}
    }

    return z ;
}

/* Descreve o movimento */
void movedisco(int k, int from, int to) {
    printf("Mova disco %d de %s para %s\n", ctz(k)+1, nomehaste[from], nomehaste[to]) ;
} 

int main(int argc, char **argv) {
    int n = 3 ;
    int from, to, k ;

    if ( argc > 1 ) n = atoi(argv[1]) ;

    /* Para n ímpar */
    if ( n & 1 ) nomehaste = nomehasteimpar ;
    else nomehaste = nomehastepar ; /* para n par */

    for(k=1; k < (1 << n); k++) {
        /* obtém haste origem */
        from = (k&(k-1))%3 ;

        /* obtém haste destino */
        to   = ((k|(k-1))+1)%3 ;

        /* descreve o movimento */
        movedisco(k, from, to) ;
    }

    return 0;
}

Conclusão

Torre de Hanói é um problema clássico na computação cuja solução mais comum envolve o uso da recursão. Observando os padrões criados pelas movimentações dos discos, foi possível elaborar um conjunto de premissas que nos levaram a uma solução iterativa, um pouco que misteriosa, pois tudo que precisávamos (disco movido e descrição do movimento) estava “escondido” nos bits do identificador do movimento. Podemos provar que a solução iterativa de fato é uma alternativa viável como solução, e no fim do post fomos capazes de criar um código para essa solução iterativa.

Referências

[1] Tower of Hanoi, by Wikipedia (Acessado em: Maio/2012)
http://en.wikipedia.org/wiki/Tower_of_Hanoi

[2] Tower of Hanoi, by Cut The Knot (Acessado em: Maio/2012)
http://www.cut-the-knot.org/recurrence/hanoi.shtml

[3] Tower Of Hanoi & Reve Puzzles, by William McCann (Acessado em: Maio/2012)
http://cs.nyu.edu/courses/summer07/G22.2340-001/Presentations/McCann.pdf

[4] Tower of Hanoi, by Wolfram MathWorld (Acessado em: Maio/2012)
http://mathworld.wolfram.com/TowerofHanoi.html

[5] The “mysterious” TH algorithm explained, by M. Kolar (Acessado em: Maio/2012)
http://hanoitower.mkolar.org/moves.html

[6] How does this work? Weird Towers of Hanoi Solution, by stackoverflow (Acessado em: Maio/2012)
http://stackoverflow.com/questions/2209860/how-does-this-work-weird-towers-of-hanoi-solution

[7] The shortest and “mysterious” TH algorithm, by M. Kolar (Acessado em: Maio/2012)
http://hanoitower.mkolar.org/shortestTHalgo.html

Codificação Base64 Em C

Introdução

A codificação base64 é muito importante no transporte de dados binários em um meio de transmissão que trabalha somente com caracteres texto. Um exemplo disso é o padrão MIME que usa essa codificação para transferir arquivos por e-mail usando o protocolo SMTP.

Nesse post iremos aprender mais sobre essa codificação e como fazer tanto um codificador quanto um decodificador usando a linguagem C.

Uso da codificação base64

Essa codificação surgiu da necessidade de transferir e armazenar dados na forma textual, evitando que dados binários sejam manipulados diretamente. Alguns protocolos de transmissão operam somente em cima de dados textuais e quando é preciso algum dado binário ser transmitido, a codificação base64 fornece um jeito simples de transformar essa cadeia binária em texto.

O alfabeto da codificação base64

Para codificar em base64 precisamos primeiramente de um alfabeto. Esse alfabeto contém todos os símbolos que serão usados na codificação. A RFC 3548 [1] define esse alfabeto como:

 Table 1: The Base 64 Alphabet

      Value Encoding  Value Encoding  Value Encoding  Value Encoding
          0 A            17 R            34 i            51 z
          1 B            18 S            35 j            52 0
          2 C            19 T            36 k            53 1
          3 D            20 U            37 l            54 2
          4 E            21 V            38 m            55 3
          5 F            22 W            39 n            56 4
          6 G            23 X            40 o            57 5
          7 H            24 Y            41 p            58 6
          8 I            25 Z            42 q            59 7
          9 J            26 a            43 r            60 8
         10 K            27 b            44 s            61 9
         11 L            28 c            45 t            62 +
         12 M            29 d            46 u            63 /
         13 N            30 e            47 v
         14 O            31 f            48 w         (pad) =
         15 P            32 g            49 x
         16 Q            33 h            50 y

Como podemos ver, o alfabeto contém 65 caracteres compatíveis com quase todo tipo de codificação existente (UTF-8, ISO-8859-1, etc). O normal é dizer 64 caracteres, mas em algumas implementações, o caractere ‘=’ (igual) é usado como padding (preenchimento).

Como codificar em base64

O processo de codificação consiste em ler grupos de três bytes da fonte e gerar quatro bytes com 6 bits ativos. Os 24 bits (3 bytes) lidos são agrupados em sequência e para cada conjunto de 6 bits, obtemos um número que deve ser mapeado no alfabeto para gerar um caractere codificado.

Como exemplo vamos codificar a string “ceu”:

+------------+-------------------+-----------------+
|Caracteres  | Código ASCII(10)  | Código ASCII(2) |
+------------+-------------------+-----------------+
|     c      |        99         |     01100011    |
+------------+-------------------+-----------------+
|     e      |        101        |     01100101    |
+------------+-------------------+-----------------+
|     u      |        117        |     01110101    |
+------------+-------------------+-----------------+

Formando uma única cadeia binária com os 24 bits, temos:

011000110110010101110101

Separando em grupos de 6 bits e já mapeando os números no alfabeto base64:

+-------------+--------------+--------------------+
| 6 bits (2)  | 6 bits (10)  | Elemento na tabela |
+-------------+--------------+--------------------+
|   011000    |      24      |          Y         |
+-------------+--------------+--------------------+
|   110110    |      54      |          2         |
+-------------+--------------+--------------------+
|   010101    |      21      |          V         |
+-------------+--------------+--------------------+
|   110101    |      53      |          1         |
+-------------+--------------+--------------------+

Portanto a string “ceu” é codificada como “Y2V1” em base64.

Operações de baixo nível

Para obtermos os “bytes com 6 bits” da cadeia de 24 bits precisaremos usar as operações de baixo nível da linguagem C, como shift’s e máscaras. Vamos deduzir quais operações devemos usar observando a codificação passo a passo da string “ceu”.

Processo de Codificação

b1, b2 e b3 são respectivamente ‘c’, ‘e’ e ‘u’ em binário.

+----------+-----------+----------+
|   b1     |    b2     |    b3    |
+----------+-----------+----------+
|01100011  | 01100101  | 01110101 |
+----------+-----------+----------+

Iremos ver como encontrar os bytes da codificação: c1, c2, c3 e c4.

1) Primeiro byte codificado

Para gerar o primeiro byte codificado precisamos somente dos 6 bits mais significativos de b1:

01100011 & 11111100

Em C:

c1 = b1 & 0xFC

2) Segundo byte codificado

O segundo byte é a junção dos dois últimos bits de b1 mais os quatros bits mais significativos
de b2:

(01100011 & 00000011) << 4 | (01100101 & 11110000) >> 4

Em C:

c2 = ( b1 & 0x03) << 4 | ( b2 & 0xF0 ) >> 4

Os dois bits de b1 devem ir para a posição 6 e 5 enquanto os 4 bits mais significativos de b2 devem ir para as posições menos significativas do byte codificado.

3) Terceiro byte codificado

Esse byte é formado pelos 4 bits menos significativos de b2 mais os 2 bits mais significativos de b3:

(01100101 & 00001111) << 2 | (01110101 & 11000000) >> 6

Em C:

c3 = ( b2 & 0x0F ) << 2 | ( b3 & 0xC0 ) >> 6

O deslocamento para esquerda é para colocar os bits de b2 nas posições mais significativas. O deslocamento para a direita é para colocar os bits 8 e 7 de b3 nas posições 2 e 1.

4) Quarto byte codificado

Esse byte nada mais é que os seis bits menos significativos de b3:

(01110101 & 00111111)

Em C:

c4 = ( b3 & 0x3F )

Encontrados os 4 bytes desejados, basta agora imprimir o caractere indexado por cada um deles.

Problemas encontrados na codificação

Se o tamanho total da entrada não for múltiplo de 3 então alguns casos especiais surgem. Por exemplo, se a entrada de dados tem tamanho menor que 24 bits, então devemos completar com zeros para que sempre tenhamos 24 bits. Um outro caso diz respeito aos últimos bytes da entrada. Nessa situação temos três casos possíveis:

  1. Se há 3 bytes para serem lidos. Nesse caso, temos os 24 bits completos e por isso não precisamos usar o preenchimento.
  2. Se há 2 bytes para serem lidos. Esses 16 bits irão gerar 3 caracteres codificados e o quarto caractere será o ‘=’.
  3. Se há 1 byte para ser lido. Esses byte é usado para gerar 2 caracteres codificados e depois acrescentamos dois ‘=’s.

O motivo de usarmos o preenchimento é para que a saída seja sempre múltipla de 4. Isso não é obrigatório, mas facilita muito o processo de decodificação de vários arquivos concatenados.

Mais à frente veremos uma versão de decodificação que não precisa de preenchimento.

Código em C para codificar em base64

O código abaixo está simples e foi feito somente para fins didáticos. Ele aceita dados na entrada padrão e gera a codificação base64 correspondente na saída padrão. Instruções sobre compilação e de uso estão no código fonte.

/*
 * [bb64e.c]
 * Codificador simples de base64.
 *
 * [Autor]
 * Marcos Paulo Ferreira (Daemonio)
 * undefinido gmail com
 * daemoniolabs.wordpress.com
 *
 * [Como Compilar]
 * $ gcc -o bb64e bb64e.c
 *
 * [Como Usar]
 * Esse programa so le a entrada padrao,
 * portanto os arquivos devem ser passados por
 * pipes ou por redirecionamento.
 *
 * = Alguns usos =
 * $ ./bb64e < /etc/passwd
 * $ echo -n 'ceu' | ./bb64e
 *
 * Seg Set  5 18:23:47 BRT 2011
 * Sex Set  9 17:22:54 BRT 2011
 */
#include <stdio.h>
#include <stdint.h> /* for uint8_t */

/* Alfabeto. */
const char b64all[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                      "abcdefghijklmnopqrstuvwxyz"
                      "0123456789"
                      "+/" ;

int main(int argc, char **argv) {
    int n, count ;
    uint8_t b[3], c[4] ;

    /* zera o contador de caracteres
     * da linha. */
    count = 0;

    /* zera o vetor de leitura. */
    b[0]=0 ; b[1]=0 ; b[2]=0 ;

    while( (n=fread(b, 1, 3, stdin)) > 0 ) {
        /* primeiro byte da codificacao. */
        c[0] = b64all[((b[0] & 0xFC) >> 2)] ;
        /* segundo byte da codificacao. */
        c[1] = b64all[(b[0] & 0x03) << 4 | (b[1] & 0xF0) >> 4];
        /* terceiro byte da codificacao. */
        c[2] = b64all[(b[1] & 0x0F) << 2 | (b[2] & 0xC0) >> 6];
        /* quarto byte da codificacao. */
        c[3] = b64all[b[2] & 0x3F] ;

        /* Mostra os dois primeiros bytes. Eles
         * existirao independentemente do valor de n.*/
        putchar(c[0]) ;
        putchar(c[1]) ;

        /* Padding para n < 3. */
        if ( n == 1 ) {
            putchar('=') ;
            putchar('=') ;
        } else if ( n == 2 ) {
            putchar(c[2]) ;
            putchar('=') ;
        } else {
            /* sem padding. */
            putchar(c[2]) ;
            putchar(c[3]) ;
        }

        /* linhas com 76 caracteres no total. */
        if ( count == 18 ) {
            count = 0 ;
            putchar('\n') ;
        } else {
            count++ ;
        }
        /* zerando */
        b[0]=0 ;
        b[1]=0 ;
        b[2]=0 ;
    }
    if ( count ) putchar('\n') ;

    return 0;
}

Baixe o código em:

https://daemoniolabs.wordpress.com/wp-content/uploads/2011/09/bb64e-c.docx

Vamos testar o código:

$ gcc -o bb64e bb64.c
$ cat /etc/passwd | ./bb64e | head
cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApiaW46eDoxOjE6YmluOi9iaW46L3NiaW4v
bm9sb2dpbgpkYWVtb246eDoyOjI6ZGFlbW9uOi9zYmluOi9zYmluL25vbG9naW4KYWRtOng6Mzo0
OmFkbTovdmFyL2FkbTovc2Jpbi9ub2xvZ2luCmxwOng6NDo3OmxwOi92YXIvc3Bvb2wvbHBkOi9z
YmluL25vbG9naW4Kc3luYzp4OjU6MDpzeW5jOi9zYmluOi9iaW4vc3luYwpzaHV0ZG93bjp4OjY6
MDpzaHV0ZG93bjovc2Jpbjovc2Jpbi9zaHV0ZG93bgpoYWx0Ong6NzowOmhhbHQ6L3NiaW46L3Ni
aW4vaGFsdAptYWlsOng6ODoxMjptYWlsOi92YXIvc3Bvb2wvbWFpbDovc2Jpbi9ub2xvZ2luCnV1
Y3A6eDoxMDoxNDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovc2Jpbi9ub2xvZ2luCm9wZXJhdG9yOng6
MTE6MDpvcGVyYXRvcjovcm9vdDovc2Jpbi9ub2xvZ2luCmdhbWVzOng6MTI6MTAwOmdhbWVzOi91
c3IvZ2FtZXM6L3NiaW4vbm9sb2dpbgpnb3BoZXI6eDoxMzozMDpnb3BoZXI6L3Zhci9nb3BoZXI6
L3NiaW4vbm9sb2dpbgpmdHA6eDoxNDo1MDpGVFAgVXNlcjovdmFyL2Z0cDovc2Jpbi9ub2xvZ2lu

# Vamos usar o decodificador padrão do linux (programa base64) para
# verificarmos se a codificação foi correta:

$ cat /etc/passwd | ./bb64e | base64 -d | head
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin

Decodificação em base64

A decodificação consiste em obter os 3 bytes originais através dos 4 bytes codificados. Esse processo é simples e pode ser feito somente por deslocamentos e operações OR. Se o caractere de preenchimento não foi usado, então temos que obter os bytes originais usando menos de 4 bytes codificados. Será esse processo que veremos aqui.

+--------+---------+---------+--------+
|  c1    |   c2    |   c3    |   c4   |
+--------+---------+---------+--------+
|011000  | 110110  | 010101  | 110101 |
+--------+---------+---------+--------+

Veremos como obter os bytes originais: b1, b2 e b3.

1) Primeiro byte original

O b1 é formado pelos 6 bits de c1 mais os 2 bits mais significativos de c2. Deslocando c2 para à esquerda duas vezes, c2 4 vezes para à direita e fazendo um OR entre os dois resultados, encontramos b1.

Em C:

b1 = (c1 << 2) | (c2 >> 4)

2) Segundo byte original

b2 é formado pelos 4 bits menos significativos de c2 mais os 4 mais significativos de c3. Se deslocarmos c2 4 vezes para à esquerda, c3 2 vezes para à direita e fazermos um OR entre os dois resultados, obtemos b2.

Em C:

b2 = (c2 << 4) | (c3 >> 2)

3) Terceiro byte original

O último byte é obtido com os 2 bits menos significativos de c3 mais o c4, totalizando os 8 bits.

Em C:

b3 = (c3 << 6) | c4

Código em C para decodificar em base64

Eu poderia escrever um código aqui, mas acabei achando um ótimo na referência[2]. O código está simples e realiza tudo o que foi explicado anteriormente.

/* Copyright (c) 2011 the authors listed at the following URL, and/or
the authors of referenced articles or incorporated external code:
http://en.literateprograms.org/Base64_(C)?action=history&offset=20070707014726

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Retrieved from: http://en.literateprograms.org/Base64_(C)?oldid=10650
*/

#include <stdio.h>

#include <string.h>

char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

int nbytes[] = { 3, 1, 1, 2 };

void xlate(unsigned char *in, int phase)
{
    unsigned char out[3];
    out[0] = in[0] << 2 | in[1] >> 4;
    out[1] = in[1] << 4 | in[2] >> 2;
    out[2] = in[2] << 6 | in[3] >> 0;
    fwrite(out, nbytes[phase], 1, stdout);

}

int main()
{
    int c, phase;
    unsigned char in[4];
    char *p;

    phase = 0;
    while((c = getchar()) != EOF)    {
        if(c == '=')    {
	    xlate(in,phase);
	    break;
	}
	p = strchr(b64, c);
	if(p)    {
	    in[phase] = p - b64;
	    phase = (phase + 1) % 4;
	    if(phase == 0)    {
	        xlate(in,phase);
	        in[0]=in[1]=in[2]=in[3]=0;
	    }
	}
    }
    return 0;
}

Baixe o código em:

http://en.literateprograms.org/Special:Downloadcode/Base64_%28C%29

Testando (salve o código como bb64d.c):

$ gcc -o bb64d base64.c

# Mais uma vez utilizando o base64 nativo do linux:

$ base64 /etc/passwd | ./bb64d | head
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin

Conclusão

O algoritmo base64 é ideal para transformar dados binários em texto. Seu uso é bastante explorado nas trocas de arquivos, especialmente entre e-mails.

Referências

[1] The Base16, Base32, and Base64 Data Encodings, RFC 3548 (Acessado em: Setembro/2011)
http://tools.ietf.org/html/rfc3548

[2]Base64 (Acessado em: Setembro/2011)
http://en.literateprograms.org/Base64_%28C%29

[3] Base64 (Acessado em: Setembro/2011)
http://en.wikipedia.org/wiki/Base64