Problema real: de la idea al algoritmo

En esta página
Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
Un lavadero de autos de barrio recibe, todos los sábados a la mañana, una fila de clientes que van llegando de a uno, sin turno previo. Se atiende siempre al que llegó primero, se cobra según el auto y el tipo de lavado, y al final del día alguien suma cuánto entró en total. Nada de eso está escrito en ningún lado: es la forma en que ya funciona el lavadero, mucho antes de que a alguien se le ocurra programarlo.
El pedido, tal como lo daría el dueño del lavadero, dice más o menos esto: "necesito un sistema que anote los turnos que van llegando, cobre según el tipo de lavado, y lleve la cuenta de cuánto se facturó en el día. A los clientes frecuentes les quiero hacer un descuento". Un enunciado así no dice todavía en qué orden se atiende, ni qué cuenta como cliente frecuente, ni si el precio depende de algo más que el tipo de lavado. Este post no presenta ninguna herramienta nueva: integra varias de las ya vistas en los bloques 1 a 3 -estructuras, funciones puras y efectos colaterales, composición y recursión- para llegar de ese pedido incompleto a un algoritmo completo, tomando en el camino las decisiones de diseño que el enunciado deja abiertas.
El pedido no dice en qué orden se atiende a los clientes. Como llegan sin turno previo, la única regla razonable -y la más común en cualquier fila de la vida diaria- es atender al que llegó primero, mismo criterio que ya formalizó pilascolas (post 12) con la cola.
El pedido tampoco dice qué es un cliente frecuente. "Frecuente" no es un dato que el enunciado traiga: es una decisión de diseño que alguien tiene que tomar antes de escribir una sola línea de algoritmo. Este post fija un umbral concreto -tres turnos ya registrados a nombre de esa persona- y lo deja explícito, en vez de esconderlo dentro de una condición sin explicar de dónde salió.
El pedido dice que se cobra "según el tipo de lavado", pero no menciona el vehículo. En la práctica, lavar una camioneta lleva más tiempo y más agua que lavar un auto chico, así que el precio también depende de eso: es información que el pedido no pidió, pero que un sistema real no puede ignorar. Y por último, "llevar la cuenta de cuánto se facturó en el día" admite dos lecturas distintas: sumar cada turno a medida que se cobra, o calcular el total de los turnos que todavía quedan pendientes en algún momento del día. Las dos son válidas, y este post termina usando las dos, cada una donde tiene sentido.
Antes de escribir el primer algoritmo conviene decidir qué herramienta resuelve cada parte. Los turnos que van llegando y se atienden en orden de llegada son una cola. Cuántos turnos previos tiene cada cliente es una asociación entre un nombre y un número: un diccionario. El precio de un turno depende de dos datos independientes -el tipo de lavado y el tipo de vehículo- y conviene calcularlo en dos pasos separados, componiendo el resultado de uno con el otro. Si a ese precio corresponde aplicarle un descuento es una decisión aparte, que tampoco tiene por qué mezclarse con el cálculo del precio base. Y calcular cuánto queda por facturar en una cola de turnos pendientes, sin usar ningún bucle, es exactamente el problema que ya resolvió recursion (post 17) para una cantidad numérica, aplicado ahora a una estructura completa en vez de a un número.
El precio base depende solo del tipo de lavado.
algoritmo calcular_precio_base(tipo_lavado):
si tipo_lavado es igual a "rápido":
devolver 3000
devolver 5000El recargo por vehículo se calcula aparte, sobre el precio que ya vino de la función anterior.
algoritmo aplicar_recargo_vehiculo(precio, tipo_vehiculo):
si tipo_vehiculo es igual a "camioneta":
sumar 1500 a precio
devolver precioNinguna de las dos funciones toca nada que no sea su propio parámetro: las dos son funciones puras, en el sentido que fijó efectos (post 15). El precio final de un turno se obtiene componiéndolas, exactamente como formalizó composicion (post 16): el resultado de la primera entra directo como parámetro de la segunda.
algoritmo calcular_precio_turno(tipo_lavado, tipo_vehiculo):
llamar a calcular_precio_base con tipo_lavado para obtener precio
llamar a aplicar_recargo_vehiculo con precio y tipo_vehiculo para obtener precio_final
devolver precio_finalUn lavado rápido de una camioneta (calcular_precio_turno("rápido", "camioneta")) da 3000 más 1500, o sea 4500. Un lavado completo de un auto (calcular_precio_turno("completo", "auto")) da 5000, sin ningún recargo.
Cuántos turnos previos tiene cada cliente se guarda en un diccionario, con el nombre como clave y la cantidad como valor: {"Marcos": 3, "Ana": 1} dice que Marcos ya tuvo tres turnos y Ana solo uno. Decidir si corresponde el descuento es una función aparte, separada del cálculo del precio.
algoritmo aplica_descuento(cliente, turnos_previos):
si turnos_previos contiene la clave cliente:
si el valor asociado a la clave cliente en turnos_previos es mayor o igual a 3:
devolver verdadero
devolver falsoLa primera condición evita buscar la cantidad de turnos de un cliente que todavía no tiene ninguno registrado. Con el umbral ya fijado en tres, el precio final compone las tres funciones anteriores en una sola.
algoritmo calcular_precio_final(cliente, tipo_lavado, tipo_vehiculo, turnos_previos):
llamar a calcular_precio_turno con tipo_lavado y tipo_vehiculo para obtener precio
llamar a aplica_descuento con cliente y turnos_previos para obtener corresponde_descuento
si corresponde_descuento:
multiplicar precio por 0.9
devolver precioCon Marcos (tres turnos previos, lavado completo, auto), el precio base es 5000, sin recargo de vehículo, y con el descuento aplicado (multiplicar por 0.9) queda en 4500. Con Ana (un turno previo, lavado rápido, camioneta), el precio es 3000 más 1500 de recargo, 4500, pero sin descuento: no llega al umbral, así que el precio final también queda en 4500, por una razón completamente distinta a la de Marcos.
Cuando un turno se atiende de verdad, el monto cobrado se suma a un total que existe afuera de cualquier función: una variable total_facturado, declarada antes de empezar el día. Registrar un cobro es, a propósito, un efecto colateral, en el mismo sentido que ya fijó efectos (post 15): la función no devuelve nada, solo modifica algo que ya existía antes de que se la llamara.
algoritmo registrar_cobro(monto):
sumar monto a total_facturadoPero a veces hace falta responder una pregunta distinta, sin cobrar nada todavía: si el lavadero cerrara ahora mismo, ¿cuánto falta facturar de los turnos que quedaron en la cola, sin atender? Esa pregunta no debería tocar total_facturado -es una proyección, no un cobro real-, así que se resuelve con una función pura y recursiva que recorre la cola sin ningún bucle.
algoritmo facturar_turnos_pendientes(cola, turnos_previos):
si la cantidad de elementos de cola es igual a 0:
devolver 0
asignar el resultado de desencolar cola a turno
asignar el valor asociado a la clave "cliente" en turno a cliente
asignar el valor asociado a la clave "lavado" en turno a tipo_lavado
asignar el valor asociado a la clave "vehiculo" en turno a tipo_vehiculo
llamar a calcular_precio_final con cliente, tipo_lavado, tipo_vehiculo y turnos_previos para obtener precio_turno
llamar a facturar_turnos_pendientes con cola y turnos_previos para obtener parcial
sumar precio_turno a parcial
devolver parcialEl caso base es la cola vacía, que no tiene nada más por facturar. Cada llamada saca un turno de adelante de la cola, calcula su precio con las mismas funciones ya definidas, y le suma lo que la llamada siguiente calcule sobre lo que queda -el mismo patrón que ya usó suma_recursiva en el post 17, aplicado ahora sobre una cola de turnos en vez de sobre un número que se achica de a uno. Las dos formas de llevar la cuenta -el efecto colateral que suma en tiempo real y la función pura que proyecta lo pendiente- conviven en el mismo programa sin ningún conflicto, la misma idea con la que ya cerraba efectos (post 15).
Con tres turnos en la cola -Marcos con un lavado completo de auto, Ana con un lavado rápido de camioneta, y Marcos otra vez con un lavado rápido de auto- y el mismo diccionario de turnos previos de arriba, facturar todo lo pendiente antes de que se atienda un solo cliente da 4500 más 4500 más 2700, es decir 11700. El tercer turno, el segundo de Marcos en el mismo día, también tiene descuento: su precio base sin recargo es 3000, y con el 10% aplicado queda en 2700.
algoritmo dia_en_el_lavadero(cola, turnos_previos):
asignar el resultado de facturar_turnos_pendientes con cola y turnos_previos a total_proyectado
devolver total_proyectadodia_en_el_lavadero no agrega ninguna herramienta nueva: solo le pone un nombre al problema completo, componiendo la recursión sobre la cola con las funciones de precio y descuento ya definidas, exactamente como plan_mudanza (post 7) le puso nombre al problema completo de una mudanza llamando a sus cuatro tramos por separado.
Un problema real casi nunca llega con un enunciado completo. Leerlo bien -separar lo que dice de lo que falta- es tan parte de resolverlo como escribir el algoritmo, y casi siempre la solución no necesita ninguna herramienta nueva: necesita combinar, con criterio, las que ya se tienen.
El próximo bloque de la serie deja de mirar cómo se resuelve un problema para empezar a preguntarse qué tan bien se resuelve, comparando algoritmos distintos para un mismo problema.