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..5d49bd54 --- /dev/null +++ b/src/main/java/com/example/api/ElprisCLI.java @@ -0,0 +1,284 @@ +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", defaultValue = "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); + timpriser = sorted + ? sorteraFallandeMedTid(timpriser) + : timpriser.stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()); + + skrivUtTimpriser(timpriser); + + + 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 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; + 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(); + } + + + + + +} 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