dcalc: Calculadora Simples Com Conversão De Bases

Introdução

Algum tempo atrás escrevi um post falando sobre coprocessos [1], mas por falta de tempo não tinha fornecido nenhum exemplo elaborado sobre o assunto. Então, hoje, estarei postando um calculadora usando os conceitos de coprocessos. Essa calculadora é bem parecida com a pcalc [4] que basicamente realiza operações aritméticas com conversão de base implícita.

dcalc

A calculadora se chama dcalc e foi baseada na pcalc. A dcalc está escrita em shell script e utiliza o bc como coprocesso para fazer o trabalho árduo, ou seja, o cálculo do valor das expressões . No mais esse script é apenas um “front-end” para o bc, sendo que seu trabalho é manipular uma expressão de entrada e fornecer para o bc a expressão modificada.

O código

Antes de tudo irei postar o código e a seguir apresentarei os exemplos.

#!/bin/bash
# [dcalc]
# Calculadora com conversão de bases embutida.
#
# [Autor]
# Marcos Paulo Ferreira (Daemonio)
# undefinido gmail com
# https://daemoniolabs.wordpress.com
# -* UTF-8 *-
#
# [Uso]
# $ chmod +x dcalc
# $ ./dcalc '20 * ( (0xBFFFFA - 0xBFFFFB) / 5 ) + 0y1001011 - 0o1'
# 70(10) : 70
# 70(2)  : 0y1000110
# 70(8)  : 0o106
# 70(16) : 0x46
#
# + Versão 1.0, by daemonio @ Sun May 20 12:37:36 BRT 2012
#
# + Versão 1.1, by daemonio @ Wed May 23 23:31:53 BRT 2012
#  - Retirada da função recparser. Agora o parse é
#    iterativo.
#  - Opção -c
#  - Melhorada no código e nos comentários.
#

#
# Variáveis do script
#

# Versão do script.
VERSAO='1.1'

# Número de digitos após casa decimal
SCALE=3

# Valor '1' para mostrar variáveis
# internas do script.
DEBUG=0

# Formato limpo, sem formatação da saída (mostrar a saída
# somente na base 10). O padrão é 0 (com formatação).
# A opção -c altera o valor dessa flag para 1.
CLEANFORMAT=0

# Correção de grau para radiano. Esse
# valor corresponde a pi/180.
PI180='.0174532925'

# Expressão modificada depois que todos os
# valores em outra bases foram convertidos.
NOVAEXPRESSAO=

# Recebe resultado final do bc.
RESPBC= 

#
# Funções
#

# Ajuda
function show_help {
echo 'dcalc '$VERSAO' - by daemonio'
echo '[uso] dcalc [-c] <expressao>'
echo '-c  :  Saida limpa, sem formatacao (base 10).'
echo 'OBS:'
echo '    0y = denota numero na base binaria.'
echo '    0x = denota numero na base hexadecimal.'
echo '    0o = denota numero na base octal.'
echo '    sen/cos = recebem parametro em graus.'
echo '    sqrt() = tira raiz quadrada.'

exit 1
}

# Debug: Mostra a mensagem do parâmetro somente
# se a variável DEBUG foi definida como '1'.
function debug {
[ "$DEBUG" = '1' ] && echo "$*"
}

# Transforma base 10 para a base N.
# A base N vem no segundo parâmetro, e o
# número em si vem no primeiro.
function base10tobaseN {
local num=$1
local base=$2
local format=

if [ "$base" = 2 ]
then
    # Valor negativo em complemento de 2.
    # Aqui transformamos para unsigned
    # com a ajuda de printf.
    num=$(printf '%u\n' "$num")
    echo "obase=2; $num; obase=A" >&4
    read -u 5; echo $REPLY
else
    [ "$base" = 8 ] && format='%o\n'
    [ "$base" = 16 ] && format='%X\n'

    printf "$format" "$num"
fi
}

