Apuntes DAM
Volver al inicio

Nóminas de una plantilla con distintos contratos

Ejercicio de JavaDifícilUnos 80 minutos

Nóminas del mes de asalariados, empleados por horas con horas extra, comerciales con comisión y becarios: bruto, retención, cotización y neto en una tabla alineada, con totales y coste para la empresa. Interfaces con métodos por defecto, clases abstractas, herencia y redondeos con enteros.

  • Interfaces con métodos default
  • Clases abstractas
  • Herencia y @Override
  • Polimorfismo
  • Excepciones para validar
  • printf con anchos

Enunciado

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.

Qué tiene que hacer el programa

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Todos los cálculos se hacen en céntimos y cada porcentaje se redondea al céntimo más cercano (las mitades suben).
  6. 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).
  7. 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.

Entrada

Una línea por empleado: TIPO nombre;dato;dato….

Datos de referencia

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 %

Ejemplos de ejecución

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

Una 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 por consola

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 €)

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 por consola

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 €)

Guía paso a paso

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

1. La interfaz con lo común

Declara en Retribuible el método abstracto bruto() y, como métodos default, la retención, la Seguridad Social y el neto, que se calculan a partir del bruto. Cualquier clase que implemente bruto() tendrá el resto gratis.

java
interface Retribuible {
    long bruto();

    default long seguridadSocial() {
        return Main.porcentaje(bruto(), 650);   // 6,5 %
    }
    ...
}
2. Porcentajes exactos con enteros

Guarda los porcentajes en centésimas (6,5 % → 650) y calcula (cantidad × centésimas + 5000) / 10000: sumar la mitad del divisor antes de dividir redondea al entero más cercano sin usar double.

3. Una clase por contrato

Cada subclase de Empleado guarda sus datos y escribe solo bruto() y tipo(). Becario además sobrescribe retencion() con @Override para devolver 0: el resto de métodos por defecto siguen funcionando y usan esa retención.

4. Validar con excepciones

Escribe un método leer(linea) que devuelva el empleado o lance IllegalArgumentException con el motivo. En el bucle, un try/catch escribe el aviso con ese mensaje: así cada comprobación es una línea y el bucle no se llena de if.

5. La tabla con printf

Un único formato para la cabecera, las filas y el total mantiene las columnas alineadas: %-20s alinea a la izquierda en 20 caracteres y %11s, a la derecha en 11.

java
String formato = "%-20s%-12s%11s%11s%11s%11s%n";
System.out.printf(formato, "Empleado", "Tipo", "Bruto", "IRPF", "SS", "Neto");

Resuélvelo aquí

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

