Por esas cosas de la vida, hace unos meses me vi implicado en un cuestionable favor, pero justificado por motivos de fuerza mayor. Se trata de esas prácticas universitarias, en este caso la correspondiente a la asignatura de fundamentos de la programación en la UPF (Universitat Pompeu Fabra) del curso 2015-2016 para la recuperación del segundo trimestre.

El primer ejercicio (Ejercicio 1), ya estaba resuelto, así que me tocaba aplicarme solamente en el Ejercicio 2. El problema es el habitual que se aduce, que hay cosas que no se han explicado en clase, que no se sabe ni por donde empezar, que no hay tiempo suficiente para desarrollarla, … Ciertamente si comparamos el Ejercicio 1, de una dificultad bastante sencilla, y con algunas pistas claras en el enunciado, como lo de usar la función gets, con el Ejercicio 2, que además de largo y redundante (hace calcular medias diferentes, y valores extremos distintos), sin dar apenas pistas, la dificultad es clara.

No creo que el problema sea que se necesitan cosas que no se han explicado en clase, sino que en una asignatura de un cuatrimestre, digamos 60 horas de clases, es imposible asumir la metodología de programación. A mi modo de ver, el problema no es del profesorado, sino más bien de los lumbreras que diseñan los planes de estudios. Sería como pretender que alguien que apenas saber leer y escribir, termine un libro corto en ese cuatrimestre.

Otro detalle que vemos, es que se sigue aplicando esa política neo-hippy, de poder hacer el programa en parejas. Vamos a ver: ¿entonces como evalúas que no haya sido uno de los miembros el que haya hecho todo? El título dice que es una práctica de fundamentos de la programación, no de programación en equipo. Lo que se pretende, es que el alumno demuestre lo aprendido, es decir a programar en lenguaje C, no que desarrolle colaborativamente. Sería como en un examen de matemáticas, donde se pudiera escoger entre una prueba tipo test, o una partida de póker contra el profesor. Ambas están relacionadas, pero sólo la primera forma prueba los conocimientos adquiridos del estudiante. No tengo remilgos en estas cosas, pero me deja tranquilo comprobar, que en este sentido, no se incumple ninguna regla. El ejercicio está desarrollado en parejas. Es decir, lo he creado yo, y lo entrega el aludido anónimo. Al fin y al cabo, tampoco especifica en ningún sitio que requisitos deben tener las parejas.

Desconozco también el tiempo estimado que los profesores consideraban necesario para terminar este ejercicio. Lo cierto es que un programador relativamente experimentado en C como es mi caso, estuvo durante una hora y media escribiendo y probando. Yo mismo pasé por algo parecido, y se que aunque no se diga, se valora más que no haya fallos, a las virguerías que puedan programarse. Así que de esa hora y media, la mitad del tiempo la invertí depurando y ejecutando paso a paso el programa. Fácilmente a un estudiante que ya tuviera nociones de programación, le podría llevar 10-20 horas. Muchas más, si fuera totalmente novato.

Me llamó la atención esos detalles tan académicos que veréis a continuación en el enunciado, referidos a nomenclaturas de nombres para archivos, y entregables, pero que en ningún momento entran en lo importante, que es la nomenclatura interna del propio programa. Tampoco predican con el ejemplo, y esa rigidez, se ve contrarrestada por heterogeneidades como bullets, que terminan en punto y aparte, y otros que no. Es una tontería, pero son cosas en las que me fijo.

El objetivo de este artículo, es intentar explicar la solución, también la daré completa, porque si quieres hacer copiar y pegar, eres muy libre, pero la iré explicando paso a paso, porque en cierta forma es mi manera de compensar los defectos del sistema. Os dejo con el enunciado:

Enunciado

Fundamentos de la programación, curso 2015-2016
Práctica recuperación 2º trimestre

Esta práctica de recuperación correspondiente al 2º trimestre (1ª parte de la asignatura) consta de 2 ejercicios independientes, cada uno de ellos valorado en 5 puntos. La nota total de esta práctica (sobre 10) será por tanto la suma de las dos notas obtenidas en cada uno de los ejercicios. Los criterios de evaluación serán los mismos que se han venido aplicando durante todo el curso. Esta práctica se podrá entregar de forma individual o en parejas.

