C01 · Analiza antes de programar: la tabla Entrada – Proceso – Salida

ENUNCIADO

Taquilla del cine. Un formulario envía por POST la edad (número), el dia (un select con los siete días de la semana) y carnet_joven (un checkbox). La entrada cuesta 9 €. Los miércoles cuesta 5,50 €. Los menores de 12 años y los mayores de 65 pagan la mitad. Con carnet joven se resta 1 €, salvo los miércoles. La página muestra el precio final y la lista de descuentos aplicados.

Paso 1. Entrada, Proceso y Salida.

Parte A · Distingue entre Entrada, Proceso y Salida.

Escribe en el cuaderno los números del 1 al 12 y, al lado de cada uno, una E, una P o una S:

  1. Comprobar que la edad es un entero entre 0 y 120.
  2. El precio final, con dos decimales y el símbolo €.
  3. El valor de dia que llega por POST.
  4. Restar 1 € si se ha marcado el carnet joven y no es miércoles.
  5. El precio general de 9 €.
  6. El mensaje «La edad debe ser un número entre 0 y 120».
  7. Comprobar que dia es uno de los siete días permitidos.
  8. El valor de carnet_joven, que solo llega si se marca la casilla.
  9. Aplicar el 50 % de descuento si la edad es menor que 12 o mayor que 65.
  10. La lista con los descuentos que se han aplicado.
  11. El valor de edad que llega por POST.
  12. Volver a mostrar el formulario con lo que escribió el usuario.

Antes de hacer la tabla: dos huecos del enunciado

Al subrayar el enunciado (paso 1) aparecen dos dudas que hay que resolver antes de programar:

  • ¿En qué orden se aplican el 50 % y el euro del carnet? No da lo mismo. Con 70 años y carnet joven, un viernes:
    • Si primero se aplica la mitad: 9 / 2 − 1 = 3,50 €.
    • Si primero se resta el euro: (9 − 1) / 2 = 4,00 €.
  • ¿El 50 % por edad se aplica también los miércoles? El enunciado solo excluye el carnet joven los miércoles,pero nososotros lo interpretamos que tampoco se aplica, así que la edad no cuenta: un niño paga 5,50 € los miercoles.

Decisiones:

  1. Primero se fija el precio del día, si es miercoles, se aplica el descuento y finalizamos .
  2. Si no es miercoles, se aplica el descuento por edad.
  3. Si no es miercoles, por último se resta el euro del carnet joven.
  4. La edad válida va de 0 a 120: un bebé tiene 0 años.

La tabla

Entrada (¿qué tengo?) Proceso (¿qué hago?) Salida (¿qué muestro?)
El método de la petición ($_SERVER['REQUEST_METHOD']): dice si se ha enviado el formulario Saber si se ha enviado el formulario El formulario persistente: la edad escrita, el día elegido y la casilla marcada
dia (POST): llega como cadena
  • Recoger dia
  • Comprobar el valor de dia
  • Si dia NO es miercoles, verificar edad.
  • Si dia es miercoles, finalizar.
  • Si dia es miercoles mostrar "Precio 5€ - Descuento por miércoles Aplicado".
edad (POST): llega como cadena
  • Recoger edad.
  • Validar: entero entre 0 y 120
  • Si edad<12 OR edad>65, aplicar 50%.
  • anotar descuento.
  • Ir a verificar carnet joven.
  • Mensaje de error, solo si hay error
  • Si edad<12 OR edad>65, mostrar "Descuento por edad Aplicado".
carnet_joven (POST): llega como cadena o no llega.
  • Comprobar si ha llegado carnet_joven
  • Restar 1 € al precio si existe carnet_joven.
  • anotar descuento.
  • Mostrar "Descuento por carnet joven Aplicado" si existe
Datos fijos: 9 €; 5,50 € los miércoles; límites de edad 12 y 65; 50 %; 1 € del carnet; lista de los 7 días; rango de edad 0–120 Si no hay errores, fijar el precio base: 5,50 € si es miércoles y 9 € si no Precio final de la entrada

