Mostrar Mensajes

Esta sección te permite ver todos los posts escritos por este usuario. Ten en cuenta que sólo puedes ver los posts escritos en zonas a las que tienes acceso en este momento.

Temas - argentinator

Páginas: [1] 2 3 4 ... 8
2
Queridos usuarios del foro.

A lo mejor hayan tenido problemas de conexión, avisados con un mensaje titulado: "Connection Problems".
Me ha ocurrido largo rato durante el día de hoy.

En estos casos, puede resultar desagradable que, tras escribir un largo mensaje, aparezca ese mensaje de error al querer publicar, y el texto completo se pierda antes de que sea recibido correctamente por el foro.

Para prevenir ese inconveniente, lo que sugiero hacer es lo siguiente:

* Tras escribir todo el texto, pintarlo todo para hacer un "copiar y pegar".
* Presionar la combinación de teclas "Control C", a fin de "copiar" todo el texto (quedando guardado en memoria).
* Si el mensaje no se pudo publicar por algún error del servidor, volver a intentar, bastando ahora presionar las teclas "Control V" para que aparezca el mensaje completo original, y así no tener que tipearlo todo desde cero.


3
En este hilo voy a poner unas dudas y unos comentarios de la parte de Lógica de Primer Orden del libro de Lógica Matemática de Ivorra (que pueden bajar de su página web).

Espero contar con la participación de Carlos. ;)

Lo pondré repartido en varios hilos, porque es un poco largo,
aunque quería dejar plasmadas todas las cuestiones de un solo golpe,
para que se sepa al menos hasta dónde quiero llegar.


4
Quiero compartir una reflexión y ver si alguien complementa o corrige.
Hay pruebas de Teoremas en teoría de grupos que salen por inducción en el orden g del grupo G.
Pero esto implica que para cada natural g estamos haciendo referencia a la clase propia C(g) que contiene todos los grupos G de orden g.
La inducción en ZFC se establece para subconjuntos del conjunto N.
Tales subconjuntos se pueden definir mediante fórmulas, las cuales sólo admiten conjuntos como terminos, y no admiten clases propias.
Luego ese tipo de inducción no puedo expresarla en ZFC.

A simple vista parece que la prueba se sale de ZFC.
Pero creo haber emparchado la situación observando que,
en realidad, estamos hablando sólo de grupos finitos,
y que un grupo de orden n es aubgrupo del grupo de permutaciones Sn.

Si definimos \(A=S_1\times S_2 \times \cdots \), el producto directo infinito de los Sn,
eso da un conjunto que no es clase propia,
y todo grupo involucrado en la prueba de un teorema sobre grupos finitos es isomorfo a un subgrupo de \(A\).
Allí la inducción sobre el orden g de un grupo G se refiere al cardinal g de un subgrupo de \(A\).
Eso más algunos retoques con isomorfismos debiera arreglar la cuestión.

Un ejemplo de tal prueba por inducción es la de que un grupo finito G tiene un p-subgrjpo de Sylow para p primo.

5
Lógica / Causalidad y lógica
« en: 13 Junio, 2015, 07:36 pm »
Me ha tocado estos días tener que reflexionar acerca de la naturaleza del tiempo.
Quisiera entender algunas cuestiones básicas del fluir del tiempo sin necesidad de teorías físicas,
apelando solamente a la razón, y más aún, al razonamiento aún no formalizado,
porque quiero entender qué papel juega el tiempo al hablar de "algoritmos" o "procedimientos" en metamatemática.
O sea, el tiempo a nivel de computación, de programas.
Si yo utilizara la Relatividad de Einstein, estaría entrando en un círculo vicioso, porque esa es una teoría basada en el formalismo matemático, que a su vez se construyó primero con elementos más intuitivos y básicos.

No obstante, puedo aludir sin problemas a hechos experimentales y verificados de la Teoría de la Relatividad,
porque la evidencia es algo tangible, concreto, y de paso es anecdótico apenas, que puede estar enmarcada o no en una teoría.
Por ejemplo, se puede comprobar experimentalmente lo siguiente:

Existen pares de fenómenos E y F y ternas de testigos u, v, w, tales que:

* En su marco de referencia, u percibe que E ocurrió antes que F, en su línea de tiempo propia.
* En su marco de referencia, v percibe que E y F ocurrieron simultáneamente, en su línea de tiempo propia.
* En su marco de referencia, w percibe que F ocurrió antes que E, en su línea de tiempo propia.

Por otra parte, se puede demotrar con las fórmulas de la teoría de la relatividad que si el evento E causa el evento F,
entonces E siempre precede a F en todos los marcos de referencia. Los mismos testigos u, v, w, verían cada uno en su marco de referencia que E sucede antes que F.

---------

Lo que yo me pregunto es si esta "demostración" puede hacerse sin usar las fórmulas de la Teoría de la Relatividad,
usando solamente el razonamiento puro.

Yo enunciaría algo más general como esto (que espero no sea tramposo):

(1) Si para que suceda el evento F es lógicamente necesario que ya haya sucedido el evento E, entonces en todos los marcos de referencia el evento E se percibe antes que el evento F.

Me parece que esto es mera consecuencia de las palabras que estoy usando para enunciar la propiedad,
aunque no sé si me estoy engañando en alguna parte.

O sea, mi "demostración" sería que: las reglas del razonamiento lógico son las mismas para todos los posibles testigos
de los eventos E y F, y que si estoy diciendo que, cualquiera sea el evento F, es lógicamente necesario que haya ocurrido el E,
quiere decir que, en cada marco de referencia, el correspondiente testigo tiene que percibir que E ha sucedido ya, para que tenga sentido o sea posible la ocurrencia de F.

-----------

Una vez que se ha establecido este orden causal entre eventos,
entonces es posible hablar de eventos ordenados en secuencia a lo largo del tiempo,
con independencia del marco de referencia, y así establecer una línea de tiempo que avance del pasado al futuro en forma absoluta.
Esta "línea" de tiempo no necesariamente es para mí un "continuo", ni nada concreto,
sino una "cosa" que tiene la propiedad de que ciertos eventos pueden ponerse en orden allí
O sea que, sólo estoy seguro de que puede extraer una sucesión discretizaada de instantes \( t_n^R \) de allí,
tal que para números enteros es \( m<n \) si y sólo si \( t_m^R<t_n^R \) en cualquier marco de referencia \( R \).

---------

