2
2
3
3
import static com .google .common .collect .ImmutableList .toImmutableList ;
4
4
import static com .google .common .collect .ImmutableSortedSet .toImmutableSortedSet ;
5
- import static io .openems .common .jscalendar .JSCalendar .RecurrenceFrequency .WEEKLY ;
6
5
import static io .openems .common .utils .JsonUtils .getAsEnum ;
7
- import static io .openems .common .utils .JsonUtils .getAsJsonArray ;
8
6
import static io .openems .common .utils .JsonUtils .getAsJsonObject ;
9
- import static io .openems .common .utils .JsonUtils .getAsLocalDateTime ;
7
+ import static io .openems .common .utils .JsonUtils .getAsOptionalJsonArray ;
8
+ import static io .openems .common .utils .JsonUtils .getAsOptionalJsonObject ;
9
+ import static io .openems .common .utils .JsonUtils .getAsOptionalString ;
10
+ import static io .openems .common .utils .JsonUtils .getAsOptionalUUID ;
11
+ import static io .openems .common .utils .JsonUtils .getAsOptionalZonedDateTime ;
10
12
import static io .openems .common .utils .JsonUtils .getAsString ;
11
- import static io .openems .common .utils .JsonUtils .getAsUUID ;
12
- import static io .openems .common .utils .JsonUtils .getAsZonedDateTime ;
13
+ import static io .openems .common .utils .JsonUtils .parseToJsonArray ;
13
14
import static io .openems .common .utils .JsonUtils .stream ;
14
15
import static io .openems .common .utils .JsonUtils .toJsonArray ;
15
16
import static java .time .DayOfWeek .FRIDAY ;
19
20
import static java .time .DayOfWeek .THURSDAY ;
20
21
import static java .time .DayOfWeek .TUESDAY ;
21
22
import static java .time .DayOfWeek .WEDNESDAY ;
23
+ import static java .time .LocalDate .EPOCH ;
22
24
import static java .time .format .DateTimeFormatter .ISO_INSTANT ;
23
25
import static java .time .format .DateTimeFormatter .ISO_LOCAL_DATE_TIME ;
24
26
import static java .time .temporal .ChronoField .NANO_OF_DAY ;
25
27
import static java .time .temporal .TemporalAdjusters .nextOrSame ;
26
28
import static java .util .Arrays .stream ;
27
- import static java .util .UUID .randomUUID ;
28
29
29
30
import java .time .DayOfWeek ;
31
+ import java .time .Duration ;
32
+ import java .time .LocalDate ;
30
33
import java .time .LocalDateTime ;
34
+ import java .time .LocalTime ;
31
35
import java .time .ZonedDateTime ;
36
+ import java .time .format .DateTimeFormatter ;
37
+ import java .time .format .DateTimeParseException ;
38
+ import java .time .temporal .ChronoUnit ;
32
39
import java .util .NoSuchElementException ;
33
40
import java .util .UUID ;
34
41
import java .util .function .Consumer ;
58
65
public class JSCalendar <PAYLOAD > {
59
66
// CHECKSTYLE:ON
60
67
61
- public static record Task <PAYLOAD >(UUID uid , ZonedDateTime updated , LocalDateTime start ,
68
+ private static final String PROPERTY_PAYLOAD = "openems.io:payload" ;
69
+
70
+ public static record Task <PAYLOAD >(UUID uid , ZonedDateTime updated , LocalDateTime start , Duration duration ,
62
71
ImmutableList <RecurrenceRule > recurrenceRules , PAYLOAD payload ) {
63
72
73
+ /**
74
+ * Parse a List of {@link Task}s from a String representing a {@link JsonArray}
75
+ * - includes checks for null and empty.
76
+ *
77
+ * @param <PAYLOAD> the type of the Payload
78
+ * @param string the {@link JsonArray} string
79
+ * @param payloadParser a parser for a Payload
80
+ * @return the List of {@link Task}s
81
+ */
82
+ public static <PAYLOAD > ImmutableList <Task <PAYLOAD >> fromStringOrEmpty (String string ,
83
+ ThrowingFunction <JsonObject , PAYLOAD , OpenemsNamedException > payloadParser ) {
84
+ if (string == null || string .isBlank ()) {
85
+ return ImmutableList .of ();
86
+ }
87
+ try {
88
+ return fromJson (parseToJsonArray (string ), payloadParser );
89
+ } catch (OpenemsNamedException e ) {
90
+ e .printStackTrace ();
91
+ return ImmutableList .of ();
92
+ }
93
+ }
94
+
64
95
/**
65
96
* Parse a List of {@link Task}s from a {@link JsonArray}.
66
97
*
@@ -76,7 +107,7 @@ public static <PAYLOAD> ImmutableList<Task<PAYLOAD>> fromJson(JsonArray json,
76
107
return stream (json ) //
77
108
.map (j -> {
78
109
try {
79
- return fromJson (JsonUtils . getAsJsonObject (j ), payloadParser );
110
+ return fromJson (getAsJsonObject (j ), payloadParser );
80
111
} catch (OpenemsNamedException e ) {
81
112
e .printStackTrace ();
82
113
throw new NoSuchElementException (e .getMessage ());
@@ -121,55 +152,68 @@ public static <PAYLOAD> Task<PAYLOAD> fromJson(JsonObject json,
121
152
if (!type .equalsIgnoreCase ("Task" )) {
122
153
throw new OpenemsException ("This is not a 'Task': " + type );
123
154
}
124
- try {
125
- var uid = getAsUUID (json , "uid" );
126
- var updated = getAsZonedDateTime (json , "updated" );
127
- var start = getAsLocalDateTime (json , "start" );
128
- var recurrenceRules = stream (getAsJsonArray (json , "recurrenceRules" )) //
129
- .map (r -> {
130
- try {
131
- return RecurrenceRule .fromJson (r );
132
- } catch (OpenemsNamedException e ) {
133
- e .printStackTrace ();
134
- throw new NoSuchElementException (e .getMessage ());
135
- }
136
- }) //
137
- .collect (toImmutableList ());
138
- var payload = payloadParser .apply (getAsJsonObject (json , "payload" ));
139
- return new Task <PAYLOAD >(uid , updated , start , recurrenceRules , payload );
140
-
141
- } catch (NoSuchElementException e ) {
142
- throw new OpenemsException ("NoSuchElementException: " + e .getMessage ());
143
- }
155
+ var b = Task .<PAYLOAD >create () //
156
+ .setUid (getAsOptionalUUID (json , "uid" ).orElse (null )) //
157
+ .setUpdated (getAsOptionalZonedDateTime (json , "updated" ).orElse (null )) //
158
+ .setStart (getAsString (json , "start" )) //
159
+ .setDuration (getAsOptionalString (json , "duration" ).orElse (null )); //
160
+ getAsOptionalJsonArray (json , "recurrenceRules" ) //
161
+ .ifPresent (j -> stream (j ) //
162
+ .forEach (r -> b .addRecurrenceRule (r )));
163
+ var rawPayload = getAsOptionalJsonObject (json , PROPERTY_PAYLOAD );
164
+ b .setPayload (rawPayload .isPresent () //
165
+ ? payloadParser .apply (rawPayload .get ()) //
166
+ : null );
167
+ return b .build ();
144
168
}
145
169
146
170
public static class Builder <PAYLOAD > {
147
- private final UUID uid ;
148
- private final ZonedDateTime updated ;
149
-
171
+ private UUID uid = null ;
172
+ private ZonedDateTime updated = null ;
150
173
private LocalDateTime start = null ;
174
+ private Duration duration = null ;
151
175
private ImmutableList .Builder <RecurrenceRule > recurrenceRules = ImmutableList .builder ();
152
176
private PAYLOAD payload = null ;
153
177
154
178
protected Builder () {
155
- this (randomUUID (), ZonedDateTime .now ());
156
179
}
157
180
158
- protected Builder (UUID uid , ZonedDateTime updated ) {
181
+ public Builder < PAYLOAD > setUid (UUID uid ) {
159
182
this .uid = uid ;
183
+ return this ;
184
+ }
185
+
186
+ public Builder <PAYLOAD > setUpdated (ZonedDateTime updated ) {
160
187
this .updated = updated ;
188
+ return this ;
161
189
}
162
190
163
191
public Builder <PAYLOAD > setStart (LocalDateTime start ) {
164
192
this .start = start ;
165
193
return this ;
166
194
}
167
195
168
- protected Builder <PAYLOAD > setStart (String start ) {
169
- this .setStart (LocalDateTime .parse (start ));
196
+ public Builder <PAYLOAD > setStart (LocalTime start ) {
197
+ return this .setStart (LocalDateTime .of (EPOCH , start ));
198
+ }
199
+
200
+ protected Builder <PAYLOAD > setStart (String start ) throws DateTimeParseException {
201
+ try {
202
+ return this .setStart (LocalDateTime .parse (start ));
203
+ } catch (DateTimeParseException e ) {
204
+ return this .setStart (LocalTime .parse (start ));
205
+ }
206
+ }
207
+
208
+ public Builder <PAYLOAD > setDuration (Duration duration ) {
209
+ this .duration = duration ;
170
210
return this ;
171
211
}
172
212
213
+ protected Builder <PAYLOAD > setDuration (String duration ) {
214
+ return this .setDuration (duration == null ? null : Duration .parse (duration ));
215
+ }
216
+
173
217
/**
174
218
* Adds a {@link RecurrenceRule}.
175
219
*
@@ -181,6 +225,21 @@ public Builder<PAYLOAD> addRecurrenceRule(RecurrenceRule recurrenceRule) {
181
225
return this ;
182
226
}
183
227
228
+ /**
229
+ * Adds a {@link RecurrenceRule}.
230
+ *
231
+ * @param json the {@link RecurrenceRule} as {@link JsonObject}
232
+ * @return myself
233
+ */
234
+ public Builder <PAYLOAD > addRecurrenceRule (JsonElement json ) throws NoSuchElementException {
235
+ try {
236
+ return this .addRecurrenceRule (RecurrenceRule .fromJson (json ));
237
+ } catch (OpenemsNamedException e ) {
238
+ e .printStackTrace ();
239
+ throw new NoSuchElementException (e .getMessage ());
240
+ }
241
+ }
242
+
184
243
/**
185
244
* Adds a {@link RecurrenceRule}.
186
245
*
@@ -200,8 +259,8 @@ public Builder<PAYLOAD> setPayload(PAYLOAD payload) {
200
259
}
201
260
202
261
public Task <PAYLOAD > build () {
203
- return new Task <PAYLOAD >(this .uid , this .updated , this .start , this .recurrenceRules . build () ,
204
- this .payload );
262
+ return new Task <PAYLOAD >(this .uid , this .updated , this .start , this .duration ,
263
+ this .recurrenceRules . build (), this . payload );
205
264
}
206
265
}
207
266
@@ -223,33 +282,48 @@ public static <PAYLOAD> Builder<PAYLOAD> create() {
223
282
*/
224
283
public JsonObject toJson (Function <PAYLOAD , JsonObject > payloadConverter ) {
225
284
var j = JsonUtils .buildJsonObject () //
226
- .addProperty ("@type" , "Task" ) //
227
- .addProperty ("uid" , this .uid .toString ()) //
228
- .addProperty ("updated" , this .updated .format (ISO_INSTANT ));
285
+ .addProperty ("@type" , "Task" );
286
+ if (this .uid != null ) {
287
+ j .addProperty ("uid" , this .uid .toString ());
288
+ }
289
+ if (this .updated != null ) {
290
+ j .addProperty ("updated" , this .updated .format (ISO_INSTANT ));
291
+ }
229
292
if (this .start != null ) {
230
- j .addProperty ("start" , this .start .format (ISO_LOCAL_DATE_TIME ));
293
+ if (LocalDate .from (this .start ).equals (EPOCH )) {
294
+ j .addProperty ("start" , this .start .format (DateTimeFormatter .ISO_LOCAL_TIME ));
295
+ } else {
296
+ j .addProperty ("start" , this .start .format (ISO_LOCAL_DATE_TIME ));
297
+ }
298
+ }
299
+ if (this .duration != null ) {
300
+ j .addProperty ("duration" , this .duration .toString ());
231
301
}
232
302
if (!this .recurrenceRules .isEmpty ()) {
233
303
j .add ("recurrenceRules" , this .recurrenceRules .stream () //
234
304
.map (RecurrenceRule ::toJson ) //
235
305
.collect (toJsonArray ()));
236
306
}
237
307
if (this .payload != null ) {
238
- j .add ("payload" , payloadConverter .apply (this .payload ));
308
+ j .add (PROPERTY_PAYLOAD , payloadConverter .apply (this .payload ));
239
309
}
240
310
return j .build ();
241
311
}
242
312
243
313
/**
244
- * Gets the next occurence of the {@link Task} at or after a date.
314
+ * Gets the next occurence of the {@link Task} (including duration) at or after
315
+ * a date.
245
316
*
246
317
* @param from the from timestamp
247
318
* @return a {@link ZonedDateTime}
248
319
*/
249
320
public ZonedDateTime getNextOccurence (ZonedDateTime from ) {
321
+ var f = this .duration == null //
322
+ ? from //
323
+ : from .minus (this .duration ); // query active tasks
250
324
var start = this .start .atZone (from .getZone ());
251
325
return this .recurrenceRules .stream () //
252
- .map (rr -> rr .getNextOccurence (from .isBefore (start ) ? start : from , start )) //
326
+ .map (rr -> rr .getNextOccurence (f .isBefore (start ) ? start : f , start )) //
253
327
.min ((o1 , o2 ) -> o1 .toInstant ().compareTo (o2 .toInstant ())) //
254
328
.orElse (null );
255
329
}
@@ -282,18 +356,20 @@ public record RecurrenceRule(RecurrenceFrequency frequency, ImmutableSortedSet<D
282
356
*/
283
357
public static RecurrenceRule fromJson (JsonElement json ) throws OpenemsNamedException , NoSuchElementException {
284
358
var frequency = getAsEnum (RecurrenceFrequency .class , json , "frequency" );
285
- var byDay = stream (getAsJsonArray (json , "byDay" )) //
286
- .map (j -> switch (JsonUtils .getAsOptionalString (j ).orElseThrow ()) {
287
- case "mo" -> MONDAY ;
288
- case "tu" -> TUESDAY ;
289
- case "we" -> WEDNESDAY ;
290
- case "th" -> THURSDAY ;
291
- case "fr" -> FRIDAY ;
292
- case "sa" -> SATURDAY ;
293
- case "su" -> SUNDAY ;
294
- default -> throw new NoSuchElementException ("" );
295
- }) //
296
- .collect (toImmutableSortedSet (Ordering .natural ()));
359
+ var byDay = getAsOptionalJsonArray (json , "byDay" ) //
360
+ .map (arr -> stream (arr ) //
361
+ .map (j -> switch (getAsOptionalString (j ).orElseThrow ()) {
362
+ case "mo" -> MONDAY ;
363
+ case "tu" -> TUESDAY ;
364
+ case "we" -> WEDNESDAY ;
365
+ case "th" -> THURSDAY ;
366
+ case "fr" -> FRIDAY ;
367
+ case "sa" -> SATURDAY ;
368
+ case "su" -> SUNDAY ;
369
+ default -> throw new NoSuchElementException ("" );
370
+ }) //
371
+ .collect (toImmutableSortedSet (Ordering .natural ()))) //
372
+ .orElse (ImmutableSortedSet .of ());
297
373
return new RecurrenceRule (frequency , byDay );
298
374
}
299
375
@@ -342,21 +418,36 @@ public static Builder create() {
342
418
* @return a {@link ZonedDateTime}
343
419
*/
344
420
public ZonedDateTime getNextOccurence (ZonedDateTime from , ZonedDateTime start ) {
345
- if (this .frequency == WEEKLY ) {
421
+ final var startTime = start .toLocalTime ();
422
+
423
+ return switch (this .frequency ) {
424
+ case DAILY -> {
425
+ var resultDay = from .truncatedTo (ChronoUnit .DAYS );
426
+ if (from .toLocalTime ().isAfter (startTime )) {
427
+ resultDay = from .plusDays (1 );
428
+ }
429
+ yield resultDay .with (NANO_OF_DAY , startTime .toNanoOfDay ());
430
+ }
431
+ case WEEKLY -> {
346
432
if (!this .byDay .isEmpty ()) {
347
- var startTime = start .toLocalTime ();
348
433
var nextByDay = this .byDay .ceiling (from .toLocalTime ().isAfter (startTime ) //
349
434
? from .getDayOfWeek ().plus (1 ) // next day
350
435
: from .getDayOfWeek ()); // same day
351
436
if (nextByDay == null ) {
352
437
nextByDay = this .byDay .first ();
353
438
}
354
- return from //
439
+ yield from //
355
440
.with (nextOrSame (nextByDay )) //
356
441
.with (NANO_OF_DAY , startTime .toNanoOfDay ());
357
442
}
443
+ // TODO: If frequency is weekly and there is no byDay property, add a byDay
444
+ // property with the sole value being the day of the week of the initial
445
+ // date-time.
446
+ yield null ; // not implemented
358
447
}
359
- return null ;
448
+ case MONTHLY -> null ; // not implemented
449
+ case YEARLY -> null ; // not implemented
450
+ };
360
451
}
361
452
362
453
/**
0 commit comments