diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..71c5d0d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Java CI with Gradle + +on: + push: + branches: [ master2 ] + pull_request: + branches: [ master2 ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 # Checks out your repository under $GITHUB_WORKSPACE + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' # Or 'adopt', 'zulu', etc. + cache: 'gradle' # Caches Gradle dependencies + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build # This will also run the 'test' task by default + - name: Run Unit Tests + run: ./gradlew test # Explicitly run the test task if 'build' doesn't cover it \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6674195..3681a76 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("java") id("application") + id("jacoco") } group = "ru.urfu" @@ -13,10 +14,26 @@ repositories { dependencies { testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core:5.+") + implementation("org.jfree:jfreechart:1.5.6") +} + +jacoco { + toolVersion = "0.8.12" } tasks.test { useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) + reports { + xml.required = false + csv.required = false + html.outputLocation = layout.buildDirectory.dir("jacocoHtml") + } } application { diff --git a/src/main/java/ru/urfu/Main.java b/src/main/java/ru/urfu/Main.java index 5845f6d..fc357de 100644 --- a/src/main/java/ru/urfu/Main.java +++ b/src/main/java/ru/urfu/Main.java @@ -1,17 +1,29 @@ package ru.urfu; -//TIP To Run code, press or -// click the icon in the gutter. +import ru.urfu.chart.ChartDrawer; +import ru.urfu.chart.ChartMapper; +import ru.urfu.model.Player; +import ru.urfu.parser.CsvParser; +import ru.urfu.resolver.Resolver; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Random; +import java.util.function.Function; +import java.util.function.ToIntFunction; + public class Main { public static void main(String[] args) { //TIP Press with your caret at the highlighted text // to see how GIGA IDE suggests fixing it. - System.out.printf("Hello and welcome!"); + var parser = new CsvParser("/Users/denis/IdeaProjects/Java2025_2/players.csv"); + + Resolver resolver = new Resolver(parser); + + System.out.println(resolver.getTeams()); - for (int i = 1; i <= 5; i++) { - //TIP Press to start debugging your code. We have set one breakpoint - // for you, but you can always add more by pressing . - System.out.println("i = " + i); - } + var chartData = ChartMapper.mapDataToChart(parser.parseCsvToList()); + ChartDrawer.showChart(chartData); } } \ No newline at end of file diff --git a/src/main/java/ru/urfu/chart/ChartDrawer.java b/src/main/java/ru/urfu/chart/ChartDrawer.java new file mode 100644 index 0000000..f3add52 --- /dev/null +++ b/src/main/java/ru/urfu/chart/ChartDrawer.java @@ -0,0 +1,22 @@ +package ru.urfu.chart; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartFrame; +import org.jfree.chart.JFreeChart; +import org.jfree.data.xy.XYSeriesCollection; + +public class ChartDrawer { + public static void showChart(XYSeriesCollection dataset) { + JFreeChart chart = ChartFactory.createXYBarChart( + "Среднее количество голов по позициям", + "Позиция", + false, + "Среднее кол-во голов", + dataset + ); + + ChartFrame frame = new ChartFrame("Среднее число голов по позиции", chart); + frame.pack(); + frame.setVisible(true); + } +} diff --git a/src/main/java/ru/urfu/chart/ChartMapper.java b/src/main/java/ru/urfu/chart/ChartMapper.java new file mode 100644 index 0000000..56d3725 --- /dev/null +++ b/src/main/java/ru/urfu/chart/ChartMapper.java @@ -0,0 +1,43 @@ +package ru.urfu.chart; + +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; +import ru.urfu.model.Player; +import ru.urfu.model.Position; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ChartMapper { + public static XYSeriesCollection mapDataToChart(List players) { + Map> positionToGoals = new HashMap<>(); + + for (Player player : players) { + Position position = player.position(); + int goals = player.goals(); + + positionToGoals.computeIfAbsent(position, k -> new ArrayList<>()).add(goals); + } + + XYSeriesCollection dataset = new XYSeriesCollection(); + + int seriesIndex = 0; + for (Map.Entry> entry : positionToGoals.entrySet()) { + Position position = entry.getKey(); + List cardsList = entry.getValue(); + + double average = cardsList.stream() + .mapToInt(Integer::intValue) + .average() + .orElse(0.0); + + XYSeries series = new XYSeries(position); + series.add(seriesIndex++, average); + dataset.addSeries(series); + } + + return dataset; + } +} diff --git a/src/main/java/ru/urfu/model/Player.java b/src/main/java/ru/urfu/model/Player.java new file mode 100644 index 0000000..eda6786 --- /dev/null +++ b/src/main/java/ru/urfu/model/Player.java @@ -0,0 +1,8 @@ +package ru.urfu.model; + +public record Player( + String name, + Position position, + String agency, + int goals +) { } diff --git a/src/main/java/ru/urfu/model/Position.java b/src/main/java/ru/urfu/model/Position.java new file mode 100644 index 0000000..0df3c0e --- /dev/null +++ b/src/main/java/ru/urfu/model/Position.java @@ -0,0 +1,8 @@ +package ru.urfu.model; + +public enum Position { + GOALKEEPER, + DEFENDER, + MIDFIELD, + FORWARD +} diff --git a/src/main/java/ru/urfu/parser/CsvParser.java b/src/main/java/ru/urfu/parser/CsvParser.java new file mode 100644 index 0000000..3ca7cbe --- /dev/null +++ b/src/main/java/ru/urfu/parser/CsvParser.java @@ -0,0 +1,39 @@ +package ru.urfu.parser; + +import ru.urfu.model.Player; +import ru.urfu.model.Position; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +public class CsvParser { + private final String path; + + public CsvParser(String path) { + this.path = path; + } + + public List parseCsvToList() { + try { + return Files.readAllLines(Paths.get(path)) + .stream() + .skip(1) + .map(CsvParser::parsePlayerRow) + .toList(); + } catch (IOException e) { + return List.of(); + } + } + + private static Player parsePlayerRow(String row) { + var cells = row.split(";"); + return new Player( + cells[0], + Position.valueOf(cells[3]), + cells[5], + Integer.parseInt(cells[8]) + ); + } +} diff --git a/src/main/java/ru/urfu/resolver/IResolver.java b/src/main/java/ru/urfu/resolver/IResolver.java index c806b3a..f158d30 100644 --- a/src/main/java/ru/urfu/resolver/IResolver.java +++ b/src/main/java/ru/urfu/resolver/IResolver.java @@ -1,15 +1,36 @@ package ru.urfu.resolver; +import ru.urfu.model.Player; + +import java.util.List; +import java.util.Map; +import java.util.Set; + public interface IResolver { + + List getPlayers(); + // Выведите количество игроков, интересы которых не представляет агентство. int getCountWithoutAgency(); - // Выведите максимальное число голов, забитых защинтником. + // Выведите максимальное число голов, забитых защитником. int getMaxDefenderGoalsCount(); // Выведите русское название позиции самого дорогого немецкого игрока. String getTheExpensiveGermanPlayerPosition(); + // Верните имена игроков, сгруппированных по позициям, на которых они играют. + Map getPlayersByPosition(); + + // Верните множество команд, которые представлены в чемпионате. + Set getTeams(); + + // Верните топ-5 команд по количеству забитых мячей, и количество этих мячей. + Map getTop5TeamsByGoalsCount(); + + // Верните агентство, сумма игроков которого наименьшая. + String getAgencyWithMinPlayersCount(); + // Выберите команду с наибольшим средним числом удалений на одного игрока. String getTheRudestTeam(); } diff --git a/src/main/java/ru/urfu/resolver/Resolver.java b/src/main/java/ru/urfu/resolver/Resolver.java new file mode 100644 index 0000000..898c042 --- /dev/null +++ b/src/main/java/ru/urfu/resolver/Resolver.java @@ -0,0 +1,72 @@ +package ru.urfu.resolver; + +import ru.urfu.model.Player; +import ru.urfu.model.Position; +import ru.urfu.parser.CsvParser; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class Resolver implements IResolver { + private final List players; + + public Resolver(CsvParser parser) { + this.players = parser.parseCsvToList(); + } + + @Override + public List getPlayers() { + return players; + } + + @Override + public int getCountWithoutAgency() { + return (int) players.stream() + .filter(player -> player.agency() == null || player.agency().isEmpty()) + .count(); + } + + @Override + public int getMaxDefenderGoalsCount() { + return players.stream() + .filter(player -> player.position() == Position.DEFENDER) + .mapToInt(Player::goals) + .max() + .orElse(0); + } + + @Override + public String getTheExpensiveGermanPlayerPosition() { + return ""; + } + + @Override + public Map getPlayersByPosition() { + return Map.of(); + } + + @Override + public Set getTeams() { + return players.stream() + .map(Player::agency) + .filter(agency -> agency != null && !agency.isEmpty()) + .collect(Collectors.toSet()); + } + + @Override + public Map getTop5TeamsByGoalsCount() { + return Map.of(); + } + + @Override + public String getAgencyWithMinPlayersCount() { + return ""; + } + + @Override + public String getTheRudestTeam() { + return ""; + } +} diff --git a/src/test/java/ru/urfu/resolver/ResolverTest.java b/src/test/java/ru/urfu/resolver/ResolverTest.java new file mode 100644 index 0000000..ca953b3 --- /dev/null +++ b/src/test/java/ru/urfu/resolver/ResolverTest.java @@ -0,0 +1,185 @@ +package ru.urfu.resolver; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import ru.urfu.model.Player; +import ru.urfu.model.Position; +import ru.urfu.parser.CsvParser; + +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ResolverTest { + private static final CsvParser mockParser = mock(CsvParser.class); + + @Test + public void testOneDefender() { + when(mockParser.parseCsvToList()) + .thenReturn(List.of(new Player("Иванов Иван", Position.DEFENDER, "", 10))); + + var resolver = new Resolver(mockParser); + + assertEquals(10, resolver.getMaxDefenderGoalsCount()); + } + + @Test + public void testNoDefender() { + when(mockParser.parseCsvToList()) + .thenReturn(List.of(new Player("Иванов Иван", Position.FORWARD, "", 10))); + + var resolver = new Resolver(mockParser); + + assertEquals(0, resolver.getMaxDefenderGoalsCount()); + } + + @Test + public void testNoPlayers() { + when(mockParser.parseCsvToList()) + .thenReturn(List.of()); + + var resolver = new Resolver(mockParser); + + assertEquals(0, resolver.getMaxDefenderGoalsCount()); + } + + @Test + void testGetCountWithoutAgency_ZeroPlayers() { + when(mockParser.parseCsvToList()).thenReturn(List.of()); + + var resolver = new Resolver(mockParser); + int count = resolver.getCountWithoutAgency(); + + assertEquals(0, count); + } + + + @Test + void testGetCountWithoutAgency_AllHaveAgency() { + List players = List.of( + new Player("Иван", Position.FORWARD, "Агентство1", 1), + new Player("Петр", Position.DEFENDER, "Агентство2", 2) + ); + when(mockParser.parseCsvToList()).thenReturn(players); + + var resolver = new Resolver(mockParser); + int count = resolver.getCountWithoutAgency(); + + assertEquals(0, count); + } + + @Test + void testGetCountWithoutAgency_SomeWithoutAgency() { + List players = List.of( + new Player("Иван", Position.FORWARD, null, 1), + new Player("Петр", Position.DEFENDER, "", 2), + new Player("Сергей", Position.MIDFIELD, "Агентство", 3) + ); + when(mockParser.parseCsvToList()).thenReturn(players); + + var resolver = new Resolver(mockParser); + int count = resolver.getCountWithoutAgency(); + + assertEquals(2, count); // Иван (null) + Петр (пустая строка) + } + + @Test + void testGetCountWithoutAgency_AllWithoutAgency() { + List players = List.of( + new Player("Иван", Position.FORWARD, null, 1), + new Player("Петр", Position.DEFENDER, "", 2) + ); + when(mockParser.parseCsvToList()).thenReturn(players); + + var resolver = new Resolver(mockParser); + int count = resolver.getCountWithoutAgency(); + + assertEquals(2, count); + } + + @Test + void testGetCountWithoutAgency_MixedNullAndEmpty() { + List players = List.of( + new Player("Иван", Position.FORWARD, null, 1), + new Player("Петр", Position.DEFENDER, "", 2), + new Player("Сергей", Position.MIDFIELD, " ", 3), // пробелы — не пусто! + new Player("Анна", Position.FORWARD, "Агентство", 4) + ); + when(mockParser.parseCsvToList()).thenReturn(players); + + var resolver = new Resolver(mockParser); + int count = resolver.getCountWithoutAgency(); + + assertEquals(2, count); + } + + @ParameterizedTest + @MethodSource("provideTestData") + void testGetTeams(List players, Set expected) { + when(mockParser.parseCsvToList()).thenReturn(players); + + var resolver = new Resolver(mockParser); + Set result = resolver.getTeams(); + + assertEquals(expected, result); + } + + private static Stream provideTestData() { + return Stream.of( + // Сценарий 1: пустой список игроков + Arguments.of( + List.of(), + Set.of() + ), + + // Сценарий 2: один игрок — одна команда + Arguments.of( + List.of(new Player("Иван", Position.FORWARD, "Barcelona", 1)), + Set.of("Barcelona") + ), + + // Сценарий 3: несколько игроков одной команды + Arguments.of( + List.of( + new Player("Иван", Position.FORWARD, "Barcelona", 1), + new Player("Петр", Position.DEFENDER, "Barcelona", 2) + ), + Set.of("Barcelona") + ), + + // Сценарий 4: игроки из разных команд + Arguments.of( + List.of( + new Player("Иван", Position.FORWARD, "Barcelona", 1), + new Player("Петр", Position.DEFENDER, "Real Madrid", 2), + new Player("Сергей", Position.MIDFIELD, "Barcelona", 3) + ), + Set.of("Barcelona", "Real Madrid") + ), + + // Сценарий 5: один игрок без агентства (null) + Arguments.of( + List.of( + new Player("Иван", Position.FORWARD, null, 1) + ), + Set.of() + ), + + // Сценарий 6: с null и пустой строкой + Arguments.of( + List.of( + new Player("Иван", Position.FORWARD, null, 1), + new Player("Петр", Position.DEFENDER, "", 2), + new Player("Сергей", Position.MIDFIELD, "Barcelona", 3) + ), + Set.of("Barcelona") + ) + ); + } +}