Si esta "demostración" es demasiado chapuza, a lo mejor podría contentarme con la existencia de tales eventos mediante
algún otro argumento, aunque quiero evitar apelar a la Física mientras sea posible.

¿Alguna opinión?


6
Estoy tratando de entender el concepto de RAZONAMIENTO.
Para eso parece que no puedo eludir la cuestión del tiempo.
Un razonamiento es un procedimiento, por eso requiere tiempo.
Pero quiero ser más preciso en esto, y evitar definir el término PROCEDIMIENTO.
Si tengo premisas A y B y una conclusión C, y consideremos un razonamiento, que por brevedad voy a referir como R, el cual infiere que de A y B se deduce C.
Parece lo.más natural del mundo que uno enuncie primero antes de cierto instante t  las premisas A y B, y que en un instante posterior t' enuncie C a fin de llevar a cabo el razonamiento R.

Me pregunto entonces: ¿es esto necesariamente así, es esta una propiedad de todo razonamiento?
¿Qué significa que R lleva de las premisas A y B a la conclusión C? ¿Es un razonamiento una TAREA que debe hacerse en determinado orden a lo largo ddl tiempo?

Sería más claro preguntar si un razonamiento es un algoritmo, pero como se trata de algo INFORMAL no me convence preguntarlo así.


8


Ya me he acostumbrado a considerar que hay una entidad intuitiva, "la" colección de (meta) números naturales, que denotaré N.

Este N es considerado el modelo estándar de las varias teorías formales de números naturales que andan por ahí (axiomas de Peano, Presburger, etc.).

No obstante, cualquier otra colección intuitiva N' de objetos, que permite dar una clara correspondencia entre los objetos 1, 2, 3, ... de N y los objetos 1', 2', 3', etc. de N', tiene que dar "otro" modelo estándar de dichas teorías.
Estos objetos de N' también se pueden sumar, multiplicar, y poner en un orden total,
con tal de contagiar las operaciones y relaciones respectivas ya dadas en N.

Estoy tratando de hablar con cuidado para no salirme del país "meta", y no caer en formalización alguna.

Ahora, la pregunta que tengo es la siguiente:

¿Es necesario que un modelo alternativo/isomorfo de N sea intuitivo?

Si logro generar de algún modo una colección infinita con un lenguaje de programación que luzca como N, ¿se consideraría también un modelo estándar de N?

---------------------------

Más concretamente:

En C es posible definir objetos "semánticamente" hablando, de tamaño arbitrario.
Por ejemplo, un programa que genera una lista enlazada de datos del mismo tipo.

Un tipo de datos T en C involucra dos cosas: un conjunto de ciertos valores (entidades abstractas, mentales, matemáticas), y un modo de representarlos en una máquina real.
Una declaración como esta:

typedef char T;
T carac;

lo que hace es definir un tipo de datos T como sinónimo de char (de hecho, es char).
El tipo T en este caso admite los valores 0 a 255.
Estos "valores" son entidades matemáticas abstractas.
Para cada valor posible, el tipo T determina un modo concreto de representarlo en una computadora, digamos como una secuencia finita de bits 0 y 1.
Finalmente está el objeto con nombre carac, declarado de tipo T.

Hay 3 tipos de entidades aquí:

1. La colección de valores matemáticos abstractos de 0 a 255.
2. La colección (abstracta, mental, intuitiva) de pares ordenados (v, r), donde v es uno de los valores dados en (1.), y r es la secuencia finita de bits conque se representa en una computadora el valor v.
3. El objeto real caract, que es un bloque concreto de memoria RAM, por ejemplo, con sus bits puestos a 0 ó 1, representando físicamente el valor v mediante la secuencia de bits indicada por r.

Podríamos todavía dividir el punto 2 en dos partes:
2.(a). La "promesa" del lenguaje C, desde un punto de vista meramente sintáctico, de que a cada valor de 0 a 255 le corresponderá una representación concreta.
2.(b). La regla propiamente dicha que transforma un valor v en una representación r, que sería usada ulteriormente en una máquina real.


El lenguaje C define (aquí estoy mintiendo un poco para simplificar, pero no es relevante), mediante la declaración typedef, una asociación entre los valores v de 0 a 255, y los modos de representación en bits r.
Esto da lugar a una colección de 255 pares ordenados (v, r), y por lo tanto no hay gran diferencia entre los puntos (1.) y (2.).
Los "estados" posibles que puede tomar el objeto caract en la memoria RAM son, por lo tanto, también los correspondientes de 0 a 255. Son 256 estados posibles.

Hasta aquí todo viene fácil de entender, porque es una asociación trivial.
Lo que quiere dejar de relieve es lo siguiente:

En 1. tenemos entidades estrictamente matemáticas (e intuitivas).
En 2.(a). tenemos de nuevo entidades intuitivas, pero que a su vez están declaradas como parte de C, como lenguaje, en el "costado" sintáctico del mismo.
En 2.(b). tenemos una regla definida por el compilador, que asocia a un valor v un formato concreto r. Esto es "semántico", pero todavía está en abstracto, como una "regla".

En 3. estamos  en el "costado" semántico del programa, ya con la versión corriendo, o sea en forma ejecutable, en que todas las declaraciones escritas en C se han convertido en acciones concretas en una máquina. En particular, una porción de memoria RAM de cierta máquina ha adoptado unos estados eléctricos determinados que indican los bits 0 y 1.

En todos los casos, las colecciones involucradas son finitas, pues el tipo char sólo admite una colección finita de valores.

En C todo tipo de datos aritmético admite sólo una cantidad finita de valores,
y por lo tanto no pueden representarse todos los números naturales.


Pero también es posible definir tipos de datos T tal que su colección de posibles valores sea infinita. Éste es el caso, por ejemplo, de las listas enlazadas.
Una estructura como la siguiente:

typedef struct T { char d; struct T *link; } T;
T number;

Eso define un tipo de datos, T, con dos campos: uno de tipo char, y otro un puntero a un objeto de nuevo de tipo T.
O sea, es una estructura recursiva.

El campo d, que es de tipo char, ya sabemos que sólo admite un número finito de valores, digamos de 0 a 255.
En cambio, el campo link es de tipo struct T*, y es un apuntador a un objeto de tipo T.

El objeto number, declarado de tipo T, tiene esta estructura:

