Problema 49.1. del curso de C(Esto proviene de:
Sección 49 del curso de C)
En todo programa, primero hay una etapa de diseño y planificación.Hay que estudiar bien el problema antes de lanzarse a programar como locos.
Veamos el enunciado:
\( \bullet \)
49.1. Pedir al usuario que ingrese una base \( b \). Luego, pedir al usuario que ingrese números reales (en decimal), y convertirlos a base \( b \) a medida que el usuario los va ingresando. Continuar hasta que el usuario ingrese
0 o un valor fuera del rango que hemos convenido adoptar para el tipo
double.
Aunque no lo parezca, el enunciado contiene algunas imprecisiones. Por ejemplo:
¿La base \( b \) es un número entero positivo?
Lo típico al hablar de cambios de bases es que: sí. Las bases son enteros positivos.
Pero hay teorías matemáticas que discuten bases con otras características.
Si éste fuera el caso, tendríamos que estudiar primero el tema de las bases "en general".
Por otro lado, siempre conviene intentar resolver primero la versión más simple de un programa, y luego atacar versiones más complicadas o sofisticadas.
\( \bullet \) Así que en una 1er versión del programa vamos a considerar sólo bases enteras positivas.
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCAhora, sin embargo, se nos presentan otros problemas.
Dado un número, digamos \( x \), en base \( b \), necesitamos representarlo con tantos dígitos como indica la base, es decir, \( b \). Esto es, un alfabeto de \( b \) símbolos.
Lo usual es que el alfabeto de dígitos contenga los primeros elementos del conjunto de dígitos decimales: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.
Esto sólo es posible cuando la base es diez o menor que diez.
Si la base es 16, se usan las letras A, B, C, D, E, F, como dígitos adicionales.
Podemos entonces usar esos mismos símbolos cuando la base sea mayor que diez y menor o igual que 16.
¿Y si la base es mayor que 16? ¿Se pueden usar las restantes letras del alfabeto?
En principio sí, pero notemos que hay algunos obstáculos.
Por ejemplo, la letra O se confundiría fácilmente con un dígito "cero".
Como la letra O ocupa la posición 15 en el alfabeto (siguiendo la secuencia de caracteres ASCII, o sea, rindiéndonos antes el imperialismo yanki), sólo podríamos llegar sin obstáculos hasta la N, que ocupa la posición 14.
No podríamos usar las letras de la P en adelante, porque el orden de los dígitos no sería ahora claro (de la N saltaríamos a la P, y eso sería confuso, pues la P representaría un valor menor que el que intuitvamente le adjudicaríamos).
Agregando los diez dígitos decimales, tendríamos como máximo unos 24 dígitos disponibles, lo cual nos permitiría representar bases que vayan hasta la 24.
Pero veo otro obstáculo. Es la letra I, que ocupa la posición 9 en el alfabeto. Es muy probable que llegue a confundirse con un dígito 1.
Pensando en eso, de nuevo descartaríamos las letras que siguen a la I, porque habría un salto antiintuitivo en la secuencia, al pasar de la H a la J en la lista de "dígitos".
Así que sólo tendríamos 8 letras disponibles para dígitos: de la A a la H (8 letras), pudiendo representar así bases hasta la 18.
Para bases mayores que 18 habría que decidir otro tipo de diseño.
Hay varias alternativas, pero cualquiera que elijamos, resulta extraño que elijamos un diseño basado en letras del alfabeto para las bases 17 y 18, y que ya para la base 19 adoptemos un diseño totalmente distinto.
Lo que podemos hacer aquí es dar opciones al usuario, para que elija la manera que él prefiera para representar dígitos.
No me convence ese enfoque, porque en la parte teórica no hemos visto herramientas del todo adecuadas para hacer que el programa venga tan "interactivo".
Así que para las bases 11 a 16 usaré el enfoque de utilizar letras del alfabeto para los dígitos adicionales, tomando lo que necesitemos de la secuencia: A, B, C, D, E, F.
Y para las bases mayores que 16 usaré sólo números escritos en decimal, pero de tal modo que los "dígitos" queden claramente separados entre sí.
Por ejemplo, un número en base 34 utilizaría los números 0 al 33 como "digitos", y esto obligaría a que pongamos un separador (por ejemplo, un apóstrofo):
11'7'9'0'33'14'11'11'2'25
El uso del apóstrofo soluciona el problema, y permite ahora salir del paso con cualquier base, porque ya tenemos un método que funciona en general.
Pero ocurre también que poner un apóstrofo por cada dígito hace muy accidentada la lectura del número.
Me parece preferible buscar un método en que no se usen esos apóstrofos.
Hacerlo así, sin más, da resultados ambiguos. El ejemplo anterior quedaría así:
1179033141111225
¿Qué significa ese número en base 34? ¿Comienza con un dígito 11, o con dos dígitos 1?
Se me ocurre aquí una solución bastante sencilla: usar dos dígitos decimales por cada dígito en base 34. Rellenar con 0's o con espacios en blanco cuando el dígito en base 34 tenga un valor menor que 10.
Así, el ejemplo anterior quedaría:
11070900331411110225
Con la convención de que cada par de dígitos decimales representa un dígito en base 34, ahora no hay ambigüedad en la lectura.
Esa seguidilla de cifras debe leerse como la secuencia de dígitos en base 34 siguiente:
11 7 9 0 33 14 11 11 2 25
Si usáramos "espacios" en vez de 0's, sería aún más fácil de leer:
11 7 9 033141111 225
Ahí se entiende que el "trozo" 033141111, que sigue a un "espacio", tiene que leerse como el dígito 0, seguido de los dígitos 33, 14, 11 y 11, ya que la ausencia de "espacios" indica que han de leerse dígitos "de a pares".
Según el contexto, esto puede resultar confuso, porque no sabemos si se trata de un solo número, o varios números separados por espacios en blanco.
Así que en vez de un espacio en blanco pondré allí algún caracter visible, como por ejemplo, un subrayado: _
El ejemplo anterior queda ahora así:
11_7_9_033141111_225
Ahora el subrayado _ sirve de marca para decir: "aquí no se lee un par de dígitos, sino uno solo".
Con la misma idea, podríamos representar dígitos en base 299, pero ahora necesitaríamos 3 dígitos decimales por cada dígito en base 299.
Por ejemplo:
115'8'27'277'66'4'11'0'114
sería un número con dígitos en base 299, separadads por apóstrofos.
Esto de los apóstrofos lo estamos usando, a fin de cuenta, como parte de la discusión del diseño, para entendernos, pero no como parte de los resultados que arrojará nuestro programa. ¿Loco no?
Nuestro programa intentará mostrar ese número con 3 caracteres, ya sea dígitos decimales o el subrayado: _.
115__8_27277_66__4_11__0114
Notemos que los dígitos entre 0 y 9 necesitan un doble subrayado: __,
mientras que los dígitos entre 10 y 99 necesitan un solo subrayado: _.
Los demás (entre 100 y 298), se leen tomando de 3 en 3 cifras decimales.
Por ejemplo, el "trozo" _27277 se lee como la secuencia de 2 dígitos en base 299: 27, 277.
A fin de cuentas, el subrayado _ está jugando el mismo papel que un 0.
Estaría de más, en ese caso.
Pero me parece a mí que un subaryado _ es más fácil de interpretar, al leerlo en pantalla, que un 0.
Contrastemos el efecto visual del mismo número en base 299 anterior, poniendo 0's en donde había subrayados:
115__8_27277_66__4_11__0114
115008027277066004011000114
En general, si la base está entre \( 10^k < b \leq 10^{k+1} \), vamos a usar la convención de utilizar por cada dígito en base \( b \), exactamente \( k \) dígitos en base diez, y reemplazando los 0's con subrayados para mayor legibilidad.
Importante: se exceptúa el caso en que el dígito en base \( b \) es él mismo el 0. Allí sí usaremos el dígito 0 para representarlo.
Este último detalle seguramente nos traerá algún pequeño escollo cuando diseñemos el programa. Por eso lo he remarcado.
Todas las "excepciones" traen dolores de cabeza que luego hay que atender.
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCuando la base es \( b=1 \), el programa debe indicar un mensaje de error, ya que no es posible escribir números en base 1.
Lo mismo si la base es 0 o un número negativo.
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCuando se debe realizar una conversión de una base a otra, suele ser muy complicado lidiar con la parte detrás del punto fraccionario.
Consideremos el siguiente ejemplo:
Es fácil pasar el número 104 (en base diez) a base 7, porque hacemos:
\( 104 / 7 \) ---> 14 con resto 6
\( 14 / 7 \) ---> 2 con resto 0
\( 2 / 7 \) ---> 0 con resto 2
Luego, 104 en base 7 es 206 (tomamos los restos de atrás hacia adelante).
Este enfoque nos obligaría a ir guardando los restos, para mostrarlos al final a todos, en orden inverso.
Pero no es necesario hacerlo así.
Podemos operar "al revés":
\( 104 / 7^2 \) ---> 2 con resto 6
\( 6 / 7^1 \) ---> 0 con resto 6
\( 6 / 7^0 \) ---> 6 con resto 0
Ahora tomamos los "cocientes" sucesivos, en vez de los "restos", y eso nos va "regurgitando" los dígitos en base 7, que nos da:
2 0 6
¿Cómo lidiar con los números que tienen una parte fraccionaria?
Pasar el número 0.104 (en base diez) a base 7.
Aquí se procede de modo algo distinto: se va multiplicando por 7 a fin de obtener los sucesivos dígitos, hasta "terminar", si es que esto es posible, o hasta hallar una repetición periódica.
\( 0.104 \cdot 7 \) ---> 0.728. Se toma la parte entera 0 como dígito en base, y el resto sigue operando:
\( 0.728 \cdot 7 \) ---> 5.096 ---> Se toma 5 como dígito, y "resto" 0.096
\( 0.096 \cdot 7 \) ---> 0.672 ---> Se toma 0 como dígito, y "resto" 0.672
\( 0.672 \cdot 7 \) ---> 4.704 ---> Se toma 4 como dígito, y "resto" 0.704
Esto está destinado a continuar sin fin.
Pero necesitamos que nuestro programa termine en algún lugar.
Deberíamos poder reconocer que hay una repetición a partir de algún momento, indicar que allí hay un período (de dígitos en base 7), y terminar allí el programa.
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCMás aún, tengamos en cuenta que el resultado obtenido no será del todo preciso, ya que en realidad los cálculos anteriores realizan una previa conversión interna del número decimal 0.104 a base 2.
Los cálculos los hace la computadora internamente en base 2, y allí podemos tener pérdidas de precisión.
Como trabajaremos con datos de tipo
double, sabemos que al menos 10 dígitos decimales serán confiables o significativos.
Así que podríamos proseguir nuestros cálculos hasta agotar esos 10 decimales significativos.
O bien, podríamos advertir al usuario que más allá de esos 10 decimales los resultados pueden contener errores, e igual le mostramos lo que nos dé.
En ese caso, tenemos que reconocer en qué momento los dígitos en base 7 comienzan a repetirse, si es que lo hacen.
Esto no resulta sencillo.
Dado que tenemos "apenas" 10 decimales significativos, al multiplicarlos en secuencia por 7 (o por otra base cualquiera) podemos obtener, como mucho, una lista de \( 10^{10} \) "restos" distintos (números entre 0 y 1).
O sea que el proceso terminar en alguna parte. No obstante, esos son demasiados dígitos a mostrar en pantalla.
Dado que la precisión (en base decimal) del tipo
double está indicada por la macro
DBL_DIG, no conviene presentar más dígitos en base \( b \) que esos cuando \( b \) es mayor que 10.
Eso dará una buena idea del número. Si queremos, podemos exhibir un par de dígitos más en base \( b \), como para "espiar" un poco más allá, a ver qué hace la computadora con esto.
Cuando la base \( b \) es menor que 10, debiéramos presentar más dígitos que la cantidad
DBL_DIG. ¿Cuántos?
Bueno, algo así como multiplicar por un factor de \( [\log_{b} 10] + 1 \) (los corchetes indican "parte entera").
Esto para base 2 nos da un factor de 4.
No veo del todo sencilla la parte de "reconocer la parte periódica" del número convertido a base \( b \).
Esto requeriría algún tipo de manipulación de fracciones.
Se puede hacer, pero lo dejaré para una 2da versión del programa.
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCEn general, se ve que tenemos procedimientos que nos permiten ir generando las cifras en base \( b \) de un número, sin necesidad de memorizar los dígitos que vamos obteniendo en alguna parte.
Esto permite resolver el problema con las herramientas teóricas que hemos visto hasta ahora, que son muy pocas.
También muestra que es posible resolver este problema sin el uso de memoria adicional.
(En realidad, estaríamos usando la pantalla del monitor como "memoria", jeje).
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCPrimero conviene escribir el esqueleto del programa, y luego ir rellenando los huecos, que suelen ser de complejidad creciente, y requieren análisis detallado.
En virtud de la previa discusión de diseño, podemos vislumbrar algunas pautas generales del esqueleto.
Como siempre, incluiremos las librerías
<stdio.h> para intercambiar datos con el usuario vía consola estándar, y
<stdlib.h> para cambiar la página de códigos a 28591 a fin de visualizar correctamente los acentos en castellano, mediante:
system("CHCP 28591");
También incluiremos la librería
<stdbool.h>, acorde a los comentarios de la
Sección 48 del curso.
Como no usaremos números complejos, no hará falta incluir esta vez
<complex.h>.
Dado que nombramos la macro
DBL_DIG de la librería
<float.h>, podríamos necesitar incluir esta librería.
De hecho, no lo haré, y esto lo decido aquí caprichosamente.
Hemos dicho en la Sección 48 que sólo asumiremos 10 dígitos decimales significativos.
Así que preferiré usar una constante llamado Significant_Digits, puesta igual a 10, y usaré ésta en vez de DBL_DIG.Esto nos evitará algunos inconvenientes imprevistos. Por ejemplo, podría darse el caso de que tengamos la suerte de tener una súpercomputadora en la que haya 40000 dígitos significativos para el tipo
double. Eso causaría estragos en nuestro inocente programa, que intentaría mostrar siempre esa cantidad de dígitos.
En una futura versión del programa, se podría pedir al usuario que elija la cantidad de dígitos significativos que desea, informándole previamente de la cantidad máxima disponible.
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCEsqueletobc.00.01.c
// BC 00.01
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
int main(void) {
system("CHCP 28591");
int Significant_Digits;
Significant_Digits = 10;
printf("BaseConv (Base Converter) v. 0.01\n\n");
printf("Este programa convierte números x de base decimal a base b.\n\n");
printf("Se le pedirá al usuario que elija una base b para trabajar.\n");
printf("Luego, se le pedirá al usuario que escriba números x en base decimal, para ser convertidos a base b.\n");
printf("Para terminar, ingrese el número x = 0\n\n");
int b; /* Base a la que convertiremos los números decimales */
do {
printf("Elija la base b = ");
scanf("%d", &b);
if (b <= 1)
printf("Error: la base debe ser b >= 2.\nIntente de nuevo.\n\n");
} while(b <= 1);
/* Desde aquí ya estamos seguros que b >= 2 */
double x; /* Esta variable contendrá el número que deseamos expresar en base b */
do {
printf("Escriba un número decimal x = ");
scanf("%lg", &x);
// Convertir x a base b, e ir mostrando los dígitos de la conversión
} while(x != 0.0);
printf("Con la elección x = 0 este programa termina.\n");
printf("Fin del programa.\n");
system("PAUSE");
return 0;
}
Arriba de todo: una línea de comentario con el nombre y versión del programa. Esto es información para el programador (por ahora, nosotros mismos solamente).
Tras la inclusión de las librerías estándar, entramos directo en la función
main(), que es en donde se desarrollará nuestro programa.
La primer sentencia cambia la página de códigos en la línea de comandos de Windows, de modo que se visualicen correctamente los acentos y eñes del castellano.
Quienes usen Linux u otro sistema, pueden quitar esa línea, o cambiarla por alguna otra cosa conveniente para su sistema particular.
Al final del programa tenemos otra llamada al sistema, sólo para hacer una pausa en la ejecución del programa, y evitar que nos desaparezca la ventana de línea de comandos antes de ver los últimos mensajes del programa.
Esta línea también puede omitirse o sustituirse por algún otro procedimiento.
Importante: Observen que sólo he hecho uso de la función
system() al principio y al final del programa, para que quede claro que sólo en esos puntos de ejecución estoy haciendo uso de llamadas al sistema. Más aún, quitando esas dos líneas, se puede omitir la inclusión de la librería
<stdlib.h>.
Al principio se declara un objeto que quedará constante a lo largo del programa: la variable entera
Significant_Digits, que ponemos igual a 10, de una vez y para siempre. Esto respeta nuestra convención hecha en la Sección 48 de la teoría, en que los dígitos significativos de valores
double serán, para nosotros, de 10.
Decisiones de diseño para casos más generales, vendrán más adelante en el curso.
A continuación viene una seguidilla de instrucciones
printf() que muestran mensajes al usuario:
- Nombre y versión del programa.
Las versiones con números menores que 1.00 se usan para programas aún no completados.- Propósito general del programa.
- Explicación del funcionamiento del programa, junto con algunas indicaciones al usuario, y resultados esperados.
- Alguna indicación o explicación de cuándo y cómo se termina el programa.
Después de esas preliminares, se declara un objeto
b de tipo
int, en el que guardaremos la información de la base de numeración a la que haremos las conversiones.
Luego se pide al usuario que elija una base b.
Se le informa con un mensaje mediante
printf() qué es el dato que debe ingresar.
Se usa
scanf() para "capturar" el dato ingresado por el usuario.
Normalmente el usuario tiene que presionar la tecla
ENTER para terminar el ingreso de datos en forma correcta.
Observemos que hemos usado una estructura
do while() para el ingreso de este dato.
¿Por qué?
Muy sencillo: para perseguir estilo pesadilla al usuario mientras este ingrese una base que no nos gusta.
Le obligamos a que ingrese un número \( b \geq 2 \), que si no, se repite el pedido de ingresar otra vez un número.
Podemos estar seguros de que al usuario rebelde le ganaremos por cansancio. Tal así funciona
Terminator: con un
do while():
do {
buscar_a_Sarah_Connor_y_atentar_contra_su_vida();
} while(Sarah_Connor_sigue_viva());
Eso sí:
Terminator no falla jamás gracias a que alguien se tomó el trabajo de escribir correctamente los
punto y coma al final de cada instrucción.
Cuando el usuario ingresa un valor de base no deseado, le informamos con un mensaje de error.
Esto requiere que previamente verifiquemos si el valor ha sido erróneo, lo cual nos obliga a utilizar una estructura
if().
Una consecuencia desagradable de esto es que acabamos por "preguntar" dos veces lo mismo: ¿es cierto que \( b\leq 1 \)? Lo preguntamos en el
if() y luego en la línea
while();.
Por ahora, esto lo dejamos así, porque es más claro a la vista.
Hay modos antiestéticos de quitar esa comparación adicional...
Una vez que el usuario ha cesado en su rebeldía, y ha elegido una base \( b\geq 2 \), observemos que podemos confiar en ello y no tenemos necesidad de verificarlo nuevamente.
Así que, desde el punto de vista lógico, es siempre cierto a partir de allí que \( b\geq 2 \), y esto lo pusimos como un comentario.
Es lo que damos en llamar: un
invariante lógico del programa.
Es una condición que sabemos que a partir de ese momento se cumple, hasta que aparezca otro comentario que nos informe de lo contrario.
Trabajaremos siempre asiduamente con comentarios del tipo "invariante lógico".Seguidamente declaramos un objeto
x de tipo
double.
Esta variable irá cambiando de valor muchas veces, pues es en donde guardaremos los números que el usuario nos vaya dando para trabajar.
Lo que sigue es un
while(), tal que en cada iteración se le pide al usuario que ingrese un número decimal (que puede tener parte fraccionaria).
A continuación se lo ha de convertir a base \( b \), pero como esto es complicado, lo postergamos, y ponemos ahí sólo una línea de comentario con la promesa de que lo vamos a resolver luego.
El ciclo se repite hasta que el usuario elige un valor de x igual a 0.0 (ó 0).
Finalmente el programa se despide con algunos mensajes.
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCContinuará...