Biblioteca: préstamos, devoluciones y multas
Un programa de consola que gestiona los préstamos de una biblioteca: alta de libros y socios, préstamos con límites, devoluciones con multa y bloqueo por retraso, e informe final. Clases, colecciones y muchas validaciones encadenadas.
- Clases y objetos
- ArrayList
- HashMap y TreeMap
- Scanner
- switch
- String.format
Enunciado
La biblioteca de un instituto lleva los préstamos en una libreta y quiere un pequeño programa de consola. Cada línea que recibe el programa es una orden: dar de alta un libro o un socio, prestar un libro, devolverlo o pedir un informe. El programa contesta a cada orden con una línea (o con varias, en el informe).
Los días se cuentan como números enteros desde el comienzo del curso (día 0, día 1…), así que no hace falta trabajar con fechas. Un libro puede tener varios ejemplares: se puede prestar mientras quede alguno libre.
Hay tres normas: cada préstamo dura 14 días; un socio no puede tener más de 3 libros a la vez; y si devuelve un libro tarde, paga 0,50 € por cada día de retraso y no puede llevarse libros durante tantos días como se retrasó, contados desde el día en que lo devuelve.
Qué tiene que hacer el programa
LIBRO isbn ejemplares títuloda de alta un libro (el título puede tener espacios) y respondeLibro añadido: título (n ejemplares), con «ejemplar» en singular si es 1. Si el ISBN ya existe:Error: ya existe un libro con el ISBN isbn.SOCIO número nombreda de alta un socio y respondeSocio añadido: nombre (número). Si el número ya existe:Error: ya existe un socio con el número número.PRESTAR día socio isbncomprueba, en este orden: que exista el socio (Error: no existe el socio número), que exista el libro (Error: no existe el libro isbn), que el socio no esté bloqueado (Error: nombre no puede llevarse libros hasta el día X), que no tenga ya 3 préstamos (Error: nombre ya tiene 3 libros prestados), que no tenga ya ese libro (Error: nombre ya tiene «título») y que quede algún ejemplar (Error: no quedan ejemplares de «título»). Si todo va bien:Préstamo: nombre se lleva «título» hasta el día X, donde X es el día del préstamo más 14.DEVOLVER día socio isbn: si ese socio no tiene prestado ese libro,Error: el socio número no tiene prestado el libro isbn. Si lo devuelve el día del vencimiento o antes,Devolución: «título» a tiempo. Si no,Devolución: «título» con N días de retraso. Multa: X,XX €(«día» en singular si es 1), se suma la multa al socio y queda bloqueado hasta el día de la devolución más los días de retraso (si ya estaba bloqueado más tiempo, se queda el bloqueo más largo).INFORMEmuestra== Informe ==,Préstamos activos: N, una línea por cada socio con préstamos, ordenados por número de socio (- nombre (número): «título 1», «título 2», con los libros en el orden en que se los llevó) y por últimoMultas: X,XX €con la suma de todas las multas.- Cualquier otra orden responde
Error: orden desconocida ORDEN. Las líneas vacías se ignoran. Los importes se escriben con coma decimal y dos decimales.
Entrada
Una orden por línea, con las palabras separadas por un espacio. Los números de día y de socio son enteros; el ISBN es una palabra sin espacios.
El programa termina cuando se acaba la entrada.
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.
Préstamos, un ejemplar agotado y una devolución a tiempo
Entrada
LIBRO 978-84-376-0494-7 2 Cien años de soledad LIBRO 978-84-204-8291-3 1 El Hobbit SOCIO 1 Ana Ruiz SOCIO 2 Luis Gil PRESTAR 1 1 978-84-376-0494-7 PRESTAR 1 2 978-84-204-8291-3 PRESTAR 3 1 978-84-204-8291-3 DEVOLVER 10 2 978-84-204-8291-3 PRESTAR 11 1 978-84-204-8291-3 INFORME
Salida por consola
Libro añadido: Cien años de soledad (2 ejemplares) Libro añadido: El Hobbit (1 ejemplar) Socio añadido: Ana Ruiz (1) Socio añadido: Luis Gil (2) Préstamo: Ana Ruiz se lleva «Cien años de soledad» hasta el día 15 Préstamo: Luis Gil se lleva «El Hobbit» hasta el día 15 Error: no quedan ejemplares de «El Hobbit» Devolución: «El Hobbit» a tiempo Préstamo: Ana Ruiz se lleva «El Hobbit» hasta el día 25 == Informe == Préstamos activos: 2 - Ana Ruiz (1): «Cien años de soledad», «El Hobbit» Multas: 0,00 €
Retrasos, multas y bloqueo
Entrada
LIBRO A1 1 Dune LIBRO B2 1 Fundación SOCIO 7 Marta Sanz PRESTAR 0 7 A1 DEVOLVER 20 7 A1 PRESTAR 22 7 B2 PRESTAR 26 7 B2 DEVOLVER 27 7 A1 DEVOLVER 41 7 B2 INFORME
Salida por consola
Libro añadido: Dune (1 ejemplar) Libro añadido: Fundación (1 ejemplar) Socio añadido: Marta Sanz (7) Préstamo: Marta Sanz se lleva «Dune» hasta el día 14 Devolución: «Dune» con 6 días de retraso. Multa: 3,00 € Error: Marta Sanz no puede llevarse libros hasta el día 26 Préstamo: Marta Sanz se lleva «Fundación» hasta el día 40 Error: el socio 7 no tiene prestado el libro A1 Devolución: «Fundación» con 1 día de retraso. Multa: 0,50 € == Informe == Préstamos activos: 0 Multas: 3,50 €
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. Las altas
Empieza por altaLibro y altaSocio: comprobar con containsKey si la clave ya existe, guardar el objeto en el mapa y escribir el mensaje. Ojo al singular de «ejemplar». Ejecuta: las cuatro primeras líneas del primer ejemplo ya deberían salir bien.
2. Prestar, comprobación a comprobación
Escribe las seis comprobaciones como una cadena de if … else if en el orden del enunciado: el orden importa, porque decide qué error sale cuando fallan varias cosas a la vez. Solo si todas pasan se resta un ejemplar y se añade el préstamo a la lista del socio.
if (s == null) {
System.out.println("Error: no existe el socio " + numero);
} else if (l == null) {
// …
} else {
l.disponibles--;
s.prestamos.add(new Prestamo(l, dia + 14));
}3. Devolver y calcular el retraso
Busca en la lista del socio el préstamo de ese ISBN. Si no lo encuentras, es un error. Si lo encuentras: quítalo de la lista, suma un ejemplar libre y calcula retraso = dia - vence. Con retraso positivo, multa = retraso × 0,50 y el bloqueo pasa a ser el mayor entre el que tenía y dia + retraso.
4. El informe
Como socios es un TreeMap, recorrerlo con values() ya da los socios ordenados por número. Para unir los títulos con comas, guárdalos en una lista y usa String.join(", ", lista).
5. Comprueba los casos raros
Antes de dar el ejercicio por terminado, prueba a mano: un socio que pide su cuarto libro, uno que pide un libro que ya tiene, una devolución de algo que no se ha prestado y una orden inventada. Los tests ocultos los comprueban.
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
LIBRO 978-84-376-0494-7 2 Cien años de soledad LIBRO 978-84-204-8291-3 1 El Hobbit SOCIO 1 Ana Ruiz SOCIO 2 Luis Gil PRESTAR 1 1 978-84-376-0494-7 PRESTAR 1 2 978-84-204-8291-3 PRESTAR 3 1 978-84-204-8291-3 DEVOLVER 10 2 978-84-204-8291-3 PRESTAR 11 1 978-84-204-8291-3 INFORME
Libro añadido: Cien años de soledad (2 ejemplares) Libro añadido: El Hobbit (1 ejemplar) Socio añadido: Ana Ruiz (1) Socio añadido: Luis Gil (2) Préstamo: Ana Ruiz se lleva «Cien años de soledad» hasta el día 15 Préstamo: Luis Gil se lleva «El Hobbit» hasta el día 15 Error: no quedan ejemplares de «El Hobbit» Devolución: «El Hobbit» a tiempo Préstamo: Ana Ruiz se lleva «El Hobbit» hasta el día 25 == Informe == Préstamos activos: 2 - Ana Ruiz (1): «Cien años de soledad», «El Hobbit» Multas: 0,00 €
Solución explicada
Ver la solución completa
1import java.util.*;
2
3public class Main {
4 static class Libro {
5 final String isbn;
6 final String titulo;
7 int disponibles;
8
9 Libro(String isbn, String titulo, int ejemplares) {
10 this.isbn = isbn;
11 this.titulo = titulo;
12 this.disponibles = ejemplares;
13 }
14 }
15
16 static class Prestamo {
17 final Libro libro;
18 final int vence;
19
20 Prestamo(Libro libro, int vence) {
21 this.libro = libro;
22 this.vence = vence;
23 }
24 }
25
26 static class Socio {
27 final int numero;
28 final String nombre;
29 final List<Prestamo> prestamos = new ArrayList<>();
30 int bloqueadoHasta = 0;
31 double multas = 0;
32
33 Socio(int numero, String nombre) {
34 this.numero = numero;
35 this.nombre = nombre;
36 }
37 }
38
39 static final int DIAS_PRESTAMO = 14;
40 static final int MAX_PRESTAMOS = 3;
41 static final double MULTA_DIA = 0.50;
42 static final Locale ES = Locale.forLanguageTag("es-ES");
43
44 static final Map<String, Libro> libros = new HashMap<>();
45 // TreeMap: los socios quedan ordenados por número, como pide el informe
46 static final Map<Integer, Socio> socios = new TreeMap<>();
47
48 public static void main(String[] args) {
49 Scanner sc = new Scanner(System.in);
50 while (sc.hasNextLine()) {
51 String linea = sc.nextLine().trim();
52 if (!linea.isEmpty()) procesar(linea);
53 }
54 }
55
56 static void procesar(String linea) {
57 String[] p = linea.split(" ");
58 switch (p[0]) {
59 case "LIBRO" -> altaLibro(p[1], Integer.parseInt(p[2]), resto(p, 3));
60 case "SOCIO" -> altaSocio(Integer.parseInt(p[1]), resto(p, 2));
61 case "PRESTAR" -> prestar(Integer.parseInt(p[1]), Integer.parseInt(p[2]), p[3]);
62 case "DEVOLVER" -> devolver(Integer.parseInt(p[1]), Integer.parseInt(p[2]), p[3]);
63 case "INFORME" -> informe();
64 default -> System.out.println("Error: orden desconocida " + p[0]);
65 }
66 }
67
68 /** Las palabras de la línea desde la posición indicada (títulos y nombres pueden tener espacios). */
69 static String resto(String[] p, int desde) {
70 return String.join(" ", Arrays.copyOfRange(p, desde, p.length));
71 }
72
73 static void altaLibro(String isbn, int ejemplares, String titulo) {
74 if (libros.containsKey(isbn)) {
75 System.out.println("Error: ya existe un libro con el ISBN " + isbn);
76 return;
77 }
78 libros.put(isbn, new Libro(isbn, titulo, ejemplares));
79 System.out.println("Libro añadido: " + titulo + " (" + ejemplares + (ejemplares == 1 ? " ejemplar)" : " ejemplares)"));
80 }
81
82 static void altaSocio(int numero, String nombre) {
83 if (socios.containsKey(numero)) {
84 System.out.println("Error: ya existe un socio con el número " + numero);
85 return;
86 }
87 socios.put(numero, new Socio(numero, nombre));
88 System.out.println("Socio añadido: " + nombre + " (" + numero + ")");
89 }
90
91 static void prestar(int dia, int numero, String isbn) {
92 Socio s = socios.get(numero);
93 Libro l = libros.get(isbn);
94 if (s == null) {
95 System.out.println("Error: no existe el socio " + numero);
96 } else if (l == null) {
97 System.out.println("Error: no existe el libro " + isbn);
98 } else if (dia < s.bloqueadoHasta) {
99 System.out.println("Error: " + s.nombre + " no puede llevarse libros hasta el día " + s.bloqueadoHasta);
100 } else if (s.prestamos.size() == MAX_PRESTAMOS) {
101 System.out.println("Error: " + s.nombre + " ya tiene " + MAX_PRESTAMOS + " libros prestados");
102 } else if (s.prestamos.stream().anyMatch(pr -> pr.libro == l)) {
103 System.out.println("Error: " + s.nombre + " ya tiene «" + l.titulo + "»");
104 } else if (l.disponibles == 0) {
105 System.out.println("Error: no quedan ejemplares de «" + l.titulo + "»");
106 } else {
107 l.disponibles--;
108 s.prestamos.add(new Prestamo(l, dia + DIAS_PRESTAMO));
109 System.out.println("Préstamo: " + s.nombre + " se lleva «" + l.titulo + "» hasta el día " + (dia + DIAS_PRESTAMO));
110 }
111 }
112
113 static void devolver(int dia, int numero, String isbn) {
114 Socio s = socios.get(numero);
115 Prestamo pr = null;
116 if (s != null) {
117 for (Prestamo x : s.prestamos) {
118 if (x.libro.isbn.equals(isbn)) pr = x;
119 }
120 }
121 if (pr == null) {
122 System.out.println("Error: el socio " + numero + " no tiene prestado el libro " + isbn);
123 return;
124 }
125 s.prestamos.remove(pr);
126 pr.libro.disponibles++;
127 int retraso = dia - pr.vence;
128 if (retraso <= 0) {
129 System.out.println("Devolución: «" + pr.libro.titulo + "» a tiempo");
130 return;
131 }
132 double multa = retraso * MULTA_DIA;
133 s.multas += multa;
134 // Un nuevo retraso puede alargar el bloqueo, nunca acortarlo
135 s.bloqueadoHasta = Math.max(s.bloqueadoHasta, dia + retraso);
136 System.out.println("Devolución: «" + pr.libro.titulo + "» con " + retraso + (retraso == 1 ? " día" : " días")
137 + " de retraso. Multa: " + euros(multa));
138 }
139
140 static void informe() {
141 System.out.println("== Informe ==");
142 int activos = 0;
143 for (Socio s : socios.values()) activos += s.prestamos.size();
144 System.out.println("Préstamos activos: " + activos);
145 double total = 0;
146 for (Socio s : socios.values()) {
147 total += s.multas;
148 if (s.prestamos.isEmpty()) continue;
149 List<String> titulos = new ArrayList<>();
150 for (Prestamo pr : s.prestamos) titulos.add("«" + pr.libro.titulo + "»");
151 System.out.println("- " + s.nombre + " (" + s.numero + "): " + String.join(", ", titulos));
152 }
153 System.out.println("Multas: " + euros(total));
154 }
155
156 static String euros(double importe) {
157 return String.format(ES, "%.2f €", importe);
158 }
159}Cada tipo de dato es una clase pequeña: Libro guarda cuántos ejemplares quedan libres, Prestamo une un libro con su día de vencimiento y Socio tiene su lista de préstamos, su bloqueo y sus multas. Así, cada orden se reduce a buscar objetos y cambiar sus atributos.
Los libros se guardan en un HashMap por ISBN y los socios en un TreeMap por número: los dos permiten buscar por clave al instante, y el TreeMap además los mantiene ordenados, que es justo lo que necesita el informe.
En prestar, la cadena de if … else if hace las comprobaciones en el orden exacto del enunciado y solo modifica datos cuando todas pasan. Es un patrón muy útil: primero validar todo, después cambiar.
En la devolución, Math.max evita que una multa pequeña acorte un bloqueo que ya era más largo. Los importes se escriben con String.format y el formato español (es-ES), que pone la coma decimal.
Para ir más allá
- Añade una orden
PAGAR socio importeque reste de las multas pendientes, y que un socio con multas sin pagar no pueda llevarse libros. - Permite renovar un préstamo (
RENOVAR día socio isbn) una sola vez y solo si no está vencido. - Lee el catálogo inicial de un fichero CSV con
Files.readAllLinesantes de empezar a procesar órdenes.