Apuntes DAM
Volver al inicio

Biblioteca: préstamos, devoluciones y multas

Ejercicio de JavaDifícilUnos 90 minutos

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

  1. LIBRO isbn ejemplares título da de alta un libro (el título puede tener espacios) y responde Libro 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.
  2. SOCIO número nombre da de alta un socio y responde Socio añadido: nombre (número). Si el número ya existe: Error: ya existe un socio con el número número.
  3. PRESTAR día socio isbn comprueba, 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.
  4. 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).
  5. INFORME muestra == 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 último Multas: X,XX € con la suma de todas las multas.
  6. 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.

java
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.

☕JavaBiblioteca: préstamos, devoluciones y multasDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
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 esperada
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 €
⏳
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
java
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 importe que 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.readAllLines antes de empezar a procesar órdenes.

Dónde se explica