La entrega deberá realizarse en un fichero .zip (NIA_NIA.zip), que contendrá los siguiente 3 ficheros:
– Fichero fuente ejercicio 1 (NIA_NIA_ej1.c)
– Fichero fuente ejercicio 2 (NIA_NIA_ej2.c)
– Memoria (NIA_NIA.pdf, con un único documento para ambos ejercicios)

Sólo podrán entregar esta práctica los estudiantes que hayan suspendido la práctica P1, y que tengan por consiguiente un suspenso en el apartado de práctica correspondiente al 2º trimestre. En ningún caso podrán entregarla estudiantes con la práctica aprobada en el
2º trimestre, con la intención de subir nota.

La práctica deberá entregarse antes del lunes 25 de julio a las 12.00.

Ejercicio 1
Escribid un programa que permita introducir una frase al usuario (sólo con letras en minúsculas y espacios en blanco) y que muestre por pantalla cuántas palabras diferentes hay en la frase, cuál es la palabra que más veces aparece y cuántas veces lo hace.
Notas:
– Se recomienda usar la función gets(), para capturar la frase introducida por el usuario.
– Si una vez introducida por el usuario, la frase contuviera otros caracteres que aquellos permitidos (i.e. sólo minúsculas y espacios), se le notificará al usuario, permitiéndole seguir introduciendo una nueva frase (hasta que ésta sea correcta).
– En caso de que empate, es decir, si hubiera varias palabras que aparecen un número máximo de veces, se seleccionará la palabra que primero aparece en la frase
– Debéis obligatoriamente definir un tipo de datos mediante estructuras para representar una palabra y el número de veces que ésta aparece. Notad que para controlar las palabras que van apareciendo deberéis ir guardando esas estructuras
en un array.
– Podéis asumir que como máximo habrá 100 palabras diferentes en la frase, e igualmente que una palabra tendrá un máximo de 20 letras.

Ejercicio 2
Supongamos que tenemos cuatro observatorios meteorológicos en Cataluña, uno por cada capital de provincia. Para cada uno de ellos se han recogido una serie de datos cada día del año.

Dichos datos son:
– temperatura máxima (número real)
– temperatura mínima (número real)
– precipitaciones totales (número entero)
– situación general (por ejemplo “soleado”, “soleado con intervalos nubosos por la tarde”, …)

Necesitamos un programa que permita:
1. Introducir esos valores de forma manual en nuestro programa: el usuario elige el día, mes y provincia y a continuación introducirá los valores.
2. Obtener la media de las temperatura máximas en una ciudad dada, para cada mes (cuando el usuario elige esta opción, elige una de las ciudades, y se le muestran los 12 valores correspondientes a cada uno de los meses del año)
3. Ídem para la media de las temperaturas mínimas
4. Ídem para el total de precipitaciones
5. Obtener el día, mes y ciudad en que se produce la temperatura más alta del año
6. Ídem con las más baja

Nota: para los puntos 2 a 6 hay que tener en cuenta que puede que no se hayan introducido los datos de todos los días, así que sólo deben tomarse en cuenta los valores de los días introducidos.

Desarrollo

Así que me pongo a ello, y opto por la herramienta que tengo más a mano: Visual C++ 2015 de Microsoft.

Lo que decido es hacer un array lineal, de una estructura que guarde todos los datos para cada ciudad. Por el mismo motivo, la hago global, y así me evito tener que ir pasando punteros a ella a las funciones. Se que es lo menos eficiente para luego acceder a los datos, y habrá que recorrerlos todos, pero montar una estructura más compleja, tipo un array indexado por día y mes, haría el código más largo:

struct
{
	char acCiudad[_MAX_PATH];
	unsigned int iDia;
	unsigned int iMes;
	float fMaxima;
	float fMinima;
	unsigned int iPrecipitaciones;
	char acSituacion[_MAX_PATH];
} gaudtObservaciones[366*4] /* = { {"barcelona", 1, 1, 10, 0, 100, "Soleado"}, { "barcelona", 2, 1, 20, 10, 200, "Lluvioso" } } */;
 