# Função de parser. Essa função converte os números em outras
# bases para a base dez (transforma 0y111 em 7, por exemplo).
# Ela também modifica as funções trigonométricas, seno e cosseno,
# adicionando o fator de correção grau-radiano.
function parsentrada {
local t=
local num=
# O '#' indica que temos um número em outra base
# na entrada.
local exp=$(echo "scale=$SCALE; $1" | sed 's/\b0/#&/g')

# Troca sen() por s() e cos() por c() e ainda
# acrescenta o fator de correção grau-radiano.
if [[ "$exp" =~ [sc] ]]
then
    exp=$(echo "$exp" | sed "s/sen *(/s($PI180 *  /g")
    exp=$(echo "$exp" | sed "s/cos *(/c($PI180 * /g")
fi

# Não há o que substituir, então simplesmente
# retorna a expressão.
if ! [[ "$exp" =~ '#' ]]
then
    echo "$exp"
    return
fi

# Daqui em diante temos números em outras bases. Com
# isso devemos substitui-los pelos seus correspondentes
# na base 10.
while [ "$exp" ]
do
    if [ "${exp:0:1}" = '#' ]
    then
        # Recebe o caracter após o
        # '#'. Pode ser, y, x ou o.
        t=${exp:2:1} ;

        # Transforma base 2 em base 10
        if [ "$t" = 'y' ]
        then
            echo "${exp:3}" | sed "s/\(^[0-1]*\).*/ibase=2; \1; ibase=A/" >&4
            read -u 5; echo $REPLY
        # Transforma base 16 em base 10
        elif [ "$t" = 'x' ]
        then
            num=$(echo "${exp:3}" | sed 's/\(^[0-9a-fA-F]*\).*/\1/; y/abcdef/ABCDEF/')
            printf '%u\n' 0x${num}
        # Transforma base 8 em base 10
        elif [ "$t" = 'o' ]
        then
            num=$(echo "${exp:3}" | sed 's/\(^[0-8]*\).*/\1/')
            printf '%u\n' 0${num}
        # Número na base 10 que começa com zero.
        else
            num=$(echo "${exp:1}" | sed 's/\(^[0-9]*\).*/\1/')
            echo $num
        fi
        # Atualiza a expressão, retirando o número em outra base.
        exp=$(echo "$exp" | sed 's/^[#xyo0-9a-fA-F]*//')
    fi

    # Coloca na saída tudo antes do primeiro '#'.
    echo "$exp" | sed 's/^\([^#]*\).*/\1/'

    # Atualiza a expressão. Agora ela aponta
    # para o próximo '#', ou seja, o próximo
    # número a ser convertido.
    exp=$(echo "$exp" | sed 's/^.[^#]*//')
done
}

# Formata a saída para algumas bases.
function formatarsaidabases {
echo -n "$1(10) : "; echo "$1" ;
echo -n "$1(2)  : 0y"; base10tobaseN $1 2
echo -n "$1(8)  : 0o"; base10tobaseN $1 8
echo -n "$1(16) : 0x"; base10tobaseN $1 16
}

#
# MAIN
#

# Testa se passou opção -c
[ "$1" = "-c" ] && CLEANFORMAT=1 && shift

# Parâmetros insuficientes.
[ -z "$1" ] && show_help

# Cria coprocesso.
# Veja que:
#   Descritor 4: Entrada do bc.
#   Descritor 5: Saída   do bc.
coproc bc -l
exec 4>&${COPROC[1]}
exec 5<&${COPROC[0]}

# Debug: Expressão vinda da linha de comando
debug "[+] Expressao linha de comando: $*"

# Obtém a nova expressão já com os valores em
# outras bases convertidos para a base 10.
NOVAEXPRESSAO=$(parsentrada "$*" | tr -d '\n'; echo)

# Debug: Mostra a saída do parser (entrada do bc).
debug "[+] Entrada bc: $NOVAEXPRESSAO"

# Envia a expressão completa para o bc.
echo "$NOVAEXPRESSAO" >&4 ; read -u 5 RESPBC

# Debug: Mostra a saída do bc (resultado total).
debug "[+] Saida bc: $RESPBC"

# Formata a saída somente se temos um número inteiro.
if [[ "$RESPBC" =~ \..*[1-9] ]]
then
    # Saída para float.
    echo "$RESPBC"
