diff --git a/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java index 20a692ac..ff5cce4e 100644 --- a/src/main/java/com/example/Main.java +++ b/src/main/java/com/example/Main.java @@ -1,9 +1,207 @@ package com.example; import com.example.api.ElpriserAPI; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + public class Main { public static void main(String[] args) { - ElpriserAPI elpriserAPI = new ElpriserAPI(); + Locale.setDefault(Locale.of("sv","SE")); + + if(args.length != 0){ + ElpriserAPI elpriserAPI = new ElpriserAPI(); + determineUsage(args, elpriserAPI); + }else{ + printHelp(); + } + } + + + private static void printHelp() { + System.out.println(""" + Usage: + --zone SE1|SE2|SE3|SE4 (required) + --date YYYY-MM-DD (optional, defaults to current date) + --sorted (optional, to display prices in descending order) + --charging 2h|4h|8h (optional, to find optimal charging windows)"""); } -} + + private static void determineUsage(String[] args, ElpriserAPI priceList) { + ElpriserAPI.Prisklass zone = null; + LocalDate parsedDate = LocalDate.now(); + int chargeDuration = -1; + boolean sorted = false; + + for (int i = 0; i < args.length; i++) { + switch(args[i]){ + case "--zone" -> zone = getZone(args, i); + case "--date" -> parsedDate = getParsedDate(args, i); + case "--charging" -> chargeDuration = getChargeDuration(args, i); + case "--sorted" -> sorted = true; + case "--help" -> { printHelp(); return; } + } + } + + if(zone == null){ + printHelp(); + System.out.println("Invalid zone input"); + } + else if(chargeDuration != -1) + printChargePrice(priceList, zone, parsedDate, chargeDuration); + else if(sorted) + printSortedPrices(priceList, zone, parsedDate); + else + printPrices(getMergedList(priceList, zone, parsedDate)); + } + + private static void printSortedPrices(ElpriserAPI priceList, ElpriserAPI.Prisklass zone, LocalDate parsedDate) { + if (parsedDate == null || zone == null) return; + + List sortedList = getMergedList(priceList, zone, parsedDate); + sortedList = sortedList.stream() + .sorted(Comparator.comparing(hourOfQuarters::price).reversed() + .thenComparing(hourOfQuarters::startDate)) + .toList(); + + printPrices(sortedList); + } + + private static void printPrices(List priceList) { + if(priceList == null) return; + if(priceList.isEmpty()){ + System.out.println("Found no data"); + } else { + double lowestPrice = Double.MAX_VALUE, highestPrice = 0, meanPrice = 0; + System.out.println("Prislista:"); + for (hourOfQuarters price : priceList) { + lowestPrice = Math.min(lowestPrice, price.price()); + highestPrice = Math.max(highestPrice, price.price()); + meanPrice += price.price(); + System.out.printf(formattedTime(price) + " %.2f öre\n", price.price() * 100); + } + System.out.printf("Lägsta pris: %.02f öre\n", lowestPrice*100); + System.out.printf("Högsta pris: %.02f öre\n", highestPrice*100); + System.out.printf("Medelpris: %.02f öre\n", meanPrice/priceList.size()*100); + } + } + + private static void printChargePrice(ElpriserAPI priceList, ElpriserAPI.Prisklass zone, LocalDate parsedDate, int chargeDuration) { + if (parsedDate == null || zone == null) return; + + if(chargeDuration == -1){ + printHelp(); + return; + } + + List prices = getMergedList(priceList, zone, parsedDate); + List chargeWindow; + int indexWindow = chargeDuration; + double lowestSum = 0; + + for (int i = 0; i < chargeDuration; i++) { + lowestSum += prices.get(i).price(); + } + + double windowSum = lowestSum; + for (int i = chargeDuration; i < prices.size(); i++) { + windowSum += prices.get(i).price() - prices.get(i - chargeDuration).price(); + if(windowSum < lowestSum){ + indexWindow = i+1; + lowestSum = windowSum; + } + } + + chargeWindow = prices.subList(indexWindow - chargeDuration, indexWindow); + + System.out.printf("Medelpris för fönster: %.2f öre\n", lowestSum/chargeDuration*100); + System.out.printf("Påbörja laddning kl %02d:00\n", chargeWindow.getFirst().startDate.getHour()); + } + + private static List convertQuartersToHours(List prices) { + List hourList = new ArrayList<>(); + if (prices.isEmpty()){ + return hourList; + } + if(prices.getFirst().timeEnd().getMinute() != 0 || prices.get(1).timeEnd().getMinute() != 0){ + int index = -1; + for (ElpriserAPI.Elpris entry : prices) { + if (entry.timeStart().getMinute() == 0) { + hourList.add(new hourOfQuarters(entry.timeStart(), entry.timeEnd(), entry.sekPerKWh()/4)); + index++; + } else { + hourList.add(new hourOfQuarters(hourList.get(index).startDate, entry.timeEnd(), entry.sekPerKWh()/4 + hourList.get(index).price)); + hourList.remove(index); + } + } + } else { + for(ElpriserAPI.Elpris entry : prices){ + hourList.add(new hourOfQuarters(entry.timeStart(), entry.timeEnd(), entry.sekPerKWh())); + } + } + return hourList; + } + + private static ElpriserAPI.Prisklass getZone(String[] args, int index) { + ElpriserAPI.Prisklass zone = null; + try { + switch (args[index+1]){ + case "SE1" -> zone = ElpriserAPI.Prisklass.SE1; + case "SE2" -> zone = ElpriserAPI.Prisklass.SE2; + case "SE3" -> zone = ElpriserAPI.Prisklass.SE3; + case "SE4" -> zone = ElpriserAPI.Prisklass.SE4; + } + } catch (IndexOutOfBoundsException e) { + return null; + } + return zone; + } + + private static LocalDate getParsedDate(String[] args, int index) { + LocalDate parsedDate; + try { + parsedDate = LocalDate.parse(args[index+1]); + } catch (DateTimeParseException | IndexOutOfBoundsException e) { + printHelp(); + System.out.println("Invalid date"); + return null; + } + return parsedDate; + } + + private static int getChargeDuration(String[] args, int index) { + int chargeDuration; + try { + chargeDuration = switch(args[index+1]){ + case "2h" -> 2; + case "4h" -> 4; + case "8h" -> 8; + default -> -1; + }; + } catch (IndexOutOfBoundsException e) { + chargeDuration = -1; + } + return chargeDuration; + } + + private static List getMergedList(ElpriserAPI priceList, ElpriserAPI.Prisklass zone, LocalDate parsedDate) { + if (parsedDate == null || zone == null) return null; + + List mergedList = priceList.getPriser(parsedDate, zone); + mergedList.addAll(priceList.getPriser(parsedDate.plusDays(1), zone)); + return convertQuartersToHours(mergedList); + } + + private static String formattedTime(hourOfQuarters source){ + String formattedTime; + formattedTime = String.format("%02d-%02d", source.startDate.getHour(), source.endDate.getHour()); + return formattedTime; + } + + record hourOfQuarters(ZonedDateTime startDate, ZonedDateTime endDate, double price){} +} \ No newline at end of file diff --git a/src/test/java/com/example/MainTest.java b/src/test/java/com/example/MainTest.java index 6199d951..c61f3589 100644 --- a/src/test/java/com/example/MainTest.java +++ b/src/test/java/com/example/MainTest.java @@ -43,7 +43,8 @@ void getPriser_shouldReturnParsedPrices_whenMockDataIsProvided() { [{"SEK_per_kWh":0.12229,"EUR_per_kWh":0.01112,"EXR":10.997148,"time_start":"2025-09-04T00:00:00+02:00","time_end":"2025-09-04T01:00:00+02:00"},{"SEK_per_kWh":0.09886,"EUR_per_kWh":0.00899,"EXR":10.997148,"time_start":"2025-09-04T01:00:00+02:00","time_end":"2025-09-04T02:00:00+02:00"},{"SEK_per_kWh":0.09095,"EUR_per_kWh":0.00827,"EXR":10.997148,"time_start":"2025-09-04T02:00:00+02:00","time_end":"2025-09-04T03:00:00+02:00"},{"SEK_per_kWh":0.04201,"EUR_per_kWh":0.00382,"EXR":10.997148,"time_start":"2025-09-04T03:00:00+02:00","time_end":"2025-09-04T04:00:00+02:00"},{"SEK_per_kWh":0.04146,"EUR_per_kWh":0.00377,"EXR":10.997148,"time_start":"2025-09-04T04:00:00+02:00","time_end":"2025-09-04T05:00:00+02:00"},{"SEK_per_kWh":0.04465,"EUR_per_kWh":0.00406,"EXR":10.997148,"time_start":"2025-09-04T05:00:00+02:00","time_end":"2025-09-04T06:00:00+02:00"},{"SEK_per_kWh":0.32991,"EUR_per_kWh":0.03,"EXR":10.997148,"time_start":"2025-09-04T06:00:00+02:00","time_end":"2025-09-04T07:00:00+02:00"},{"SEK_per_kWh":0.47123,"EUR_per_kWh":0.04285,"EXR":10.997148,"time_start":"2025-09-04T07:00:00+02:00","time_end":"2025-09-04T08:00:00+02:00"},{"SEK_per_kWh":0.68182,"EUR_per_kWh":0.062,"EXR":10.997148,"time_start":"2025-09-04T08:00:00+02:00","time_end":"2025-09-04T09:00:00+02:00"},{"SEK_per_kWh":0.4125,"EUR_per_kWh":0.03751,"EXR":10.997148,"time_start":"2025-09-04T09:00:00+02:00","time_end":"2025-09-04T10:00:00+02:00"},{"SEK_per_kWh":0.29571,"EUR_per_kWh":0.02689,"EXR":10.997148,"time_start":"2025-09-04T10:00:00+02:00","time_end":"2025-09-04T11:00:00+02:00"},{"SEK_per_kWh":0.06136,"EUR_per_kWh":0.00558,"EXR":10.997148,"time_start":"2025-09-04T11:00:00+02:00","time_end":"2025-09-04T12:00:00+02:00"},{"SEK_per_kWh":0.03662,"EUR_per_kWh":0.00333,"EXR":10.997148,"time_start":"2025-09-04T12:00:00+02:00","time_end":"2025-09-04T13:00:00+02:00"},{"SEK_per_kWh":0.0375,"EUR_per_kWh":0.00341,"EXR":10.997148,"time_start":"2025-09-04T13:00:00+02:00","time_end":"2025-09-04T14:00:00+02:00"},{"SEK_per_kWh":0.26822,"EUR_per_kWh":0.02439,"EXR":10.997148,"time_start":"2025-09-04T14:00:00+02:00","time_end":"2025-09-04T15:00:00+02:00"},{"SEK_per_kWh":0.30429,"EUR_per_kWh":0.02767,"EXR":10.997148,"time_start":"2025-09-04T15:00:00+02:00","time_end":"2025-09-04T16:00:00+02:00"},{"SEK_per_kWh":0.36675,"EUR_per_kWh":0.03335,"EXR":10.997148,"time_start":"2025-09-04T16:00:00+02:00","time_end":"2025-09-04T17:00:00+02:00"},{"SEK_per_kWh":0.58296,"EUR_per_kWh":0.05301,"EXR":10.997148,"time_start":"2025-09-04T17:00:00+02:00","time_end":"2025-09-04T18:00:00+02:00"},{"SEK_per_kWh":0.92145,"EUR_per_kWh":0.08379,"EXR":10.997148,"time_start":"2025-09-04T18:00:00+02:00","time_end":"2025-09-04T19:00:00+02:00"},{"SEK_per_kWh":1.5054,"EUR_per_kWh":0.13689,"EXR":10.997148,"time_start":"2025-09-04T19:00:00+02:00","time_end":"2025-09-04T20:00:00+02:00"},{"SEK_per_kWh":1.00888,"EUR_per_kWh":0.09174,"EXR":10.997148,"time_start":"2025-09-04T20:00:00+02:00","time_end":"2025-09-04T21:00:00+02:00"},{"SEK_per_kWh":0.63179,"EUR_per_kWh":0.05745,"EXR":10.997148,"time_start":"2025-09-04T21:00:00+02:00","time_end":"2025-09-04T22:00:00+02:00"},{"SEK_per_kWh":0.56382,"EUR_per_kWh":0.05127,"EXR":10.997148,"time_start":"2025-09-04T22:00:00+02:00","time_end":"2025-09-04T23:00:00+02:00"},{"SEK_per_kWh":0.52951,"EUR_per_kWh":0.04815,"EXR":10.997148,"time_start":"2025-09-04T23:00:00+02:00","time_end":"2025-09-05T00:00:00+02:00"}]"""; // 2. Set the mock response using the static method. - ElpriserAPI.setMockResponse(fakeJson); + LocalDate today = LocalDate.of(2025, 9, 4); + ElpriserAPI.setMockResponseForDate(today,fakeJson); // 3. Create an instance of the class as a student would. ElpriserAPI api = new ElpriserAPI(false); // Disable caching for predictable tests @@ -109,7 +110,8 @@ void displayMeanPrice_withValidData() { {"SEK_per_kWh":0.30,"EUR_per_kWh":0.03,"EXR":10.0,"time_start":"2025-09-04T02:00:00+02:00","time_end":"2025-09-04T03:00:00+02:00"}, {"SEK_per_kWh":0.40,"EUR_per_kWh":0.04,"EXR":10.0,"time_start":"2025-09-04T03:00:00+02:00","time_end":"2025-09-04T04:00:00+02:00"}]"""; - ElpriserAPI.setMockResponse(mockJson); + LocalDate today = LocalDate.of(2025, 9, 4); + ElpriserAPI.setMockResponseForDate(today,mockJson); Main.main(new String[]{"--zone", "SE3", "--date", "2025-09-04"}); @@ -127,7 +129,8 @@ void displayMinMaxPrices_withValidData() { {"SEK_per_kWh":0.80,"EUR_per_kWh":0.08,"EXR":10.0,"time_start":"2025-09-04T02:00:00+02:00","time_end":"2025-09-04T03:00:00+02:00"}, {"SEK_per_kWh":0.30,"EUR_per_kWh":0.03,"EXR":10.0,"time_start":"2025-09-04T03:00:00+02:00","time_end":"2025-09-04T04:00:00+02:00"}]"""; - ElpriserAPI.setMockResponse(mockJson); + LocalDate today = LocalDate.of(2025, 9, 4); + ElpriserAPI.setMockResponseForDate(today,mockJson); Main.main(new String[]{"--zone", "SE1", "--date", "2025-09-04"}); @@ -144,13 +147,22 @@ void displayMinMaxPrices_withValidData() { @Test void displaySortedPrices_whenRequested() { - String mockJson = """ - [{"SEK_per_kWh":0.30,"EUR_per_kWh":0.03,"EXR":10.0,"time_start":"2025-09-04T00:00:00+02:00","time_end":"2025-09-04T01:00:00+02:00"}, - {"SEK_per_kWh":0.10,"EUR_per_kWh":0.01,"EXR":10.0,"time_start":"2025-09-04T01:00:00+02:00","time_end":"2025-09-04T02:00:00+02:00"}, - {"SEK_per_kWh":0.20,"EUR_per_kWh":0.02,"EXR":10.0,"time_start":"2025-09-04T02:00:00+02:00","time_end":"2025-09-04T03:00:00+02:00"}, - {"SEK_per_kWh":0.10,"EUR_per_kWh":0.01,"EXR":10.0,"time_start":"2025-09-04T03:00:00+02:00","time_end":"2025-09-04T04:00:00+02:00"}]"""; + // This test ensures charging window can span days when next day data exists + LocalDate today = LocalDate.of(2025, 9, 4); + LocalDate tomorrow = today.plusDays(1); - ElpriserAPI.setMockResponse(mockJson); + String mockJsonToday = """ + [{"SEK_per_kWh":0.30,"EUR_per_kWh":0.03,"EXR":10.0,"time_start":"2025-09-04T20:00:00+02:00","time_end":"2025-09-04T21:00:00+02:00"}, + {"SEK_per_kWh":0.10,"EUR_per_kWh":0.01,"EXR":10.0,"time_start":"2025-09-04T21:00:00+02:00","time_end":"2025-09-04T22:00:00+02:00"}, + {"SEK_per_kWh":0.20,"EUR_per_kWh":0.02,"EXR":10.0,"time_start":"2025-09-04T22:00:00+02:00","time_end":"2025-09-04T23:00:00+02:00"}, + {"SEK_per_kWh":0.10,"EUR_per_kWh":0.01,"EXR":10.0,"time_start":"2025-09-04T23:00:00+02:00","time_end":"2025-09-04T00:00:00+02:00"}]"""; + String mockJsonTomorrow = """ + [{"SEK_per_kWh":0.10,"EUR_per_kWh":0.01,"EXR":10.0,"time_start":"2025-09-05T00:00:00+02:00","time_end":"2025-09-05T01:00:00+02:00"}, + {"SEK_per_kWh":0.15,"EUR_per_kWh":0.015,"EXR":10.0,"time_start":"2025-09-05T01:00:00+02:00","time_end":"2025-09-05T02:00:00+02:00"}, + {"SEK_per_kWh":0.15,"EUR_per_kWh":0.015,"EXR":10.0,"time_start":"2025-09-05T02:00:00+02:00","time_end":"2025-09-05T03:00:00+02:00"}]"""; + + ElpriserAPI.setMockResponseForDate(today, mockJsonToday); + ElpriserAPI.setMockResponseForDate(tomorrow, mockJsonTomorrow); Main.main(new String[]{"--zone", "SE2", "--date", "2025-09-04", "--sorted"}); @@ -158,10 +170,13 @@ void displaySortedPrices_whenRequested() { // Expected sorted output (ascending by price) List expectedOrder = List.of( - "01-02 10,00 öre", - "03-04 10,00 öre", - "02-03 20,00 öre", - "00-01 30,00 öre" + "20-21 30,00 öre", + "22-23 20,00 öre", + "01-02 15,00 öre", + "02-03 15,00 öre", + "21-22 10,00 öre", + "23-00 10,00 öre", + "00-01 10,00 öre" ); // Extract actual lines that match the pattern @@ -183,7 +198,9 @@ void findOptimalCharging2Hours() { {"SEK_per_kWh":0.15,"EUR_per_kWh":0.015,"EXR":10.0,"time_start":"2025-09-04T03:00:00+02:00","time_end":"2025-09-04T04:00:00+02:00"}, {"SEK_per_kWh":0.30,"EUR_per_kWh":0.03,"EXR":10.0,"time_start":"2025-09-04T04:00:00+02:00","time_end":"2025-09-04T05:00:00+02:00"}]"""; - ElpriserAPI.setMockResponse(mockJson); + LocalDate today = LocalDate.of(2025, 9, 4); + + ElpriserAPI.setMockResponseForDate(today, mockJson); Main.main(new String[]{"--zone", "SE3", "--date", "2025-09-04", "--charging", "2h"}); @@ -204,7 +221,9 @@ void findOptimalCharging4Hours() { {"SEK_per_kWh":0.20,"EUR_per_kWh":0.02,"EXR":10.0,"time_start":"2025-09-04T04:00:00+02:00","time_end":"2025-09-04T05:00:00+02:00"}, {"SEK_per_kWh":0.30,"EUR_per_kWh":0.03,"EXR":10.0,"time_start":"2025-09-04T05:00:00+02:00","time_end":"2025-09-04T06:00:00+02:00"}]"""; - ElpriserAPI.setMockResponse(mockJson); + LocalDate today = LocalDate.of(2025, 9, 4); + + ElpriserAPI.setMockResponseForDate(today, mockJson); Main.main(new String[]{"--zone", "SE1", "--date", "2025-09-04", "--charging", "4h"}); @@ -223,7 +242,9 @@ void chargingWindowDoesNotUseNextDay_whenNextDayUnavailable() { [{"SEK_per_kWh":0.20,"EUR_per_kWh":0.02,"EXR":10.0,"time_start":"2025-09-04T00:00:00+02:00","time_end":"2025-09-04T01:00:00+02:00"}, {"SEK_per_kWh":0.10,"EUR_per_kWh":0.01,"EXR":10.0,"time_start":"2025-09-04T01:00:00+02:00","time_end":"2025-09-04T02:00:00+02:00"}, {"SEK_per_kWh":0.15,"EUR_per_kWh":0.015,"EXR":10.0,"time_start":"2025-09-04T02:00:00+02:00","time_end":"2025-09-04T03:00:00+02:00"}]"""; - ElpriserAPI.setMockResponse(mockJsonToday); + LocalDate today = LocalDate.of(2025, 9, 4); + ElpriserAPI.setMockResponseForDate(today,mockJsonToday); + Main.main(new String[]{"--zone", "SE3", "--date", "2025-09-04", "--charging", "2h"}); String output = bos.toString(); // Best 2h window should be 01-03 (0.10 + 0.15) @@ -235,7 +256,7 @@ void chargingWindowDoesNotUseNextDay_whenNextDayUnavailable() { void findOptimalCharging8Hours() { // Create mock data with 12 hours to allow for 8-hour window StringBuilder jsonBuilder = new StringBuilder("["); - double[] prices = {0.50, 0.10, 0.05, 0.15, 0.08, 0.12, 0.06, 0.09, 0.25, 0.30, 0.35, 0.40}; + double[] prices = {0.50, 0.10, 0.05, 0.15, 0.08, 0.12, 0.06, 0.09, 0.25, 0.30, 0.35, 0.40, 0.50, 0.10, 0.05, 0.15, 0.08, 0.12, 0.06, 0.09, 0.25, 0.30, 0.35, 0.40}; for (int i = 0; i < prices.length; i++) { if (i > 0) jsonBuilder.append(","); @@ -243,12 +264,20 @@ void findOptimalCharging8Hours() { Locale.US, """ {"SEK_per_kWh":%.2f,"EUR_per_kWh":%.3f,"EXR":10.0,"time_start":"2025-09-04T%02d:00:00+02:00","time_end":"2025-09-04T%02d:00:00+02:00"}""", - prices[i], prices[i] / 10, i, i + 1 + prices[i], prices[i] / 10, i, (i + 1) % 24 )); } jsonBuilder.append("]"); - ElpriserAPI.setMockResponse(jsonBuilder.toString()); + LocalDate today = LocalDate.of(2025, 9, 4); + ElpriserAPI.setMockResponseForDate(today, jsonBuilder.toString()); + + LocalDate tomorrow = today.plusDays(1); + String mockJsonTomorrow = """ + [{"SEK_per_kWh":0.1,"EUR_per_kWh":0.01,"EXR":10.0,"time_start":"2025-09-05T00:00:00+02:00","time_end":"2025-09-05T01:00:00+02:00"}, + {"SEK_per_kWh":0.15,"EUR_per_kWh":0.015,"EXR":10.0,"time_start":"2025-09-05T01:00:00+02:00","time_end":"2025-09-05T02:00:00+02:00"}, + {"SEK_per_kWh":0.15,"EUR_per_kWh":0.015,"EXR":10.0,"time_start":"2025-09-05T02:00:00+02:00","time_end":"2025-09-05T03:00:00+02:00"}]"""; + ElpriserAPI.setMockResponseForDate(tomorrow, mockJsonTomorrow); Main.main(new String[]{"--zone", "SE4", "--date", "2025-09-04", "--charging", "8h"}); @@ -359,66 +388,63 @@ void chargingWindowSpansToNextDay_whenCheapestCrossesMidnight() { } @Test - public void testHourlyMinMaxPrices() { - List quarterHourPrices = new ArrayList<>(); - - // Simulate 96 prices: 24 hours, each with 4 quarter-hour prices - for (int i = 0; i < 96; i++) { - quarterHourPrices.add((double) (i % 24)); // repeating hourly pattern - } + void testHourlyMinMaxPrices_with96Entries() { + // --- ARRANGE --- + LocalDate today = LocalDate.of(2025, 9, 4); + StringBuilder jsonBuilder = new StringBuilder("["); - // Expected hourly averages - List hourlyAverages = new ArrayList<>(); - for (int i = 0; i < 24; i++) { - double sum = 0; - for (int j = 0; j < 4; j++) { - sum += quarterHourPrices.get(i * 4 + j); + for (int hour = 0; hour < 24; hour++) { + for (int quarter = 0; quarter < 4; quarter++) { + if (hour > 0 || quarter > 0) { + jsonBuilder.append(","); + } + double price = (hour * 0.1) + (quarter * 0.01) + 0.10; + String time_start = String.format("2025-09-04T%02d:%02d:00+02:00", hour, quarter * 15); + String time_end = String.format("2025-09-04T%02d:%02d:00+02:00", hour, (quarter + 1) * 15); + if (quarter == 3) { // Handle end of hour + time_end = String.format("2025-09-04T%02d:00:00+02:00", (hour + 1) % 24); + } + + jsonBuilder.append(String.format(Locale.US, + """ + {"SEK_per_kWh":%.4f,"EUR_per_kWh":0.01,"EXR":10.0,"time_start":"%s","time_end":"%s"}""", + price, time_start, time_end)); } - hourlyAverages.add(sum / 4.0); } + jsonBuilder.append("]"); + ElpriserAPI.setMockResponseForDate(today, jsonBuilder.toString()); - double expectedMin = Collections.min(hourlyAverages); - double expectedMax = Collections.max(hourlyAverages); + // --- ACT --- + Main.main(new String[]{"--zone", "SE3", "--date", "2025-09-04"}); - // Call your method under test - PriceRange result = PriceCalculator.calculateHourlyMinMax(quarterHourPrices); + // --- ASSERT --- + String output = bos.toString(); + assertThat(output).containsIgnoringCase("lägsta pris"); + assertThat(output).containsIgnoringCase("högsta pris"); + assertThat(output).containsIgnoringCase("medelpris"); - assertThat(result.getMin()).isCloseTo(expectedMin, within(0.001)); - assertThat(result.getMax()).isCloseTo(expectedMax, within(0.001)); + // Expected Min: Hour 0 -> avg(0.10, 0.11, 0.12, 0.13) = 0.115 SEK/kWh = 11,50 öre + // Expected Max: Hour 23 -> avg(2.40, 2.41, 2.42, 2.43) = 2.415 SEK/kWh = 241,50 öre + assertThat(output).contains("00-01"); // Cheapest hour + assertThat(output).contains("23-00"); // Most expensive hour + assertThat(output).contains(formatOre(0.115)); + assertThat(output).contains(formatOre(2.415)); + + // Calculate overall average for the day + double totalSum = 0; + for (int hour = 0; hour < 24; hour++) { + for (int quarter = 0; quarter < 4; quarter++) { + totalSum += (hour * 0.1) + (quarter * 0.01) + 0.10; + } + } + double expectedMean = totalSum / 96; + assertThat(output).contains("Medelpris: " + formatOre(expectedMean) + " öre"); } private String formatOre(double sekPerKWh) { double ore = sekPerKWh * 100.0; - DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.of("sv", "SE")); + DecimalFormatSymbols symbols = new DecimalFormatSymbols(new Locale("sv", "SE")); DecimalFormat df = new DecimalFormat("0.00", symbols); return df.format(ore); } -} -class PriceRange { - private final double min; - private final double max; - - public PriceRange(double min, double max) { - this.min = min; - this.max = max; - } - - public double getMin() { return min; } - public double getMax() { return max; } -} - -class PriceCalculator { - public static PriceRange calculateHourlyMinMax(List quarterHourPrices) { - List hourlyAverages = new ArrayList<>(); - for (int i = 0; i < 24; i++) { - double sum = 0; - for (int j = 0; j < 4; j++) { - sum += quarterHourPrices.get(i * 4 + j); - } - hourlyAverages.add(sum / 4.0); - } - double min = Collections.min(hourlyAverages); - double max = Collections.max(hourlyAverages); - return new PriceRange(min, max); - } } \ No newline at end of file