Errores habituales al construir esta tabla

  • Poner acciones en la columna Salida. «Aplicar el 50 %» o «quitar 1 €» son proceso. En Salida solo va lo que se ve: el precio, la lista y los errores.
  • Olvidar los datos fijos. El 9 €, el 5,50 €, el 12 y el 65 se usan en los cálculos. Si no están en Entrada, acabarás escribiendo números «mágicos» en mitad del código.
  • Aplicar descuentos el miércoles. El miércoles tiene su propio precio y ahí termina el cálculo: no cuentan ni la edad ni el carnet.
  • Cambiar el orden de los descuentos. Primero va la edad y después el carnet. Al revés, el precio sale distinto.
  • Validar solo la edad. El select se puede manipular con F12, así que dia también se valida (patrón G).
  • Olvidar el formulario persistente. Es la salida 12 de la parte A.
  • Copiar mal los datos del enunciado. El precio de los miércoles es 5,50 €, no 5 €.
  • Confundir el botón con un dato. El botón de enviar no hace falta para saber si se ha enviado el formulario: se mira el método de la petición (patrón B).

Paso 3. Pasos para resolver

Rellena en el cuaderno los pasos a resolver tomando como referencia los valores .

Paso 4. Pseudocódigo y traza

4.1. Pseudocódigo

DATOS FIJOS
    precio_general ← 9          precio_miercoles ← 5,50
    edad_infantil ← 12          edad_mayor ← 65
    descuento_carnet ← 1
    dias ← lunes, martes, miércoles, jueves, viernes, sábado, domingo

errores ← lista vacía     precio ← nada     descuentos ← lista vacía

SI se ha enviado el formulario ENTONCES
    edad ← dato "edad" sin espacios al principio ni al final
    dia ← dato "dia"
    carnet ← ¿ha llegado "carnet_joven"?

    SI edad no es un entero entre 0 y 120 ENTONCES
        errores['edad'] ← "La edad debe ser un número entre 0 y 120."
    FIN SI
    SI dia no está en dias ENTONCES
        errores['dia'] ← "Elige un día de la semana."
    FIN SI

    SI errores está vacía ENTONCES
        SI dia = miércoles ENTONCES
            precio ← precio_miercoles
            añadir "Precio de miércoles (5,50 €)" a descuentos
        SI NO
            precio ← precio_general

            SI edad < edad_infantil O edad > edad_mayor ENTONCES
                precio ← precio / 2
                añadir "Descuento por edad (50 %)" a descuentos
            FIN SI

            SI carnet ENTONCES
                precio ← precio − descuento_carnet
                añadir "Carnet joven (−1 €)" a descuentos
            FIN SI
        FIN SI
    FIN SI
FIN SI

mostrar el formulario con edad, dia y carnet
SI hay errores ENTONCES mostrar cada error junto a su campo
SI hay precio ENTONCES mostrar el precio con 2 decimales y €, y la lista de descuentos

Fíjate en dos detalles:

  • Los descuentos están dentro del SI NO del miércoles. Por eso, si es miércoles, no se llega a comprobarlos y ya no hace falta preguntar «Y dia ≠ miércoles» en el carnet.
  • Dentro del SI NO, los dos SI van uno detrás de otro, no con SI NO, porque una persona puede tener los dos descuentos. El orden en que se escriben es el orden en que se aplican: primero la edad y después el carnet.

4.2. Traza

Sigue el pseudocódigo a mano y apunta el precio después de cada SI:

Caso edad dia carnet Precio base Tras la edad Tras el carnet Descuentos Se ve
Normal 30 lunes No 9 9 9 ninguno 9,00 €, «Sin descuentos»
Carnet en miércoles 30 miércoles Sí 5,50 — — miércoles 5,50 € (no se comprueba el carnet)
Niño en miércoles 8 miércoles No 5,50 — — miércoles 5,50 € (no se comprueba la edad)
Todo a la vez 70 viernes Sí 9 4,50 3,50 edad, carnet 3,50 €
Límite 12 martes Sí 9 9 8 carnet 8,00 € (12 no es menor que 12)
Límite 0 sábado No 9 4,50 4,50 edad 4,50 €
Erróneo abc lunes — — — — — Error en la edad
Erróneo 30 (vacío) — — — — — Error en el día

Si tu traza da estos resultados, el algoritmo está bien y solo queda traducirlo a PHP.


Paso 5. Programa poco a poco

