77
88import java .io .ByteArrayOutputStream ;
99import java .io .PrintStream ;
10+ import java .text .DecimalFormat ;
11+ import java .text .DecimalFormatSymbols ;
1012import java .time .LocalDate ;
11- import java .util .List ;
13+ import java .util .*;
14+ import java .util .stream .Collectors ;
1215
1316import static org .assertj .core .api .Assertions .assertThat ;
17+ import static org .assertj .core .api .Assertions .within ;
1418import static org .junit .jupiter .api .Assertions .assertNotNull ;
1519import static org .junit .jupiter .api .Assertions .assertTrue ;
1620
@@ -77,24 +81,31 @@ void showHelp_whenNoArguments() {
7781 assertThat (output ).containsIgnoringCase ("usage" );
7882 assertThat (output ).containsIgnoringCase ("zone" );
7983 assertThat (output ).containsIgnoringCase ("date" );
84+ assertThat (output ).containsIgnoringCase ("sorted" );
85+
8086 }
8187
8288 @ Test
8389 void showHelp_withHelpFlag () {
8490 Main .main (new String []{"--help" });
8591
8692 String output = bos .toString ();
87- assertThat (output ).containsIgnoringCase ("electricity price optimizer" );
8893 assertThat (output ).containsIgnoringCase ("--zone" );
8994 assertThat (output ).containsIgnoringCase ("--date" );
95+ assertThat (output ).containsIgnoringCase ("--charging" );
96+ assertThat (output ).containsIgnoringCase ("--sorted" );
97+ assertThat (output ).containsIgnoringCase ("SE1" )
98+ .containsIgnoringCase ("SE2" )
99+ .containsIgnoringCase ("SE3" )
100+ .containsIgnoringCase ("SE4" );
90101 }
91102
92103 @ Test
93104 void displayMeanPrice_withValidData () {
94105 // Mock data with known values for predictable mean calculation
95106 String mockJson = """
96107 [{"SEK_per_kWh":0.10,"EUR_per_kWh":0.01,"EXR":10.0,"time_start":"2025-09-04T00:00:00+02:00","time_end":"2025-09-04T01:00:00+02:00"},
97- {"SEK_per_kWh":0.20 ,"EUR_per_kWh":0.02,"EXR":10.0,"time_start":"2025-09-04T01:00:00+02:00","time_end":"2025-09-04T02:00:00+02:00"},
108+ {"SEK_per_kWh":0.202 ,"EUR_per_kWh":0.02,"EXR":10.0,"time_start":"2025-09-04T01:00:00+02:00","time_end":"2025-09-04T02:00:00+02:00"},
98109 {"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"},
99110 {"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"}]""" ;
100111
@@ -123,28 +134,44 @@ void displayMinMaxPrices_withValidData() {
123134 String output = bos .toString ();
124135 assertThat (output ).containsIgnoringCase ("lägsta pris" );
125136 assertThat (output ).containsIgnoringCase ("högsta pris" );
137+ assertThat (output ).containsIgnoringCase ("medelpris" );
126138 assertThat (output ).contains ("01-02" ); // Cheapest hour (0.10)
127139 assertThat (output ).contains ("02-03" ); // Most expensive hour (0.80)
128- assertThat (output ).contains ("10" ); // 10 öre (cheapest)
129- assertThat (output ).contains ("80" ); // 80 öre (most expensive)
140+ assertThat (output ).contains ("10,00" ); // 10 öre (cheapest)
141+ assertThat (output ).contains ("80,00" ); // 80 öre (most expensive)
142+ assertThat (output ).contains ("80,00" ); // 42,50 öre (medelpris)
130143 }
131144
132145 @ Test
133146 void displaySortedPrices_whenRequested () {
134147 String mockJson = """
135148 [{"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"},
136149 {"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"},
137- {"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"}]""" ;
150+ {"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"},
151+ {"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"}]""" ;
138152
139153 ElpriserAPI .setMockResponse (mockJson );
140154
141155 Main .main (new String []{"--zone" , "SE2" , "--date" , "2025-09-04" , "--sorted" });
142156
143157 String output = bos .toString ();
144- // Should show prices in descending order
145- assertThat (output ).contains ("00-01 30 öre" );
146- assertThat (output ).contains ("02-03 20 öre" );
147- assertThat (output ).contains ("01-02 10 öre" );
158+
159+ // Expected sorted output (ascending by price)
160+ List <String > expectedOrder = List .of (
161+ "01-02 10,00 öre" ,
162+ "03-04 10,00 öre" ,
163+ "02-03 20,00 öre" ,
164+ "00-01 30,00 öre"
165+ );
166+
167+ // Extract actual lines that match the pattern
168+ List <String > actualSortedLines = Arrays .stream (output .split ("\n " ))
169+ .map (String ::trim ) // 1. Trim leading/trailing whitespace
170+ .filter (line -> line .matches ("^\\ d{2}-\\ d{2}\\ s+\\ d+,\\ d{2}\\ s+öre$" )) // 2. Use a more flexible regex
171+ .collect (Collectors .toList ());
172+
173+ // Assert that actual lines match expected order
174+ assertThat (actualSortedLines ).containsExactlyElementsOf (expectedOrder );
148175 }
149176
150177 @ Test
@@ -198,6 +225,7 @@ void findOptimalCharging8Hours() {
198225 for (int i = 0 ; i < prices .length ; i ++) {
199226 if (i > 0 ) jsonBuilder .append ("," );
200227 jsonBuilder .append (String .format (
228+ Locale .US ,
201229 """
202230 {"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"}""" ,
203231 prices [i ], prices [i ] / 10 , i , i + 1
@@ -212,21 +240,30 @@ void findOptimalCharging8Hours() {
212240 String output = bos .toString ();
213241 assertThat (output ).containsIgnoringCase ("påbörja laddning" );
214242 assertThat (output ).containsIgnoringCase ("medelpris" );
243+
244+ // Precise value checks
245+ // Cheapest 8-hour window is from hour 1 to hour 8 (prices[1] to prices[8])
246+ double expectedAvg = Arrays .stream (prices , 1 , 9 ).average ().orElseThrow ();
247+ String expectedStartHour = String .format ("%02d:00" , 1 );
248+ String expectedAvgStr = formatOre (expectedAvg );
249+
250+ assertThat (output ).contains ("kl " + expectedStartHour );
251+ assertThat (output ).contains ("Medelpris för fönster: " + expectedAvgStr + " öre" );
215252 }
216253
217254 @ Test
218255 void handleInvalidZone () {
219256 Main .main (new String []{"--zone" , "SE5" , "--date" , "2025-09-04" });
220257
221- String output = bos .toString ();
258+ String output = bos .toString (). toLowerCase () ;
222259 assertThat (output ).containsAnyOf ("invalid zone" , "ogiltig zon" , "fel zon" );
223260 }
224261
225262 @ Test
226263 void handleInvalidDate () {
227264 Main .main (new String []{"--zone" , "SE3" , "--date" , "invalid-date" });
228265
229- String output = bos .toString ();
266+ String output = bos .toString (). toLowerCase () ;
230267 assertThat (output ).containsAnyOf ("invalid date" , "ogiltigt datum" , "fel datum" );
231268 }
232269
@@ -256,7 +293,7 @@ void handleNoDataAvailable() {
256293
257294 Main .main (new String []{"--zone" , "SE3" , "--date" , "2025-09-04" });
258295
259- String output = bos .toString ();
296+ String output = bos .toString (). toLowerCase () ;
260297 assertThat (output ).containsAnyOf ("no data" , "ingen data" , "inga priser" );
261298 }
262299
@@ -277,4 +314,68 @@ void handleMultipleDaysData() {
277314 assertThat (output ).containsIgnoringCase ("påbörja laddning" );
278315 // Should be able to find optimal charging window across day boundary
279316 }
317+
318+ @ Test
319+ public void testHourlyMinMaxPrices () {
320+ List <Double > quarterHourPrices = new ArrayList <>();
321+
322+ // Simulate 96 prices: 24 hours, each with 4 quarter-hour prices
323+ for (int i = 0 ; i < 96 ; i ++) {
324+ quarterHourPrices .add ((double ) (i % 24 )); // repeating hourly pattern
325+ }
326+
327+ // Expected hourly averages
328+ List <Double > hourlyAverages = new ArrayList <>();
329+ for (int i = 0 ; i < 24 ; i ++) {
330+ double sum = 0 ;
331+ for (int j = 0 ; j < 4 ; j ++) {
332+ sum += quarterHourPrices .get (i * 4 + j );
333+ }
334+ hourlyAverages .add (sum / 4.0 );
335+ }
336+
337+ double expectedMin = Collections .min (hourlyAverages );
338+ double expectedMax = Collections .max (hourlyAverages );
339+
340+ // Call your method under test
341+ PriceRange result = PriceCalculator .calculateHourlyMinMax (quarterHourPrices );
342+
343+ assertThat (result .getMin ()).isCloseTo (expectedMin , within (0.001 ));
344+ assertThat (result .getMax ()).isCloseTo (expectedMax , within (0.001 ));
345+ }
346+
347+ private String formatOre (double sekPerKWh ) {
348+ double ore = sekPerKWh * 100.0 ;
349+ DecimalFormatSymbols symbols = DecimalFormatSymbols .getInstance (Locale .of ("sv" , "SE" ));
350+ DecimalFormat df = new DecimalFormat ("0.00" , symbols );
351+ return df .format (ore );
352+ }
280353}
354+ class PriceRange {
355+ private final double min ;
356+ private final double max ;
357+
358+ public PriceRange (double min , double max ) {
359+ this .min = min ;
360+ this .max = max ;
361+ }
362+
363+ public double getMin () { return min ; }
364+ public double getMax () { return max ; }
365+ }
366+
367+ class PriceCalculator {
368+ public static PriceRange calculateHourlyMinMax (List <Double > quarterHourPrices ) {
369+ List <Double > hourlyAverages = new ArrayList <>();
370+ for (int i = 0 ; i < 24 ; i ++) {
371+ double sum = 0 ;
372+ for (int j = 0 ; j < 4 ; j ++) {
373+ sum += quarterHourPrices .get (i * 4 + j );
374+ }
375+ hourlyAverages .add (sum / 4.0 );
376+ }
377+ double min = Collections .min (hourlyAverages );
378+ double max = Collections .max (hourlyAverages );
379+ return new PriceRange (min , max );
380+ }
381+ }
0 commit comments