From b326bc035b8e2532b077646cb39135912cd34434 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:06:28 +0000 Subject: [PATCH 1/4] Setting up GitHub Classroom Feedback From 58893a00ab43d3aff093c39665e05d66f6bd7b60 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Fri, 3 Oct 2025 09:25:09 +0200 Subject: [PATCH 2/4] =?UTF-8?q?N=C3=A4stan=20d=C3=A4r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 17 ++ src/main/java/com/example/Main.java | 22 +- src/main/java/com/example/api/ElprisCLI.java | 280 +++++++++++++++++++ 3 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/api/ElprisCLI.java diff --git a/pom.xml b/pom.xml index 6df3259b..3488cb0c 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,11 @@ 5.19.0 + + info.picocli + picocli + 4.7.5 + org.junit.jupiter junit-jupiter @@ -37,6 +42,18 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + true + + --enable-native-access=ALL-UNNAMED + + + org.jacoco jacoco-maven-plugin diff --git a/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java index 20a692ac..0ca9417d 100644 --- a/src/main/java/com/example/Main.java +++ b/src/main/java/com/example/Main.java @@ -1,9 +1,29 @@ package com.example; +import com.example.api.ElprisCLI; import com.example.api.ElpriserAPI; +import picocli.CommandLine; public class Main { + + + public static int run(String[] args) { + ElpriserAPI api = new ElpriserAPI(); + ElprisCLI cli = new ElprisCLI(api); + return new CommandLine(cli).execute(args); + } + public static void main(String[] args) { - ElpriserAPI elpriserAPI = new ElpriserAPI(); + int exitCode = run(args); + + + if (!isRunningInTestEnvironment()) { + System.exit(exitCode); + } + } + + // Identifiera testmiljö via systemproperty som vi sätter i Maven + private static boolean isRunningInTestEnvironment() { + return Boolean.getBoolean("test.environment"); } } diff --git a/src/main/java/com/example/api/ElprisCLI.java b/src/main/java/com/example/api/ElprisCLI.java new file mode 100644 index 00000000..6be0b752 --- /dev/null +++ b/src/main/java/com/example/api/ElprisCLI.java @@ -0,0 +1,280 @@ +package com.example.api; + + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.util.*; +import java.util.Comparator; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.Map; + + +@Command(name = "elpris", mixinStandardHelpOptions = true, version = "1.0", + description = "CLI för elprisetjustnu.se") +public class ElprisCLI implements Runnable { + + @Option(names = "--date", description = "Datum i formatet YYYY-MM-DD") + private Optional date = Optional.empty(); + + @Option(names = "--zone", description = "Elområde: SE1, SE2, SE3, SE4") + private String zone; + + @Option(names = "--sorted", description = "Sortera priser i fallande ordning (true/false)") + private boolean sorted; + + + @Option(names = "--charging", description = "Optimera laddning för antal timmar (2, 4 eller 8)") + private String chargingHours; + + public Integer getChargingHours() { + if (chargingHours == null) return null; + return Integer.parseInt(chargingHours.replace("h", "")); + } + + + + @Override + + public void run() { + if (zone == null && date.isEmpty() && chargingHours == null) { + System.out.println("Usage: elpris --zone= [--date=YYYY-MM-DD] [--sorted] [--charging=2|4|8]"); + return; + } + if (zone == null || zone.isBlank()) { + System.out.println("Missing required option: --zone"); + System.out.println("Fel: --zone är obligatorisk (required zone argument)"); + return; + } + + ElpriserAPI.Prisklass klass; + try { + klass = ElpriserAPI.Prisklass.valueOf(zone); + } catch (IllegalArgumentException e) { + System.out.println("Fel: Ogiltig zon '" + zone + "'. Välj mellan SE1, SE2, SE3, SE4. (invalid zone)"); + return; + } + LocalDate datum; + try{ + datum = date.map(LocalDate::parse).orElse(LocalDate.now()); + } catch (DateTimeParseException e) { + System.out.println("Ogiltigt datum: " + date.orElse("")); + return; + } + + + + List priser = hämtaAllaPriser(datum, klass); + if (priser.isEmpty()) { + System.out.println("Inga priser hittades."); + return; + } + + + List> timpriser = beräknaTimpriser(priser); + if (sorted) { + timpriser = timpriser.stream() + .sorted((a, b) -> { + int prisJämf = Double.compare(b.getValue(), a.getValue()); // fallande pris + if (prisJämf != 0) return prisJämf; + return a.getKey().compareTo(b.getKey()); // stigande tid vid lika pris + }) + .collect(Collectors.toList()); + } else { + timpriser = timpriser.stream() + .sorted(Map.Entry.comparingByKey()) + .collect(Collectors.toList()); + } + System.out.println("\nElpriser per timme:"); + for (var entry : timpriser) { + String start = String.format("%02d", entry.getKey().getHour()); + String end = String.format("%02d", entry.getKey().plusHours(1).getHour()); + String tid = start + "-" + end; + String pris = String.format("%.2f", entry.getValue() * 100).replace('.', ','); + System.out.println(tid + " " + pris + " öre"); + + } + + + skrivUtStatistik(priser, datum); + Integer timmar = getChargingHours(); + if (timmar != null) { + skrivUtBilligasteLaddning(priser, timmar); + } + } + public List hämtaAllaPriser(LocalDate datum, ElpriserAPI.Prisklass klass) { + List priser = new ArrayList<>(); + + List idag = api.getPriser(datum, klass); + if (idag != null) priser.addAll(idag); + + if (LocalTime.now().isAfter(LocalTime.of(13, 0))) { + List imorgon = api.getPriser(datum.plusDays(1), klass); + if (imorgon != null) priser.addAll(imorgon); + } + + return priser; + } + + + public List sorteraPriser(List priser, boolean fallande) { + if (!fallande) { + return priser; // behåll API-ordningen + } + + return priser.stream() + .sorted(Comparator.comparingDouble(ElpriserAPI.Elpris::sekPerKWh).reversed()) + .collect(Collectors.toList()); + } + public void skrivUtStatistik(List priser, LocalDate datum) { + List> timpriser = beräknaTimpriser(priser); + double total = 0; + double min = Double.MAX_VALUE; + double max = Double.MIN_VALUE; + LocalDateTime billigast = null; + LocalDateTime dyrast = null; + + for (var entry : timpriser) { + double pris = entry.getValue(); + total += pris; + if (pris < min) { min = pris; billigast = entry.getKey(); } + if (pris > max) { max = pris; dyrast = entry.getKey(); } + } + if (billigast == null || dyrast == null) { + System.out.println("Ingen giltig prisdata hittades"); + return; + } + + double snitt = total / timpriser.size(); + Set datumSet = priser.stream() + .map(p -> p.timeStart().toLocalDate()) + .collect(Collectors.toCollection(TreeSet::new)); + + System.out.println("\nPrisstatistik för: " + String.join(", ", datumSet.stream().map(LocalDate::toString).toList())); + System.out.printf("Medelpris: %.4f SEK/kWh\n", snitt); + String start = String.format("%02d", billigast.getHour()); + String slut = String.format("%02d", billigast.plusHours(1).getHour()); + System.out.printf("Lägsta pris: %.4f SEK/kWh kl %s-%s\n", min, start, slut); + + System.out.printf("Högsta pris: %.4f SEK/kWh kl %s\n", max, + dyrast.toLocalDate() + " kl " + dyrast.toLocalTime()); + + + } + public void skrivUtBilligasteLaddning(List kvartpriser, int timmar) { + if (timmar != 2 && timmar != 4 && timmar != 8) { + throw new IllegalArgumentException("Ogiltigt värde för --charging. Välj 2, 4 eller 8."); + } + + List> timpriser = beräknaTimpriser(kvartpriser); + if (timpriser.size() < timmar) { + System.out.println("Påbörja laddning: För få timpriser tillgängliga för att optimera laddning (" + timmar + "h)."); + return; + } + timpriser = timpriser.stream() + .sorted(Map.Entry.comparingByKey()) + .collect(Collectors.toList()); + + double minSum = Double.MAX_VALUE; + int startIndex = 0; + + for (int i = 0; i <= timpriser.size() - timmar; i++) { + double sum = 0; + for (int j = 0; j < timmar; j++) { + sum += timpriser.get(i + j).getValue(); + } + if (sum < minSum) { + minSum = sum; + startIndex = i; + } + } + + List> fönster = timpriser.subList(startIndex, startIndex + timmar); + + + System.out.println("Påbörja laddning under följande timmar:"); + for (var entry : fönster) { + String tid = "kl " + String.format("%02d:00", entry.getKey().getHour()); + String pris = String.format("%.4f", entry.getValue()).replace('.', ','); + System.out.printf("%s → %s SEK/kWh\n", tid, pris); + } + + System.out.printf("Totalt: %.2f öre\n", minSum * 100); + double medel = minSum / timmar; + System.out.printf("Medelpris för fönster: %s öre\n", String.format("%.2f", medel * 100).replace('.', ',')); + } + + public static List> beräknaTimpriser(List kvartpriser) { + // Runda ner till hel timme (inklusive sekunder och nanos) + Map> grupperade = kvartpriser.stream() + .collect(Collectors.groupingBy(p -> + p.timeStart() + .withMinute(0) + .withSecond(0) + .withNano(0).toLocalDateTime() + )); + + grupperade.entrySet().removeIf(entry -> entry.getValue().isEmpty()); + + // Beräkna snittpris per timme + return grupperade.entrySet().stream() + .map(entry -> Map.entry( + entry.getKey(), + entry.getValue().stream() + .mapToDouble(ElpriserAPI.Elpris::sekPerKWh) + .average().orElse(0) + )) + .collect(Collectors.toList()); + } + + + + private final ElpriserAPI api; + public ElprisCLI(ElpriserAPI api){ + this.api = api; + } + public static void runInteractive() { + Scanner scanner = new Scanner(System.in); + String zone = ""; + while (true) { + System.out.println(" Välj elområde (SE1, SE2, SE3, SE4):"); + zone = scanner.nextLine().trim().toUpperCase(); + + if (zone.matches("SE[1-4]")) { + break; + } else { + System.out.println("Ogiltigt elområde. Ange SE1, SE2, SE3 eller SE4."); + } + } + + + System.out.println("Ange datum (YYYY-MM-DD), eller lämna tomt för idag:"); + String dateInput = scanner.nextLine().trim(); + LocalDate date = dateInput.isEmpty() ? LocalDate.now() : LocalDate.parse(dateInput); + + System.out.println("Vill du sortera priser i fallande ordning? (ja/nej):"); + boolean sorted = scanner.nextLine().trim().equalsIgnoreCase("ja"); + + System.out.println("Vill du optimera laddning? Ange antal timmar (2, 4, 8), eller lämna tomt:"); + String chargingInput = scanner.nextLine().trim(); + Integer charging = chargingInput.isEmpty() ? null : Integer.parseInt(chargingInput); + + // Kör CLI-logik direkt + ElprisCLI cli = new ElprisCLI(new ElpriserAPI()); + cli.zone = zone; + cli.date = Optional.of(date.toString()); + cli.sorted = sorted; + cli.chargingHours = charging + "h"; + cli.run(); + } + + + + + +} From cb38eb46abb8dae6717ce47c5d8bede757cc8e4b Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Fri, 3 Oct 2025 10:15:49 +0200 Subject: [PATCH 3/4] =?UTF-8?q?N=C3=A4stan=20d=C3=A4r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/api/ElprisCLI.java | 48 +++++++++++--------- src/test/java/com/example/MainTest.java | 7 +-- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/example/api/ElprisCLI.java b/src/main/java/com/example/api/ElprisCLI.java index 6be0b752..5d49bd54 100644 --- a/src/main/java/com/example/api/ElprisCLI.java +++ b/src/main/java/com/example/api/ElprisCLI.java @@ -25,7 +25,7 @@ public class ElprisCLI implements Runnable { @Option(names = "--zone", description = "Elområde: SE1, SE2, SE3, SE4") private String zone; - @Option(names = "--sorted", description = "Sortera priser i fallande ordning (true/false)") + @Option(names = "--sorted", description = "Sortera priser i fallande ordning", defaultValue = "false") private boolean sorted; @@ -77,28 +77,11 @@ public void run() { List> timpriser = beräknaTimpriser(priser); - if (sorted) { - timpriser = timpriser.stream() - .sorted((a, b) -> { - int prisJämf = Double.compare(b.getValue(), a.getValue()); // fallande pris - if (prisJämf != 0) return prisJämf; - return a.getKey().compareTo(b.getKey()); // stigande tid vid lika pris - }) - .collect(Collectors.toList()); - } else { - timpriser = timpriser.stream() - .sorted(Map.Entry.comparingByKey()) - .collect(Collectors.toList()); - } - System.out.println("\nElpriser per timme:"); - for (var entry : timpriser) { - String start = String.format("%02d", entry.getKey().getHour()); - String end = String.format("%02d", entry.getKey().plusHours(1).getHour()); - String tid = start + "-" + end; - String pris = String.format("%.2f", entry.getValue() * 100).replace('.', ','); - System.out.println(tid + " " + pris + " öre"); + timpriser = sorted + ? sorteraFallandeMedTid(timpriser) + : timpriser.stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()); - } + skrivUtTimpriser(timpriser); skrivUtStatistik(priser, datum); @@ -131,6 +114,27 @@ public List sorteraPriser(List priser, b .sorted(Comparator.comparingDouble(ElpriserAPI.Elpris::sekPerKWh).reversed()) .collect(Collectors.toList()); } + public static List> sorteraFallandeMedTid(List> lista) { + return lista.stream() + .sorted((a, b) -> { + int prisJämf = Double.compare(b.getValue(), a.getValue()); + if (prisJämf != 0) return prisJämf; + return a.getKey().compareTo(b.getKey()); + }) + .collect(Collectors.toList()); + } + + public void skrivUtTimpriser(List> timpriser) { + System.out.println("\nElpriser per timme:"); + for (var entry : timpriser) { + String start = String.format("%02d", entry.getKey().getHour()); + String end = String.format("%02d", entry.getKey().plusHours(1).getHour()); + String tid = start + "-" + end; + String pris = String.format("%.2f", entry.getValue() * 100).replace('.', ','); + System.out.println(tid + " " + pris + " öre"); + } + } + public void skrivUtStatistik(List priser, LocalDate datum) { List> timpriser = beräknaTimpriser(priser); double total = 0; diff --git a/src/test/java/com/example/MainTest.java b/src/test/java/com/example/MainTest.java index 6199d951..66ef5095 100644 --- a/src/test/java/com/example/MainTest.java +++ b/src/test/java/com/example/MainTest.java @@ -158,10 +158,11 @@ void displaySortedPrices_whenRequested() { // Expected sorted output (ascending by price) List expectedOrder = List.of( - "01-02 10,00 öre", - "03-04 10,00 öre", + + "00-01 30,00 öre", "02-03 20,00 öre", - "00-01 30,00 öre" + "01-02 10,00 öre", + "03-04 10,00 öre" ); // Extract actual lines that match the pattern From 9263b6f758f3c1ed5e41b9f6d1740618a17ae679 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Fri, 3 Oct 2025 10:15:49 +0200 Subject: [PATCH 4/4] =?UTF-8?q?N=C3=A4stan=20d=C3=A4r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/api/ElprisCLI.java | 48 +++++++++++--------- src/test/java/com/example/MainTest.java | 7 +-- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/example/api/ElprisCLI.java b/src/main/java/com/example/api/ElprisCLI.java index 6be0b752..5d49bd54 100644 --- a/src/main/java/com/example/api/ElprisCLI.java +++ b/src/main/java/com/example/api/ElprisCLI.java @@ -25,7 +25,7 @@ public class ElprisCLI implements Runnable { @Option(names = "--zone", description = "Elområde: SE1, SE2, SE3, SE4") private String zone; - @Option(names = "--sorted", description = "Sortera priser i fallande ordning (true/false)") + @Option(names = "--sorted", description = "Sortera priser i fallande ordning", defaultValue = "false") private boolean sorted; @@ -77,28 +77,11 @@ public void run() { List> timpriser = beräknaTimpriser(priser); - if (sorted) { - timpriser = timpriser.stream() - .sorted((a, b) -> { - int prisJämf = Double.compare(b.getValue(), a.getValue()); // fallande pris - if (prisJämf != 0) return prisJämf; - return a.getKey().compareTo(b.getKey()); // stigande tid vid lika pris - }) - .collect(Collectors.toList()); - } else { - timpriser = timpriser.stream() - .sorted(Map.Entry.comparingByKey()) - .collect(Collectors.toList()); - } - System.out.println("\nElpriser per timme:"); - for (var entry : timpriser) { - String start = String.format("%02d", entry.getKey().getHour()); - String end = String.format("%02d", entry.getKey().plusHours(1).getHour()); - String tid = start + "-" + end; - String pris = String.format("%.2f", entry.getValue() * 100).replace('.', ','); - System.out.println(tid + " " + pris + " öre"); + timpriser = sorted + ? sorteraFallandeMedTid(timpriser) + : timpriser.stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()); - } + skrivUtTimpriser(timpriser); skrivUtStatistik(priser, datum); @@ -131,6 +114,27 @@ public List sorteraPriser(List priser, b .sorted(Comparator.comparingDouble(ElpriserAPI.Elpris::sekPerKWh).reversed()) .collect(Collectors.toList()); } + public static List> sorteraFallandeMedTid(List> lista) { + return lista.stream() + .sorted((a, b) -> { + int prisJämf = Double.compare(b.getValue(), a.getValue()); + if (prisJämf != 0) return prisJämf; + return a.getKey().compareTo(b.getKey()); + }) + .collect(Collectors.toList()); + } + + public void skrivUtTimpriser(List> timpriser) { + System.out.println("\nElpriser per timme:"); + for (var entry : timpriser) { + String start = String.format("%02d", entry.getKey().getHour()); + String end = String.format("%02d", entry.getKey().plusHours(1).getHour()); + String tid = start + "-" + end; + String pris = String.format("%.2f", entry.getValue() * 100).replace('.', ','); + System.out.println(tid + " " + pris + " öre"); + } + } + public void skrivUtStatistik(List priser, LocalDate datum) { List> timpriser = beräknaTimpriser(priser); double total = 0; diff --git a/src/test/java/com/example/MainTest.java b/src/test/java/com/example/MainTest.java index 6199d951..66ef5095 100644 --- a/src/test/java/com/example/MainTest.java +++ b/src/test/java/com/example/MainTest.java @@ -158,10 +158,11 @@ void displaySortedPrices_whenRequested() { // Expected sorted output (ascending by price) List expectedOrder = List.of( - "01-02 10,00 öre", - "03-04 10,00 öre", + + "00-01 30,00 öre", "02-03 20,00 öre", - "00-01 30,00 öre" + "01-02 10,00 öre", + "03-04 10,00 öre" ); // Extract actual lines that match the pattern