diff --git a/src/main/java/com/example/Anteckningar b/src/main/java/com/example/Anteckningar new file mode 100644 index 00000000..211f31e3 --- /dev/null +++ b/src/main/java/com/example/Anteckningar @@ -0,0 +1,15 @@ + ************************************ + ************bra att ha kod********** + ************************************ + + //Ta in och parsea ett datum via readLine + while(true){ + try{ + System.out.println("Enter a date:"); + chosenDate = LocalDate.parse(System.console().readLine()); + break; + } catch(Exception e){ + chosenDate = LocalDate.now(); + System.out.println("-------!!!!!INVALID FORMAT!!!!!!-------\nPlease enter a date in the \"yyyy-mm-dd\" format:"); + } + } diff --git a/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java index 20a692ac..7f9d5ca5 100644 --- a/src/main/java/com/example/Main.java +++ b/src/main/java/com/example/Main.java @@ -1,9 +1,598 @@ package com.example; import com.example.api.ElpriserAPI; +import com.example.api.ElpriserAPI.Prisklass; +import com.example.api.ElpriserAPI.Elpris; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + public class Main { public static void main(String[] args) { - ElpriserAPI elpriserAPI = new ElpriserAPI(); + + // Skapa objekt av klasssen ElpriserAPI för att kunna använda dess innehåll + ElpriserAPI api = new ElpriserAPI(); + + // Variabler + Prisklass zone = Prisklass.SE1; + LocalDate date = LocalDate.now(); + + List firstDay; + List nextDay; + List bothDays; + List sortedBothDays; + List sortedFirstDay; + int[] chargingWindowIndex; + String[] minMaxMean; + String chargingWindow; + String categoryInput; + int chargingHours = 0; + + boolean isProgramRunning = true; + boolean isCliRunning = false; + boolean isSorted = false; + + /* ***************************************************************************************************************************************** */ + /* ************************************************************** KÖR PROGRAM ************************************************************** */ + /* ***************************************************************************************************************************************** */ + + System.out.println("\n\n\n\n\n" + """ + \t###################################################################### + \t###################### APPLIKATION INITIERAD ######################### + \t###################################################################### + """); + + /* ######## Kolla om programmet ska köras som CLI eller interactive ####### */ + + // Om endast 1 eller 0 argument, anta att det är --help eller + // att användaren angett fel och behöver hjälp. + if (args.length == 0 || (args.length == 1 && args[0].equalsIgnoreCase("--help"))) { + helpCli(); + return; + } + else if (args.length == 1 && args[0].equalsIgnoreCase("--interactive")) { + boolean isMenuRunning = true; + boolean isZoneSet = false; + + while (isMenuRunning) { + drawInterfaceMenu(); + categoryInput = chooseInterfaceCategory(); + switch (categoryInput) { + case "usage": + break; + case "zone": + zone = zoneInterfaceInput(); + isZoneSet = true; + break; + case "date": + date = dateInterfaceInput(); + break; + case "charging": + chargingWindow = chargingInterfaceInput(); + break; + case "sorted": + isSorted = !isSorted; + System.out.println("Sorted satt till " + isSorted); + break; + case "run": + if (isZoneSet){ + isMenuRunning = false; + } + else { + System.out.println("En zon måste anges innan programmet kan köras."); + } + break; + case "exit": + return; + default: + isMenuRunning = false; + isProgramRunning = false; + break; + } + } + } else { + isCliRunning = true; + //#### SKAPA VARIABLER ####// + // Validera zoninput + if (zoneValidation(checkArgsForZone(args), isCliRunning) != null) { + zone = zoneValidation(checkArgsForZone(args), true); + } else { + //isProgramRunning = false; + return; + } + date = checkArgsForDate(args); + chargingWindow = checkArgsForCharging(args); + chargingHours = Integer.parseInt(chargingWindow.substring(0, 1)); + isSorted = checkArgsForSorted(args); + } + + + /* ######## KÖR PROGRAMMET ####### */ + if (isProgramRunning) { + + // Skapa första dagens lista + if (!api.getPriser(date, zone).isEmpty()) { + firstDay = new ArrayList<>(api.getPriser(date, zone)); + } else { + System.out.println("ingen data kunde hämtas, avslutar programmet."); + return; + } + // Skapa andra dagens lista + nextDay = new ArrayList<>(api.getPriser(date.plusDays(1), zone)); + // Lägg ihop listorna + bothDays = addLists(firstDay, nextDay); + + // Sortera listorna + sortedFirstDay = sortList(firstDay); + sortedBothDays = sortList(bothDays); + + //#### 96 entries list ####// + if (firstDay.size() == 96) { + quarterlyToHourly(firstDay); + } else { + //#### Min, Max, Mean ####// + minMaxMean = minMaxMeanToArray(minPrice(sortedFirstDay), maxPrice(sortedFirstDay), meanPrice(sortedFirstDay), sortedFirstDay); + System.out.println("\n"); + for (String s : minMaxMean) { + System.out.print(s); + } + + //#### Optimal charging hours ####// + if (chargingHours > 0) { + chargingWindowIndex = chargingWindowSorting(bothDays, chargingHours); + // Skriv ut optimala laddningsfönstret + printCharging(bothDays, chargingHours, chargingWindowIndex[0]); + } + + //#### Print lists ####// + if (isSorted && !sortedBothDays.isEmpty()) { + System.out.println("Sorterad lista två dagar:"); + for (int i = sortedBothDays.size() - 1; i >= 0; i--) { + // Kan snygga upp detta om jag orkar lägga tid på det + System.out.print(timeFormatter(sortedBothDays.get(i).timeStart().getHour(), sortedBothDays.get(i).timeEnd().getHour()) + " "); + System.out.println(decimalFormatter(sortedBothDays.get(i).sekPerKWh()) + " öre"); + } + } else if (isSorted && !sortedFirstDay.isEmpty()) { + System.out.println("Sorterad lista en dag:"); + for (int i = sortedFirstDay.size() - 1; i >= 0; i--) { + // Kan snygga upp detta om jag orkar lägga tid på det + System.out.print(timeFormatter(sortedFirstDay.get(i).timeStart().getHour(), sortedFirstDay.get(i).timeEnd().getHour()) + " "); + System.out.println(decimalFormatter(sortedFirstDay.get(i).sekPerKWh()) + " öre"); + } + } else { + System.out.println("No data found.."); + } + + } + System.out.println("\n"); + } + + System.out.println("\nProgrammet avslutas...\n"); + } + + + /* ***************************************************************************************************************************************** */ + /* **************************************************************** METODER **************************************************************** */ + /* ***************************************************************************************************************************************** */ + + /* ################## CLI ################## */ + + public static void helpCli() { + + System.out.println("\n\n\t################################################## USAGE #########################################################\n"); + System.out.println("\tZONE : --zone SE1/SE2/SE3/SE4 | Vilken zon priserna ska hämtas ifrån."); + System.out.println("\t SE1 = norra Sverige | SE2 = norra mellansverige | SE3 = södra mellansverige | SE4 = södra Sverige\n"); + System.out.println("\tDATE : --date YYYY-MM-DD | Vilket datum priserna ska hämtas ifrån. Om datum inte angivits används dagens datum.\n"); + System.out.println("\tCHARGING : --charging 0H/2H/4H/8H | Ange för att hitta det billigaste laddningsfönstret.\n"); + System.out.println("\tSORTED : --sorted | Sortera listan från lägsta pris till högsta pris.\n"); + System.out.println("\tINTERACTIVE : --interactive | Ange värden via gränssnitt."); + + System.out.println("\n\t##################################################################################################################"); + } + + public static String checkArgsForZone(String[] argsArray) { + for (int i = 0; i < argsArray.length; i++) { + if (argsArray[i].equalsIgnoreCase("--zone") && i < argsArray.length - 1) { + return argsArray[i + 1]; + } + } + System.out.println("zone required : Ingen zon har angetts."); + return "null"; + } + + public static LocalDate checkArgsForDate(String[] argsArray) { + for (int i = 0; i < argsArray.length; i++) { + if (argsArray[i].equalsIgnoreCase("--date") && i < argsArray.length - 1) { + return dateValidation(argsArray[i + 1]); + } + } + return LocalDate.now(); + } + + public static String checkArgsForCharging(String[] argsArray) { + for (int i = 0; i < argsArray.length; i++) { + if (argsArray[i].equalsIgnoreCase("--charging") && i < argsArray.length - 1) { + return chargingWindowValidation(argsArray[i + 1]); + } + } + return "0H"; + } + + public static boolean checkArgsForSorted(String[] argsArray) { + for (String array : argsArray) { + if (array.equalsIgnoreCase("--sorted")) { + return true; + } + } + return false; + } + + /* ################## INTERFACE ################## */ + + public static void drawInterfaceMenu() { + System.out.println("\n***************** KATEGORIER *****************"); + System.out.println(" | zone | date | charging | sorted | run | exit |"); + } + + public static String chooseInterfaceCategory() { + String[] categories = {"usage", "zone", "date", "charging", "sorted", "run"}; + String categoryInput; + + while (true) { + categoryInput = System.console().readLine("Välj kategori: "); + if (categoryInput.equalsIgnoreCase("exit")) { + break; + } else { + for (String category : categories) { + if (category.equalsIgnoreCase(categoryInput)) { + return categoryInput; + } + } + } + + } + return "null"; + } + + public static Prisklass zoneInterfaceInput() { + Prisklass zone = zoneValidation(System.console().readLine("Ange en zon enligt formatet \"SE1\", \"SE2\", \"SE3\" eller \"SE4\": "), false); + System.out.println("Zon satt till: " + zone); + return zone; + } + + public static LocalDate dateInterfaceInput() { + LocalDate date = dateValidation(System.console().readLine("Ange ett datum enligt formatet \"YYYY-MM-DD\": ")); + System.out.println("Datum satt till: " + date); + return date; + } + + public static String chargingInterfaceInput() { + String charging = chargingWindowValidation(System.console().readLine("Ange ett laddningsfönster enligt formatet \"0H\", \"2H\",\"4H\", \"8H\": ")); + System.out.println("Laddfönster satt till: " + charging); + return charging; + } + + /* ################## ZONES ################## */ + + public static Prisklass zoneValidation(String methodInput, Boolean isCliRunning) { + String[] enums = {"se1", "se2", "se3", "se4"}; + while (true) { + if (methodInput != null) { + for (String e : enums) { + if (methodInput.equalsIgnoreCase(e)) { + return Prisklass.valueOf(methodInput.toUpperCase()); + } + } + } + + if (isCliRunning) { + System.out.println("ogiltig zon"); + System.out.print("!! zone required: Ogiltig zon. "); + return null; + } else { + System.out.println("ogiltig zon"); + System.out.println("!! zone required: Ogiltig zon."); + zoneInterfaceInput(); + } + } + } + + /* ################## CALCULATIONS ################## */ + // Returnerar medelvärdet + public static double meanPrice(List list) { + double sum = 0; + + // "Error handling" + if (list.isEmpty()) { + return 0; + } + + for (Elpris number : list) { + sum += number.sekPerKWh(); + } + + return (sum / list.size()); + } + + public static double meanPriceCharging(List list, int chargingHours, int index) { + double sum = 0; + for (int i = 0; i < chargingHours; i++) { + sum += list.get(index + i).sekPerKWh(); + + } + return sum / chargingHours; + } + + // Returnerar lägsta priset + public static double minPrice(List sortedList) { + List tempList = new ArrayList<>(sortedList); + // "Error handling" + if (sortedList.isEmpty()) { + return 0; + } + return tempList.getFirst().sekPerKWh(); + } + + // Returnerar högsta priset + public static double maxPrice(List sortedList) { + List tempList = new ArrayList<>(sortedList); + // "Error handling" + if (sortedList.isEmpty()) { + return 0; + } + return tempList.getLast().sekPerKWh(); + } + + // Skriv min, max, mean till array + public static String[] minMaxMeanToArray(double min, double max, double mean, List sortedList) { + String minHours = timeFormatter(sortedList.getFirst().timeStart().getHour(), sortedList.getFirst().timeEnd().getHour()); + String maxHours = timeFormatter(sortedList.getLast().timeStart().getHour(), sortedList.getLast().timeEnd().getHour()); + return new String[]{"lägsta pris: " + minHours + " ", decimalFormatter(min), " öre", "\nhögsta pris: " + maxHours + " ", decimalFormatter(max), " öre", "\nmedelpris: ", decimalFormatter(mean), " öre\n"}; + } + + /* ################## LISTS ################## */ + + // Lägger ihop första och andra dagens värden + public static List addLists(List firstDay, List secondDay) { + // Temporär lista + List twoDayList = new ArrayList<>(); + + // Kolla om värdena laddats ned + if (!firstDay.isEmpty() && !secondDay.isEmpty()) { + twoDayList.addAll(firstDay); + twoDayList.addAll(secondDay); + } else { + twoDayList.addAll(firstDay); + } + return twoDayList; + } + + // Sorterar listan + public static List sortList(List unsortedList) { + List sortedList = new ArrayList<>(unsortedList); + int sortedListSize = sortedList.size(); + boolean doneSorting = false; + + if (sortedListSize > 0) { + while (!doneSorting) { + doneSorting = true; + for (int i = 0; i < sortedListSize - 1; i++) { + for (int j = i + 1; j < sortedListSize; j++) { + + // Endast för att snygga upp i koden + double iSekPerKwh = sortedList.get(i).sekPerKWh(); + double jSekPerKwh = sortedList.get(j).sekPerKWh(); + // Tidsvariabler för att jämföra vilken som ska komma först i sorterade listan + LocalDateTime firstHour = sortedList.get(i).timeStart().toLocalDateTime(); + LocalDateTime secondHour = sortedList.get(j).timeStart().toLocalDateTime(); + + // Byt plats på elementen om billigare pris hittas + if (iSekPerKwh > jSekPerKwh || (iSekPerKwh == jSekPerKwh && firstHour.isBefore(secondHour))) { + doneSorting = false; + Elpris tempFirstDay = sortedList.get(i); + sortedList.set(i, sortedList.get(j)); + sortedList.set(j, tempFirstDay); + } + } + } + } + } + return sortedList; + } + + public static void quarterlyToHourly(List quarterlyList) { + double cheapestPrice = 0; + double mostExpensivePrice = 0; + double meanPrice = 0; + int cheapestHourStart = 1; + int cheapestHourEnd = 1; + int mostExpensiveHourStart = 2; + int mostExpensiveHourEnd = 2; + + for (int i = 0; i < 96; i = i + 4) { + double currentPrice; + double sekPerKWh = 0; + + List tempLoopList = quarterlyList.subList(i, i + 4); + + // Räkna ut summan av timmens kvartspriser + for (int j = 0; j < tempLoopList.size(); j++) { + sekPerKWh += tempLoopList.get(j).sekPerKWh(); + } + currentPrice = sekPerKWh / 4; + + // Om billigare + if (cheapestPrice == 0 || currentPrice < cheapestPrice) { + cheapestPrice = currentPrice; + cheapestHourStart = tempLoopList.getFirst().timeStart().getHour(); + if (cheapestHourStart == 23) { + cheapestHourEnd = 0; + } else { + cheapestHourEnd = (cheapestHourStart + 1); + } + } + + // Om dyrare + else if (currentPrice > mostExpensivePrice) { + mostExpensivePrice = currentPrice; + mostExpensiveHourStart = tempLoopList.getFirst().timeStart().getHour(); + if (mostExpensiveHourStart == 23) { + mostExpensiveHourEnd = 0; + } else { + mostExpensiveHourEnd = mostExpensiveHourStart + 1; + } + } + } + + // Räkna ut medelpriset + for (int i = 0; i < 96; i++) { + meanPrice += quarterlyList.get(i).sekPerKWh(); + } + + meanPrice = meanPrice / 96; + + System.out.println("\n96 entries:"); + System.out.println("Lägsta pris kl " + timeFormatter(cheapestHourStart, cheapestHourEnd) + " " + decimalFormatterCharging(cheapestPrice) + " öre"); + System.out.println("Högsta pris kl " + timeFormatter(mostExpensiveHourStart, mostExpensiveHourEnd) + " " + decimalFormatterCharging(mostExpensivePrice) + " öre"); + System.out.println("Medelpris: " + decimalFormatterCharging(meanPrice) + " öre\n"); + } + + public static int[] chargingWindowSorting(List unsortedList, int chargingWindow) { + List list = new ArrayList<>(unsortedList); + int indexSize = (list.size() - chargingWindow) + 1; + + // Skapar array som sparar startindex för alla charging windows + int[] windowIndexing = new int[indexSize]; + + // Börja med att sätta startindex till 0,1,2,3 osv. + for (int m = 0; m < indexSize; m++) { + windowIndexing[m] = m; + } + + for (int i = 0; i <= indexSize - 2; i++) { + double firstSum = 0; + double secondSum = 0; + int nextElement = i + 1; + // Tidsvariabler för laddningsfönster till att jämföra vilken som ska komma först i sorterade listan + LocalDateTime firstHour = list.get(i).timeStart().toLocalDateTime(); + LocalDateTime secondHour = list.get(nextElement).timeStart().toLocalDateTime(); + + // Summera värdena för första laddningsfönstret + for (int j = 0; j < chargingWindow; j++) { + firstSum += list.get(i + j).sekPerKWh(); + } + + // Summera värdena för andra laddningsfönstret + for (int l = 0; l < chargingWindow; l++) { + secondSum += list.get(nextElement + l).sekPerKWh(); + } + + // Byt plats på elementen om billigare fönster hittas eller om priserna är lika, lägg det tidigaste datumet/tiden först + if (firstSum > secondSum || (firstSum == secondSum && firstHour.isBefore(secondHour))) { + windowIndexing[i] = nextElement; + windowIndexing[nextElement] = i; + } + } + return windowIndexing; + } + + /* ################## OTHER ################## */ + + // Validerar datuminput + public static LocalDate dateValidation(String dateInput) { + if (!dateInput.isEmpty()) { + try { + return LocalDate.parse(dateInput); + } catch (Exception e) { + System.out.println("!! invalid date: \"" + dateInput + "\" inte enligt formatet \"YYYY-MM-DD\": Applikationen kommer att köras med dagens datum."); + } + } + return LocalDate.now(); + } + + public static String integerFormatter(int Number) { + NumberFormat integerFormatter = new DecimalFormat("00"); + return integerFormatter.format(Number); + } + + // Formatera start- och sluttid till formatet"00-00" + public static String timeFormatter(int startHour, int endHour) { + return String.format(integerFormatter(startHour) + "-" + integerFormatter(endHour)); + } + + // Formatera start-tid till formatet"00:00" + public static String timeFormatter(LocalDateTime timeStart) { + int hour = timeStart.getHour(); + int minute = timeStart.getMinute(); + return integerFormatter(hour) + ":" + integerFormatter(minute); + } + + // Ev. ändring: Denna skulle ev. kunna skicka tillbaka int + public static String chargingWindowValidation(String chargingInput) { + String[] chargingWindow = {"0H", "2H", "4H", "8H"}; + + for (String hours : chargingWindow) { + if (chargingInput.equalsIgnoreCase(hours)) { + return hours.toUpperCase(); + } + } + System.out.println("!! OGILTIGT LADDNINGSFÖNSTER: \"" + chargingInput + "\". Kör programmet utan laddningsfönster."); + return "0H"; + } + + // Formaterar till 00,00 + public static String decimalFormatter(double decimalNumber) { + //Formatering till svenska decimaler + NumberFormat decimalFormatering = NumberFormat.getNumberInstance(Locale.of("sv", "SE")); + //Hur många siffror/decimaler som ska ingå + decimalFormatering.setMaximumFractionDigits(2); + decimalFormatering.setMinimumFractionDigits(2); + decimalFormatering.setMinimumIntegerDigits(2); + + if (decimalNumber == 0) { + return "No data."; + } + + return decimalFormatering.format(decimalNumber * 100); + } + + // Formaterar till 0,0 + public static String decimalFormatterCharging(double decimalNumber) { + //Formatering till svenska decimaler + NumberFormat decimalFormatering = NumberFormat.getNumberInstance(Locale.of("sv", "SE")); + //Hur många siffror/decimaler som ska ingå + decimalFormatering.setMaximumFractionDigits(2); + decimalFormatering.setMinimumFractionDigits(2); + decimalFormatering.setMinimumIntegerDigits(1); + + if (decimalNumber == 0) { + return "No data."; + } + + return decimalFormatering.format(decimalNumber * 100); + } + + /* ################## PRINTING ################## */ + public static void printCharging(List inputList, int chargingHours, int index) { + List list = new ArrayList<>(inputList); + double meanPrice = meanPriceCharging(list, chargingHours, index); + String formattedPrice; + String startHour = timeFormatter(list.get(index).timeStart().toLocalDateTime()); + + formattedPrice = String.format(decimalFormatterCharging(meanPrice)); + + System.out.println("\nFör optimalt " + chargingHours + " timmars laddningsfönster:"); + System.out.println("Påbörja laddning kl " + startHour); + System.out.println("Medelpris för fönster: " + formattedPrice + " öre\n"); } } + + + + 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