Apuntes DAM
Volver al inicio

Reservas de pistas de pádel con clases

Ejercicio de JavaScriptDifícilUnos 70 minutos

El motor de reservas de un club de pádel: tres pistas, nueve turnos de hora y media, precios de hora valle y punta, descuento para socios, límite de reservas por persona, cancelaciones y un resumen de ocupación. Clases con campos privados, Map, Set y los métodos filter, map, some y reduce.

  • Clases y campos privados (#)
  • Map y Set
  • filter, map, some, reduce
  • Destructuring y rest
  • switch
  • Plantillas de texto

Enunciado

Un club de pádel quiere una web para reservar pistas desde el móvil. Antes de hacer la interfaz, el equipo escribe el «motor» de reservas como una clase que no sabe nada del HTML: recibe órdenes y devuelve textos. Así se puede probar en la consola con Node.js y después usarla tal cual en la página.

El club tiene 3 pistas y nueve turnos de 90 minutos cada día, de las 09:00 a las 22:30. Por la mañana y a primera hora de la tarde (turnos de las 09:00 a las 15:00) es hora valle y la pista cuesta 12 €; desde las 16:30, hora punta, 18 €. Los socios tienen un 30 % de descuento, y para que todo el mundo pueda jugar nadie puede tener más de dos reservas a la vez.

Qué tiene que hacer el programa

  1. Cada línea es una orden; la palabra de la orden puede ir en mayúsculas o minúsculas. Las líneas vacías se ignoran y una orden desconocida escribe Orden desconocida: palabra.
  2. SOCIOS Ana,Luis Mora añade socios (nombres separados por comas, sin espacios alrededor) y escribe Socios: N con el total de socios.
  3. RESERVA pista hora nombre (el nombre puede tener espacios) comprueba, en este orden: que la pista sea 1, 2 o 3 (Pista no válida: X), que la hora sea uno de los turnos (Hora no válida: X), que la pista esté libre a esa hora (La pista P ya está reservada a las HH:MM) y que esa persona no tenga ya 2 reservas (Nombre ya tiene 2 reservas). Si todo va bien, la reserva recibe un número (1, 2, 3… sin reutilizar los cancelados) y se escribe Reserva N: pista P a las HH:MM para Nombre (X €).
  4. CANCELA n escribe Reserva n cancelada y libera la pista, o No existe la reserva n.
  5. LIBRES pista escribe Pista P libre a las 09:00, 10:30, … con sus turnos libres en orden, o Pista P sin horas libres, o Pista no válida: X.
  6. RESUMEN escribe una línea por pista, Pista P: R de 9 horas (Q %) con el porcentaje redondeado al entero; Recaudación prevista: X € con la suma de las reservas vigentes; y Mejor cliente: Nombre (N reservas) (la persona con más reservas vigentes; si empatan, la primera alfabéticamente; «reserva» si es 1), o Mejor cliente: ninguno.
  7. Los importes se escriben con dos decimales y coma decimal: 12,60 €.

Entrada

Una orden por línea: SOCIOS, RESERVA, CANCELA, LIBRES o RESUMEN.

Datos de referencia

Turnos y precios
TurnosTipoPrecioSocios (−30 %)
09:00, 10:30, 12:00, 13:30, 15:00valle12,00 €8,40 €
16:30, 18:00, 19:30, 21:00punta18,00 €12,60 €

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Una mañana de reservas

Entrada

SOCIOS Ana,Luis Mora
RESERVA 2 18:00 Ana
RESERVA 1 09:00 Pedro Gil
RESERVA 2 18:00 Pedro Gil
RESERVA 3 10:30 Ana
RESERVA 1 12:00 Ana
RESERVA 4 12:00 Eva
RESERVA 1 11:00 Eva
CANCELA 2
CANCELA 9
LIBRES 1
RESERVA 1 21:00 Luis Mora
RESUMEN

Salida por consola

Socios: 2
Reserva 1: pista 2 a las 18:00 para Ana (12,60 €)
Reserva 2: pista 1 a las 09:00 para Pedro Gil (12,00 €)
La pista 2 ya está reservada a las 18:00
Reserva 3: pista 3 a las 10:30 para Ana (8,40 €)
Ana ya tiene 2 reservas
Pista no válida: 4
Hora no válida: 11:00
Reserva 2 cancelada
No existe la reserva 9
Pista 1 libre a las 09:00, 10:30, 12:00, 13:30, 15:00, 16:30, 18:00, 19:30, 21:00
Reserva 4: pista 1 a las 21:00 para Luis Mora (12,60 €)
Pista 1: 1 de 9 horas (11 %)
Pista 2: 1 de 9 horas (11 %)
Pista 3: 1 de 9 horas (11 %)
Recaudación prevista: 33,60 €
Mejor cliente: Ana (2 reservas)

Órdenes en minúsculas

Entrada

reserva 3 15:00 Marta
libres 3
resumen
borrar 1

Salida por consola

Reserva 1: pista 3 a las 15:00 para Marta (12,00 €)
Pista 3 libre a las 09:00, 10:30, 12:00, 13:30, 16:30, 18:00, 19:30, 21:00
Pista 1: 0 de 9 horas (0 %)
Pista 2: 0 de 9 horas (0 %)
Pista 3: 1 de 9 horas (11 %)
Recaudación prevista: 12,00 €
Mejor cliente: Marta (1 reserva)
Orden desconocida: borrar

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Una clase que devuelve textos

Haz que cada método de Club devuelva el texto que hay que escribir en lugar de escribirlo él. El bucle principal hace console.log(club.reservar(...)). Así la misma clase serviría en una página web, escribiendo el texto en el DOM.

2. Campos privados

Con #reservas y #socios, nadie desde fuera de la clase puede modificar las reservas sin pasar por sus reglas. Un Map id → reserva permite cancelar al instante con delete, que además devuelve si existía.

javascript
class Club {
  #reservas = new Map();
  #siguiente = 1;
  #socios = new Set();
  ...
}
3. Preguntas con métodos de arrays

Convierte las reservas en array con [...this.#reservas.values()] y pregúntale: some para saber si una pista está ocupada a una hora, filter(...).length para contar las reservas de una persona y reduce para sumar la recaudación.

javascript
this.#lista().some((r) => r.pista === pista && r.hora === hora)
4. Separar la orden de sus argumentos

const [orden, ...resto] = texto.split(/\s+/) deja la orden en una variable y el resto de palabras en un array. El nombre de la reserva son las palabras desde la tercera: resto.slice(2).join(" ").

5. El mejor cliente

Cuenta las reservas por persona con reduce sobre un Map, pásalo a array y ordénalo por número descendente y, a igualdad, por nombre. El primero del array es el ganador.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

🟨JavaScriptReservas de pistas de pádel con clasesDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
SOCIOS Ana,Luis Mora
RESERVA 2 18:00 Ana
RESERVA 1 09:00 Pedro Gil
RESERVA 2 18:00 Pedro Gil
RESERVA 3 10:30 Ana
RESERVA 1 12:00 Ana
RESERVA 4 12:00 Eva
RESERVA 1 11:00 Eva
CANCELA 2
CANCELA 9
LIBRES 1
RESERVA 1 21:00 Luis Mora
RESUMEN
Salida esperada
Socios: 2
Reserva 1: pista 2 a las 18:00 para Ana (12,60 €)
Reserva 2: pista 1 a las 09:00 para Pedro Gil (12,00 €)
La pista 2 ya está reservada a las 18:00
Reserva 3: pista 3 a las 10:30 para Ana (8,40 €)
Ana ya tiene 2 reservas
Pista no válida: 4
Hora no válida: 11:00
Reserva 2 cancelada
No existe la reserva 9
Pista 1 libre a las 09:00, 10:30, 12:00, 13:30, 15:00, 16:30, 18:00, 19:30, 21:00
Reserva 4: pista 1 a las 21:00 para Luis Mora (12,60 €)
Pista 1: 1 de 9 horas (11 %)
Pista 2: 1 de 9 horas (11 %)
Pista 3: 1 de 9 horas (11 %)
Recaudación prevista: 33,60 €
Mejor cliente: Ana (2 reservas)
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
javascript
1const lineas = require("fs").readFileSync(0, "utf8").split("\n");
2
3const HORAS = ["09:00", "10:30", "12:00", "13:30", "15:00", "16:30", "18:00", "19:30", "21:00"];
4const PISTAS = 3;
5
6const euros = (centimos) => `${(centimos / 100).toFixed(2).replace(".", ",")} €`;
7
8class Reserva {
9  constructor(id, pista, hora, nombre, precio) {
10    this.id = id;
11    this.pista = pista;
12    this.hora = hora;
13    this.nombre = nombre;
14    this.precio = precio;
15  }
16}
17
18class Club {
19  #reservas = new Map();   // id → Reserva
20  #siguiente = 1;
21  #socios = new Set();
22
23  anadirSocios(nombres) {
24    nombres.forEach((n) => this.#socios.add(n));
25    return `Socios: ${this.#socios.size}`;
26  }
27
28  #lista() {
29    return [...this.#reservas.values()];
30  }
31
32  reservar(pista, hora, nombre) {
33    if (!Number.isInteger(pista) || pista < 1 || pista > PISTAS) return `Pista no válida: ${pista}`;
34    if (!HORAS.includes(hora)) return `Hora no válida: ${hora}`;
35    if (this.#lista().some((r) => r.pista === pista && r.hora === hora)) return `La pista ${pista} ya está reservada a las ${hora}`;
36    if (this.#lista().filter((r) => r.nombre === nombre).length >= 2) return `${nombre} ya tiene 2 reservas`;
37
38    // Hora valle hasta las 15:00 (12 €) y punta desde las 16:30 (18 €); los socios pagan un 30 % menos
39    const base = HORAS.indexOf(hora) < 5 ? 1200 : 1800;
40    const precio = this.#socios.has(nombre) ? (base * 70) / 100 : base;
41    const reserva = new Reserva(this.#siguiente++, pista, hora, nombre, precio);
42    this.#reservas.set(reserva.id, reserva);
43    return `Reserva ${reserva.id}: pista ${pista} a las ${hora} para ${nombre} (${euros(precio)})`;
44  }
45
46  cancelar(id) {
47    return this.#reservas.delete(id) ? `Reserva ${id} cancelada` : `No existe la reserva ${id}`;
48  }
49
50  libres(pista) {
51    if (!Number.isInteger(pista) || pista < 1 || pista > PISTAS) return `Pista no válida: ${pista}`;
52    const ocupadas = this.#lista().filter((r) => r.pista === pista).map((r) => r.hora);
53    const libres = HORAS.filter((h) => !ocupadas.includes(h));
54    return libres.length ? `Pista ${pista} libre a las ${libres.join(", ")}` : `Pista ${pista} sin horas libres`;
55  }
56
57  resumen() {
58    const salida = [];
59    for (let p = 1; p <= PISTAS; p++) {
60      const n = this.#lista().filter((r) => r.pista === p).length;
61      salida.push(`Pista ${p}: ${n} de ${HORAS.length} horas (${Math.round((n * 100) / HORAS.length)} %)`);
62    }
63    salida.push(`Recaudación prevista: ${euros(this.#lista().reduce((suma, r) => suma + r.precio, 0))}`);
64
65    const porCliente = this.#lista().reduce((mapa, r) => mapa.set(r.nombre, (mapa.get(r.nombre) ?? 0) + 1), new Map());
66    const [mejor] = [...porCliente].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
67    salida.push(mejor ? `Mejor cliente: ${mejor[0]} (${mejor[1]} ${mejor[1] === 1 ? "reserva" : "reservas"})` : "Mejor cliente: ninguno");
68    return salida.join("\n");
69  }
70}
71
72const club = new Club();
73for (const linea of lineas) {
74  const texto = linea.trim();
75  if (!texto) continue;
76  const [orden, ...resto] = texto.split(/\s+/);
77  switch (orden.toUpperCase()) {
78    case "SOCIOS":
79      console.log(club.anadirSocios(resto.join(" ").split(",").map((n) => n.trim()).filter(Boolean)));
80      break;
81    case "RESERVA":
82      console.log(club.reservar(Number(resto[0]), resto[1] ?? "", resto.slice(2).join(" ")));
83      break;
84    case "CANCELA":
85      console.log(club.cancelar(Number(resto[0])));
86      break;
87    case "LIBRES":
88      console.log(club.libres(Number(resto[0])));
89      break;
90    case "RESUMEN":
91      console.log(club.resumen());
92      break;
93    default:
94      console.log(`Orden desconocida: ${orden}`);
95  }
96}

La clase Club encapsula las reglas del negocio: los campos privados impiden saltárselas y cada método es una operación con sentido para el club (reservar, cancelar, consultar). El programa principal solo traduce líneas de texto en llamadas.

Los métodos de arrays expresan cada pregunta en una línea: some (¿hay alguna?), filter + length (¿cuántas?), map (dame solo las horas), reduce (súmalas). Encadenarlos sobre la lista de reservas evita bucles con contadores y banderas.

Los identificadores se generan con un contador que nunca retrocede: si se reutilizaran los números de las reservas canceladas, un cliente con el justificante antiguo podría cancelar la reserva de otro.

El precio se calcula en céntimos y solo se formatea al escribirlo. (base * 70) / 100 da 840 y 1260, cantidades exactas: no aparece ningún 8.399999 por el camino.

Para ir más allá

  • Admite reservas en varios días (RESERVA 2026-10-12 2 18:00 Ana) con el límite de dos por día.
  • Añade una lista de espera: si alguien cancela, la reserva pasa automáticamente al primero que esperaba esa pista y hora.
  • Haz la interfaz web: una cuadrícula de pistas por turnos en la que se reserva haciendo clic, usando esta misma clase.

Dónde se explica