unsigned int giObservaciones = 0 /* 2 */ ;

La clave es que reservo como máximo una posición para cada día del año, que como puede ser bisiesto son 366, y las multiplico por las 4 provincias de Catalunya. Dentro están todos los campos que vamos a necesitar guardar para cada punto de observación. Utilizo la constante _MAX_PATH para no complicarme con las longitudes de las cadenas de caracteres. Para las ciudades, dado que la más largas son Barcelona y Tarragona, lo podría haber definido de 11 (10 caracteres y el NULL final), pero no nos viene de unos pocos bytes más. Para la situación uso lo mismo, no se como de larga podrá ser, pero 255 caracteres dan para mucho.

Otra anotación, es esa manía/tendencia a utilizar términos matemáticos, en una disciplina que dejó de tener que ver con las matemáticas hace 5 décadas. La vemos por ejemplo en lo del número real para las temperaturas. Os recuerdo, que un número real, es un superconjunto que engloba los números racionales, y los iracionales. O sea que 31/7 o Pi, son números de este tipo. Sabéis que Pi, tiene una cantidad de decimales infinita, igual que 31/7, así que necesitaríamos un ordenador con memoria infinita para modelar un número real, y lógicamente, un tiempo de cálculo infinito para operar con ellos. Es por eso, que en informática no existen los números reales, y como habéis visto, son virtualmente imposibles de implementar. Así que tenemos tipos de datos primitivos, que permiten aproximarlos como float, double o long double. Son no obstante aproximaciones, por lo que en este punto, y tomando el enunciado al pie de la letra, simplemente podríamos abortar su desarrollo al ser imposible. A falta de más detalles, que cómo veis faltan muchos, he optado por el más simple de ellos, el float, que considero suficiente para este menester. En menor grado, que las precipitaciones totales sean un número entero, vuelve a ser impracticable, no hay números enteros en programación, lo que hay son tipos de datos para contener algunos números enteros (los números enteros van desde -infinito hasta +infinito). He optado por el tipo básico int, y me he tomado la libertad de ponerlo unsigned. Sería un subconjunto de lo que a ellos con su terminología pseudomatemática, les gusta denominar números naturales. Pero, ¿desde cuando las precipitaciones totales pueden tener un número negativo?.

La segunda variable es sólo un índice que me indica cuál es la siguiente posición libre en el array. Lo que hay comentado es la información para poder rellenar el array estáticamente, y poder ir depurando sin tener que pasar por la introducción de datos.

He intentado que el código sea ANSI C, sin utilizar características de C99. Me da la impresión, que muchas de ellas, el corrector las consideraría C++, y sería un suspenso directo…

Introducir los valores, no tiene complejidad usamos scanf, y los metemos directamente en el array. No se exige ninguna comprobación de formato, ni gestión de errores, así que vamos por la vía rápida:

void DoIntroducirValores (void)
{
	puts("Introducir valores - Introduzca DIA MES CIUDAD :");
	scanf("%u %u %s", &gaudtObservaciones[giObservaciones].iDia, &gaudtObservaciones[giObservaciones].iMes, &gaudtObservaciones[giObservaciones].acCiudad);
 
	puts("Introducir valores - Introduzca MÁXIMA MÍNIMA PRECIPITACIONES SITUACIÓN:");
	scanf("%f %f %u %s", &gaudtObservaciones[giObservaciones].fMaxima, &gaudtObservaciones[giObservaciones].fMinima, &gaudtObservaciones[giObservaciones].iPrecipitaciones, &gaudtObservaciones[giObservaciones].acSituacion);
 
	giObservaciones++;
}

Me surge un inconveniente, y es que scanf, se considera una versión no segura, y Visual C++ lo reporta por defecto como un error. Propone que reemplacemos su llamada por las versiones seguras (scanf_s). Éstas no son tan portables, y me da a mi que usarlas, sería un cateo directo por parte del revisor. Tiramos de MSDN, y vemos que hay que definir _CRT_SECURE_NO_WARNINGS para que nos permita usarla. La incluyo en las opciones del preprocesador del IDE, y así nadie se asusta al ver defines “raros” en el código.