☕JavaNóminas de una plantilla con distintos contratosDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
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 €)
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
java
1import java.util.ArrayList;
2import java.util.List;
3import java.util.Locale;
4import java.util.Scanner;
5
6interface Retribuible {
7    /** Bruto del mes, en céntimos. */
8    long bruto();
9
10    /** Retención de IRPF de la tabla simplificada de la empresa: un tipo según el tramo del bruto mensual. */
11    default long retencion() {
12        long b = bruto();
13        int centesimas = b <= 100000 ? 200 : b <= 200000 ? 1200 : b <= 300000 ? 1800 : 2400;
14        return Main.porcentaje(b, centesimas);
15    }
16
17    /** Cotización del trabajador a la Seguridad Social: un 6,5 % del bruto en este ejercicio. */
18    default long seguridadSocial() {
19        return Main.porcentaje(bruto(), 650);
20    }
21
22    default long neto() {
23        return bruto() - retencion() - seguridadSocial();
24    }
25}
26
27abstract class Empleado implements Retribuible {
28    private final String nombre;
29
30    Empleado(String nombre) { this.nombre = nombre; }
31
32    String getNombre() { return nombre; }
33
34    abstract String tipo();
35}
36
37class Asalariado extends Empleado {
38    private final long anual;
39    private final int pagas;
40
41    Asalariado(String nombre, long anual, int pagas) {
42        super(nombre);
43        this.anual = anual;
44        this.pagas = pagas;
45    }
46
47    String tipo() { return "asalariado"; }
48
49    // Redondeo al céntimo más cercano con enteros: (2a + p) / 2p
50    public long bruto() { return (anual * 2 + pagas) / (2L * pagas); }
51}
52
53class PorHoras extends Empleado {
54    private final int horas;
55    private final long precioHora;
56
57    PorHoras(String nombre, int horas, long precioHora) {
58        super(nombre);
59        this.horas = horas;
60        this.precioHora = precioHora;
61    }
62
63    String tipo() { return "por horas"; }
64
65    public long bruto() {
66        int normales = Math.min(horas, 160), extra = Math.max(0, horas - 160);
67        return normales * precioHora + Main.porcentaje(extra * precioHora, 12500);   // las extra, al 125 %
68    }
69}
70
71class Comercial extends Empleado {
72    private final long base, ventas, comision;   // comisión en centésimas de %
73
74    Comercial(String nombre, long base, long ventas, long comision) {
75        super(nombre);
76        this.base = base;
77        this.ventas = ventas;
78        this.comision = comision;
79    }
80
81    String tipo() { return "comercial"; }
82
83    public long bruto() { return base + Math.min(Main.porcentaje(ventas, comision), 200000); }
84}
85
86class Becario extends Empleado {
87    private final long ayuda;
88
89    Becario(String nombre, long ayuda) {
90        super(nombre);
91        this.ayuda = ayuda;
92    }
93
94    String tipo() { return "becario"; }
95
96    public long bruto() { return ayuda; }
97
98    // La beca no lleva retención en este ejercicio: se sobrescribe el método por defecto
99    @Override
100    public long retencion() { return 0; }
101}
102
103public class Main {
104    /** cantidad × centésimas / 10000, redondeado al céntimo (las mitades suben). */
105    static long porcentaje(long cantidad, long centesimas) {
106        return (cantidad * centesimas + 5000) / 10000;
107    }
108
109    /** «1234.5» → 123450; -1 si no es un número positivo con hasta 2 decimales. */
110    static long centimos(String texto) {
111        if (!texto.matches("\\d{1,9}(\\.\\d{1,2})?")) return -1;
112        String[] p = texto.split("\\.");
113        return Long.parseLong(p[0]) * 100 + (p.length == 2 ? Long.parseLong((p[1] + "0").substring(0, 2)) : 0);
114    }
115
116    static String euros(long c) {
117        return String.format(Locale.ROOT, "%,d", c / 100).replace(',', '.') + "," + String.format("%02d", c % 100);
118    }
119
120    /** El empleado de una línea, o una excepción con el motivo por el que se ignora. */
121    static Empleado leer(String linea) {
122        int espacio = linea.indexOf(' ');
123        if (espacio < 0) throw new IllegalArgumentException("faltan o sobran datos");
124        String tipo = linea.substring(0, espacio).toUpperCase();
125        String[] d = linea.substring(espacio + 1).split(";", -1);
126        int esperados = switch (tipo) {
127            case "ASALARIADO", "HORAS" -> 3;
128            case "COMERCIAL" -> 4;
129            case "BECARIO" -> 2;
130            default -> throw new IllegalArgumentException("tipo desconocido " + tipo);
131        };
132        if (d.length != esperados || d[0].isBlank()) throw new IllegalArgumentException("faltan o sobran datos");
133        String nombre = d[0].trim();
134        long[] n = new long[d.length];
135        for (int i = 1; i < d.length; i++) {
136            n[i] = centimos(d[i].trim());
137            if (n[i] < 0) throw new IllegalArgumentException("dato no válido");
138        }
139        switch (tipo) {
140            case "ASALARIADO" -> {
141                if (n[2] != 1200 && n[2] != 1400) throw new IllegalArgumentException("dato no válido");
142                return new Asalariado(nombre, n[1], (int) (n[2] / 100));
143            }
144            case "HORAS" -> {
145                if (n[1] % 100 != 0 || n[1] > 30000) throw new IllegalArgumentException("dato no válido");
146                return new PorHoras(nombre, (int) (n[1] / 100), n[2]);
147            }
148            case "COMERCIAL" -> {
149                if (n[3] > 2000) throw new IllegalArgumentException("dato no válido");
150                return new Comercial(nombre, n[1], n[2], n[3]);
151            }
152            default -> {
153                return new Becario(nombre, n[1]);
154            }
155        }
156    }
157
158    public static void main(String[] args) {
159        Scanner sc = new Scanner(System.in);
160        List<Empleado> plantilla = new ArrayList<>();
161        int numero = 0;
162        while (sc.hasNextLine()) {
163            String linea = sc.nextLine().trim();
164            numero++;
165            if (linea.isEmpty()) continue;
166            try {
167                plantilla.add(leer(linea));
168            } catch (IllegalArgumentException e) {
169                System.out.println("Línea " + numero + " ignorada: " + e.getMessage());
170            }
171        }
172        if (plantilla.isEmpty()) {
173            System.out.println("No hay nóminas que calcular.");
174            return;
175        }
176
177        String formato = "%-20s%-12s%11s%11s%11s%11s%n";
178        System.out.printf(formato, "Empleado", "Tipo", "Bruto", "IRPF", "SS", "Neto");
179        long bruto = 0, irpf = 0, ss = 0, neto = 0;
180        Empleado mejor = plantilla.get(0);
181        for (Empleado e : plantilla) {
182            // El mismo código sirve para cualquier empleado: cada clase sabe calcular su bruto
183            System.out.printf(formato, e.getNombre(), e.tipo(), euros(e.bruto()), euros(e.retencion()), euros(e.seguridadSocial()), euros(e.neto()));
184            bruto += e.bruto();
185            irpf += e.retencion();
186            ss += e.seguridadSocial();
187            neto += e.neto();
188            if (e.neto() > mejor.neto()) mejor = e;
189        }
190        System.out.printf(formato, "TOTAL", "", euros(bruto), euros(irpf), euros(ss), euros(neto));
191        System.out.println("Coste para la empresa: " + euros(bruto + porcentaje(bruto, 3000)) + " €");
192        System.out.println("Mayor neto: " + mejor.getNombre() + " (" + euros(mejor.neto()) + " €)");
193    }
194}

La interfaz Retribuible define el contrato (todo lo que se puede pagar tiene un bruto) y sus métodos por defecto implementan las reglas comunes. Las clases concretas solo dicen cómo se calcula su bruto: el código de las deducciones existe una sola vez.

Becario muestra para qué sirve sobrescribir un método por defecto: cambia una regla para un caso concreto, y neto(), que no se ha tocado, la usa automáticamente porque llama a retencion() a través del objeto.

La clase abstracta Empleado aporta lo que la interfaz no puede tener: un atributo (el nombre) con su constructor. Interfaz para el comportamiento, clase abstracta para el estado común: es una combinación muy habitual en Java.

Todo el dinero va en céntimos y cada porcentaje se redondea en un solo sitio, porcentaje. Así la tabla siempre cuadra: el neto de cada fila es exactamente su bruto menos sus deducciones, y los totales son la suma de las filas.

Para ir más allá

  • Aplica el IRPF por tramos de verdad (cada parte del sueldo a su tipo) en lugar de un tipo único, y compara los resultados.
  • Añade pagas extra: en junio y diciembre, los asalariados de 14 pagas cobran el doble.
  • Genera cada nómina como una página HTML imprimible con los datos de la empresa y del trabajador.

Dónde se explica