Estos son los trozos en el orden en que se van a programar. Cada uno se prueba antes de pasar al siguiente:

  • Trozo 1. Los datos fijos y el formulario, con el select generado por un bucle.
  • Trozo 2. Saber si se ha enviado el formulario y recoger los datos.
  • Trozo 3. Validar la edad y el día.
  • Trozo 4. Calcular el precio y la lista de descuentos.
  • Trozo 5. Mostrar el formulario persistente, los errores y el resultado.

Trozo 1 · Datos fijos y formulario

Los datos fijos de la columna Entrada se escriben al principio, como constantes. Así, si mañana la entrada sube a 9,50 €, solo cambias una línea.

Los días van en un array asociativo:

  • la clave es el value que viaja en el formulario, sin tildes;
  • el valor es el texto que ve el usuario.

Con ese array se generan las <option> con un foreach, en lugar de escribirlas a mano.

<?php
// ── Datos fijos ──────────────────────────────────────────────
const PRECIO_GENERAL   = 9.00;
const PRECIO_MIERCOLES = 5.50;
const DESCUENTO_CARNET = 1.00;
const EDAD_INFANTIL    = 12;   // los menores de 12 pagan la mitad
const EDAD_MAYOR       = 65;   // los mayores de 65 pagan la mitad

// Días permitidos: clave = value del formulario, valor = texto visible
$dias = [
    'lunes'     => 'Lunes',
    'martes'    => 'Martes',
    'miercoles' => 'Miércoles',
    'jueves'    => 'Jueves',
    'viernes'   => 'Viernes',
    'sabado'    => 'Sábado',
    'domingo'   => 'Domingo',
];
?>
<form method="post">
    <input type="text" name="edad">
    <select name="dia">
        <option value="">Elige un día</option>
        <?php foreach ($dias as $valor => $nombre): ?>
            <option value="<?= $valor ?>"><?= $nombre ?></option>
        <?php endforeach; ?>
    </select>
    <label><input type="checkbox" name="carnet_joven" value="si"> Tengo carnet joven</label>
    <button type="submit">Calcular precio</button>
</form>

Abre el inspector (F12) y comprueba que hay 8 <option>: la vacía y los 7 días, cada uno con su value sin tilde.

git add ut03c01.php
git commit -m "C01: datos fijos y formulario de la taquilla"

Trozo 2 · ¿Se ha enviado? Recoger los datos

Entre los datos fijos y el ?>, añade la zona de lógica (patrón A). Empieza por las variables que usará el HTML, con los valores de la primera visita:

// ── Variables que usa el HTML (valores de la primera visita) ──
$edadTexto  = '';      // lo que escribió el usuario
$dia        = '';      // el día elegido
$carnet     = false;   // ¿se marcó la casilla?
$errores    = [];      // errores por campo
$precio     = null;    // precio final, si todo es válido
$descuentos = [];      // textos de los descuentos aplicados

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $edadTexto = trim($_POST['edad'] ?? '');
    $dia       = $_POST['dia'] ?? '';
    $carnet    = isset($_POST['carnet_joven']);

    echo '<pre>';                         // depuración provisional
    var_dump($_POST, $carnet);
    echo '</pre>';
}

Envía 70, Viernes y la casilla marcada. Después repite sin marcar la casilla:

array(3) {                          array(2) {
  ["edad"]=> string(2) "70"            ["edad"]=> string(2) "70"
  ["dia"]=> string(7) "viernes"        ["dia"]=> string(7) "viernes"
  ["carnet_joven"]=> string(2) "si"  }
}                                   bool(false)
bool(true)

Con la casilla sin marcar, la clave carnet_joven ni siquiera aparece en $_POST. Por eso no se pregunta por su valor, sino si existe, con isset(). Fíjate también en que la edad llega como string, aunque escribas un número.

Trozo 3 · Validar

Sustituye el var_dump() por la validación. Cada error se guarda con el nombre de su campo como clave (patrón C), para mostrarlo después debajo de ese campo:

    // Validar la edad: entero entre 0 y 120
    $edad = filter_var($edadTexto, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 0, 'max_range' => 120],
    ]);
    if ($edad === false) {
        $errores['edad'] = 'La edad debe ser un número entre 0 y 120.';
    }

    // Validar el día con lista blanca (patrón G)
    if (!array_key_exists($dia, $dias)) {
        $errores['dia'] = 'Elige un día de la semana.';
    }

Pruébalo con un var_dump($edad, $errores) provisional:

