Clasificación de una liga de fútbol
Lee los resultados de una liga, calcula partidos, victorias, goles y puntos de cada equipo, y muestra la clasificación ordenada con cuatro criterios de desempate y columnas alineadas. Validación de datos, Map, Comparator y String.format.
- Clases
- LinkedHashMap
- Comparator encadenado
- split y matches
- String.format
- Validación de la entrada
Enunciado
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.
Qué tiene que hacer el programa
- 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) yEquipo 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.
Entrada
Una línea por partido: local;goles-goles;visitante. Puede haber líneas vacías y líneas mal escritas.
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 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 por consola
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)
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 por consola
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)
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. El modelo: una clase Equipo
Cada equipo acumula sus estadísticas. Los puntos y la diferencia de goles no hace falta guardarlos: se calculan a partir de los demás campos con dos métodos (puntos() y dg()).
2. Leer y validar
Separa con split(";", -1) (el -1 conserva las partes vacías, para que ;; dé tres partes vacías en lugar de ninguna) y comprueba las condiciones del enunciado. Para el marcador, matches("[0-9]+-[0-9]+") dice si son dos números separados por un guion.
String[] partes = linea.split(";", -1);
if (!esValida(partes)) {
System.out.println("Aviso: la línea " + numLinea + " no es un resultado válido y se ignora");
continue;
}3. Registrar un partido en los dos equipos
Un partido cuenta para el local y para el visitante, con los goles cruzados. Si escribes un método registrar(equipo, golesAFavor, golesEnContra), lo llamas dos veces: registrar(local, gl, gv) y registrar(visitante, gv, gl). Para obtener el objeto de un equipo (creándolo si es la primera vez que aparece) usa computeIfAbsent.
4. Ordenar con varios criterios
Comparator permite encadenar criterios con thenComparing. Los tres primeros son de mayor a menor, así que llevan reversed(); el nombre va de menor a mayor.
tabla.sort(Comparator.comparingInt(Equipo::puntos).reversed()
.thenComparing(Comparator.comparingInt(Equipo::dg).reversed())
.thenComparing(Comparator.comparingInt((Equipo x) -> x.gf).reversed())
.thenComparing((Equipo x) -> x.nombre));5. Formatear la salida
%-20s rellena el texto por la derecha hasta 20 caracteres y %3d alinea un número a la derecha en 3. La diferencia de goles se convierte antes a texto para poder ponerle el «+». Para la media con coma decimal, String.format(ES, "%.2f", …) con el Locale español.
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
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
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)
Solución explicada
Ver la solución completa
1import java.util.*;
2
3public class Main {
4 static class Equipo {
5 final String nombre;
6 int pj, g, e, p, gf, gc;
7
8 Equipo(String nombre) {
9 this.nombre = nombre;
10 }
11
12 int dg() {
13 return gf - gc;
14 }
15
16 int puntos() {
17 return 3 * g + e;
18 }
19 }
20
21 static final Locale ES = Locale.forLanguageTag("es-ES");
22 // LinkedHashMap: busca por nombre al instante y recuerda el orden de aparición
23 static final Map<String, Equipo> equipos = new LinkedHashMap<>();
24
25 public static void main(String[] args) {
26 Scanner sc = new Scanner(System.in);
27 int numLinea = 0, partidos = 0, goles = 0;
28 while (sc.hasNextLine()) {
29 String linea = sc.nextLine();
30 numLinea++;
31 if (linea.isBlank()) continue;
32 String[] partes = linea.split(";", -1);
33 if (!esValida(partes)) {
34 System.out.println("Aviso: la línea " + numLinea + " no es un resultado válido y se ignora");
35 continue;
36 }
37 String[] marcador = partes[1].trim().split("-");
38 int golesLocal = Integer.parseInt(marcador[0]);
39 int golesVisitante = Integer.parseInt(marcador[1]);
40 registrar(equipo(partes[0].trim()), golesLocal, golesVisitante);
41 registrar(equipo(partes[2].trim()), golesVisitante, golesLocal);
42 partidos++;
43 goles += golesLocal + golesVisitante;
44 }
45 if (partidos == 0) {
46 System.out.println("No hay resultados válidos.");
47 return;
48 }
49 List<Equipo> tabla = new ArrayList<>(equipos.values());
50 tabla.sort(Comparator.comparingInt(Equipo::puntos).reversed()
51 .thenComparing(Comparator.comparingInt(Equipo::dg).reversed())
52 .thenComparing(Comparator.comparingInt((Equipo x) -> x.gf).reversed())
53 .thenComparing((Equipo x) -> x.nombre));
54 imprimirTabla(tabla);
55 System.out.println();
56 System.out.println(String.format(ES, "Partidos: %d · Goles: %d · Media: %.2f goles por partido", partidos, goles, (double) goles / partidos));
57 // El primero de la tabla con menos goles en contra: así un empate lo gana el mejor clasificado
58 Equipo menos = tabla.get(0);
59 for (Equipo t : tabla) {
60 if (t.gc < menos.gc) menos = t;
61 }
62 System.out.println("Equipo menos goleado: " + menos.nombre + " (" + menos.gc + (menos.gc == 1 ? " gol" : " goles") + " en contra)");
63 }
64
65 static boolean esValida(String[] partes) {
66 return partes.length == 3
67 && !partes[0].isBlank() && !partes[2].isBlank()
68 && partes[1].trim().matches("[0-9]+-[0-9]+")
69 && !partes[0].trim().equals(partes[2].trim());
70 }
71
72 static Equipo equipo(String nombre) {
73 return equipos.computeIfAbsent(nombre, Equipo::new);
74 }
75
76 static void registrar(Equipo t, int aFavor, int enContra) {
77 t.pj++;
78 t.gf += aFavor;
79 t.gc += enContra;
80 if (aFavor > enContra) t.g++;
81 else if (aFavor == enContra) t.e++;
82 else t.p++;
83 }
84
85 static void imprimirTabla(List<Equipo> tabla) {
86 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"));
87 for (int i = 0; i < tabla.size(); i++) {
88 Equipo t = tabla.get(i);
89 String dg = t.dg() > 0 ? "+" + t.dg() : String.valueOf(t.dg());
90 System.out.println(String.format("%-4d%-20s%3d%3d%3d%3d%4d%4d%5s%5d", i + 1, t.nombre, t.pj, t.g, t.e, t.p, t.gf, t.gc, dg, t.puntos()));
91 }
92 }
93}La clase Equipo guarda solo los datos que cambian con cada partido; los puntos y la diferencia de goles se calculan con métodos, así nunca pueden quedar desincronizados.
Un LinkedHashMap permite buscar un equipo por su nombre al instante y recuerda el orden de aparición. computeIfAbsent crea el equipo la primera vez que aparece y lo devuelve las siguientes.
La validación usa split(";", -1) para no perder partes vacías y una expresión regular sencilla para el marcador, así que Integer.parseInt nunca recibe algo que no sea un número.
La ordenación encadena cuatro comparadores: puntos, diferencia de goles y goles a favor de mayor a menor (reversed), y el nombre en orden alfabético. Al recorrer la tabla ya ordenada para buscar al menos goleado, el primero que se encuentra con el mínimo es el mejor clasificado, que es lo que pide el enunciado para los empates.
Para ir más allá
- Añade una columna con la racha de los últimos tres partidos (por ejemplo
GEP). - Lee los resultados de un fichero cuyo nombre se pasa como argumento y escribe la clasificación en otro fichero CSV.
- Añade el criterio de desempate del enfrentamiento directo: entre dos equipos empatados a puntos, va delante el que ganó el partido entre ellos.