Ahora viene el punto que os adelantaba de repetitivo y monótono, hay que calcular tres medias diferentes (de temperatura máxima, de temperatura mínima, y de precipitaciones). Lo que hice fue escribir una única función, que dependiendo del parámetro que se le pasaba, hiciera la media de temperaturas máximas, de mínimas, o de precipitaciones, pero dado que gran parte del código es común, me ahorraba reescribir, y reprobar tres cosas que hacen lo mismo, pero sobre diferentes campos.

Quizás la lectura de la situación, la debería haber hecho independiente con gets, digamos que este caso, es un tanto particular, y dependerá de la implementación concreta que tenga scanf por parte de la librería de C del compilador, el que sea capaz de identificar los espacios como pertenecientes al último campo, o no.

La idea en todos los casos es la misma, para cada mes, recorremos todos los elementos globales, en busca de ese mes, y que corresponda a la ciudad que hemos seleccionado. La ciudad es sensible a mayúsculas y minúsculas, algo que podría haberse solventado usando simplemente stricmp en vez de strcmp, pero que como no se pide, he omitido:

void DoObtenerMedia (unsigned int piTipo)
{
	unsigned int iMes;
	unsigned int iObservacion;
	unsigned int iCuenta;
	float fSuma;
	char acCiudad[_MAX_PATH];
 
	if (piTipo == 0)
	{
		puts("Obtener Media Máxima - Introduzca CIUDAD:");
	}
	else if (piTipo == 1)
	{
		puts("Obtener Media Mínima - Introduzca CIUDAD:");
	}
	else
	{
		puts("Obtener Media Precipitaciones - Introduzca CIUDAD:");
	}
	scanf("%s", acCiudad);
 
	for (iMes = 1; iMes <= 12; iMes++)
	{
		fSuma = 0;
		iCuenta = 0;
 
		for (iObservacion = 0; iObservacion < giObservaciones; iObservacion++)
		{
			if ((iMes == gaudtObservaciones[iObservacion].iMes) && (strcmp(acCiudad, gaudtObservaciones[iObservacion].acCiudad) == 0))
			{
				if (piTipo == 0)
				{
					fSuma += gaudtObservaciones[iObservacion].fMaxima;
				}
				else if (piTipo == 1)
				{
					fSuma += gaudtObservaciones[iObservacion].fMinima;
				}
				else
				{
					fSuma += gaudtObservaciones[iObservacion].iPrecipitaciones;
				}
				iCuenta++;
			}
		}
		printf("Mes: %u: %f\n", iMes, fSuma / iCuenta);
	}
}

La solución está muy lejos de ser óptima, requiere recorrerse 12 veces (una por cada mes), la estructura. Pero consideré mejor primar la facilidad de implementación, y la rapidez de desarrollo. En el otro caso, es decir, unos datos indexados por día, mes y ciudad, habría que haber tenido especial cuidado identificando aquellos que estaban llenos, de aquellos que no.

Debo hacer notar, que no considero la división por cero un error. Es decir, si no hay datos para ese mes, se sacará por pantalla simplemente NaN, Inf, o como el compilador gestione estos casos.

El siguiente punto, es buscar las temperaturas máximas y los mínimas. Aquí la estructura de datos montada, es ideal. La recorremos entera, y buscamos el máximo sobre el campo de temperatura máxima, o el mínimo sobre el campo temperatura mínima. Me planteé que los datos pudieran no ser consistentes, y que la máxima fuera inferior a la mínima o viceversa, como hacen por ejemplo en AccuWeather, pero no codifiqué ese caso. La idea aquí, es que inicializo el valor máximo al entero corto mínimo con la constante SHRT_MIN, para asegurarme que en la primera iteración, ya obtiene el dato real. Normalmente esto se hace inicializándolo como el primer elemento del array, y empezar a procesar desde el segundo. Sería una solución más veloz, pero que exige controlar si el array tiene más de un elemento o no, y por tanto, aumenta la complejidad.

Nuevamente, hago una función, que calcule las dos cosas en función del parámetro. Como en las medias, de no hacerlo, tendríamos que escribir y probar 5 funciones diferentes, y un código que ahora son 200 lineas, podría aumentar a 400 o 500 fácilmente.