Escribes $edad $errores
30, Lunes int(30) vacío
0, Lunes int(0) vacío
abc, Lunes bool(false) clave edad
121, Lunes bool(false) clave edad
30, Elige un día int(30) clave dia
30 y, con F12, cambias un value a festivo int(30) clave dia

Aquí === false es imprescindible

Un bebé tiene 0 años y filter_var() devuelve int(0), que es válido. Si escribieras if (!$edad), el 0 se evaluaría como falso y la página diría que la edad no es válida.

Trozo 4 · Calcular

Debajo de la validación, y solo si no hay errores, se traduce el cálculo del pseudocódigo con la misma estructura: un if … else para el miércoles y, dentro del else, dos if seguidos:

    // Calcular el precio solo si todo es válido
    if (empty($errores)) {
        if ($dia === 'miercoles') {
            // Miércoles: precio especial y ningún descuento más
            $precio       = PRECIO_MIERCOLES;
            $descuentos[] = 'Precio de miércoles (5,50 €)';
        } else {
            // Resto de días: precio general y descuentos en orden
            $precio = PRECIO_GENERAL;

            // 1. Descuento por edad
            if ($edad < EDAD_INFANTIL || $edad > EDAD_MAYOR) {
                $precio       = $precio / 2;
                $descuentos[] = 'Descuento por edad (50 %)';
            }

            // 2. Carnet joven, después del descuento por edad
            if ($carnet) {
                $precio      -= DESCUENTO_CARNET;
                $descuentos[] = 'Carnet joven (−1 €)';
            }
        }
    }

Los comentarios // 1. y // 2. recuerdan que el orden de los dos if no se puede cambiar: si los intercambias, el caso de 70 años con carnet da 4,00 € en lugar de 3,50 €.

Comprueba con un var_dump($precio, $descuentos) provisional los casos de la traza. Por ejemplo, con 70, Viernes y carnet:

float(3.5)
array(2) {
  [0]=> string(25) "Descuento por edad (50 %)"
  [1]=> string(23) "Carnet joven (−1 €)"
}

Si cada caso coincide con la traza del paso 4, borra el var_dump() y guarda:

git commit -am "C01: validación y cálculo del precio"

Trozo 5 · Mostrar

La zona de presentación solo muestra lo que ya está calculado:

  • Edad persistente (patrones D y E): value con $edadTexto, escapado con htmlspecialchars().
  • Día persistente: dentro del foreach, se añade selected a la opción cuyo value coincide con $dia.
  • Casilla persistente: se añade checked si $carnet es true.
  • Errores: cada campo pregunta si existe su clave en $errores. Si existe, se pone en rojo (is-danger) y muestra el mensaje debajo.
  • Resultado: solo si $precio no es null. El precio se formatea con number_format() usando coma decimal. Esto es dar formato al dato para mostrarlo, no calcular. Si no hay descuentos, se muestra «Sin descuentos».
git commit -am "C01: formulario persistente, errores y resultado"

Vuelve a pasar todos los casos de la traza en el navegador y comprueba que coinciden.


Código completo

ut03c01.php
<?php
/**
 * C01 · Taquilla del cine
 * Página autoprocesada: calcula el precio de la entrada según el día,
 * la edad y el carnet joven, y muestra los descuentos aplicados.
 */

// ── Datos fijos ──────────────────────────────────────────────
const PRECIO_GENERAL   = 9.00;
const PRECIO_MIERCOLES = 5.50;
const DESCUENTO_CARNET = 1.00;
const EDAD_INFANTIL    = 12;   // los menores de 12 pagan la mitad
const EDAD_MAYOR       = 65;   // los mayores de 65 pagan la mitad

// Días permitidos: clave = value del formulario, valor = texto visible
$dias = [
    'lunes'     => 'Lunes',
    'martes'    => 'Martes',
    'miercoles' => 'Miércoles',
    'jueves'    => 'Jueves',
    'viernes'   => 'Viernes',
    'sabado'    => 'Sábado',
    'domingo'   => 'Domingo',
];

// ── Variables que usa el HTML (valores de la primera visita) ──
$edadTexto  = '';      // lo que escribió el usuario
$dia        = '';      // el día elegido
$carnet     = false;   // ¿se marcó la casilla?
$errores    = [];      // errores por campo
$precio     = null;    // precio final, si todo es válido
$descuentos = [];      // textos de los descuentos aplicados