elif [ "$CLEANFORMAT" = "0" ]
then
    # Saída para inteiro formatado.
    RESPBC="$(echo "$RESPBC" | sed '/\..*/s///')"
    formatarsaidabases "$RESPBC"
else
    # Saída para inteiro sem formato.
    RESPBC="$(echo "$RESPBC" | sed '/\..*/s///')"
    echo "$RESPBC"
fi

#EOF

Exemplos de Uso

A conversão implícita de base é feita através de prefixos. Esses prefixos são:

+---------+-------------+
|Prefixo  |     Base    |
+---------+-------------+
|   0x    | Hexadecimal |
+---------+-------------+
|   0y    |   Binária   |
+---------+-------------+
|   0o    |    Octal    |
+---------+-------------+

Sabendo disso agora fica fácil utilizar essa calculadora.

$ chmod +x dcalc
$ ./dcalc '20 * (0y1101101)'
2180(10) : 2180
2180(2)  : 0y100010000100
2180(8)  : 0o4204
2180(16) : 0x884

O número 0y1101101 nada mais é que o 109 na base 2. Assim o resultado será 20*109 = 2180 na base 10. Veja que, quando o resultado é inteiro, a calculadora mostra o resultado nas 4 bases mais usuais: decimal, octal, binária e hexadecimal.

$ ./dcalc '-17'
-17(10) : -17
-17(2)  : 0y1111111111111111111111111111111111111111111111111111111111101111
-17(8)  : 0o1777777777777777777757
-17(16) : 0xFFFFFFFFFFFFFFEF
$ ./dcalc '-20 - 0y1010'
-30(10) : -30
-30(2)  : 0y1111111111111111111111111111111111111111111111111111111111100010
-30(8)  : 0o1777777777777777777742
-30(16) : 0xFFFFFFFFFFFFFFE2

Para números negativos, a saída já sai em complemento de dois (o mesmo não ocorre para o bc/dc).

Podemos também usar expressões mais complexas:

$ ./dcalc '(0xFFAA + 0y111110)*(0o5557 + 0y1100) - 0xBABA'
192491966(10) : 192491966
192491966(2)  : 0y1011011110010011000110111110
192491966(8)  : 0o1336230676
192491966(16) : 0xB7931BE

dcalc é muito útil quando estamos debugando algum programa e necessitamos calcular offsets de memória. Imagine um buffer que se inicia no endereço 0xbfffffa4 e tem tamanho 64 bytes. Então o fim desse buffer está no endereço:

$ ./dcalc '0xBFFFFFA4 - 64'
3221225316(10) : 3221225316
3221225316(2)  : 0y10111111111111111111111101100100
3221225316(8)  : 0o27777777544
3221225316(16) : 0xBFFFFF64

O endereço final é dado nas 4 bases, mas o mais útil é na base hex, cujo valor deu: 0xBFFFFF64.

Algumas funções matemáticas também estão implementadas, graças ao bc. São elas:

+---------+---------------------------------+
| Função  |            Descrição            |
+---------+---------------------------------+
| sen(x)  |   Calcula seno de x (em graus)  |
+---------+---------------------------------+
| cos(x)  | Calcula cosseno de x (em graus) |
+---------+---------------------------------+
|sqrt(x)  |    Calcula raiz quadrada de x   |
+---------+---------------------------------+
|  e(x)   |        Função exponencial       |
+---------+---------------------------------+

Exemplos usando essas funções matemáticas:

$ ./dcalc 'sen(30)'
.500

Veja que o parâmetro é dado em graus, o oposto do bc, cujo parâmetro deve ser em radianos.

$ ./dcalc '(sen(0x1E)/cos(0x1E)) * 1/sen(0y11110)'
1.154

Esse exemplo calculou a secante de 30, pois 0x1E = 0y11110 = 30.

$ ./dcalc '(5 + sqrt(1))/2 + (5 - sqrt(1))/2'
5(10) : 5
5(2)  : 0y101
5(8)  : 0o5
5(16) : 0x5

O exemplo acima calculou a soma das raízes da equação de segundo grau: x^2 – 5x + 6 [3].