void DoObtenerPico (unsigned int piTipo)
{
	unsigned int iElem;
	float fValor;
	unsigned int iObservacion;
 
 
	if (piTipo == 0)
	{
		puts("Obtener más alta:");
	}
	else
	{
		puts("Obtener más baja:");
	}
 
	iElem = -1;
 
	if (piTipo == 0)
	{
		fValor = SHRT_MIN;
	}
	else
	{
		fValor = SHRT_MAX;
	}
 
	for (iObservacion = 0; iObservacion < giObservaciones; iObservacion++)
	{
		if ((piTipo == 0) && (gaudtObservaciones[iObservacion].fMaxima > fValor))
		{
			fValor = gaudtObservaciones[iObservacion].fMaxima;
			iElem = iObservacion;
		}
		else if (gaudtObservaciones[iObservacion].fMinima < fValor)
		{
			fValor = gaudtObservaciones[iObservacion].fMinima;
			iElem = iObservacion;
		}
	}
 
	if (piTipo == 0)
	{
		printf("Máxima - Día: %u Mes: %u: Ciudad: %s Máxima: %f Situación: %s\n", gaudtObservaciones[iElem].iDia, gaudtObservaciones[iElem].iMes, gaudtObservaciones[iElem].acCiudad, gaudtObservaciones[iElem].fMaxima, gaudtObservaciones[iElem].acSituacion);
	}
	else
	{
		printf("Mínima - Día: %u Mes: %u: Ciudad: %s Mínima: %f Situación: %s\n", gaudtObservaciones[iElem].iDia, gaudtObservaciones[iElem].iMes, gaudtObservaciones[iElem].acCiudad, gaudtObservaciones[iElem].fMaxima, gaudtObservaciones[iElem].acSituacion);
	}
}

Os habréis dado cuenta, que pese a que se pide guardar la situación, no se hace nada con ella, así que opté por mostrarla en 5) y 6) en esta tercera versión que publico aquí. Es absurdo solicitar al usuario una información, con la que luego no vamos a operar.

Ya sólo nos queda, un menú de opciones para que el usuario pueda acceder a las diferentes funciones del programa:

void DoMenu (void)
{
	char cTecla;
 
 
	/* Mientras no pulsemos 0 para salir */
	do
	{
		puts(
			"Escoja opción:\n"
			"1. Introducir valores\n"
			"2. Obtener media de temperaturas máximas para cada mes\n"
			"3. Obtener media de temperaturas mínimas para cada mes\n"
			"4. Obtener media de precipitaciones para cada mes\n"
			"5. Obtener día, mes y ciudad con la temperatura más alta\n"
			"6. Obtener día, mes y ciudad con la temperatura más baja\n"
			"0. Salir\n"
			"\n"
			);
 
		while (!isdigit(cTecla = getchar()));
 
		switch (cTecla)
		{
			case '1':
				DoIntroducirValores();
				break;
			case '2':
				DoObtenerMedia(0);
				break;
			case '3':
				DoObtenerMedia(1);
				break;
			case '4':
				DoObtenerMedia(2);
				break;
			case '5':
				DoObtenerPico(0);
				break;
			case '6':
				DoObtenerPico(1);
				break;
		}
	}
	while (cTecla != '0');
}

En general, y como no se habla de protección contra errores, veréis que aunque el código es sencillo y limpio, pero distando de ser elegante, es fácil hacerlo cascar. Si no se introduce dato alguno, tendremos errores de arrays fuera de índice a tutiplén.

En esta función, sólo aprecio dos puntos destacables. El primero es que aprovecho del concatenado de cadenas que hace C, para utilizar una sola llamada a puts con todas las lineas a sacar por pantalla al mismo tiempo. El segundo, es que me aseguro que la opción introducida sea un dígito, para descartar los \r (CR: Carriage Return) o \n (LF: Line Feed) que puede valer la tecla cuando pulsamos RETURN.

