Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
<mockito.version>5.19.0</mockito.version>
</properties>
<dependencies>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.5</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand All @@ -37,6 +42,18 @@
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<systemPropertyVariables>
<test.environment>true</test.environment>
</systemPropertyVariables>
<argLine>--enable-native-access=ALL-UNNAMED</argLine>
</configuration>
</plugin>

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
Expand Down
22 changes: 21 additions & 1 deletion src/main/java/com/example/Main.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
284 changes: 284 additions & 0 deletions src/main/java/com/example/api/ElprisCLI.java
Original file line number Diff line number Diff line change
@@ -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<String> 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=<SE1-SE4> [--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<ElpriserAPI.Elpris> priser = hämtaAllaPriser(datum, klass);
if (priser.isEmpty()) {
System.out.println("Inga priser hittades.");
return;
}


List<Map.Entry<LocalDateTime, Double>> 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<ElpriserAPI.Elpris> hämtaAllaPriser(LocalDate datum, ElpriserAPI.Prisklass klass) {
List<ElpriserAPI.Elpris> priser = new ArrayList<>();

List<ElpriserAPI.Elpris> idag = api.getPriser(datum, klass);
if (idag != null) priser.addAll(idag);

if (LocalTime.now().isAfter(LocalTime.of(13, 0))) {
List<ElpriserAPI.Elpris> imorgon = api.getPriser(datum.plusDays(1), klass);
if (imorgon != null) priser.addAll(imorgon);
}

return priser;
}


public List<ElpriserAPI.Elpris> sorteraPriser(List<ElpriserAPI.Elpris> 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<Map.Entry<LocalDateTime, Double>> sorteraFallandeMedTid(List<Map.Entry<LocalDateTime, Double>> 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<Map.Entry<LocalDateTime, Double>> 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<ElpriserAPI.Elpris> priser, LocalDate datum) {
List<Map.Entry<LocalDateTime, Double>> 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<LocalDate> 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<ElpriserAPI.Elpris> 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<Map.Entry<LocalDateTime, Double>> 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<Map.Entry<LocalDateTime, Double>> 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<Map.Entry<LocalDateTime, Double>> beräknaTimpriser(List<ElpriserAPI.Elpris> kvartpriser) {
// Runda ner till hel timme (inklusive sekunder och nanos)
Map<LocalDateTime, List<ElpriserAPI.Elpris>> 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();
}





}
7 changes: 4 additions & 3 deletions src/test/java/com/example/MainTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,11 @@ void displaySortedPrices_whenRequested() {

// Expected sorted output (ascending by price)
List<String> 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
Expand Down
Loading