// ── Zona de lógica ───────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Recoger los datos
    $edadTexto = trim($_POST['edad'] ?? '');
    $dia       = $_POST['dia'] ?? '';
    $carnet    = isset($_POST['carnet_joven']);   // el checkbox solo llega si se marca

    // Validar la edad: entero entre 0 y 120
    $edad = filter_var($edadTexto, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 0, 'max_range' => 120],
    ]);
    if ($edad === false) {
        $errores['edad'] = 'La edad debe ser un número entre 0 y 120.';
    }

    // Validar el día con lista blanca
    if (!array_key_exists($dia, $dias)) {
        $errores['dia'] = 'Elige un día de la semana.';
    }

    // Calcular el precio solo si todo es válido
    if (empty($errores)) {
        if ($dia === 'miercoles') {
            // Miércoles: precio especial y ningún descuento más
            $precio       = PRECIO_MIERCOLES;
            $descuentos[] = 'Precio de miércoles (5,50 €)';
        } else {
            // Resto de días: precio general y descuentos en orden
            $precio = PRECIO_GENERAL;

            // 1. Descuento por edad
            if ($edad < EDAD_INFANTIL || $edad > EDAD_MAYOR) {
                $precio       = $precio / 2;
                $descuentos[] = 'Descuento por edad (50 %)';
            }

            // 2. Carnet joven, después del descuento por edad
            if ($carnet) {
                $precio      -= DESCUENTO_CARNET;
                $descuentos[] = 'Carnet joven (−1 €)';
            }
        }
    }
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>C01 · Taquilla del cine</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
</head>
<body>
<section class="section">
    <div class="container" style="max-width: 32rem;">
        <h1 class="title">Taquilla del cine</h1>

        <!-- Sin "action": el formulario se envía a esta misma página -->
        <form method="post">
            <div class="field">
                <label class="label" for="edad">Edad</label>
                <div class="control">
                    <input class="input<?= isset($errores['edad']) ? ' is-danger' : '' ?>"
                           type="text" id="edad" name="edad" inputmode="numeric"
                           value="<?= htmlspecialchars($edadTexto) ?>">
                </div>
                <?php if (isset($errores['edad'])): ?>
                    <p class="help is-danger"><?= $errores['edad'] ?></p>
                <?php endif; ?>
            </div>

            <div class="field">
                <label class="label" for="dia">Día</label>
                <div class="control">
                    <div class="select<?= isset($errores['dia']) ? ' is-danger' : '' ?>">
                        <select id="dia" name="dia">
                            <option value="">Elige un día</option>
                            <?php foreach ($dias as $valor => $nombre): ?>
                                <option value="<?= $valor ?>"<?= $valor === $dia ? ' selected' : '' ?>><?= $nombre ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                </div>
                <?php if (isset($errores['dia'])): ?>
                    <p class="help is-danger"><?= $errores['dia'] ?></p>
                <?php endif; ?>
            </div>

            <div class="field">
                <label class="checkbox">
                    <input type="checkbox" name="carnet_joven" value="si"<?= $carnet ? ' checked' : '' ?>>
                    Tengo carnet joven
                </label>
            </div>

            <div class="field">
                <button type="submit" class="button is-primary">Calcular precio</button>
            </div>
        </form>

        <?php if ($precio !== null): ?>
            <div class="notification is-success content mt-5">
                <p>Precio final: <strong><?= number_format($precio, 2, ',', '.') ?> €</strong></p>
                <?php if (empty($descuentos)): ?>
                    <p>Sin descuentos.</p>
                <?php else: ?>
                    <p>Descuentos aplicados:</p>
                    <ul>
                        <?php foreach ($descuentos as $descuento): ?>
                            <li><?= $descuento ?></li>
                        <?php endforeach; ?>
                    </ul>
                <?php endif; ?>
            </div>
        <?php endif; ?>
    </div>
</section>
</body>
</html>

Los errores, los nombres de los días y los textos de los descuentos se muestran sin htmlspecialchars() porque los escribimos nosotros en el código. $edadTexto sí viene del usuario, y por eso se escapa.

$dia también viene del usuario, pero nunca se muestra directamente: solo se compara con las claves de $dias.