Detalhes sobre o código

1) Coprocessos

Logo no início do script é declarado um coprocesso usando o bc. Isso significa que o bc estará “conectado” ao script e poderá receber dados pelo descritor 4 e retornar dados em 5 (veja mais sobre exec em [2]).

Basicamente onde temos >&4 e read -u 5 é quando estamos interagindo com o coprocesso. Operações de conversões e aritméticas são feitas usando esse coprocesso.

2) Parser

A função parsentrada recebe a expressão de entrada e substitui os valores 0[xyo][0-9] por seu correspondente na base 10. Os números da forma 0[xyo][0-9] são substituídos por #[xyo][0-9], sendo o caractere especial ‘#’ usado como indicador. Dentro do loop while dessa função, há um conjunto de if’s que testam a base do número em questão, e após conversão desse número na base 10, o resultado é mostrado na penúltima linha da função.

Para entender como essa função foi elaborada, veja como ela manipula a seguinte entrada:

$ ./dcalc '20 * (0y1010 + 0xFACA) - 50'

parsentrada
         exp = 20 * (#0y1010 + #0xFACA) - 50 )
              |_____|
             saída = 20 * (
         exp = #0y1010 + #0xFACA) - 50 )
              |______|
             if $t = 'y' --> saída = 10
         exp = + #0xFACA) - 50 )
              |_|
             saída =  + 
         exp = #0xFACA) - 50 )
               |_____|
            if $t = 'x' --> saída = 64202
         exp = ) - 50)
               |_____|
             saída = ) - 50

Ao final, a saída completa será:
20 * (
10
 + 
64202
 ) - 50

A variável NOVAEXPRESSAO recebe a saída acima só que executando um tr para retirar os ‘\n’ desnecessários. No final teremos:

NOVAEXPRESSAO="20 * ( 10 + 64202 ) - 50"

O valor de NOVAEXPRESSAO é passado para o coprocesso bc, e o resultado é:

echo "$NOVAEXPRESSAO" >&4; read -u 5 RESPC
RESPC=1284190 # Resultado final

Se o resultado for inteiro e a opção -c não foi passada, a saída será formatada, ou seja, o resultado será mostrado nas 4 bases. Se o resultado tem casas decimais ou a opção -c foi usada, a saída será sem formatação.

Setando a variável DEBUG com valor 1, podemos ver a expressão final e o resultado dela:

$ ./dcalc -c '20 * (0y1010 + 0xFACA) - 50'
[+] Expressao linha de comando: 20 * (0y1010 + 0xFACA) - 50 # expressão de entrada
[+] Entrada bc: scale=3; 20 * (10 + 64202) - 50             # expressão final
[+] Saida bc: 1284190                                       # resultado da expressão
1284190

3) Conversão com printf

As conversões entre as bases 10, 16 e 8 é realizada usando printf, um comando built-in do bash. O problema do printf é que as operações são limitadas em 64 bits, isso é, qualquer número a ser convertido  maior que 2^64, resulta num erro de overflow.

4) TODO

Se você achou esse script útil, fique livre em usá-lo e modificá-lo. Qualquer alteração no código que você julgue útil, poste aqui nos comentários.

Referências

[1] Entendendo Coprocessos (coproc) Do Bash, by Daemonio (Acessado em: Maio/2012)
https://daemoniolabs.wordpress.com/2011/07/19/entendendo-coprocessos-coproc-do-bash/

[2] Manipulando Arquivos Descritores No Shell, by Daemonio (Acessado em: Maio/2012)
https://daemoniolabs.wordpress.com/2012/02/24/manipulando-arquivos-descritores-no-shell/

[3] Raiz de uma Equação Completa do 2º grau, by Marcos Noé (Acessado em: Maio/2012)
http://www.brasilescola.com/matematica/raiz-uma-equacao-2-grau.htm

[4] pcalc, by vipier (Acessado em: Maio/2012)
http://sourceforge.net/projects/pcalc/

Um pensamento sobre “dcalc: Calculadora Simples Com Conversão De Bases

Deixe um comentário