Apuntes DAM
Volver al inicio

Ejercicios de Java resueltos

Los 153 ejercicios de Java de la web en una sola hoja: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Entornos de Desarrollo

1. Gráfico de trabajo pendiente (burndown) de un sprint

Difícil · Desarrollo de Software · apuntesdam.com/subject/entornos/topic/desarrollo-software

En Scrum, el burndown muestra cada día cuántos puntos de historia quedan por terminar. La entrada es la duración del sprint en días y, después, cada historia con su identificador, sus puntos y el día en que se terminó (0 si no se terminó). Muestra la cabecera «Sprint de D días, T puntos comprometidos» y, para cada día de 0 a D, una línea con el formato «Día %2d | barra | %2d (ideal %.1f)», donde la barra son tantos # como puntos quedan, rellenada con espacios hasta T caracteres, y el ideal es la recta que baja de T a 0 (T − T·d/D). Si quedan más puntos que el ideal redondeado hacia arriba, añade « (retraso)». Una historia solo descuenta sus puntos al terminarse (no hay puntos a medias). Al final, la velocidad (puntos terminados) y las historias que vuelven al product backlog (o «ninguna»).

Código de partida (java)
import java.util.*; public class Main {    record Historia(String id, int puntos, int diaTerminada) { }   // diaTerminada = 0 si no se terminó     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int dias = sc.nextInt();        List<Historia> hs = new ArrayList<>();        while (sc.hasNext()) hs.add(new Historia(sc.next(), sc.nextInt(), sc.nextInt()));        // TODO: mostrar el burndown día a día, la velocidad y las historias pendientes    }}

Ejemplo: Sprint que acaba bien

Entrada

5
H1 3 2
H2 5 4
H3 2 5

Salida esperada

Sprint de 5 días, 10 puntos comprometidos
Día  0 | ########## | 10 (ideal 10.0)
Día  1 | ########## | 10 (ideal 8.0)  (retraso)
Día  2 | #######    |  7 (ideal 6.0)  (retraso)
Día  3 | #######    |  7 (ideal 4.0)  (retraso)
Día  4 | ##         |  2 (ideal 2.0)
Día  5 |            |  0 (ideal 0.0)
Velocidad: 10 puntos
Vuelven al product backlog: ninguna

Ejemplo: Sprint con retraso

Entrada

4
H1 5 3
H2 3 0
H3 2 4
H4 1 1

Salida esperada

Sprint de 4 días, 11 puntos comprometidos
Día  0 | ########### | 11 (ideal 11.0)
Día  1 | ##########  | 10 (ideal 8.3)  (retraso)
Día  2 | ##########  | 10 (ideal 5.5)  (retraso)
Día  3 | #####       |  5 (ideal 2.8)  (retraso)
Día  4 | ###         |  3 (ideal 0.0)  (retraso)
Velocidad: 8 puntos
Vuelven al product backlog: H2

2. Planificación con dependencias y camino crítico

Muy difícil · Desarrollo de Software · apuntesdam.com/subject/entornos/topic/desarrollo-software

Para planificar un proyecto se descompone en tareas con su duración y las tareas que deben terminar antes (como en un diagrama de Gantt o PERT). Cada línea de la entrada es «ID duración» y, opcionalmente, la lista de tareas previas separadas por comas. Si una tarea depende de otra que no existe, muestra «Error: X depende de Y, que no existe». Calcula un orden topológico con el algoritmo de Kahn: empieza por las tareas sin previas en el orden de la entrada y usa una cola; cada tarea, al salir, libera a las que dependen de ella (en el orden en que se declararon). Si no se pueden ordenar todas, «Error: hay dependencias circulares». Después calcula el inicio más temprano (el máximo fin de sus previas) y el fin de cada tarea, la duración del proyecto, el fin más tardío de cada tarea sin retrasar el proyecto (recorriendo el orden al revés) y la holgura (fin tardío − fin temprano). Muestra el orden, una línea por tarea con «%-3s inicio %2d fin %2d holgura %d», la duración y el camino crítico: las tareas con holgura 0, en orden topológico, unidas por « → ».

Código de partida (java)
import java.util.*; public class Main {    record Tarea(String id, int duracion, List<String> previas) { }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Tarea> tareas = new LinkedHashMap<>();        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.isEmpty()) continue;            // Formato: ID duración [previa1,previa2,...]            String[] p = l.split("\\s+");            List<String> previas = p.length > 2 ? List.of(p[2].split(",")) : List.of();            tareas.put(p[0], new Tarea(p[0], Integer.parseInt(p[1]), previas));        }        // TODO: orden topológico, inicio y fin más tempranos, duración del proyecto,        //       holgura de cada tarea y camino crítico (o error si hay un ciclo)    }}

Ejemplo: Proyecto web

Entrada

A 3
B 2 A
C 4 A
D 1 B,C
E 2 D

Salida esperada

Orden: A B C D E
A   inicio  0  fin  3  holgura 0
B   inicio  3  fin  5  holgura 2
C   inicio  3  fin  7  holgura 0
D   inicio  7  fin  8  holgura 0
E   inicio  8  fin 10  holgura 0
Duración del proyecto: 10 días
Camino crítico: A → C → D → E

Ejemplo: Dos ramas

Entrada

Analisis 5
BD 3 Analisis
API 6 BD
UI 4 Analisis
Pruebas 2 API,UI
Manual 1 UI

Salida esperada

Orden: Analisis BD UI API Manual Pruebas
Analisis inicio  0  fin  5  holgura 0
BD  inicio  5  fin  8  holgura 0
UI  inicio  5  fin  9  holgura 5
API inicio  8  fin 14  holgura 0
Manual inicio  9  fin 10  holgura 6
Pruebas inicio 14  fin 16  holgura 0
Duración del proyecto: 16 días
Camino crítico: Analisis → BD → API → Pruebas

3. Depura el resumen de notas

Difícil · Instalación y uso de IDEs · apuntesdam.com/subject/entornos/topic/ides

El método resumen debería calcular la media (con decimales), cuántas notas son aprobados (5 o más), la nota máxima y la mínima de un grupo de notas enteras de 0 a 10. Compila y parece funcionar, pero tiene cuatro errores. Usa el depurador de tu IDE (pon un punto de ruptura dentro del bucle y observa i, suma, max y min en cada vuelta) o añade trazas temporales, encuentra los cuatro y corrígelos sin reescribir el método desde cero.

Código de partida (java)
import java.util.*; public class Main {    // Este código compila pero tiene CUATRO errores. Encuéntralos con el depurador (o con trazas) y corrígelos.    static String resumen(int[] notas) {        int suma = 0, aprobados = 0, max = 0, min = 0;        for (int i = 1; i < notas.length; i++) {            suma += notas[i];            if (notas[i] > 5) aprobados++;            if (notas[i] > max) max = notas[i];            if (notas[i] < min) min = notas[i];        }        double media = suma / notas.length;        return String.format(Locale.US, "media %.2f, aprobados %d de %d, máxima %d, mínima %d",                media, aprobados, notas.length, max, min);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.isEmpty()) continue;            int[] notas = Arrays.stream(l.split("\\s+")).mapToInt(Integer::parseInt).toArray();            System.out.println(Arrays.toString(notas) + " → " + resumen(notas));        }    }}

Ejemplo: Grupo variado

Entrada

7 4 9 5 10

Salida esperada

[7, 4, 9, 5, 10] → media 7.00, aprobados 4 de 5, máxima 10, mínima 4

Ejemplo: Todos suspensos

Entrada

3 1 4

Salida esperada

[3, 1, 4] → media 2.67, aprobados 0 de 3, máxima 4, mínima 1

4. Depura una búsqueda binaria

Muy difícil · Instalación y uso de IDEs · apuntesdam.com/subject/entornos/topic/ides

primeraMayorOIgual debe devolver la primera posición de un array ordenado cuyo valor es mayor o igual que x, o a.length si todos son menores (es el «lower bound» que usan Arrays.binarySearch y las bases de datos en sus índices). Con ella, contar calcula cuántas veces aparece x. La versión actual tiene varios errores: según el caso, se queda en un bucle infinito, devuelve una posición equivocada o nunca devuelve a.length. Depúrala con casos pequeños (arrays de 1 o 2 elementos), anotando lo, hi y medio en cada vuelta, y corrígela. Además, el cálculo de medio puede desbordarse con arrays de más de mil millones de elementos: cámbialo por una forma segura.

Código de partida (java)
import java.util.*; public class Main {    // Primera posición i tal que a[i] >= x (a.length si no hay ninguna). El array está ordenado.    // Tiene errores que solo aparecen en algunos casos: encuéntralos.    static int primeraMayorOIgual(int[] a, int x) {        int lo = 0, hi = a.length - 1;        int vueltas = 0;                           // protección para que no se cuelgue mientras lo depuras        while (lo < hi && vueltas++ < 100) {            int medio = (lo + hi) / 2;            if (a[medio] < x) lo = medio;            else hi = medio - 1;        }        return lo;    }     static int contar(int[] a, int x) {        return primeraMayorOIgual(a, x + 1) - primeraMayorOIgual(a, x);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int[] a = Arrays.stream(sc.nextLine().trim().split("\\s+")).mapToInt(Integer::parseInt).toArray();        while (sc.hasNextInt()) {            int x = sc.nextInt();            System.out.println("x=" + x + ": primera posición >= x → " + primeraMayorOIgual(a, x) + ", apariciones → " + contar(a, x));        }    }}

Ejemplo: Con repetidos

Entrada

1 3 3 3 5 8
3
4
0
9

Salida esperada

x=3: primera posición >= x → 1, apariciones → 3
x=4: primera posición >= x → 4, apariciones → 0
x=0: primera posición >= x → 0, apariciones → 0
x=9: primera posición >= x → 6, apariciones → 0

Ejemplo: Arrays pequeños

Entrada

7
7
8
6

Salida esperada

x=7: primera posición >= x → 0, apariciones → 1
x=8: primera posición >= x → 1, apariciones → 0
x=6: primera posición >= x → 0, apariciones → 0

5. Valores límite: casos que detectan errores

Difícil · Diseño y realización de pruebas · apuntesdam.com/subject/entornos/topic/diseno-pruebas

La técnica de valores límite dice que los errores se esconden en las fronteras entre clases de equivalencia. El método tarifa ya está bien hecho; tu trabajo es escribir en CASOS las pruebas, cada una con un peso y la tarifa esperada. El programa comprueba que tus casos pasan con el método correcto y después los ejecuta contra seis versiones con un error cada una (mutantes), como los que comete cualquier programador: un < en vez de <=, un precio equivocado o un límite olvidado. Un mutante queda «detectado» si alguno de tus casos da un resultado distinto. Consigue detectar los seis con casos razonados, no al azar.

Código de partida (java)
import java.util.*;import java.util.function.DoubleUnaryOperator; public class Main {    // Tarifa de envío según el peso en kg. -1 significa «no admitido».    //   peso <= 0        → -1    //   0 < peso <= 1    → 4.5    //   1 < peso <= 5    → 7    //   5 < peso <= 20   → 12    //   peso > 20        → -1    static double tarifa(double peso) {        if (peso <= 0) return -1;        if (peso <= 1) return 4.5;        if (peso <= 5) return 7;        if (peso <= 20) return 12;        return -1;    }     // TODO: escribe aquí tus casos de prueba: {peso, resultado esperado}    static final double[][] CASOS = {        {3, 7},    };     // ---- No modifiques lo que sigue: versiones con un error cada una («mutantes»)    static final Map<String, DoubleUnaryOperator> MUTANTES = new LinkedHashMap<>();    static {        MUTANTES.put("M1 (< 1 en vez de <= 1)", p -> p <= 0 ? -1 : p < 1 ? 4.5 : p <= 5 ? 7 : p <= 20 ? 12 : -1);        MUTANTES.put("M2 (< 5 en vez de <= 5)", p -> p <= 0 ? -1 : p <= 1 ? 4.5 : p < 5 ? 7 : p <= 20 ? 12 : -1);        MUTANTES.put("M3 (< 20 en vez de <= 20)", p -> p <= 0 ? -1 : p <= 1 ? 4.5 : p <= 5 ? 7 : p < 20 ? 12 : -1);        MUTANTES.put("M4 (< 0 en vez de <= 0)", p -> p < 0 ? -1 : p <= 1 ? 4.5 : p <= 5 ? 7 : p <= 20 ? 12 : -1);        MUTANTES.put("M5 (sin límite superior)", p -> p <= 0 ? -1 : p <= 1 ? 4.5 : p <= 5 ? 7 : 12);        MUTANTES.put("M6 (7.5 en vez de 7)", p -> p <= 0 ? -1 : p <= 1 ? 4.5 : p <= 5 ? 7.5 : p <= 20 ? 12 : -1);    }     public static void main(String[] args) {        String fallo = null;        for (double[] c : CASOS)            if (tarifa(c[0]) != c[1] && fallo == null) fallo = "tarifa(" + c[0] + ") da " + tarifa(c[0]) + ", no " + c[1];        System.out.println("Casos correctos con el original: " + (fallo == null ? "sí" : "no (" + fallo + ")"));        List<String> vivos = new ArrayList<>();        for (var m : MUTANTES.entrySet()) {            boolean detectado = false;            for (double[] c : CASOS) if (m.getValue().applyAsDouble(c[0]) != c[1]) detectado = true;            if (!detectado) vivos.add(m.getKey());        }        System.out.println("Mutantes detectados: " + (MUTANTES.size() - vivos.size()) + " de " + MUTANTES.size());        System.out.println("Sobreviven: " + (vivos.isEmpty() ? "ninguno" : String.join(", ", vivos)));    }}

Ejemplo: Detección de mutantes

Salida esperada

Casos correctos con el original: sí
Mutantes detectados: 6 de 6
Sobreviven: ninguno

6. Matar mutantes con varias condiciones

Muy difícil · Diseño y realización de pruebas · apuntesdam.com/subject/entornos/topic/diseno-pruebas

El precio de la entrada de un museo depende de la edad, de si se es socio y de si es fin de semana (las reglas están en el comentario de ORIGINAL). Escribe en CASOS las pruebas necesarias, cada una con la edad, si es socio (1/0), si es fin de semana (1/0) y el precio esperado, para detectar los mutantes: errores en los límites de edad, en el orden en que se aplican los descuentos, en el tope de 0 € o en cómo se combinan las condiciones. Atención: uno de los ocho es un mutante equivalente, que se comporta exactamente igual que el original con cualquier entrada posible, así que ningún caso puede detectarlo. Detecta los otros siete y averigua cuál es el equivalente y por qué. Con muchas condiciones, probar todas las combinaciones crece muy deprisa: busca un conjunto pequeño (con unos 12 casos basta) en el que cada condición y cada límite influyan en algún resultado.

Código de partida (java)
import java.util.*; public class Main {    interface Precio { double calcular(int edad, boolean socio, boolean finDeSemana); }     // Precio de la entrada a un museo:    //   edad < 0 → -1 (error)    //   base 10 €; los fines de semana, 12 €    //   menores de 12: 50 % de descuento; desde 65: 40 % de descuento    //   los socios tienen además 2 € menos sobre el precio ya descontado (nunca por debajo de 0)    static final Precio ORIGINAL = (edad, socio, finde) -> {        if (edad < 0) return -1;        double p = finde ? 12 : 10;        if (edad < 12) p *= 0.5;        else if (edad >= 65) p *= 0.6;        if (socio) p = Math.max(0, p - 2);        return p;    };     // TODO: tus casos: {edad, socio (0/1), fin de semana (0/1), precio esperado}    static final double[][] CASOS = {        {30, 0, 0, 10},    };     // ---- No modifiques lo que sigue    static final Map<String, Precio> MUTANTES = new LinkedHashMap<>();    static {        MUTANTES.put("M1 (<= 12)", (e, s, f) -> e < 0 ? -1 : aplicar(f ? 12 : 10, e <= 12, e >= 65, s));        MUTANTES.put("M2 (> 65)", (e, s, f) -> e < 0 ? -1 : aplicar(f ? 12 : 10, e < 12, e > 65, s));        MUTANTES.put("M3 (<= 0 es error)", (e, s, f) -> e <= 0 ? -1 : aplicar(f ? 12 : 10, e < 12, e >= 65, s));        MUTANTES.put("M4 (socio antes del %)", (e, s, f) -> {            if (e < 0) return -1;            double p = (f ? 12 : 10) - (s ? 2 : 0);            return e < 12 ? p * 0.5 : e >= 65 ? p * 0.6 : p;        });        MUTANTES.put("M5 (sin tope en 0)", (e, s, f) -> {            if (e < 0) return -1;            double p = f ? 12 : 10;            if (e < 12) p *= 0.5; else if (e >= 65) p *= 0.6;            return s ? p - 2 : p;        });        MUTANTES.put("M6 (ignora el fin de semana con descuento)", (e, s, f) -> e < 0 ? -1 : aplicar((f && e >= 12 && e < 65) ? 12 : 10, e < 12, e >= 65, s));        MUTANTES.put("M7 (el socio solo descuenta a adultos)", (e, s, f) -> e < 0 ? -1 : aplicar(f ? 12 : 10, e < 12, e >= 65, s && e >= 12 && e < 65));        MUTANTES.put("M8 (65 o más: 50 %)", (e, s, f) -> {            if (e < 0) return -1;            double p = f ? 12 : 10;            if (e < 12 || e >= 65) p *= 0.5;            if (s) p = Math.max(0, p - 2);            return p;        });    }     static double aplicar(double p, boolean nino, boolean mayor, boolean socio) {        if (nino) p *= 0.5; else if (mayor) p *= 0.6;        if (socio) p = Math.max(0, p - 2);        return p;    }     static boolean igual(double a, double b) { return Math.abs(a - b) < 1e-9; }     public static void main(String[] args) {        String fallo = null;        for (double[] c : CASOS) {            double r = ORIGINAL.calcular((int) c[0], c[1] == 1, c[2] == 1);            if (!igual(r, c[3]) && fallo == null) fallo = Arrays.toString(c) + " da " + r;        }        System.out.println("Casos correctos con el original: " + (fallo == null ? "sí" : "no (" + fallo + ")"));        List<String> vivos = new ArrayList<>();        for (var m : MUTANTES.entrySet()) {            boolean detectado = false;            for (double[] c : CASOS)                if (!igual(m.getValue().calcular((int) c[0], c[1] == 1, c[2] == 1), c[3])) detectado = true;            if (!detectado) vivos.add(m.getKey());        }        System.out.println("Mutantes detectados: " + (MUTANTES.size() - vivos.size()) + " de " + MUTANTES.size());        System.out.println("Sobreviven: " + (vivos.isEmpty() ? "ninguno" : String.join(", ", vivos)));    }}

Ejemplo: Detección de mutantes

Salida esperada

Casos correctos con el original: sí
Mutantes detectados: 7 de 8
Sobreviven: M5 (sin tope en 0)

7. Un analizador de código en miniatura

Difícil · Optimización y Documentación · apuntesdam.com/subject/entornos/topic/optimizacion-documentacion

Herramientas como Checkstyle o PMD leen el código fuente y avisan de los «malos olores». Implementa analizar con cuatro reglas, recorriendo las líneas en orden. Detecta la cabecera de un método con la expresión regular METODO (el grupo 2 es el nombre y el 3, los parámetros). Regla 1: si el nombre no sigue lowerCamelCase (empieza por minúscula y solo tiene letras y cifras), «Línea n: el método «X» no sigue lowerCamelCase». Regla 2: si tiene más de MAX_PARAMETROS parámetros, «Línea n: «X» tiene p parámetros (máximo 3)». Regla 3: en cada línea que no declare una constante (que no contenga «final»), cada número distinto de 0 y 1 que no esté dentro de una cadena ni de un comentario // es «Línea n: número mágico N». Regla 4: un método que, desde su cabecera hasta la llave que lo cierra (cuenta { y }), mide más de MAX_LINEAS_METODO líneas da «Línea inicio: «X» mide L líneas (máximo 8)», y este aviso se añade al encontrar la llave de cierre. El main muestra los avisos y el total.

Código de partida (java)
import java.util.*;import java.util.regex.*; public class Main {    static final int MAX_LINEAS_METODO = 8, MAX_PARAMETROS = 3;    static final Pattern METODO = Pattern.compile(            "^\\s*(public|private|protected|static|\\s)+[\\w<>\\[\\]]+\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*\\{");     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String> lineas = new ArrayList<>();        while (sc.hasNextLine()) lineas.add(sc.nextLine());        List<String> avisos = analizar(lineas);        avisos.forEach(System.out::println);        System.out.println(avisos.size() + (avisos.size() == 1 ? " aviso" : " avisos"));    }     static List<String> analizar(List<String> lineas) {        List<String> avisos = new ArrayList<>();        // TODO: recorrer las líneas y aplicar las cuatro reglas        return avisos;    }}

Ejemplo: Nombre y número mágico

Entrada

public class Factura {
    static final double IVA = 0.21;

    public double total(double base) {
        return base * (1 + IVA);
    }

    public double Descuento(double total, int unidades) {
        if (unidades > 10) return total * 0.95;
        return total;
    }
}

Salida esperada

Línea 8: el método «Descuento» no sigue lowerCamelCase
Línea 9: número mágico 10
Línea 9: número mágico 0.95
3 avisos

Ejemplo: Método largo con muchos parámetros

Entrada

public class Pedidos {
    private double calcular(String cliente, int unidades, double precio, boolean urgente, String pais) {
        double total = unidades * precio;
        if (urgente) {
            total += 15;
        }
        if (pais.equals("ES")) {
            total *= 1.21;
        }
        String msg = "Pedido de 99 unidades";
        return total;
    }

    public static void main(String[] args) {
        System.out.println(new Pedidos().calcular("ana", 3, 2.5, true, "ES"));
    }
}

Salida esperada

Línea 2: «calcular» tiene 5 parámetros (máximo 3)
Línea 5: número mágico 15
Línea 8: número mágico 1.21
Línea 2: «calcular» mide 11 líneas (máximo 8)
Línea 15: número mágico 3
Línea 15: número mágico 2.5
6 avisos

8. Un Git en miniatura: base de fusión

Muy difícil · Optimización y Documentación · apuntesdam.com/subject/entornos/topic/optimizacion-documentacion

En Git, cada commit apunta a sus padres (uno normalmente, dos si es una fusión) y una rama es solo un nombre que apunta a un commit. Para fusionar, Git busca la base de fusión: el antepasado común más reciente de los dos commits. Implementa antepasados (el propio commit y todos los que se alcanzan siguiendo padres) y baseDeFusion: de los antepasados comunes, el que no es antepasado de ningún otro común (si hubiera varios, el primero en orden de creación; si no hay ninguno, null). Con eso, el main decide qué haría git merge: «ya actualizado» si el origen es la base, «fast-forward» si el destino es la base (basta con mover la rama) o «fusión de tres vías» en otro caso.

Código de partida (java)
import java.util.*; public class Main {    static final Map<String, List<String>> padres = new LinkedHashMap<>();   // commit → sus padres    static final Map<String, String> ramas = new TreeMap<>();                // rama → commit al que apunta     // Todos los antepasados de un commit, incluido él mismo    static Set<String> antepasados(String c) {        // TODO        return new HashSet<>();    }     // Antepasado común «más reciente» (el que no es antepasado de otro antepasado común); null si no hay    static String baseDeFusion(String a, String b) {        // TODO        return null;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] t = sc.nextLine().trim().split("\\s+");            switch (t[0]) {                case "COMMIT" -> padres.put(t[1], t.length > 2 ? List.of(t[2].split(",")) : List.of());                case "RAMA" -> ramas.put(t[1], t[2]);                case "BASE" -> System.out.println("merge-base " + t[1] + " " + t[2] + " → " + baseDeFusion(ramas.get(t[1]), ramas.get(t[2])));                case "FUSIONAR" -> {        // FUSIONAR destino origen: ¿qué haría git merge origen estando en destino?                    String d = ramas.get(t[1]), o = ramas.get(t[2]);                    String base = baseDeFusion(d, o);                    String r;                    if (o.equals(base)) r = "ya actualizado (nada que fusionar)";                    else if (d.equals(base)) r = "fast-forward: " + t[1] + " pasa de " + d + " a " + o;                    else r = "fusión de tres vías (base " + base + "): se crea un commit de fusión";                    System.out.println("git merge " + t[2] + " en " + t[1] + " → " + r);                }            }        }    }}

Ejemplo: Ramas divergentes

Entrada

COMMIT c1
COMMIT c2 c1
COMMIT c3 c2
COMMIT c4 c3
COMMIT c5 c2
COMMIT c6 c5
RAMA main c4
RAMA login c6
RAMA hotfix c3
BASE main login
FUSIONAR main login
FUSIONAR main hotfix
FUSIONAR hotfix main

Salida esperada

merge-base main login → c2
git merge login en main → fusión de tres vías (base c2): se crea un commit de fusión
git merge hotfix en main → ya actualizado (nada que fusionar)
git merge main en hotfix → fast-forward: hotfix pasa de c3 a c4

Ejemplo: Después de fusionar

Entrada

COMMIT c1
COMMIT c2 c1
COMMIT c3 c2
COMMIT c4 c3
COMMIT c5 c2
COMMIT c6 c5
RAMA main c4
RAMA login c6
RAMA hotfix c3
COMMIT c7 c4,c6
RAMA main c7
BASE main login
FUSIONAR main login
FUSIONAR login main

Salida esperada

merge-base main login → c6
git merge login en main → ya actualizado (nada que fusionar)
git merge main en login → fast-forward: login pasa de c6 a c7

9. Dobles de prueba escritos a mano

Difícil · Pruebas con JUnit y Mockito · apuntesdam.com/subject/entornos/topic/pruebas-junit

Para probar ServicioPedidos sin una base de datos ni un servidor de correo reales se usan dobles de prueba: objetos que implementan las mismas interfaces. Completa InventarioFalso, cuyo constructor recibe el stock que devolverá stock() para cualquier producto y que anota cada llamada a descontar como «producto -unidades» en una lista que devuelve descuentos(), y CorreoEspia, que no envía nada: guarda cada envío como «destinatario | asunto» en una lista que devuelve enviados(). Es lo que hace Mockito automáticamente con when(...).thenReturn(...) y verify(...). El main ya prueba el servicio con distintos casos y muestra lo que han registrado tus dobles.

Código de partida (java)
import java.util.*; interface Inventario {    int stock(String producto);    void descontar(String producto, int unidades);} interface Correo {    void enviar(String destinatario, String asunto);} // Clase que se quiere probar: no sabe si habla con una base de datos real o con un dobleclass ServicioPedidos {    private final Inventario inventario;    private final Correo correo;     ServicioPedidos(Inventario inventario, Correo correo) {        this.inventario = inventario;        this.correo = correo;    }     boolean pedir(String cliente, String producto, int unidades) {        if (unidades <= 0) return false;        if (inventario.stock(producto) < unidades) {            correo.enviar("almacen@tienda.es", "Sin stock: " + producto);            return false;        }        inventario.descontar(producto, unidades);        correo.enviar(cliente, "Pedido confirmado: " + unidades + " x " + producto);        return true;    }} // TODO: un doble de Inventario con un stock fijado por la prueba que anote las llamadas a descontarclass InventarioFalso implements Inventario {    InventarioFalso(int stockFijo) { }    public int stock(String producto) { return 0; }    public void descontar(String producto, int unidades) { }    List<String> descuentos() { return List.of(); }} // TODO: un doble de Correo que guarde cada envío como "destinatario | asunto"class CorreoEspia implements Correo {    public void enviar(String destinatario, String asunto) { }    List<String> enviados() { return List.of(); }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] t = sc.nextLine().trim().split(" ");   // stock cliente producto unidades            InventarioFalso inv = new InventarioFalso(Integer.parseInt(t[0]));            CorreoEspia correo = new CorreoEspia();            boolean ok = new ServicioPedidos(inv, correo).pedir(t[1], t[2], Integer.parseInt(t[3]));            System.out.println("pedir(" + t[1] + ", " + t[2] + ", " + t[3] + ") con stock " + t[0] + " → " + ok);            System.out.println("  descuentos: " + inv.descuentos());            System.out.println("  correos: " + correo.enviados());        }    }}

Ejemplo: Pedido correcto

Entrada

10 ana@mail.com teclado 3

Salida esperada

pedir(ana@mail.com, teclado, 3) con stock 10 → true
  descuentos: [teclado -3]
  correos: [ana@mail.com | Pedido confirmado: 3 x teclado]

Ejemplo: Sin stock y unidades no válidas

Entrada

2 luis@mail.com raton 5
5 eva@mail.com monitor 0

Salida esperada

pedir(luis@mail.com, raton, 5) con stock 2 → false
  descuentos: []
  correos: [almacen@tienda.es | Sin stock: raton]
pedir(eva@mail.com, monitor, 0) con stock 5 → false
  descuentos: []
  correos: []

10. Tu propio JUnit con reflexión

Muy difícil · Pruebas con JUnit y Mockito · apuntesdam.com/subject/entornos/topic/pruebas-junit

¿Cómo encuentra JUnit los métodos de prueba? Con reflexión: inspecciona la clase en tiempo de ejecución y busca los métodos con la anotación @Test. Implementa ejecutar: localiza los métodos anotados con @Prueba y @Antes de la clase, ordena las pruebas por nombre y, para cada una, crea una instancia nueva de la clase (así una prueba no afecta a otra), ejecuta los métodos @Antes y después la prueba. Si termina bien, «✔ nombre»; si lanza FalloAsercion, es un fallo: «✘ nombre: mensaje»; si lanza cualquier otra excepción, es un error: «‼ nombre: NombreSimpleDeLaExcepción». Al final, «Pruebas: N, correctas: a, fallidas: b, con error: c». Los métodos sin anotación no se ejecutan.

Código de partida (java)
import java.lang.annotation.*;import java.lang.reflect.*;import java.util.*; // Anotaciones propias, como @Test y @BeforeEach de JUnit@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Prueba { }@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Antes { } class FalloAsercion extends RuntimeException {    FalloAsercion(String m) { super(m); }} class Afirmar {    static void iguales(Object esperado, Object real) {        if (!Objects.equals(esperado, real)) throw new FalloAsercion("se esperaba <" + esperado + "> pero fue <" + real + ">");    }    static void cierto(boolean c, String mensaje) {        if (!c) throw new FalloAsercion(mensaje);    }} // ---- Las pruebas de ejemplo (no las modifiques)class PilaTest {    private Deque<Integer> pila;     @Antes void preparar() { pila = new ArrayDeque<>(); pila.push(1); pila.push(2); }     @Prueba void cimaEsElUltimo() { Afirmar.iguales(2, pila.peek()); }    @Prueba void sacarReduce() { pila.pop(); Afirmar.iguales(1, pila.size()); }    @Prueba void cadaPruebaEmpiezaLimpia() { Afirmar.iguales(2, pila.size()); }    @Prueba void fallaAProposito() { Afirmar.iguales(3, pila.size()); }    @Prueba void lanzaExcepcion() { new ArrayDeque<Integer>().pop(); }    void noEsUnaPrueba() { throw new IllegalStateException("no debería ejecutarse"); }} public class Main {    // Ejecuta todos los métodos @Prueba de la clase, en orden alfabético, con una instancia nueva cada vez    static void ejecutar(Class<?> clase) throws Exception {        // TODO    }     public static void main(String[] args) throws Exception {        ejecutar(PilaTest.class);    }}

Ejemplo: Pila de ejemplo

Salida esperada

✔ cadaPruebaEmpiezaLimpia
✔ cimaEsElUltimo
✘ fallaAProposito: se esperaba <3> pero fue <2>
‼ lanzaExcepcion: NoSuchElementException
✔ sacarReduce
Pruebas: 5, correctas: 3, fallidas: 1, con error: 1

11. Del diagrama de estados al código

Difícil · Diagramas UML · apuntesdam.com/subject/entornos/topic/diagramas-uml

El comentario de la clase describe el diagrama de estados de un pedido: sus estados, los eventos que provocan cada transición y las acciones asociadas. Implementa aplicar para que, según el estado actual y el evento, cambie de estado y devuelva el nuevo estado con su acción entre corchetes si la tiene («CANCELADO [devolver el dinero]», «PAGADO [reenviar]»). Un paquete perdido vuelve a PAGADO para reenviarlo, pero a la tercera pérdida el pedido pasa a CANCELADO con devolución. Cualquier evento que no tenga una transición desde el estado actual (incluidos todos los eventos en los estados finales) devuelve «transición no permitida (sigue en ESTADO)» sin cambiar nada.

Código de partida (java)
import java.util.*; public class Main {    // Diagrama de estados de un pedido (estado inicial: CARRITO)    //   CARRITO   --pagar-->     PAGADO    //   CARRITO   --cancelar-->  CANCELADO    //   PAGADO    --enviar-->    ENVIADO    //   PAGADO    --cancelar-->  CANCELADO   [acción: devolver el dinero]    //   ENVIADO   --entregar-->  ENTREGADO    //   ENVIADO   --perder-->    PAGADO      [acción: reenviar; como mucho 2 veces, después CANCELADO con devolución]    //   ENTREGADO y CANCELADO son estados finales    enum Estado { CARRITO, PAGADO, ENVIADO, ENTREGADO, CANCELADO }     static Estado estado = Estado.CARRITO;    static int perdidas = 0;     // Aplica un evento y devuelve el texto que describe lo ocurrido    static String aplicar(String evento) {        // TODO        return "";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNext()) {            String ev = sc.next();            Estado antes = estado;            String r = aplicar(ev);            System.out.println(antes + " --" + ev + "--> " + r);        }        System.out.println("Estado final: " + estado);    }}

Ejemplo: Camino feliz

Entrada

pagar enviar entregar

Salida esperada

CARRITO --pagar--> PAGADO
PAGADO --enviar--> ENVIADO
ENVIADO --entregar--> ENTREGADO
Estado final: ENTREGADO

Ejemplo: Eventos no válidos y cancelación

Entrada

enviar pagar cancelar entregar

Salida esperada

CARRITO --enviar--> transición no permitida (sigue en CARRITO)
CARRITO --pagar--> PAGADO
PAGADO --cancelar--> CANCELADO [devolver el dinero]
CANCELADO --entregar--> transición no permitida (sigue en CANCELADO)
Estado final: CANCELADO

12. Ingeniería inversa: de las clases al diagrama

Muy difícil · Diagramas UML · apuntesdam.com/subject/entornos/topic/diagramas-uml

Herramientas como IntelliJ o PlantUML pueden generar el diagrama de clases a partir del código. Hazlo tú con reflexión: para cada clase de la lista, en orden, escribe su bloque de PlantUML («interface X {», «abstract class X {» o «class X {»). Dentro, los atributos ordenados por nombre con su visibilidad (+ public, - private, # protected, ~ paquete) como «-dni : String», y después los métodos declarados en la clase, también por nombre, como «+matricular(Asignatura) : void». Un atributo cuyo tipo es otra clase del modelo no se escribe como atributo sino como asociación «Asignatura --> "1" Profesor : titular»; uno de tipo List<X> es una asociación con multiplicidad * («Alumno --> "*" Matricula : matriculas»). Al final, después de todos los bloques y en el orden en que se descubrieron mientras se recorrían las clases (primero las asociaciones de cada clase y luego su herencia), las relaciones, incluidas la herencia «Persona <|-- Alumno» y la realización de interfaces «Evaluable <|.. Alumno».

Código de partida (java)
import java.lang.reflect.*;import java.util.*; // ---- Modelo de ejemplo (no lo modifiques)abstract class Persona {    protected String nombre;    private String dni;    public String getNombre() { return nombre; }}interface Evaluable { double nota(); }class Alumno extends Persona implements Evaluable {    private int curso;    private List<Matricula> matriculas;    public double nota() { return 0; }    public void matricular(Asignatura a) { }}class Profesor extends Persona {    private double salario;    private List<Asignatura> imparte;}class Asignatura {    private String codigo;    int horas;    private Profesor titular;}class Matricula {    private Asignatura asignatura;    private double nota;} public class Main {    static final List<Class<?>> MODELO = List.of(Persona.class, Evaluable.class, Alumno.class, Profesor.class, Asignatura.class, Matricula.class);     static String visibilidad(int mod) {        return Modifier.isPublic(mod) ? "+" : Modifier.isPrivate(mod) ? "-" : Modifier.isProtected(mod) ? "#" : "~";    }     // Genera el diagrama de clases en PlantUML a partir de las clases compiladas    static String plantUml(List<Class<?>> clases) {        StringBuilder sb = new StringBuilder("@startuml\n");        // TODO        return sb.append("@enduml").toString();    }     public static void main(String[] args) {        System.out.println(plantUml(MODELO));    }}

Ejemplo: Modelo de un instituto

Salida esperada

@startuml
abstract class Persona {
  -dni : String
  #nombre : String
  +getNombre() : String
}
interface Evaluable {
  +nota() : double
}
class Alumno {
  -curso : int
  +matricular(Asignatura) : void
  +nota() : double
}
class Profesor {
  -salario : double
}
class Asignatura {
  -codigo : String
  ~horas : int
}
class Matricula {
  -nota : double
}
Alumno --> "*" Matricula : matriculas
Persona <|-- Alumno
Evaluable <|.. Alumno
Profesor --> "*" Asignatura : imparte
Persona <|-- Profesor
Asignatura --> "1" Profesor : titular
Matricula --> "1" Asignatura : asignatura
@enduml

13. Depura el programa de notas

Fácil · Ejercicios de Entornos de Desarrollo · apuntesdam.com/subject/entornos/topic/ejercicios-entornos

Este programa lee N notas y debe mostrar su media con dos decimales y la nota máxima, pero tiene tres errores. Usa el depurador (o trazas) para encontrarlos y corrígelos sin reescribir el programa. Entrada: N y después las N notas.

Código de partida (java)
import java.util.*; public class Main {     // Devuelve la media de las notas (con decimales)    static double media(int[] notas) {        int suma = 0;        for (int i = 1; i <= notas.length - 1; i++) {            suma += notas[i];        }        return suma / notas.length;    }     // Devuelve la nota más alta    static int maxima(int[] notas) {        int max = notas[0];        for (int n : notas) {            if (n < max) max = n;        }        return max;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        int[] notas = new int[n];        for (int i = 0; i < n; i++) notas[i] = sc.nextInt();        System.out.printf(Locale.US, "Media: %.2f%n", media(notas));        System.out.println("Máxima: " + maxima(notas));    }}

Ejemplo: Cuatro notas

Entrada

4
5 7 8 10

Salida esperada

Media: 7.50
Máxima: 10

Ejemplo: Media con decimales

Entrada

3
6 6 7

Salida esperada

Media: 6.33
Máxima: 7

14. Valores límite: clasificar edades

Fácil · Ejercicios de Entornos de Desarrollo · apuntesdam.com/subject/entornos/topic/ejercicios-entornos

Implementa clasificar(edad) según esta especificación: edad < 0 o > 120 → "Error"; 0–11 → "Niño"; 12–17 → "Adolescente"; 18–64 → "Adulto"; 65–120 → "Jubilado". Los tests usan justo los valores límite de cada clase de equivalencia, que es donde suelen esconderse los errores. Entrada: una edad por línea.

Código de partida (java)
import java.util.*; public class Main {     static String clasificar(int edad) {        // TODO: implementa la especificación        return "";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            System.out.println(clasificar(sc.nextInt()));        }    }}

Ejemplo: Límites inferiores

Entrada

-1
0
11
12

Salida esperada

Error
Niño
Niño
Adolescente

Ejemplo: Límites centrales

Entrada

17
18
64
65

Salida esperada

Adolescente
Adulto
Adulto
Jubilado

15. Caja negra: el problema del triángulo

Medio · Ejercicios de Entornos de Desarrollo · apuntesdam.com/subject/entornos/topic/ejercicios-entornos

Clásico de las pruebas de caja negra. Implementa tipo(a, b, c), que recibe las longitudes de tres lados enteros y devuelve "Equilátero", "Isósceles", "Escaleno" o "No es triángulo" (si algún lado es ≤ 0 o si un lado es mayor o igual que la suma de los otros dos). Entrada: tres enteros por línea.

Código de partida (java)
import java.util.*; public class Main {     static String tipo(int a, int b, int c) {        // TODO: clasifica el triángulo        return "";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            System.out.println(tipo(sc.nextInt(), sc.nextInt(), sc.nextInt()));        }    }}

Ejemplo: Tipos válidos

Entrada

3 3 3
3 3 5
3 4 5

Salida esperada

Equilátero
Isósceles
Escaleno

Ejemplo: Casos inválidos

Entrada

0 4 5
1 2 3
-3 4 5

Salida esperada

No es triángulo
No es triángulo
No es triángulo

16. Refactoriza: extraer método

Medio · Ejercicios de Entornos de Desarrollo · apuntesdam.com/subject/entornos/topic/ejercicios-entornos

El cálculo original del precio estaba en un único bloque lleno de números mágicos: p = base * unidades; si VIP p -= 15 %; si SOCIO p -= 5 %; si unidades >= 10 p -= 10 %; p += 21 % de IVA. Ya se ha extraído la estructura en métodos pequeños. Completa descuentoCliente(), descuentoVolumen() y el IVA de precioFinal() para que el comportamiento sea idéntico al original. Entrada por línea: precio base, unidades y tipo de cliente (NORMAL, SOCIO o VIP).

Código de partida (java)
import java.util.*; public class Main {     static final double IVA = 0.21;     static double descuentoCliente(String tipo) {        // TODO: 0.15 para VIP, 0.05 para SOCIO y 0 para el resto        return 0;    }     static double descuentoVolumen(int unidades) {        // TODO: 0.10 a partir de 10 unidades        return 0;    }     static double aplicarDescuento(double importe, double descuento) {        return importe * (1 - descuento);    }     static double precioFinal(double base, int unidades, String tipo) {        double importe = base * unidades;        importe = aplicarDescuento(importe, descuentoCliente(tipo));        importe = aplicarDescuento(importe, descuentoVolumen(unidades));        // TODO: aplica el IVA usando la constante        return importe;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in).useLocale(Locale.US);        while (sc.hasNext()) {            double base = sc.nextDouble();            int unidades = sc.nextInt();            String tipo = sc.next();            System.out.printf(Locale.US, "%.2f%n", precioFinal(base, unidades, tipo));        }    }}

Ejemplo: Cliente normal

Entrada

10 2 NORMAL

Salida esperada

24.20

Ejemplo: Socio con descuento por volumen

Entrada

20 12 SOCIO

Salida esperada

248.29

17. Del diagrama de clases al código

Medio · Ejercicios de Entornos de Desarrollo · apuntesdam.com/subject/entornos/topic/ejercicios-entornos

Implementa el diagrama de clases: la clase abstracta Figura (nombre, area() abstracto y toString() ya hechos) y sus subclases Circulo (radio) y Rectangulo (base, altura), que heredan de ella. El programa lee figuras (C radio o R base altura), las muestra y suma sus áreas. Figura ◁── Circulo (- radio: double) Figura ◁── Rectangulo (- base: double, - altura: double)

Código de partida (java)
import java.util.*; abstract class Figura {    protected String nombre;     public Figura(String nombre) { this.nombre = nombre; }     public abstract double area();     @Override    public String toString() {        return String.format(Locale.US, "%s: %.2f", nombre, area());    }} class Circulo extends Figura {    private double radio;     public Circulo(double radio) {        super("Círculo");        // TODO: guarda el radio    }     @Override    public double area() {        return 0; // TODO: π·r²    }} class Rectangulo extends Figura {    // TODO: atributos base y altura     public Rectangulo(double base, double altura) {        super("Rectángulo");        // TODO: guarda base y altura    }     @Override    public double area() {        return 0; // TODO: base·altura    }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in).useLocale(Locale.US);        List<Figura> figuras = new ArrayList<>();        while (sc.hasNext()) {            String tipo = sc.next();            if (tipo.equals("C")) figuras.add(new Circulo(sc.nextDouble()));            else figuras.add(new Rectangulo(sc.nextDouble(), sc.nextDouble()));        }        double total = 0;        for (Figura f : figuras) {            System.out.println(f);            total += f.area();        }        System.out.printf(Locale.US, "Total: %.2f%n", total);    }}

Ejemplo: Un círculo y un rectángulo

Entrada

C 1
R 3 4

Salida esperada

Círculo: 3.14
Rectángulo: 12.00
Total: 15.14

Programación

18. Del pseudocódigo a Java

Fácil · Fundamentos de la Programación · apuntesdam.com/subject/programacion/topic/introduccion-programacion

Traduce a Java este pseudocódigo: Leer n Si n = 0 entonces Escribir «cero» Si no Si n > 0 entonces signo ← «positivo» si no signo ← «negativo» Si n mod 2 = 0 entonces paridad ← «par» si no paridad ← «impar» Escribir signo, « », paridad Fin si Por ejemplo, para 7 muestra «positivo impar» y para -4, «negativo par».

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        // TODO: traduce el pseudocódigo del enunciado    }}

Ejemplo: Positivo impar

Entrada

7

Salida esperada

positivo impar

Ejemplo: Negativo par

Entrada

-4

Salida esperada

negativo par

Ejemplo: Cero

Entrada

0

Salida esperada

cero

19. Traza del algoritmo de Euclides

Medio · Fundamentos de la Programación · apuntesdam.com/subject/programacion/topic/introduccion-programacion

El algoritmo de Euclides, de hace más de 2000 años, calcula el máximo común divisor de dos números: mientras b no sea 0, se divide a entre b, a pasa a valer b y b pasa a valer el resto. Lee a y b (enteros positivos) y muestra la traza: en cada paso, «Paso k: a = cociente · b + resto»; al final, «mcd = m en n pasos» («1 paso» en singular). Hacer trazas así es la forma de comprobar a mano que un algoritmo es correcto.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int a = sc.nextInt(), b = sc.nextInt();        // TODO: algoritmo de Euclides mostrando la traza    }}

Ejemplo: mcd(1071, 462)

Entrada

1071 462

Salida esperada

Paso 1: 1071 = 2 · 462 + 147
Paso 2: 462 = 3 · 147 + 21
Paso 3: 147 = 7 · 21 + 0
mcd = 21 en 3 pasos

Ejemplo: Divisible

Entrada

48 12

Salida esperada

Paso 1: 48 = 4 · 12 + 0
mcd = 12 en 1 paso

20. Conversor de bases sin atajos

Difícil · Fundamentos de la Programación · apuntesdam.com/subject/programacion/topic/introduccion-programacion

Los ordenadores trabajan en binario y los programadores leemos a menudo hexadecimal (colores #FF8800, direcciones de memoria). Implementa aBase(n, base), que convierte un entero no negativo a cualquier base de 2 a 16 sin usar Integer.toBinaryString ni similares: se divide sucesivamente entre la base y los restos, leídos de abajo arriba, son las cifras (10 → A, 11 → B… 15 → F). El programa lee varios números y muestra cada uno en binario, octal y hexadecimal.

Código de partida (java)
import java.util.Scanner; public class Main {    static String aBase(int n, int base) {        // TODO: divisiones sucesivas entre la base        return "";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            int n = sc.nextInt();            System.out.println(n + " → binario " + aBase(n, 2) + " · octal " + aBase(n, 8) + " · hexadecimal " + aBase(n, 16));        }    }}

Ejemplo: Varios números

Entrada

10
255
0
4096

Salida esperada

10 → binario 1010 · octal 12 · hexadecimal A
255 → binario 11111111 · octal 377 · hexadecimal FF
0 → binario 0 · octal 0 · hexadecimal 0
4096 → binario 1000000000000 · octal 10000 · hexadecimal 1000

21. Simula una máquina con su propio ensamblador

Muy difícil · Fundamentos de la Programación · apuntesdam.com/subject/programacion/topic/introduccion-programacion

Un procesador repite sin parar el ciclo de captación y ejecución: lee la instrucción a la que apunta el contador de programa (PC), la ejecuta y pasa a la siguiente (o salta). Simula una máquina con un acumulador, 16 posiciones de memoria (a 0) y estas instrucciones, una por línea (las líneas vacías y los comentarios con ; se ignoran; el número de línea cuenta desde 0 sin ellas): LOADI v (acumulador = v), LOAD d (acumulador = memoria[d]), STORE d (memoria[d] = acumulador), ADD d y SUB d (suma o resta memoria[d] al acumulador), JMP l (salta a la instrucción l), JZ l (salta si el acumulador es 0), JNEG l (salta si es negativo), PRINT (muestra el acumulador) y HALT (muestra «HALT tras n instrucciones · acumulador = v» y termina). Errores: si el PC se sale del programa, «ERROR: el programa termina sin HALT»; si una instrucción no existe, «ERROR: instrucción desconocida X en la línea l»; y si se han ejecutado ya 1000 instrucciones sin terminar, «ERROR: demasiadas instrucciones (¿bucle infinito?)».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String[]> programa = new ArrayList<>();        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (!l.isEmpty() && !l.startsWith(";")) programa.add(l.split("\\s+"));        }        int[] memoria = new int[16];        int acumulador = 0, pc = 0, ejecutadas = 0;        // TODO: ciclo de captación y ejecución de instrucciones    }}

Ejemplo: Suma de 1 a 5

Entrada

; Suma de 1 a 5 con un bucle
LOADI 5
STORE 0      ; m[0] = contador
LOADI 0
STORE 1      ; m[1] = suma
LOADI 1
STORE 2      ; m[2] = 1 (constante)
LOAD 0       ; 6: bucle
JZ 15
LOAD 1
ADD 0
STORE 1
LOAD 0
SUB 2
STORE 0
JMP 6
LOAD 1       ; 15: fin
PRINT
HALT

Salida esperada

15
HALT tras 56 instrucciones · acumulador = 15

Ejemplo: Cuenta atrás

Entrada

LOADI 3
STORE 0
LOADI 1
STORE 1
LOAD 0
PRINT
SUB 1
STORE 0
JZ 10
JMP 4
HALT

Salida esperada

3
2
1
HALT tras 22 instrucciones · acumulador = 0

22. Desglose de segundos

Fácil · Introducción a Java · apuntesdam.com/subject/programacion/topic/introduccion-java

Lee un número de segundos y desglósalo en días, horas, minutos y segundos usando la división entera (/) y el resto (%). Muestra «N s = d d h h m min s s» y después «Formato reloj: hh:mm:ss (+d días)» («+1 día» en singular) con dos cifras en horas, minutos y segundos.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        long total = sc.nextLong();        // TODO: días, horas, minutos y segundos    }}

Ejemplo: Un día y pico

Entrada

90061

Salida esperada

90061 s = 1 d 1 h 1 min 1 s
Formato reloj: 01:01:01 (+1 día)

Ejemplo: Menos de un minuto

Entrada

45

Salida esperada

45 s = 0 d 0 h 0 min 45 s
Formato reloj: 00:00:45 (+0 días)

23. Las trampas de los tipos numéricos

Medio · Introducción a Java · apuntesdam.com/subject/programacion/topic/introduccion-java

Los tipos de Java tienen límites y reglas que causan errores muy habituales. Lee dos enteros a y b (b distinto de 0) y muestra, en este orden: «int: a * b = r» calculado con int (puede desbordarse); «long: a * b = r» convirtiendo antes a long; «división entera: a / b = r»; «división real: a / b = r» convirtiendo a double antes de dividir; «casting: (int) real = r», truncando esa división real; y «char: 'A' + k = L (código c)», donde k = |a| % 26 y L es la letra que está k posiciones después de la A.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int a = sc.nextInt(), b = sc.nextInt();        // TODO: muestra las seis líneas del enunciado    }}

Ejemplo: Sin desbordamiento

Entrada

7 2

Salida esperada

int: 7 * 2 = 14
long: 7 * 2 = 14
división entera: 7 / 2 = 3
división real: 7 / 2 = 3.5
casting: (int) 3.5 = 3
char: 'A' + 7 = H (código 72)

Ejemplo: Con desbordamiento

Entrada

100000 30000

Salida esperada

int: 100000 * 30000 = -1294967296
long: 100000 * 30000 = 3000000000
división entera: 100000 / 30000 = 3
división real: 100000 / 30000 = 3.3333333333333335
casting: (int) 3.3333333333333335 = 3
char: 'A' + 4 = E (código 69)

24. La cuota de un préstamo

Difícil · Introducción a Java · apuntesdam.com/subject/programacion/topic/introduccion-java

Lee el capital de un préstamo, el interés anual en % y los años. La cuota mensual (sistema francés) es C · i / (1 − (1 + i)^−n), con i el interés mensual (anual / 100 / 12) y n el número de meses; si el interés es 0, la cuota es C / n. Muestra «Cuota mensual: X EUR (n cuotas)», «Total pagado: T EUR · intereses: I EUR» y el desglose de las tres primeras cuotas (o menos si hay menos): en cada una, los intereses son el capital pendiente por i, lo amortizado es la cuota menos los intereses y el pendiente baja en lo amortizado: «Cuota k: intereses a · amortiza b · pendiente c». Todos los importes con dos decimales y punto decimal.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in).useLocale(Locale.US);        double capital = sc.nextDouble();      // euros        double interesAnual = sc.nextDouble(); // en %        int anios = sc.nextInt();        // TODO: cuota mensual, intereses totales y las tres primeras cuotas    }}

Ejemplo: Hipoteca

Entrada

150000 3.5 25

Salida esperada

Cuota mensual: 750.94 EUR (300 cuotas)
Total pagado: 225280.61 EUR · intereses: 75280.61 EUR
Cuota 1: intereses 437.50 · amortiza 313.44 · pendiente 149686.56
Cuota 2: intereses 436.59 · amortiza 314.35 · pendiente 149372.22
Cuota 3: intereses 435.67 · amortiza 315.27 · pendiente 149056.95

Ejemplo: Coche sin intereses

Entrada

12000 0 1

Salida esperada

Cuota mensual: 1000.00 EUR (12 cuotas)
Total pagado: 12000.00 EUR · intereses: 0.00 EUR
Cuota 1: intereses 0.00 · amortiza 1000.00 · pendiente 11000.00
Cuota 2: intereses 0.00 · amortiza 1000.00 · pendiente 10000.00
Cuota 3: intereses 0.00 · amortiza 1000.00 · pendiente 9000.00

25. Permisos de Unix con operadores de bits

Muy difícil · Introducción a Java · apuntesdam.com/subject/programacion/topic/introduccion-java

Los permisos de un fichero en Linux son 9 bits: lectura, escritura y ejecución (r = 4, w = 2, x = 1) para el usuario (bits 8-6), el grupo (5-3) y los demás (2-0); por eso se escriben en octal, como 754. La entrada empieza con los permisos en octal y sigue con órdenes «+bits quién», «-bits quién» o «=bits quién» (bits: combinación de r, w, x; quién: u, g, o o a para todos), como en chmod. Implementa simbolico(permisos), que devuelve la forma rwxr-xr--, y aplica cada orden con operadores de bits: + enciende los bits (|), - los apaga (& con ~) y = deja en ese grupo exactamente esos bits. Tras cada orden se muestra «orden quién → ooo rwx…» (en octal con tres cifras).

Código de partida (java)
import java.util.Scanner; public class Main {    static String simbolico(int permisos) {        // TODO: rwx por cada bit encendido, - por cada apagado        return "";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int permisos = Integer.parseInt(sc.next(), 8);      // p. ej. 754 en octal        System.out.println("Inicial: " + String.format("%03o", permisos) + " " + simbolico(permisos));        while (sc.hasNext()) {            String orden = sc.next();                       // +x, -w, =r…            String quien = sc.next();                        // u, g, o o a            // TODO: aplicar la orden con operadores de bits            System.out.println(orden + " " + quien + " → " + String.format("%03o", permisos) + " " + simbolico(permisos));        }    }}

Ejemplo: Ficheros de una web

Entrada

644
+x u
-r o
=rw g
+w a

Salida esperada

Inicial: 644 rw-r--r--
+x u → 744 rwxr--r--
-r o → 740 rwxr-----
=rw g → 760 rwxrw----
+w a → 762 rwxrw--w-

Ejemplo: Script

Entrada

700
+rx a
-w u
=x o

Salida esperada

Inicial: 700 rwx------
+rx a → 755 rwxr-xr-x
-w u → 555 r-xr-xr-x
=x o → 551 r-xr-x--x

26. Calificación a partir de la nota

Fácil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

Lee una nota entera por teclado y muestra su calificación: Suspenso (0-4), Aprobado (5), Bien (6), Notable (7-8) o Sobresaliente (9-10). Si la nota no está entre 0 y 10, muestra 'Nota no valida'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int nota = sc.nextInt();         // TODO: muestra la calificación correspondiente        System.out.println("?");    }}

Ejemplo: Nota 4 → Suspenso

Entrada

4

Salida esperada

Suspenso

Ejemplo: Nota 6 → Bien

Entrada

6

Salida esperada

Bien

Ejemplo: Nota 9 → Sobresaliente

Entrada

9

Salida esperada

Sobresaliente

27. Encuentra el fallo: años bisiestos

Fácil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

El programa lee varios años y debe decir si cada uno es bisiesto: lo es si es divisible entre 4, salvo los divisibles entre 100 que no lo son entre 400 (1900 no es bisiesto; 2000 sí). Hay años que salen mal. Corrige la condición.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            int a = sc.nextInt();            boolean bisiesto = a % 4 == 0 || a % 100 != 0 || a % 400 == 0;            System.out.println(a + (bisiesto ? ": bisiesto" : ": no bisiesto"));        }    }}

Ejemplo: 2024, 2023, 1900 y 2000

Entrada

2024
2023
1900
2000

Salida esperada

2024: bisiesto
2023: no bisiesto
1900: no bisiesto
2000: bisiesto

28. Encuentra el fallo: la nota en palabras

Fácil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

El programa convierte una nota entera (0 a 10) en su calificación: 0-4 Insuficiente, 5 Suficiente, 6 Bien, 7-8 Notable, 9-10 Sobresaliente. Con un 6 escribe varias palabras en lugar de una. Corrígelo.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        int nota = new Scanner(System.in).nextInt();        switch (nota) {            case 5:                System.out.println("Suficiente");                break;            case 6:                System.out.println("Bien");            case 7:            case 8:                System.out.println("Notable");                break;            case 9:            case 10:                System.out.println("Sobresaliente");                break;            default:                System.out.println("Insuficiente");        }    }}

Ejemplo: Un 6

Entrada

6

Salida esperada

Bien

Ejemplo: Un 8

Entrada

8

Salida esperada

Notable

29. Año bisiesto

Medio · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

Lee un año y di si es bisiesto. Un año es bisiesto si es divisible entre 4, excepto los divisibles entre 100, salvo que también lo sean entre 400. Formato: '2024 es bisiesto' o '2023 no es bisiesto'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int anio = sc.nextInt();         boolean bisiesto = false; // TODO: calcula si es bisiesto         if (bisiesto) {            System.out.println(anio + " es bisiesto");        } else {            System.out.println(anio + " no es bisiesto");        }    }}

Ejemplo: 2024 es bisiesto

Entrada

2024

Salida esperada

2024 es bisiesto

Ejemplo: 1900 no es bisiesto (divisible entre 100)

Entrada

1900

Salida esperada

1900 no es bisiesto

30. Validar una fecha y calcular el día siguiente

Difícil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

Lee fechas «día mes año» (una por línea) y, para cada una, muestra «dd/mm/aaaa: no válida (el mes debe estar entre 1 y 12)», «dd/mm/aaaa: no válida (ese mes tiene N días)» o «dd/mm/aaaa → siguiente: dd/mm/aaaa», añadiendo « (año bisiesto)» si el año de la fecha lo es. Un año es bisiesto si es divisible entre 4 y no entre 100, o si es divisible entre 400 (2024 y 2000 lo son; 1900 no). Abril, junio, septiembre y noviembre tienen 30 días y febrero 28 o 29.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            int d = sc.nextInt(), m = sc.nextInt(), a = sc.nextInt();            // TODO: validar la fecha y calcular el día siguiente        }    }}

Ejemplo: Fechas variadas

Entrada

15 3 2025
28 2 2024
29 2 2023
31 12 1999
31 4 2025
10 13 2025

Salida esperada

15/03/2025 → siguiente: 16/03/2025
28/02/2024 → siguiente: 29/02/2024 (año bisiesto)
29/02/2023: no válida (ese mes tiene 28 días)
31/12/1999 → siguiente: 01/01/2000
31/04/2025: no válida (ese mes tiene 30 días)
10/13/2025: no válida (el mes debe estar entre 1 y 12)

31. IRPF por tramos (simplificado)

Muy difícil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

El IRPF es progresivo: cada tramo de la base se grava a un tipo distinto. Implementa escala(base) con la escala estatal y autonómica sumadas (simplificada): hasta 12 450 € al 19 %; de 12 450 a 20 200 al 24 %; hasta 35 200 al 30 %; hasta 60 000 al 37 %; hasta 300 000 al 45 %; y más al 47 %. Cada tramo se suma a lo acumulado en los anteriores (por ejemplo, escala(20 200) = 2 365,50 + 0,24 · 7 750). Lee el salario bruto anual, el número de hijos y la edad. Seguridad Social = 6,35 % del bruto; base = bruto − Seguridad Social − 2 000 (gastos), sin bajar de 0. Mínimo personal y familiar = 5 550, +1 150 si tiene más de 65 años, +2 400 por el primer hijo, +2 700 por el segundo, +4 000 por el tercero y +4 500 por cada uno a partir del cuarto. Cuota = escala(base) − escala(mínimo), o 0 si el mínimo no es menor que la base. Muestra «Base liquidable», «Mínimo personal y familiar», «Cuota de IRPF», «Tipo efectivo: x %» (cuota sobre el bruto) y «Neto anual: … · al mes (14 pagas): …», todo con dos decimales y punto decimal.

Código de partida (java)
import java.util.*; public class Main {    static double escala(double base) {        // TODO: impuesto por tramos de una base        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in).useLocale(Locale.US);        double bruto = sc.nextDouble();        int hijos = sc.nextInt();        int edad = sc.nextInt();        // TODO: base, mínimo personal y familiar, cuota, tipo efectivo y neto    }}

Ejemplo: Sueldo medio sin hijos

Entrada

28000 0 30

Salida esperada

Base liquidable: 24222.00
Mínimo personal y familiar: 5550.00
Cuota de IRPF: 4377.60
Tipo efectivo: 15.63 %
Neto anual: 21844.40 · al mes (14 pagas): 1560.31

Ejemplo: Dos hijos

Entrada

45000 2 42

Salida esperada

Base liquidable: 40142.50
Mínimo personal y familiar: 10650.00
Cuota de IRPF: 8530.73
Tipo efectivo: 18.96 %
Neto anual: 33611.78 · al mes (14 pagas): 2400.84

Ejemplo: Sueldo bajo

Entrada

14000 1 25

Salida esperada

Base liquidable: 11111.00
Mínimo personal y familiar: 7950.00
Cuota de IRPF: 600.59
Tipo efectivo: 4.29 %
Neto anual: 12510.41 · al mes (14 pagas): 893.60

32. Tabla de multiplicar

Fácil · Bucles · apuntesdam.com/subject/programacion/topic/bucles

Lee un número n y muestra su tabla de multiplicar del 1 al 10 con el formato 'n x i = resultado', una línea por operación.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();         // TODO: muestra la tabla de multiplicar de n    }}

Ejemplo: Tabla del 7

Entrada

7

Salida esperada

7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70

33. Encuentra el fallo: la suma de 1 a n

Fácil · Bucles · apuntesdam.com/subject/programacion/topic/bucles

El programa lee n y debe mostrar la suma 1 + 2 + … + n. Para n = 4 debería salir 10, pero sale 6. Corrige el bucle.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        int n = new Scanner(System.in).nextInt();        int suma = 0;        for (int i = 1; i < n; i++) {            suma += i;        }        System.out.println("Suma: " + suma);    }}

Ejemplo: n = 4

Entrada

4

Salida esperada

Suma: 10

34. Suma de los dígitos

Medio · Bucles · apuntesdam.com/subject/programacion/topic/bucles

Lee un número entero positivo y calcula la suma de sus dígitos usando un bucle while. Formato: 'Suma: X'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        int suma = 0;         // TODO: suma los dígitos de n con un bucle while         System.out.println("Suma: " + suma);    }}

Ejemplo: 1234 → 10

Entrada

1234

Salida esperada

Suma: 10

Ejemplo: 9 → 9

Entrada

9

Salida esperada

Suma: 9

35. Encuentra el fallo: la media que pierde decimales

Medio · Bucles · apuntesdam.com/subject/programacion/topic/bucles

El programa lee números enteros hasta un 0 y muestra su media con dos decimales. Con 7, 8 y 8 debería salir 7,67 (en formato 7.67) pero sale 7.00. Corrígelo sin cambiar el tipo de los números leídos.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int suma = 0, contador = 0;        int n = sc.nextInt();        while (n != 0) {            suma += n;            contador++;            n = sc.nextInt();        }        double media = suma / contador;        System.out.printf(java.util.Locale.US, "Media: %.2f%n", media);    }}

Ejemplo: 7, 8 y 8

Entrada

7 8 8 0

Salida esperada

Media: 7.67

36. La conjetura de Collatz

Difícil · Bucles · apuntesdam.com/subject/programacion/topic/bucles

Toma un número: si es par, divídelo entre 2; si es impar, multiplícalo por 3 y súmale 1. Repite. La conjetura de Collatz dice que siempre se acaba llegando a 1, y nadie ha conseguido demostrarlo. Lee a y b (1 ≤ a ≤ b). Muestra la secuencia de a separada por « → » y después «Entre a y b: m necesita p pasos; el valor más alto alcanzado es t», donde m es el número de ese intervalo que más pasos necesita para llegar a 1 (el menor si hay empate) y t el mayor valor que aparece en cualquiera de esas secuencias. Usa long: los valores intermedios crecen mucho.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int a = sc.nextInt(), b = sc.nextInt();        // TODO: secuencia de a y número con más pasos entre a y b    }}

Ejemplo: Del 6 al 10

Entrada

6 10

Salida esperada

6 → 3 → 10 → 5 → 16 → 8 → 4 → 2 → 1
Entre 6 y 10: 9 necesita 19 pasos; el valor más alto alcanzado es 52

Ejemplo: Del 27 al 30

Entrada

27 30

Salida esperada

27 → 82 → 41 → 124 → 62 → 31 → 94 → 47 → 142 → 71 → 214 → 107 → 322 → 161 → 484 → 242 → 121 → 364 → 182 → 91 → 274 → 137 → 412 → 206 → 103 → 310 → 155 → 466 → 233 → 700 → 350 → 175 → 526 → 263 → 790 → 395 → 1186 → 593 → 1780 → 890 → 445 → 1336 → 668 → 334 → 167 → 502 → 251 → 754 → 377 → 1132 → 566 → 283 → 850 → 425 → 1276 → 638 → 319 → 958 → 479 → 1438 → 719 → 2158 → 1079 → 3238 → 1619 → 4858 → 2429 → 7288 → 3644 → 1822 → 911 → 2734 → 1367 → 4102 → 2051 → 6154 → 3077 → 9232 → 4616 → 2308 → 1154 → 577 → 1732 → 866 → 433 → 1300 → 650 → 325 → 976 → 488 → 244 → 122 → 61 → 184 → 92 → 46 → 23 → 70 → 35 → 106 → 53 → 160 → 80 → 40 → 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1
Entre 27 y 30: 27 necesita 111 pasos; el valor más alto alcanzado es 9232

37. El calendario de un mes

Muy difícil · Bucles · apuntesdam.com/subject/programacion/topic/bucles

Lee un año y un mes y muestra su calendario como el de la orden cal de Linux, empezando la semana en lunes: el nombre del mes y el año; la cabecera «Lu Ma Mi Ju Vi Sá Do»; y las semanas, cada día con dos caracteres alineado a la derecha y separado por un espacio, dejando huecos de tres espacios antes del día 1 (sin espacios al final de las líneas). Termina con «N días · F de fin de semana · empieza en día». Para saber en qué día de la semana cae el día 1 usa la congruencia de Zeller: con m = mes e y = año, pero contando enero y febrero como los meses 13 y 14 del año anterior, k = y % 100 y j = y / 100, h = (1 + 13·(m+1)/5 + k + k/4 + j/4 + 5·j) % 7 da 0 = sábado, 1 = domingo, 2 = lunes… (divisiones enteras).

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int anio = sc.nextInt(), mes = sc.nextInt();        String[] nombres = {"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto",                            "Septiembre", "Octubre", "Noviembre", "Diciembre"};        // TODO: días del mes, día de la semana del día 1 y la cuadrícula    }}

Ejemplo: Octubre de 2025

Entrada

2025 10

Salida esperada

Octubre 2025
Lu Ma Mi Ju Vi Sá Do
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
31 días · 8 de fin de semana · empieza en miércoles

Ejemplo: Febrero bisiesto

Entrada

2024 2

Salida esperada

Febrero 2024
Lu Ma Mi Ju Vi Sá Do
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29
29 días · 8 de fin de semana · empieza en jueves

Ejemplo: Septiembre de 2025

Entrada

2025 9

Salida esperada

Septiembre 2025
Lu Ma Mi Ju Vi Sá Do
 1  2  3  4  5  6  7
 8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
30 días · 8 de fin de semana · empieza en lunes

38. Números primos con una función

Medio · Métodos · apuntesdam.com/subject/programacion/topic/metodos

Completa la función esPrimo(int n) y úsala para mostrar, separados por espacios, todos los números primos desde 2 hasta el número leído.

Código de partida (java)
import java.util.Scanner; public class Main {     static boolean esPrimo(int n) {        // TODO: devuelve true si n es primo        return false;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int limite = sc.nextInt();        StringBuilder sb = new StringBuilder();        for (int i = 2; i <= limite; i++) {            if (esPrimo(i)) sb.append(i).append(" ");        }        System.out.println(sb.toString().trim());    }}

Ejemplo: Primos hasta 10

Entrada

10

Salida esperada

2 3 5 7

Ejemplo: Primos hasta 20

Entrada

20

Salida esperada

2 3 5 7 11 13 17 19

39. Factorial recursivo

Medio · Métodos · apuntesdam.com/subject/programacion/topic/metodos

Implementa la función recursiva factorial(int n) que devuelva n! como long. El programa muestra 'n! = resultado'. Recuerda que 0! = 1.

Código de partida (java)
import java.util.Scanner; public class Main {     static long factorial(int n) {        // TODO: implementa el factorial de forma recursiva        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        System.out.println(n + "! = " + factorial(n));    }}

Ejemplo: 5! = 120

Entrada

5

Salida esperada

5! = 120

Ejemplo: 0! = 1

Entrada

0

Salida esperada

0! = 1

40. Encuentra el fallo: el método que no cambia nada

Medio · Métodos · apuntesdam.com/subject/programacion/topic/metodos

El método aplicarDescuento debería rebajar el precio un porcentaje, pero el programa sigue mostrando el precio original. Corrige el método y su llamada para que el precio cambie.

Código de partida (java)
public class Main {    static void aplicarDescuento(double precio, int porcentaje) {        precio = precio - precio * porcentaje / 100;    }     public static void main(String[] args) {        double precio = 50;        aplicarDescuento(precio, 20);        System.out.println("Precio final: " + precio);    }}

Ejemplo: 50 € con un 20 %

Salida esperada

Precio final: 40.0

41. Las torres de Hanói

Difícil · Métodos · apuntesdam.com/subject/programacion/topic/metodos

Hay tres postes (A, B y C) y n discos de distinto tamaño apilados en A, del mayor abajo al menor arriba. Hay que pasarlos todos a C moviendo un disco cada vez y sin poner nunca uno grande encima de uno pequeño. La solución recursiva es sorprendentemente corta: para mover n discos de origen a destino, se mueven los n−1 de encima al poste auxiliar, se mueve el disco n al destino y se vuelven a mover los n−1 encima de él. Completa hanoi para que cuente los movimientos y, si mostrar es cierto (n ≤ 4), escriba cada uno como «Disco k: X → Y». El main ya muestra el total y lo compara con 2^n − 1.

Código de partida (java)
import java.util.Scanner; public class Main {    static int movimientos = 0;     static void hanoi(int n, char origen, char destino, char auxiliar, boolean mostrar) {        // TODO: mover n discos de origen a destino usando auxiliar    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        hanoi(n, 'A', 'C', 'B', n <= 4);        System.out.println(n + (n == 1 ? " disco: " : " discos: ") + movimientos + (movimientos == 1 ? " movimiento" : " movimientos") + " (2^" + n + " - 1 = " + ((1L << n) - 1) + ")");    }}

Ejemplo: Tres discos

Entrada

3

Salida esperada

Disco 1: A → C
Disco 2: A → B
Disco 1: C → B
Disco 3: A → C
Disco 1: B → A
Disco 2: B → C
Disco 1: A → C
3 discos: 7 movimientos (2^3 - 1 = 7)

Ejemplo: Un disco

Entrada

1

Salida esperada

Disco 1: A → C
1 disco: 1 movimiento (2^1 - 1 = 1)

42. Una calculadora con descenso recursivo

Muy difícil · Métodos · apuntesdam.com/subject/programacion/topic/metodos

Los compiladores leen las expresiones con analizadores recursivos. Implementa uno para expresiones de enteros (long) con +, −, *, / (división entera), % , paréntesis y el signo menos delante de un número o de un paréntesis, respetando la prioridad: primero paréntesis y signo, después * / %, y por último + −, de izquierda a derecha. Se usa una gramática con tres métodos que se llaman entre sí: expresion() lee términos separados por + o −; termino() lee factores separados por *, / o %; factor() lee un número, un − seguido de otro factor o una expresión entre paréntesis (llamando otra vez a expresion: ahí está la recursividad). texto es la línea sin espacios y pos la posición actual. Los errores se lanzan con throw new ErrorExpresion(mensaje): «división entre cero», «falta «)»», «la expresión termina antes de tiempo» y «se esperaba un número en la posición p». El main ya muestra «expresión = resultado» o «expresión → error: mensaje».

Código de partida (java)
import java.util.Scanner; public class Main {    static String texto;    static int pos;     static class ErrorExpresion extends RuntimeException {        ErrorExpresion(String mensaje) { super(mensaje); }    }     static long expresion() {        // TODO: términos separados por + o -        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine();            if (linea.isBlank()) continue;            texto = linea.replace(" ", "");            pos = 0;            try {                long valor = expresion();                if (pos < texto.length()) throw new ErrorExpresion("sobra «" + texto.charAt(pos) + "» en la posición " + pos);                System.out.println(linea.trim() + " = " + valor);            } catch (ErrorExpresion e) {                System.out.println(linea.trim() + " → error: " + e.getMessage());            }        }    }}

Ejemplo: Expresiones variadas

Entrada

2 + 3 * 4
(2 + 3) * 4
100 / 7 % 3
-(8 - 10) * -3
2 * (3 + (4 - 1) * 5) - 6
7 / (3 - 3)
(1 + 2
4 + * 5

Salida esperada

2 + 3 * 4 = 14
(2 + 3) * 4 = 20
100 / 7 % 3 = 2
-(8 - 10) * -3 = -6
2 * (3 + (4 - 1) * 5) - 6 = 30
7 / (3 - 3) → error: división entre cero
(1 + 2 → error: falta «)»
4 + * 5 → error: se esperaba un número en la posición 2

43. Máximo y mínimo de un array

Fácil · Arrays · apuntesdam.com/subject/programacion/topic/arrays

Dado un array de enteros, encuentra el valor máximo, el valor mínimo y las posiciones donde se encuentran. Imprime cada resultado en una línea separada con el formato: 'Maximo: X (posicion Y)' y 'Minimo: X (posicion Y)'.

Código de partida (java)
public class Main {    public static void main(String[] args) {        int[] numeros = {34, 12, 78, 5, 90, 23, 67, 45};         // TODO: encuentra el máximo y su posición        int max = numeros[0];        int posMax = 0;        // Tu código aquí         // TODO: encuentra el mínimo y su posición        int min = numeros[0];        int posMin = 0;        // Tu código aquí         System.out.println("Maximo: " + max + " (posicion " + posMax + ")");        System.out.println("Minimo: " + min + " (posicion " + posMin + ")");    }}

Ejemplo: Array {34,12,78,5,90,23,67,45} → máximo 90 en pos 4, mínimo 5 en pos 3

Salida esperada

Maximo: 90 (posicion 4)
Minimo: 5 (posicion 3)

44. Encuentra el fallo: el máximo de temperaturas bajo cero

Fácil · Arrays · apuntesdam.com/subject/programacion/topic/arrays

El programa lee varias temperaturas y muestra la más alta. Funciona en verano, pero en invierno, con todas bajo cero, dice que la máxima es 0. Corrígelo.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        int[] t = new int[n];        for (int i = 0; i < n; i++) t[i] = sc.nextInt();        int max = 0;        for (int x : t) {            if (x > max) max = x;        }        System.out.println("Máxima: " + max);    }}

Ejemplo: Verano

Entrada

4
31 28 35 30

Salida esperada

Máxima: 35

Ejemplo: Invierno

Entrada

3
-5 -2 -9

Salida esperada

Máxima: -2

45. Invertir un array

Medio · Arrays · apuntesdam.com/subject/programacion/topic/arrays

Invierte el contenido de un array sin usar un segundo array auxiliar. El programa debe imprimir el array original y el array invertido usando Arrays.toString().

Código de partida (java)
import java.util.Arrays; public class Main {    public static void main(String[] args) {        int[] array = {1, 2, 3, 4, 5, 6, 7, 8};         System.out.println("Original:  " + Arrays.toString(array));         // TODO: invierte el array en su lugar (sin array auxiliar)        // Tu código aquí         System.out.println("Invertido: " + Arrays.toString(array));    }}

Ejemplo: Invertir {1,2,3,4,5,6,7,8} → {8,7,6,5,4,3,2,1}

Salida esperada

Original:  [1, 2, 3, 4, 5, 6, 7, 8]
Invertido: [8, 7, 6, 5, 4, 3, 2, 1]

46. Estadísticas completas de notas

Medio · Arrays · apuntesdam.com/subject/programacion/topic/arrays

Dado un array de notas (valores entre 0 y 10), calcula y muestra: la media con dos decimales, cuántos alumnos han aprobado (nota >= 5), cuántos han suspendido, la nota más alta y la nota más baja.

Código de partida (java)
public class Main {    public static void main(String[] args) {        double[] notas = {8.5, 4.0, 6.5, 9.0, 3.5, 7.0, 5.0, 8.0, 2.5, 6.0};         // TODO: calcula todas las estadísticas en un solo recorrido        double suma = 0;        int aprobados = 0;        int suspensos = 0;        double maxima = notas[0];        double minima = notas[0];         // Tu bucle aquí         double media = suma / notas.length;         System.out.printf("Media: %.2f%n", media);        System.out.println("Aprobados: " + aprobados);        System.out.println("Suspensos: " + suspensos);        System.out.printf("Nota maxima: %.1f%n", maxima);        System.out.printf("Nota minima: %.1f%n", minima);    }}

Ejemplo: Estadísticas correctas para {8.5,4.0,6.5,9.0,3.5,7.0,5.0,8.0,2.5,6.0}

Salida esperada

Media: 6.00
Aprobados: 7
Suspensos: 3
Nota maxima: 9.0
Nota minima: 2.5

47. Suma de filas y columnas de una matriz

Difícil · Arrays · apuntesdam.com/subject/programacion/topic/arrays

Dada una matriz de enteros 3x3, calcula e imprime la suma de cada fila y la suma de cada columna. Formato: 'Fila 0: X', 'Fila 1: Y', 'Fila 2: Z', 'Col 0: A', 'Col 1: B', 'Col 2: C'.

Código de partida (java)
public class Main {    public static void main(String[] args) {        int[][] matriz = {            {1, 2, 3},            {4, 5, 6},            {7, 8, 9}        };         // TODO: imprime la suma de cada fila        // Fila 0: 6, Fila 1: 15, Fila 2: 24        for (int i = 0; i < matriz.length; i++) {            int sumaFila = 0;            // Tu código aquí            System.out.println("Fila " + i + ": " + sumaFila);        }         // TODO: imprime la suma de cada columna        // Col 0: 12, Col 1: 15, Col 2: 18        for (int j = 0; j < matriz[0].length; j++) {            int sumaCol = 0;            // Tu código aquí            System.out.println("Col " + j + ": " + sumaCol);        }    }}

Ejemplo: Sumas de {{1,2,3},{4,5,6},{7,8,9}}

Salida esperada

Fila 0: 6
Fila 1: 15
Fila 2: 24
Col 0: 12
Col 1: 15
Col 2: 18

48. El juego de la vida de Conway

Muy difícil · Arrays · apuntesdam.com/subject/programacion/topic/arrays

En una cuadrícula, cada célula está viva (#) o muerta (.). En cada generación, todas cambian a la vez según sus 8 vecinas: una viva sigue viva si tiene 2 o 3 vecinas vivas (si no, muere); una muerta nace si tiene exactamente 3. Fuera de la cuadrícula no hay células. La entrada es el número de generaciones y las filas de la cuadrícula inicial. Implementa siguiente(t), que devuelve una matriz nueva con la siguiente generación, y simula: si una generación es igual a la anterior, muestra «Estable desde la generación g» (g es la última que cambió, contando la inicial como 0) y para. Al final muestra la cuadrícula y «Células vivas: n».

Código de partida (java)
import java.util.*; public class Main {    static boolean[][] siguiente(boolean[][] t) {        // TODO: calcular la siguiente generación        return t;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int generaciones = sc.nextInt();        List<String> filas = new ArrayList<>();        while (sc.hasNext()) filas.add(sc.next());        boolean[][] t = new boolean[filas.size()][filas.get(0).length()];        for (int i = 0; i < t.length; i++)            for (int j = 0; j < t[0].length; j++) t[i][j] = filas.get(i).charAt(j) == '#';        // TODO: avanzar las generaciones, detectar si se estabiliza y mostrar el resultado    }}

Ejemplo: Planeador

Entrada

4
......
..#...
...#..
.###..
......
......

Salida esperada

......
......
...#..
....#.
..###.
......
Células vivas: 5

Ejemplo: Parpadeador

Entrada

3
.....
..#..
..#..
..#..
.....

Salida esperada

.....
.....
.###.
.....
.....
Células vivas: 3

Ejemplo: Bloque estable

Entrada

10
....
.##.
.##.
....

Salida esperada

Estable desde la generación 0
....
.##.
.##.
....
Células vivas: 4

49. Contar vocales

Fácil · Strings · apuntesdam.com/subject/programacion/topic/strings

Lee una frase y cuenta cuántas vocales (a, e, i, o, u, en mayúscula o minúscula) contiene. Formato: 'Vocales: N'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String frase = sc.nextLine();        int vocales = 0;         // TODO: cuenta las vocales de la frase         System.out.println("Vocales: " + vocales);    }}

Ejemplo: 'Hola Mundo'

Entrada

Hola Mundo

Salida esperada

Vocales: 4

Ejemplo: 'programacion'

Entrada

programacion

Salida esperada

Vocales: 5

50. Encuentra el fallo: la contraseña que nunca es correcta

Fácil · Strings · apuntesdam.com/subject/programacion/topic/strings

El programa lee una contraseña y debe decir «Acceso concedido» si es «java2025». Aunque se escriba bien, siempre dice «Acceso denegado». Corrígelo.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        String clave = new Scanner(System.in).nextLine();        if (clave == "java2025") {            System.out.println("Acceso concedido");        } else {            System.out.println("Acceso denegado");        }    }}

Ejemplo: Contraseña correcta

Entrada

java2025

Salida esperada

Acceso concedido

Ejemplo: Contraseña incorrecta

Entrada

java2024

Salida esperada

Acceso denegado

51. Palíndromos

Medio · Strings · apuntesdam.com/subject/programacion/topic/strings

Lee una frase y di si es palíndroma ignorando espacios y mayúsculas. Muestra 'Es palindromo' o 'No es palindromo'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String frase = sc.nextLine();         boolean palindromo = false; // TODO         System.out.println(palindromo ? "Es palindromo" : "No es palindromo");    }}

Ejemplo: 'Anita lava la tina'

Entrada

Anita lava la tina

Salida esperada

Es palindromo

Ejemplo: 'Java'

Entrada

Java

Salida esperada

No es palindromo

52. Encuentra el fallo: el palíndromo exigente

Medio · Strings · apuntesdam.com/subject/programacion/topic/strings

Una frase es palíndroma si se lee igual al revés sin contar mayúsculas ni espacios. El programa dice que «Anita lava la tina» no lo es. Corrígelo para que ignore mayúsculas y espacios.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        String frase = new Scanner(System.in).nextLine();        String limpia = frase;        String inversa = new StringBuilder(limpia).reverse().toString();        System.out.println(limpia.equals(inversa) ? "Es palíndromo" : "No es palíndromo");    }}

Ejemplo: Anita lava la tina

Entrada

Anita lava la tina

Salida esperada

Es palíndromo

Ejemplo: No palíndromo

Entrada

Hola mundo

Salida esperada

No es palíndromo

53. El cifrado de Vigenère

Difícil · Strings · apuntesdam.com/subject/programacion/topic/strings

El cifrado de Vigenère (siglo XVI) desplaza cada letra tantas posiciones como indica la letra de una clave que se repite: con la clave LIMON, la primera letra se desplaza 11 (L), la segunda 8 (I), etc. La entrada es «cifrar» o «descifrar», la clave (solo letras) y el texto. Implementa vigenere: las letras del alfabeto inglés (A-Z, a-z) se desplazan respetando mayúsculas y minúsculas y dando la vuelta al llegar a la Z; cualquier otro carácter (espacios, signos, ñ, letras con tilde) se copia sin cambios y no consume letra de la clave. Para descifrar se desplaza en sentido contrario.

Código de partida (java)
import java.util.Scanner; public class Main {    static String vigenere(String texto, String clave, boolean cifrar) {        // TODO: desplazar cada letra según la letra de la clave que le toca        return texto;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String modo = sc.nextLine().trim(), clave = sc.nextLine().trim(), texto = sc.nextLine();        System.out.println(vigenere(texto, clave, modo.equals("cifrar")));    }}

Ejemplo: Cifrar

Entrada

cifrar
LIMON
Ataque al amanecer

Salida esperada

Lbmehp ix ozlvqqrc

Ejemplo: Descifrar

Entrada

descifrar
LIMON
Lbmehp ix ozlvqqrc

Salida esperada

Ataque al amanecer

54. Justificar un texto

Muy difícil · Strings · apuntesdam.com/subject/programacion/topic/strings

Los procesadores de textos justifican los párrafos: reparten los espacios para que todas las líneas ocupen el mismo ancho. La primera línea de la entrada es el ancho y el resto, el texto (que puede venir en varias líneas). Reparte las palabras en líneas metiendo en cada una todas las que quepan con un espacio entre ellas. Justifica cada línea añadiendo espacios entre palabras hasta llegar al ancho; si los espacios no se reparten por igual, los primeros huecos reciben uno más. La última línea y las que solo tienen una palabra van alineadas a la izquierda. Muestra cada línea entre barras «|…|» (rellenando con espacios hasta el ancho) y al final «n líneas de ancho caracteres». Si alguna palabra es más larga que el ancho, muestra «Error: la palabra «p» no cabe en ancho caracteres» y nada más.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int ancho = Integer.parseInt(sc.nextLine().trim());        StringBuilder todo = new StringBuilder();        while (sc.hasNextLine()) todo.append(sc.nextLine()).append(' ');        String[] palabras = todo.toString().trim().split("\\s+");        // TODO: repartir en líneas y justificarlas    }}

Ejemplo: Ancho 30

Entrada

30
Un buen programa no es el que funciona, sino el que
cualquier compañero puede leer y cambiar sin miedo dentro de
seis meses.

Salida esperada

|Un  buen programa no es el que|
|funciona,    sino    el    que|
|cualquier compañero puede leer|
|y  cambiar sin miedo dentro de|
|seis meses.                   |
5 líneas de 30 caracteres

Ejemplo: Ancho 16

Entrada

16
El veloz murciélago hindú comía feliz cardillo y kiwi.

Salida esperada

|El         veloz|
|murciélago hindú|
|comía      feliz|
|cardillo y kiwi.|
4 líneas de 16 caracteres

55. Encuentra el fallo: el alumno sin nombre

Fácil · Clases y Objetos · apuntesdam.com/subject/programacion/topic/clases

La clase Alumno guarda el nombre y la nota, pero al imprimir un alumno sale «null: 0.0». Corrige el constructor.

Código de partida (java)
public class Main {    static class Alumno {        private String nombre;        private double nota;         Alumno(String nombre, double nota) {            nombre = nombre;            nota = nota;        }         @Override        public String toString() {            return nombre + ": " + nota;        }    }     public static void main(String[] args) {        System.out.println(new Alumno("Lucía", 8.5));        System.out.println(new Alumno("Mario", 6.0));    }}

Ejemplo: Dos alumnos

Salida esperada

Lucía: 8.5
Mario: 6.0

56. Una clase Fraccion inmutable

Difícil · Clases y Objetos · apuntesdam.com/subject/programacion/topic/clases

Completa la clase Fraccion. El constructor debe lanzar IllegalArgumentException("denominador 0") si el denominador es 0, dejar el signo en el numerador (1/-2 pasa a -1/2) y simplificar con el máximo común divisor (4/8 pasa a 1/2). Los métodos sumar, restar, multiplicar y dividir devuelven una fracción nueva sin modificar las originales; dividir entre una fracción que vale 0 lanza ArithmeticException("división entre cero"). toString muestra «num/den», o solo el numerador si el denominador es 1. El main ya lee las operaciones (por ejemplo «1/2 + 1/3») y muestra los resultados o los errores.

Código de partida (java)
import java.util.Scanner; class Fraccion {    private final long num;    private final long den;     Fraccion(long num, long den) {        // TODO: rechazar denominador 0, simplificar y dejar el signo en el numerador        this.num = num;        this.den = den;    }     private static long mcd(long a, long b) {        return b == 0 ? Math.abs(a) : mcd(b, a % b);    }     Fraccion sumar(Fraccion o) {        // TODO        return this;    }     Fraccion restar(Fraccion o) {        // TODO        return this;    }     Fraccion multiplicar(Fraccion o) {        // TODO        return this;    }     Fraccion dividir(Fraccion o) {        // TODO: lanzar ArithmeticException("división entre cero") si o vale 0        return this;    }     @Override    public String toString() {        // TODO: si el denominador es 1, solo el numerador        return num + "/" + den;    }} public class Main {    static Fraccion leer(String s) {        String[] p = s.split("/");        return new Fraccion(Long.parseLong(p[0]), p.length > 1 ? Long.parseLong(p[1]) : 1);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = Integer.parseInt(sc.nextLine().trim());        for (int i = 0; i < n; i++) {            String linea = sc.nextLine().trim();            String[] t = linea.split(" ");            try {                Fraccion a = leer(t[0]), b = leer(t[2]);                Fraccion r = switch (t[1]) {                    case "+" -> a.sumar(b);                    case "-" -> a.restar(b);                    case "*" -> a.multiplicar(b);                    default -> a.dividir(b);                };                System.out.println(linea + " = " + r);            } catch (ArithmeticException | IllegalArgumentException e) {                System.out.println(linea + " → Error: " + e.getMessage());            }        }    }}

Ejemplo: Las cuatro operaciones

Entrada

4
1/2 + 1/3
3/4 - 1/4
2/3 * 9/4
1/2 / 3/4

Salida esperada

1/2 + 1/3 = 5/6
3/4 - 1/4 = 1/2
2/3 * 9/4 = 3/2
1/2 / 3/4 = 2/3

Ejemplo: Signos y enteros

Entrada

4
1/-2 + 1/2
-3/-6 * 4
5/3 - 2/3
7 / -14

Salida esperada

1/-2 + 1/2 = 0
-3/-6 * 4 = 2
5/3 - 2/3 = 1
7 / -14 = -1/2

57. Máquina expendedora

Muy difícil · Clases y Objetos · apuntesdam.com/subject/programacion/topic/clases

Completa la clase Maquina (los precios y las monedas van en céntimos). insertar acepta monedas de 5, 10, 20, 50, 100 y 200 y responde «Crédito: 1,50 €»; cualquier otra da «Moneda no válida: 3». comprar comprueba, en este orden, que el producto existe («No existe el producto X9»), que queda stock («Zumo agotado») y que el crédito llega («Saldo insuficiente: faltan 0,40 €»). Después calcula el cambio de mayor a menor moneda, usando las monedas de la caja más las que se acaban de insertar; si así no se puede dar exacto responde «No hay cambio: inserte el importe exacto» y no vende. Si vende: «Sale Agua. Cambio: 1×0,10 €» (o «Cambio: nada»), la caja se actualiza y el crédito vuelve a 0. devolver entrega las monedas insertadas («Devuelto: 1×2,00 €, 1×0,50 €» o «Devuelto: nada»). estado lista cada producto con su stock y el total de la caja. Usa el método euros que ya tienes para dar formato.

Código de partida (java)
import java.util.*; class Producto {    final String codigo, nombre;    final int precio;            // en céntimos    int stock;     Producto(String codigo, String nombre, int precio, int stock) {        this.codigo = codigo;        this.nombre = nombre;        this.precio = precio;        this.stock = stock;    }} class Maquina {    static final int[] VALORES = {200, 100, 50, 20, 10, 5};    private final Map<String, Producto> productos = new LinkedHashMap<>();    private final Map<Integer, Integer> monedas = new TreeMap<>(Comparator.reverseOrder());    private final List<Integer> insertadas = new ArrayList<>();     Maquina() {        for (int v : VALORES) monedas.put(v, 0);    }     static String euros(int centimos) {        return centimos / 100 + "," + String.format("%02d", centimos % 100) + " €";    }     void cargarMonedas(int valor, int cantidad) {        monedas.merge(valor, cantidad, Integer::sum);    }     void añadirProducto(Producto p) {        productos.put(p.codigo, p);    }     String insertar(int valor) {        // TODO        return "";    }     String comprar(String codigo) {        // TODO        return "";    }     String devolver() {        // TODO        return "";    }     String estado() {        // TODO        return "";    }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Maquina m = new Maquina();        while (sc.hasNextLine()) {            String[] t = sc.nextLine().trim().split(" ");            switch (t[0]) {                case "MONEDAS" -> m.cargarMonedas(Integer.parseInt(t[1]), Integer.parseInt(t[2]));                case "PRODUCTO" -> m.añadirProducto(new Producto(t[1], t[2], Integer.parseInt(t[3]), Integer.parseInt(t[4])));                case "INSERTAR" -> System.out.println(m.insertar(Integer.parseInt(t[1])));                case "COMPRAR" -> System.out.println(m.comprar(t[1]));                case "DEVOLVER" -> System.out.println(m.devolver());                case "ESTADO" -> System.out.print(m.estado());                default -> { }            }        }    }}

Ejemplo: Compra con cambio

Entrada

MONEDAS 100 2
MONEDAS 50 1
MONEDAS 20 3
MONEDAS 10 2
PRODUCTO A1 Agua 90 2
PRODUCTO B2 Galletas 135 1
PRODUCTO C3 Zumo 160 0
INSERTAR 100
COMPRAR A1
ESTADO

Salida esperada

Crédito: 1,00 €
Sale Agua. Cambio: 1×0,10 €
A1 Agua 0,90 € (1)
B2 Galletas 1,35 € (1)
C3 Zumo 1,60 € (0)
Caja: 4,20 €

Ejemplo: Errores y devolución

Entrada

MONEDAS 100 2
MONEDAS 50 1
MONEDAS 20 3
MONEDAS 10 2
PRODUCTO A1 Agua 90 2
PRODUCTO B2 Galletas 135 1
PRODUCTO C3 Zumo 160 0
INSERTAR 3
INSERTAR 50
COMPRAR B2
COMPRAR C3
COMPRAR X9
INSERTAR 200
DEVOLVER
DEVOLVER

Salida esperada

Moneda no válida: 3
Crédito: 0,50 €
Saldo insuficiente: faltan 0,85 €
Zumo agotado
No existe el producto X9
Crédito: 2,50 €
Devuelto: 1×2,00 €, 1×0,50 €
Devuelto: nada

58. Cuentas bancarias con herencia

Difícil · Herencia · apuntesdam.com/subject/programacion/topic/herencia

La clase abstracta Cuenta ya guarda el IBAN y el saldo y sabe ingresar. Completa sus dos subclases. CuentaCorriente permite quedarse en descubierto hasta −300 € (si una retirada lo supera lanza IllegalStateException("supera el descubierto")) y al cierre de mes cobra 2 € de comisión si el saldo es negativo. CuentaAhorro no admite descubierto (IllegalStateException("saldo insuficiente")), permite como mucho 3 retiradas al mes (IllegalStateException("máximo de retiradas del mes")) y al cierre suma un 0,5 % de interés y reinicia el contador de retiradas. En las dos, un importe menor o igual que 0 lanza IllegalArgumentException("importe no válido"). El main ya procesa las órdenes y trata las cuentas de forma polimórfica.

Código de partida (java)
import java.util.*; abstract class Cuenta {    private final String iban;    protected double saldo;     Cuenta(String iban, double saldoInicial) {        this.iban = iban;        this.saldo = saldoInicial;    }     String getIban() { return iban; }    double getSaldo() { return saldo; }     void ingresar(double importe) {        if (importe <= 0) throw new IllegalArgumentException("importe no válido");        saldo += importe;    }     // Cada tipo de cuenta decide si se puede retirar    abstract void retirar(double importe);     // Se llama al final de cada mes    abstract void cierreMes();     abstract String tipo();     @Override    public String toString() {        return String.format(Locale.US, "%s %s: %.2f", tipo(), iban, saldo);    }} class CuentaCorriente extends Cuenta {    // TODO: descubierto hasta -300; comisión de 2 € al cierre si el saldo es negativo    CuentaCorriente(String iban, double saldo) { super(iban, saldo); }    void retirar(double importe) { }    void cierreMes() { }    String tipo() { return "Corriente"; }} class CuentaAhorro extends Cuenta {    // TODO: sin descubierto, máximo 3 retiradas al mes, 0,5 % de interés al cierre    CuentaAhorro(String iban, double saldo) { super(iban, saldo); }    void retirar(double importe) { }    void cierreMes() { }    String tipo() { return "Ahorro"; }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Cuenta> cuentas = new LinkedHashMap<>();        while (sc.hasNextLine()) {            String[] t = sc.nextLine().trim().split(" ");            try {                switch (t[0]) {                    case "CORRIENTE" -> cuentas.put(t[1], new CuentaCorriente(t[1], Double.parseDouble(t[2])));                    case "AHORRO" -> cuentas.put(t[1], new CuentaAhorro(t[1], Double.parseDouble(t[2])));                    case "INGRESAR" -> cuentas.get(t[1]).ingresar(Double.parseDouble(t[2]));                    case "RETIRAR" -> cuentas.get(t[1]).retirar(Double.parseDouble(t[2]));                    case "MES" -> {                        for (Cuenta c : cuentas.values()) c.cierreMes();                        System.out.println("-- cierre de mes --");                    }                    case "VER" -> cuentas.values().forEach(System.out::println);                }            } catch (IllegalArgumentException | IllegalStateException e) {                System.out.println(String.join(" ", t) + " → " + e.getMessage());            }        }    }}

Ejemplo: Descubierto y comisión

Entrada

CORRIENTE ES01 100
RETIRAR ES01 350
RETIRAR ES01 100
MES
VER

Salida esperada

RETIRAR ES01 100 → supera el descubierto
-- cierre de mes --
Corriente ES01: -252.00

Ejemplo: Límite de retiradas e interés

Entrada

AHORRO ES02 1000
RETIRAR ES02 100
RETIRAR ES02 100
RETIRAR ES02 100
RETIRAR ES02 100
MES
RETIRAR ES02 50
VER

Salida esperada

RETIRAR ES02 100 → máximo de retiradas del mes
-- cierre de mes --
Ahorro ES02: 653.50

59. Encuentra el fallo: el producto que no está en la lista

Difícil · Herencia · apuntesdam.com/subject/programacion/topic/herencia

La clase Producto define equals para que dos productos con el mismo código sean iguales, pero lista.contains(...) dice que no encuentra uno que sí está. Corrige equals (y lo que haga falta) para que contains funcione.

Código de partida (java)
import java.util.*; public class Main {    static class Producto {        final String codigo;        final String nombre;        Producto(String codigo, String nombre) { this.codigo = codigo; this.nombre = nombre; }         public boolean equals(Producto otro) {            return codigo.equals(otro.codigo);        }    }     public static void main(String[] args) {        List<Producto> lista = new ArrayList<>(List.of(new Producto("A1", "Teclado"), new Producto("B2", "Ratón")));        System.out.println("¿Está B2? " + lista.contains(new Producto("B2", "Ratón inalámbrico")));        System.out.println("¿Está C3? " + lista.contains(new Producto("C3", "Monitor")));    }}

Ejemplo: Buscar por código

Salida esperada

¿Está B2? true
¿Está C3? false

60. Expresiones matemáticas con polimorfismo

Muy difícil · Herencia · apuntesdam.com/subject/programacion/topic/herencia

Una expresión como (2 · x) + 3 se puede representar como un árbol de objetos: una Suma cuyo operando izquierdo es un Producto de Num(2) y Var(x), y cuyo derecho es Num(3). Num, Var y la clase abstracta Binaria (que ya sabe escribirse entre paréntesis) están hechas. Completa Suma, Resta y Producto con tres métodos: evaluar (con los valores de las variables), derivar respecto de una variable (la suma y la resta derivan cada operando; el producto usa la regla (a·b)' = a'·b + a·b') y simplificar, que primero simplifica los operandos y luego aplica estas reglas: dos números se operan; 0 + e y e + 0 dan e; e − 0 da e; e − e (mismo texto) da 0; 0 · e y e · 0 dan 0; 1 · e y e · 1 dan e. La entrada es la expresión en notación prefija y los valores de las variables.

Código de partida (java)
import java.util.*; abstract class Expr {    abstract double evaluar(Map<String, Double> vars);    abstract Expr derivar(String v);    Expr simplificar() { return this; }     static String num(double d) {        return d == Math.floor(d) && !Double.isInfinite(d) ? String.valueOf((long) d) : String.valueOf(d);    }} class Num extends Expr {    final double valor;    Num(double valor) { this.valor = valor; }    double evaluar(Map<String, Double> vars) { return valor; }    Expr derivar(String v) { return new Num(0); }    public String toString() { return num(valor); }} class Var extends Expr {    final String nombre;    Var(String nombre) { this.nombre = nombre; }    double evaluar(Map<String, Double> vars) { return vars.get(nombre); }    Expr derivar(String v) { return new Num(nombre.equals(v) ? 1 : 0); }    public String toString() { return nombre; }} // Clase base de las operaciones con dos operandosabstract class Binaria extends Expr {    final Expr izq, der;    Binaria(Expr izq, Expr der) { this.izq = izq; this.der = der; }    abstract String simbolo();    public String toString() { return "(" + izq + " " + simbolo() + " " + der + ")"; }} class Suma extends Binaria {    Suma(Expr a, Expr b) { super(a, b); }    String simbolo() { return "+"; }    // TODO: evaluar, derivar y simplificar    double evaluar(Map<String, Double> vars) { return 0; }    Expr derivar(String v) { return this; }} class Resta extends Binaria {    Resta(Expr a, Expr b) { super(a, b); }    String simbolo() { return "-"; }    // TODO: evaluar, derivar y simplificar    double evaluar(Map<String, Double> vars) { return 0; }    Expr derivar(String v) { return this; }} class Producto extends Binaria {    Producto(Expr a, Expr b) { super(a, b); }    String simbolo() { return "*"; }    // TODO: evaluar, derivar y simplificar    double evaluar(Map<String, Double> vars) { return 0; }    Expr derivar(String v) { return this; }} public class Main {    // Lee una expresión en notación prefija: "+ * 2 x 3" es (2 * x) + 3    static Expr leer(Iterator<String> it) {        String t = it.next();        return switch (t) {            case "+" -> new Suma(leer(it), leer(it));            case "-" -> new Resta(leer(it), leer(it));            case "*" -> new Producto(leer(it), leer(it));            default -> Character.isLetter(t.charAt(0)) ? new Var(t) : new Num(Double.parseDouble(t));        };    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Expr e = leer(Arrays.asList(sc.nextLine().trim().split(" ")).iterator());        Map<String, Double> vars = new TreeMap<>();        while (sc.hasNext()) vars.put(sc.next(), Double.parseDouble(sc.next()));        System.out.println("Expresión: " + e);        System.out.println("Simplificada: " + e.simplificar());        System.out.println("Valor: " + Expr.num(e.evaluar(vars)));        Expr d = e.derivar("x");        System.out.println("Derivada respecto de x: " + d);        System.out.println("Derivada simplificada: " + d.simplificar());    }}

Ejemplo: Recta

Entrada

+ * 2 x 3
x 4

Salida esperada

Expresión: ((2 * x) + 3)
Simplificada: ((2 * x) + 3)
Valor: 11
Derivada respecto de x: (((0 * x) + (2 * 1)) + 0)
Derivada simplificada: 2

Ejemplo: Parábola

Entrada

- * x x * 3 x
x 5

Salida esperada

Expresión: ((x * x) - (3 * x))
Simplificada: ((x * x) - (3 * x))
Valor: 10
Derivada respecto de x: (((1 * x) + (x * 1)) - ((0 * x) + (3 * 1)))
Derivada simplificada: ((x + x) - 3)

61. Clasificación de una liga con Comparator

Difícil · Interfaces · apuntesdam.com/subject/programacion/topic/interfaces

Cada línea de la entrada es un partido con el formato «Local 2-1 Visitante». Completa registrar, que actualiza las estadísticas de un equipo con los goles a favor y en contra de un partido (3 puntos por victoria y 1 por empate), y el Comparator CLASIFICACION, que ordena por puntos de mayor a menor; a igualdad de puntos, por diferencia de goles; después por goles a favor (ambos de mayor a menor) y, si todo coincide, por nombre en orden alfabético. El main ya lee los partidos y muestra la tabla.

Código de partida (java)
import java.util.*; class Equipo {    final String nombre;    int jugados, ganados, empatados, perdidos, golesFavor, golesContra;     Equipo(String nombre) { this.nombre = nombre; }     int puntos() { return ganados * 3 + empatados; }    int diferencia() { return golesFavor - golesContra; }     void registrar(int favor, int contra) {        // TODO: actualizar partidos, resultado y goles    }} public class Main {    // TODO: puntos (de más a menos), diferencia de goles, goles a favor y, por último, nombre (A-Z)    static final Comparator<Equipo> CLASIFICACION = (a, b) -> 0;     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Equipo> equipos = new HashMap<>();        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            if (linea.isEmpty()) continue;            // Formato: Local 2-1 Visitante            String[] t = linea.split(" ");            String[] goles = t[1].split("-");            int gl = Integer.parseInt(goles[0]), gv = Integer.parseInt(goles[1]);            equipos.computeIfAbsent(t[0], Equipo::new).registrar(gl, gv);            equipos.computeIfAbsent(t[2], Equipo::new).registrar(gv, gl);        }        List<Equipo> tabla = new ArrayList<>(equipos.values());        tabla.sort(CLASIFICACION);        System.out.println("Pos Equipo      PJ  G  E  P  GF  GC  DG Pts");        int pos = 1;        for (Equipo e : tabla)            System.out.printf("%3d %-10s %3d %2d %2d %2d %3d %3d %+3d %3d%n", pos++, e.nombre, e.jugados,                    e.ganados, e.empatados, e.perdidos, e.golesFavor, e.golesContra, e.diferencia(), e.puntos());    }}

Ejemplo: Jornada simple

Entrada

Leones 2-1 Tigres
Osos 0-0 Lobos
Tigres 3-0 Osos
Lobos 1-1 Leones

Salida esperada

Pos Equipo      PJ  G  E  P  GF  GC  DG Pts
  1 Leones       2  1  1  0   3   2  +1   4
  2 Tigres       2  1  0  1   4   2  +2   3
  3 Lobos        2  0  2  0   1   1  +0   2
  4 Osos         2  0  1  1   0   3  -3   1

Ejemplo: Desempates

Entrada

Alfa 3-0 Delta
Beta 2-0 Delta
Gamma 3-1 Delta
Epsilon 2-0 Delta

Salida esperada

Pos Equipo      PJ  G  E  P  GF  GC  DG Pts
  1 Alfa         1  1  0  0   3   0  +3   3
  2 Gamma        1  1  0  0   3   1  +2   3
  3 Beta         1  1  0  0   2   0  +2   3
  4 Epsilon      1  1  0  0   2   0  +2   3
  5 Delta        4  0  0  4   1  10  -9   0

62. Reglas de contraseñas componibles

Muy difícil · Interfaces · apuntesdam.com/subject/programacion/topic/interfaces

Regla es una interfaz funcional: su único método abstracto, comprobar, devuelve la lista de errores de un texto (vacía si lo cumple). Completa sus métodos default y static para poder combinar reglas como piezas. y(otra) devuelve una regla que exige las dos y junta los errores de ambas, en orden. o(otra) se cumple si se cumple cualquiera de las dos; si fallan ambas, da un único error con los dos mensajes unidos por « o ». minimo(n) falla con «mínimo n caracteres». contiene(clase, nombre) exige algún carácter de la clase de expresión regular [clase] y falla con «falta nombre». noContiene(palabra) falla, sin distinguir mayúsculas, con «no puede contener «palabra»». El main ya monta dos políticas con estas piezas y comprueba una contraseña por línea.

Código de partida (java)
import java.util.*; @FunctionalInterfaceinterface Regla {    // Lista de errores; vacía si el texto cumple la regla    List<String> comprobar(String s);     default Regla y(Regla otra) {        // TODO: se deben cumplir las dos; errores de ambas        return this;    }     default Regla o(Regla otra) {        // TODO: basta con que se cumpla una; si fallan las dos, un único error «e1 o e2»        return this;    }     static Regla minimo(int n) {        // TODO: «mínimo n caracteres»        return s -> List.of();    }     static Regla contiene(String clase, String nombre) {        // TODO: el texto debe tener algún carácter de la clase [clase]; si no, «falta nombre»        return s -> List.of();    }     static Regla noContiene(String palabra) {        // TODO: sin distinguir mayúsculas; si la contiene, «no puede contener «palabra»»        return s -> List.of();    }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String politica = sc.nextLine().trim();        Regla regla = switch (politica) {            case "BASICA" -> Regla.minimo(6).y(Regla.contiene("0-9", "un número"));            default -> Regla.minimo(10)                    .y(Regla.contiene("0-9", "un número"))                    .y(Regla.contiene("A-Z", "una mayúscula").o(Regla.contiene("^a-zA-Z0-9", "un símbolo")))                    .y(Regla.noContiene("password"))                    .y(Regla.noContiene("1234"));        };        while (sc.hasNextLine()) {            String clave = sc.nextLine();            List<String> errores = regla.comprobar(clave);            System.out.println(errores.isEmpty() ? "✓ " + clave : "✗ " + clave + ": " + String.join("; ", errores));        }    }}

Ejemplo: Política básica

Entrada

BASICA
abc
abcdef
abc123

Salida esperada

✗ abc: mínimo 6 caracteres; falta un número
✗ abcdef: falta un número
✓ abc123

Ejemplo: Política estricta

Entrada

ESTRICTA
Corto1
MiClaveSegura7
miclavesegura7
miclave-segura7
MyPassword2025

Salida esperada

✗ Corto1: mínimo 10 caracteres
✓ MiClaveSegura7
✗ miclavesegura7: falta una mayúscula o falta un símbolo
✓ miclave-segura7
✗ MyPassword2025: no puede contener «password»

63. Validar y resumir un CSV de ventas

Difícil · Ficheros y Streams · apuntesdam.com/subject/programacion/topic/ficheros

La entrada es un fichero CSV (separado por «;», con decimales con coma) con la cabecera fecha;producto;unidades;precio y una venta por línea. Valida cada línea y muestra sus errores con su número de línea en el fichero (la cabecera es la 1; las líneas en blanco se saltan pero cuentan): «se esperaban 4 campos y hay N», «producto vacío», «unidades no válidas («dos»)», «precio no válido («2,5O»)» o «valores negativos o cero» (unidades ≤ 0 o precio < 0). Solo el primer error de cada línea. Con las líneas correctas, acumula el importe (unidades × precio) por producto y muestra «--- N líneas correctas, M con errores ---», los productos de mayor a menor importe (a igualdad, por nombre) con formato «%-10s %9.2f» y una línea TOTAL.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {    public static void main(String[] args) throws IOException {        // La entrada estándar hace de fichero: se lee igual que con un FileReader        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));        Map<String, Double> ventas = new HashMap<>();        int correctas = 0, erroneas = 0;        String cabecera = br.readLine();        String linea;        int numLinea = 1;        while ((linea = br.readLine()) != null) {            numLinea++;            if (linea.isBlank()) continue;            // TODO: validar la línea; si es correcta, acumular unidades × precio en el producto        }        // TODO: mostrar el resumen ordenado por importe (de mayor a menor)    }}

Ejemplo: Fichero correcto

Entrada

fecha;producto;unidades;precio
2025-03-01;Café;3;1,20
2025-03-01;Tostada;2;2,50
2025-03-02;Café;5;1,20
2025-03-02;Zumo;1;3

Salida esperada

--- 4 líneas correctas, 0 con errores ---
Café            9.60
Tostada         5.00
Zumo            3.00
TOTAL          17.60

Ejemplo: Con errores

Entrada

fecha;producto;unidades;precio
2025-03-01;Café;3;1,20
2025-03-01;Tostada;dos;2,50
2025-03-01;Zumo;1

2025-03-02;Café;-1;1,20
2025-03-02;;1;2
2025-03-03;Tostada;4;2,5O
2025-03-03;Tostada;4;2,50

Salida esperada

Línea 3: unidades no válidas («dos»)
Línea 4: se esperaban 4 campos y hay 3
Línea 6: valores negativos o cero
Línea 7: producto vacío
Línea 8: precio no válido («2,5O»)
--- 2 líneas correctas, 5 con errores ---
Tostada        10.00
Café            3.60
TOTAL          13.60

64. Comparar dos ficheros (diff)

Muy difícil · Ficheros y Streams · apuntesdam.com/subject/programacion/topic/ficheros

Herramientas como git diff muestran qué ha cambiado entre dos versiones de un fichero. La entrada contiene las líneas de la versión antigua, una línea «=====» y las de la nueva. Implementa diff: debe conservar el mayor número posible de líneas comunes en orden (la subsecuencia común más larga, LCS) y marcar el resto. Cada línea del resultado empieza por un espacio si está en las dos versiones, por «-» si solo está en la antigua y por «+» si solo está en la nueva. Recorre las dos versiones desde el principio: si las líneas actuales son iguales, se conservan; si no, se quita la de la antigua cuando eso no empeora la LCS (lcs[i+1][j] ≥ lcs[i][j+1]) y, si no, se añade la de la nueva. El main ya cuenta los cambios.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {    // Devuelve las líneas del diff: " línea" (igual), "-línea" (solo en a), "+línea" (solo en b)    static List<String> diff(List<String> a, List<String> b) {        // TODO        return new ArrayList<>();    }     public static void main(String[] args) throws IOException {        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));        List<String> a = new ArrayList<>(), b = new ArrayList<>();        List<String> actual = a;        String linea;        while ((linea = br.readLine()) != null) {            if (linea.equals("=====")) { actual = b; continue; }            actual.add(linea);        }        List<String> d = diff(a, b);        int quitadas = 0, añadidas = 0;        for (String l : d) {            System.out.println(l);            if (l.startsWith("-")) quitadas++;            if (l.startsWith("+")) añadidas++;        }        System.out.println("@@ " + quitadas + (quitadas == 1 ? " línea quitada, " : " líneas quitadas, ")                + añadidas + (añadidas == 1 ? " añadida @@" : " añadidas @@"));    }}

Ejemplo: Un cambio

Entrada

uno
dos
tres
cuatro
=====
uno
DOS
tres
cuatro

Salida esperada

uno
-dos
+DOS
 tres
 cuatro
@@ 1 línea quitada, 1 añadida @@

Ejemplo: Código

Entrada

public class A {
    int x;
    void f() {
        x++;
    }
}
=====
public class A {
    private int x;
    void f() {
        x++;
        log();
    }
}

Salida esperada

public class A {
-    int x;
+    private int x;
     void f() {
         x++;
+        log();
     }
 }
@@ 1 línea quitada, 2 añadidas @@

65. Encuentra el fallo: borrar mientras se recorre

Medio · Colecciones · apuntesdam.com/subject/programacion/topic/colecciones

El programa debe quitar de la lista las tareas terminadas (las que empiezan por «x ») y mostrar las pendientes. En lugar de eso, termina con ConcurrentModificationException. Corrígelo.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        List<String> tareas = new ArrayList<>(List.of("x comprar pan", "estudiar Java", "x llamar a Ana", "x pagar luz", "hacer la práctica"));        for (String t : tareas) {            if (t.startsWith("x ")) {                tareas.remove(t);            }        }        System.out.println("Pendientes: " + tareas);    }}

Ejemplo: Tres tareas terminadas

Salida esperada

Pendientes: [estudiar Java, hacer la práctica]

66. Encuentra el fallo: el contador de palabras olvidadizo

Medio · Colecciones · apuntesdam.com/subject/programacion/topic/colecciones

El programa cuenta cuántas veces aparece cada palabra de una frase y las muestra en orden alfabético. Todas salen con 1 aunque se repitan. Corrígelo.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        String frase = new Scanner(System.in).nextLine().toLowerCase();        Map<String, Integer> contador = new TreeMap<>();        for (String p : frase.split(" ")) {            contador.put(p, 1);        }        contador.forEach((p, n) -> System.out.println(p + " " + n));    }}

Ejemplo: Frase con repeticiones

Entrada

el perro y el gato y el loro

Salida esperada

el 3
gato 1
loro 1
perro 1
y 2

67. Agrupar anagramas

Difícil · Colecciones · apuntesdam.com/subject/programacion/topic/colecciones

Dos palabras son anagramas si tienen las mismas letras en distinto orden (amor, mora, Roma, ramo). La entrada es una lista de palabras. Implementa agrupar: trabaja en minúsculas (las letras con tilde cuentan como distintas: lámina no es anagrama de animal), ignora las palabras repetidas y devuelve solo los grupos con 2 o más palabras distintas. Cada grupo va ordenado alfabéticamente y los grupos se ordenan de más a menos palabras y, a igualdad, por su primera palabra. Si no hay ninguno, el main muestra «No hay anagramas».

Código de partida (java)
import java.util.*; public class Main {    // Devuelve los grupos de anagramas (2 o más palabras), cada uno ordenado alfabéticamente    // y los grupos de más a menos palabras (a igualdad, por su primera palabra)    static List<List<String>> agrupar(List<String> palabras) {        // TODO        return new ArrayList<>();    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String> palabras = new ArrayList<>();        while (sc.hasNext()) palabras.add(sc.next());        List<List<String>> grupos = agrupar(palabras);        if (grupos.isEmpty()) System.out.println("No hay anagramas");        for (List<String> g : grupos) System.out.println(g.size() + ": " + String.join(", ", g));    }}

Ejemplo: Ejemplo

Entrada

amor saco Roma cosa mora ramo casa ocas

Salida esperada

4: amor, mora, ramo, roma
3: cosa, ocas, saco

Ejemplo: Repetidas y mayúsculas

Entrada

Lámina animal Manila listen silent enlist Listen tinsel perro

Salida esperada

4: enlist, listen, silent, tinsel
2: animal, manila

68. Ruta más corta en el metro

Muy difícil · Colecciones · apuntesdam.com/subject/programacion/topic/colecciones

Cada línea «LINEA nombre e1 e2 e3…» conecta estaciones consecutivas en los dos sentidos. Cada «RUTA origen destino» pide el camino con menos paradas. Implementa ruta con una búsqueda en anchura (BFS): una cola con las estaciones por explorar y un mapa que guarda, para cada estación descubierta, desde cuál se llegó a ella. Al sacar el destino de la cola, reconstruye el camino hacia atrás siguiendo ese mapa. Las vecinas se exploran en orden alfabético (ya vienen así en red), lo que decide el camino cuando hay empate. Si no se puede llegar, devuelve null. El main muestra «Sol → Opera → Callao (2 paradas)».

Código de partida (java)
import java.util.*; public class Main {    // Estación → estaciones vecinas (en orden alfabético)    static Map<String, Set<String>> red = new TreeMap<>();     static void conectar(String a, String b) {        red.computeIfAbsent(a, k -> new TreeSet<>()).add(b);        red.computeIfAbsent(b, k -> new TreeSet<>()).add(a);    }     // Camino con menos paradas de origen a destino (incluidos los dos), o null si no hay    static List<String> ruta(String origen, String destino) {        // TODO: búsqueda en anchura        return null;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            if (linea.isEmpty()) continue;            String[] t = linea.split(" ");            if (t[0].equals("LINEA")) {                for (int i = 2; i < t.length - 1; i++) conectar(t[i], t[i + 1]);            } else if (t[0].equals("RUTA")) {                if (!red.containsKey(t[1]) || !red.containsKey(t[2])) {                    System.out.println("Estación desconocida");                    continue;                }                List<String> r = ruta(t[1], t[2]);                if (r == null) System.out.println(t[1] + " → " + t[2] + ": sin conexión");                else System.out.println(String.join(" → ", r) + " (" + (r.size() - 1) + " paradas)");            }        }    }}

Ejemplo: Misma línea y transbordo

Entrada

LINEA L1 Sol Opera Callao Plaza Norte
LINEA L2 Sol Retiro Goya Este
LINEA L3 Callao Centro Goya
LINEA L4 Isla Faro
RUTA Sol Callao
RUTA Opera Este

Salida esperada

Sol → Opera → Callao (2 paradas)
Opera → Callao → Centro → Goya → Este (4 paradas)

Ejemplo: Varios caminos

Entrada

LINEA L1 Sol Opera Callao Plaza Norte
LINEA L2 Sol Retiro Goya Este
LINEA L3 Callao Centro Goya
LINEA L4 Isla Faro
RUTA Plaza Retiro
RUTA Norte Sol
RUTA Goya Goya

Salida esperada

Plaza → Callao → Centro → Goya → Retiro (4 paradas)
Norte → Plaza → Callao → Opera → Sol (4 paradas)
Goya (0 paradas)

Acceso a Datos

69. Media de notas desde un fichero CSV

Fácil · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

El programa guarda en notas.csv las líneas recibidas por teclado (formato nombre;nota). Completa leerMedia() para que lea el fichero línea a línea con BufferedReader y devuelva la media de las notas. Se imprime con dos decimales.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {     static double leerMedia(File fichero) throws IOException {        double suma = 0;        int contador = 0;        // TODO: lee el fichero línea a línea y acumula las notas         return contador == 0 ? 0 : suma / contador;    }     public static void main(String[] args) throws IOException {        // Guarda en notas.csv las líneas recibidas por teclado        File fichero = new File("notas.csv");        Scanner sc = new Scanner(System.in);        try (PrintWriter pw = new PrintWriter(new FileWriter(fichero))) {            while (sc.hasNextLine()) pw.println(sc.nextLine());        }         System.out.printf(Locale.US, "Media: %.2f%n", leerMedia(fichero));    }}

Ejemplo: Tres alumnos

Entrada

Ana;8
Luis;6
Marta;9.5

Salida esperada

Media: 7.83

Ejemplo: Un alumno

Entrada

Pedro;5

Salida esperada

Media: 5.00

70. Serializar una lista de productos

Medio · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

Completa guardar() y cargar() para escribir la lista de productos en productos.obj con ObjectOutputStream y volver a leerla con ObjectInputStream. El programa lee los productos por teclado (nombre;precio), los guarda, los carga de nuevo y muestra cuántos hay y el importe total.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {     static class Producto implements Serializable {        private static final long serialVersionUID = 1L;        String nombre;        double precio;        Producto(String nombre, double precio) { this.nombre = nombre; this.precio = precio; }    }     static void guardar(List<Producto> productos, File fichero) throws IOException {        // TODO: escribe la lista en el fichero    }     @SuppressWarnings("unchecked")    static List<Producto> cargar(File fichero) throws IOException, ClassNotFoundException {        // TODO: lee la lista desde el fichero        return new ArrayList<>();    }     public static void main(String[] args) throws Exception {        List<Producto> productos = new ArrayList<>();        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] p = sc.nextLine().split(";");            if (p.length == 2) productos.add(new Producto(p[0], Double.parseDouble(p[1])));        }         File fichero = new File("productos.obj");        guardar(productos, fichero);        List<Producto> leidos = cargar(fichero);         double total = 0;        for (Producto p : leidos) total += p.precio;        System.out.printf(Locale.US, "%d productos, total %.2f%n", leidos.size(), total);    }}

Ejemplo: Tres productos

Entrada

Teclado;49.99
Ratón;19.5
Monitor;189

Salida esperada

3 productos, total 258.49

71. Leer un XML con DOM

Medio · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

El programa guarda en biblioteca.xml el documento recibido por teclado. Completa procesar() para cargarlo con DOM, recorrer los elementos <libro> y mostrar cuántos libros hay y cuántos están disponibles (atributo disponible="si").

Código de partida (java)
import java.io.*;import java.nio.file.*;import javax.xml.parsers.*;import org.w3c.dom.*; public class Main {     static void procesar(File fichero) throws Exception {        int total = 0, disponibles = 0;        // TODO: carga el XML con DOM y cuenta los libros         System.out.println("Libros: " + total + ", disponibles: " + disponibles);    }     public static void main(String[] args) throws Exception {        File fichero = new File("biblioteca.xml");        Files.write(fichero.toPath(), System.in.readAllBytes());        procesar(fichero);    }}

Ejemplo: Tres libros, dos disponibles

Entrada

<biblioteca><libro disponible="si"><titulo>Java</titulo></libro><libro disponible="no"><titulo>SQL</titulo></libro><libro disponible="si"><titulo>XML</titulo></libro></biblioteca>

Salida esperada

Libros: 3, disponibles: 2

72. Acceso aleatorio a registros

Difícil · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

El programa crea empleados.dat con registros de tamaño fijo: id (int, 4 bytes), nombre (10 caracteres = 20 bytes) y salario (double, 8 bytes). Completa leerRegistro() para que use seek() y lea directamente el registro número n (empezando en 1) sin recorrer los anteriores. Se lee n por teclado.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {    static final int TAM = 4 + 20 + 8; // id + nombre(10 chars) + salario     static void escribir(RandomAccessFile raf, int id, String nombre, double salario) throws IOException {        raf.writeInt(id);        StringBuilder sb = new StringBuilder(nombre);        sb.setLength(10); // rellena o recorta a 10 caracteres        raf.writeChars(sb.toString());        raf.writeDouble(salario);    }     static String leerRegistro(RandomAccessFile raf, int n) throws IOException {        // TODO: sitúate en el registro n y lee sus tres campos        return "";    }     public static void main(String[] args) throws IOException {        try (RandomAccessFile raf = new RandomAccessFile("empleados.dat", "rw")) {            raf.setLength(0);            escribir(raf, 1, "Ana", 1850.5);            escribir(raf, 2, "Luis", 2100);            escribir(raf, 3, "Marta", 2475.25);            escribir(raf, 4, "Jorge", 1990);             int n = new Scanner(System.in).nextInt();            System.out.println(leerRegistro(raf, n));        }    }}

Ejemplo: Registro 1

Entrada

1

Salida esperada

1 Ana 1850.5

Ejemplo: Registro 3

Entrada

3

Salida esperada

3 Marta 2475.25

73. Un CSV de verdad: comillas, separadores y saltos de línea

Difícil · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

Partir cada línea por el separador falla con CSV reales: un campo entre comillas dobles puede contener el separador, saltos de línea y comillas (escritas dos veces: ""). Lee todo el texto (separador ; como el Excel en español) y analízalo carácter a carácter según el RFC 4180: fuera de comillas, ; separa campos y el salto de línea termina el registro (las líneas vacías se ignoran); una comilla al principio abre un campo entrecomillado, dentro del cual "" es una comilla y " cierra. La primera fila es la cabecera. Para cada registro de datos con tantos campos como la cabecera, muestra «Registro i:» y una línea « cabecera = valor» por campo (los saltos de línea dentro del valor se muestran como \n y un valor vacío como «(vacío)»); una comilla en mitad de un campo sin comillas es un carácter normal; si el número de campos no coincide, «Error en el registro i: tiene X campos y se esperaban M». Termina con «Registros válidos: V de T · Campos: M». Si el texto acaba con una comilla sin cerrar: «Error: comilla sin cerrar en el registro i» (i = registros completos antes de ese) y nada más.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in).useDelimiter("\\A");        String texto = sc.hasNext() ? sc.next().replace("\r", "") : "";        List<List<String>> registros = new ArrayList<>();        // TODO: analizar el CSV carácter a carácter (separador ; y comillas dobles)    }}

Ejemplo: Alumnos con campos difíciles

Entrada

nif;nombre;direccion;nota
12345678Z;"García; Ana";"Calle Mayor 3
2º B";8,5
87654321X;Luis "el rápido";Avenida Sol 1;7
X1234567L;"Pérez, ""Lolo""";;
00000000T;Falta;un campo

Salida esperada

Registro 1:
  nif = 12345678Z
  nombre = García; Ana
  direccion = Calle Mayor 3
2º B
  nota = 8,5
Registro 2:
  nif = 87654321X
  nombre = Luis "el rápido"
  direccion = Avenida Sol 1
  nota = 7
Registro 3:
  nif = X1234567L
  nombre = Pérez, "Lolo"
  direccion = (vacío)
  nota = (vacío)
Error en el registro 4: tiene 3 campos y se esperaban 4
Registros válidos: 3 de 4 · Campos: 4

74. Leer un formato binario propio con versión y CRC

Muy difícil · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

Muchas aplicaciones guardan sus datos en un formato binario propio. Este guarda alumnos así (todo en big-endian, como DataOutputStream): 4 bytes ASCII «ALUM»; un short con la versión (1 o 2); un int con el número de registros; por cada registro, un int id, un short con la longitud en bytes del nombre seguido del nombre en UTF-8, en la versión 2 un byte con el curso, y un float con la nota; y al final un int con el CRC32 de todos los bytes anteriores. La entrada es el fichero en hexadecimal. Comprueba en este orden: la firma (si no es ALUM, «Formato desconocido»), la versión («Versión X no soportada»), que no se acaben los bytes antes de tiempo («Fichero truncado») y el CRC («CRC incorrecto: esperado XXXXXXXX, calculado YYYYYYYY», en hexadecimal con 8 cifras). Si todo es correcto, muestra cada registro como «id · nombre · nota» (en la versión 2, «id · nombre · curso N · nota»), con la nota con dos decimales, y «Registros: n · media M · versión v · CRC OK».

Código de partida (java)
import java.nio.*;import java.util.*;import java.util.zip.CRC32; public class Main {    static byte[] desdeHex(String h) {        byte[] b = new byte[h.length() / 2];        for (int i = 0; i < b.length; i++) b[i] = (byte) Integer.parseInt(h.substring(2 * i, 2 * i + 2), 16);        return b;    }     public static void main(String[] args) {        byte[] datos = desdeHex(new Scanner(System.in).nextLine().trim());        ByteBuffer buf = ByteBuffer.wrap(datos);          // big-endian, como DataInputStream        // TODO: leer cabecera, registros y CRC    }}

Ejemplo: Versión 1 correcta

Entrada

414C554D00010000000300000001000B416E612047617263C3AD614108000000000002000B49C3B1616B69205275697A40C8000000000003000C5A6FC3AB204D617274C3AD6E411C000054FEC7DF

Salida esperada

1 · Ana García · 8.50
2 · Iñaki Ruiz · 6.25
3 · Zoë Martín · 9.75
Registros: 3 · media 8.17 · versión 1 · CRC OK

Ejemplo: Versión 2 con curso

Entrada

414C554D0002000000020000000A00064C7563C3AD610140F000000000000B000C4A6F73C3A9204D6172C3AD610240A0000027FC88E5

Salida esperada

10 · Lucía · curso 1 · 7.50
11 · José María · curso 2 · 5.00
Registros: 2 · media 6.25 · versión 2 · CRC OK

Ejemplo: CRC alterado

Entrada

414C554D00010000000300000001000B416E612047617263C3AD614108000000000002000B49C3B1616B69205275697A40C8000000000003000C5A6FC3AB204D617274C3AD6E411C000054FEC7DE

Salida esperada

CRC incorrecto: esperado 54FEC7DE, calculado 54FEC7DF

75. Sentencias preparadas frente a concatenación

Medio · Conectores JDBC · apuntesdam.com/subject/acceso-datos/topic/jdbc

Un PreparedStatement envía la sentencia y los datos por separado, así que un dato nunca puede convertirse en código SQL. Para verlo, simula lo que pasaría al concatenar. La primera línea es la plantilla con ? como marcadores; cada línea siguiente es un parámetro «tipo:valor» con tipo s (texto), i (entero), d (decimal con punto), b (booleano), f (fecha) o n (nulo). Los ? dentro de un literal entre comillas simples de la plantilla no son marcadores. Muestra «Parámetros: N» y, si no coincide con los dados, «Error: la sentencia tiene N parámetros y se han dado M». Valida que los enteros y decimales lo sean («Error: el parámetro k no es un entero: x» / «no es un decimal»). Después muestra «Concatenando: …» (cada texto entre comillas tal cual) y «Preparada: …» (con las comillas simples del texto duplicadas, que es como queda el dato escapado); los booleanos son TRUE/FALSE, las fechas DATE '…' y los nulos NULL. Termina con «¡Inyección! Con la concatenación el texto del usuario cambia la estructura de la consulta» si algún texto contiene una comilla simple, o «Las dos versiones son equivalentes con estos datos».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String plantilla = sc.nextLine();        List<String[]> params = new ArrayList<>();          // {tipo, valor}        while (sc.hasNextLine()) {            String l = sc.nextLine();            if (l.isBlank()) continue;            int dos = l.indexOf(':');            params.add(new String[] { l.substring(0, dos), l.substring(dos + 1) });        }        // TODO: localizar los ? fuera de los literales y montar las dos versiones    }}

Ejemplo: Inicio de sesión atacado

Entrada

SELECT * FROM usuario WHERE login = ? AND clave = ?
s:admin
s:' OR '1'='1

Salida esperada

Parámetros: 2
Concatenando: SELECT * FROM usuario WHERE login = 'admin' AND clave = '' OR '1'='1'
Preparada:    SELECT * FROM usuario WHERE login = 'admin' AND clave = ''' OR ''1''=''1'
¡Inyección! Con la concatenación el texto del usuario cambia la estructura de la consulta

Ejemplo: Tipos y ? en un literal

Entrada

UPDATE alumno SET nota = ?, aprobado = ?, nacimiento = ?, tutor = ? WHERE nombre = ? AND obs <> '¿Qué?'
d:7.5
b:true
f:2005-03-14
n:
s:O'Brien

Salida esperada

Parámetros: 5
Concatenando: UPDATE alumno SET nota = 7.5, aprobado = TRUE, nacimiento = DATE '2005-03-14', tutor = NULL WHERE nombre = 'O'Brien' AND obs <> '¿Qué?'
Preparada:    UPDATE alumno SET nota = 7.5, aprobado = TRUE, nacimiento = DATE '2005-03-14', tutor = NULL WHERE nombre = 'O''Brien' AND obs <> '¿Qué?'
¡Inyección! Con la concatenación el texto del usuario cambia la estructura de la consulta

76. Un pool de conexiones por dentro

Difícil · Conectores JDBC · apuntesdam.com/subject/acceso-datos/topic/jdbc

Abrir una conexión a la base de datos cuesta decenas de milisegundos, así que las aplicaciones usan un pool (HikariCP, el de Tomcat…) que las reutiliza. La primera línea es «max M espera W» y las siguientes, ordenadas por tiempo, «t pedir cliente» o «t devolver cliente». Al pedir: si hay una conexión libre se entrega la que lleva más tiempo libre («t: X recibe la conexión #k (reutilizada)»); si no, y hay menos de M creadas en total, se crea una nueva numerada desde 1 («(nueva)»); si no, el cliente espera en una cola FIFO («t: X espera (pool lleno)»). Al devolver: si alguien espera, la conexión pasa al primero de la cola («t: X devuelve #k → pasa a Y (esperó d ms)»); si no, queda libre («t: X devuelve #k (queda libre)»); si el cliente no tenía conexión, «t: Error: X no tiene conexión». Quien lleva esperando más de W ms (su inicio + W es menor que el instante del siguiente evento) se va antes de procesar ese evento: «inicio+W: X agota la espera (timeout)»; al terminar los eventos se agotan los que queden. Termina con «Conexiones creadas: c · Reutilizadas: r · Esperas: e · Timeouts: o · Espera media: m ms» (media entera de las esperas que sí recibieron conexión; 0 si ninguna) y «Al final: c abiertas, l libres, u en uso».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String[] conf = sc.nextLine().trim().split(" ");     // «max M espera W»        int max = Integer.parseInt(conf[1]), espera = Integer.parseInt(conf[3]);        List<String[]> eventos = new ArrayList<>();           // {tiempo, accion, cliente}        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (!l.isEmpty()) eventos.add(l.split(" "));        }        // TODO: simular el pool (conexiones libres, en uso, cola de espera y tiempos límite)    }}

Ejemplo: Aplicación web

Entrada

max 2 espera 100
0 pedir web1
5 pedir web2
10 pedir web3
40 devolver web1
45 pedir web4
50 devolver web3
60 pedir web5
70 devolver web2
80 devolver web5
90 pedir web7
200 pedir web6
210 devolver web9
320 devolver web4
330 pedir web8
340 pedir web10

Salida esperada

0: web1 recibe la conexión #1 (nueva)
5: web2 recibe la conexión #2 (nueva)
10: web3 espera (pool lleno)
40: web1 devuelve #1 → pasa a web3 (esperó 30 ms)
45: web4 espera (pool lleno)
50: web3 devuelve #1 → pasa a web4 (esperó 5 ms)
60: web5 espera (pool lleno)
70: web2 devuelve #2 → pasa a web5 (esperó 10 ms)
80: web5 devuelve #2 (queda libre)
90: web7 recibe la conexión #2 (reutilizada)
200: web6 espera (pool lleno)
210: Error: web9 no tiene conexión
300: web6 agota la espera (timeout)
320: web4 devuelve #1 (queda libre)
330: web8 recibe la conexión #1 (reutilizada)
340: web10 espera (pool lleno)
440: web10 agota la espera (timeout)
Conexiones creadas: 2 · Reutilizadas: 2 · Esperas: 5 · Timeouts: 2 · Espera media: 15 ms
Al final: 2 abiertas, 0 libres, 2 en uso

77. Transacciones con puntos de guardado y bloqueos

Muy difícil · Conectores JDBC · apuntesdam.com/subject/acceso-datos/topic/jdbc

Simula dos conexiones JDBC (sesiones A y B) sobre unas cuentas, como haría una base de datos con nivel de aislamiento READ COMMITTED. La primera línea es «cuentas C1=v1 C2=v2 …» y cada línea siguiente «S orden»: BEGIN (abre una transacción: setAutoCommit(false); si ya hay una, «S: ya hay una transacción abierta»); SELECT c («S ve c = v»: el valor propio pendiente si la sesión lo ha cambiado en su transacción y, si no, el confirmado: nunca ve lo no confirmado de la otra); UPDATE c ±n (si la otra sesión tiene c cambiada en una transacción abierta, «S: UPDATE c bloqueado por O (se cancela la sentencia)»; si el saldo quedaría negativo, «S: UPDATE c rechazado: el saldo quedaría en v»; si no, sin transacción se confirma al momento «S: c = v (confirmado)» y dentro de una, «S: c = v»); SAVEPOINT nombre; ROLLBACK TO nombre (vuelve al estado del punto y borra los puntos posteriores; «S: vuelta a nombre» o «S: no existe el punto de guardado nombre»); COMMIT («S: COMMIT (n cambios)», n = cuentas distintas cambiadas) y ROLLBACK («S: ROLLBACK (n cambios descartados)»). COMMIT, ROLLBACK y SAVEPOINT sin transacción: «S: no hay transacción». Un UPDATE rechazado no deshace la transacción. Al final, por cada sesión con una transacción abierta (A y luego B), «S tenía una transacción abierta: se deshace al cerrar la conexión», y «Confirmado: C1=v1 …» en el orden inicial.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Integer> confirmado = new LinkedHashMap<>();   // lo que ve cualquiera        for (String par : sc.nextLine().trim().split(" ")) {            if (!par.contains("=")) continue;                     // «cuentas A1=100 A2=50 …»            String[] kv = par.split("=");            confirmado.put(kv[0], Integer.parseInt(kv[1]));        }        // Por sesión: cambios pendientes (null si no hay transacción) y puntos de guardado        Map<String, Map<String, Integer>> pendientes = new HashMap<>();        Map<String, LinkedHashMap<String, Map<String, Integer>>> puntos = new HashMap<>();        // TODO: procesar las órdenes de las sesiones A y B    }}

Ejemplo: Transferencia con comisión

Entrada

cuentas A1=100 A2=50 A3=0
A BEGIN
A UPDATE A1 -30
A UPDATE A2 +30
B SELECT A1
A SELECT A1
B UPDATE A1 +5
B UPDATE A3 +10
A SAVEPOINT antes_comision
A UPDATE A2 -100
A UPDATE A3 -2
A ROLLBACK TO antes_comision
A UPDATE A2 -1
A COMMIT
B SELECT A1
B UPDATE A1 +5
B BEGIN
B UPDATE A2 -79
B SELECT A2
A SELECT A2

Salida esperada

A: BEGIN
A: A1 = 70
A: A2 = 80
B ve A1 = 100
A ve A1 = 70
B: UPDATE A1 bloqueado por A (se cancela la sentencia)
B: A3 = 10 (confirmado)
A: SAVEPOINT antes_comision
A: UPDATE A2 rechazado: el saldo quedaría en -20
A: A3 = 8
A: vuelta a antes_comision
A: A2 = 79
A: COMMIT (2 cambios)
B ve A1 = 70
B: A1 = 75 (confirmado)
B: BEGIN
B: A2 = 0
B ve A2 = 0
A ve A2 = 79
B tenía una transacción abierta: se deshace al cerrar la conexión
Confirmado: A1=75 A2=79 A3=10

78. El problema N+1 y las estrategias de carga

Medio · Mapeo Objeto-Relacional (Hibernate/JPA) · apuntesdam.com/subject/acceso-datos/topic/hibernate

Con JPA, una consulta inocente como «SELECT a FROM Autor a» seguida de a.getLibros() en un bucle puede lanzar una consulta por autor: el problema N+1. Simula lo que hace Hibernate. La entrada tiene líneas «autor id nombre», «libro id idAutor título» y «estrategia E». Se ejecuta la consulta de autores (ordenados por id) y después, para cada autor, se accede a sus libros (ordenados por id) y se muestra «Nombre: n libros (Título, Título)» («1 libro», o «0 libros» sin paréntesis). Muestra cada SQL en el momento en que se lanza: «SQL: select * from autor order by id» al principio y, según la estrategia: LAZY, «SQL: select * from libro where autor_id = X» justo antes de usar los libros de cada autor; EAGER, esas mismas consultas pero todas seguidas tras la primera (con JPQL, EAGER no evita el N+1); JOIN FETCH, una única consulta «SQL: select * from autor a left join libro l on l.autor_id = a.id order by a.id» que lo trae todo; BATCH k, al necesitar los libros de un autor no cargado se cargan los de hasta k autores no cargados a partir de él: «SQL: select * from libro where autor_id in (1, 2)»; SUBSELECT, la primera vez se cargan los de todos: «SQL: select * from libro where autor_id in (select id from autor)». Termina con «Consultas: n (estrategia E)».

Código de partida (java)
import java.util.*; public class Main {    record Autor(int id, String nombre) { }    record Libro(int id, int autorId, String titulo) { }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<Autor> autores = new ArrayList<>();        List<Libro> libros = new ArrayList<>();        String estrategia = "LAZY";        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.startsWith("autor ")) {                String[] p = l.split(" ", 3);                autores.add(new Autor(Integer.parseInt(p[1]), p[2]));            } else if (l.startsWith("libro ")) {                String[] p = l.split(" ", 4);                libros.add(new Libro(Integer.parseInt(p[1]), Integer.parseInt(p[2]), p[3]));            } else if (l.startsWith("estrategia ")) {                estrategia = l.substring(11).trim();            }        }        autores.sort(Comparator.comparingInt(Autor::id));        libros.sort(Comparator.comparingInt(Libro::id));        // TODO: simular «SELECT a FROM Autor a» y el acceso a a.getLibros() de cada autor    }}

Ejemplo: Estrategia LAZY

Entrada

autor 1 Cervantes
autor 2 Galdós
autor 3 Emilia Pardo Bazán
autor 4 Machado
autor 5 Lorca
libro 10 1 Don Quijote
libro 11 2 Fortunata y Jacinta
libro 12 1 Novelas ejemplares
libro 13 3 Los pazos de Ulloa
libro 14 5 Bodas de sangre
libro 15 5 Yerma
libro 16 2 Misericordia
estrategia LAZY

Salida esperada

SQL: select * from autor order by id
SQL: select * from libro where autor_id = 1
Cervantes: 2 libros (Don Quijote, Novelas ejemplares)
SQL: select * from libro where autor_id = 2
Galdós: 2 libros (Fortunata y Jacinta, Misericordia)
SQL: select * from libro where autor_id = 3
Emilia Pardo Bazán: 1 libro (Los pazos de Ulloa)
SQL: select * from libro where autor_id = 4
Machado: 0 libros
SQL: select * from libro where autor_id = 5
Lorca: 2 libros (Bodas de sangre, Yerma)
Consultas: 6 (estrategia LAZY)

Ejemplo: Estrategia JOIN FETCH

Entrada

autor 1 Cervantes
autor 2 Galdós
autor 3 Emilia Pardo Bazán
autor 4 Machado
autor 5 Lorca
libro 10 1 Don Quijote
libro 11 2 Fortunata y Jacinta
libro 12 1 Novelas ejemplares
libro 13 3 Los pazos de Ulloa
libro 14 5 Bodas de sangre
libro 15 5 Yerma
libro 16 2 Misericordia
estrategia JOIN FETCH

Salida esperada

SQL: select * from autor a left join libro l on l.autor_id = a.id order by a.id
Cervantes: 2 libros (Don Quijote, Novelas ejemplares)
Galdós: 2 libros (Fortunata y Jacinta, Misericordia)
Emilia Pardo Bazán: 1 libro (Los pazos de Ulloa)
Machado: 0 libros
Lorca: 2 libros (Bodas de sangre, Yerma)
Consultas: 1 (estrategia JOIN FETCH)

Ejemplo: Estrategia BATCH 2

Entrada

autor 1 Cervantes
autor 2 Galdós
autor 3 Emilia Pardo Bazán
autor 4 Machado
autor 5 Lorca
libro 10 1 Don Quijote
libro 11 2 Fortunata y Jacinta
libro 12 1 Novelas ejemplares
libro 13 3 Los pazos de Ulloa
libro 14 5 Bodas de sangre
libro 15 5 Yerma
libro 16 2 Misericordia
estrategia BATCH 2

Salida esperada

SQL: select * from autor order by id
SQL: select * from libro where autor_id in (1, 2)
Cervantes: 2 libros (Don Quijote, Novelas ejemplares)
Galdós: 2 libros (Fortunata y Jacinta, Misericordia)
SQL: select * from libro where autor_id in (3, 4)
Emilia Pardo Bazán: 1 libro (Los pazos de Ulloa)
Machado: 0 libros
SQL: select * from libro where autor_id in (5)
Lorca: 2 libros (Bodas de sangre, Yerma)
Consultas: 4 (estrategia BATCH 2)

79. Los estados de una entidad JPA

Difícil · Mapeo Objeto-Relacional (Hibernate/JPA) · apuntesdam.com/subject/acceso-datos/topic/hibernate

En JPA una entidad puede estar NUEVA (transient), GESTIONADA (managed: el EntityManager vigila sus cambios), SEPARADA (detached) o ELIMINADA (removed), y el SQL no se lanza cuando llamas a los métodos sino en el flush. Simula un EntityManager con una única tabla por entidad e ids IDENTITY (1, 2, 3…). Órdenes: «new v Clase c1=v1,c2=v2» («v: NUEVA (transient)»); «persist v» (NUEVA → inserta al momento: «SQL: insert into clase (c1, c2) values ('v1', 'v2') → id N» y pasa a GESTIONADA; si ya lo está, «v ya está gestionada: no se hace nada»; si está separada, «PersistentObjectException: v está separada (usa merge)»); «set v campo=valor» (si no está gestionada, «v (estado): el cambio solo está en memoria»); «find v Clase id» (si esa fila ya está gestionada en el contexto, se devuelve la misma instancia sin SQL: «v es la misma instancia que w (caché de primer nivel, sin SQL)», con w la primera variable por orden alfabético que la apunta; si no, «SQL: select * from clase where id = N» y «v: GESTIONADA {campos}» o «v = null»); «detach v»; «merge v w» (copia el estado de w en la instancia gestionada con su id, cargándola con un select si no está en el contexto; w no cambia: «v: copia gestionada de w (w sigue separada)»); «remove v» (solo gestionadas: «v: ELIMINADA (se borrará en el flush)»; si no, «IllegalArgumentException: v no está gestionada»); «flush» y «commit» (recorren el contexto en orden: las eliminadas lanzan «SQL: delete …» y dejan de estar en el contexto; las gestionadas con campos distintos de su copia del último flush lanzan «SQL: update clase set c = 'v', … where id = N» solo con los campos cambiados; después «flush: n sentencias» o «COMMIT (n sentencias en el flush)», con «1 sentencia» en singular); «close» («Contexto cerrado: n entidades pasan a SEPARADA») y «estado v».

Código de partida (java)
import java.util.*; public class Main {    static class Entidad {        Long id;        String tabla;        Map<String, String> campos = new LinkedHashMap<>();        String estado = "NUEVA";                   // NUEVA, GESTIONADA, SEPARADA, ELIMINADA        Map<String, String> instantanea;           // copia al pasar a gestionada o tras el último flush    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Entidad> variables = new HashMap<>();        Map<Long, Map<String, String>> baseDatos = new TreeMap<>();        List<Entidad> contexto = new ArrayList<>();    // contexto de persistencia (en orden de entrada)        long[] siguienteId = { 1 };        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            if (linea.isEmpty()) continue;            String[] p = linea.split(" ", 3);            // TODO: new, persist, set, find, detach, merge, remove, flush, commit, close y estado        }    }}

Ejemplo: Ciclo de vida completo

Entrada

new e1 Alumno nombre=Ana,nota=6
persist e1
set e1 nota=8
set e1 nota=6
flush
set e1 nota=9
find e2 Alumno 1
commit
detach e1
set e1 nombre=Ana María
commit
persist e1
merge e3 e1
estado e3
commit
remove e1
remove e3
commit
find e4 Alumno 1

Salida esperada

e1: NUEVA (transient)
SQL: insert into alumno (nombre, nota) values ('Ana', '6') → id 1
flush: 0 sentencias
e2 es la misma instancia que e1 (caché de primer nivel, sin SQL)
SQL: update alumno set nota = '9' where id = 1
COMMIT (1 sentencia en el flush)
e1: SEPARADA
e1 (separada): el cambio solo está en memoria
COMMIT (0 sentencias en el flush)
PersistentObjectException: e1 está separada (usa merge)
SQL: select * from alumno where id = 1
e3: copia gestionada de e1 (e1 sigue separada)
e3: GESTIONADA id=1 {nombre=Ana María, nota=9}
SQL: update alumno set nombre = 'Ana María' where id = 1
COMMIT (1 sentencia en el flush)
IllegalArgumentException: e1 no está gestionada
e3: ELIMINADA (se borrará en el flush)
SQL: delete from alumno where id = 1
COMMIT (1 sentencia en el flush)
SQL: select * from alumno where id = 1
e4 = null

80. Arrays y tipos compuestos de PostgreSQL

Medio · BD Objeto-Relacionales y Orientadas a Objetos · apuntesdam.com/subject/acceso-datos/topic/bd-objeto-relacionales

PostgreSQL permite columnas de tipo array (text[]) y tipos compuestos (CREATE TYPE direccion AS (calle text, cp text, ciudad text)). Si se leen por JDBC con getString, llegan en su formato de texto y hay que interpretarlo. Cada línea de la entrada es «array {…}» o «compuesto (…)»; implementa leerArray y leerCompuesto, que devuelven la lista de valores (null para los nulos). Array: elementos separados por comas; un elemento entre comillas dobles puede contener comas y espacios (los espacios de fuera de las comillas se ignoran) y dentro de él \\ escapa el carácter siguiente (\" es una comilla); sin comillas se quitan los espacios de alrededor y NULL (en cualquier combinación de mayúsculas) es nulo, pero "NULL" entre comillas es el texto NULL; {} es un array vacío. Compuesto: campos separados por comas; un campo vacío sin comillas es nulo y "" es una cadena vacía; entre comillas, "" o \" es una comilla; sin comillas el texto se toma tal cual. El main ya muestra cada resultado como «tipo ["a", null, …] · n valores · nulos: k».

Código de partida (java)
import java.util.*; public class Main {    static String json(List<String> valores) {        List<String> partes = new ArrayList<>();        for (String v : valores)            partes.add(v == null ? "null" : "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\"");        return "[" + String.join(", ", partes) + "]";    }     static List<String> leerArray(String texto) {        // TODO: elementos separados por comas, comillas con escapes y NULL        return new ArrayList<>();    }     static List<String> leerCompuesto(String texto) {        // TODO: campos separados por comas, vacío = nulo y comillas dobladas        return new ArrayList<>();    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String l = sc.nextLine();            if (l.isBlank()) continue;            String tipo = l.substring(0, l.indexOf(' ')), texto = l.substring(l.indexOf(' ') + 1).trim();            List<String> v = tipo.equals("array") ? leerArray(texto) : leerCompuesto(texto);            System.out.println(tipo + " " + json(v) + " · " + v.size() + (v.size() == 1 ? " valor" : " valores")                + " · nulos: " + v.stream().filter(Objects::isNull).count());        }    }}

Ejemplo: Arrays y compuestos

Entrada

array {Lunes,Martes,"Fin de semana",NULL}
array {}
array {"NULL",null, 3 , "con \"comillas\" y \\barra"}
compuesto ("Calle Mayor, 3",28013,Madrid,)
compuesto (,"",abc)
compuesto ("dijo ""hola""",x)

Salida esperada

array ["Lunes", "Martes", "Fin de semana", null] · 4 valores · nulos: 1
array [] · 0 valores · nulos: 0
array ["NULL", null, "3", "con \"comillas\" y \\barra"] · 4 valores · nulos: 1
compuesto ["Calle Mayor, 3", "28013", "Madrid", null] · 4 valores · nulos: 1
compuesto [null, "", "abc"] · 3 valores · nulos: 1
compuesto ["dijo \"hola\"", "x"] · 2 valores · nulos: 0

81. Profundidad de activación en una base de objetos

Difícil · BD Objeto-Relacionales y Orientadas a Objetos · apuntesdam.com/subject/acceso-datos/topic/bd-objeto-relacionales

Las bases de datos orientadas a objetos (db4o, ObjectDB) guardan grafos de objetos con referencias entre ellos. Al recuperar un objeto no cargan todo el grafo: lo «activan» solo hasta cierta profundidad, y los objetos más lejanos quedan como referencias sin rellenar. Las líneas «objeto ID Clase campo=valor ref=@ID …» definen el almacén (los valores con @ son referencias a otros objetos, que pueden formar ciclos). Implementa activar para que «activar ID k» muestre el árbol: con profundidad ≥ 1 el objeto se activa y se muestra «Clase ID {campo=valor, …}» y después cada referencia, en orden, con dos espacios más de sangría y el prefijo «campo → », activada con profundidad k−1; con profundidad 0 se muestra «Clase ID (sin activar)»; si el id no existe, «null (ID no existe)». El main cuenta los objetos distintos activados.

Código de partida (java)
import java.util.*; public class Main {    static class Objeto {        String id, clase;        Map<String, String> datos = new LinkedHashMap<>();       // campos simples        Map<String, String> referencias = new LinkedHashMap<>(); // campo → id del objeto apuntado    }     static Map<String, Objeto> almacen = new LinkedHashMap<>();    static Set<String> activados = new HashSet<>();     static void activar(String id, int profundidad, String sangria, String prefijo) {        // TODO: mostrar el objeto y activar sus referencias con profundidad - 1    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            if (p[0].equals("objeto")) {                     // objeto ID Clase campo=valor ref=@ID …                Objeto o = new Objeto();                o.id = p[1]; o.clase = p[2];                for (int i = 3; i < p.length; i++) {                    String[] kv = p[i].split("=");                    if (kv[1].startsWith("@")) o.referencias.put(kv[0], kv[1].substring(1));                    else o.datos.put(kv[0], kv[1]);                }                almacen.put(o.id, o);            } else if (p[0].equals("activar")) {           // activar ID profundidad                activados.clear();                System.out.println("== activar " + p[1] + " con profundidad " + p[2]);                activar(p[1], Integer.parseInt(p[2]), "", "");                System.out.println("Objetos leídos del disco: " + activados.size());            }        }    }}

Ejemplo: Profundidad 1

Entrada

objeto C1 Curso nombre=1DAM tutor=@P1 delegado=@A1
objeto P1 Profesor nombre=Ana departamento=@D1
objeto D1 Departamento nombre=Informática jefe=@P1
objeto A1 Alumno nombre=Luis grupo=@C1 mentor=@A9
activar C1 1

Salida esperada

== activar C1 con profundidad 1
Curso C1 {nombre=1DAM}
  tutor → Profesor P1 (sin activar)
  delegado → Alumno A1 (sin activar)
Objetos leídos del disco: 1

Ejemplo: Profundidad 3 con ciclo

Entrada

objeto C1 Curso nombre=1DAM tutor=@P1 delegado=@A1
objeto P1 Profesor nombre=Ana departamento=@D1
objeto D1 Departamento nombre=Informática jefe=@P1
objeto A1 Alumno nombre=Luis grupo=@C1 mentor=@A9
activar C1 3

Salida esperada

== activar C1 con profundidad 3
Curso C1 {nombre=1DAM}
  tutor → Profesor P1 {nombre=Ana}
    departamento → Departamento D1 {nombre=Informática}
      jefe → Profesor P1 (sin activar)
  delegado → Alumno A1 {nombre=Luis}
    grupo → Curso C1 {nombre=1DAM}
      tutor → Profesor P1 (sin activar)
      delegado → Alumno A1 (sin activar)
    mentor → null (A9 no existe)
Objetos leídos del disco: 4

82. Herencia de tablas en PostgreSQL

Muy difícil · BD Objeto-Relacionales y Orientadas a Objetos · apuntesdam.com/subject/acceso-datos/topic/bd-objeto-relacionales

PostgreSQL permite que una tabla herede de otra (CREATE TABLE alumno (curso text) INHERITS (persona)): la hija tiene las columnas del padre más las suyas y, al consultar el padre, también salen las filas de las hijas. Pero las restricciones UNIQUE (y las claves primarias) no se heredan entre tablas: cada tabla solo comprueba sus propias filas. Simúlalo. Órdenes: «tabla t columnas=a,b [hereda=padre] [unico=col]» (muestra «CREATE TABLE t (todas las columnas) [INHERITS (padre)] [UNIQUE (col)]»; las columnas heredadas van primero); «insertar t a=1,b=2» (columnas no indicadas a NULL; si una no existe, «ERROR: la columna x no existe en t»; si viola el único de esa tabla, comparando solo con las filas de esa misma tabla, «ERROR: valor duplicado col=v en t»; si no, «INSERT en t»); «consultar t» y «consultar solo t» (como SELECT * FROM t y SELECT * FROM ONLY t). La consulta muestra «SELECT * FROM [ONLY ]t (col | col …)», cada fila con dos espacios delante y sus valores para las columnas de t separados por « | » (NULL si no tiene), añadiendo « [hija]» a las filas que vienen de otra tabla, y al final « n filas» («1 fila»). Primero van las filas propias y después, sin ONLY, las de cada hija en orden de creación (y las de sus hijas, recursivamente).

Código de partida (java)
import java.util.*; public class Main {    static class Tabla {        String nombre, padre, unico;        List<String> columnas = new ArrayList<>();        List<Map<String, String>> filas = new ArrayList<>();        List<String> hijas = new ArrayList<>();    }     static Map<String, Tabla> tablas = new LinkedHashMap<>();     // TODO: static void recoger(Tabla consultada, Tabla t, boolean solo, List<String> salida)     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.isEmpty()) continue;            String[] p = l.split(" ");            // TODO: tabla, insertar, consultar y consultar solo        }    }}

Ejemplo: Personas, alumnos y profesores

Entrada

tabla persona columnas=dni,nombre unico=dni
tabla alumno columnas=curso hereda=persona unico=dni
tabla profesor columnas=departamento hereda=persona
tabla delegado columnas=desde hereda=alumno
insertar persona dni=111,nombre=Conserje
insertar alumno dni=222,nombre=Ana,curso=1DAM
insertar alumno dni=222,nombre=Otra,curso=2DAM
insertar alumno dni=111,nombre=Luis,curso=1DAM
insertar profesor dni=333,nombre=Marta,departamento=Informática
insertar delegado dni=444,nombre=Eva,curso=2DAM,desde=2025
insertar profesor dni=555,nombre=Pepe,curso=1DAM
insertar persona dni=111,nombre=Duplicado
consultar persona
consultar solo persona
consultar alumno

Salida esperada

CREATE TABLE persona (dni, nombre) UNIQUE (dni)
CREATE TABLE alumno (dni, nombre, curso) INHERITS (persona) UNIQUE (dni)
CREATE TABLE profesor (dni, nombre, departamento) INHERITS (persona)
CREATE TABLE delegado (dni, nombre, curso, desde) INHERITS (alumno)
INSERT en persona
INSERT en alumno
ERROR: valor duplicado dni=222 en alumno
INSERT en alumno
INSERT en profesor
INSERT en delegado
ERROR: la columna curso no existe en profesor
ERROR: valor duplicado dni=111 en persona
SELECT * FROM persona (dni | nombre)
  111 | Conserje
  222 | Ana   [alumno]
  111 | Luis   [alumno]
  444 | Eva   [delegado]
  333 | Marta   [profesor]
  5 filas
SELECT * FROM ONLY persona (dni | nombre)
  111 | Conserje
  1 fila
SELECT * FROM alumno (dni | nombre | curso)
  222 | Ana | 1DAM
  111 | Luis | 1DAM
  444 | Eva | 2DAM   [delegado]
  3 filas

83. Propiedad ligada (bound property)

Fácil · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

Completa el JavaBean Termostato para que su propiedad temperatura sea ligada: cada vez que cambie de valor debe avisar a los oyentes con firePropertyChange. El programa lee temperaturas por teclado y los oyentes imprimen cada cambio (si el valor no cambia, no se debe notificar).

Código de partida (java)
import java.beans.*;import java.io.Serializable;import java.util.Scanner; public class Main {     public static class Termostato implements Serializable {        private final PropertyChangeSupport soporte = new PropertyChangeSupport(this);        private int temperatura = 20;         public Termostato() {}         public int getTemperatura() { return temperatura; }         public void setTemperatura(int nueva) {            // TODO: asigna el nuevo valor y avisa a los oyentes del cambio            temperatura = nueva;        }         public void addPropertyChangeListener(PropertyChangeListener l) { soporte.addPropertyChangeListener(l); }        public void removePropertyChangeListener(PropertyChangeListener l) { soporte.removePropertyChangeListener(l); }    }     public static void main(String[] args) {        Termostato t = new Termostato();        t.addPropertyChangeListener(e ->            System.out.println(e.getPropertyName() + ": " + e.getOldValue() + " -> " + e.getNewValue()));         Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) t.setTemperatura(sc.nextInt());        System.out.println("Final: " + t.getTemperatura());    }}

Ejemplo: Tres cambios

Entrada

22 25 18

Salida esperada

temperatura: 20 -> 22
temperatura: 22 -> 25
temperatura: 25 -> 18
Final: 18

Ejemplo: Valor repetido: no se notifica

Entrada

20 21 21

Salida esperada

temperatura: 20 -> 21
Final: 21

84. Propiedad restringida (constrained)

Medio · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

El componente CuentaBancaria tiene una propiedad saldo restringida: antes de cambiarla pregunta a los oyentes con fireVetoableChange, y cualquiera puede vetar el cambio lanzando PropertyVetoException. Completa setSaldo. El programa registra un oyente que veta saldos negativos y aplica los movimientos leídos por teclado.

Código de partida (java)
import java.beans.*;import java.util.Scanner; public class Main {     public static class CuentaBancaria {        private final VetoableChangeSupport vetos = new VetoableChangeSupport(this);        private double saldo = 100;         public double getSaldo() { return saldo; }         public void setSaldo(double nuevo) throws PropertyVetoException {            // TODO: pregunta a los oyentes y, si nadie veta, cambia el saldo            saldo = nuevo;        }         public void addVetoableChangeListener(VetoableChangeListener l) { vetos.addVetoableChangeListener(l); }    }     public static void main(String[] args) {        CuentaBancaria cuenta = new CuentaBancaria();        cuenta.addVetoableChangeListener(e -> {            if ((double) e.getNewValue() < 0) throw new PropertyVetoException("saldo negativo", e);        });         Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            int movimiento = sc.nextInt();            try {                cuenta.setSaldo(cuenta.getSaldo() + movimiento);                System.out.println("OK " + movimiento + " -> " + cuenta.getSaldo());            } catch (PropertyVetoException ex) {                System.out.println("Vetado " + movimiento + ": " + ex.getMessage());            }        }    }}

Ejemplo: Ingreso y retirada válidos

Entrada

50 -120

Salida esperada

OK 50 -> 150.0
OK -120 -> 30.0

Ejemplo: Retirada que deja saldo negativo

Entrada

-150 20

Salida esperada

Vetado -150: saldo negativo
OK 20 -> 120.0

85. Persistencia de un JavaBean en XML

Medio · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

Los JavaBeans pueden guardarse en XML con XMLEncoder y recuperarse con XMLDecoder, siempre que sigan las convenciones (constructor vacío, getters y setters). Completa guardar() y cargar() para el bean Configuracion. El programa lee el tema y el tamaño de letra por teclado, guarda el bean en config.xml, lo vuelve a cargar y muestra sus valores.

Código de partida (java)
import java.beans.*;import java.io.*;import java.util.Scanner; public class Main {     public static class Configuracion {        private String tema = "claro";        private int tamanoLetra = 14;         public Configuracion() {}        public String getTema() { return tema; }        public void setTema(String tema) { this.tema = tema; }        public int getTamanoLetra() { return tamanoLetra; }        public void setTamanoLetra(int t) { this.tamanoLetra = t; }    }     static void guardar(Configuracion c, File fichero) throws IOException {        // TODO: guarda el bean en XML con XMLEncoder    }     static Configuracion cargar(File fichero) throws IOException {        // TODO: recupera el bean con XMLDecoder        return new Configuracion();    }     public static void main(String[] args) throws IOException {        Scanner sc = new Scanner(System.in);        Configuracion c = new Configuracion();        c.setTema(sc.next());        c.setTamanoLetra(sc.nextInt());         File f = new File("config.xml");        guardar(c, f);        Configuracion leida = cargar(f);        System.out.println(leida.getTema() + " " + leida.getTamanoLetra());        System.out.println("XML generado: " + (f.length() > 0));    }}

Ejemplo: Tema oscuro, letra 16

Entrada

oscuro 16

Salida esperada

oscuro 16
XML generado: true

86. Componente DAO con eventos

Difícil · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

Implementa AlumnoDaoMemoria, un componente de acceso a datos que cumple la interfaz AlumnoDao y además avisa a sus oyentes (DaoListener) cada vez que se guarda o se borra un alumno. El programa ejecuta las órdenes leídas por teclado: 'G nombre nota' guarda, 'B nombre' borra y 'L' lista los alumnos ordenados por nombre.

Código de partida (java)
import java.util.*; public class Main {     public interface DaoListener { void datosCambiados(String accion, String nombre); }     public interface AlumnoDao {        void guardar(String nombre, double nota);        boolean borrar(String nombre);        Map<String, Double> listar();        void addDaoListener(DaoListener l);    }     public static class AlumnoDaoMemoria implements AlumnoDao {        private final Map<String, Double> alumnos = new TreeMap<>();        private final List<DaoListener> oyentes = new ArrayList<>();         public void guardar(String nombre, double nota) {            // TODO: guarda y avisa a los oyentes        }         public boolean borrar(String nombre) {            // TODO: borra y avisa a los oyentes si existía            return false;        }         public Map<String, Double> listar() { return Collections.unmodifiableMap(alumnos); }         public void addDaoListener(DaoListener l) { oyentes.add(l); }    }     public static void main(String[] args) {        AlumnoDao dao = new AlumnoDaoMemoria();        dao.addDaoListener((accion, nombre) -> System.out.println("[evento] " + accion + " " + nombre));         Scanner sc = new Scanner(System.in);        while (sc.hasNext()) {            String orden = sc.next();            switch (orden) {                case "G" -> dao.guardar(sc.next(), Double.parseDouble(sc.next()));                case "B" -> { String n = sc.next(); if (!dao.borrar(n)) System.out.println("No existe " + n); }                case "L" -> dao.listar().forEach((n, nota) -> System.out.println(n + " " + nota));                default -> System.out.println("Orden desconocida");            }        }    }}

Ejemplo: Guardar y listar

Entrada

G Luis 6 G Ana 8.5 L

Salida esperada

[evento] guardado Luis
[evento] guardado Ana
Ana 8.5
Luis 6.0

Ejemplo: Borrar existente e inexistente

Entrada

G Ana 7 B Ana B Pepe L

Salida esperada

[evento] guardado Ana
[evento] borrado Ana
No existe Pepe

87. Un contenedor de inyección de dependencias

Muy difícil · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

Spring, Jakarta CDI o Micronaut son contenedores de componentes: el programa declara qué componentes hay y de qué dependen, y el contenedor los crea en el orden correcto y los inyecta. Simula uno. «componente N [depende=A,B] [ambito=prototype]» declara un componente (singleton por defecto: una sola instancia; prototype: una nueva cada vez). «obtener N» muestra «obtener N» y antes de crear nada valida el grafo recorriendo las dependencias en orden: si falta un componente, « Error: no existe el componente N» (el pedido) o « Error: falta el componente X (lo necesita Y)»; si hay un ciclo, « Error: dependencia circular A → B → C → A» (desde la primera aparición). Si es válido, crea primero las dependencias (en orden, recursivamente) y después el componente, numerando las instancias globalmente desde 1: « crear X#k» (con « (prototype)» si lo es); los singletons ya creados se reutilizan sin mostrar nada. Termina con «→ N#k» y « (ya existía)» si era un singleton ya creado. «cerrar» destruye los singletons en orden inverso a su creación (« destruir X#k»; los prototype no los gestiona el contenedor), los olvida y muestra «Contenedor cerrado».

Código de partida (java)
import java.util.*; public class Main {    static Map<String, List<String>> dependencias = new LinkedHashMap<>();    static Map<String, String> ambitos = new HashMap<>();    static Map<String, Integer> singletons = new HashMap<>();   // componente → número de instancia    static List<String> creados = new ArrayList<>();            // singletons en orden de creación (para cerrar)    static int contador = 0;     // TODO: static String validar(String nombre, List<String> camino, String quien)    //       static int resolver(String nombre)     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            switch (p[0]) {                case "componente" -> {                      // componente N [depende=A,B] [ambito=prototype]                    List<String> deps = new ArrayList<>();                    String ambito = "singleton";                    for (int i = 2; i < p.length; i++) {                        if (p[i].startsWith("depende=")) deps.addAll(List.of(p[i].substring(8).split(",")));                        if (p[i].startsWith("ambito=")) ambito = p[i].substring(7);                    }                    dependencias.put(p[1], deps);                    ambitos.put(p[1], ambito);                }                case "obtener" -> {                    System.out.println("obtener " + p[1]);                    // TODO: validar y, si no hay error, resolver                }                case "cerrar" -> {                    // TODO: destruir los singletons en orden inverso al de creación                    System.out.println("Contenedor cerrado");                }            }        }    }}

Ejemplo: Tienda

Entrada

componente DataSource
componente PedidoDao depende=DataSource
componente ClienteDao depende=DataSource
componente Correo ambito=prototype
componente Notificador depende=Correo
componente PedidoService depende=PedidoDao,ClienteDao,Notificador
componente Informe depende=PedidoDao,Correo ambito=prototype
obtener PedidoService
obtener PedidoService
obtener Informe
obtener Informe
obtener Factura
cerrar
obtener ClienteDao

Salida esperada

obtener PedidoService
  crear DataSource#1
  crear PedidoDao#2
  crear ClienteDao#3
  crear Correo#4 (prototype)
  crear Notificador#5
  crear PedidoService#6
→ PedidoService#6
obtener PedidoService
→ PedidoService#6 (ya existía)
obtener Informe
  crear Correo#7 (prototype)
  crear Informe#8 (prototype)
→ Informe#8
obtener Informe
  crear Correo#9 (prototype)
  crear Informe#10 (prototype)
→ Informe#10
obtener Factura
  Error: no existe el componente Factura
  destruir PedidoService#6
  destruir Notificador#5
  destruir ClienteDao#3
  destruir PedidoDao#2
  destruir DataSource#1
Contenedor cerrado
obtener ClienteDao
  crear DataSource#11
  crear ClienteDao#12
→ ClienteDao#12

88. Filtrar un fichero de log

Fácil · Ejercicios de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/ejercicios-acceso-datos

El programa guarda en app.log las líneas recibidas por teclado. Completa filtrar() para que lea app.log y escriba en errores.log solo las líneas que contienen "ERROR", devolviendo cuántas ha copiado. Después se muestra el número y el contenido de errores.log.

Código de partida (java)
import java.io.*;import java.nio.file.*;import java.util.*; public class Main {     static int filtrar(Path origen, Path destino) throws IOException {        // TODO: copia a destino las líneas de origen que contienen "ERROR"        return 0;    }     public static void main(String[] args) throws IOException {        Path log = Path.of("app.log");        Files.write(log, new String(System.in.readAllBytes()).lines().toList());         Path errores = Path.of("errores.log");        int n = filtrar(log, errores);        System.out.println("Errores: " + n);        if (Files.exists(errores)) Files.readAllLines(errores).forEach(System.out::println);    }}

Ejemplo: Dos errores

Entrada

10:00 INFO arranque
10:01 ERROR conexión perdida
10:02 WARN reintento
10:03 ERROR tiempo agotado

Salida esperada

Errores: 2
10:01 ERROR conexión perdida
10:03 ERROR tiempo agotado

Ejemplo: Sin errores

Entrada

INFO todo bien
INFO fin

Salida esperada

Errores: 0

89. Conversor de CSV a JSON

Medio · Ejercicios de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/ejercicios-acceso-datos

Convierte un fichero CSV con cabecera (nombre;ciudad;edad) en un array JSON, sin librerías externas. El programa guarda la entrada en personas.csv; completa convertir() para que devuelva el JSON con el formato exacto del ejemplo: [{"nombre":"Ana","ciudad":"Logroño","edad":20}, …] (la edad sin comillas, porque es un número).

Código de partida (java)
import java.io.*;import java.nio.file.*;import java.util.*; public class Main {     static String convertir(Path csv) throws IOException {        List<String> lineas = Files.readAllLines(csv);        String[] cabecera = lineas.get(0).split(";");        List<String> objetos = new ArrayList<>();        // TODO: recorre las líneas de datos y construye un objeto JSON por línea         return "[" + String.join(",", objetos) + "]";    }     public static void main(String[] args) throws IOException {        Path csv = Path.of("personas.csv");        Files.write(csv, new String(System.in.readAllBytes()).lines().toList());        System.out.println(convertir(csv));    }}

Ejemplo: Dos personas

Entrada

nombre;ciudad;edad
Ana;Logroño;20
Luis;Arnedo;22

Salida esperada

[{"nombre":"Ana","ciudad":"Logroño","edad":20},{"nombre":"Luis","ciudad":"Arnedo","edad":22}]

Ejemplo: Solo cabecera

Entrada

nombre;ciudad;edad

Salida esperada

[]

90. Media de precios por categoría (DOM)

Difícil · Ejercicios de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/ejercicios-acceso-datos

El programa guarda el XML recibido en catalogo.xml. Cada <producto> tiene un atributo categoria y un elemento <precio>. Completa medias() para devolver un mapa ordenado categoría → precio medio. Se imprime cada categoría con su media con dos decimales.

Código de partida (java)
import java.io.*;import java.nio.file.*;import java.util.*;import javax.xml.parsers.*;import org.w3c.dom.*; public class Main {     static Map<String, Double> medias(File fichero) throws Exception {        Map<String, Double> resultado = new TreeMap<>();        // TODO: calcula el precio medio de cada categoría         return resultado;    }     public static void main(String[] args) throws Exception {        File f = new File("catalogo.xml");        Files.write(f.toPath(), System.in.readAllBytes());        medias(f).forEach((cat, media) -> System.out.printf(Locale.US, "%s %.2f%n", cat, media));    }}

Ejemplo: Dos categorías

Entrada

<catalogo><producto categoria="audio"><precio>40</precio></producto><producto categoria="video"><precio>100</precio></producto><producto categoria="audio"><precio>50</precio></producto></catalogo>

Salida esperada

audio 45.00
video 100.00

Desarrollo de Interfaces

91. La validación de un formulario de registro

Medio · Generación de Interfaces Gráficas · apuntesdam.com/subject/desarrollo-interfaces/topic/generacion-interfaces

Antes de habilitar el botón «Registrar», una interfaz valida cada campo y muestra un mensaje junto a él. Cada línea de la entrada es «campo=valor» (nombre, email, edad, clave y repetir; puede faltar alguno, y entonces vale vacío). Comprueba, en este orden: nombre obligatorio («nombre: es obligatorio») y de al menos 2 caracteres («nombre: debe tener al menos 2 caracteres»); email con formato algo@algo.dominio, con al menos 2 letras en el dominio («email: formato no válido»); edad entera («edad: debe ser un número entero») entre 18 y 120 («edad: debe estar entre 18 y 120»); clave con al menos 8 caracteres, una mayúscula y un número («clave: necesita 8 caracteres, una mayúscula, un número», con solo lo que falte); y repetir igual a clave («repetir: las contraseñas no coinciden»). Muestra cada error y al final «N errores: botón Registrar desactivado» («1 error» en singular), o «Formulario válido: botón Registrar activado».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, String> campos = new LinkedHashMap<>();        while (sc.hasNextLine()) {            String linea = sc.nextLine();            int i = linea.indexOf('=');            if (i > 0) campos.put(linea.substring(0, i).trim(), linea.substring(i + 1).trim());        }        // TODO: valida cada campo y muestra los errores como lo haría el formulario    }}

Ejemplo: Formulario correcto

Entrada

nombre=Lucía
email=lucia@correo.es
edad=19
clave=Segura123
repetir=Segura123

Salida esperada

Formulario válido: botón Registrar activado

Ejemplo: Varios errores

Entrada

nombre=L
email=lucia@correo
edad=17
clave=abc
repetir=abd

Salida esperada

nombre: debe tener al menos 2 caracteres
email: formato no válido
edad: debe estar entre 18 y 120
clave: necesita 8 caracteres, una mayúscula, un número
repetir: las contraseñas no coinciden
5 errores: botón Registrar desactivado

92. Repartir el ancho como un GridPane

Difícil · Generación de Interfaces Gráficas · apuntesdam.com/subject/desarrollo-interfaces/topic/generacion-interfaces

Un contenedor de rejilla (GridPane de JavaFX, GridBagLayout de Swing o grid de CSS) reparte su ancho entre columnas. La entrada es «ancho W», «hueco H» (separación entre columnas) y una línea por columna: «nombre fija N» (N píxeles), «nombre porcentaje P» (P % del espacio disponible) o «nombre crece» (se reparte a partes iguales lo que sobra). El espacio disponible es W − H × (columnas − 1). Muestra para cada columna «nombre: x=X ancho=A» (1 decimal, x empieza en 0 y entre columnas se suma el hueco). Si no hay columnas que crezcan y sobra espacio, añade «Sobran S px a la derecha». Si lo fijo y los porcentajes ya superan el disponible, o no queda nada para las columnas que crecen, muestra solo «No cabe: faltan F px» (lo que excede, más 1 px por cada columna que crece, que necesita al menos uno).

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int ancho = Integer.parseInt(sc.nextLine().split(" ")[1]);        int hueco = Integer.parseInt(sc.nextLine().split(" ")[1]);        List<String[]> columnas = new ArrayList<>();   // {nombre, tipo, valor}        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (!l.isEmpty()) columnas.add(l.split(" "));        }        // TODO: calcula la posición y el ancho de cada columna como un GridPane    }}

Ejemplo: Formulario de tres columnas

Entrada

ancho 800
hueco 10
etiqueta fija 120
campo crece
ayuda porcentaje 25

Salida esperada

etiqueta: x=0.0 ancho=120.0
campo: x=130.0 ancho=465.0
ayuda: x=605.0 ancho=195.0

Ejemplo: Dos que crecen

Entrada

ancho 1000
hueco 20
menu fija 200
contenido crece
lateral crece

Salida esperada

menu: x=0.0 ancho=200.0
contenido: x=220.0 ancho=380.0
lateral: x=620.0 ancho=380.0

Ejemplo: Sobra espacio

Entrada

ancho 600
hueco 0
a fija 100
b porcentaje 50

Salida esperada

a: x=0.0 ancho=100.0
b: x=100.0 ancho=300.0
Sobran 200.0 px a la derecha

93. Captura y burbuja de un evento

Muy difícil · Generación de Interfaces Gráficas · apuntesdam.com/subject/desarrollo-interfaces/topic/generacion-interfaces

En JavaFX (y en el DOM), un evento recorre la jerarquía de nodos en dos fases: en la de captura se ejecutan los filtros (addEventFilter) desde la raíz hasta el nodo objetivo, y en la de burbuja los manejadores (addEventHandler / setOnAction) desde el objetivo hasta la raíz. Si alguno consume el evento (event.consume()), los demás nodos ya no lo reciben, aunque se terminan de ejecutar los demás del mismo nodo. Las líneas son «nodo HIJO PADRE», «filtro NODO [consume]», «manejador NODO [consume]» (en el orden en que se registraron) y «clic NODO» (el objetivo). Muestra «Ruta: Raíz > … > Objetivo», cada filtro o manejador ejecutado con «filtro de NODO» o «manejador de NODO» (y « (consume el evento)» si lo consume) y al final «Evento consumido» o «El evento llegó a la raíz sin consumirse».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, String> padre = new HashMap<>();        List<String[]> manejadores = new ArrayList<>();   // {tipo (filtro|manejador), nodo, consume?}        String objetivo = null;        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            switch (p[0]) {                case "nodo" -> padre.put(p[1], p[2]);          // nodo HIJO PADRE                case "filtro", "manejador" -> manejadores.add(p);                case "clic" -> objetivo = p[1];                default -> { }            }        }        // TODO: simula la cadena de despacho del evento (captura y burbuja)    }}

Ejemplo: Sin consumir

Entrada

nodo VBox Ventana
nodo HBox VBox
nodo Boton HBox
nodo Etiqueta HBox
manejador Boton
filtro VBox
manejador Ventana
manejador HBox
clic Boton

Salida esperada

Ruta: Ventana > VBox > HBox > Boton
filtro de VBox
manejador de Boton
manejador de HBox
manejador de Ventana
El evento llegó a la raíz sin consumirse

Ejemplo: Un filtro consume

Entrada

nodo VBox Ventana
nodo HBox VBox
nodo Boton HBox
nodo Etiqueta HBox
filtro Ventana
filtro HBox consume
filtro Boton
manejador Boton
clic Boton

Salida esperada

Ruta: Ventana > VBox > HBox > Boton
filtro de Ventana
filtro de HBox (consume el evento)
Evento consumido

94. Un intérprete de órdenes de voz

Medio · Interfaces Naturales de Usuario · apuntesdam.com/subject/desarrollo-interfaces/topic/interfaces-naturales

El reconocedor de voz (SpeechRecognizer en Android) devuelve texto; la aplicación debe extraer la intención y sus datos. Cada línea es una transcripción. Normalízala (la función normalizar quita tildes, signos y espacios de más) y reconoce, en este orden: una alarma («pon/poner/pone/programa [una] alarma a las H[:MM] [y media|y cuarto]») → «ALARMA HH:MM» con dos cifras, o «ERROR: hora no válida» si la hora pasa de 23 o los minutos de 59; una llamada («llama/llamar a NOMBRE») → «LLAMAR Nombre Apellido» con la inicial de cada palabra en mayúscula (sin tildes, porque se han quitado al normalizar); el volumen («sube/baja [el] volumen [N]», 10 si no se dice) → «VOLUMEN +N» o «VOLUMEN -N»; abrir una aplicación («abre/abrir/inicia [la app|la aplicacion] NOMBRE») → «ABRIR nombre». Si no encaja nada, «NO ENTENDIDO: frase original».

Código de partida (java)
import java.text.Normalizer;import java.util.*;import java.util.regex.*; public class Main {    static String normalizar(String s) {        String sinTildes = Normalizer.normalize(s.toLowerCase(), Normalizer.Form.NFD).replaceAll("\\p{M}", "");        return sinTildes.replaceAll("[^a-z0-9: ]", " ").replaceAll("\\s+", " ").trim();    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String frase = sc.nextLine();            if (frase.isBlank()) continue;            String t = normalizar(frase);            // TODO: reconoce la intención y sus datos        }    }}

Ejemplo: Órdenes habituales

Entrada

Pon una alarma a las 7:30
Llama a mamá
Sube el volumen
Abre Spotify, por favor
¿Qué tiempo hace mañana?

Salida esperada

ALARMA 07:30
LLAMAR Mama
VOLUMEN +10
ABRIR spotify
NO ENTENDIDO: ¿Qué tiempo hace mañana?

Ejemplo: Variantes

Entrada

Oye, programa alarma a las 6 y media
baja volumen 25
Poner alarma a las 25
llamar a Juan Carlos

Salida esperada

ALARMA 06:30
VOLUMEN -25
ERROR: hora no válida
LLAMAR Juan Carlos

95. Clasificar gestos táctiles

Difícil · Interfaces Naturales de Usuario · apuntesdam.com/subject/desarrollo-interfaces/topic/interfaces-naturales

Un detector de gestos (GestureDetector en Android, o los eventos de puntero en JavaFX y en la web) decide qué ha hecho el dedo a partir de sus posiciones. Cada gesto es un bloque de líneas «tiempo_ms x y» (de que el dedo toca a que se levanta), y los gestos se separan con una línea en blanco. Con el primer y el último punto calcula la distancia (Math.hypot), la duración y la velocidad (px/ms). Clasifica: si la distancia es menor que 10 px, «TOQUE» (menos de 300 ms), «PULSACIÓN LARGA» (500 ms o más) o «INDECISO» (entre medias); si es de al menos 50 px y la velocidad de al menos 0.3 px/ms, un deslizamiento en la dirección del eje dominante: «DESLIZAR DERECHA/IZQUIERDA» si |dx| ≥ |dy| o «DESLIZAR ABAJO/ARRIBA» (la y crece hacia abajo en la pantalla); en otro caso, «ARRASTRE». Muestra «Gesto N: TIPO (D px en T ms, V px/ms)» con D y T sin decimales y V con 2.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<List<double[]>> gestos = new ArrayList<>();        List<double[]> actual = new ArrayList<>();        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.isEmpty()) { if (!actual.isEmpty()) { gestos.add(actual); actual = new ArrayList<>(); } continue; }            String[] p = l.split(" ");               // tiempo_ms x y            actual.add(new double[]{Double.parseDouble(p[0]), Double.parseDouble(p[1]), Double.parseDouble(p[2])});        }        if (!actual.isEmpty()) gestos.add(actual);        // TODO: clasifica cada gesto (toque, pulsación larga, deslizamiento o arrastre)    }}

Ejemplo: Gestos básicos

Entrada

0 100 200
80 102 201

0 50 300
120 200 305
200 320 310

0 300 300
700 303 302

Salida esperada

Gesto 1: TOQUE (2 px en 80 ms, 0.03 px/ms)
Gesto 2: DESLIZAR DERECHA (270 px en 200 ms, 1.35 px/ms)
Gesto 3: PULSACIÓN LARGA (4 px en 700 ms, 0.01 px/ms)

Ejemplo: Vertical y lento

Entrada

0 200 600
150 205 300

0 100 100
1000 260 100

0 10 10
400 12 13

Salida esperada

Gesto 1: DESLIZAR ARRIBA (300 px en 150 ms, 2.00 px/ms)
Gesto 2: ARRASTRE (160 px en 1000 ms, 0.16 px/ms)
Gesto 3: INDECISO (4 px en 400 ms, 0.01 px/ms)

96. ¿Qué gesto hace la mano?

Muy difícil · Interfaces Naturales de Usuario · apuntesdam.com/subject/desarrollo-interfaces/topic/interfaces-naturales

Los detectores de manos (MediaPipe Hands, ML Kit) devuelven las coordenadas de 21 puntos de la mano. Aquí recibes 11 por mano, en coordenadas normalizadas (de 0 a 1, la y hacia abajo): «muneca x y» y, para cada dedo (pulgar, indice, corazon, anular, menique), su punta («dedo_punta x y») y su articulación media («dedo_medio x y»). Las manos se separan con «---». Un dedo está extendido si su punta está más lejos de la muñeca que su articulación media multiplicada por 1.15 (1.1 en el pulgar): así no importa cómo esté girada la mano. Hay PINZA si la distancia entre las puntas del pulgar y del índice es menor que 0.25 veces la distancia de la muñeca a la articulación media del índice. Gestos, en este orden: PINZA, PUÑO (ningún dedo extendido), MANO ABIERTA (los cinco), PULGAR (solo el pulgar), VICTORIA (solo índice y corazón), SEÑALAR (solo el índice) o DESCONOCIDO. Muestra «Mano N: GESTO (extendidos: dedo1, dedo2…)», con los dedos en el orden del array DEDOS o «ninguno».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<Map<String, double[]>> manos = new ArrayList<>();        Map<String, double[]> actual = new LinkedHashMap<>();        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.equals("---")) { manos.add(actual); actual = new LinkedHashMap<>(); continue; }            if (l.isEmpty()) continue;            String[] p = l.split(" ");                 // punto x y  (coordenadas normalizadas de 0 a 1)            actual.put(p[0], new double[]{Double.parseDouble(p[1]), Double.parseDouble(p[2])});        }        if (!actual.isEmpty()) manos.add(actual);        String[] DEDOS = {"pulgar", "indice", "corazon", "anular", "menique"};        // TODO: decide qué dedos están extendidos y qué gesto hace cada mano    }}

Ejemplo: Gestos básicos

Entrada

muneca 0.500 0.850
pulgar_medio 0.344 0.760
pulgar_punta 0.370 0.775
indice_medio 0.435 0.609
indice_punta 0.448 0.657
corazon_medio 0.500 0.600
corazon_punta 0.500 0.650
anular_medio 0.565 0.609
anular_punta 0.552 0.657
menique_medio 0.625 0.633
menique_punta 0.600 0.677
---
muneca 0.500 0.850
pulgar_medio 0.344 0.760
pulgar_punta 0.240 0.700
indice_medio 0.435 0.609
indice_punta 0.391 0.444
corazon_medio 0.500 0.600
corazon_punta 0.500 0.430
anular_medio 0.565 0.609
anular_punta 0.609 0.444
menique_medio 0.625 0.633
menique_punta 0.710 0.486
---
muneca 0.500 0.850
pulgar_medio 0.344 0.760
pulgar_punta 0.370 0.775
indice_medio 0.435 0.609
indice_punta 0.391 0.444
corazon_medio 0.500 0.600
corazon_punta 0.500 0.430
anular_medio 0.565 0.609
anular_punta 0.552 0.657
menique_medio 0.625 0.633
menique_punta 0.600 0.677
---
muneca 0.500 0.850
pulgar_medio 0.344 0.760
pulgar_punta 0.370 0.775
indice_medio 0.435 0.609
indice_punta 0.391 0.444
corazon_medio 0.500 0.600
corazon_punta 0.500 0.650
anular_medio 0.565 0.609
anular_punta 0.552 0.657
menique_medio 0.625 0.633
menique_punta 0.600 0.677

Salida esperada

Mano 1: PUÑO (extendidos: ninguno)
Mano 2: MANO ABIERTA (extendidos: pulgar, indice, corazon, anular, menique)
Mano 3: VICTORIA (extendidos: indice, corazon)
Mano 4: SEÑALAR (extendidos: indice)

Ejemplo: Mano girada y pinza

Entrada

muneca 0.500 0.850
pulgar_medio 0.554 0.756
pulgar_punta 0.545 0.772
indice_medio 0.645 0.811
indice_punta 0.743 0.785
corazon_medio 0.650 0.850
corazon_punta 0.752 0.850
anular_medio 0.645 0.889
anular_punta 0.616 0.881
menique_medio 0.630 0.925
menique_punta 0.604 0.910
---
muneca 0.500 0.850
pulgar_medio 0.326 0.897
pulgar_punta 0.210 0.928
indice_medio 0.283 0.725
indice_punta 0.327 0.750
corazon_medio 0.323 0.673
corazon_punta 0.359 0.709
anular_medio 0.375 0.633
anular_punta 0.400 0.677
menique_medio 0.435 0.609
menique_punta 0.448 0.657
---
muneca 0.500 0.850
pulgar_medio 0.344 0.760
pulgar_punta 0.401 0.454
indice_medio 0.435 0.609
indice_punta 0.391 0.444
corazon_medio 0.500 0.600
corazon_punta 0.500 0.430
anular_medio 0.565 0.609
anular_punta 0.609 0.444
menique_medio 0.625 0.633
menique_punta 0.710 0.486

Salida esperada

Mano 1: VICTORIA (extendidos: indice, corazon)
Mano 2: PULGAR (extendidos: pulgar)
Mano 3: PINZA (extendidos: pulgar, indice, corazon, anular, menique)

97. Un componente de valoración por estrellas

Medio · Creación de Componentes Visuales · apuntesdam.com/subject/desarrollo-interfaces/topic/creacion-componentes

Completa la clase Valoracion, un componente reutilizable como los que se crean extendiendo un control (JavaFX) o una View (Android). Tiene un valor entre 0 y 5 en medios puntos. setValor recorta el valor al rango [0, 5] y lo redondea al medio punto más cercano (Math.round(v * 2) / 2.0); si cambia, avisa con «cambio: viejo → nuevo». clic(n) pone n estrellas, salvo que n sea igual al valor actual, que lo pone a 0 (pulsar otra vez la misma estrella borra la valoración). pintar() devuelve las 5 estrellas («★» llena, «½» media, «☆» vacía) seguidas del valor entre paréntesis. Los valores se muestran sin «.0» si son enteros (3, no 3.0).

Código de partida (java)
import java.util.*; public class Main {    // TODO: completa el componente de valoración    static class Valoracion {        private double valor = 0;         void setValor(double v) { }        void clic(int estrella) { }        String pintar() { return ""; }    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Valoracion v = new Valoracion();        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            if (p[0].equals("set")) v.setValor(Double.parseDouble(p[1]));            else if (p[0].equals("clic")) v.clic(Integer.parseInt(p[1]));            else continue;            System.out.println(v.pintar());        }    }}

Ejemplo: Uso normal

Entrada

clic 4
set 3.7
set 3.6
clic 4
clic 4

Salida esperada

cambio: 0 → 4
★★★★☆ (4)
cambio: 4 → 3.5
★★★½☆ (3.5)
★★★½☆ (3.5)
cambio: 3.5 → 4
★★★★☆ (4)
cambio: 4 → 0
☆☆☆☆☆ (0)

98. Propiedades observables y bindings

Difícil · Creación de Componentes Visuales · apuntesdam.com/subject/desarrollo-interfaces/topic/creacion-componentes

Las propiedades de JavaFX (IntegerProperty, StringProperty…) avisan a sus oyentes cuando cambian y se pueden enlazar: una propiedad enlazada con bind sigue siempre el valor de otra. Implementa Propiedad (valor inicial 0) para que funcionen las órdenes: «set P v» cambia el valor (si P está enlazada, muestra «ERROR: P está enlazada a Q y no se puede cambiar»); «escuchar P» añade un oyente; «bind P Q» enlaza P a Q (P toma el valor de Q y lo sigue en adelante; si crearía un ciclo, «ERROR: enlace circular»); «unbind P» rompe el enlace; y «get P» muestra «P = valor». Cuando un valor cambia de verdad (si es igual, no pasa nada), cada oyente de esa propiedad muestra « P oyente N: viejo → nuevo» (N desde 1, con dos espacios delante) y el cambio se propaga a las propiedades enlazadas a ella, en el orden en que se enlazaron.

Código de partida (java)
import java.util.*;import java.util.function.*; public class Main {    // TODO: implementa una propiedad observable con listeners y enlace (bind) como las de JavaFX    static class Propiedad {        final String nombre;        Propiedad(String nombre) { this.nombre = nombre; }        int get() { return 0; }        void set(int v) { }        void escuchar() { }        void bind(Propiedad otra) { }        void unbind() { }    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Propiedad> props = new LinkedHashMap<>();        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            if (p.length < 2) continue;            Propiedad a = props.computeIfAbsent(p[1], Propiedad::new);            switch (p[0]) {                case "set" -> a.set(Integer.parseInt(p[2]));                case "escuchar" -> a.escuchar();                case "bind" -> a.bind(props.computeIfAbsent(p[2], Propiedad::new));                case "unbind" -> a.unbind();                case "get" -> System.out.println(a.nombre + " = " + a.get());            }        }    }}

Ejemplo: Enlace sencillo

Entrada

escuchar ancho
set ancho 100
set ancho 100
escuchar copia
bind copia ancho
set ancho 250
set copia 5
get copia
unbind copia
set copia 5
get copia

Salida esperada

ancho oyente 1: 0 → 100
  copia oyente 1: 0 → 100
  ancho oyente 1: 100 → 250
  copia oyente 1: 100 → 250
ERROR: copia está enlazada a ancho y no se puede cambiar
copia = 250
  copia oyente 1: 250 → 5
copia = 5

Ejemplo: Cadena y ciclo

Entrada

escuchar c
bind b a
bind c b
set a 7
bind a c
escuchar a
set a 8
get c

Salida esperada

c oyente 1: 0 → 7
ERROR: enlace circular
  a oyente 1: 7 → 8
  c oyente 1: 7 → 8
c = 8

99. Un campo de texto con máscara

Muy difícil · Creación de Componentes Visuales · apuntesdam.com/subject/desarrollo-interfaces/topic/creacion-componentes

Los componentes de entrada con máscara (fechas, teléfonos, matrículas) solo aceptan caracteres válidos e insertan solos los separadores. La primera línea es «mascara M», donde en M # es un dígito, A una letra (se guarda en mayúscula), ? cualquier carácter y el resto son literales; la segunda, las teclas pulsadas separadas por espacios (un carácter, ESPACIO o BORRAR). Al escribir, primero se añaden los literales que haya antes del siguiente hueco; si la tecla no encaja con el hueco, se rechaza «(se esperaba un dígito)» o «(se esperaba una letra)»; si la máscara ya está llena, «(campo completo)». BORRAR quita el último carácter y, además, los literales que queden al final. Muestra «Pantalla: …» con lo escrito y los huecos restantes como _ (los literales sin escribir se muestran tal cual), «Completo: sí/no» y una línea «Rechazada: tecla (motivo)» por cada tecla rechazada.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String mascara = sc.nextLine().substring("mascara ".length());   // # dígito, A letra, ? cualquiera; lo demás es literal        List<String> teclas = new ArrayList<>(Arrays.asList(sc.nextLine().split(" ")));        // TODO: simula un campo de texto con máscara pulsando las teclas en orden    }}

Ejemplo: Fecha

Entrada

mascara ##/##/####
1 5 0 3 2 0 2 6

Salida esperada

Pantalla: 15/03/2026
Completo: sí

Ejemplo: Matrícula con errores

Entrada

mascara #### AAA
1 2 x 3 4 b c 9 d e

Salida esperada

Pantalla: 1234 BCD
Completo: sí
Rechazada: x (se esperaba un dígito)
Rechazada: 9 (se esperaba una letra)
Rechazada: e (campo completo)

Ejemplo: Teléfono con borrado

Entrada

mascara (###) ### ###
9 4 2 BORRAR BORRAR BORRAR 6 1 2 3 4 5 6 7 8 9

Salida esperada

Pantalla: (612) 345 678
Completo: sí
Rechazada: 9 (campo completo)

100. Contraste de colores según WCAG

Medio · Usabilidad y Accesibilidad · apuntesdam.com/subject/desarrollo-interfaces/topic/usabilidad-accesibilidad

Cada línea tiene dos colores en hexadecimal (#rrggbb o #rgb): el del texto y el del fondo. Implementa la luminancia relativa de WCAG (cada canal se divide entre 255 y se linealiza: v ≤ 0.03928 → v / 12.92; si no, ((v + 0.055) / 1.055)^2.4; después L = 0.2126 R + 0.7152 G + 0.0722 B) y el contraste (L clara + 0.05) / (L oscura + 0.05). Muestra «texto sobre fondo: R:1 · texto normal X · texto grande Y», con R con 2 decimales; para texto normal, AAA si R ≥ 7, AA si R ≥ 4.5; para texto grande (18 pt, o 14 pt en negrita), AAA si R ≥ 4.5, AA si R ≥ 3; si no llega, «no cumple».

Código de partida (java)
import java.util.*; public class Main {    // TODO: luminancia relativa y contraste según WCAG    static double contraste(String a, String b) { return 0; }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            if (p.length < 2) continue;            double r = contraste(p[0], p[1]);            // TODO: muestra el contraste y si cumple AA y AAA para texto normal y grande        }    }}

Ejemplo: Combinaciones habituales

Entrada

#000000 #ffffff
#767676 #ffffff
#ffffff #4f46e5
#999 #fff

Salida esperada

#000000 sobre #ffffff: 21.00:1 · texto normal AAA · texto grande AAA
#767676 sobre #ffffff: 4.54:1 · texto normal AA · texto grande AAA
#ffffff sobre #4f46e5: 6.29:1 · texto normal AA · texto grande AAA
#999 sobre #fff: 2.85:1 · texto normal no cumple · texto grande no cumple

101. El orden de tabulación de un formulario

Difícil · Usabilidad y Accesibilidad · apuntesdam.com/subject/desarrollo-interfaces/topic/usabilidad-accesibilidad

Quien usa el teclado recorre la interfaz con Tab, así que el orden del foco debe ser lógico. Cada línea es un control «id x y tabindex habilitado» (habilitado vale si o no). Calcula el orden como lo hacen los navegadores y la mayoría de frameworks: primero los controles con tabindex positivo, de menor a mayor (a igualdad, por posición); después los de tabindex 0, en orden de lectura (por y y, a igual y, por x). Los deshabilitados y los de tabindex negativo no reciben el foco. Muestra «N. id» para cada control en orden, después «Aviso: K control(es) con tabindex positivo alteran el orden natural» si hay alguno, y por último, en el orden de la entrada, «Fuera del orden: id (deshabilitado)» o «Fuera del orden: id (tabindex -1)».

Código de partida (java)
import java.util.*; public class Main {    record Control(String id, int x, int y, int tabIndex, boolean habilitado) { }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<Control> controles = new ArrayList<>();        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");   // id x y tabindex habilitado(si|no)            if (p.length < 5) continue;            controles.add(new Control(p[0], Integer.parseInt(p[1]), Integer.parseInt(p[2]), Integer.parseInt(p[3]), p[4].equals("si")));        }        // TODO: calcula el orden en que recibe el foco cada control al pulsar Tab    }}

Ejemplo: Formulario bien hecho

Entrada

nombre 20 10 0 si
apellidos 220 10 0 si
email 20 60 0 si
enviar 20 120 0 si
cancelar 120 120 0 si

Salida esperada

1. nombre
2. apellidos
3. email
4. enviar
5. cancelar

Ejemplo: Con tabindex positivos y deshabilitados

Entrada

buscar 500 5 3 si
nombre 20 50 0 si
menu 10 5 1 si
ayuda 400 5 -1 si
email 20 100 0 no
enviar 20 150 0 si
logo 5 5 2 si

Salida esperada

1. menu
2. logo
3. buscar
4. nombre
5. enviar
Aviso: 3 control(es) con tabindex positivo alteran el orden natural
Fuera del orden: ayuda (tabindex -1)
Fuera del orden: email (deshabilitado)

102. ¿Se entiende este texto?

Muy difícil · Usabilidad y Accesibilidad · apuntesdam.com/subject/desarrollo-interfaces/topic/usabilidad-accesibilidad

La claridad del lenguaje también es accesibilidad. El índice de Fernández Huerta mide la legibilidad de un texto en castellano: L = 206.84 − 0.60 · P − 1.02 · F, donde P son las sílabas por cada 100 palabras y F la media de palabras por frase: cuanto más alto, más fácil. Implementa silabas(): cuenta los núcleos vocálicos, de modo que una vocal empieza sílaba si va tras una consonante o si ella y la anterior son fuertes (a, e, o y cualquier vocal con tilde: hiato, como en «po-e-sí-a» o «le-í»); i y u sin tilde se unen a la vocal de al lado (diptongo, como en «ai-re» o «cie-lo»); la y final tras consonante cuenta como vocal; toda palabra tiene al menos 1 sílaba. Las frases se separan por . ! ? y las palabras son las secuencias de letras. Muestra «Palabras: N · Frases: N · Sílabas: N», «Sílabas por palabra: X · Palabras por frase: Y» (2 y 1 decimales) e «Índice Fernández Huerta: L (nivel)», con L con 1 decimal y el nivel: muy fácil (≥ 90), fácil (≥ 80), algo fácil (≥ 70), normal (≥ 60), algo difícil (≥ 50), difícil (≥ 30) o muy difícil.

Código de partida (java)
import java.text.Normalizer;import java.util.*; public class Main {    static final String VOCALES = "aeiouáéíóúü";    static final String FUERTES = "aeoáéíóú";   // las débiles i, u sin tilde forman diptongo     // TODO: cuenta las sílabas de una palabra en castellano    static int silabas(String palabra) { return 0; }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        StringBuilder todo = new StringBuilder();        while (sc.hasNextLine()) todo.append(sc.nextLine()).append(' ');        String texto = todo.toString().trim();        // TODO: calcula el índice de legibilidad de Fernández Huerta    }}

Ejemplo: Mensaje de error claro

Entrada

No se ha podido guardar. Revisa tu conexión y vuelve a intentarlo.

Salida esperada

Palabras: 12 · Frases: 2 · Sílabas: 23
Sílabas por palabra: 1.92 · Palabras por frase: 6.0
Índice Fernández Huerta: 85.7 (fácil)

Ejemplo: Texto administrativo

Entrada

La presente aplicación procederá a la verificación pormenorizada de la documentación aportada por el interesado, conforme a la normativa vigente, con carácter previo a la tramitación definitiva del expediente correspondiente.

Salida esperada

Palabras: 30 · Frases: 1 · Sílabas: 82
Sílabas por palabra: 2.73 · Palabras por frase: 30.0
Índice Fernández Huerta: 12.2 (muy difícil)

Ejemplo: Palabras con diptongos e hiatos

Entrada

El día que leí la poesía de mi abuela, oí el ruido del aire. Hoy sigue aquí.

Salida esperada

Palabras: 17 · Frases: 2 · Sílabas: 29
Sílabas por palabra: 1.71 · Palabras por frase: 8.5
Índice Fernández Huerta: 95.8 (muy fácil)

103. El CHANGELOG a partir de los commits

Medio · Documentación de Aplicaciones · apuntesdam.com/subject/desarrollo-interfaces/topic/documentacion-aplicaciones

Con la convención Conventional Commits, cada mensaje de commit tiene la forma «tipo(ámbito opcional)!opcional: descripción», y de ellos se deducen la siguiente versión semántica y el CHANGELOG. La primera línea es «version X.Y.Z»; las siguientes, los commits. Un «!» tras el tipo indica un cambio incompatible (sube la versión mayor y pone a cero las otras), «feat» sube la menor (y pone a cero el parche) y «fix» el parche; gana el cambio más importante. Genera «## X.Y.Z» con la nueva versión y, separadas por líneas en blanco, las secciones que tengan entradas, en este orden: «### Cambios incompatibles», «### Novedades» y «### Correcciones», con una línea «- ámbito: descripción» (o «- descripción» sin ámbito) por commit. Termina con una línea en blanco y «(N commits sin entrada en el CHANGELOG)», contando los de otros tipos y los que no siguen la convención. Si no hay ningún feat, fix ni «!», muestra solo «Sin cambios publicables: se mantiene la versión X.Y.Z».

Código de partida (java)
import java.util.*;import java.util.regex.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String version = sc.nextLine().split(" ")[1];        // versión actual, p. ej. 1.4.2        List<String> commits = new ArrayList<>();        while (sc.hasNextLine()) { String l = sc.nextLine().trim(); if (!l.isEmpty()) commits.add(l); }        // TODO: calcula la nueva versión semántica y genera el CHANGELOG    }}

Ejemplo: Versión menor

Entrada

version 1.4.2
feat(informes): exportar a Excel
fix: el total no incluía el IVA
docs: actualizar el manual
fix(login): mensaje de error confuso
Arreglos varios

Salida esperada

## 1.5.0

### Novedades
- **informes**: exportar a Excel

### Correcciones
- el total no incluía el IVA
- **login**: mensaje de error confuso

(2 commits sin entrada en el CHANGELOG)

Ejemplo: Cambio incompatible

Entrada

version 2.9.7
feat(api)!: los importes pasan a ser céntimos
feat: modo oscuro
chore: actualizar dependencias

Salida esperada

## 3.0.0

### Cambios incompatibles
- **api**: los importes pasan a ser céntimos

### Novedades
- modo oscuro

(1 commits sin entrada en el CHANGELOG)

Ejemplo: Sin cambios publicables

Entrada

version 0.3.0
docs: README
test: pruebas del carrito

Salida esperada

Sin cambios publicables: se mantiene la versión 0.3.0

104. De Javadoc a una página de documentación

Difícil · Documentación de Aplicaciones · apuntesdam.com/subject/desarrollo-interfaces/topic/documentacion-aplicaciones

Las herramientas como javadoc o Dokka generan la documentación de una API a partir de los comentarios /** … */. La entrada es el código de una clase Java. Para cada método público (una línea que empieza por «public», contiene «(» y no es la declaración de la clase), en orden: si justo antes tiene un comentario Javadoc, genera «### nombre», la firma entre comillas invertidas (sin «public» ni lo que va desde la llave), una línea en blanco y la descripción (las líneas de texto del comentario unidas con un espacio); después, si los hay, una línea en blanco y «Parámetros» con «- nombre: descripción» por cada @param; «Devuelve: …» para @return; y «Lanza» con «- Excepción: descripción» por cada @throws; y una línea en blanco final. Un Javadoc seguido de un método no público se descarta. Termina con «Documentados: N · Sin documentar: método1, método2» (o «ninguno») con los públicos sin Javadoc.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String> lineas = new ArrayList<>();        while (sc.hasNextLine()) lineas.add(sc.nextLine());        // TODO: extrae los comentarios Javadoc de los métodos públicos y genera la documentación en Markdown    }}

Ejemplo: Clase Carrito

Entrada

public class Carrito {
    private final List<Linea> lineas = new ArrayList<>();

    /**
     * Añade un producto al carrito. Si ya estaba,
     * suma la cantidad.
     * @param producto producto que se añade
     * @param cantidad unidades, mayor que cero
     * @throws IllegalArgumentException si la cantidad no es positiva
     */
    public void anadir(Producto producto, int cantidad) {
        // ...
    }

    /** Importe total con IVA.
     * @return el total en euros, redondeado a dos decimales */
    public double total() {
        return 0;
    }

    public void vaciar() {
        lineas.clear();
    }

    /**
     * Cuenta las unidades de todas las líneas.
     */
    private int unidades() {
        return 0;
    }

    public boolean estaVacio() { return lineas.isEmpty(); }
}

Salida esperada

### anadir
`void anadir(Producto producto, int cantidad)`

Añade un producto al carrito. Si ya estaba, suma la cantidad.

**Parámetros**
- `producto`: producto que se añade
- `cantidad`: unidades, mayor que cero

**Lanza**
- `IllegalArgumentException`: si la cantidad no es positiva

### total
`double total()`

Importe total con IVA.

**Devuelve**: el total en euros, redondeado a dos decimales

Documentados: 2 · Sin documentar: vaciar, estaVacio

105. Un revisor automático de manuales

Muy difícil · Documentación de Aplicaciones · apuntesdam.com/subject/desarrollo-interfaces/topic/documentacion-aplicaciones

Igual que se pasa un linter al código, se puede revisar la documentación. La entrada es un manual en Markdown. Ignorando lo que hay dentro de bloques de código (entre líneas que empiezan por ```), detecta en este orden: (1) en una primera pasada por los títulos, que el documento empiece con un título de nivel 1 («Línea N: el documento debe empezar con un título de nivel 1») y que no se salten niveles («Línea N: salto de título de nivel A a B», por ejemplo de ## a ####), guardando el ancla de cada título (minúsculas, sin tildes, solo letras, números, espacios y guiones, y los espacios cambiados por guiones); (2) en una segunda pasada, las imágenes ![texto](ruta) sin texto alternativo («Línea N: la imagen RUTA no tiene texto alternativo»), los enlaces cuyo texto es «aquí», «aqui», «pulsa aquí» o «este enlace» («Línea N: el texto del enlace «X» no dice adónde lleva») y los enlaces internos #ancla que no existen («Línea N: el enlace #X apunta a una sección que no existe»); (3) si queda un bloque de código sin cerrar, «Hay un bloque de código sin cerrar». Termina con «N problemas» o «Manual correcto».

Código de partida (java)
import java.util.*;import java.util.regex.*;import java.text.Normalizer; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String> lineas = new ArrayList<>();        while (sc.hasNextLine()) lineas.add(sc.nextLine());        // TODO: revisa el manual y muestra los problemas encontrados    }}

Ejemplo: Manual con problemas

Entrada

# Manual de usuario de Reservas

Consulta la [instalación](#instalacion) antes de empezar.

## Instalación
Descarga el instalador [aquí](https://ejemplo.es/descargas).

#### Requisitos
![](img/requisitos.png)

## Crear una reserva
1. Pulsa **Nueva**.
2. Revisa la sección de [errores frecuentes](#errores-frecuentes).

```
# esto es un comentario, no un título
![](no-es-imagen.png)
```

## Errores habituales
![Mensaje de error de conexión](img/error.png)

Salida esperada

Línea 8: salto de título de nivel 2 a 4
Línea 6: el texto del enlace «aquí» no dice adónde lleva
Línea 9: la imagen img/requisitos.png no tiene texto alternativo
Línea 13: el enlace #errores-frecuentes apunta a una sección que no existe
4 problemas

Ejemplo: Manual correcto

Entrada

# Guía

## Uso
Ver [la instalación](#instalacion).

## Instalación
![Pantalla de inicio](inicio.png)

Salida esperada

Manual correcto

106. Versiones semánticas y rangos de dependencias

Medio · Distribución de Aplicaciones · apuntesdam.com/subject/desarrollo-interfaces/topic/distribucion-aplicaciones

Los gestores de paquetes (Maven, npm, Gradle) eligen versiones comparándolas según el versionado semántico. La primera línea es una lista de versiones separadas por espacios, que pueden tener una etiqueta de prelanzamiento («1.2.0-beta.2»); el programa las ordena con comparar() y las muestra. Cada línea siguiente es un rango: «^X.Y.Z» acepta versiones desde X.Y.Z hasta antes de la siguiente mayor (si X es 0, hasta la siguiente menor; si X e Y son 0, solo ese parche), «~X.Y.Z» hasta antes de la siguiente menor, y una versión sola solo esa. Para cada rango, el programa muestra la versión estable (sin etiqueta) más alta que lo cumple, o «ninguna». Implementa comparar: primero por los tres números; una versión sin etiqueta es mayor que la misma con etiqueta; entre etiquetas se comparan sus partes separadas por puntos (las numéricas como números y menores que las de texto, y las de texto alfabéticamente) y, si una se acaba antes, es menor.

Código de partida (java)
import java.util.*; public class Main {    // TODO: compara dos versiones semánticas (negativo si a < b, 0 si iguales, positivo si a > b)    static int comparar(String a, String b) { return 0; }     // TODO: ¿cumple la versión el rango? (^X.Y.Z, ~X.Y.Z o una versión exacta)    static boolean cumple(String version, String rango) { return false; }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String> versiones = new ArrayList<>(Arrays.asList(sc.nextLine().trim().split(" ")));        versiones.sort(Main::comparar);        System.out.println("Ordenadas: " + String.join(" < ", versiones));        while (sc.hasNextLine()) {            String rango = sc.nextLine().trim();            if (rango.isEmpty()) continue;            String mejor = null;            for (String v : versiones) if (cumple(v, rango) && !v.contains("-")) mejor = v;   // la más alta estable            System.out.println(rango + " → " + (mejor == null ? "ninguna" : mejor));        }    }}

Ejemplo: Dependencia típica

Entrada

1.2.0 1.10.1 1.2.10 2.0.0 1.9.0-beta 2.0.0-rc.1 1.2.3
^1.2.0
~1.2.0
^2.0.0
1.9.0

Salida esperada

Ordenadas: 1.2.0 < 1.2.3 < 1.2.10 < 1.9.0-beta < 1.10.1 < 2.0.0-rc.1 < 2.0.0
^1.2.0 → 1.10.1
~1.2.0 → 1.2.10
^2.0.0 → 2.0.0
1.9.0 → ninguna

Ejemplo: Versiones 0.x

Entrada

0.3.1 0.3.9 0.4.0 0.0.3 0.0.4 1.0.0-alpha
^0.3.1
^0.0.3
~0.3.0

Salida esperada

Ordenadas: 0.0.3 < 0.0.4 < 0.3.1 < 0.3.9 < 0.4.0 < 1.0.0-alpha
^0.3.1 → 0.3.9
^0.0.3 → 0.0.3
~0.3.0 → 0.3.9

107. ¿Puedo distribuir mi aplicación con estas librerías?

Difícil · Distribución de Aplicaciones · apuntesdam.com/subject/desarrollo-interfaces/topic/distribucion-aplicaciones

Antes de distribuir hay que revisar las licencias de las dependencias. La primera línea es «proyecto LICENCIA» (MIT, Apache-2.0, GPL-3.0, GPL-2.0 o Propietaria) y cada línea siguiente una dependencia «nombre licencia enlace», con enlace estatico (incluida en el mismo JAR) o dinamico (un JAR aparte que se carga). Reglas: MIT, BSD-3-Clause, Apache-2.0 y EPL-2.0 son permisivas → «OK (permisiva: incluye su aviso de copyright)», salvo Apache-2.0 en un proyecto GPL-2.0 → «PROBLEMA (Apache-2.0 no es compatible con GPL-2.0)»; LGPL-3.0 dinámica → «OK (LGPL enlazada dinámicamente: el usuario debe poder sustituirla)» (cuenta como aviso), estática en proyecto GPL → «OK (compatible con GPL)», estática en otro → «PROBLEMA (LGPL enlazada estáticamente obliga a facilitar el código objeto para reenlazar)»; GPL-3.0 en proyecto GPL-3.0 → «OK (misma licencia)», GPL-2.0 en proyecto GPL-3.0 → «PROBLEMA (GPL-2.0 sola no es compatible con GPL-3.0)», cualquier GPL en otro proyecto → «PROBLEMA (copyleft fuerte: obligaría a publicar el proyecto como LICENCIA)»; AGPL-3.0 → «PROBLEMA (AGPL: también obliga a compartir el código si se ofrece como servicio en red)»; cualquier otra → «REVISAR (licencia desconocida: hay que leerla)» (aviso). Muestra «nombre [licencia, enlace]: veredicto» y «Resumen: P problemas, A avisos → se puede distribuir» o «→ no distribuir sin resolverlos».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String[] proyecto = sc.nextLine().trim().split(" ");     // proyecto LICENCIA (MIT, Apache-2.0, GPL-3.0, Propietaria)        String licProyecto = proyecto[1];        List<String[]> deps = new ArrayList<>();                 // nombre licencia enlace(estatico|dinamico)        while (sc.hasNextLine()) { String l = sc.nextLine().trim(); if (!l.isEmpty()) deps.add(l.split(" ")); }        // TODO: revisa si cada dependencia es compatible con la licencia del proyecto    }}

Ejemplo: App propietaria

Entrada

proyecto Propietaria
jackson Apache-2.0 estatico
openpdf LGPL-3.0 dinamico
mysql-connector GPL-2.0 dinamico
logback EPL-2.0 estatico
hibernate LGPL-3.0 estatico

Salida esperada

jackson [Apache-2.0, estatico]: OK (permisiva: incluye su aviso de copyright)
openpdf [LGPL-3.0, dinamico]: OK (LGPL enlazada dinámicamente: el usuario debe poder sustituirla)
mysql-connector [GPL-2.0, dinamico]: PROBLEMA (copyleft fuerte: obligaría a publicar el proyecto como GPL-2.0)
logback [EPL-2.0, estatico]: OK (permisiva: incluye su aviso de copyright)
hibernate [LGPL-3.0, estatico]: PROBLEMA (LGPL enlazada estáticamente obliga a facilitar el código objeto para reenlazar)
Resumen: 2 problemas, 1 avisos → no distribuir sin resolverlos

Ejemplo: App GPL

Entrada

proyecto GPL-3.0
libreria-gpl GPL-3.0 estatico
vieja GPL-2.0 estatico
jsoup MIT estatico
rara WTFPL estatico

Salida esperada

libreria-gpl [GPL-3.0, estatico]: OK (misma licencia)
vieja [GPL-2.0, estatico]: PROBLEMA (GPL-2.0 sola no es compatible con GPL-3.0)
jsoup [MIT, estatico]: OK (permisiva: incluye su aviso de copyright)
rara [WTFPL, estatico]: REVISAR (licencia desconocida: hay que leerla)
Resumen: 1 problemas, 1 avisos → no distribuir sin resolverlos

108. Un actualizador incremental con despliegue escalonado

Muy difícil · Distribución de Aplicaciones · apuntesdam.com/subject/desarrollo-interfaces/topic/distribucion-aplicaciones

Las aplicaciones de escritorio modernas se actualizan solas descargando solo lo que ha cambiado y publicando la versión nueva poco a poco. La entrada tiene una configuración «clave valor» (version_instalada, version_nueva, minima, porcentaje y usuario) y dos manifiestos, tras «[local]» y «[remoto]», con líneas «ruta tamaño_bytes hash». Si la versión instalada ya es la nueva o superior: «Ya tienes la última versión (X)». Si es menor que la mínima, la actualización es obligatoria. Si no, el usuario la recibe solo si su grupo, Math.floorMod(usuario.hashCode(), 100), es menor que el porcentaje publicado; si no, «Actualización N aún no disponible para este usuario (grupo G, se publica al P %)». Cuando hay actualización: «Actualizar de A a N (obligatoria: la versión mínima es M)» o «… (grupo G)», después « NUEVO ruta», « CAMBIA ruta» (mismo nombre, distinto hash) en orden alfabético, « BORRAR ruta» para lo que ya no existe, y «Descarga: F ficheros, X KB de T KB (ahorro del S %)» (KB = 1024 bytes, 1 decimal; ahorro sin decimales).

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, String> conf = new HashMap<>();        Map<String, String[]> local = new TreeMap<>(), remoto = new TreeMap<>();   // ruta → {tamaño, hash}        Map<String, String[]> actual = null;        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.isEmpty()) continue;            if (l.equals("[local]")) { actual = local; continue; }            if (l.equals("[remoto]")) { actual = remoto; continue; }            String[] p = l.split(" ");            if (actual == null) conf.put(p[0], p[1]);              // version_instalada, version_nueva, minima, porcentaje, usuario            else actual.put(p[0], new String[]{p[1], p[2]});        }        // TODO: decide si este usuario recibe la actualización y qué ficheros descargar o borrar    }}

Ejemplo: Actualización parcial

Entrada

version_instalada 2.3.1
version_nueva 2.4.0
minima 2.0.0
porcentaje 100
usuario ana@hotel.es
[local]
app.jar 812000 a1f3
lib/javafx-controls.jar 2400000 9c0d
lib/openpdf.jar 1300000 77aa
informes/factura.jasper 45000 c3c3
ayuda/manual.html 120000 e5e5
[remoto]
app.jar 830000 b2b2
lib/javafx-controls.jar 2400000 9c0d
lib/openpdf.jar 1350000 88bb
informes/factura.jasper 45000 c3c3
informes/ocupacion.jasper 52000 d4d4

Salida esperada

Actualizar de 2.3.1 a 2.4.0 (grupo 48)
  CAMBIA app.jar
  NUEVO informes/ocupacion.jasper
  CAMBIA lib/openpdf.jar
  BORRAR ayuda/manual.html
Descarga: 3 ficheros, 2179.7 KB de 4567.4 KB (ahorro del 52 %)

Ejemplo: Aún no publicada para este usuario

Entrada

version_instalada 2.3.1
version_nueva 2.4.0
minima 2.0.0
porcentaje 10
usuario ana@hotel.es
[local]
app.jar 812000 a1f3
lib/javafx-controls.jar 2400000 9c0d
lib/openpdf.jar 1300000 77aa
informes/factura.jasper 45000 c3c3
ayuda/manual.html 120000 e5e5
[remoto]
app.jar 830000 b2b2
lib/javafx-controls.jar 2400000 9c0d
lib/openpdf.jar 1350000 88bb
informes/factura.jasper 45000 c3c3
informes/ocupacion.jasper 52000 d4d4

Salida esperada

Actualización 2.4.0 aún no disponible para este usuario (grupo 48, se publica al 10 %)

Ejemplo: Obligatoria y al día

Entrada

version_instalada 1.9.5
version_nueva 2.4.0
minima 2.0.0
porcentaje 0
usuario luis
[local]
app.jar 812000 a1f3
lib/javafx-controls.jar 2400000 9c0d
lib/openpdf.jar 1300000 77aa
informes/factura.jasper 45000 c3c3
ayuda/manual.html 120000 e5e5
[remoto]
app.jar 812000 a1f3
lib/javafx-controls.jar 2400000 9c0d
lib/openpdf.jar 1350000 88bb
informes/factura.jasper 45000 c3c3
informes/ocupacion.jasper 52000 d4d4

Salida esperada

Actualizar de 1.9.5 a 2.4.0 (obligatoria: la versión mínima es 2.0.0)
  NUEVO informes/ocupacion.jasper
  CAMBIA lib/openpdf.jar
  BORRAR ayuda/manual.html
Descarga: 2 ficheros, 1369.1 KB de 4549.8 KB (ahorro del 70 %)

109. Casos de prueba por valores límite

Medio · Pruebas de Interfaces · apuntesdam.com/subject/desarrollo-interfaces/topic/pruebas-interfaces

Los errores se esconden en los bordes. Cada línea describe un campo de un formulario: «campo tipo min max», con tipo entero (el valor debe estar entre min y max) o texto (la longitud debe estar entre min y max). Genera sus casos de prueba: «== campo (tipo, min..max)» y, numerados « campo-N: valor → resultado», primero los valores límite min − 1, min, min + 1, max − 1, max y max + 1 (sin repetir y, en texto, sin longitudes negativas), cada uno «válido» o «inválido». En los enteros, el valor límite se muestra tal cual, y después se añaden un valor intermedio ((min + max) / 2 → válido, si no estaba ya) y «abc» → «inválido (no es un número)». En los textos, una longitud 0 se muestra como "", hasta 12 como la cadena de «a» entre comillas seguida de «(N caracteres)» («(1 carácter)» en singular) y más larga como «texto de N caracteres»; después se añade «" " (solo espacios) → inválido (vacío tras recortar)». En ambos, el último caso es «(vacío) → inválido (obligatorio)».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");     // campo tipo min max            if (p.length < 4) continue;            String campo = p[0], tipo = p[1];            int min = Integer.parseInt(p[2]), max = Integer.parseInt(p[3]);            // TODO: genera los casos de prueba por valores límite y clases de equivalencia        }    }}

Ejemplo: Edad y nombre

Entrada

edad entero 18 65
nombre texto 2 10

Salida esperada

== edad (entero, 18..65)
  edad-1: 17 → inválido
  edad-2: 18 → válido
  edad-3: 19 → válido
  edad-4: 64 → válido
  edad-5: 65 → válido
  edad-6: 66 → inválido
  edad-7: 41 → válido
  edad-8: abc → inválido (no es un número)
  edad-9: (vacío) → inválido (obligatorio)
== nombre (texto, 2..10)
  nombre-1: "a" (1 carácter) → inválido
  nombre-2: "aa" (2 caracteres) → válido
  nombre-3: "aaa" (3 caracteres) → válido
  nombre-4: "aaaaaaaaa" (9 caracteres) → válido
  nombre-5: "aaaaaaaaaa" (10 caracteres) → válido
  nombre-6: "aaaaaaaaaaa" (11 caracteres) → inválido
  nombre-7: "   " (solo espacios) → inválido (vacío tras recortar)
  nombre-8: (vacío) → inválido (obligatorio)

Ejemplo: Rango estrecho

Entrada

planta entero 0 2
cp texto 5 5

Salida esperada

== planta (entero, 0..2)
  planta-1: -1 → inválido
  planta-2: 0 → válido
  planta-3: 1 → válido
  planta-4: 2 → válido
  planta-5: 3 → inválido
  planta-6: abc → inválido (no es un número)
  planta-7: (vacío) → inválido (obligatorio)
== cp (texto, 5..5)
  cp-1: "aaaa" (4 caracteres) → inválido
  cp-2: "aaaaa" (5 caracteres) → válido
  cp-3: "aaaaaa" (6 caracteres) → inválido
  cp-4: "   " (solo espacios) → inválido (vacío tras recortar)
  cp-5: (vacío) → inválido (obligatorio)

110. Un mini ejecutor de pruebas unitarias

Difícil · Pruebas de Interfaces · apuntesdam.com/subject/desarrollo-interfaces/topic/pruebas-interfaces

Implementa precioFinal(importe, código, socio) según esta especificación y un pequeño ejecutor de pruebas al estilo de JUnit. Especificación: un importe negativo lanza IllegalArgumentException; el código VERANO10 descuenta un 10 % y VIP25 un 25 % solo si el importe es de al menos 100 €; los socios tienen un 5 % adicional; el descuento total nunca pasa del 30 %; se suman 4,95 € de envío si el importe con descuento es menor de 50 € (salvo que el importe sea 0); y el resultado se redondea a céntimos. Cada línea de la entrada es un caso «nombre importe código(o -) socio(si|no) esperado», donde esperado es un número o la palabra excepcion. El ejecutor muestra «PASA nombre» (dos espacios), «PASA nombre (lanza IllegalArgumentException)», «FALLA nombre: esperado X, obtenido Y», «FALLA nombre: se esperaba una excepción y devolvió Y» o «ERROR nombre: Excepcion: mensaje» si salta una excepción no esperada, y al final «Pruebas: N · Pasan: P · Fallan: F · Errores: E». Compara con una tolerancia de 0,005.

Código de partida (java)
import java.util.*; public class Main {    // TODO: implementa precioFinal según la especificación del enunciado    static double precioFinal(double importe, String codigo, boolean socio) {        return importe;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // Cada línea: nombre_prueba importe codigo(o -) socio(si|no) esperado        // TODO: ejecuta cada caso, compara con el esperado (tolerancia 0.005) y muestra el informe    }}

Ejemplo: Especificación correcta

Entrada

sin_descuento 40 - no 44.95
envio_gratis_justo 50 - no 50.00
verano 100 VERANO10 no 90.00
vip_minimo 100 VIP25 no 75.00
vip_insuficiente 99.99 VIP25 no 99.99
socio 40 - si 42.95
tope_30 200 VIP25 si 140.00
importe_negativo -5 - no excepcion
gratis 0 VERANO10 no 0.00

Salida esperada

PASA  sin_descuento
PASA  envio_gratis_justo
PASA  verano
PASA  vip_minimo
PASA  vip_insuficiente
PASA  socio
PASA  tope_30
PASA  importe_negativo (lanza IllegalArgumentException)
PASA  gratis
Pruebas: 9 · Pasan: 9 · Fallan: 0 · Errores: 0

Ejemplo: Casos mal escritos

Entrada

esperado_mal 40 - no 40.00
espera_excepcion 10 - no excepcion
negativo_sin_esperarlo -1 - no 0

Salida esperada

FALLA esperado_mal: esperado 40.00, obtenido 44.95
FALLA espera_excepcion: se esperaba una excepción y devolvió 14.95
ERROR negativo_sin_esperarlo: IllegalArgumentException: importe negativo
Pruebas: 3 · Pasan: 0 · Fallan: 2 · Errores: 1

111. Pruebas de interfaz guionizadas

Muy difícil · Pruebas de Interfaces · apuntesdam.com/subject/desarrollo-interfaces/topic/pruebas-interfaces

Las herramientas de pruebas de extremo a extremo (Playwright, Selenium, TestFX) siguen un guion: escribir, pulsar y esperar un resultado. Completa el modelo de PantallaAcceso y el ejecutor. Reglas de la pantalla: el botón Entrar se habilita cuando el usuario no está en blanco, la clave tiene al menos 4 caracteres y hay menos de 3 intentos fallidos; pulsar con el botón deshabilitado no hace nada; con datos correctos (usuario sin distinguir mayúsculas y sin espacios alrededor) se pasa a la pantalla principal con el mensaje «Bienvenida, Usuario» (tal como se escribió, sin espacios); si no, se borra la clave, se deshabilita el botón, suma un intento y el mensaje es «Usuario o contraseña incorrectos (N intentos más)» o, al tercer fallo, «Cuenta bloqueada: demasiados intentos». El guion tiene líneas «escribe campo texto», «pulsa entrar» y «espera qué valor» (qué: mensaje, boton —habilitado/deshabilitado—, clave o pantalla —acceso/principal—); las líneas vacías y las que empiezan por # se ignoran. Por cada «espera» muestra «✓ paso N: qué = «valor»» o «✗ paso N: qué esperaba «X» pero es «Y»»; para una orden desconocida, «? paso N: orden desconocida «orden»». Termina con «PRUEBA SUPERADA» o «PRUEBA FALLIDA (F comprobaciones)».

Código de partida (java)
import java.util.*; public class Main {    // Modelo de una pantalla de acceso: campos usuario y clave, botón entrar y un mensaje    static class PantallaAcceso {        String usuario = "", clave = "", mensaje = "";        boolean entrarHabilitado = false, dentro = false;        int intentos = 0;        static final Map<String, String> CUENTAS = Map.of("ana", "Segura123", "luis", "Clave2026");         // TODO: escribir en un campo, pulsar el botón y actualizar el estado según las reglas        void escribir(String campo, String texto) { }        void pulsarEntrar() { }    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // TODO: ejecuta el guion de prueba paso a paso y comprueba cada «espera»    }}

Ejemplo: Acceso correcto

Entrada

# camino feliz
espera boton deshabilitado
escribe usuario Ana
escribe clave Segura123
espera boton habilitado
pulsa entrar
espera pantalla principal
espera mensaje Bienvenida, Ana

Salida esperada

✓ paso 1: boton = «deshabilitado»
✓ paso 4: boton = «habilitado»
✓ paso 6: pantalla = «principal»
✓ paso 7: mensaje = «Bienvenida, Ana»
PRUEBA SUPERADA

Ejemplo: Tres intentos fallidos

Entrada

escribe usuario luis
escribe clave mala
pulsa entrar
espera mensaje Usuario o contraseña incorrectos (2 intentos más)
espera clave 
escribe clave otra1
pulsa entrar
escribe clave otra2
pulsa entrar
espera mensaje Cuenta bloqueada: demasiados intentos
escribe clave Clave2026
espera boton deshabilitado
pulsa entrar
espera pantalla acceso

Salida esperada

✓ paso 4: mensaje = «Usuario o contraseña incorrectos (2 intentos más)»
✓ paso 5: clave = «»
✓ paso 10: mensaje = «Cuenta bloqueada: demasiados intentos»
✓ paso 12: boton = «deshabilitado»
✓ paso 14: pantalla = «acceso»
PRUEBA SUPERADA

112. Validar un NIF en un formulario

Fácil · Ejercicios de Desarrollo de Interfaces · apuntesdam.com/subject/desarrollo-interfaces/topic/ejercicios-desarrollo-interfaces

Antes de guardar un formulario hay que validar el NIF. Implementa nifValido: 8 dígitos y una letra (mayúscula o minúscula) cuya letra de control sea la correcta (TRWAGMYFPDXBNJZSQVHLCKE[número % 23]). Se ignoran espacios y guiones. El programa lee un NIF por línea y muestra «válido» o «no válido».

Código de partida (java)
import java.util.*; public class Main {    static final String LETRAS = "TRWAGMYFPDXBNJZSQVHLCKE";     static boolean nifValido(String nif) {        // TODO        return false;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String l = sc.nextLine();            if (!l.isBlank()) System.out.println(nifValido(l) ? "válido" : "no válido");        }    }}

Ejemplo: NIF correctos e incorrectos

Entrada

12345678Z
12345678A
00000000T

Salida esperada

válido
no válido
válido

Ejemplo: Formato

Entrada

1234567Z
12 345 678-z
ABCDEFGHZ

Salida esperada

no válido
válido
no válido

113. Informe agrupado con subtotales

Medio · Ejercicios de Desarrollo de Interfaces · apuntesdam.com/subject/desarrollo-interfaces/topic/ejercicios-desarrollo-interfaces

Una pantalla de informes recibe líneas «categoría;importe». Muestra las categorías en orden alfabético con su subtotal y al final el total, con dos decimales y coma decimal: Audio: 104,95 Informática: 179,00 TOTAL: 283,95

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // TODO: lee las líneas categoría;importe y muestra el informe    }}

Ejemplo: Tres líneas

Entrada

Audio;59.95
Informática;179
Audio;45

Salida esperada

Audio: 104,95
Informática: 179,00
TOTAL: 283,95

114. Paginación de una tabla

Medio · Ejercicios de Desarrollo de Interfaces · apuntesdam.com/subject/desarrollo-interfaces/topic/ejercicios-desarrollo-interfaces

Una tabla muestra los resultados en páginas. La primera línea de la entrada tiene el tamaño de página y la página pedida (empezando en 1); después, un elemento por línea. Muestra los elementos de esa página y al final «Página X de Y». Si la página no existe, muestra solo «Página no válida». Si no hay elementos, «Sin resultados».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int tam = sc.nextInt(), pagina = sc.nextInt();        sc.nextLine();        List<String> elementos = new ArrayList<>();        while (sc.hasNextLine()) {            String l = sc.nextLine();            if (!l.isBlank()) elementos.add(l);        }        // TODO    }}

Ejemplo: Segunda página

Entrada

3 2
Ana
Luis
Marta
Pablo
Rosa

Salida esperada

Pablo
Rosa
Página 2 de 2

Ejemplo: Página fuera de rango

Entrada

2 4
a
b
c

Salida esperada

Página no válida

115. Comparar versiones semánticas

Medio · Ejercicios de Desarrollo de Interfaces · apuntesdam.com/subject/desarrollo-interfaces/topic/ejercicios-desarrollo-interfaces

El actualizador de la aplicación compara la versión instalada con la publicada. Cada línea tiene dos versiones MAYOR.MENOR.PARCHE separadas por un espacio. Muestra «<», «>» o «=» según la primera sea menor, mayor o igual que la segunda. Ojo: 2.10.0 es mayor que 2.9.5 (se comparan números, no textos).

Código de partida (java)
import java.util.*; public class Main {     static int comparar(String a, String b) {        // TODO: negativo si a < b, 0 si son iguales, positivo si a > b        return a.compareTo(b);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNext()) {            int c = comparar(sc.next(), sc.next());            System.out.println(c < 0 ? "<" : c > 0 ? ">" : "=");        }    }}

Ejemplo: Casos básicos

Entrada

1.0.0 1.0.1
2.1.0 2.0.9
3.4.5 3.4.5

Salida esperada

<
>
=

Ejemplo: Números de dos cifras

Entrada

2.10.0 2.9.5
1.2.10 1.2.9

Salida esperada

>
>

Programación Multimedia y Dispositivos Móviles

116. Densidades de pantalla y recursos de Android

Medio · Tecnologías y Plataformas Móviles · apuntesdam.com/subject/prog-multimedia/topic/tecnologias-plataformas-movil

Android agrupa las pantallas por densidad para escoger imágenes y por anchura mínima (smallest width) para escoger diseños. Cada línea es un dispositivo «nombre anchoPx altoPx pulgadas». Calcula su densidad real (diagonal en píxeles / pulgadas), el grupo de densidad más cercano (ldpi 120, mdpi 160, hdpi 240, xhdpi 320, xxhdpi 480, xxxhdpi 640; ante empate, el menor), el factor de escala (dpi del grupo / 160), su tamaño en dp (lado corto y lado largo divididos por el factor, redondeados), si es «móvil», «tableta» (lado corto ≥ 600 dp) o «tableta grande» (≥ 720 dp) con su carpeta de diseños (layout, layout-sw600dp o layout-sw720dp) y cuántos píxeles mide un icono de 48 dp. Formato: «nombre: D dpi → grupo (×F), A × L dp, tipo → carpeta, icono 48 dp = N px», con el factor sin «.0» si es entero (×1.5, ×2).

Código de partida (java)
import java.util.*; public class Main {    static final String[] NOMBRES = {"ldpi", "mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"};    static final int[] DPI = {120, 160, 240, 320, 480, 640};     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");      // nombre anchoPx altoPx pulgadas            if (p.length < 4) continue;            // TODO: calcula densidad, tamaño en dp y carpeta de recursos        }    }}

Ejemplo: Móviles y tableta

Entrada

Pixel7 1080 2400 6.3
GalaxyTabS9 1600 2560 11
MovilBarato 720 1600 6.5

Salida esperada

Pixel7: 418 dpi → xxhdpi (×3), 360 × 800 dp, móvil → layout, icono 48 dp = 144 px
GalaxyTabS9: 274 dpi → hdpi (×1.5), 1067 × 1707 dp, tableta grande → layout-sw720dp, icono 48 dp = 72 px
MovilBarato: 270 dpi → hdpi (×1.5), 480 × 1067 dp, móvil → layout, icono 48 dp = 72 px

117. El ciclo de vida de una Activity

Difícil · Tecnologías y Plataformas Móviles · apuntesdam.com/subject/prog-multimedia/topic/tecnologias-plataformas-movil

Entender cuándo llama Android a cada método del ciclo de vida evita perder datos. Cada línea es un evento; simula las llamadas y el estado de la pantalla con un campo de texto: «abrir» (onCreate → onStart → onResume), «escribir TEXTO» (el usuario escribe, sin llamadas), «dialogo» (un diálogo encima: onPause si estaba reanudada) y «cerrar_dialogo» (onResume si estaba pausada), «home» (onPause si estaba reanudada, onStop, onSaveInstanceState: se guarda el texto), «volver» (si estaba detenida: onRestart → onStart → onResume), «girar» (se destruye y se recrea: onPause → onStop → onSaveInstanceState → onDestroy → onCreate → onStart → onRestoreInstanceState → onResume, y el texto se recupera de lo guardado), «matar_proceso» (si estaba detenida, el sistema elimina el proceso: «(proceso eliminado sin más llamadas)», estado NINGUNO y el texto en memoria se pierde), «reabrir» (onCreate → onStart → onRestoreInstanceState → onResume, recuperando lo guardado) y «atras» (onPause si estaba reanudada, onStop, onDestroy: estado DESTRUIDA y no se guarda nada). Muestra «evento: llamadas separadas por → | estado ESTADO | texto «texto»» (para «escribir», la llamada es «(el usuario escribe «texto»)»; para un evento desconocido, «(evento desconocido)»).

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // Estados: NINGUNO, CREADA, INICIADA, REANUDADA, PAUSADA, DETENIDA, DESTRUIDA        // TODO: simula el ciclo de vida de una Activity ante cada evento    }}

Ejemplo: Uso normal

Entrada

abrir
escribir Hola
dialogo
cerrar_dialogo
home
volver
atras

Salida esperada

abrir: onCreate → onStart → onResume | estado REANUDADA | texto «»
escribir: (el usuario escribe «Hola») | estado REANUDADA | texto «Hola»
dialogo: onPause | estado PAUSADA | texto «Hola»
cerrar_dialogo: onResume | estado REANUDADA | texto «Hola»
home: onPause → onStop → onSaveInstanceState | estado DETENIDA | texto «Hola»
volver: onRestart → onStart → onResume | estado REANUDADA | texto «Hola»
atras: onPause → onStop → onDestroy | estado DESTRUIDA | texto «»

Ejemplo: Giro y proceso eliminado

Entrada

abrir
escribir Pedido 42
girar
home
matar_proceso
reabrir

Salida esperada

abrir: onCreate → onStart → onResume | estado REANUDADA | texto «»
escribir: (el usuario escribe «Pedido 42») | estado REANUDADA | texto «Pedido 42»
girar: onPause → onStop → onSaveInstanceState → onDestroy → onCreate → onStart → onRestoreInstanceState → onResume | estado REANUDADA | texto «Pedido 42»
home: onPause → onStop → onSaveInstanceState | estado DETENIDA | texto «Pedido 42»
matar_proceso: (proceso eliminado sin más llamadas) | estado NINGUNO | texto «»
reabrir: onCreate → onStart → onRestoreInstanceState → onResume | estado REANUDADA | texto «Pedido 42»

118. Elegir el minSdk de una app

Muy difícil · Tecnologías y Plataformas Móviles · apuntesdam.com/subject/prog-multimedia/topic/tecnologias-plataformas-movil

Subir el minSdk permite usar APIs más nuevas sin comprobaciones, pero deja fuera a quienes tienen móviles antiguos. La entrada tiene líneas «cuota NIVEL PORCENTAJE» (dispositivos activos con cada nivel de API; puede que no sumen 100, así que hay que normalizar), «api NOMBRE NIVEL» (APIs que usa la app y el nivel en que aparecieron), «minSdk N» y «objetivo P». Muestra «minSdk N: X % de los dispositivos» (los de nivel ≥ N, 1 decimal); el minSdk más alto que aún cubre al menos el objetivo: «Recomendado para el P %: minSdk M (X %)» (P sin decimales) o «Ningún minSdk alcanza el P %»; y para cada API, «NOMBRE: disponible siempre (API L)» si L ≤ minSdk, o «NOMBRE: comprobar Build.VERSION.SDK_INT >= L (no disponible en el X % de los dispositivos)» con el porcentaje de dispositivos entre minSdk y L − 1.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        TreeMap<Integer, Double> cuota = new TreeMap<>();            // nivel de API → % de dispositivos activos        List<String[]> apis = new ArrayList<>();                    // nombre nivelMinimo        int minSdk = 0;        double objetivo = 0;        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            switch (p[0]) {                case "cuota" -> cuota.put(Integer.parseInt(p[1]), Double.parseDouble(p[2]));                case "api" -> apis.add(new String[]{p[1], p[2]});                case "minSdk" -> minSdk = Integer.parseInt(p[1]);                case "objetivo" -> objetivo = Double.parseDouble(p[1]);                default -> { }            }        }        // TODO: cobertura del minSdk, minSdk recomendado y comprobaciones de versión necesarias    }}

Ejemplo: App de pagos

Entrada

cuota 24 2.0
cuota 26 4.0
cuota 28 6.5
cuota 29 9.0
cuota 30 13.5
cuota 31 15.0
cuota 33 20.0
cuota 34 18.0
cuota 35 12.0
api Notificaciones 26
api PhotoPicker 33
api PermisoNotificaciones 33
minSdk 26
objetivo 95

Salida esperada

minSdk 26: 98.0 % de los dispositivos
Recomendado para el 95 %: minSdk 26 (98.0 %)
Notificaciones: disponible siempre (API 26)
PhotoPicker: comprobar Build.VERSION.SDK_INT >= 33 (no disponible en el 48.0 % de los dispositivos)
PermisoNotificaciones: comprobar Build.VERSION.SDK_INT >= 33 (no disponible en el 48.0 % de los dispositivos)

119. Las diferencias de una lista, como DiffUtil

Medio · Desarrollo de Aplicaciones Móviles · apuntesdam.com/subject/prog-multimedia/topic/desarrollo-aplicaciones-movil

Para no redibujar una lista entera (RecyclerView o LazyColumn) cada vez que cambian los datos, Android calcula las diferencias entre la lista vieja y la nueva (DiffUtil, ListAdapter). La primera línea es la lista antigua y la segunda la nueva, como elementos «id:contenido» separados por espacios (cualquiera puede estar vacía). Muestra, primero, «notifyItemRemoved: id» por cada elemento que ya no está (en el orden de la lista vieja); después, recorriendo la lista nueva, «notifyItemInserted(posición): id» para los nuevos y «notifyItemChanged(posición): id viejo → nuevo» para los que cambian de contenido. Termina con «Sin cambios: A · Insertados: B · Borrados: C · Modificados: D» (añadiendo « · Hay elementos movidos» si los elementos que siguen en ambas listas cambian de orden relativo) y con «No hace falta redibujar» o «Se redibujan N de M filas (notifyDataSetChanged redibujaría M)», donde N son insertados más modificados.

Código de partida (java)
import java.util.*; public class Main {    record Elemento(String id, String contenido) { }     static List<Elemento> leer(String linea) {        List<Elemento> l = new ArrayList<>();        if (linea.isBlank()) return l;        for (String par : linea.trim().split(" ")) {            String[] p = par.split(":");            l.add(new Elemento(p[0], p[1]));        }        return l;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<Elemento> antes = leer(sc.nextLine()), despues = leer(sc.hasNextLine() ? sc.nextLine() : "");        // TODO: calcula los cambios entre las dos listas como DiffUtil    }}

Ejemplo: Lista de tareas

Entrada

1:Pan 2:Leche 3:Huevos 4:Fruta
1:Pan 3:Huevos(12) 5:Café 4:Fruta

Salida esperada

notifyItemRemoved: 2
notifyItemChanged(1): 3 Huevos → Huevos(12)
notifyItemInserted(2): 5
Sin cambios: 2 · Insertados: 1 · Borrados: 1 · Modificados: 1
Se redibujan 2 de 4 filas (notifyDataSetChanged redibujaría 4)

Ejemplo: Reordenada

Entrada

a:x b:y c:z
c:z a:x b:y

Salida esperada

Sin cambios: 3 · Insertados: 0 · Borrados: 0 · Modificados: 0 · Hay elementos movidos
Se redibujan 0 de 3 filas (notifyDataSetChanged redibujaría 3)

Ejemplo: Sin cambios

Entrada

a:1 b:2
a:1 b:2

Salida esperada

Sin cambios: 2 · Insertados: 0 · Borrados: 0 · Modificados: 0
No hace falta redibujar

120. La pila de navegación de una app

Difícil · Desarrollo de Aplicaciones Móviles · apuntesdam.com/subject/prog-multimedia/topic/desarrollo-aplicaciones-movil

El componente Navigation de Android mantiene una pila de pantallas (back stack). La primera línea es «inicio DESTINO»; después vienen órdenes «navigate DESTINO [popUpTo=X] [inclusive] [singleTop]» y «back». navigate apila el destino; con popUpTo=X, antes se desapilan las pantallas hasta dejar X en la cima (con inclusive, también X), lo que se usa por ejemplo tras iniciar sesión para que «atrás» no vuelva al acceso; con singleTop, si el destino ya está en la cima no se apila otra vez («orden: ya estás en DESTINO (singleTop)»). Si X no está en la pila, «ERROR: X no está en la pila» y no se hace nada. back desapila; si solo queda una pantalla, «back: se cierra la app desde DESTINO» y se termina. Tras cada orden que cambia la pila (y tras un back que no cierra), muestra la orden y «→ [base > … > cima]».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Deque<String> pila = new ArrayDeque<>();      // la cima es la pantalla visible        pila.push(sc.nextLine().trim().split(" ")[1]);  // inicio DESTINO        // TODO: procesa navigate y back como el componente Navigation    }}

Ejemplo: Inicio de sesión

Entrada

inicio splash
navigate login popUpTo=splash inclusive
navigate inicio popUpTo=login inclusive
navigate detalle
navigate detalle singleTop
navigate carrito
back
back
back

Salida esperada

navigate login popUpTo=splash inclusive → [login]
navigate inicio popUpTo=login inclusive → [inicio]
navigate detalle → [inicio > detalle]
navigate detalle singleTop: ya estás en detalle (singleTop)
navigate carrito → [inicio > detalle > carrito]
back → [inicio > detalle]
back → [inicio]
back: se cierra la app desde inicio

121. Sincronización sin conexión con conflictos

Muy difícil · Desarrollo de Aplicaciones Móviles · apuntesdam.com/subject/prog-multimedia/topic/desarrollo-aplicaciones-movil

Una app «offline first» guarda los cambios hechos sin conexión y los envía al recuperarla. La entrada tiene tres secciones: [servidor] con el estado actual «id valor tiempo», [base] con la versión que tenía el móvil cuando se quedó sin conexión, y [cambios] con las operaciones pendientes «crear id valor tiempo», «editar id valor tiempo» o «borrar id tiempo» (los valores no tienen espacios). Para cada cambio, en orden: crear algo que ya existe → «CONFLICTO id: ya existe en el servidor (se conserva «valor»)»; editar o borrar algo que ya no existe → «IGNORADO id: ya no existe en el servidor»; si el elemento ha cambiado en el servidor desde la base (distinto tiempo) y el cambio del servidor es más reciente → «CONFLICTO id: gana el servidor «valor» (tiempo) frente al cambio local (tiempo)» (estrategia «gana el último»); en otro caso se aplica: «CREADO id = «valor»», «BORRADO id» o «EDITADO id = «valor»», añadiendo « (gana el cambio local, más reciente)» o « (sobre un cambio del servidor más antiguo)» si había cambio en el servidor. Termina con «Servidor tras sincronizar:», cada elemento « id = «valor» (tiempo)» por orden de id y «Aplicados: A · Conflictos: C».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, String[]> servidor = new TreeMap<>(), local = new TreeMap<>();   // id → {valor, marca de tiempo}        List<String[]> pendientes = new ArrayList<>();                            // cambios hechos sin conexión        String seccion = "";        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.isEmpty()) continue;            if (l.startsWith("[")) { seccion = l; continue; }            String[] p = l.split(" ");            switch (seccion) {                case "[servidor]" -> servidor.put(p[0], new String[]{p[1], p[2]});                case "[base]" -> local.put(p[0], new String[]{p[1], p[2]});                case "[cambios]" -> pendientes.add(p);              // op id valor tiempo  (op: crear, editar, borrar)                default -> { }            }        }        // TODO: aplica los cambios pendientes al servidor resolviendo conflictos y muestra el resultado    }}

Ejemplo: Tareas

Entrada

[servidor]
t1 Comprar_pan 100
t2 Llamar_al_banco 250
t3 Entregar_la_práctica 120
[base]
t1 Comprar_pan 100
t2 Llamar_al_banco 110
t3 Entregar_la_práctica 120
[cambios]
editar t1 Comprar_pan_integral 300
editar t2 Llamar_a_las_9 200
crear t4 Recoger_paquete 210
borrar t3 220
crear t1 Duplicada 230

Salida esperada

EDITADO t1 = «Comprar_pan_integral»
CONFLICTO t2: gana el servidor «Llamar_al_banco» (250) frente al cambio local (200)
CREADO t4 = «Recoger_paquete»
BORRADO t3
CONFLICTO t1: ya existe en el servidor (se conserva «Comprar_pan_integral»)
Servidor tras sincronizar:
  t1 = «Comprar_pan_integral» (300)
  t2 = «Llamar_al_banco» (250)
  t4 = «Recoger_paquete» (210)
Aplicados: 3 · Conflictos: 2

122. Física con paso fijo, acumulador e interpolación

Medio · Motores de Juegos · apuntesdam.com/subject/prog-multimedia/topic/motores-juegos

Los motores dibujan a la frecuencia que pueden, pero calculan la física en pasos fijos (FixedUpdate en Unity, _physics_process en Godot). La entrada tiene «paso P» (ms de cada paso de física), «velocidad V» (px/s de un objeto que avanza en x desde 0) y una línea con la duración en ms de cada fotograma. Para cada fotograma: suma su duración a un acumulador (como máximo 250 ms por fotograma, para que un parón no obligue a simular cientos de pasos), ejecuta pasos de física mientras el acumulador llegue a P (guardando la posición anterior y avanzando V·P/1000) y dibuja interpolando entre la posición anterior y la actual con alfa = acumulador / P. Muestra «Fi dt=D[ (limitado a 250)]: N paso(s), sobran A ms, x=X, se dibuja en Y (alfa Z)» con un decimal en las posiciones y dos en alfa, y al final «Fotogramas: F · Pasos de física: N · Tiempo simulado: T ms».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int paso = Integer.parseInt(sc.nextLine().trim().split(" ")[1]);              // «paso 20» (ms)        double velocidad = Double.parseDouble(sc.nextLine().trim().split(" ")[1]);   // «velocidad 120» (px/s)        String[] deltas = sc.nextLine().trim().split(" ");                            // ms de cada fotograma        final int MAXIMO = 250;                                                       // evita la «espiral de la muerte»        double x = 0, xAnterior = 0;        int acumulado = 0, totalPasos = 0;        // TODO: bucle con paso fijo, acumulador e interpolación    }}

Ejemplo: 60 FPS con tirones

Entrada

paso 20
velocidad 120
16 16 17 16 50 16 300 8

Salida esperada

F1 dt=16: 0 pasos, sobran 16 ms, x=0.0, se dibuja en 0.0 (alfa 0.80)
F2 dt=16: 1 paso, sobran 12 ms, x=2.4, se dibuja en 1.4 (alfa 0.60)
F3 dt=17: 1 paso, sobran 9 ms, x=4.8, se dibuja en 3.5 (alfa 0.45)
F4 dt=16: 1 paso, sobran 5 ms, x=7.2, se dibuja en 5.4 (alfa 0.25)
F5 dt=50: 2 pasos, sobran 15 ms, x=12.0, se dibuja en 11.4 (alfa 0.75)
F6 dt=16: 1 paso, sobran 11 ms, x=14.4, se dibuja en 13.3 (alfa 0.55)
F7 dt=300 (limitado a 250): 13 pasos, sobran 1 ms, x=45.6, se dibuja en 43.3 (alfa 0.05)
F8 dt=8: 0 pasos, sobran 9 ms, x=45.6, se dibuja en 44.3 (alfa 0.45)
Fotogramas: 8 · Pasos de física: 19 · Tiempo simulado: 380 ms

123. Colisiones con rejilla espacial

Difícil · Motores de Juegos · apuntesdam.com/subject/prog-multimedia/topic/motores-juegos

Comprobar todos los objetos contra todos cuesta n·(n−1)/2 comprobaciones: con 1000 balas y enemigos, medio millón por fotograma. Los motores primero descartan los pares lejanos (fase amplia) con estructuras como una rejilla espacial. La primera línea es «celda C» y las siguientes cajas «nombre x y ancho alto» (enteros, y hacia abajo). Calcula los choques por fuerza bruta (AABB, ya dado en chocan) y con la rejilla: cada caja se apunta en todas las celdas que ocupa, de floorDiv(x, C) a floorDiv(x+ancho−1, C) en x e igual en y; solo se comprueban los pares distintos que comparten al menos una celda. Muestra «Celdas ocupadas: K · La más llena: (cx,cy) con M» (la primera creada si hay empate), «Choques: a-b c-d…» (cada par en el orden de entrada, ordenados por la primera caja y luego por la segunda; «ninguno» si no hay), «Comprobaciones: fuerza bruta N · rejilla R (ahorro P %)» (P redondeado; 0 si N es 0) y «Mismo resultado: sí».

Código de partida (java)
import java.util.*; public class Main {    record Caja(String nombre, int x, int y, int ancho, int alto) { }     static boolean chocan(Caja a, Caja b) {        return a.x() < b.x() + b.ancho() && a.x() + a.ancho() > b.x()            && a.y() < b.y() + b.alto() && a.y() + a.alto() > b.y();    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int celda = Integer.parseInt(sc.nextLine().trim().split(" ")[1]);   // «celda 64»        List<Caja> cajas = new ArrayList<>();        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            if (p.length < 5) continue;            cajas.add(new Caja(p[0], Integer.parseInt(p[1]), Integer.parseInt(p[2]), Integer.parseInt(p[3]), Integer.parseInt(p[4])));        }        // TODO: fuerza bruta frente a rejilla espacial    }}

Ejemplo: Nivel de plataformas

Entrada

celda 64
jugador 40 300 24 32
suelo 0 330 640 20
moneda1 60 300 12 12
moneda2 400 200 12 12
enemigo 380 196 30 30
bala 600 40 6 6
plataforma 360 226 120 16

Salida esperada

Celdas ocupadas: 16 · La más llena: (6,3) con 3
Choques: jugador-suelo jugador-moneda1 moneda2-enemigo
Comprobaciones: fuerza bruta 21 · rejilla 5 (ahorro 76 %)
Mismo resultado: sí

Ejemplo: Coordenadas negativas

Entrada

celda 50
a -30 -30 40 40
b 0 0 10 10
c 200 200 10 10

Salida esperada

Celdas ocupadas: 5 · La más llena: (0,0) con 2
Choques: a-b
Comprobaciones: fuerza bruta 3 · rejilla 1 (ahorro 67 %)
Mismo resultado: sí

124. El efecto túnel: detección discreta frente a continua

Muy difícil · Motores de Juegos · apuntesdam.com/subject/prog-multimedia/topic/motores-juegos

Si una bala avanza 25 píxeles por fotograma y la pared mide 10, puede estar delante en un fotograma y detrás en el siguiente sin que nunca se detecte el choque (efecto túnel). Por eso los motores ofrecen detección continua (Continuous en el Rigidbody de Unity, CCD en Godot). La entrada tiene «pared x y ancho alto», «fps F» y balas «nombre x y vx vy» (velocidades en px/s, y hacia abajo). Simula 1 segundo. Discreta: en cada fotograma k = 1…F la bala está en (x + vx·k/F, y + vy·k/F) y choca si está estrictamente dentro de la pared. Continua: calcula con el método de las franjas el instante t ∈ [0, 1] en que el segmento recorrido entra en la pared (si la velocidad en un eje es 0, la bala debe estar estrictamente dentro de la franja de ese eje; si no, no hay impacto). Muestra «nombre: discreta → choca en el fotograma K (t=T s) | no detecta nada | continua → impacto en t=T s en (X, Y) por la cara izquierda/derecha/superior/inferior | no hay impacto | empieza dentro de la pared», añadiendo « ¡ATRAVIESA LA PARED!» si la continua ve un impacto (sin empezar dentro) que la discreta no detecta; al final «Efecto túnel: N de M balas». Tiempos con tres decimales y posiciones con uno.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String[] m = sc.nextLine().trim().split(" ");                     // «pared x y ancho alto»        double px = Double.parseDouble(m[1]), py = Double.parseDouble(m[2]);        double pw = Double.parseDouble(m[3]), ph = Double.parseDouble(m[4]);        int fps = Integer.parseInt(sc.nextLine().trim().split(" ")[1]);   // «fps 60»        int tuneles = 0, balas = 0;        while (sc.hasNextLine()) {            String[] b = sc.nextLine().trim().split(" ");            if (b.length < 5) continue;            String nombre = b[0];            double x0 = Double.parseDouble(b[1]), y0 = Double.parseDouble(b[2]);            double vx = Double.parseDouble(b[3]), vy = Double.parseDouble(b[4]);            balas++;            // TODO: detección discreta frente a continua (barrido)        }        System.out.println("Efecto túnel: " + tuneles + " de " + balas + " balas");    }}

Ejemplo: Pared fina

Entrada

pared 300 0 10 200
fps 60
lenta 5 100 600 0
rapida 0 50 1500 0
vertical 305 -50 0 400
diagonal 100 250 600 -300
falla 0 300 900 0

Salida esperada

lenta: discreta → choca en el fotograma 30 (t=0.500 s) | continua → impacto en t=0.492 s en (300.0, 100.0) por la cara izquierda
rapida: discreta → no detecta nada | continua → impacto en t=0.200 s en (300.0, 50.0) por la cara izquierda ¡ATRAVIESA LA PARED!
vertical: discreta → choca en el fotograma 8 (t=0.133 s) | continua → impacto en t=0.125 s en (305.0, 0.0) por la cara superior
diagonal: discreta → no detecta nada | continua → impacto en t=0.333 s en (300.0, 150.0) por la cara izquierda ¡ATRAVIESA LA PARED!
falla: discreta → no detecta nada | continua → no hay impacto
Efecto túnel: 2 de 5 balas

125. ¿Quién gana al tres en raya?

Fácil · Ejercicios de Programación Multimedia y Dispositivos Móviles · apuntesdam.com/subject/prog-multimedia/topic/ejercicios-pmdm

La entrada son 3 líneas con el tablero (X, O o . para las casillas vacías). Muestra «Gana X», «Gana O», «Empate» (tablero lleno sin ganador) o «Sigue la partida».

Código de partida (java)
import java.util.*; public class Main {     static String resultado(char[][] t) {        // TODO        return "Sigue la partida";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        char[][] t = new char[3][];        for (int i = 0; i < 3; i++) t[i] = sc.next().toCharArray();        System.out.println(resultado(t));    }}

Ejemplo: Gana X en diagonal

Entrada

X.O
OX.
..X

Salida esperada

Gana X

Ejemplo: Empate

Entrada

XOX
XOO
OXX

Salida esperada

Empate

Ejemplo: Sigue

Entrada

X..
.O.
...

Salida esperada

Sigue la partida

126. Una generación del juego de la vida

Medio · Ejercicios de Programación Multimedia y Dispositivos Móviles · apuntesdam.com/subject/prog-multimedia/topic/ejercicios-pmdm

La primera línea tiene filas y columnas; después, el tablero con # (célula viva) y . (muerta). Calcula la siguiente generación del juego de la vida de Conway y muéstrala: una célula viva con 2 o 3 vecinas vivas sobrevive; una muerta con exactamente 3 vecinas vivas nace; el resto muere o sigue muerta. Los bordes no se conectan.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int f = sc.nextInt(), c = sc.nextInt();        char[][] t = new char[f][];        for (int i = 0; i < f; i++) t[i] = sc.next().toCharArray();        // TODO: calcula y muestra la siguiente generación        for (char[] fila : t) System.out.println(new String(fila));    }}

Ejemplo: Parpadeador

Entrada

5 5
.....
..#..
..#..
..#..
.....

Salida esperada

.....
.....
.###.
.....
.....

Ejemplo: Bloque estable

Entrada

4 4
....
.##.
.##.
....

Salida esperada

....
.##.
.##.
....

127. Puntuación con combos

Medio · Ejercicios de Programación Multimedia y Dispositivos Móviles · apuntesdam.com/subject/prog-multimedia/topic/ejercicios-pmdm

Por la entrada llega la secuencia de eventos de una partida, uno por línea: MONEDA (10 puntos), ENEMIGO (50 puntos), GOLPE (pierdes una vida y el combo vuelve a 0). Cada MONEDA o ENEMIGO seguidos aumentan el combo en 1 y los puntos del evento se multiplican por el multiplicador actual: 1 con combo 0-4, 2 con combo 5-9 y 3 con combo 10 o más (el combo se incrementa antes de calcular). Empiezas con 3 vidas; con 0 vidas la partida termina y se ignoran los eventos siguientes. Muestra «Puntos: N», «Vidas: V» y «Mejor combo: C».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int puntos = 0, vidas = 3, combo = 0, mejor = 0;        while (sc.hasNext()) {            String evento = sc.next();            // TODO        }        System.out.println("Puntos: " + puntos);        System.out.println("Vidas: " + vidas);        System.out.println("Mejor combo: " + mejor);    }}

Ejemplo: Combo hasta x2

Entrada

MONEDA
MONEDA
MONEDA
MONEDA
MONEDA
ENEMIGO

Salida esperada

Puntos: 160
Vidas: 3
Mejor combo: 6

Ejemplo: Golpe reinicia el combo

Entrada

ENEMIGO
GOLPE
MONEDA

Salida esperada

Puntos: 60
Vidas: 2
Mejor combo: 1

128. Camino más corto en un laberinto

Difícil · Ejercicios de Programación Multimedia y Dispositivos Móviles · apuntesdam.com/subject/prog-multimedia/topic/ejercicios-pmdm

La IA de un enemigo necesita saber cuántos pasos hay hasta el jugador. La primera línea tiene filas y columnas; después el mapa con # (muro), . (suelo), E (enemigo) y J (jugador). Moviéndose en 4 direcciones, muestra el número mínimo de pasos de E a J, o «Sin camino» si no se puede llegar.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int f = sc.nextInt(), c = sc.nextInt();        char[][] m = new char[f][];        for (int i = 0; i < f; i++) m[i] = sc.next().toCharArray();        // TODO: BFS desde E hasta J        System.out.println("Sin camino");    }}

Ejemplo: Laberinto sencillo

Entrada

3 5
E..#.
.#...
...#J

Salida esperada

6

Ejemplo: Camino directo

Entrada

1 4
E..J

Salida esperada

3

Ejemplo: Sin camino

Entrada

3 3
E#.
##.
..J

Salida esperada

Sin camino

Programación de Servicios y Procesos

129. Coordinador de procesos

Difícil · Programación Concurrente · apuntesdam.com/subject/prog-servicios/topic/concurrencia

El programa puede funcionar en dos modos. Si recibe los argumentos «trabajador desde hasta», es un proceso hijo: cuenta los primos de ese tramo y escribe el número (ya está hecho). Si no, es el coordinador: lee un límite y un número de procesos, reparte el intervalo [1, límite] en tantos tramos iguales como procesos (el último se queda con el resto) y lanza un proceso hijo por tramo con ProcessBuilder, ejecutando esta misma clase. Primero lanza todos y después lee la salida de cada uno en orden y espera a que termine. Muestra «Proceso i [desde, hasta]: n primos» y, al final, «Total: N primos hasta límite».

Código de partida (java)
import java.io.*;import java.util.*; public class Main {    static boolean esPrimo(long n) {        if (n < 2) return false;        for (long d = 2; d * d <= n; d++) if (n % d == 0) return false;        return true;    }     static long contar(long desde, long hasta) {        long c = 0;        for (long i = desde; i <= hasta; i++) if (esPrimo(i)) c++;        return c;    }     public static void main(String[] args) throws Exception {        // Modo trabajador: el proceso hijo cuenta su tramo y escribe el resultado        if (args.length == 3 && args[0].equals("trabajador")) {            System.out.println(contar(Long.parseLong(args[1]), Long.parseLong(args[2])));            return;        }        Scanner sc = new Scanner(System.in);        long limite = sc.nextLong();        int procesos = sc.nextInt();        // TODO: repartir [1, limite] en tramos, lanzar un proceso hijo por tramo,        //       leer su resultado y mostrar el resumen    }}

Ejemplo: 10 000 con 4 procesos

Entrada

10000 4

Salida esperada

Proceso 1 [1, 2500]: 367 primos
Proceso 2 [2501, 5000]: 302 primos
Proceso 3 [5001, 7500]: 281 primos
Proceso 4 [7501, 10000]: 279 primos
Total: 1229 primos hasta 10000

Ejemplo: Reparto con resto

Entrada

100 3

Salida esperada

Proceso 1 [1, 33]: 11 primos
Proceso 2 [34, 66]: 7 primos
Proceso 3 [67, 100]: 7 primos
Total: 25 primos hasta 100

130. Planificador de CPU: FCFS, SJF y Round Robin

Muy difícil · Programación Concurrente · apuntesdam.com/subject/prog-servicios/topic/concurrencia

El sistema operativo decide qué proceso usa la CPU en cada momento. La entrada es el quantum y, después, cada proceso con su identificador (una letra), su instante de llegada y su ráfaga de CPU. Implementa los tres algoritmos devolviendo la línea de tiempo: un carácter por unidad de tiempo con la letra del proceso que se ejecuta, o «-» si la CPU está libre. FCFS ejecuta los procesos enteros por orden de llegada (a igualdad, en el orden de la entrada). SJF, no expropiativo, cada vez que la CPU queda libre elige, de los que ya han llegado, el de ráfaga más corta (a igualdad, el primero de la entrada). Round Robin da turnos de como mucho quantum unidades con una cola FIFO: los procesos que llegan durante un turno entran en la cola antes que el proceso que acaba de ser expulsado. El método informe, ya hecho, calcula el tiempo de espera de cada proceso (fin − llegada − ráfaga) y la media.

Código de partida (java)
import java.util.*; public class Main {    record Proceso(String id, int llegada, int rafaga) { }     // Devuelve la línea de tiempo: una letra (id) por unidad de tiempo, o '-' si la CPU está libre    static String fcfs(List<Proceso> ps) {        // TODO: el primero que llega se ejecuta entero        return "";    }     static String sjf(List<Proceso> ps) {        // TODO: no expropiativo; de los que han llegado, el de ráfaga más corta        return "";    }     static String roundRobin(List<Proceso> ps, int quantum) {        // TODO: turnos de como mucho quantum unidades        return "";    }     // Calcula y muestra los tiempos de espera a partir de la línea de tiempo    static void informe(String nombre, String linea, List<Proceso> ps) {        StringBuilder sb = new StringBuilder(nombre + ": " + linea + " |");        double suma = 0;        for (Proceso p : ps) {            int fin = linea.lastIndexOf(p.id().charAt(0)) + 1;            int espera = fin - p.llegada() - p.rafaga();            suma += espera;            sb.append(" ").append(p.id()).append("=").append(espera);        }        sb.append(String.format(Locale.US, " | media %.2f", suma / ps.size()));        System.out.println(sb);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int quantum = sc.nextInt();        List<Proceso> ps = new ArrayList<>();        while (sc.hasNext()) ps.add(new Proceso(sc.next(), sc.nextInt(), sc.nextInt()));        informe("FCFS", fcfs(ps), ps);        informe("SJF ", sjf(ps), ps);        informe("RR  ", roundRobin(ps, quantum), ps);    }}

Ejemplo: Ejemplo de clase

Entrada

2
A 0 5
B 1 3
C 2 1
D 3 2

Salida esperada

FCFS: AAAAABBBCDD | A=0 B=4 C=6 D=6 | media 4.00
SJF : AAAAACDDBBB | A=0 B=7 C=3 D=3 | media 3.25
RR  : AABBCAADDBA | A=6 B=6 C=2 D=4 | media 4.50

Ejemplo: CPU libre al principio

Entrada

3
A 2 4
B 3 2
C 10 1

Salida esperada

FCFS: --AAAABB--C | A=0 B=3 C=0 | media 1.00
SJF : --AAAABB--C | A=0 B=3 C=0 | media 1.00
RR  : --AAABBA--C | A=2 B=2 C=0 | media 1.33

131. Suma en paralelo con hilos

Difícil · Hilos y Multithreading en Java · apuntesdam.com/subject/prog-servicios/topic/hilos

La primera cifra de la entrada es el número de hilos y el resto, los números que hay que sumar. Reparte el array en tantos bloques consecutivos como hilos, lo más iguales posible: si no es divisible, los primeros bloques tienen un elemento más (con 10 números y 3 hilos: 4, 3 y 3). Cada hilo suma su bloque y guarda el resultado en su posición de parciales. El main debe esperar a que terminen todos antes de mostrar los resultados (esa parte ya está hecha). Un hilo puede quedarse sin elementos: entonces su parcial es 0.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) throws InterruptedException {        Scanner sc = new Scanner(System.in);        int hilos = sc.nextInt();        List<Long> lista = new ArrayList<>();        while (sc.hasNextLong()) lista.add(sc.nextLong());        long[] datos = lista.stream().mapToLong(Long::longValue).toArray();         long[] parciales = new long[hilos];      // cada hilo escribe solo en su posición        // TODO: crear los hilos, repartir el array en bloques, arrancarlos y esperar a que terminen         long total = 0;        for (int i = 0; i < hilos; i++) {            System.out.println("Hilo " + i + ": " + parciales[i]);            total += parciales[i];        }        System.out.println("Total: " + total);    }}

Ejemplo: 3 hilos, 10 números

Entrada

3
1 2 3 4 5 6 7 8 9 10

Salida esperada

Hilo 0: 10
Hilo 1: 18
Hilo 2: 27
Total: 55

Ejemplo: Más hilos que números

Entrada

4
100 200 300

Salida esperada

Hilo 0: 100
Hilo 1: 200
Hilo 2: 300
Hilo 3: 0
Total: 600

132. Productor-consumidor con un búfer acotado

Muy difícil · Hilos y Multithreading en Java · apuntesdam.com/subject/prog-servicios/topic/hilos

Varios productores dejan números en un búfer de capacidad limitada y un consumidor los va sacando. Implementa los métodos sincronizados de BufferAcotado con wait y notifyAll: poner espera mientras el búfer está lleno y sacar espera mientras está vacío; los dos avisan a los demás hilos cuando cambian el estado. Usa un array circular (inicio y cantidad) para que sacar sea inmediato. El main, ya hecho, lanza los productores, espera a que terminen, mete un FIN para el consumidor y muestra cuántos elementos de cada productor se consumieron y la suma: si no se pierde ni se repite ningún elemento, cada productor aporta todos los suyos.

Código de partida (java)
import java.util.*; class BufferAcotado {    private final int[] datos;    private int inicio = 0, cantidad = 0;     BufferAcotado(int capacidad) {        datos = new int[capacidad];    }     // Espera mientras el búfer está lleno    public synchronized void poner(int v) throws InterruptedException {        // TODO    }     // Espera mientras el búfer está vacío    public synchronized int sacar() throws InterruptedException {        // TODO        return -1;    }} public class Main {    static final int FIN = -1;                    // «píldora venenosa»: avisa al consumidor de que no hay más     public static void main(String[] args) throws InterruptedException {        Scanner sc = new Scanner(System.in);        int capacidad = sc.nextInt(), productores = sc.nextInt(), porProductor = sc.nextInt();        BufferAcotado buffer = new BufferAcotado(capacidad);        int[] consumidosDe = new int[productores];        long[] suma = new long[1];         List<Thread> hilos = new ArrayList<>();        for (int p = 0; p < productores; p++) {            final int id = p;            hilos.add(new Thread(() -> {                try {                    // Cada valor codifica su productor: id * 1000 + número                    for (int k = 1; k <= porProductor; k++) buffer.poner(id * 1000 + k);                } catch (InterruptedException e) { Thread.currentThread().interrupt(); }            }));        }        Thread consumidor = new Thread(() -> {            try {                int v;                while ((v = buffer.sacar()) != FIN) {                    consumidosDe[v / 1000]++;                    suma[0] += v % 1000;                }            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }        });        consumidor.start();        for (Thread h : hilos) h.start();        for (Thread h : hilos) h.join();        buffer.poner(FIN);        consumidor.join();         for (int p = 0; p < productores; p++) System.out.println("Productor " + p + ": " + consumidosDe[p] + " elementos consumidos");        System.out.println("Suma de los números: " + suma[0]);    }}

Ejemplo: Capacidad 2, 3 productores

Entrada

2 3 50

Salida esperada

Productor 0: 50 elementos consumidos
Productor 1: 50 elementos consumidos
Productor 2: 50 elementos consumidos
Suma de los números: 3825

Ejemplo: Capacidad 1

Entrada

1 4 25

Salida esperada

Productor 0: 25 elementos consumidos
Productor 1: 25 elementos consumidos
Productor 2: 25 elementos consumidos
Productor 3: 25 elementos consumidos
Suma de los números: 1300

133. Servidor de un protocolo de texto

Difícil · Sockets en Java · apuntesdam.com/subject/prog-servicios/topic/sockets

El main ya arranca un servidor TCP en un puerto libre y un cliente que le envía cada línea de la entrada y muestra la respuesta. Implementa atender, que gestiona la conexión de un cliente siguiendo este protocolo (una orden por línea, una respuesta por línea): «SUMA n1 n2 …» responde «OK total» (o «ERROR números no válidos»); «MAYUS texto» responde «OK TEXTO»; «INVERTIR texto» responde el texto al revés; «CONTAR» responde «OK n», con el número de órdenes atendidas antes en esta conexión (todas, incluidas las erróneas y otros CONTAR); «ADIOS» responde «OK hasta luego» y cierra. Las órdenes no distinguen mayúsculas y cualquier otra da «ERROR orden desconocida: X». Usa UTF-8 en los dos sentidos.

Código de partida (java)
import java.io.*;import java.net.*;import java.util.*; public class Main {    // Atiende a UN cliente: lee órdenes línea a línea y responde a cada una    static void atender(Socket cliente) throws IOException {        // TODO: leer cada línea, responder según el protocolo y terminar con ADIOS    }     public static void main(String[] args) throws Exception {        try (ServerSocket servidor = new ServerSocket(0)) {          // puerto libre cualquiera            Thread hiloServidor = new Thread(() -> {                try (Socket c = servidor.accept()) {                    atender(c);                } catch (IOException e) {                    System.out.println("Error del servidor: " + e.getMessage());                }            });            hiloServidor.start();             // Cliente: envía cada línea de la entrada y muestra la respuesta            Scanner sc = new Scanner(System.in);            try (Socket s = new Socket("localhost", servidor.getLocalPort());                 PrintWriter out = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), "UTF-8"), true);                 BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream(), "UTF-8"))) {                while (sc.hasNextLine()) {                    String orden = sc.nextLine();                    out.println(orden);                    String r;                    try {                        r = in.readLine();                    } catch (IOException e) {                        r = null;                                      // el servidor cerró la conexión                    }                    System.out.println("> " + orden);                    System.out.println("< " + (r == null ? "(conexión cerrada)" : r));                    if (r == null || orden.equals("ADIOS")) break;                }            }            hiloServidor.join();        }    }}

Ejemplo: Órdenes básicas

Entrada

SUMA 2 3 4
MAYUS hola mundo
INVERTIR socket
CONTAR
ADIOS

Salida esperada

> SUMA 2 3 4
< OK 9
> MAYUS hola mundo
< OK HOLA MUNDO
> INVERTIR socket
< OK tekcos
> CONTAR
< OK 3
> ADIOS
< OK hasta luego

Ejemplo: Errores

Entrada

suma 1 dos
BORRAR todo
CONTAR
adios

Salida esperada

> suma 1 dos
< ERROR números no válidos
> BORRAR todo
< ERROR orden desconocida: BORRAR
> CONTAR
< OK 2
> adios
< OK hasta luego

134. Mensajes fiables sobre UDP

Muy difícil · Sockets en Java · apuntesdam.com/subject/prog-servicios/topic/sockets

UDP no garantiza que los paquetes lleguen. El servidor de este reto lo simula: pierde uno de cada cuatro paquetes que recibe y una de cada tres respuestas. Implementa enviarFiable con el protocolo de parada y espera (stop-and-wait) que usan TCP y muchos juegos: envía el datagrama «seq:texto», espera como mucho ESPERA_MS milisegundos un datagrama «ACK seq» y, si no llega, reenvía el mismo mensaje, hasta MAX_INTENTOS veces. Devuelve el número de intentos que han hecho falta, o −1. Ignora cualquier ACK de otro número de secuencia. El servidor descarta los mensajes duplicados (cuando se pierde un ACK, el cliente reenvía algo que ya había llegado) gracias al número de secuencia, así que al final debe haber recibido todos los mensajes, en orden y una sola vez.

Código de partida (java)
import java.io.*;import java.net.*;import java.nio.charset.StandardCharsets;import java.util.*; public class Main {    static final int ESPERA_MS = 150, MAX_INTENTOS = 5;     // ---- Servidor (ya hecho): pierde paquetes a propósito para simular una red mala    static class Servidor extends Thread {        final DatagramSocket ds;        final List<String> recibidos = new ArrayList<>();        int paquetes = 0, duplicados = 0, ultimoSeq = 0;         Servidor() throws SocketException { ds = new DatagramSocket(0); }         public void run() {            byte[] buf = new byte[1024];            try {                while (true) {                    DatagramPacket p = new DatagramPacket(buf, buf.length);                    ds.receive(p);                    paquetes++;                    if (paquetes % 4 == 0) continue;                  // se pierde el paquete que llega                    String msg = new String(p.getData(), 0, p.getLength(), StandardCharsets.UTF_8);                    int seq = Integer.parseInt(msg.substring(0, msg.indexOf(':')));                    String datos = msg.substring(msg.indexOf(':') + 1);                    if (seq == ultimoSeq) duplicados++;                // ya lo teníamos: era una retransmisión                    else if (seq == ultimoSeq + 1) { ultimoSeq = seq; if (!datos.equals("FIN")) recibidos.add(datos); }                    if (paquetes % 3 == 0) continue;                  // se pierde el ACK de respuesta                    byte[] ack = ("ACK " + seq).getBytes(StandardCharsets.UTF_8);                    ds.send(new DatagramPacket(ack, ack.length, p.getSocketAddress()));                    if (datos.equals("FIN")) return;                }            } catch (IOException e) { }        }    }     // Envía un mensaje y espera su ACK; si no llega a tiempo, lo reenvía.    // Devuelve el número de intentos, o -1 si se agotan.    static int enviarFiable(DatagramSocket ds, SocketAddress destino, int seq, String texto) throws IOException {        // TODO        byte[] datos = (seq + ":" + texto).getBytes(StandardCharsets.UTF_8);        ds.send(new DatagramPacket(datos, datos.length, destino));        return 1;    }     public static void main(String[] args) throws Exception {        Servidor srv = new Servidor();        srv.start();        SocketAddress destino = new InetSocketAddress("localhost", srv.ds.getLocalPort());        Scanner sc = new Scanner(System.in);        List<String> mensajes = new ArrayList<>();        while (sc.hasNextLine()) { String l = sc.nextLine().trim(); if (!l.isEmpty()) mensajes.add(l); }        mensajes.add("FIN");        try (DatagramSocket ds = new DatagramSocket()) {            for (int i = 0; i < mensajes.size(); i++) {                int intentos = enviarFiable(ds, destino, i + 1, mensajes.get(i));                System.out.println("Mensaje " + (i + 1) + " «" + mensajes.get(i) + "»: "                        + (intentos < 0 ? "sin respuesta" : intentos + (intentos == 1 ? " intento" : " intentos")));            }        }        srv.join(2000);        srv.ds.close();        System.out.println("El servidor recibió en orden: " + String.join(" | ", srv.recibidos));        System.out.println("Retransmisiones duplicadas descartadas: " + srv.duplicados);    }}

Ejemplo: Cuatro mensajes

Entrada

hola
que
tal
estás

Salida esperada

Mensaje 1 «hola»: 1 intento
Mensaje 2 «que»: 1 intento
Mensaje 3 «tal»: 3 intentos
Mensaje 4 «estás»: 2 intentos
Mensaje 5 «FIN»: 3 intentos
El servidor recibió en orden: hola | que | tal | estás
Retransmisiones duplicadas descartadas: 3

Ejemplo: Uno solo

Entrada

ping

Salida esperada

Mensaje 1 «ping»: 1 intento
Mensaje 2 «FIN»: 1 intento
El servidor recibió en orden: ping
Retransmisiones duplicadas descartadas: 0

135. Una API REST con HttpServer

Difícil · Desarrollo de Servicios en Red · apuntesdam.com/subject/prog-servicios/topic/servicios-red-java

Completa manejar para que el servidor ofrezca una API REST de tareas. GET /tareas devuelve 200 y la lista en JSON, ordenada por id: [{"id":1,"texto":"..."}]. POST /tareas con el texto como cuerpo crea una tarea con el siguiente id, responde 201 con la tarea en JSON y la cabecera Location; si el texto está vacío, 400 con {"error":"texto vacío"}. GET /tareas/{id} devuelve 200 con la tarea; DELETE /tareas/{id} la borra y responde 204 sin cuerpo. Si el id no es un número: 400 {"error":"id no válido"}; si no existe: 404 {"error":"no existe la tarea N"}. Cualquier otro método: 405 sin cuerpo. Usa los métodos responder y json, ya hechos. El main envía con HttpClient las peticiones de la entrada y muestra el código y el cuerpo.

Código de partida (java)
import com.sun.net.httpserver.*;import java.io.*;import java.net.*;import java.net.http.*;import java.nio.charset.StandardCharsets;import java.util.*;import java.util.concurrent.*;import java.util.concurrent.atomic.AtomicInteger; public class Main {    static final Map<Integer, String> tareas = new ConcurrentSkipListMap<>();   // id → texto, ordenadas por id    static final AtomicInteger siguienteId = new AtomicInteger(1);     static void responder(HttpExchange ex, int estado, String cuerpo) throws IOException {        byte[] b = cuerpo.getBytes(StandardCharsets.UTF_8);        ex.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");        ex.sendResponseHeaders(estado, b.length == 0 ? -1 : b.length);        if (b.length > 0) ex.getResponseBody().write(b);        ex.close();    }     static String json(int id, String texto) {        return "{\"id\":" + id + ",\"texto\":\"" + texto.replace("\\", "\\\\").replace("\"", "\\\"") + "\"}";    }     // Atiende /tareas y /tareas/{id}    static void manejar(HttpExchange ex) throws IOException {        // TODO: GET /tareas, POST /tareas, GET /tareas/{id}, DELETE /tareas/{id} y los errores        responder(ex, 501, "");    }     public static void main(String[] args) throws Exception {        HttpServer srv = HttpServer.create(new InetSocketAddress("localhost", 0), 0);        srv.createContext("/tareas", Main::manejar);        srv.start();        String base = "http://localhost:" + srv.getAddress().getPort();         HttpClient cliente = HttpClient.newHttpClient();        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] t = sc.nextLine().trim().split(" ", 3);       // MÉTODO ruta [cuerpo]            HttpRequest.BodyPublisher cuerpo = t.length > 2                    ? HttpRequest.BodyPublishers.ofString(t[2]) : HttpRequest.BodyPublishers.noBody();            HttpRequest req = HttpRequest.newBuilder(URI.create(base + t[1])).method(t[0], cuerpo).build();            HttpResponse<String> r = cliente.send(req, HttpResponse.BodyHandlers.ofString());            System.out.println(t[0] + " " + t[1] + " → " + r.statusCode() + (r.body().isEmpty() ? "" : " " + r.body()));        }        srv.stop(0);    }}

Ejemplo: Crear, listar y borrar

Entrada

POST /tareas Comprar pan
POST /tareas Estudiar PSP
GET /tareas
DELETE /tareas/1
GET /tareas

Salida esperada

POST /tareas → 201 {"id":1,"texto":"Comprar pan"}
POST /tareas → 201 {"id":2,"texto":"Estudiar PSP"}
GET /tareas → 200 [{"id":1,"texto":"Comprar pan"},{"id":2,"texto":"Estudiar PSP"}]
DELETE /tareas/1 → 204
GET /tareas → 200 [{"id":2,"texto":"Estudiar PSP"}]

Ejemplo: Errores

Entrada

GET /tareas/7
GET /tareas/abc
POST /tareas
PUT /tareas
DELETE /tareas/1

Salida esperada

GET /tareas/7 → 404 {"error":"no existe la tarea 7"}
GET /tareas/abc → 400 {"error":"id no válido"}
POST /tareas → 400 {"error":"texto vacío"}
PUT /tareas → 405
DELETE /tareas/1 → 404 {"error":"no existe la tarea 1"}

136. Disyuntor (circuit breaker) para un servicio inestable

Muy difícil · Desarrollo de Servicios en Red · apuntesdam.com/subject/prog-servicios/topic/servicios-red-java

Cuando un servicio remoto se cae, seguir llamándolo empeora las cosas: cada petición espera un tiempo de espera completo y sobrecarga al servicio cuando intenta recuperarse. El patrón disyuntor lo evita con tres estados. CERRADO: las llamadas pasan; tras umbralFallos fallos seguidos, pasa a ABIERTO y el contador de fallos vuelve a 0. ABIERTO: se rechazan las llamadas sin hacerlas durante espera instantes desde que se abrió. Pasado ese tiempo, la siguiente llamada se permite como prueba y el estado pasa a SEMIABIERTO. SEMIABIERTO: si la prueba sale bien, vuelve a CERRADO (y se reinician los fallos); si falla, vuelve a ABIERTO desde ese instante (aunque sea un solo fallo). Un éxito en CERRADO también reinicia el contador de fallos. Implementa permitir y registrar. La entrada es el umbral, la espera y una cadena con el resultado que daría el servicio en cada instante (E o F); el main simula el tiempo.

Código de partida (java)
import java.util.*; class CircuitBreaker {    enum Estado { CERRADO, ABIERTO, SEMIABIERTO }     private final int umbralFallos;       // fallos seguidos para abrir    private final int espera;             // instantes que permanece abierto    private Estado estado = Estado.CERRADO;    private int fallosSeguidos = 0;    private int abiertoDesde = 0;     CircuitBreaker(int umbralFallos, int espera) {        this.umbralFallos = umbralFallos;        this.espera = espera;    }     Estado estado() { return estado; }     // ¿Se puede llamar al servicio en el instante t? (puede pasar de ABIERTO a SEMIABIERTO)    boolean permitir(int t) {        // TODO        return true;    }     // Se informa del resultado de una llamada que se permitió    void registrar(boolean exito, int t) {        // TODO    }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        CircuitBreaker cb = new CircuitBreaker(sc.nextInt(), sc.nextInt());        String resultados = sc.next();               // una letra por instante: E = el servicio responde, F = falla        int llamadas = 0, rechazadas = 0;        for (int t = 0; t < resultados.length(); t++) {            String linea;            if (cb.permitir(t)) {                llamadas++;                boolean ok = resultados.charAt(t) == 'E';                cb.registrar(ok, t);                linea = (ok ? "llamada OK " : "llamada FALLA");            } else {                rechazadas++;                linea = "rechazada   ";            }            System.out.println("t=" + t + " " + linea + " → " + cb.estado());        }        System.out.println("Llamadas al servicio: " + llamadas + ", rechazadas sin llamar: " + rechazadas);    }}

Ejemplo: Caída y recuperación

Entrada

3 4
EEFFFEEEEEFEEE

Salida esperada

t=0 llamada OK  → CERRADO
t=1 llamada OK  → CERRADO
t=2 llamada FALLA → CERRADO
t=3 llamada FALLA → CERRADO
t=4 llamada FALLA → ABIERTO
t=5 rechazada    → ABIERTO
t=6 rechazada    → ABIERTO
t=7 rechazada    → ABIERTO
t=8 llamada OK  → CERRADO
t=9 llamada OK  → CERRADO
t=10 llamada FALLA → CERRADO
t=11 llamada OK  → CERRADO
t=12 llamada OK  → CERRADO
t=13 llamada OK  → CERRADO
Llamadas al servicio: 11, rechazadas sin llamar: 3

Ejemplo: La prueba falla

Entrada

3 2
FFFEFEFEEE

Salida esperada

t=0 llamada FALLA → CERRADO
t=1 llamada FALLA → CERRADO
t=2 llamada FALLA → ABIERTO
t=3 rechazada    → ABIERTO
t=4 llamada FALLA → ABIERTO
t=5 rechazada    → ABIERTO
t=6 llamada FALLA → ABIERTO
t=7 rechazada    → ABIERTO
t=8 llamada OK  → CERRADO
t=9 llamada OK  → CERRADO
Llamadas al servicio: 7, rechazadas sin llamar: 3

137. Guardar contraseñas con PBKDF2

Difícil · Programación Segura en Java · apuntesdam.com/subject/prog-servicios/topic/programacion-segura

Nunca se guarda una contraseña, ni cifrada: se guarda un hash lento con sal. Implementa guardar, que genera una sal aleatoria de 16 bytes con SecureRandom, deriva el hash con el método derivar (PBKDF2-HMAC-SHA256, 32 bytes) usando ITERACIONES y devuelve el texto «pbkdf2$iteraciones$sal$hash», con la sal y el hash en Base64. Implementa también verificar, que separa las cuatro partes (devuelve false si el formato no es correcto), vuelve a derivar el hash de la contraseña introducida con la misma sal y las mismas iteraciones guardadas y lo compara en tiempo constante con MessageDigest.isEqual. El main comprueba el formato, que dos guardados de la misma contraseña son distintos y la verificación; la orden VECTOR calcula un hash con una sal fija para comprobar que derivar se usa bien.

Código de partida (java)
import java.security.*;import java.security.spec.*;import java.util.*;import javax.crypto.*;import javax.crypto.spec.*; public class Main {    static final int ITERACIONES = 120_000;     // Deriva 32 bytes de la contraseña con PBKDF2-HMAC-SHA256    static byte[] derivar(String clave, byte[] sal, int iteraciones) throws GeneralSecurityException {        PBEKeySpec spec = new PBEKeySpec(clave.toCharArray(), sal, iteraciones, 256);        return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();    }     // Devuelve "pbkdf2$iteraciones$salBase64$hashBase64" con una sal aleatoria de 16 bytes    static String guardar(String clave) throws GeneralSecurityException {        // TODO        return clave;    }     // Comprueba una contraseña contra lo guardado, en tiempo constante    static boolean verificar(String clave, String guardado) throws GeneralSecurityException {        // TODO        return guardado.equals(clave);    }     public static void main(String[] args) throws Exception {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] t = sc.nextLine().split(" ", 3);            switch (t[0]) {                case "VECTOR" -> {          // VECTOR salHex clave: resultado reproducible con 1000 iteraciones                    byte[] sal = HexFormat.of().parseHex(t[1]);                    System.out.println("Hash: " + HexFormat.of().formatHex(derivar(t[2], sal, 1000)));                }                case "PROBAR" -> {          // PROBAR claveGuardada claveIntroducida                    String g1 = guardar(t[1]), g2 = guardar(t[1]);                    String[] p = g1.split("\\
quot;);
System.out.println("Formato: " + p.length + " partes, algoritmo " + p[0] + ", " + (p.length > 1 ? p[1] : "?") + " iteraciones, sal de " + Base64.getDecoder().decode(p.length > 2 ? p[2] : "").length + " bytes, hash de " + Base64.getDecoder().decode(p.length > 3 ? p[3] : "").length + " bytes"); System.out.println("Dos guardados de la misma clave son distintos: " + !g1.equals(g2)); System.out.println("La clave guardada no aparece en el texto: " + !g1.contains(t[1])); System.out.println("Verificar «" + t[2] + "»: " + verificar(t[2], g1)); } } } }}

Ejemplo: Vector fijo

Entrada

VECTOR 000102030405060708090a0b0c0d0e0f patata

Salida esperada

Hash: e5816b525c317a12ddd9f411a9304dc0e131d02a179b24a02834f989d4613578

Ejemplo: Clave correcta

Entrada

PROBAR Clave.Segura-2025 Clave.Segura-2025

Salida esperada

Formato: 4 partes, algoritmo pbkdf2, 120000 iteraciones, sal de 16 bytes, hash de 32 bytes
Dos guardados de la misma clave son distintos: true
La clave guardada no aparece en el texto: true
Verificar «Clave.Segura-2025»: true

138. Tokens firmados con HMAC (como un JWT)

Muy difícil · Programación Segura en Java · apuntesdam.com/subject/prog-servicios/topic/programacion-segura

Tras iniciar sesión, muchas APIs entregan un token que el cliente envía en cada petición. Para que no se pueda falsificar, va firmado. Implementa crear: el token son tres partes separadas por puntos, cada una en Base64URL sin relleno: la cabecera {"alg":"HS256","typ":"JWT"}, los datos {"sub":"usuario","rol":"rol","exp":instante} (exactamente con ese formato, sin espacios) y la firma HMAC-SHA256 de «cabecera.datos» con la clave del servidor (usa el método hmac). Implementa verificar: si no hay tres partes o no se pueden decodificar, «INVÁLIDO: formato»; si la firma no coincide (compárala en tiempo constante), «INVÁLIDO: firma»; si ahora ≥ exp, «INVÁLIDO: caducado»; si todo está bien, «VÁLIDO usuario rol». Comprueba la firma antes de mirar los datos. El método campo, ya hecho, lee un valor del JSON de los datos.

Código de partida (java)
import java.nio.charset.StandardCharsets;import java.security.*;import java.util.*;import javax.crypto.*;import javax.crypto.spec.*; public class Main {    static final Base64.Encoder B64 = Base64.getUrlEncoder().withoutPadding();    static final Base64.Decoder D64 = Base64.getUrlDecoder();     static byte[] hmac(byte[] clave, String datos) throws GeneralSecurityException {        Mac mac = Mac.getInstance("HmacSHA256");        mac.init(new SecretKeySpec(clave, "HmacSHA256"));        return mac.doFinal(datos.getBytes(StandardCharsets.UTF_8));    }     // Token: base64url(cabecera) + "." + base64url(datos) + "." + base64url(HMAC de las dos primeras partes)    // La cabecera es {"alg":"HS256","typ":"JWT"} y los datos {"sub":"usuario","rol":"rol","exp":instante}    static String crear(byte[] clave, String usuario, String rol, long exp) throws GeneralSecurityException {        // TODO        return "";    }     // Devuelve "VÁLIDO usuario rol", o "INVÁLIDO: motivo" (formato, firma o caducado)    static String verificar(byte[] clave, String token, long ahora) throws GeneralSecurityException {        // TODO        return "VÁLIDO";    }     // Extrae un campo de un JSON plano y sencillo como el de los datos    static String campo(String json, String nombre) {        int i = json.indexOf("\"" + nombre + "\":");        if (i < 0) return null;        int ini = i + nombre.length() + 3;        if (json.charAt(ini) == '"') return json.substring(ini + 1, json.indexOf('"', ini + 1));        int fin = ini;        while (fin < json.length() && (Character.isDigit(json.charAt(fin)) || json.charAt(fin) == '-')) fin++;        return json.substring(ini, fin);    }     public static void main(String[] args) throws Exception {        byte[] clave = "clave-secreta-del-servidor-de-32b".getBytes(StandardCharsets.UTF_8);        byte[] otraClave = "otra-clave-que-no-es-la-buena-32".getBytes(StandardCharsets.UTF_8);        Scanner sc = new Scanner(System.in);        String usuario = sc.next(), rol = sc.next();        long exp = sc.nextLong();        String token = crear(clave, usuario, rol, exp);        System.out.println("Token: " + token);        while (sc.hasNext()) {            String prueba = sc.next();            long ahora = sc.nextLong();            String t = switch (prueba) {                case "normal" -> token;                case "otra-clave" -> crear(otraClave, usuario, rol, exp);                case "cambiar-rol" -> {               // el atacante edita los datos y deja la firma original                    String[] p = token.split("\\.");                    String datos = new String(D64.decode(p.length > 1 ? p[1] : ""), StandardCharsets.UTF_8).replace(rol, "admin");                    yield p[0] + "." + B64.encodeToString(datos.getBytes(StandardCharsets.UTF_8)) + "." + (p.length > 2 ? p[2] : "");                }                default -> "esto.no.es-un-token.valido";            };            System.out.println(prueba + " en t=" + ahora + ": " + verificar(clave, t, ahora));        }    }}

Ejemplo: Token válido y caducado

Entrada

ana alumna 1000
normal 500
normal 1000

Salida esperada

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmEiLCJyb2wiOiJhbHVtbmEiLCJleHAiOjEwMDB9.fs9mjeocvakBG8JvEwlZgu3Yd5TgKgDUZUhGdUUXpKs
normal en t=500: VÁLIDO ana alumna
normal en t=1000: INVÁLIDO: caducado

Ejemplo: Ataques

Entrada

luis profesor 2000
cambiar-rol 10
otra-clave 10
basura 10

Salida esperada

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsdWlzIiwicm9sIjoicHJvZmVzb3IiLCJleHAiOjIwMDB9.IcQ4KtkPdyIVXmF3_Qw5jwIsK4X8GRGP0noHWUgIYKY
cambiar-rol en t=10: INVÁLIDO: firma
otra-clave en t=10: INVÁLIDO: firma
basura en t=10: INVÁLIDO: formato

Ejercicios largos

139. Biblioteca: préstamos, devoluciones y multas

Difícil · 90 minutos · apuntesdam.com/ejercicios/java/biblioteca-prestamos-y-multas

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.

Requisitos

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

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

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

  • 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).

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

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

Formato de la 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.

Ejemplo: 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 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 €

Ejemplo: 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 esperada

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 €
Código de partida (java)
import java.util.*; public class Main {    static class Libro {        final String isbn;        final String titulo;        int disponibles;         Libro(String isbn, String titulo, int ejemplares) {            this.isbn = isbn;            this.titulo = titulo;            this.disponibles = ejemplares;        }    }     static class Prestamo {        final Libro libro;        final int vence;         Prestamo(Libro libro, int vence) {            this.libro = libro;            this.vence = vence;        }    }     static class Socio {        final int numero;        final String nombre;        final List<Prestamo> prestamos = new ArrayList<>();        int bloqueadoHasta = 0;        double multas = 0;         Socio(int numero, String nombre) {            this.numero = numero;            this.nombre = nombre;        }    }     static final Locale ES = Locale.forLanguageTag("es-ES");    static final Map<String, Libro> libros = new HashMap<>();    static final Map<Integer, Socio> socios = new TreeMap<>();     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            if (!linea.isEmpty()) procesar(linea);        }    }     static void procesar(String linea) {        String[] p = linea.split(" ");        switch (p[0]) {            case "LIBRO" -> altaLibro(p[1], Integer.parseInt(p[2]), resto(p, 3));            case "SOCIO" -> altaSocio(Integer.parseInt(p[1]), resto(p, 2));            case "PRESTAR" -> prestar(Integer.parseInt(p[1]), Integer.parseInt(p[2]), p[3]);            case "DEVOLVER" -> devolver(Integer.parseInt(p[1]), Integer.parseInt(p[2]), p[3]);            case "INFORME" -> informe();            default -> System.out.println("Error: orden desconocida " + p[0]);        }    }     static String resto(String[] p, int desde) {        return String.join(" ", Arrays.copyOfRange(p, desde, p.length));    }     static void altaLibro(String isbn, int ejemplares, String titulo) {        // TODO    }     static void altaSocio(int numero, String nombre) {        // TODO    }     static void prestar(int dia, int numero, String isbn) {        // TODO: comprueba las condiciones en el orden del enunciado    }     static void devolver(int dia, int numero, String isbn) {        // TODO: retraso, multa y bloqueo    }     static void informe() {        // TODO    }     static String euros(double importe) {        return String.format(ES, "%.2f €", importe);    }}

140. Clasificación de una liga de fútbol

Difícil · 75 minutos · apuntesdam.com/ejercicios/java/clasificacion-de-una-liga

Una liga de fútbol de barrio apunta los resultados de cada jornada en un fichero de texto y quiere un programa que saque la clasificación sin tener que echar cuentas a mano. El fichero lo rellenan varias personas, así que a veces hay líneas vacías o mal escritas: el programa debe avisar de ellas y seguir con las demás.

Cada línea válida es un partido: equipo local;goles-goles;equipo visitante, por ejemplo Atlético Norte;2-1;Real Sur. Una victoria da 3 puntos, un empate 1 y una derrota 0. Los nombres de los equipos tienen como mucho 18 caracteres y pueden llevar espacios.

La clasificación se ordena por puntos. Si dos equipos empatan a puntos, va delante el de mayor diferencia de goles (goles a favor menos goles en contra); si siguen empatados, el que más goles ha marcado; y si aún así empatan, por orden alfabético del nombre.

Requisitos

  • Lee la entrada línea a línea. Las líneas vacías (o solo con espacios) se ignoran sin avisar.

  • Una línea es válida si al separarla por ; salen exactamente tres partes, los dos nombres no están vacíos, son distintos (un equipo no juega contra sí mismo) y el marcador son dos números enteros separados por un guion. Se ignoran los espacios al principio y al final de cada parte.

  • Por cada línea no válida escribe en el momento Aviso: la línea N no es un resultado válido y se ignora, donde N es su número de línea en la entrada (contando también las vacías, empezando en 1).

  • Para cada equipo cuenta: partidos jugados (PJ), ganados (G), empatados (E), perdidos (P), goles a favor (GF), goles en contra (GC), diferencia de goles (DG) y puntos (Pts).

  • Escribe la clasificación con una cabecera y una fila por equipo, con este formato exacto de columnas: String.format("%-4d%-20s%3d%3d%3d%3d%4d%4d%5s%5d", …) para las filas (la DG va como texto, con signo + si es positiva) y la cabecera que ya trae el código inicial.

  • Después, una línea en blanco y dos líneas más: Partidos: N · Goles: M · Media: X,XX goles por partido (media con dos decimales y coma) y Equipo menos goleado: nombre (N goles en contra), con «gol» en singular si es 1. Si varios equipos empatan con menos goles en contra, se escribe el que va más arriba en la clasificación.

  • Si no hay ningún resultado válido, después de los avisos escribe solo No hay resultados válidos.

Formato de la entrada

  • Una línea por partido: local;goles-goles;visitante. Puede haber líneas vacías y líneas mal escritas.

Ejemplo: Una liga de cuatro equipos

Entrada

Atlético Norte;2-1;Real Sur
Deportivo Este;0-0;Unión Oeste
Real Sur;3-3;Deportivo Este
Unión Oeste;1-2;Atlético Norte
Atlético Norte;2-0;Deportivo Este
Real Sur;1-0;Unión Oeste

Salida esperada

Pos Equipo               PJ  G  E  P  GF  GC   DG  Pts
1   Atlético Norte        3  3  0  0   6   2   +4    9
2   Real Sur              3  1  1  1   5   5    0    4
3   Deportivo Este        3  0  2  1   3   5   -2    2
4   Unión Oeste           3  0  1  2   1   3   -2    1

Partidos: 6 · Goles: 15 · Media: 2,50 goles por partido
Equipo menos goleado: Atlético Norte (2 goles en contra)

Ejemplo: Con avisos y empates a puntos

Entrada

Ciudad Alta;1-0;Puerto Bajo
Puerto Bajo;3-1;Valle Verde
Valle Verde;2-0;Ciudad Alta

esto no es un resultado
Montaña Azul;2;Valle Verde
Puerto Bajo;0-0;Puerto Bajo
Montaña Azul;1-1;Ciudad Alta

Salida esperada

Aviso: la línea 5 no es un resultado válido y se ignora
Aviso: la línea 6 no es un resultado válido y se ignora
Aviso: la línea 7 no es un resultado válido y se ignora
Pos Equipo               PJ  G  E  P  GF  GC   DG  Pts
1   Ciudad Alta           3  1  1  1   2   3   -1    4
2   Puerto Bajo           2  1  0  1   3   2   +1    3
3   Valle Verde           2  1  0  1   3   3    0    3
4   Montaña Azul          1  0  1  0   1   1    0    1

Partidos: 4 · Goles: 9 · Media: 2,25 goles por partido
Equipo menos goleado: Montaña Azul (1 gol en contra)
Código de partida (java)
import java.util.*; public class Main {    static class Equipo {        final String nombre;        int pj, g, e, p, gf, gc;         Equipo(String nombre) {            this.nombre = nombre;        }         int dg() {            return 0; // TODO: diferencia de goles        }         int puntos() {            return 0; // TODO: 3 por victoria y 1 por empate        }    }     static final Locale ES = Locale.forLanguageTag("es-ES");    static final Map<String, Equipo> equipos = new LinkedHashMap<>();     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int numLinea = 0;        while (sc.hasNextLine()) {            String linea = sc.nextLine();            numLinea++;            // TODO: ignora las líneas vacías, valida el resto y registra el resultado en los dos equipos        }        // TODO: ordena los equipos, imprime la tabla y las dos líneas finales    }     static boolean esValida(String[] partes) {        return false; // TODO    }     static Equipo equipo(String nombre) {        return equipos.computeIfAbsent(nombre, Equipo::new);    }     static void registrar(Equipo t, int aFavor, int enContra) {        // TODO    }     static void imprimirTabla(List<Equipo> tabla) {        System.out.println(String.format("%-4s%-20s%3s%3s%3s%3s%4s%4s%5s%5s", "Pos", "Equipo", "PJ", "G", "E", "P", "GF", "GC", "DG", "Pts"));        // TODO: una fila por equipo con el formato del enunciado    }}

141. Cajero automático con PIN, límite diario y billetes

Medio · 50 minutos · apuntesdam.com/ejercicios/java/cajero-automatico

Un banco quiere probar la lógica de sus cajeros antes de instalarla en las máquinas. En lugar de pulsar botones, el programa recibe por teclado lo que hace el cliente, una orden por línea, y responde con lo que aparecería en la pantalla del cajero.

La primera línea no la escribe el cliente: es lo que el cajero lee de la tarjeta, el saldo de la cuenta y el PIN correcto. Mientras el cliente no acierte el PIN no puede hacer nada más, y si falla tres veces el cajero se queda la tarjeta.

El cajero solo tiene billetes de 50, 20 y 10 €, así que solo se puede sacar dinero en múltiplos de 10, y nunca más de 600 € al día entre todas las retiradas. Como cualquier programa que maneja dinero, trabaja en céntimos enteros: sumar 49,95 en coma flotante acaba dando 49,94999… y el saldo dejaría de cuadrar.

Requisitos

  • La primera línea es saldo pin, por ejemplo 850.50 1234. Después, cada línea es una orden (en mayúsculas o minúsculas) con su argumento separado por un espacio. Las líneas vacías se ignoran.

  • Hasta acertar el PIN, cualquier orden que no sea PIN responde Introduzca el PIN. PIN n responde PIN correcto si acierta; si falla, PIN incorrecto. Quedan 2 intentos o PIN incorrecto. Queda 1 intento, y al tercer fallo PIN incorrecto. Tarjeta retenida y el programa termina sin leer nada más.

  • Con el PIN correcto: SALDO escribe Saldo: X; PIN escribe Ya ha introducido el PIN; cualquier orden desconocida, Operación desconocida.

  • INGRESAR cantidad: la cantidad es un número positivo con punto decimal y como mucho dos decimales. Escribe Ingresados X. Saldo: Y, o Cantidad no válida.

  • RETIRAR cantidad, comprobando en este orden: que sea un múltiplo de 10 € mayor que 0 (si no, Importe no válido: debe ser múltiplo de 10 €), que haya saldo (Saldo insuficiente) y que con lo ya retirado hoy no se pasen los 600 € (Supera el límite diario: hoy puede retirar hasta X). Si todo va bien: Retirados X: desglose. Saldo: Y, con el desglose en el menor número de billetes, de mayor a menor y sin los que no se usan (por ejemplo 3x50 1x20 1x10).

  • SALIR escribe Operaciones: N · Retirado hoy: X (N cuenta los ingresos y retiradas hechos) y Retire su tarjeta, y termina: lo que venga después no se procesa. Si la entrada se acaba sin SALIR y el cliente se había identificado, el cajero escribe esas dos mismas líneas.

  • Los importes se escriben con punto de millar, coma decimal y dos decimales, seguidos de un espacio y €: 1.012.345,60 €.

Formato de la entrada

  • Línea 1: saldo pin. Resto: una orden por línea (PIN, SALDO, INGRESAR, RETIRAR, SALIR).

Ejemplo: Una sesión completa

Entrada

850.50 1234
SALDO
PIN 1111
PIN 1234
SALDO
RETIRAR 180
INGRESAR 49.95
RETIRAR 75
RETIRAR 500
RETIRAR 420
CONSULTAR
SALIR
SALDO

Salida esperada

Introduzca el PIN
PIN incorrecto. Quedan 2 intentos
PIN correcto
Saldo: 850,50 €
Retirados 180,00 €: 3x50 1x20 1x10. Saldo: 670,50 €
Ingresados 49,95 €. Saldo: 720,45 €
Importe no válido: debe ser múltiplo de 10 €
Supera el límite diario: hoy puede retirar hasta 420,00 €
Retirados 420,00 €: 8x50 1x20. Saldo: 300,45 €
Operación desconocida
Operaciones: 3 · Retirado hoy: 600,00 €
Retire su tarjeta

Ejemplo: Tres fallos de PIN

Entrada

2000 9876
PIN 1
PIN 2
PIN 3
SALDO

Salida esperada

PIN incorrecto. Quedan 2 intentos
PIN incorrecto. Queda 1 intento
PIN incorrecto. Tarjeta retenida
Código de partida (java)
import java.util.Locale;import java.util.Scanner; public class Main {    static final long LIMITE_DIARIO = 60000;      // 600 € en céntimos    static final int[] BILLETES = {50, 20, 10};     /** Céntimos → «1.234,50 €». */    static String euros(long centimos) {        String entero = String.format(Locale.ROOT, "%,d", centimos / 100).replace(',', '.');        return entero + "," + String.format("%02d", centimos % 100) + " €";    }     /** «49.95» → 4995 céntimos; -1 si no es una cantidad válida (hasta 2 decimales). */    static long centimos(String texto) {        // TODO        return -1;    }     /** Desglose en billetes de 50, 20 y 10 con el menor número de billetes: «3x50 1x20 1x10». */    static String desglose(long euros) {        // TODO        return "";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String[] inicio = sc.nextLine().trim().split("\\s+");        long saldo = centimos(inicio[0]);        String pin = inicio[1];         while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            // TODO: PIN (3 intentos), SALDO, INGRESAR, RETIRAR y SALIR        }    }}

142. Buscaminas: números, casilla más peligrosa y zonas despejadas

Difícil · 70 minutos · apuntesdam.com/ejercicios/java/buscaminas-numeros-y-zonas

En el Buscaminas, cada casilla sin mina muestra cuántas minas hay en las ocho casillas que la rodean (también en diagonal). Con esos números el jugador deduce dónde están las minas.

Cuando se pulsa una casilla con un 0, el juego no la abre sola: abre de golpe todos los ceros conectados con ella (también en diagonal), como una mancha. Cada una de esas manchas es una «zona despejada», y contar cuántas hay da una idea de lo fácil que es un tablero.

Vas a escribir la parte del juego que prepara un tablero nuevo: recibe dónde están las minas y calcula todo lo demás. Los tableros vienen de un fichero de niveles que a veces está mal escrito, así que hay que validarlo.

Requisitos

  • La primera línea tiene el número de filas y de columnas separados por un espacio (de 1 a 30 cada uno). Después vienen tantas líneas como filas, con exactamente tantos caracteres como columnas: * es una mina y . una casilla libre.

  • Si la cabecera no es válida, escribe Tablero no válido: línea 1. Si una fila no tiene la longitud correcta o tiene otro carácter, Tablero no válido: línea N (N es su número de línea en la entrada, contando la cabecera). Si faltan filas, Tablero no válido: faltan filas. En todos los casos no se escribe nada más.

  • Escribe Tablero: y el tablero con las minas como * y cada casilla libre sustituida por el número de minas vecinas (de 0 a 8).

  • Minas: M de T casillas (P %), con el porcentaje con un decimal y coma decimal.

  • Casilla más peligrosa: fila F, columna C (K minas alrededor): la casilla libre con más minas vecinas (filas y columnas se numeran desde 1; si hay empate, la primera leyendo por filas; «mina» en singular si K es 1). Si ninguna casilla libre tiene minas alrededor (o no hay casillas libres), Casilla más peligrosa: ninguna.

  • Zonas despejadas: Z: cuántos grupos de casillas con 0 hay, considerando que dos ceros están conectados si son vecinos, también en diagonal.

Formato de la entrada

  • Línea 1: filas columnas. Después, una línea por fila con * (mina) y . (libre).

Ejemplo: Un tablero de 5 × 6

Entrada

5 6
*.....
..*...
......
...**.
......

Salida esperada

Tablero:
*21100
12*100
012321
001**1
001221
Minas: 4 de 30 casillas (13,3 %)
Casilla más peligrosa: fila 3, columna 4 (3 minas alrededor)
Zonas despejadas: 2

Ejemplo: Un tablero mal escrito

Entrada

3 4
..*.
.x..
....

Salida esperada

Tablero no válido: línea 3
Código de partida (java)
import java.util.Locale;import java.util.Scanner; public class Main {    static int filas, columnas;    static char[][] tablero;    static int[][] cuenta;     /** Minas en las 8 casillas vecinas de (f, c). */    static int minasAlrededor(int f, int c) {        // TODO        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // TODO: lee y valida la cabecera y las filas del tablero        // TODO: escribe el tablero con los números, las minas, la casilla más peligrosa y las zonas despejadas    }}

143. Cifrado de Vigenère con el alfabeto español

Difícil · 60 minutos · apuntesdam.com/ejercicios/java/cifrado-vigenere-con-enie

El cifrado de Vigenère se usó durante siglos para mensajes militares y diplomáticos. Funciona como el de César (desplazar cada letra unas posiciones en el alfabeto), pero el desplazamiento cambia en cada letra según una palabra clave: con la clave LIMON, la primera letra se desplaza 11 posiciones (la L), la segunda 8 (la I), y así sucesivamente, volviendo a empezar la clave cuando se acaba.

La mayoría de las implementaciones usan el alfabeto inglés de 26 letras. Aquí se usa el español, con la Ñ entre la N y la O: 27 letras en total, de la A (posición 0) a la Z (posición 26). Al desplazar más allá de la Z se vuelve a empezar por la A.

Los espacios, los números, los signos y las vocales con tilde no se cifran: se copian tal cual y no gastan letra de la clave. Las minúsculas se cifran como su mayúscula y siguen siendo minúsculas.

Requisitos

  • Cada línea es una orden seguida de su argumento (todo lo que hay tras el primer espacio). La orden puede ir en mayúsculas o minúsculas; las líneas vacías se ignoran. Una orden desconocida escribe Orden desconocida: ORDEN (en mayúsculas).

  • CLAVE palabra guarda la clave si todas sus letras son del alfabeto (en mayúsculas o minúsculas, la Ñ incluida) y escribe Clave guardada (N letras) («letra» si es una). Si no, Clave no válida: solo letras de la A a la Z y la Ñ y se mantiene la clave anterior.

  • CIFRAR texto escribe Cifrado: …: cada letra del alfabeto se desplaza hacia delante tantas posiciones como la posición de la letra de la clave que le toca. La clave vuelve a empezar en cada orden y solo avanza con las letras que se cifran.

  • DESCIFRAR texto escribe Descifrado: … deshaciendo el desplazamiento.

  • Si no hay clave guardada, CIFRAR y DESCIFRAR escriben Falta la clave; si hay clave pero no texto, Falta el texto.

  • ANALIZAR texto escribe Letras: N · Más frecuentes: A 5, B 3, C 2: el total de letras del alfabeto del texto (sin distinguir mayúsculas) y las tres más repetidas con su número de apariciones, de más a menos (si hay empate, en el orden del alfabeto; si hay menos de tres letras distintas, las que haya). Si no hay ninguna letra, solo Letras: 0.

Formato de la entrada

  • Una orden por línea: CLAVE, CIFRAR, DESCIFRAR o ANALIZAR, seguida de su argumento.

Posición de cada letra
ABCDEFGHIJKLMNÑOPQRSTUVWXYZ
01234567891011121314151617181920212223242526

Ejemplo: Cifrar, descifrar y analizar

Entrada

CIFRAR hola
CLAVE Limón
CLAVE Limon
CIFRAR Ataque al amanecer, ¡ya!
DESCIFRAR Lbmfho iw oylupqqc, ¡gm!
ANALIZAR El pingüino Ñoño come ñoquis

Salida esperada

Falta la clave
Clave no válida: solo letras de la A a la Z y la Ñ
Clave guardada (5 letras)
Cifrado: Lbmfho iw oylupqqc, ¡gm!
Descifrado: Ataque al amanecer, ¡ya!
Letras: 23 · Más frecuentes: O 5, I 3, Ñ 3

Ejemplo: Órdenes en minúsculas

Entrada

clave sol
cifrar
descifrar Zzz zzz
analizar ab ba
analizar 1234 ¿?
borrar todo

Salida esperada

Clave guardada (3 letras)
Falta el texto
Descifrado: Hlo hlo
Letras: 4 · Más frecuentes: A 2, B 2
Letras: 0
Orden desconocida: BORRAR
Código de partida (java)
import java.util.Scanner; public class Main {    static final String ALFABETO = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ";    static final int N = ALFABETO.length();   // 27 letras     /** La clave en mayúsculas, o null si tiene algo que no sea una letra del alfabeto. */    static String validarClave(String texto) {        // TODO        return null;    }     /** Cifra (sentido = 1) o descifra (sentido = -1). Solo cambian las letras del alfabeto, que conservan su caja. */    static String vigenere(String texto, String clave, int sentido) {        // TODO        return texto;    }     /** «Letras: 23 · Más frecuentes: O 5, Ñ 3, E 2». */    static String analizar(String texto) {        // TODO        return "Letras: 0";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String clave = null;        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            // TODO: separa la orden del argumento y atiende CLAVE, CIFRAR, DESCIFRAR y ANALIZAR        }    }}

144. Parking con tarifas por tipo de vehículo

Difícil · 75 minutos · apuntesdam.com/ejercicios/java/parking-con-tarifas

El aparcamiento de un centro comercial tiene 4 plazas para coches, 2 para motos y 1 para furgonetas (es una versión de prueba: el real tiene cientos). La barrera de entrada y la de salida envían al programa una línea por cada vehículo que pasa.

Cada tipo de vehículo paga distinto, pero todos con la misma regla: los 10 primeros minutos son gratis (para quien entra y se va) y, a partir de ahí, se cobra cada cuarto de hora empezado, con un máximo por día. Esa regla común es justo lo que debe ir en una clase padre, y lo que cambia (el precio y el máximo), en cada clase hija.

Las cámaras leen las matrículas en el formato actual español: cuatro cifras y tres consonantes, sin vocales, ni Ñ ni Q, para evitar palabras y confusiones.

Requisitos

  • Cada línea es una orden: ENTRA HH:MM tipo matrícula, SALE HH:MM matrícula u OCUPACION. La orden, el tipo y la matrícula pueden venir en mayúsculas o minúsculas (la matrícula se guarda y se muestra en mayúsculas). Una línea con un número de palabras distinto del de su orden escribe Orden incompleta; una orden desconocida, Orden desconocida. Las líneas vacías se ignoran.

  • ENTRA comprueba, en este orden: la hora (Hora no válida: X, de 00:00 a 23:59), el tipo (Tipo no válido: X; son coche, moto y furgoneta), la matrícula (Matrícula no válida: X: 4 cifras y 3 letras de BCDFGHJKLMNPRSTVWXYZ), que no esté ya dentro (X ya está dentro) y que queden plazas de su tipo (No quedan plazas de tipo). Si todo va bien: Entra X (tipo) a las HH:MM.

  • SALE comprueba la hora, que el vehículo esté dentro (X no está en el parking) y que la salida no sea anterior a la entrada (La hora de salida es anterior a la entrada). Si todo va bien, el vehículo deja libre su plaza y se escribe Sale X (tipo) tras D: I, con la duración como 2 h 35 min, 3 h o 8 min y el importe en euros (6,60 €).

  • Tarifas: hasta 10 minutos, 0 €. Después, por cada 15 minutos empezados: coche 0,60 €, moto 0,30 € y furgoneta 0,90 €, con un máximo de 18 €, 9 € y 25 € respectivamente. Todas las estancias son del mismo día.

  • OCUPACION escribe Ocupación: coche 2/4 · moto 0/2 · furgoneta 1/1.

  • Al terminar la entrada: Recaudación: X (N salidas) («salida» si es 1) y Siguen dentro: … con las matrículas que no han salido en orden alfabético, separadas por coma y espacio, o ninguno.

Formato de la entrada

  • Una orden por línea: ENTRA HH:MM tipo matrícula, SALE HH:MM matrícula u OCUPACION.

Plazas y tarifas
TipoPlazasCada 15 min empezadosMáximo diario
coche40,60 €18,00 €
moto20,30 €9,00 €
furgoneta10,90 €25,00 €

Ejemplo: Un día de parking

Entrada

ENTRA 08:15 coche 1234BCD
ENTRA 08:20 moto 5678FGH
ENTRA 08:25 furgoneta 9012JKL
ENTRA 08:30 furgoneta 3456MNP
OCUPACION
SALE 08:28 5678FGH
SALE 10:50 1234BCD
ENTRA 11:00 coche 1234bcd
SALE 19:45 9012JKL
SALE 12:00 0000BBB

Salida esperada

Entra 1234BCD (coche) a las 08:15
Entra 5678FGH (moto) a las 08:20
Entra 9012JKL (furgoneta) a las 08:25
No quedan plazas de furgoneta
Ocupación: coche 1/4 · moto 1/2 · furgoneta 1/1
Sale 5678FGH (moto) tras 8 min: 0,00 €
Sale 1234BCD (coche) tras 2 h 35 min: 6,60 €
Entra 1234BCD (coche) a las 11:00
Sale 9012JKL (furgoneta) tras 11 h 20 min: 25,00 €
0000BBB no está en el parking
Recaudación: 31,60 € (3 salidas)
Siguen dentro: 1234BCD

Ejemplo: Datos no válidos

Entrada

ENTRA 25:00 coche 1234BCD
ENTRA 09:00 bici 1234BCD
ENTRA 09:00 coche 1234ABC
ENTRA 09:00 coche 1234BC
ENTRA 09:05 moto 1111BBB
ENTRA 09:06 moto 1111BBB
SALE 09:00 1111BBB
ENTRA
ESTADO

Salida esperada

Hora no válida: 25:00
Tipo no válido: bici
Matrícula no válida: 1234ABC
Matrícula no válida: 1234BC
Entra 1111BBB (moto) a las 09:05
1111BBB ya está dentro
La hora de salida es anterior a la entrada
Orden incompleta
Orden desconocida
Recaudación: 0,00 € (0 salidas)
Siguen dentro: 1111BBB
Código de partida (java)
import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Locale;import java.util.Map;import java.util.Scanner; abstract class Vehiculo {    private final String matricula;    private final int entrada;   // minutos desde las 00:00     Vehiculo(String matricula, int entrada) {        this.matricula = matricula;        this.entrada = entrada;    }     String getMatricula() { return matricula; }    int getEntrada() { return entrada; }     abstract String tipo();    /** Céntimos por cada 15 minutos empezados. */    abstract long precioFraccion();    /** Lo máximo que se cobra en un día, en céntimos. */    abstract long maximoDiario();     /** Importe en céntimos de una estancia: 10 minutos gratis y, después, cada cuarto de hora empezado, con un máximo. */    long importe(int minutos) {        // TODO        return 0;    }} // TODO: las clases Coche, Moto y Furgoneta public class Main {    static final List<String> TIPOS = List.of("coche", "moto", "furgoneta");    static final Map<String, Integer> PLAZAS = Map.of("coche", 4, "moto", 2, "furgoneta", 1);     static final Map<String, Vehiculo> dentro = new HashMap<>();     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            // TODO: ENTRA, SALE y OCUPACION        }        // TODO: recaudación y vehículos que siguen dentro    }}

145. Evaluador de expresiones con pilas y variables

Muy difícil · 100 minutos · apuntesdam.com/ejercicios/java/evaluador-de-expresiones-con-variables

Las calculadoras, las hojas de cálculo y los compiladores tienen que entender expresiones como 3 + 4 * (2 - 1), donde la multiplicación va antes que la suma y los paréntesis cambian el orden. Hacerlo leyendo de izquierda a derecha no funciona: hay que reorganizar la expresión.

La solución clásica, del informático Edsger Dijkstra, es el algoritmo de la estación de maniobras (shunting-yard): pasa la expresión a notación postfija, en la que los operadores van detrás de sus operandos (3 4 2 1 - * +) y ya no hacen falta paréntesis ni prioridades. Evaluar la postfija es muy sencillo con una pila: cada número se apila y cada operador desapila dos, opera y apila el resultado.

Tu calculadora trabajará con números enteros (long), recordará variables y explicará cada error en lugar de fallar: es la parte de un intérprete que se ocupa de las expresiones.

Requisitos

  • Cada línea no vacía es una expresión, una asignación nombre = expresión o la orden VARIABLES. Los nombres de variable empiezan por letra o _ y siguen con letras, cifras o _; distinguen mayúsculas.

  • Las expresiones usan enteros sin signo, variables, paréntesis, +, -, *, / (división entera, como en Java) y % (resto, como en Java), con los espacios que se quiera. *, / y % tienen más prioridad que + y -, y operadores de la misma prioridad se aplican de izquierda a derecha. Un - al principio, tras ( o tras otro operador es un menos unario, que tiene la mayor prioridad.

  • Para una expresión correcta escribe línea → postfija → resultado: la línea tal como se ha leído (sin espacios al principio ni al final), la postfija con sus elementos separados por un espacio (el menos unario se escribe ~) y el valor. En una asignación el resultado es nombre = valor, y la variable queda guardada.

  • Si hay un error al separar la expresión o al pasarla a postfija, escribe línea → Error: mensaje; si el error aparece al evaluarla, línea → postfija → Error: mensaje (sin la postfija si está vacía). Los mensajes son: carácter no válido 'c', paréntesis sin abrir, paréntesis sin cerrar, expresión incompleta (falta un operando o un operador), división entre cero, variable desconocida nombre, número demasiado grande (no cabe en un long), número no válido texto (empieza por cifra pero tiene letras) y desbordamiento (el resultado de una operación no cabe en un long). Una asignación con error no cambia la variable.

  • VARIABLES escribe Variables: a = 1, b = 2 con todas las variables por orden alfabético, o Variables: ninguna.

Formato de la entrada

  • Una expresión, asignación u orden VARIABLES por línea.

Ejemplo: Prioridades, menos unario y variables

Entrada

3 + 4 * (2 - 1)
(3 + 4) * 2 - 10 / 3
x = 2 * -5
y = x * x % 7
-(x + 3) * y
VARIABLES

Salida esperada

3 + 4 * (2 - 1) → 3 4 2 1 - * + → 7
(3 + 4) * 2 - 10 / 3 → 3 4 + 2 * 10 3 / - → 11
x = 2 * -5 → 2 5 ~ * → x = -10
y = x * x % 7 → x x * 7 % → y = 2
-(x + 3) * y → x 3 + ~ y * → 14
Variables: x = -10, y = 2

Ejemplo: Errores

Entrada

8 / (4 - 2 * 2)
(1 + 2
1 + 2)
3 +
5 $ 2
z + 1
4 5

Salida esperada

8 / (4 - 2 * 2) → 8 4 2 2 * - / → Error: división entre cero
(1 + 2 → Error: paréntesis sin cerrar
1 + 2) → Error: paréntesis sin abrir
3 + → 3 + → Error: expresión incompleta
5 $ 2 → Error: carácter no válido '
#x27; z + 1 → z 1 + → Error: variable desconocida z 4 5 → 4 5 → Error: expresión incompleta
Código de partida (java)
import java.util.ArrayDeque;import java.util.ArrayList;import java.util.Deque;import java.util.List;import java.util.Map;import java.util.Scanner;import java.util.TreeMap; public class Main {    /** Error de una expresión, con el mensaje que se muestra. */    static class ErrorExpresion extends Exception {        ErrorExpresion(String mensaje) { super(mensaje); }    }     static final Map<String, Long> variables = new TreeMap<>();     /** «x * (2 - 15)» → [x, *, (, 2, -, 15, )]. */    static List<String> separar(String texto) throws ErrorExpresion {        // TODO        return new ArrayList<>();    }     /** Notación infija → postfija (algoritmo de la estación de maniobras). */    static List<String> aPostfija(List<String> tokens) throws ErrorExpresion {        // TODO        return new ArrayList<>();    }     static long evaluar(List<String> postfija) throws ErrorExpresion {        // TODO        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            // TODO: VARIABLES, asignaciones y expresiones        }    }}

146. Conversor de CSV a JSON con comillas y tipos

Difícil · 75 minutos · apuntesdam.com/ejercicios/java/conversor-csv-a-json

Una academia exporta sus alumnos de una hoja de cálculo en CSV y quiere cargarlos en una aplicación web que solo entiende JSON. Parece trivial (separar por comas), pero los datos reales lo complican: hay apellidos como «López, Juan» que llevan una coma dentro y por eso van entre comillas, y textos que contienen las propias comillas, que en CSV se escriben dobles ("").

Además, en JSON los tipos importan: 8.5 es un número, true un booleano, un campo vacío es null y el resto son textos entre comillas. Hay una trampa clásica: un código postal como 08001 parece un número, pero si se convierte se pierde el cero. En JSON un número no puede empezar por cero, así que esos valores se quedan como texto.

Algunas líneas vienen mal (les faltan campos o tienen comillas sin cerrar). No se convierten, pero el programa dice cuáles eran para poder corregirlas.

Requisitos

  • La primera línea no vacía es la cabecera, con los nombres de los campos. Cada línea siguiente es un registro. Las líneas en blanco se ignoran (pero cuentan para numerar las líneas).

  • Los campos se separan por comas. Un campo puede ir entre comillas dobles, y entonces puede contener comas; dentro de las comillas, "" representa una comilla. A cada campo (y a cada nombre de la cabecera) se le quitan los espacios del principio y del final.

  • Un registro con un número de campos distinto del de la cabecera, o con comillas sin cerrar, no se convierte: se anota su número de línea.

  • Tipos: un campo vacío es null; un campo que cumple el formato de número JSON (signo menos opcional, 0 o una cifra del 1 al 9 seguida de más cifras, y opcionalmente un punto y decimales) se escribe tal cual; true y false son booleanos; todo lo demás es texto entre comillas, con " escrito \", \ escrito \\ y el tabulador escrito \t.

  • Salida: un array JSON con un objeto por registro, en el orden del fichero, con dos espacios de sangría para cada objeto y cuatro para cada propiedad ("campo": valor), comas entre elementos y nada al final de la última línea. Sin registros, [].

  • Después del JSON, Registros: N · Líneas ignoradas: M (l1, l2…), sin el paréntesis si M es 0. Si la entrada no tiene ni la cabecera, solo El fichero está vacío.

Formato de la entrada

  • Un fichero CSV: cabecera y una línea por registro, campos separados por comas y entre comillas cuando hace falta.

Ejemplo: Alumnos de una academia

Entrada

id,nombre,ciudad,cp,nota,beca
1,Ana Gil,Madrid,28001,8.5,true
2,"López, Juan",Barcelona,08001,6,false
3,"Eva ""la rápida"" Ruiz",Sevilla,41001,,true
4,Luis,Valencia,46001,7.25
5,Marta Díaz,Bilbao,48001,-0.5,false

Salida esperada

[
  {
    "id": 1,
    "nombre": "Ana Gil",
    "ciudad": "Madrid",
    "cp": 28001,
    "nota": 8.5,
    "beca": true
  },
  {
    "id": 2,
    "nombre": "López, Juan",
    "ciudad": "Barcelona",
    "cp": "08001",
    "nota": 6,
    "beca": false
  },
  {
    "id": 3,
    "nombre": "Eva \"la rápida\" Ruiz",
    "ciudad": "Sevilla",
    "cp": 41001,
    "nota": null,
    "beca": true
  },
  {
    "id": 5,
    "nombre": "Marta Díaz",
    "ciudad": "Bilbao",
    "cp": 48001,
    "nota": -0.5,
    "beca": false
  }
]
Registros: 4 · Líneas ignoradas: 1 (5)

Ejemplo: Espacios, barras y comillas sin cerrar

Entrada

codigo , descripcion , precio
A1 , "Cable HDMI 2m" , 7.95
A2,"Ruta C:\temp",0
A3,"Sin cerrar,1

A4,,

Salida esperada

[
  {
    "codigo": "A1",
    "descripcion": "Cable HDMI 2m",
    "precio": 7.95
  },
  {
    "codigo": "A2",
    "descripcion": "Ruta C:\\temp",
    "precio": 0
  },
  {
    "codigo": "A4",
    "descripcion": null,
    "precio": null
  }
]
Registros: 3 · Líneas ignoradas: 1 (4)
Código de partida (java)
import java.util.ArrayList;import java.util.List;import java.util.Scanner; public class Main {    /** Campos de una línea CSV separados por comas, con comillas dobles opcionales ("" es una comilla). null si hay comillas sin cerrar. */    static List<String> campos(String linea) {        // TODO        return new ArrayList<>();    }     /** Texto JSON entre comillas, con las comillas, barras y tabuladores escapados. */    static String cadena(String s) {        // TODO        return "\"" + s + "\"";    }     /** El valor JSON de un campo: null, número, booleano o texto. */    static String valor(String campo) {        // TODO        return cadena(campo);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // TODO: lee la cabecera y cada línea, y escribe el JSON y el resumen    }}

147. Nóminas de una plantilla con distintos contratos

Difícil · 80 minutos · apuntesdam.com/ejercicios/java/nominas-de-una-plantilla

Una pequeña empresa calcula las nóminas con una hoja de cálculo que nadie se atreve a tocar. Quiere sustituirla por un programa que reciba la plantilla del mes y saque la tabla de nóminas.

Hay cuatro tipos de contrato y cada uno calcula el bruto a su manera, pero las deducciones son iguales para todos: una retención de IRPF según el tramo del bruto y una cotización a la Seguridad Social. Excepto los becarios, a los que en este ejercicio no se les retiene IRPF. Es el caso de libro para una interfaz con métodos por defecto que una clase concreta puede sobrescribir.

Las cifras están simplificadas (una tabla de retenciones de la empresa en lugar del cálculo real del IRPF, y un único porcentaje de cotización): lo que importa es la estructura del programa y que cada céntimo cuadre.

Requisitos

  • Cada línea es TIPO datos, con los datos separados por ;: ASALARIADO nombre;salario anual;pagas (12 o 14 pagas), HORAS nombre;horas;precio por hora (horas enteras, como mucho 300), COMERCIAL nombre;base;ventas;comisión % (comisión hasta el 20 %) y BECARIO nombre;ayuda. Las cantidades usan punto decimal y hasta dos decimales. El tipo puede ir en minúsculas.

  • Una línea con un tipo desconocido escribe Línea N ignorada: tipo desconocido TIPO; con más o menos datos de los que le tocan (o sin nombre), Línea N ignorada: faltan o sobran datos; con un número mal escrito o fuera de rango, Línea N ignorada: dato no válido. N es el número de línea en la entrada (las vacías también cuentan). Estos avisos salen antes que la tabla.

  • Bruto mensual: asalariado, salario anual ÷ pagas; por horas, las primeras 160 horas al precio normal y las siguientes al 125 %; comercial, base + comisión sobre las ventas, con la comisión limitada a 2.000 €; becario, la ayuda.

  • Retención de IRPF, un único tipo sobre todo el bruto: hasta 1.000 €, el 2 %; hasta 2.000 €, el 12 %; hasta 3.000 €, el 18 %; por encima, el 24 %. Los becarios no tienen retención. Seguridad Social: el 6,5 % del bruto para todos. Neto = bruto − IRPF − Seguridad Social.

  • Todos los cálculos se hacen en céntimos y cada porcentaje se redondea al céntimo más cercano (las mitades suben).

  • Tabla: la cabecera Empleado, Tipo, Bruto, IRPF, SS, Neto y una fila por empleado en el orden de la entrada, con el nombre en 20 caracteres y el tipo (asalariado, por horas, comercial, becario) en 12, alineados a la izquierda, y los cuatro importes en 11 caracteres a la derecha, con punto de millar y coma decimal y sin símbolo. Después, una fila TOTAL con las sumas (el tipo en blanco).

  • Para terminar, Coste para la empresa: X € (el bruto total más un 30 % de cotización de la empresa) y Mayor neto: nombre (X €) (el primero si hay empate). Si no hay ningún empleado válido, después de los avisos solo No hay nóminas que calcular.

Formato de la entrada

  • Una línea por empleado: TIPO nombre;dato;dato….

Retención de IRPF (tabla de la empresa)
Bruto mensualTipo
hasta 1.000,00 €2 %
de 1.000,01 € a 2.000,00 €12 %
de 2.000,01 € a 3.000,00 €18 %
más de 3.000,00 €24 %

Ejemplo: Una plantilla con los cuatro contratos

Entrada

ASALARIADO Ana Gil;30000;14
ASALARIADO Luis Mora;48000;12
HORAS Eva Ruiz;172;12.50
COMERCIAL Pablo Sanz;1100;42000;3.5
BECARIO Irene Vidal;600

Salida esperada

Empleado            Tipo              Bruto       IRPF         SS       Neto
Ana Gil             asalariado     2.142,86     385,71     139,29   1.617,86
Luis Mora           asalariado     4.000,00     960,00     260,00   2.780,00
Eva Ruiz            por horas      2.187,50     393,75     142,19   1.651,56
Pablo Sanz          comercial      2.570,00     462,60     167,05   1.940,35
Irene Vidal         becario          600,00       0,00      39,00     561,00
TOTAL                             11.500,36   2.202,06     747,53   8.550,77
Coste para la empresa: 14.950,47 €
Mayor neto: Luis Mora (2.780,00 €)

Ejemplo: Líneas que no se pueden calcular

Entrada

ASALARIADO Mar Gómez;25000;13
JEFE Pedro;5000
HORAS Ana;abc;10

COMERCIAL Leo;1000;5000
becario Sol;450.5

Salida esperada

Línea 1 ignorada: dato no válido
Línea 2 ignorada: tipo desconocido JEFE
Línea 3 ignorada: dato no válido
Línea 5 ignorada: faltan o sobran datos
Empleado            Tipo              Bruto       IRPF         SS       Neto
Sol                 becario          450,50       0,00      29,28     421,22
TOTAL                                450,50       0,00      29,28     421,22
Coste para la empresa: 585,65 €
Mayor neto: Sol (421,22 €)
Código de partida (java)
import java.util.ArrayList;import java.util.List;import java.util.Locale;import java.util.Scanner; interface Retribuible {    /** Bruto del mes, en céntimos. */    long bruto();     // TODO: métodos por defecto retencion(), seguridadSocial() y neto()} abstract class Empleado implements Retribuible {    private final String nombre;     Empleado(String nombre) { this.nombre = nombre; }     String getNombre() { return nombre; }     abstract String tipo();} // TODO: Asalariado, PorHoras, Comercial y Becario public class Main {    /** cantidad × centésimas / 10000, redondeado al céntimo (las mitades suben). */    static long porcentaje(long cantidad, long centesimas) {        return (cantidad * centesimas + 5000) / 10000;    }     static String euros(long c) {        return String.format(Locale.ROOT, "%,d", c / 100).replace(',', '.') + "," + String.format("%02d", c % 100);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<Empleado> plantilla = new ArrayList<>();        // TODO: lee y valida cada línea, y escribe la tabla de nóminas y el resumen    }}

148. Contador de palabras en paralelo con un pool de hilos

Difícil · 70 minutos · apuntesdam.com/ejercicios/java/contador-de-palabras-con-hilos

Contar las palabras de un texto enorme (las actas de un ayuntamiento, los registros de un servidor) se puede repartir: cada hilo cuenta un trozo y al final se suman los recuentos. Es el esquema «divide y vencerás» que usan desde los procesadores de varios núcleos hasta MapReduce en los centros de datos.

El error típico es que todos los hilos escriban en el mismo HashMap: no está preparado para accesos simultáneos y se pierden recuentos (o se corrompe). La forma limpia es que cada hilo cuente en su propio mapa y devuelva el resultado; el hilo principal es el único que los combina.

La prueba de que el programa es correcto es que el resultado no dependa del número de hilos ni del orden en que terminen: con 1, 3 u 8 hilos el informe de palabras tiene que ser exactamente el mismo.

Requisitos

  • La primera línea es el número de hilos, de 1 a 8 (si no, Número de hilos no válido: debe estar entre 1 y 8 y nada más). El resto es el texto; las líneas en blanco no cuentan. Si no queda ninguna línea, No hay texto que analizar.

  • Se usan tantos hilos como se pidan, pero nunca más que líneas. Las líneas se reparten en bloques consecutivos lo más iguales posible: si no salen exactos, los primeros bloques tienen una línea más. Cada bloque lo cuenta un hilo de un ExecutorService.

  • Una palabra es una secuencia de letras (de la a a la z, las vocales con tilde, la ü y la ñ), sin distinguir mayúsculas; cualquier otro carácter separa palabras.

  • Escribe Hilos: H · Líneas: L y una línea por bloque, en orden, con dos espacios delante: Bloque [i-j]: N palabras (o [i] si el bloque tiene una sola línea; las líneas se numeran desde 1 sin contar las vacías).

  • Después, Palabras: N · Distintas: D con el total de palabras y de palabras distintas, y Más frecuentes: seguido de las 5 palabras más repetidas (o las que haya) sin contar las palabras vacías de la tabla, cada una como el número en 4 caracteres alineado a la derecha, un espacio y la palabra. A igual número, por orden alfabético según compareTo.

Formato de la entrada

  • Línea 1: número de hilos (1 a 8). Resto: el texto.

Palabras vacías (no salen en «Más frecuentes»)
Palabras
de, la, que, el, en, y, a, los, se, del, las, un, por, con, no, una, su, para, es, al, lo, como

Ejemplo: Un texto con tres hilos

Entrada

3
La programación concurrente permite que varios hilos trabajen a la vez.
Cada hilo cuenta las palabras de su bloque de líneas.
Si los hilos comparten un mapa sin protección, se pierden datos.
Por eso cada hilo usa un mapa propio y devuelve el resultado.
El hilo principal espera con get() y suma los mapas parciales.
Con un hilo o con ocho hilos, el resultado debe ser el mismo.
La concurrencia bien hecha no cambia el resultado: solo el tiempo.

Salida esperada

Hilos: 3 · Líneas: 7
  Bloque [1-3]: 32 palabras
  Bloque [4-5]: 23 palabras
  Bloque [6-7]: 24 palabras
Palabras: 79 · Distintas: 56
Más frecuentes:
   4 hilo
   3 hilos
   3 resultado
   2 cada
   2 mapa

Ejemplo: Número de hilos no válido

Entrada

12
Un texto cualquiera

Salida esperada

Número de hilos no válido: debe estar entre 1 y 8
Código de partida (java)
import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Scanner;import java.util.Set;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future; public class Main {    static final Set<String> VACIAS = Set.of("de", "la", "que", "el", "en", "y", "a", "los", "se", "del", "las", "un",            "por", "con", "no", "una", "su", "para", "es", "al", "lo", "como");     /** Lo que hace cada hilo: contar las palabras de su bloque en un mapa propio, sin compartir nada. */    static Map<String, Integer> contar(List<String> lineas) {        // TODO        return new HashMap<>();    }     public static void main(String[] args) throws Exception {        Scanner sc = new Scanner(System.in);        // TODO: lee el número de hilos y el texto, reparte los bloques en un pool de hilos y une los resultados    }}

149. Validador de DNI, NIE e IBAN diseñado con clases de equivalencia

Medio · 50 minutos · apuntesdam.com/ejercicios/java/validador-dni-nie-iban

Casi todas las aplicaciones de gestión piden un DNI, un NIE o un IBAN, y casi todas los validan mal: aceptan letras que no corresponden, rechazan minúsculas o se atascan con los espacios del IBAN. Los tres llevan un control matemático que permite detectar la mayoría de los errores al teclear.

La letra del DNI es el resto de dividir el número entre 23, buscado en la tabla TRWAGMYFPDXBNJZSQVHLCKE. El NIE de los extranjeros funciona igual cambiando la X, la Y y la Z iniciales por 0, 1 y 2. El IBAN usa otro método (ISO 13616): se pasan los cuatro primeros caracteres al final, cada letra se sustituye por un número (A = 10, B = 11… Z = 35) y el número resultante, de unas 26 cifras, tiene que dar resto 1 al dividirlo entre 97.

Este ejercicio es también de Entornos de Desarrollo: las pruebas no se han elegido al azar, sino con la técnica de clases de equivalencia (un caso representativo de cada grupo de entradas que el programa trata igual) y valores límite (los bordes de cada grupo). Tienes la tabla abajo: tu programa tiene que pasar todos esos casos.

Requisitos

  • Cada línea es TIPO valor, con TIPO DNI, NIE o IBAN en mayúsculas o minúsculas. Las líneas vacías se ignoran. Para cada línea escribe TIPO valor → válido o TIPO valor → no válido: motivo, con el tipo en mayúsculas y el valor tal como venía (sin espacios al principio ni al final).

  • DNI: 8 cifras y una letra (mayúscula o minúscula). Si no tiene ese formato, formato incorrecto (8 cifras y una letra); si la letra no es la que toca, la letra debería ser L.

  • NIE: X, Y o Z, 7 cifras y una letra. Si no tiene ese formato, formato incorrecto (X, Y o Z, 7 cifras y una letra); si la letra no cuadra, el mismo mensaje que el DNI.

  • IBAN (se ignoran los espacios y las minúsculas): si no empieza por ES, solo se admiten IBAN españoles; si no tiene 24 caracteres, un IBAN español tiene 24 caracteres; si tras ES hay algo que no sea una cifra, después de ES solo puede haber cifras; si el control módulo 97 no da 1, los dígitos de control no cuadran. Se comprueba en ese orden.

  • Un tipo desconocido escribe TIPO valor → no válido: tipo de documento desconocido.

  • Al final, Válidos: V de T.

Formato de la entrada

  • Una línea por documento: DNI valor, NIE valor o IBAN valor.

Clases de equivalencia y valores límite del DNI
ClaseCaso de pruebaResultado esperado
Válida: 8 cifras y su letra12345678Zválido
Válida: letra en minúscula12345678zválido
Límite inferior del número00000000Tválido
Límite superior del número99999999Rválido
No válida: letra que no toca12345678Ala letra debería ser Z
No válida: 7 cifras1234567Zformato incorrecto
No válida: 9 cifras123456789Zformato incorrecto
No válida: sin letra12345678formato incorrecto
Clases de equivalencia del IBAN
ClaseCaso de pruebaResultado esperado
Válida, con espaciosES91 2100 0418 4502 0005 1332válido
Válida, en minúsculas y sin espacioses9121000418450200051332válido
Otro paísDE89370400440532013000solo se admiten IBAN españoles
Longitud incorrectaES91210004184502000513324 caracteres
Letras tras ESES91 2100 0418 4502 0005 13ABsolo cifras
Control incorrecto (una cifra cambiada)ES91 2100 0418 4502 0005 1333no cuadran

Ejemplo: DNI y NIE

Entrada

DNI 12345678Z
DNI 12345678z
DNI 12345678A
DNI 1234567Z
NIE X1234567L
NIE W1234567L
PASAPORTE AB123456

Salida esperada

DNI 12345678Z → válido
DNI 12345678z → válido
DNI 12345678A → no válido: la letra debería ser Z
DNI 1234567Z → no válido: formato incorrecto (8 cifras y una letra)
NIE X1234567L → válido
NIE W1234567L → no válido: formato incorrecto (X, Y o Z, 7 cifras y una letra)
PASAPORTE AB123456 → no válido: tipo de documento desconocido
Válidos: 3 de 7

Ejemplo: IBAN

Entrada

IBAN ES91 2100 0418 4502 0005 1332
iban es9121000418450200051332
IBAN DE89370400440532013000
IBAN ES912100041845020005133
IBAN ES91 2100 0418 4502 0005 13AB
IBAN ES91 2100 0418 4502 0005 1333

Salida esperada

IBAN ES91 2100 0418 4502 0005 1332 → válido
IBAN es9121000418450200051332 → válido
IBAN DE89370400440532013000 → no válido: solo se admiten IBAN españoles
IBAN ES912100041845020005133 → no válido: un IBAN español tiene 24 caracteres
IBAN ES91 2100 0418 4502 0005 13AB → no válido: después de ES solo puede haber cifras
IBAN ES91 2100 0418 4502 0005 1333 → no válido: los dígitos de control no cuadran
Válidos: 2 de 6
Código de partida (java)
import java.util.Scanner; public class Main {    static final String LETRAS = "TRWAGMYFPDXBNJZSQVHLCKE";     /** null si el DNI es correcto, o el motivo por el que no lo es. */    static String validarDni(String dni) {        // TODO        return null;    }     /** El NIE se valida como un DNI cambiando X, Y y Z por 0, 1 y 2. */    static String validarNie(String nie) {        // TODO        return null;    }     /** IBAN español: 24 caracteres y control módulo 97 (ISO 13616). */    static String validarIban(String iban) {        // TODO        return null;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // TODO: valida cada línea y escribe el resultado y el resumen    }}

150. Fichero de productos con registros de tamaño fijo y acceso directo

Muy difícil · 90 minutos · apuntesdam.com/ejercicios/java/fichero-de-registros-con-acceso-directo

Antes de las bases de datos, los programas de gestión guardaban sus datos en ficheros binarios con registros de tamaño fijo. Todavía hoy es la forma de entender cómo trabaja un gestor de bases de datos por dentro: si cada registro ocupa siempre lo mismo, la posición del registro número N se calcula con una multiplicación y se puede ir directamente a él, sin leer los anteriores.

En este fichero cada producto ocupa 77 bytes: el código (int, 4 bytes), el nombre (30 caracteres char de 2 bytes, 60 en total, rellenando con el carácter \0), el precio (double, 8), el stock (int, 4) y una marca de borrado (boolean, 1). El producto con código N vive en el byte (N − 1) × 77: es acceso directo, y por eso puede haber huecos entre productos.

Borrar un registro desplazando todos los siguientes sería lentísimo en un fichero grande, así que las bajas son lógicas: se pone la marca de borrado a true y el hueco queda libre para un alta futura con ese código.

Requisitos

  • El programa crea un fichero temporal vacío y ejecuta una orden por línea (la orden puede ir en minúsculas; las líneas vacías se ignoran; una orden desconocida escribe Orden desconocida).

  • ALTA código;nombre;precio;stock: el código es un entero de 1 a 9999, el nombre no puede estar vacío, el precio es un número no negativo con hasta dos decimales y el stock un entero no negativo (si no, Datos no válidos). Si ya hay un producto activo con ese código, Ya existe el producto N. Si no, se escribe el registro en su posición y se escribe Alta del producto N en el byte B, añadiendo (nombre recortado a 30 caracteres) si el nombre era más largo.

  • PRECIO código precio cambia solo el precio, sin tocar el resto del registro: Precio del producto N: antes → después (o Datos no válidos / No existe el producto N).

  • BAJA código pone la marca de borrado: Baja lógica del producto N, o No existe el producto N si no está activo. VER código escribe N · nombre · precio · S uds o No existe el producto N.

  • LISTAR escribe Productos activos: A y, con dos espacios delante, cada producto activo por orden de código con el formato de VER.

  • INFORME escribe Fichero: T bytes, H huecos de registro, A activos, L libres o borrados · valor del stock X, donde H es el tamaño del fichero entre 77 y el valor es la suma de precio × stock de los activos. Los precios se escriben con dos decimales, coma decimal y €.

  • Un registro está activo si el fichero llega hasta él, el código guardado en él es el suyo (los huecos que se crean al escribir más allá del final están llenos de ceros) y no tiene la marca de borrado.

Formato de la entrada

  • Una orden por línea: ALTA, PRECIO, BAJA, VER, LISTAR o INFORME.

Estructura de un registro (77 bytes)
CampoTipoBytesDesplazamiento
códigoint40
nombre30 × char604
preciodouble864
stockint472
borradoboolean176

Ejemplo: Altas, cambios y bajas

Entrada

ALTA 1;Ratón inalámbrico;9.95;20
ALTA 3;Teclado mecánico;49.90;5
ALTA 1;Repetido;1;1
LISTAR
PRECIO 3 44.50
BAJA 1
VER 1
VER 3
VER 2
INFORME

Salida esperada

Alta del producto 1 en el byte 0
Alta del producto 3 en el byte 154
Ya existe el producto 1
Productos activos: 2
  1 · Ratón inalámbrico · 9,95 € · 20 uds
  3 · Teclado mecánico · 49,90 € · 5 uds
Precio del producto 3: 49,90 € → 44,50 €
Baja lógica del producto 1
No existe el producto 1
3 · Teclado mecánico · 44,50 € · 5 uds
No existe el producto 2
Fichero: 231 bytes, 3 huecos de registro, 1 activos, 2 libres o borrados · valor del stock 222,50 €

Ejemplo: Datos que no valen

Entrada

ALTA 0;Cero;1;1
ALTA 10;;1;1
ALTA 10;Monitor;-5;1
ALTA 10;Monitor;99.999;1
ALTA abc
PRECIO 7 10
BAJA 7
listar
BORRAR 1

Salida esperada

Datos no válidos
Datos no válidos
Datos no válidos
Datos no válidos
Datos no válidos
No existe el producto 7
No existe el producto 7
Productos activos: 0
Orden desconocida
Código de partida (java)
import java.io.File;import java.io.IOException;import java.io.RandomAccessFile;import java.util.Locale;import java.util.Scanner; public class Main {    static final int NOMBRE = 30;                          // caracteres del nombre    static final int TAM = 4 + NOMBRE * 2 + 8 + 4 + 1;     // código + nombre + precio + stock + borrado = 77 bytes     static RandomAccessFile f;     /** Acceso directo: el registro del código N empieza en el byte (N - 1) × 77. */    static long posicion(int codigo) {        return (long) (codigo - 1) * TAM;    }     static String euros(double x) {        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";    }     static void orden(String linea) throws IOException {        // TODO: ALTA, PRECIO, BAJA, VER, LISTAR e INFORME con seek, read… y write…    }     public static void main(String[] args) throws IOException {        File fichero = File.createTempFile("productos", ".dat");        fichero.deleteOnExit();        try (RandomAccessFile raf = new RandomAccessFile(fichero, "rw")) {            f = raf;            Scanner sc = new Scanner(System.in);            while (sc.hasNextLine()) {                String linea = sc.nextLine().trim();                if (!linea.isEmpty()) orden(linea);            }        }    }}

151. Servidor de órdenes con sockets e hilos

Muy difícil · 100 minutos · apuntesdam.com/ejercicios/java/servidor-de-ordenes-con-sockets

Muchos servicios de Internet (SMTP, FTP, Redis, POP3) usan protocolos de texto: el cliente envía una orden por línea y el servidor responde con otra línea que empieza por un código (OK, ERR). Son fáciles de depurar, porque se pueden probar a mano con telnet o netcat.

Vas a escribir un servidor de este tipo en Java. Escucha en un puerto, acepta conexiones y atiende a cada cliente en un hilo distinto, para que un cliente lento no bloquee a los demás. Tiene un pequeño almacén clave-valor compartido por todas las conexiones, al estilo de Redis, así que tiene que ser seguro para varios hilos.

Para poder probarlo sin dos programas, el mismo main arranca el servidor en un hilo y hace de cliente: lee las órdenes de la entrada, las envía por el socket y escribe lo que contesta el servidor. Una línea --- cierra la conexión y abre otra, como si llegara un cliente nuevo.

Requisitos

  • El servidor escucha en localhost en un puerto libre (puerto 0) y atiende cada conexión en un hilo nuevo. Lee líneas en UTF-8 y responde a cada una con una línea. Cuando recibe ADIOS responde y cierra la conexión.

  • Protocolo (las órdenes se reconocen en mayúsculas o minúsculas): SUMA a b → OK suma con enteros de hasta 9 cifras y signo opcional, o ERR números no válidos; MAYUS texto → OK TEXTO; INVIERTE texto → OK otxet (sin texto, ERR falta el texto); GUARDA clave valor (el valor puede tener espacios) → OK guardado u OK sustituido (antes: valor), y ERR uso: GUARDA clave valor si falta algo; LEE clave → OK valor o ERR no existe clave; CUENTA → OK número de claves; ADIOS → OK adiós. Una línea vacía es ERR línea vacía y cualquier otra orden ERR orden desconocida.

  • El cliente conecta al empezar y escribe == Cliente N conectado. Por cada línea de la entrada (incluidas las vacías) la envía, espera la respuesta y escribe > línea (solo > si la línea está vacía) y < respuesta. Una línea --- cierra la conexión actual y abre una nueva (con su mensaje de conexión); después de ADIOS, la siguiente línea también abre una conexión nueva.

  • Al terminar la entrada, el cliente cierra su conexión, se cierra el servidor y se escribe Servidor cerrado · claves guardadas: N.

  • El almacén es común a todas las conexiones: lo que guarda un cliente lo puede leer el siguiente.

Formato de la entrada

  • Una orden del protocolo por línea; --- abre una conexión nueva.

Ejemplo: Dos clientes que comparten el almacén

Entrada

SUMA 20 22
MAYUS hola, mundo
INVIERTE ñandú
GUARDA curso 2º DAM
GUARDA curso 1º DAW
CUENTA
ADIOS
LEE curso
GUARDA tutor Ana Gil
---
LEE tutor
LEE profe
CUENTA

Salida esperada

== Cliente 1 conectado
> SUMA 20 22
< OK 42
> MAYUS hola, mundo
< OK HOLA, MUNDO
> INVIERTE ñandú
< OK údnañ
> GUARDA curso 2º DAM
< OK guardado
> GUARDA curso 1º DAW
< OK sustituido (antes: 2º DAM)
> CUENTA
< OK 1
> ADIOS
< OK adiós
== Cliente 2 conectado
> LEE curso
< OK 1º DAW
> GUARDA tutor Ana Gil
< OK guardado
== Cliente 3 conectado
> LEE tutor
< OK Ana Gil
> LEE profe
< ERR no existe profe
> CUENTA
< OK 2
Servidor cerrado · claves guardadas: 2

Ejemplo: Errores del protocolo

Entrada

SUMA 1
SUMA uno dos
MAYUS

GUARDA solo
BORRA todo
suma -5 3

Salida esperada

== Cliente 1 conectado
> SUMA 1
< ERR números no válidos
> SUMA uno dos
< ERR números no válidos
> MAYUS
< ERR falta el texto
>
< ERR línea vacía
> GUARDA solo
< ERR uso: GUARDA clave valor
> BORRA todo
< ERR orden desconocida
> suma -5 3
< OK -2
Servidor cerrado · claves guardadas: 0
Código de partida (java)
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.io.PrintWriter;import java.net.ServerSocket;import java.net.Socket;import java.nio.charset.StandardCharsets;import java.util.Map;import java.util.Scanner;import java.util.concurrent.ConcurrentHashMap; public class Main {    static final Map<String, String> almacen = new ConcurrentHashMap<>();     /** La respuesta del protocolo a una línea. */    static String responder(String linea) {        // TODO: SUMA, MAYUS, INVIERTE, GUARDA, LEE, CUENTA y ADIOS        return "ERR sin hacer";    }     /** Atiende a un cliente: una respuesta por línea hasta ADIOS o hasta que cierre la conexión. */    static void atender(Socket socket) {        // TODO    }     public static void main(String[] args) throws Exception {        // TODO: arranca el servidor en un hilo, conecta el cliente y envía las líneas de la entrada    }}

152. Almacén de contraseñas seguro con PBKDF2 y sal

Difícil · 70 minutos · apuntesdam.com/ejercicios/java/almacen-de-contrasenas-con-pbkdf2

Guardar contraseñas en texto plano, o con un hash rápido como MD5 o SHA-256 sin más, es uno de los fallos de seguridad más graves y más frecuentes. Si alguien roba la base de datos, con un hash rápido puede probar miles de millones de contraseñas por segundo.

La forma correcta es usar una función de derivación lenta a propósito, como PBKDF2, bcrypt o Argon2, con una sal aleatoria distinta para cada usuario. La sal hace que dos usuarios con la misma contraseña tengan hashes distintos (y que no sirvan las tablas precalculadas), y las miles de iteraciones hacen que cada intento cueste tiempo.

El inicio de sesión también tiene sus reglas: comparar los hashes en tiempo constante, bloquear la cuenta tras varios fallos seguidos y responder lo mismo si el usuario no existe que si la contraseña es incorrecta, para que nadie pueda averiguar qué usuarios hay. Como la sal es aleatoria, el programa nunca escribe hashes: solo el resultado de cada operación.

Requisitos

  • Cada línea es una orden con sus argumentos separados por espacios (la orden en mayúsculas o minúsculas; las líneas vacías se ignoran). Antes de responder se escribe > ORDEN usuario: (la orden en mayúsculas y solo el primer argumento, nunca la contraseña) y después la respuesta en la misma línea. Una orden desconocida responde orden desconocida.

  • REGISTRA usuario contraseña: el usuario ya existe; o, si la contraseña no cumple la política, contraseña rechazada, motivo con el primer motivo: debe tener al menos 10 caracteres, debe tener mayúsculas, minúsculas y números, no puede contener el nombre de usuario (sin distinguir mayúsculas); si no, guarda una sal aleatoria de 16 bytes y el hash PBKDF2WithHmacSHA256 (10.000 iteraciones, 256 bits) y responde registrado.

  • ENTRA usuario contraseña: si el usuario no existe o la contraseña no es correcta, usuario o contraseña incorrectos; al tercer fallo seguido, usuario o contraseña incorrectos; cuenta bloqueada tras 3 fallos; con la cuenta bloqueada, cuenta bloqueada aunque la contraseña sea buena; si es correcta, bienvenido y el contador de fallos vuelve a 0.

  • CAMBIA usuario actual nueva: no se puede cambiar la contraseña si el usuario no existe, está bloqueado o la actual no es correcta; después, la política (contraseña rechazada, motivo) y la nueva contraseña debe ser distinta; si todo va bien, nueva sal y nuevo hash, y contraseña cambiada.

  • DESBLOQUEA usuario → desbloqueada (y fallos a 0) o no existe. COMPARA u1 u2 → ¿mismo hash que u2? sí/no o no existe alguno de los usuarios. INFO usuario → PBKDF2WithHmacSHA256, 10000 iteraciones, sal de 16 bytes, hash de 32 bytes (con , bloqueada al final si lo está) o no existe.

  • Una orden con un número de argumentos que no es el suyo responde uso: … con su formato (uso: REGISTRA usuario contraseña, uso: ENTRA usuario contraseña, uso: CAMBIA usuario actual nueva).

  • Las contraseñas se comparan con MessageDigest.isEqual, nunca con Arrays.equals ni comparando textos.

Formato de la entrada

  • Una orden por línea: REGISTRA, ENTRA, CAMBIA, DESBLOQUEA, COMPARA o INFO.

Ejemplo: Registro, inicio de sesión y bloqueo

Entrada

REGISTRA ana Corta1
REGISTRA ana sinmayusculas123
REGISTRA ana MiAnaClave2026
REGISTRA ana Gatos.Tigres.2026
REGISTRA eva Gatos.Tigres.2026
REGISTRA ana OtraClave99999
COMPARA ana eva
ENTRA ana gatos.tigres.2026
ENTRA nadie Gatos.Tigres.2026
ENTRA ana Gatos.Tigres.2026
ENTRA eva mal
ENTRA eva mal
ENTRA eva mal
ENTRA eva Gatos.Tigres.2026
INFO eva

Salida esperada

> REGISTRA ana: contraseña rechazada, debe tener al menos 10 caracteres
> REGISTRA ana: contraseña rechazada, debe tener mayúsculas, minúsculas y números
> REGISTRA ana: contraseña rechazada, no puede contener el nombre de usuario
> REGISTRA ana: registrado
> REGISTRA eva: registrado
> REGISTRA ana: el usuario ya existe
> COMPARA ana: ¿mismo hash que eva? no
> ENTRA ana: usuario o contraseña incorrectos
> ENTRA nadie: usuario o contraseña incorrectos
> ENTRA ana: bienvenido
> ENTRA eva: usuario o contraseña incorrectos
> ENTRA eva: usuario o contraseña incorrectos
> ENTRA eva: usuario o contraseña incorrectos; cuenta bloqueada tras 3 fallos
> ENTRA eva: cuenta bloqueada
> INFO eva: PBKDF2WithHmacSHA256, 10000 iteraciones, sal de 16 bytes, hash de 32 bytes, bloqueada

Ejemplo: Cambio de contraseña y desbloqueo

Entrada

REGISTRA luis Montañas.2026
CAMBIA luis Equivocada1 Nueva.Clave.27
CAMBIA luis Montañas.2026 Montañas.2026
CAMBIA luis Montañas.2026 corta
CAMBIA luis Montañas.2026 Nueva.Clave.27
ENTRA luis Montañas.2026
ENTRA luis Nueva.Clave.27
DESBLOQUEA luis
INFO luis

Salida esperada

> REGISTRA luis: registrado
> CAMBIA luis: no se puede cambiar la contraseña
> CAMBIA luis: la nueva contraseña debe ser distinta
> CAMBIA luis: contraseña rechazada, debe tener al menos 10 caracteres
> CAMBIA luis: contraseña cambiada
> ENTRA luis: usuario o contraseña incorrectos
> ENTRA luis: bienvenido
> DESBLOQUEA luis: desbloqueada
> INFO luis: PBKDF2WithHmacSHA256, 10000 iteraciones, sal de 16 bytes, hash de 32 bytes
Código de partida (java)
import java.security.MessageDigest;import java.security.SecureRandom;import java.util.HashMap;import java.util.Map;import java.util.Scanner;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec; public class Main {    static final String ALGORITMO = "PBKDF2WithHmacSHA256";    static final int ITERACIONES = 10_000;    // en producción, cientos de miles: aquí menos para que las pruebas vayan rápido    static final int MAX_FALLOS = 3;     static class Cuenta {        byte[] sal;        byte[] hash;        int fallos;        boolean bloqueada;    }     static final Map<String, Cuenta> cuentas = new HashMap<>();    static final SecureRandom azar = new SecureRandom();     static byte[] derivar(String contrasena, byte[] sal) throws Exception {        PBEKeySpec spec = new PBEKeySpec(contrasena.toCharArray(), sal, ITERACIONES, 256);        return SecretKeyFactory.getInstance(ALGORITMO).generateSecret(spec).getEncoded();    }     public static void main(String[] args) throws Exception {        Scanner sc = new Scanner(System.in);        // TODO: REGISTRA, ENTRA, CAMBIA, DESBLOQUEA, COMPARA e INFO    }}

153. Depura el programa de notas: cinco errores escondidos

Medio · 45 minutos · apuntesdam.com/ejercicios/java/depura-el-calculo-de-notas

Un compañero ha escrito el programa que calcula las notas finales de Programación y lo ha dejado para que lo revises antes de publicar las notas. Compila, no lanza excepciones y con su ejemplo de prueba (tres alumnos con notas «normales») el resultado parecía razonable. Pero varios alumnos se han quejado de que su nota no cuadra.

La regla de evaluación es: cada una de las dos prácticas vale un 20 % y el examen un 60 %; si el examen tiene menos de un 4, la nota final no puede pasar de 4 (el examen no se compensa), y la nota se redondea a un decimal. Aprueba quien tiene un 5 o más.

El código del editor tiene exactamente cinco errores, cada uno de una línea. No está permitido reescribir el programa: hay que encontrar cada error y corregirlo. Es lo que se hace en un proyecto real, donde reescribir lo que no se entiende introduce errores nuevos.

Requisitos

  • La entrada tiene una línea por alumno: nombre;práctica1;práctica2;examen, con notas de 0 a 10 con punto decimal.

  • Nota final = 0,2 × práctica1 + 0,2 × práctica2 + 0,6 × examen; si el examen es menor que 4, la nota final es como mucho 4; se redondea a un decimal (las mitades suben).

  • Se escribe una línea por alumno, de mayor a menor nota (a igualdad, por nombre): el nombre en 20 caracteres a la izquierda y la nota con un decimal y coma en 5 caracteres a la derecha.

  • Al final, Media: X (la media de las notas finales con un decimal) y Aprobados: A de T.

  • Corrige los cinco errores del programa sin cambiar su estructura. Las pruebas ocultas están pensadas para detectar cada uno.

Formato de la entrada

  • Una línea por alumno: nombre;práctica1;práctica2;examen.

Ejemplo: Una clase de cinco alumnos

Entrada

Ana Gil;8;9;7.5
Luis Mora;6;5;4
Eva Ruiz;10;10;3.5
Pablo Sanz;5;5;5
Marta Peña;7.5;6.5;6.2

Salida esperada

Ana Gil               7,9
Marta Peña            6,5
Pablo Sanz            5,0
Luis Mora             4,6
Eva Ruiz              4,0
Media: 5,6
Aprobados: 3 de 5

Ejemplo: El examen de 4 sí compensa

Entrada

Irene Vidal;9;9;4
Jorge Lara;3;3;3.9

Salida esperada

Irene Vidal           6,0
Jorge Lara            3,5
Media: 4,8
Aprobados: 1 de 2
Código de partida (java)
import java.util.ArrayList;import java.util.List;import java.util.Locale;import java.util.Scanner; public class Main {    record Alumno(String nombre, double nota) {}     /** Nota final: prácticas al 20 % cada una y examen al 60 %; con menos de un 4 en el examen, como mucho un 4. */    static double notaFinal(double practica1, double practica2, double examen) {        double nota = 0.2 * practica1 + 0.2 * practica1 + 0.6 * examen;        if (examen <= 4) nota = Math.min(nota, 4);        return Math.round(nota * 10) / 10;    }     static String uno(double x) {        return String.format(Locale.ROOT, "%.1f", x).replace('.', ',');    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String> lineas = new ArrayList<>();        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (!l.isEmpty()) lineas.add(l);        }         List<Alumno> alumnos = new ArrayList<>();        for (int i = 1; i < lineas.size(); i++) {            String[] p = lineas.get(i).split(";");            alumnos.add(new Alumno(p[0], notaFinal(Double.parseDouble(p[1]), Double.parseDouble(p[2]), Double.parseDouble(p[3]))));        }        alumnos.sort((a, b) -> a.nota() != b.nota() ? Double.compare(b.nota(), a.nota()) : a.nombre().compareTo(b.nombre()));         double suma = 0;        int aprobados = 0;        for (Alumno a : alumnos) {            System.out.printf("%-20s%5s%n", a.nombre(), uno(a.nota()));            suma += a.nota();            if (a.nota() > 5) aprobados++;        }        System.out.println("Media: " + uno(suma / alumnos.size()));        System.out.println("Aprobados: " + aprobados + " de " + alumnos.size());    }}