diff --git a/README.md b/README.md index ee9d69b..7ad25d2 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,21 @@ 4. Найденный в соответствии с условием задачи месяц должен выводиться на английском языке в нижнем регистре. Если месяцев несколько, то на вывод они все подаются на английском языке в нижнем регистре в порядке их следования в течение года. ## Автор решения +Стаценко Михаил Александрович ## Описание реализации +Для выполнения поставленной задачи разработано консольное Spring Boot приложение. + +Первым аргументом командной строки передается путь к файлу с исходными данными. `ReportService` в методе `generateReport` при помощи вспомогательных методов читает json файл по указанному пути, затем считает сумму, потраченную пользователями для каждого месяца, после чего находит месяц(-ы) с максимальной суммой и возвращает строку в формате json в соответствии с заданием. + +Поскольку речь идет об анализе заказов на маркетплейсе, отчет, вероятно, будет создаваться на основании миллионов заказов, поэтому для подсчета суммы, потраченной пользователями по месяцам используется параллельная обработка, а файл с исходными данными читается при помощи `BufferedReader`. ## Инструкция по сборке и запуску решения +На тестовом стенде должны быть установлены Maven и Java 21+ версии + +Сначала необходимо клонировать репозиторий на локальную машину: +`git clone https://github.com/MikhailStatsenko/school2024-test-task1.git` + +Затем нужно перейти в склонированную директорию и последовательно выполнить команды: +`mvn clean package` +`java -jar target/analytics-1.0.jar [путь к файлу с исходными данными]` \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8ee326d --- /dev/null +++ b/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + edu.croc + analytics + 1.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + + + UTF-8 + UTF-8 + + 21 + + 21 + 21 + + + + + org.springframework.boot + spring-boot-starter + + + + org.projectlombok + lombok + true + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.17.0 + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${project.build.directory}/lib + false + false + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + lib/ + edu.croc.analytics.AnalyticsApplication + + + + + + + \ No newline at end of file diff --git a/src/main/java/edu/croc/analytics/AnalyticsApplication.java b/src/main/java/edu/croc/analytics/AnalyticsApplication.java new file mode 100644 index 0000000..8bf0563 --- /dev/null +++ b/src/main/java/edu/croc/analytics/AnalyticsApplication.java @@ -0,0 +1,29 @@ +package edu.croc.analytics; + +import edu.croc.analytics.service.ReportService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@Slf4j +@SpringBootApplication +@RequiredArgsConstructor +public class AnalyticsApplication implements CommandLineRunner { + private final ReportService reportService; + + public static void main(String[] args) { + SpringApplication.run(AnalyticsApplication.class, args); + } + + @Override + public void run(String... args) { + try { + String report = reportService.generateReport(args[0]); + System.out.println(report); + } catch (Exception e) { + log.error("Error generating report", e); + } + } +} diff --git a/src/main/java/edu/croc/analytics/configuration/ApplicationConfiguration.java b/src/main/java/edu/croc/analytics/configuration/ApplicationConfiguration.java new file mode 100644 index 0000000..5543903 --- /dev/null +++ b/src/main/java/edu/croc/analytics/configuration/ApplicationConfiguration.java @@ -0,0 +1,15 @@ +package edu.croc.analytics.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ApplicationConfiguration { + @Bean + public ObjectMapper objectMapper() { + return JsonMapper.builder().addModule(new JavaTimeModule()).build(); + } +} diff --git a/src/main/java/edu/croc/analytics/dto/Order.java b/src/main/java/edu/croc/analytics/dto/Order.java new file mode 100644 index 0000000..27263dd --- /dev/null +++ b/src/main/java/edu/croc/analytics/dto/Order.java @@ -0,0 +1,27 @@ +package edu.croc.analytics.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +public record Order ( + @JsonProperty("user_id") + String userId, + + @JsonProperty("ordered_at") + LocalDateTime orderedAt, + + @JsonProperty("status") + Status status, + + @JsonProperty("total") + BigDecimal total) { + + public enum Status { + COMPLETED, + CANCELED, + CREATED, + DELIVERY + } +} diff --git a/src/main/java/edu/croc/analytics/service/ReportService.java b/src/main/java/edu/croc/analytics/service/ReportService.java new file mode 100644 index 0000000..bf0d0fc --- /dev/null +++ b/src/main/java/edu/croc/analytics/service/ReportService.java @@ -0,0 +1,69 @@ +package edu.croc.analytics.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.croc.analytics.dto.Order; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.Month; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ReportService { + private final ObjectMapper mapper; + + private record Report(List months) {} + + public String generateReport(String filePath) throws IOException { + Order[] orders = readOrdersJson(filePath); + + Map totals = getTotalByMonth(orders); + + List months = getMaxTotalMonths(totals); + return mapper.writeValueAsString(new Report(months)); + } + + private Order[] readOrdersJson(String filePath) throws IOException { + try(var source = new BufferedReader( + new InputStreamReader(new FileInputStream(filePath), StandardCharsets.UTF_8))) { + return mapper.readValue(source, Order[].class); + } + } + + private Map getTotalByMonth(Order[] orders) { + Map totals = new ConcurrentHashMap<>(Month.values().length); + for (Month month : Month.values()) { + totals.put(month, BigDecimal.ZERO); + } + + Arrays.stream(orders).parallel() + .filter(order -> order.status().equals(Order.Status.COMPLETED)) + .forEach(order -> totals.compute( + order.orderedAt().getMonth(), + (moth, currentMothTotal) -> currentMothTotal.add(order.total())) + ); + return totals; + } + + private List getMaxTotalMonths(Map totals) { + BigDecimal max = Collections.max(totals.values()); + + List result = new ArrayList<>(); + for (Month month : Month.values()) { + if (totals.get(month).compareTo(max) == 0) { + result.add(month.name().toLowerCase()); + } + } + return result; + } +}