number: [d, link---> [d, link---> [d, link---> ... [d, link--->#] ... ]]]

He indicado con un signo # el final de la lista.

Este "final" de lista puede irse corriendo hacia la derecha tanto como uno quiera.
El lenguaje C permite ir agregando nuevos objetos de tipo T, de uno en uno, que serán apuntados por "link".

--------------

Ahora bien.
La idea es que cada campo "d" de tipo char sea un caracter indicando un dígito (en la base que queramos, por ejemplo decimal o binaria): '0', '1', ¿etc.?

Y cada "link" apunta hacia el siguiente "dígito".

Esta estructura está permitida por el lenguaje C, o sea que yo podría hablar,
acorde al punto (2.), de todos las listas enlazadas posibles que puedo asignar
a un objeto de tipo T, de manera que quede representado un número con cualquier cantidad de dígitos.

Algo más o menos parecido y más sencillo de entender sería,
simplemente, generar una cadena de caracteres formada por
tantos dígitos como queramos.
Hay inconvenientes técnicos, que me impiden especificar una cadena de caracteres de longitud arbitraria desde el lenguaje mismo. (La longitud máxima, aunque muy grande, sigue siendo finita...).

Por eso prefiero usar el planteo de las listas enlazadas.

Ahora bien, las computadoras reales no tienen suficiente memoria para representar los dígitos de un número demasiado grande.

Sin embargo el lenguaje C por sí mismo no impide nada desde el punto de vista sintáctico (o sea que las entidades indicadas en el punto (2.) serían infinitas), y de hecho, amontonando muchas tarjetas de memoria RAM sería posible representar cualquier secuencia finita de dígitos, por grande que sea.

Así que, suponiendo siempre un Universo de infinitos átomos (que no hay),
sería posible representar los dígitos de cualquier número natural estándar en una computadora concreta.

Algo todavía más sencillo sería el mero acto de imprimir dígitos en un rollo de papel de una impresora.

Entonces mi pregunta es si acaso la colección de todos los posibles objetos así generados pueden considerarse todavía un "modelo estándar de N".

Y con estándar me refiero, de paso, a que no hay elementos no estándar.
De hecho, para "generar" estos números, no hay más remedio que comenzar desde un objeto con un solo dígito, e ir agregando de uno en uno, a lo largo de la línea temporal, los dígitos que van haciendo falta.

Ya que sobre los dígitos puedo definir las operaciones de suma y multiplicación en forma mecánica e iterativa (de un dígito pasando al siguiente), me da toda la sensación de ser un sustituto del N "estándar".


9
Si bien es una pregunta que tiene que ver con computadoras,
va en la sección de Metamatemática,
porque la motiva exclusivamente una duda de fundamentos de la lógica.
(Nada tiene que ver con la discusión sobre inteligencia artificial en otro hilo).

Primero que nada, pongo un ejemplo que me parece claro a mí mismo.
Supongamos una demostración de un razonamiento cómo éste:

(x = y) & (y = z) ----> (x = z)

La demostración de algo como eso se puede hacer en base a los axiomas de la lógica,
siguiendo reglas mecánicas que transforman un texto en otro.
Por ello, es algo que, si se realiza en forma mecánica con una computadora, y se imprimen los pasos de la demostración,
el resultado se consideraría una demostración igual de válida que si la hubiera escrito un humano,
porque la demostración es, apenas, la escritura de unos pasos de lógica formal o matemática formal.

Sin embargo, como hemos hablado varias veces en el foro, en metamatemática intervienen algunos ingredientes mágicos:

(1) Una capacidad inherente de razonamiento, que no es formal.
(2) Números naturales intuitivos.
(3) Interpretación de variables en un modelo "de carne y hueso".
(4) Modelos concretos en alguna parte (¿en la imaginación?).
(5) Nociones de Verdadero y Falso al afirmar hechos sobre entes de un modelo, o relaciones y funciones sobre entes que son parte de un modelo.

Estos "ingredientes" no son formales.

Pero si yo quiero que un programa informático muestre resultados que evidencien ese tipo de cosas, no tengo más remedio que representar todo de manera escrita, o sea, imprimiendo caracteres.
Esto es una especie de "formalización".

La duda que tengo es cuál sería la manera más correcta en que yo tendría que hacer un programa, de modo que cuando escriba respuestas en la pantalla, sean juzgadas como "metamatemáticas", y que no se confundan con las demostraciones mecánicas formales.
Por ejemplo, me parece más o menos fácil (aunque no trivial) hacer que un programa "razone" sobre frases sencillas en español.
Pero conceptualmente, no sé si a esto se le puede llamar estrictamente "razonamiento" en el mismo sentido que el ingrediente (1).
Y lo mismo con los demás ingredientes.

Otro ejemplo.
Podría considerar modelos finitos, con relaciones y funciones entre sus elementos, perfectamente descriptibles por tablas finitas de datos.
Esto no requiere intuición alguna, porque es todo finito.
¿Se considera igualmente un modelo?
Si el programa dice: "la relación R entre los elementos A y B del modelo M es Verdadera", ¿está bien dicho? ¿Se considera bien hecho?
Si tengo una tabla de datos, finita, con una relación definida por una tabla, y a partir de esa tabla hago un cálculo e imprimo en pantalla que la relación entre dos elementos A y B es "verdadera", ¿es "verdaderamente" verdadera, o sea, es correcto interpretar que es verdadera en el mismo sentido que se considera en un texto de metamatemática?

Espero que se entienda la pregunta.
Es algo conceptual.

Por otro lado, estaría haciendo metamatemática a partir de un lenguaje formal (el lenguaje de programación de turno). Así que los conceptos me dan vueltas en círculos.
Pues si bien creo entender esto de que es posible realizar razonamientos formales en ZFC sobre cuestiones metamatemáticas, eso no es suficiente para justificar lo que ocurre en el universo metamatemático, pues justamente ZFC no se ha probado que sea consistente.

(No obstante, los lenguajes de programación son distintas, porque se puede suponer de ellos que están sujetos a restricciones de finitud, desde varios "flancos"...)

10
No me convenzo de lo que significa "intuición matemática", sobretodo "hasta dónde se supone que llega".

En esta búsqueda encontré un libro dedicado exclusivamente al tema:

RICHARD L. TIESZEN, Mathematical Intuition

He comenzado a leerlo, y es sobretodo filosófico, como es natural.
Parece ser una referencia adecuada para estudiar el tema.

Me gustaría saber vuestra opinión personal sobre el libro, si es que les resulta:
Atinado, desatinado, una porquería, da en el clavo, vale la pena leerlo, no vale la pena malgastar tiempo leyéndolo,
es científico, anticientífico, pseudocientífico, demasiado delirante, tiene la cuota necesaria de delirio, etc.

De todos modos lo voy a leer a fin de cuentas.

11
Encontré en Wikipedia una lista de una gran cantidad de lógicas:

http://en.wikipedia.org/wiki/Intermediate_logic

La pregunta va dirigida a Carlos, quien me ha dicho que hay una sola lógica "intuitiva", diǵámoslo así,
la cual se formaliza con la lógica de 1er orden, y que permite demostrar toda sentencia "verdadera".

Entonces, ¿qué relación tienen todas esas lógicas intermedias, incluyendo la versión formalizada del "intuicionismo", con la lógica clásica?
¿Acaso formalizan sólo "porciones" de la lógica clásica?
¿Son teorías consideradas "incompletas"?


12
Acerca de las diferencias entre infinito potencial e infinito real parece que hay mucha investigación en estos días.
He partido de leer un artículo de un tal Nelson, de Princeton, https://web.math.princeton.edu/~nelson/papers/e.pdf,
que por las dudas dejo colgado como archivo adjunto.

En ese artículo Nelson define los números naturales que llama "counting" (o "para contar" o "que cuentan"), dando a entender que se trata de la versión intuicionista de los números naturales. Ellos se van construyendo de 1 en 1, quién sabe hasta dónde.

Afirma que en realidad existen dos sistemas de números naturales, y no uno solo: uno en que los naturales van apareciendo de uno en uno (sistema A, por Aristóteles), y otro en donde todos los naturales existen a la vez, de un solo golpe (infinito real, sistema P, por Platón).

Una cosa es dudar del sistema P, otra cosa es dudar del sistema A, pero otra distinta es estar de acuerdo con los dos sistemas.

Leí el artículo muy rápidamente, así que no lo he masticado bien.
Pero deja en claro que el punto de vista del infinito potencial no sólo tiene muchos adeptos hoy en día, sino que es área activa de investigación.
Además, afirma que hace más fácil resultados clásicos.

Yo cada vez entiendo menos cuál es, entonces, "el" sistema de números naturales que se supone tienen en mente quienes estudian Fundamentos.
¿Alguna idea?

13
Enlaces sugeridos / ¡Acceso al foro aún cuando está colgado!
« en: 27 Enero, 2014, 11:47 pm »
La web www.archive.org guarda copias de las páginas webs de años anteriores.
Aproximadamente una vez por mes hace una copia de lo que se ha publicado en el foro.
No sé si esta copia se hace automáticamente o alguien la sugiere. En todo caso, yo no fui.

Se puede acceder a copias antiguas del foro a través de este enlace:

http://web.archive.org/web/*/www.rinconmatematico.com/foros

Ahí hay que elegir el año que nos interese en la barra superior,
y debajo en el almanaque elegir la fecha que nos interese.

A mí me sirvió para recuperar unos posts que había perdido parcialmente.

No se actualiza todos los días, así que no hay garantía de poder ver los posts más recientes.
Pero puede servir en caso de que la web esté colgada y queramos ver algún post de más de un mes de antigüedad.

Saludos

14
Tutoriales y fórmulas con LaTeX / Información general sobre LaTeX
« en: 14 Noviembre, 2013, 05:07 am »
Para obtener toda la información existente y actualizada sobre LaTeX, hay que ir a la web del catálogo de TeX online:

CTAN

15
Zona de resolución / Curso de C - Ejercicio 49.1
« en: 06 Octubre, 2013, 07:12 pm »
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.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Ahora, 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.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Cuando 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.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Cuando 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.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Má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.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

En 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).

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Primero 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.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Esqueleto

bc.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.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Continuará... :)

16
Problemas y Desafíos / !
« en: 29 Septiembre, 2013, 06:36 pm »
!

17
Álgebra / !
« en: 29 Septiembre, 2013, 06:34 pm »
!

18
Matemáticas Generales / !
« en: 29 Septiembre, 2013, 06:33 pm »
!

19
Matemática Discreta y Algoritmos / ?
« en: 29 Septiembre, 2013, 06:32 pm »
?

20
Librería NBIO.H

Estos días estuve jugando con un programa acá en el foro,
que calculaba el m.c.d. de dos números,
pero que derivó en un análisis de cómo mejorar la interacción con el usuario.

Programando en C,
muchas veces surgen dudas cuando usamos scanf() o gets(), y funciones similares.

Voy a colgar acá una librería que resuelve en forma básica varios problemas que surgen en la práctica sobre este tema, en forma recurrente.



Problema 1: ¿Cómo detectar si una string está compuesta sólo por caracteres "blancos"?

Solución 1
Entendemos por caracteres blancos no sólo a los espacios, sino también a los caracteres de control reconocidos por C: \t, \n, \a, \f, etc.

Para esto podemos confiar en la función isspace() de la librería estándar ctype.h,
que reconoce en forma correcta cuándo un caracter dado es un "blanco" según la definición entendida por el estándar C.

Ahí les va:


int blank(char *s) {
    for ( ; *s; ++s)
       if (!isspace(*s))
          /* Se ha encontrado un caracter que no es blanco. */
          return false;
    
    /* s es una string que sólo contiene blancos (al "estilo" isspace()) */
    return true;
}


Nota: Si bien la función devuelve sólo 1 (true) ó 0 (false), he puesto un tipo de retorno int, por misteriosas razones... (Es para aplicarla a problemas que analizamos luego).

[cerrar]



Problema 2: ¿Cómo detectar si un caracter pertenece a un conjunto dado de caracteres?

Solución 2

Lo que hacemos es definir el conjunto como una string que simplemente lista los caracteres admisibles.

A continuación, basta escribir una sencilla función de búsqueda:


/* Buscar un caracter en una cadena */
bool char_in_str(char c, char* s) {
   for ( ; *s; s++)
      if (c == *s)
         return true; /* c "pertenece" a s */
   /* c "no pertenece" a s */  
   return false;    
}

[cerrar]



Problema 3: ¿Cómo asignarle un valor a una variable string?

Solución 3
Lo que se hace es copiar una string en otra, caracter a caracter.


void nb_set_string(char* restrict dest, const char* restrict src) {
    /* Copia src en dest */
    for( ; (*dest = *src) != '\0'; src++, dest++)
      ;
}
/* ---> OUT: dest == src, strlen(dest) == strlen(src) */


La palabra clave restrict intenta prevenir superposiciones en memoria de los bytes de cada string.
[cerrar]



Problema 4: ¿Cómo frenar el programa antes de que termine?

Solución 4
Muchas veces, al hacer un programa sencillo,
este termina abruptamente y se cierra la ventana de ejecución,
no permitiendo que veamos los últimos resultados mostrados en la pantalla.

Las soluciones típicas que se suelen usar consisten en poner antes del fin de main() una de estas sentencias:


getchar();           /* Requiere stdio.h */
system("PAUSE"); /* Requiere stdlib.h */


Lo malo de system("PAUSE"); es que sólo funciona si en el sistema subyacente hay un comando llamado PAUSE que sirva para "frenar" la consola y esperar hasta que el usuario presiona ENTER.

Lo malo de getchar(); es que,
en mi experiencia (usando el compilador GCC),
se obtienen efectos indeseados al combinarlo con llamadas a scanf().
Parece ser que no son del todo compatibles.
De algún modo, al usar scanf() queda algún caracter en la cola de espera de la entrada estándar,
y esto hace que getchar(); "siga de largo", no logrando el efecto de "frenado".

Al parecer, los efectos de getchar() y fgets( ... , stdin) (o gets()) son compatibles entre sí,
y estos errores pueden evitarse.

Aquí la solución es compleja, porque requiere que todo el tiempo tengamos una disciplina en el diseño:

* Evitar el uso directo de scanf().

En las operaciones de lectura haríamos algo como esto:

gets(buffer);
sscanf(buffer, "cadena de formato", &variables...);

/* Y cuando queramos frenar la consola: */

getchar();


Podemos colocar todo en una función, que llamaremos enter_to_continue(),
y de paso explicamos cómo usarla correctamente.
Es más, hasta podemos agregarle, si queremos, un mensaje que informe alguna cosa al usuario:


/* Mostrar un mensaje al usuario y esperar a que presione ENTER para seguir. */
/* Se usa una combinación de fgets(..., stdin) y getchar() para              */
/* leer de forma correcta caracteres "basura" hasta alcanzar el fin de línea */
/* (Con (MINGW) GCC <= 4.8 no es compatible con previas llamadas directas scanf())   */
void enter_to_continue(char* msg) {
      puts(msg);
      
      char minibuff[1] = "";      /* == { '\0' } */
      fgets(minibuff, 1, stdin);
      /* buffer[0] == '\0'; */
      /* Se han leído 0 caracteres            */
      /*     ---> No se ha podido leer el fin de línea */
      while(getchar()!='\n') ;
}


[cerrar]



Problema 5: ¿Cómo leer desde la entrada estándar una string en forma segura?

Solución 5
Quienes hayan usado scanf() para leer datos habrán tropezado más de una vez con extrañas e indeseables vicisitudes.
Puede que sea un problema de la distribución (la mía es (MINGW) GCC 4.7).
Como sea, eso me ha motivado a "postergar" la lectura de los datos ingresados por el usuario en la entrada estándar (teclado), que en C es un archivo que se llama stdio (en la librería estándar stdio.h).

Ya he comentado antes la técnica básica para esto:

gets(buffer);
sscanf(buffer, "%formatos...", &variables...);


La función gets() lee una línea completa del teclado hasta alcanzar el fin de línea '\n'.
El caracter '\n' es descartado y reeplazado por el caracter nulo '\0'.
El resultado se guarda en buffer, que es una variable de tipo char[].

Pero aquí surge un nuevo problema:

* ¡La función gets() no es segura!  :o :o :o


La razón de esto es simple: si del teclado se han leído más caracteres que los que caben en el array de caracteres buffer[], entonces no hay espacio suficiente, y se produce una sobreescritura de posiciones prohibidas de la memoria, causando estragos en el programa, y en el sistema.

Usar gets() es un pecado en la "religión" de la informática.

(Hay una conspiración global contra el uso de goto, pero yo digo que el goto no es grave, si se lo sabe usar bien. De hecho, esto le pasa a cualquier aspecto de un lenguaje de programación. En cambio el uso de gets() sí tiene que estar prohibido, porque el programador no puede controlar sus efectos).

Para evitar esto, hay que usar algún mecanismo análogo a gets(), pero que impida que se lean más caracteres que los que puede albergar el buffer.

Por suerte en stdio.h hay una alternativa que es la función fgets(): Lee líneas de archivos de texto, hasta una cantidad especificada como máximo de caracteres.
El archivo de texto que nos interesa leer es stdin: la entrada estándar (o teclado).

Si buffer es un array de N caracteres,
entonces tendríamos que leer así:

fgets(buffer, N, stdin);

Esta sentencia lee N - 1  :o :o caracteres de stdin (o sea el teclado), agrega un caracter nulo '\0',
y el resultado lo pone en buffer.
Uno de los caracteres leídos y guardados en buffer será el fin de línea '\n' (cosa que no ocurría con gets()).

Listo, ya no hay problemas de sobreescritura de memoria...  ;D

Pero se arruina todo de nuevo  >:( porque si el usuario teclea más que N - 1 caracteres,
estos quedan en la "cola de espera" de stdin,
y serán tomados como caracteres válidas en una próxima lectura de stdin,
ya sea que se haga con fgets(..., stdin), gets(), getchar() o scanf().

Esto es un nuevo problema a resolver.
Lo que necesitamos es detectar si se ha presionado o no la tecla ENTER, y en caso negativo, seguir leyendo caracteres de la "cola de espera" de stdin hasta encontrar un fin de línea '\n'.
¿Cómo se hace esto?   ??? ??? ???

Para resolver la cuestión, no nos salgamos de contexto, porque la solución puede no ser aplicable en todos los casos.
Hemos leído datos con fgets(buffer, N, stdin), y queremos determinar si se ha alcanzado el fin de línea o no.

Si buffer contiene algún '\n', quiere decir que fgets() lo ha leído y guardado allí, y que en la posición siguiente hay un '\0'.
Pero este caso no nos interesa...
Lo que nos interesa es saber cuándo NO HAY '\n' en buffer, o sea, cuándos fgets() no alcanzó a leer hasta el fin de línea.
Si esto ha ocurrido, entonces el buffer está lleno hasta la posición \( N-2 \) de caracteres distintos de '\n' y '\0'. Y hay un '\0' en la posición \( N-1 \).
Recíprocamente, si estas condiciones ocurren, quiere decir también que el fin de línea no se ha alcanzado, porque en ese caso el buffer se ha llenado hasta la posición \( N-2 \) con datos distintos al fin de línea.

Por lo tanto, es sencillo saber la condición lógica que debe cumplirse en el programa para saber si el fin de línea se ha alcanzado o no:

fgets(buffer, N, stdin) pudo alcanzar el fin de línea si y sólo si (buffer[N-2] != '\0') && (buffer[N-2] != '\n')

Si esta condición se cumple, leemos con getchar() sucesivos caracteres de stdin hasta encontrar el próximo fin de línea '\n':


while(getchar() != '\n')
    ;  /* sentencia vacía */


Todo esto funciona si el número de caracteres del buffer satisface \( N \geq 2 \).
¿Qué pasa si \( N == 1 \)?
En este caso sólo hay espacio en el buffer para 1 caracter: el nulo '\0'.
O sea que la llamada fgets(buffer, 1, stdin) nunca alcanza el fin de línea.
Aquí siempre habrá que "limpiar" la "cola de espera" buscando el próximo fin de línea.

Hay otro problema.  :-\  :'( :'(

La variable buffer[] puede que sea utilizada en el programa para diversas cosas que al programador se le ocurran, y entonces fgets() no tiene el control de lo que ocurre con los caracteres del array.
Por lo tanto, puede que a veces en la posición buffer[N-2] haya un caracter distinto del fin de línea y del nulo, aún en el caso en que fgets() haya leído el fin de línea.

Esto sólo puede ocurrir si previamente había información "basura" en buffer[].
Lo ideal sería limpiar toooooodo el array buffer[], rellenando con '\0' todos las posiciones.
Sin embargo esto es ineficiente, y además es innecesario.

Dado que la única posición que nos preocupa es buffer[N-2], basta conque limpiemos esa sola posición de memoria, ya que la "basura" de las otras posiciones no nos afecta en modo alguno.
Limpiar 1 solo caracter es más eficiente, y no importa cuán grande sea N, se tarda siempre lo mismo en esta tarea de "limpieza" tan sencilla:

buffer[N-2] = '\0';

Por fin hemos terminado de analizar todos los casos.
Ahora podemos poner todo junto en una función que haga el trabajo.
Con comentarios y todo, queda así:


/* Se usa la función "segura" fgets() en vez de gets()                     */
/* No permite al usuario ingresar cadenas de longitud mayor que el buffer. */
/* buffer debe tener al menos N caracteres disponibles.                    */
/* Si buffer no es un array con tamaño suficiente, nb_gets() no es válida. */
char* nb_gets(char *buffer, int N) {
    if (N <= 0)
        /* No hay espacio para leer */
        return NULL;
    /* N >= 1 */
    if (N == 1) {
        /* Sólo hay espacio para el caracter nulo        */
        fgets(buffer, N, stdin);
        /* buffer[0] == '\0'; */
        /* Se han leído N - 1 == 0 caracteres            */
        /*     ---> No se ha podido leer el fin de línea */
        while(getchar()!='\n')
          ;
                  
        return buffer;
    };
    /* N >= 2 */    
      
    buffer[N-2] = '\0'; /* Limpieza rápida del buffer (ver abajo) */
    fgets(buffer, N, stdin);
    /* Se cumple la siguiente propiedad:                                */
    /* (no_se_alcanzó_fin_de_línea)                                     */
    /*          <---->                                                  */
    /* (buffer[N-2] != '\0') && (buffer[N-2] != '\n')                 */
    /* Por lo tanto es suficiente limpiar el caracter .buffer[N-2]      */
    
    if ((buffer[N-2] != '\0') && (buffer[N-2] != '\n'))
       /* No se ha alcanzado el fin de línea */
       while ( getchar() != '\n' ) /* flush_stdin... */
            /* ---> produce una descarga de stdin */
            /*      descartando caracteres hasta llegar al fin de línea */
           /* */ ;
    /* else ; */
      /* Sí se ha alcanzado el fin de línea: no se hace nada. */
      
    return buffer;
}


Si queremos verla limpia de tandos comentarios y aserciones, queda esto:


char* nb_gets(char *buffer, int N) {
    if (N <= 0)
        /* No hay espacio para leer */
        return NULL;

    if (N == 1) {
        fgets(buffer, N, stdin);
        while(getchar() != '\n')
          ;
                  
        return buffer;
    };

    buffer[N-2] = '\0'; /* Limpieza rápida del buffer (ver abajo) */
    fgets(buffer, N, stdin);
    if ((buffer[N-2] != '\0') && (buffer[N-2] != '\n'))
       while (getchar() != '\n')
           ;
      
    return buffer;
}

[cerrar]



Problema 6: ¿Cómo se hace para saber si una lectura del tipo scanf(), fscanf(), sscanf() ha producido o tenido un error?

Spoiler
Lo que muchas veces olvidamos es que esas funciones retornan un valor entero que informa del éxito que se ha tenido leyendo datos del archivo de entrada.
Si el resultado de la operación es EOF, quiere decir que hubo un error en la lectura de los datos.
En cambio, si es un entero no negativo, indica cuántos argumentos se han podido reemplezar correctamente, acorde al tipo de datos especificado.

Cuando se encuentra un error de cualquier tipo, la función scanf() (por ejemplo) interrumpe su trabajo y retorna de inmediato al programa principal.
Entonces la cantidad de argumentos correctamente reemplazados puede ser menos que el total de parámetros que se le pasaron como argumento.

Para saber si hubo algún error, hay que comparar la cantidad de directivas % de la cadena de formato pasada a scanf() con el valor de retorno de scanf() que informa del número de reemplazos efectivamente realizados con éxito.
Además, no hay que contar los casos de %% ni de %*, que no reemplazan argumentos de verdad.

El único modo de lograr esto es analizar la cadena de formato, y contar cuántas directivas hay.
El problema consiste, entonces, en hacer un recuento exacto del número de directivas "efectivas" de la cadena de formato.

Según el estándar C99, se puede deducir que si al caracter % le siguen ciertos caracteres, entonces estamos ante una directiva válida. En caso negativo, preferiremos informar que hay un error del programador (un error fatal).
Una vez asegurados de que la cadena de formato no contiene directivas desconocidas,
observamos que es posible intercalar dígitos entre el caracter % y la directiva propiamente dicha.
Para no ser engañados por una directiva errónea a la que le han intercalado un dígito, vamos a ignorar todos los dígitos de la cadena de formato.
No es este el enfoque más exacto, pero aún así nos resuelve la cuestión, salvo por un muy pequeño matiz, que no tiene importancia práctica, que luego veremos.

Lo que haremos será un algoritmo que detecte la presencia de un '%' en una cadena de formato, y espere a analizar el caracter siguiente, a ver si conviene contarlo o no.
Se deja guardada la información en una variable tipo "flag" (un banderín), que es true cuando se ha encontrado un '%' y false en otro caso. Además, hay que apañarse para quitar los casos indeseables de %% y %*.

Nuestro algoritmo funciona correctamente con cadenas de formato correctas.
Con cadenas de formato inválidas, o tal que el estándar no aclara cómo se supone que han de funcionar, seguirán siendo inválidas para nosotros, pero la interpretación de la razón que la invalida es diferente.
Esto es un detalle técnico sutil, pero que en la práctica no afecta, porque un programa correcto sólo ha de tener cadenas de formato válidas.

Ahí les va:


/* El entero retornado por nb_scanfdirs() es: */
/* n >= 0: Se hallaron n directivas válidas (C99) para argumentos reales.   */
/* n < 0:  Error: hay directivas no reconocidas.                            */
/* Nota adicional: Las directivas que contienen dígitos antepuesto ("%20f") */
/* son reconocidas correctamente. */
/* Nota ténica: */
/* Los casos "%digitos%" son erróneos en C99 (ver sección 7.19.6.2.12).     */
/* pero al mismo tiempo son reconocidos inexactamente por __bui_N_ARGS().   */
/* El resultado es un programa erróneo en cualquier caso, aunque            */
/* la "semántica" para este caso de sscanf() y NB_SCANF() no coincidirá.    */
int nb_scanfdirs(char *s) {
    int n;
    bool before = false;
    
    for (n = 0; *s; s++)
      /* n == 0 ---> before == false */
      /* n > 0  --->                                              */
      /*     [ (último caracter leído no-dígito de s) == '%'      */
      /*               <----> before == true ]                    */
      if (*s == '%')
        /* Recordar que se ha encontrado un signo '%' en esta posición, */
        /* pero no tomar ninguna acción.                                */
        /* Si antes ya había un '%', es que estamos ante el caso %%,    */
        /* y entonces se debe "cancelar" el estado del flag "before".   */
        /* Si no, registramos la aparición de '%' como true,            */
        /* para ser analizado en la siguiente iteración.                */
        before = ( (before)? false: true );
      else {
        /* El caracter actual no es '%' */    
        if (before)
          /* *(s-1) == '%' (el caracter previo era un '%')          */
          /* Directiva % detectada.                                 */
          if (char_in_str(*s, SCANF_SAFE_DIGITS))
             /* Si se encuentra un dígito, mejor ignorarlo,                 */
             /* y seguir buscando el próximo caracter que no sea un dígito. */
/* CONTINUE */
             continue;
             /* before == true */
             /*     ---> Directivas con dígitos se reconocen correctamente. */
            
          else if (char_in_str(*s, SCANF_SAFE_ARG_SET_C99))
             /* La directiva % hallada corresponde a un caso "seguro",  */
             /* que además corresponde a un argumento real.             */
             /* ---> entonces incrementar el contador.                  */
             n++;
          else if (char_in_str(*s, SCANF_SAFE_SPE_SET_C99))
             /* La directiva % hallada corresponde a un caso "seguro",  */
             /* pero que no corresponde a un argumento real.            */
             ; /* Nada */
          else
             /* Se encontró una directiva desconocida. */
             /* Se termina abruptamente la función y retorna error. */
/* RETURN */
             return NB_FMT_NOT_VALID; /* < 0 */
        else /* if(before) */
          ;
                    
        before = false;
        /* Se registra para la iteración siguiente */
        /* que el caracter actual no es un '%'     */
      }
    /* n == número de directivas % halladas en s.                        */
    /* Sólo se cuentan casos que efectivamente se asignan a argumentos,  */
    /* de los tipos especificados por el estándar C99.                   */
    /* No se cuentan los casos de %%, %*.                                */
          
    return n;        
}


Ahora una comparación del tipo:

scanf(fmt, ...) != nb_scanfdirs(fmt)

nos informará de que hubo un error en la lectura de datos con scanf().

[cerrar]



Problema 7: ¿Cómo pasar parámetros de un tipo concreto a una macro?

Solución 7
Sabemos que las macros tipo función como:

#define MOSTRAR_CUADRADO(X) printf("%d", ((X) * (X)));

toman argumentos arbitrarios, y no hay modo de indicar en la lista de parámetros si han de ser de un tipo de datos específico u otro.
Ni siquiera podemos asegurar que sea algo con sentido lo que "cae" en X.

Es que X es sólo una abreviatura de una "porción de texto" que se usará para ser reemplazada dentro de la macro.

Pero podemos aprovechar las capacidades del compilador para forzar la macro a que los parámetros sean de un tipo específico.
Por ejemplo, si queremos que nuestra macro anterior sólo acepte X como un dato de tipo int,
lo que haremos es declarar una variable temporal de tipo int, y asignarle el valor X.
Si la asignación es sintácticamente correcta, el programa compilará, y si no, entonces no lo hará.

Para hacer las cosas con prolijidad, y evitar efectos indeseados, encerraremos el código de la macro entre llaves { }.
Además, el nombre de la variable temporal tiene que ser tal que no entre en conflicto con otros nombres que pudiera usar el programador en otros lugares.
Lo típico es usar para estos casos nombres que empiecen con __.

Así, tenemos ahora:

#define MOSTRAR_CUADRADO(X) {    \
       int __temp_x = (X);       \
       printf("%d\n", __temp_x); \
  }


Cuando escribamos:

MOSTRAR_CUADRADO(15.18741);

el compilador nos dará una advertencia o en otros casos dará un error.

Por otra parte, la variable temporal __temp_x queda "encapsulada": ya no podrá ser referenciada una vez terminada la macro.
Esto ahorra potenciales problemas. (¿Qué pasa si al programador se le ocurre hacer algo con una variable llamada "también" __temp_x?: sólo desastres podrían ocurrir).

Una ventaja adicional de este método es que evita algunos efectos indeseables de las macros.
Por ejemplo, en la versión original:

int x = 5;
MOSTRAR_CUADRADO(++x);

hubiéramos obtenido como resultado 49, que es incorrecto, porque el resultado deseado es \( (5+1)^2=36 \).

Eso es un ejemplo típico que enseña los "peligros"  :o :o :o (mira cómo tiemblo...) del uso de las macros.

Con la nueva versión, que usa la variable temporal, se obtiene el resultado correcto 36.



Sin embargo, hemos puesto un bloque entre llaves { } para que la variable temporal funcione.
Aún si quitáramos las llaves, la macro no deja de ser una colección de sentencias,
las cuales han de "terminar", sin dejar "flotando" un valor como resultado.
¿Qué diablos estoy diciendo?

La expresión: x = 5, x = (3 + x)
arroja el valor 8 como resultado, y puede usarse aún dentro de otras expresiones,
para proseguir con cálculos más complejos.
En cambio:

int x = 5;

es una sentencia que no puede insertarse en medio de otros cálculos: debe terminar ahí.

Si ahora quisiéramos una macro como:

#define CUADRADO(X) ((X) * (X))

¿cómo conseguiríamos el mismo efecto que antes, de "obligar" a que X sea de tipo int, y que además se eviten otros efectos indeseables típicos de las macros tipo función?

Esa macro quiere servir para hacer algo como esto:

int x = 5;
int ans;

ans = 70 - CUADRADO(x);
printf("%d\n", ans);


El resultado sería 45...

Si en la macro ponemos llaves { },
o tan sólo hacemos el intento de declarar una variable temporal int __temp_x, como antes,
ya no podremos invocar la macro como parte de una expresión.
La línea siguiente dará un error del compilador:

ans = 70 - CUADRADO(x);

Una solución parcial sería la de tener previamente definida una variable auxiliar de tipo int, e invocarla dentro de la macro:

int __temp_x;
#define CUADRADO(X) ((__temp_x = (X)), __temp_x * __temp_x)


El operador coma (,) va descartando todos los cálculos que aparecen,
y el resultado de toda la expresión es el que queda a la derecha de la última coma.
Por eso podemos hacer casi cualquier antes de calcular el dichoso cuadrado.
Vemos que esta técnica impide que la macro tenga efectos indeseables:

Se asegura que el dato X asignado a __temp_x es de tipo int,
y "lee" una sola vez el parámetro X, para luego operar con ese valor tranquilamente dentro del resto de la macro,
tal como hace una función con un parámetro pasado "por valor".

Lo único malo acá es que la variable "temporal" tuvo que definirse externamente a la macro.
Por ahora intentemos evitar eso lo más posible.

La "solución" que voy a emplear es la "paciencia".
En efecto: voy a esperar a que los compiladores adopten las nuevas características del estándar C 2011, en que se puede especificar el tipo de datos del parámetro de una macro.

(Más adelante voy a ensayar una alternativa, pero será complicada...)

[cerrar]



(Nuevo)

Problema 8: ¿Cómo crear una macro tipo "bloque" y que sea "segura"?

Solución 8
Una macro tipo bloque es algo como esto:

#define MACRO { \
    sentencias...; \
    sentencias...; \
    \
  }


Es decir, el cuerpo de la macro es una lista de sentencias que se encierran entre llaves { }.
A un grupo de instrucciones encerradas entre llaves se le suele denominar "bloque".

La utilidad de las llaves al definir una macro extensa y complicada,
es que aporta "seguridad".
Es que las macros por sí solas son fuente de desmanes sintácticos difíciles de detectar.
Si no se encierran bien las instrucciones entre llaves, pueden "pegarse" de formas extrañas e inarmónicas con el resto del programa, dando errores o comportamientos antiintuitivos.

Que el código de una macro sea "seguro" quiere decir que "se inserta bien y armoniosamente con el resto del programa".
Una macro sin parámetros tiene que verse intuitivamente y encajar en el código como si fuese, digamos, cualquier otro identificador (constante o variable).
Una macro con parámetros tiene que verse intuitivamente y encajar en el código como si fuese una función.

El uso de llaves parece ayudarnos en estos objetivos, pero hay una sutileza:

* Los bloques a veces se interpretan de modo diferente según las sentencias circundantes.

Por ejemplo:

if (1)
  calc();
else
   ! calc();


Si calc() es una función, el código compila bien.
Si no, si calc() es una macro puesta en bloque, la compilación falla antes de la línea "else".

La única solución que hay para esto (en el C estándar)
es usar otra construcción: en vez del bloque "pelado",
ponerlo como parte de un do {} while() que ejecuta una sola vez, así:

#define MACRO  do { \
    sentencias...; \
    sentencias...; \
    \
  } while(0)


Al final se pone while(0) para que el cuerpo de la macro se ejecute 1 sola vez.
Además, es importante "no poner" un punto y coma (;) al final, para que encaje armoniosamente en otras porciones de código. Ahora, si calc() es una macro tipo función escrita como un bloque encerrado con do {} while(0), encajará en el programa de la misma forma que una función, y casi no se notará la diferencia...

Hay sin embargo una diferencia esencial entre las funciones y las macros de bloque: éstas últimas no permiten que se retorne un valor, o sea, se computa como una sentencia y no como una expresión.
Son como funciones sin retorno (similar a los procedimientos de Pascal).

Para que la construcción no quede tan fea, y tenga un sentido más inteligible para el programador,
podemos definir macros apropiadas que se sustituyan por do y while(0), así:

#define __BegSafeBlock do {
#define __EndSafeBlock } while(0)


Es crucial colocar las llaves ahí mismo en la definición de esas macros,
porque si no, algún desubicado podría querer usar __EndSafeBlock como el "inicio" de un while() {...}, en vez del final de un do {...} while(), que es exactamente lo que queremos.

Lo ideal sería poner esas definiciones en una librería aparte, donde se documente exactamente para qué diablos se han definido. Esta explicación del propósito de las macros hará que se eviten errores accidentales: todos los errores por el uso de dichas macros será mera intención del programador.
Ejemplo:

#define __BegSafeBlock do {
#define __EndSafeBlock } while(0)

#define MOSTRAR_FRASE(S) \
__BegSafeBlock \
       printf("El programador le envía el siguiente mensaje:\n"); \
       printf("%s", S); \
       getchar(); \
__EndSafeBlock

int main(void) {
  MOSTRAR_FRASE("To C or not to C\n");
}  

[cerrar]

:)

Páginas: [1] 2 3 4 ... 8