diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a356f3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +solution/target/ \ No newline at end of file diff --git a/README.md b/README.md index ee9d69b..f5b6b9d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,66 @@ 4. Найденный в соответствии с условием задачи месяц должен выводиться на английском языке в нижнем регистре. Если месяцев несколько, то на вывод они все подаются на английском языке в нижнем регистре в порядке их следования в течение года. ## Автор решения - +Васильков Александр Сергеевич ## Описание реализации +Описание основных классов и их назначения: + +_Main.java_: + +- Этот класс является главным классом приложения. +- В методе main происходит обработка аргументов командной строки, чтение данных из JSON-файла, инициализация объектов, необходимых для работы с JSON (ObjectMapper), вызов метода solve из класса Solution для обработки заказов и вывод результата. + + _Solution.java_: + +- Этот класс отвечает за решение задачи по обработке заказов. +- В конструкторе Solution принимаются массив заказов и статус заказа для фильтрации. +- Метод solve осуществляет фильтрацию заказов по статусу, суммирование общей суммы заказов по месяцам и поиск месяцев с максимальным оборотом. +- Вспомогательные методы getMonthlyMaxValue, findOrderedMonthsEqualToMaxTotal и filterOrdersAndSumByMonthTotal используются для выполнения отдельных частей задачи. + + _OrderModels.java_: + +- Этот класс содержит модели данных для заказов. +- Определяется перечисление Status для статусов заказа. +- Внутри класса определены две записи: Order для хранения данных о заказе и Result для хранения результата обработки заказов. + +Алгоритм работы программы : +- Программа получает имя JSON-файла из аргументов командной строки. +- Путь к файлу определяется с помощью вспомогательного класса FileUtils. +- Создается экземпляр ObjectMapper, который используется для чтения данных из JSON-файла в массив объектов заказов. +- Создается экземпляр класса Solution, который принимает массив заказов и статус заказа для фильтрации. +- В методе solve заказы фильтруются по статусу "COMPLETED", суммируется общая сумма заказов по месяцам, находится месяц с максимальным оборотом и возвраащется результат(месяцы), который предварительно отсортирвоан в порядке следования месяцев в течение года. +- Результат обработки заказов преобразуется в формат JSON. JSON-строка выводится в стандартный вывод. +- Программа перехватывает возможные исключения выводит соответствующие сообщения об ошибке. + + ## Инструкция по сборке и запуску решения +## Windows +> Предварительно: `Необходимо открыть консоль.` ВВодим команды представленные ниже. +```sh +git clone https://github.com/Ioutcast/school2024-test-task1.git +cd .\school2024-test-task1\solution\out +``` +Запуск программы с помощью скрипта. Вводим в консоль : +```sh +start.bat +``` +Запуск программы ручками. Вводим в консоль : +```sh +java -jar vasilkov-task-solution-jar-with-dependencies.jar input.json +``` +## Linux +> Предварительно: `Необходимо открыть консоль.` ВВодим команды представленные ниже. +```sh +git clone https://github.com/Ioutcast/school2024-test-task1.git +cd ./school2024-test-task1/solution/out +``` +Запуск программы с помощью скрипта. Вводим в консоль : +```sh +chmod +x start.sh +./start.sh +``` +Запуск программы ручками. Вводим в консоль : +```sh +java -jar vasilkov-task-solution-jar-with-dependencies.jar input.json +``` diff --git a/format.json b/solution/out/input.json similarity index 100% rename from format.json rename to solution/out/input.json diff --git a/solution/out/start.bat b/solution/out/start.bat new file mode 100644 index 0000000..1004725 --- /dev/null +++ b/solution/out/start.bat @@ -0,0 +1,5 @@ +@echo off +chcp 65001 +java -jar vasilkov-task-solution-jar-with-dependencies.jar input.json +echo. +pause \ No newline at end of file diff --git a/solution/out/start.sh b/solution/out/start.sh new file mode 100644 index 0000000..f1a35a5 --- /dev/null +++ b/solution/out/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash +java -jar vasilkov-task-solution-jar-with-dependencies.jar input.json +echo "" \ No newline at end of file diff --git a/solution/out/vasilkov-task-solution-jar-with-dependencies.jar b/solution/out/vasilkov-task-solution-jar-with-dependencies.jar new file mode 100644 index 0000000..f9761f4 Binary files /dev/null and b/solution/out/vasilkov-task-solution-jar-with-dependencies.jar differ diff --git a/solution/pom.xml b/solution/pom.xml new file mode 100644 index 0000000..8bb1c07 --- /dev/null +++ b/solution/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + vasilkov + solution + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + 1.18.20 + 2.17.0 + 2.23.1 + 5.10.2 + + + + + org.projectlombok + lombok + ${lombok.version} + compile + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + + vasilkov.Main + + + + jar-with-dependencies + + out + vasilkov-task-solution + + + + make-assembly + package + + single + + + + + + + \ No newline at end of file diff --git a/solution/src/main/java/vasilkov/CustomObjectMapperProvider.java b/solution/src/main/java/vasilkov/CustomObjectMapperProvider.java new file mode 100644 index 0000000..6ffa553 --- /dev/null +++ b/solution/src/main/java/vasilkov/CustomObjectMapperProvider.java @@ -0,0 +1,37 @@ +package vasilkov; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.json.JsonWriteFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +/** + * Класс, предоставляющий настраиваемый объект ObjectMapper. + */ +public class CustomObjectMapperProvider implements ObjectMapperProvider { + + /** + * Получение настроенного объекта ObjectMapper. + * @return Настроенный объект ObjectMapper. + */ + @Override + public ObjectMapper getObjectMapper() { + + ObjectMapper mapper = new ObjectMapper(); + + // Регистрация модуля для работы с датами и временем в формате Java Time + mapper.registerModule(new JavaTimeModule()); + + mapper.disable(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature()); + + return mapper; + } + +} diff --git a/solution/src/main/java/vasilkov/FileUtils.java b/solution/src/main/java/vasilkov/FileUtils.java new file mode 100644 index 0000000..a88e15f --- /dev/null +++ b/solution/src/main/java/vasilkov/FileUtils.java @@ -0,0 +1,22 @@ +package vasilkov; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Утилитарный класс для работы с файлами. + */ +public class FileUtils { + + /** + * Получение пути к ресурсному файлу по его имени. + * @param resourceName Имя ресурсного файла. + * @return Путь к ресурсному файлу. + */ + public static String getResourceFilePath(String resourceName) { + // Формирование пути к ресурсному файлу + Path resourcePath = Paths.get(resourceName); + return resourcePath.toString(); + } + +} \ No newline at end of file diff --git a/solution/src/main/java/vasilkov/Main.java b/solution/src/main/java/vasilkov/Main.java new file mode 100644 index 0000000..462c0d9 --- /dev/null +++ b/solution/src/main/java/vasilkov/Main.java @@ -0,0 +1,83 @@ +package vasilkov; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.AccessDeniedException; +import java.nio.file.InvalidPathException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.util.*; + +/** + * Главный класс приложения для обработки заказов. + */ +public class Main { + // Задание статуса заказа по умолчанию + private static final OrderModels.Status status = OrderModels.Status.COMPLETED; + + private static final Logger logger = LogManager.getLogger(Main.class); + + /** + * Точка входа в программу. + * @param args Аргументы командной строки (имя JSON-файла). + */ + public static void main(String[] args) { + + if (args.length != 1) { + logger.error("Usage: java -jar ваш_файл.jar имя_файла.json"); + return; + } + + // Получение имени JSON-файла из аргументов командной строки + String fileName = args[0]; + + // Получение пути к файлу с данными + String filePath = FileUtils.getResourceFilePath(fileName); + + try { + + //Конфигурирование инструмента для чтения и вывода Json + ObjectMapperProvider objectMapperProvider = new CustomObjectMapperProvider(); + ObjectMapper mapper = objectMapperProvider.getObjectMapper(); + + // Чтение данных из файла и преобразование их в массив заказов + List orderArray = List.of( + mapper.readValue( + new File(filePath), + OrderModels.Order[].class + ) + ); + + Solution solution = new Solution(orderArray, status); + + // Получение результата решения и его преобразование в JSON + OrderModels.Result result = solution.solve(); + + String json = mapper.writeValueAsString(result); + + System.out.print(json); + + }catch (NoSuchFileException | AccessDeniedException | InvalidPathException e) { + logger.error("An IO error occurred while accessing the file: \n " + e.getMessage()); + }catch (InvalidFormatException e) { + logger.error("Invalid format inside file: \n" + e.getMessage()); + }catch (JsonMappingException e) { + logger.error("Error occurred during JSON mapping: \n" + e.getMessage()); + }catch (NullPointerException e) { + logger.error("NullPointerException occurred: \n" + e.getMessage()); + }catch (IOException e) { + logger.error("An IO error occurred while processing the file: \n" + e.getMessage()); + }catch (NoSuchElementException e) { + logger.error("Error occurred because file is empty! \nor\nThere NO!!! elements with \"status\": \"COMPLETED\"!"); + }catch (Exception e) { + logger.error("An unexpected error occurred: \n" + e.getMessage()); + } + } + +} diff --git a/solution/src/main/java/vasilkov/ObjectMapperProvider.java b/solution/src/main/java/vasilkov/ObjectMapperProvider.java new file mode 100644 index 0000000..52f63a4 --- /dev/null +++ b/solution/src/main/java/vasilkov/ObjectMapperProvider.java @@ -0,0 +1,14 @@ +package vasilkov; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Интерфейс, определяющий метод для предоставления объекта ObjectMapper. + */ +public interface ObjectMapperProvider { + /** + * Получение объекта ObjectMapper. + * @return Объект ObjectMapper. + */ + ObjectMapper getObjectMapper(); +} diff --git a/solution/src/main/java/vasilkov/OrderModels.java b/solution/src/main/java/vasilkov/OrderModels.java new file mode 100644 index 0000000..f88a930 --- /dev/null +++ b/solution/src/main/java/vasilkov/OrderModels.java @@ -0,0 +1,97 @@ +package vasilkov; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + +/** + * Модели данных для заказов. + */ +// Комментарий +// Не хотелось делать решение громоздким(иметь 10тки классов) +public class OrderModels { + /** + * Перечисление статусов заказа. + */ + public enum Status { COMPLETED, CANCELED, CREATED, DELIVERY } + + /** + * Запись данных о заказе. + */ + public record Order( + UUID user_id, // Идентификатор пользователя + LocalDateTime ordered_at, // Время размещения заказа + Status status, // Статус заказа + double total // Общая сумма заказа + ) {} + + /** + * Запись для хранения результата. + */ + @JsonSerialize(using = Result.CustomStringSerializer.class) + public record Result( + List months // Список месяцев в результате + ) { + /** + * Вспомогательный метод для получения имени переменной, содержащей список месяцев. + * @return Имя переменной списка месяцев. + */ + private static String getResultVariableName() { + return Result.class.getRecordComponents()[0].getName(); + } + + /** + * Кастомный сериализатор для преобразования списка месяцев в JSON формат с использованием специальных символов. + */ + public static class CustomStringSerializer extends StdSerializer { + public CustomStringSerializer() { + this(null); + } + public CustomStringSerializer(Class n) { + super(n); + } + + @Override + public void serialize(OrderModels.Result result, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + + final String fieldNamePrefix = "«"; + final String fieldNameSuffix = "»"; + final String fieldName = fieldNamePrefix + getResultVariableName() + fieldNameSuffix; + final String startArray = ": ["; + final String arrayDelimiter = ", "; + final String endArray = "]"; + + + jsonGenerator.writeStartObject(); + jsonGenerator.writeFieldName(fieldName); + + jsonGenerator.writeRaw(startArray); + + for ( + Iterator iterator = result.months().iterator(); + iterator.hasNext(); + ) + { + String month = iterator.next(); + jsonGenerator.writeRaw(fieldNamePrefix + month + fieldNameSuffix); + + if (iterator.hasNext()) { + jsonGenerator.writeRaw(arrayDelimiter); + } + } + + jsonGenerator.writeRaw(endArray); + + jsonGenerator.writeEndObject(); + + } + } + } +} diff --git a/solution/src/main/java/vasilkov/Solution.java b/solution/src/main/java/vasilkov/Solution.java new file mode 100644 index 0000000..824f450 --- /dev/null +++ b/solution/src/main/java/vasilkov/Solution.java @@ -0,0 +1,78 @@ +package vasilkov; + +import java.time.Month; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Класс, представляющий решение задачи по обработке заказов. + */ +public class Solution { + private final List orderArray; // Массив заказов + private final OrderModels.Status status; // Статус заказа для фильтрации + + /** + * Конструктор класса Solution. + * @param orderArray Массив заказов. + * @param status Статус заказа для фильтрации. + */ + public Solution(List orderArray, OrderModels.Status status) { + this.orderArray = orderArray; + this.status = status; + } + + /** + * Метод для решения задачи по обработке заказов. + * @return Результат обработки заказов. + * @throws NoSuchElementException Исключение, если не найден максимальный месячный оборот. + * @throws NullPointerException Исключение, если найдены пустые значения в данных о заказах. + */ + public OrderModels.Result solve() throws NoSuchElementException, NullPointerException{ + + Map monthlyTotal = filterOrdersAndSumByMonthTotal(); + + Double monthlyMaxTotal = getMonthlyMaxValue(monthlyTotal); + + List monthsWithMaxTotal = findOrderedMonthsEqualToMaxTotal(monthlyTotal, monthlyMaxTotal); + + return new OrderModels.Result(monthsWithMaxTotal); + } + + /** + * Получение максимального значения оборота за месяц. + * @param monthlyTotal Месячные обороты. + * @return Максимальное значение оборота за месяц. + * @throws NoSuchElementException Исключение, если не найден максимальный месячный оборот. + */ + private Double getMonthlyMaxValue(Map monthlyTotal) throws NoSuchElementException { + return Collections.max(monthlyTotal.values()); + } + + /** + * Поиск месяцев с максимальным оборотом. + * @param monthlyTotal Месячный оборот. + * @param maxValue Максимальное значение оборота за месяц. + * @return Список месяцев с максимальным оборотом. + */ + private List findOrderedMonthsEqualToMaxTotal(Map monthlyTotal, Double maxValue) { + return monthlyTotal.entrySet().stream() + .filter(entry -> entry.getValue().equals(maxValue)) + .map(Map.Entry::getKey) + .sorted(Comparator.comparing(m -> Month.valueOf(m.toUpperCase()))) + .collect(Collectors.toList()); + } + + /** + * Фильтрация заказов по статусу и суммирование общей суммы по месяцам. + * @return Месячный оборот. + */ + private Map filterOrdersAndSumByMonthTotal() { + return orderArray.stream() + .filter(it->it.status().equals(status)) + .collect(Collectors.groupingBy( + order -> order.ordered_at().getMonth().toString().toLowerCase(), + Collectors.summingDouble(OrderModels.Order::total) + ) + ); + } +} diff --git a/solution/src/main/resources/log4j2.xml b/solution/src/main/resources/log4j2.xml new file mode 100644 index 0000000..ef8213b --- /dev/null +++ b/solution/src/main/resources/log4j2.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/solution/src/test/java/MainTest.java b/solution/src/test/java/MainTest.java new file mode 100644 index 0000000..55f9ef7 --- /dev/null +++ b/solution/src/test/java/MainTest.java @@ -0,0 +1,81 @@ +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import vasilkov.Main; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Класс для тестирования класса Main. + */ +public class MainTest { + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + /** + * Устанавливает перенаправление стандартного вывода перед каждым тестом. + */ + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outputStream)); + } + + /** + * Восстанавливает стандартный вывод после каждого теста. + */ + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + } + + /** + * Проверяем при передаче корректного файла. + */ + @Test + public void testMainWithValidFile() { + + String correctFilePath = "src\\test\\resources\\format.json"; + String[] args = {correctFilePath}; + + Main.main(args); + + String expectedOutput = "{«months»: [«december»]}"; + + assertEquals(expectedOutput, outputStream.toString()); + + } + + /** + * Проверяем при передаче файла с некорректным форматом. + */ + @Test + public void testMainWithInvalidFormatFile() { + String filePath = "src\\test\\resources\\wrong_json.json"; + String[] args = {filePath}; + + Main.main(args); + + String expectedOutput = "Error occurred during JSON mapping:"; + assertTrue(outputStream.toString().contains(expectedOutput)); + } + + /** + * Проверяем при наличии более одного месяца в результате. + */ + @Test + public void testMainWithMoreThanOneMonthForResult() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + + String correctFilePath = "src\\test\\resources\\correct.json"; + String[] args = {correctFilePath}; + + Main.main(args); + + String expectedOutput = "{«months»: [«february», «march», «may»]}"; + assertEquals(expectedOutput, outputStream.toString()); + } +} diff --git a/solution/src/test/resources/correct.json b/solution/src/test/resources/correct.json new file mode 100644 index 0000000..dc8c8e6 --- /dev/null +++ b/solution/src/test/resources/correct.json @@ -0,0 +1,20 @@ +[ + { + "user_id": "3acfb0b7-04bd-4978-be4c-3929372277c1", + "ordered_at": "2023-05-16T13:56:39.492", + "status": "COMPLETED", + "total": "1500.00" + }, + { + "user_id": "25b003b9-ab22-4a24-a616-dd0303f983d8", + "ordered_at": "2023-02-05T08:34:21.123", + "status": "COMPLETED", + "total": "1500.00" + }, + { + "user_id": "e1470ada-fcbb-4424-8c46-065b6409ca4b", + "ordered_at": "2016-03-16T13:56:39.492", + "status": "COMPLETED", + "total": "1500.00" + } +] \ No newline at end of file diff --git a/solution/src/test/resources/format.json b/solution/src/test/resources/format.json new file mode 100644 index 0000000..94aad31 --- /dev/null +++ b/solution/src/test/resources/format.json @@ -0,0 +1,38 @@ +[ + { + "user_id": "3acfb0b7-04bd-4978-be4c-3929372277c1", + "ordered_at": "2023-01-16T13:56:39.492", + "status": "COMPLETED", + "total": "1917.00" + }, + { + "user_id": "25b003b9-ab22-4a24-a616-dd0303f983d8", + "ordered_at": "2023-03-05T08:34:21.123", + "status": "COMPLETED", + "total": "13990.00" + }, + { + "user_id": "e1470ada-fcbb-4424-8c46-065b6409ca4b", + "ordered_at": "2016-03-16T13:56:39.492", + "status": "COMPLETED", + "total": "215.50" + }, + { + "user_id": "081a47a5-b7bf-462c-a11a-68002a179152", + "ordered_at": "2023-12-08T21:36:59.281", + "status": "COMPLETED", + "total": "49499.00" + }, + { + "user_id": "0999c6aa-1bac-4ded-9a54-92fff4f34d69", + "ordered_at": "2023-12-14T11:10:29.408", + "status": "CANCELED", + "total": "13650.00" + }, + { + "user_id": "0999c6aa-1bac-4ded-9a54-92fff4f34d69", + "ordered_at": "2023-12-14T11:15:31.108", + "status": "COMPLETED", + "total": "14760.00" + } +] \ No newline at end of file diff --git a/solution/src/test/resources/wrong_json.json b/solution/src/test/resources/wrong_json.json new file mode 100644 index 0000000..fbfe775 --- /dev/null +++ b/solution/src/test/resources/wrong_json.json @@ -0,0 +1,8 @@ +[ + /0000000000 + { + "user_id": "3acfb0b7-04bd-4978-be4c-3929372277c1", + "ordered_at": "2023-01-16T13:56:39.492", + "status": "COMPLETED" + } +] \ No newline at end of file