Nóminas de una plantilla con distintos contratos
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
- 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 %) yBECARIO 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,Netoy 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 filaTOTALcon 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) yMayor neto: nombre (X €)(el primero si hay empate). Si no hay ningún empleado válido, después de los avisos soloNo hay nóminas que calcular.
Entrada
Una línea por empleado: TIPO nombre;dato;dato….
Datos de referencia
| Bruto mensual | Tipo |
|---|---|
| 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.
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.
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.
Ejemplo
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
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 €)
Solución explicada
Ver la solución completa
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.