Reservas de pistas de pádel con clases
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
- 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. SOCIOS Ana,Luis Moraañade socios (nombres separados por comas, sin espacios alrededor) y escribeSocios: Ncon el total de socios.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 escribeReserva N: pista P a las HH:MM para Nombre (X €).CANCELA nescribeReserva n canceladay libera la pista, oNo existe la reserva n.LIBRES pistaescribePista P libre a las 09:00, 10:30, …con sus turnos libres en orden, oPista P sin horas libres, oPista no válida: X.RESUMENescribe 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; yMejor cliente: Nombre (N reservas)(la persona con más reservas vigentes; si empatan, la primera alfabéticamente; «reserva» si es 1), oMejor cliente: ninguno.- 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 | Tipo | Precio | Socios (−30 %) |
|---|---|---|---|
| 09:00, 10:30, 12:00, 13:30, 15:00 | valle | 12,00 € | 8,40 € |
| 16:30, 18:00, 19:30, 21:00 | punta | 18,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.
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.
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.
Ejemplo
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
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)
Solución explicada
Ver la solución completa
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.