Se pide también entregar una memoria. Espero que hayan concretado que esperan en ella, porque estoy más que perdido. ¿Es una toma de requisitos, un análisis orgánico, una análisis funcional, una estimación de esfuerzo, un recopilatorio de anécdotas durante el desarrollo, o incluso algo cómo este post? Me sorprende que necesiten ese tipo de material, y en cambio no requieran que por ejemplo el código fuente a entregar esté bien comentado.

Al terminar, vuelvo a releer el enunciado. Muchas veces se redactan con el objetivo (maléfico), de pasar por alto algún detalle que luego resulta ser crítico, y así bajarnos la puntuación. Me doy cuenta que mientras el primer enunciado, claramente pide “Escribid un programa que…”, en el segundo, todo queda en el aire. Pues empieza con un “Supongamos que…”, para luego continuar con “Necesitamos un programa que permita…”. Vamos, que tampoco nos está exigiendo que ese programa lo escribamos nosotros. Tal vez valdría con una página escrita donde explicáramos que pasaríamos el enunciado a algún programador de software a medida, y que una vez abonados 200€, nos lo tendría terminado en un par de días.

Conclusiones

Si has llegado hasta aquí, enhorabuena, porque algo habrás aprendido sobre C, que te será muy útil. Pero si sólo andabas buscando un enlace, te lo dejo aquí (NIA_NIA_ej2.c 7 Kb.).

A excepción de que me he entretenido con el ejercicio, me parece incomprensible que incluso alguien que lleva más de 20 años programando en C, tenga tantas dudas, y que el enunciado sea tan inconsistente.

Os he hablado de tipos de números matemáticos, que son imposibles de modelar en un ordenador al 100%. Pero dista mucho de unos requerimientos. No se habla de unidades, y por ejemplo si las precipitaciones fueran en picolitros, las variables excederían de rango. Tampoco se nos habla de la temperatura, ¿hablamos de Celsius, Farenheit, o Kelvin por ejemplo?.

Se añade mucha retórica, como las provincias de Catalunya. ¿No sería suficiente decir que tenemos 4 observatorios en diferentes sitios? A efectos de codificación, nos da exactamente igual su ubicación geográfica. Por el contrario, no se detalla lo que se espera de nosotros. ¿Se valora que sea eficiente? ¿Qué sea compacto? ¿Qué sea sencillo? ¿Debe haber un control exhaustivo de errores? ¿En qué plataformas debería funcionar o compilar? ¿Debemos incluir unas instrucciones detalladas para el usuario sobre lo que hace el programa, en qué formato introducir los datos, y cómo devolverá los resultados? ¿O asumimos que éste ya tiene cierto conocimiento de la aplicación?

Incuestionablemente es un mal ejemplo de enunciado, que por ende puede causar malentendidos por el alumno que lo desarrolla, y también a la hora de evaluarlo. Porque si hablamos de fundamentos de la programación, las buenas enseñanzas, deben empezar en primer lugar por los enunciados. Y ahí si que tiene mucha culpa el profesor o el becario que lo ha redactado.

Además, si miramos los metadatos del PDF orignal, veremos que nos indica algo así:

Así que el título del documento es Mastermind. Muy poco relevante a lo que se nos pide. ¿Tan complicado era llamarlo Observatorios?. Es obvio que la plantilla de Word que han usado, está reutilizada de otras prácticas anteriores. En fin, que además de ser un buen profesor, hay que parecerlo, y esto es un gesto de dejadez, que por supuesto no incentivará la pulcritud de esos futuros programadores.

Desconozco la nota que pondrán, porque no queda nada claro qué valoran. El funcionamiento es el que se entiende por esperado, así que debería ser un 10, pero ante tal vaguedad, la nota podría ir en el rango de 5-10 sin ningún problema. Lo que si puedo decir, es que viendo este enunciado, al profesor/becario responsable, le pongo un 4 a lo sumo, y nos vemos en la convocatoria de septiembre.

Si se da la circunstancia que el/los responsables encuentra esta entrada con Google, les recomiendo también FileOptimizer, que ha conseguido reducir el peso del PDF de los originales 126 Kb., a menos de 43 Kb. Lo dejo también aquí (PracticaRec2TJuliol16.pdf 43 Kb.).

Por supuesto, les insto si lo desean a revisar el código fuente para actualizar conocimientos.