Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright © 2018 Endless Mobile, Inc.
4 : : *
5 : : * This library is free software; you can redistribute it and/or
6 : : * modify it under the terms of the GNU Lesser General Public
7 : : * License as published by the Free Software Foundation; either
8 : : * version 2.1 of the License, or (at your option) any later version.
9 : : *
10 : : * This library is distributed in the hope that it will be useful,
11 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 : : * Lesser General Public License for more details.
14 : : *
15 : : * You should have received a copy of the GNU Lesser General Public
16 : : * License along with this library; if not, write to the Free Software
17 : : * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 : : *
19 : : * Authors:
20 : : * - Philip Withnall <withnall@endlessm.com>
21 : : */
22 : :
23 : : #include "config.h"
24 : :
25 : : #include <glib/gi18n-lib.h>
26 : : #include <glib.h>
27 : : #include <glib-object.h>
28 : : #include <gio/gio.h>
29 : : #include <libmogwai-tariff/enums.h>
30 : : #include <libmogwai-tariff/period.h>
31 : :
32 : :
33 [ + + ]: 12 : G_DEFINE_QUARK (MwtPeriodError, mwt_period_error)
34 : :
35 : : static void mwt_period_constructed (GObject *object);
36 : : static void mwt_period_dispose (GObject *object);
37 : : static void mwt_period_get_property (GObject *object,
38 : : guint property_id,
39 : : GValue *value,
40 : : GParamSpec *pspec);
41 : : static void mwt_period_set_property (GObject *object,
42 : : guint property_id,
43 : : const GValue *value,
44 : : GParamSpec *pspec);
45 : :
46 : : /**
47 : : * MwtPeriod:
48 : : *
49 : : * A representation of a period in a tariff where the tariff properties are
50 : : * constant (for example, a single capacity limit applies to the whole period).
51 : : *
52 : : * It has a start and end time, and properties which control how it repeats (if
53 : : * at all). The start time is inclusive, but the end time is exclusive (which
54 : : * makes handling of leap seconds at the end of a period easier).
55 : : *
56 : : * Repeats take leap years and timezone changes into account. For example, if a
57 : : * period spans 01:00 to 06:00 on 31st January, and repeats every month, a
58 : : * recurrence will happen on 28th February (or 29th February on a leap year),
59 : : * on 31st March, 30th April, etc.
60 : : *
61 : : * If a period spans 01:00 to 02:00 on a normal day, and a DST transition
62 : : * happens where the clocks go forward by 1 hour at 01:00 on a certain day, any
63 : : * recurrence of the period on that day will be skipped. Recurrences on days
64 : : * after the DST transition will happen at 01:00 to 02:00 in the new timezone.
65 : : *
66 : : * For a DST transition where the clocks go backward by 1 hour at 02:00 on a
67 : : * certain day, the time span 01:00–02:00 will happen twice. Any recurrence of a
68 : : * period which spans 01:00 to 02:00 will happen on the first occurrence of the
69 : : * time span, and will not repeat during the second occurrence.
70 : : *
71 : : * The #MwtPeriod class is immutable once loaded or constructed.
72 : : *
73 : : * Since: 0.1.0
74 : : */
75 : : struct _MwtPeriod
76 : : {
77 : : GObject parent;
78 : :
79 : : GDateTime *start; /* (owned) */
80 : : GDateTime *end; /* (owned) */
81 : :
82 : : MwtPeriodRepeatType repeat_type;
83 : : guint repeat_period;
84 : :
85 : : guint64 capacity_limit;
86 : : };
87 : :
88 : : typedef enum
89 : : {
90 : : PROP_START = 1,
91 : : PROP_END,
92 : : PROP_REPEAT_TYPE,
93 : : PROP_REPEAT_PERIOD,
94 : : PROP_CAPACITY_LIMIT,
95 : : } MwtPeriodProperty;
96 : :
97 [ + + + - : 3476 : G_DEFINE_TYPE (MwtPeriod, mwt_period, G_TYPE_OBJECT)
+ + ]
98 : :
99 : : static void
100 : 13 : mwt_period_class_init (MwtPeriodClass *klass)
101 : : {
102 : 13 : GObjectClass *object_class = (GObjectClass *) klass;
103 : 13 : GParamSpec *props[PROP_CAPACITY_LIMIT + 1] = { NULL, };
104 : :
105 : 13 : object_class->constructed = mwt_period_constructed;
106 : 13 : object_class->dispose = mwt_period_dispose;
107 : 13 : object_class->get_property = mwt_period_get_property;
108 : 13 : object_class->set_property = mwt_period_set_property;
109 : :
110 : : /**
111 : : * MwtPeriod:start:
112 : : *
113 : : * Date/Time when the period starts for the first time (inclusive).
114 : : *
115 : : * Since: 0.1.0
116 : : */
117 : 13 : props[PROP_START] =
118 : 13 : g_param_spec_boxed ("start", "Start Date/Time",
119 : : "Date/Time when the period starts for the first time.",
120 : : G_TYPE_DATE_TIME,
121 : : G_PARAM_READWRITE |
122 : : G_PARAM_CONSTRUCT_ONLY |
123 : : G_PARAM_STATIC_STRINGS);
124 : :
125 : : /**
126 : : * MwtPeriod:end:
127 : : *
128 : : * Date/Time when the period ends for the first time (exclusive). Repeats of
129 : : * this period will occur after this date/time.
130 : : *
131 : : * Since: 0.1.0
132 : : */
133 : 13 : props[PROP_END] =
134 : 13 : g_param_spec_boxed ("end", "End Date/Time",
135 : : "Date/Time when the period ends for the first time.",
136 : : G_TYPE_DATE_TIME,
137 : : G_PARAM_READWRITE |
138 : : G_PARAM_CONSTRUCT_ONLY |
139 : : G_PARAM_STATIC_STRINGS);
140 : :
141 : : /**
142 : : * MwtPeriod:repeat-type:
143 : : *
144 : : * How this period repeats (if at all).
145 : : *
146 : : * A more generalised way of repeating periods may be added in future,
147 : : * following the example of iCalendar’s `RRULE`.
148 : : *
149 : : * Since: 0.1.0
150 : : */
151 : 13 : props[PROP_REPEAT_TYPE] =
152 : 13 : g_param_spec_enum ("repeat-type", "Repeat Type",
153 : : "How this period repeats (if at all).",
154 : : MWT_TYPE_PERIOD_REPEAT_TYPE,
155 : : MWT_PERIOD_REPEAT_NONE,
156 : : G_PARAM_READWRITE |
157 : : G_PARAM_CONSTRUCT_ONLY |
158 : : G_PARAM_STATIC_STRINGS);
159 : :
160 : : /**
161 : : * MwtPeriod:repeat-period:
162 : : *
163 : : * The period between repeats of this period. For example, if
164 : : * #MwtPeriod:repeat-type was %MWT_PERIOD_REPEAT_HOUR and
165 : : * #MwtPeriod:repeat-period was 6, this period would repeat once every 6
166 : : * hours.
167 : : *
168 : : * Since: 0.1.0
169 : : */
170 : 13 : props[PROP_REPEAT_PERIOD] =
171 : 13 : g_param_spec_uint ("repeat-period", "Repeat Period",
172 : : "The period between repeats of this period.",
173 : : 0, G_MAXUINT, 0,
174 : : G_PARAM_READWRITE |
175 : : G_PARAM_CONSTRUCT_ONLY |
176 : : G_PARAM_STATIC_STRINGS);
177 : :
178 : : /**
179 : : * MwtPeriod:capacity-limit:
180 : : *
181 : : * Limit on the download capacity allowed during each repeat of this period,
182 : : * in bytes. If this is zero, no downloading is allowed during any repeat of
183 : : * this period. If it is %G_MAXUINT64, no limit is applied.
184 : : *
185 : : * The default is %G_MAXUINT64 (no limit).
186 : : *
187 : : * Since: 0.1.0
188 : : */
189 : 13 : props[PROP_CAPACITY_LIMIT] =
190 : 13 : g_param_spec_uint64 ("capacity-limit", "Capacity Limit",
191 : : "Limit on the download capacity allowed during "
192 : : "each repeat of this period, in bytes.",
193 : : 0, G_MAXUINT64, G_MAXUINT64,
194 : : G_PARAM_READWRITE |
195 : : G_PARAM_CONSTRUCT_ONLY |
196 : : G_PARAM_STATIC_STRINGS);
197 : :
198 : 13 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
199 : 13 : }
200 : :
201 : : static void
202 : 142 : mwt_period_init (MwtPeriod *self)
203 : : {
204 : : /* Nothing to do here. */
205 : 142 : }
206 : :
207 : : static void
208 : 142 : mwt_period_constructed (GObject *object)
209 : : {
210 : 142 : MwtPeriod *self = MWT_PERIOD (object);
211 : :
212 : 142 : G_OBJECT_CLASS (mwt_period_parent_class)->constructed (object);
213 : :
214 : : /* Validate properties. */
215 [ - + ]: 142 : g_assert (mwt_period_validate (self->start, self->end, self->repeat_type,
216 : : self->repeat_period, NULL));
217 : 142 : }
218 : :
219 : : static void
220 : 142 : mwt_period_dispose (GObject *object)
221 : : {
222 : 142 : MwtPeriod *self = MWT_PERIOD (object);
223 : :
224 [ + - ]: 142 : g_clear_pointer (&self->start, g_date_time_unref);
225 [ + - ]: 142 : g_clear_pointer (&self->end, g_date_time_unref);
226 : :
227 : : /* Chain up to the parent class */
228 : 142 : G_OBJECT_CLASS (mwt_period_parent_class)->dispose (object);
229 : 142 : }
230 : :
231 : : static void
232 : 5 : mwt_period_get_property (GObject *object,
233 : : guint property_id,
234 : : GValue *value,
235 : : GParamSpec *pspec)
236 : : {
237 : 5 : MwtPeriod *self = MWT_PERIOD (object);
238 : :
239 [ + + + + : 5 : switch ((MwtPeriodProperty) property_id)
+ - ]
240 : : {
241 : 1 : case PROP_START:
242 : 1 : g_value_set_boxed (value, self->start);
243 : 1 : break;
244 : 1 : case PROP_END:
245 : 1 : g_value_set_boxed (value, self->end);
246 : 1 : break;
247 : 1 : case PROP_REPEAT_TYPE:
248 : 1 : g_value_set_enum (value, self->repeat_type);
249 : 1 : break;
250 : 1 : case PROP_REPEAT_PERIOD:
251 : 1 : g_value_set_uint (value, self->repeat_period);
252 : 1 : break;
253 : 1 : case PROP_CAPACITY_LIMIT:
254 : 1 : g_value_set_uint64 (value, self->capacity_limit);
255 : 1 : break;
256 : 0 : default:
257 : 0 : g_assert_not_reached ();
258 : : }
259 : 5 : }
260 : :
261 : : static void
262 : 710 : mwt_period_set_property (GObject *object,
263 : : guint property_id,
264 : : const GValue *value,
265 : : GParamSpec *pspec)
266 : : {
267 : 710 : MwtPeriod *self = MWT_PERIOD (object);
268 : :
269 [ + + + + : 710 : switch ((MwtPeriodProperty) property_id)
+ - ]
270 : : {
271 : 142 : case PROP_START:
272 : : /* Construct only. */
273 [ - + ]: 142 : g_assert (self->start == NULL);
274 : 142 : self->start = g_value_dup_boxed (value);
275 : 142 : break;
276 : 142 : case PROP_END:
277 : : /* Construct only. */
278 [ - + ]: 142 : g_assert (self->end == NULL);
279 : 142 : self->end = g_value_dup_boxed (value);
280 : 142 : break;
281 : 142 : case PROP_REPEAT_TYPE:
282 : : /* Construct only. */
283 [ - + ]: 142 : g_assert (self->repeat_type == MWT_PERIOD_REPEAT_NONE);
284 : 142 : self->repeat_type = g_value_get_enum (value);
285 : 142 : break;
286 : 142 : case PROP_REPEAT_PERIOD:
287 : : /* Construct only. */
288 [ - + ]: 142 : g_assert (self->repeat_period == 0);
289 : 142 : self->repeat_period = g_value_get_uint (value);
290 : 142 : break;
291 : 142 : case PROP_CAPACITY_LIMIT:
292 : : /* Construct only. */
293 [ - + ]: 142 : g_assert (self->capacity_limit == 0);
294 : 142 : self->capacity_limit = g_value_get_uint64 (value);
295 : 142 : break;
296 : 0 : default:
297 : 0 : g_assert_not_reached ();
298 : : }
299 : 710 : }
300 : :
301 : : /**
302 : : * mwt_period_validate:
303 : : * @start: (nullable): start date/time (see #MwtPeriod:start)
304 : : * @end: (nullable): end date/time (see #MwtPeriod:end)
305 : : * @repeat_type: repeat type (see #MwtPeriod:repeat-type)
306 : : * @repeat_period: repeat period (see #MwtPeriod:repeat-period)
307 : : * @error: return location for a #GError, or %NULL
308 : : *
309 : : * Validate the given #MwtPeriod properties, returning %MWT_PERIOD_ERROR_INVALID
310 : : * if any of them are invalid. All inputs are allowed to the property arguments
311 : : * (except @error): no inputs are a programmer error.
312 : : *
313 : : * It is guaranteed that if this function returns %TRUE for a given set of
314 : : * inputs, mwt_period_new() will succeed for those inputs.
315 : : *
316 : : * Returns: %TRUE if valid, %FALSE otherwise
317 : : * Since: 0.1.0
318 : : */
319 : : gboolean
320 : 163 : mwt_period_validate (GDateTime *start,
321 : : GDateTime *end,
322 : : MwtPeriodRepeatType repeat_type,
323 : : guint repeat_period,
324 : : GError **error)
325 : : {
326 [ + + - + ]: 163 : g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
327 : :
328 [ + + + + ]: 163 : if (start == NULL ||
329 [ + + ]: 161 : end == NULL ||
330 : 161 : g_date_time_compare (start, end) >= 0)
331 : : {
332 : 3 : g_set_error_literal (error, MWT_PERIOD_ERROR, MWT_PERIOD_ERROR_INVALID,
333 : : _("Invalid start/end times for period."));
334 : 3 : return FALSE;
335 : : }
336 : :
337 [ + - ]: 160 : if (!((gint) repeat_type >= MWT_PERIOD_REPEAT_NONE &&
338 [ + + ]: 160 : (gint) repeat_type <= MWT_PERIOD_REPEAT_YEAR))
339 : : {
340 : 1 : g_set_error_literal (error, MWT_PERIOD_ERROR, MWT_PERIOD_ERROR_INVALID,
341 : : _("Invalid repeat type for period."));
342 : 1 : return FALSE;
343 : : }
344 : :
345 [ + + ]: 159 : if ((repeat_type == MWT_PERIOD_REPEAT_NONE) != (repeat_period == 0))
346 : : {
347 : 2 : g_set_error_literal (error, MWT_PERIOD_ERROR, MWT_PERIOD_ERROR_INVALID,
348 : : _("Invalid repeat properties for period."));
349 : 2 : return FALSE;
350 : : }
351 : :
352 : 157 : return TRUE;
353 : : }
354 : :
355 : : /**
356 : : * mwt_period_new:
357 : : * @start: start date/time (see #MwtPeriod:start)
358 : : * @end: end date/time (see #MwtPeriod:end)
359 : : * @repeat_type: repeat type (see #MwtPeriod:repeat-type)
360 : : * @repeat_period: repeat period (see #MwtPeriod:repeat-period)
361 : : * @...: additional properties, given as property name followed by value, with
362 : : * a %NULL terminator
363 : : *
364 : : * Create a #MwtPeriod object with the given properties. The @start, @end,
365 : : * @repeat_type and @repeat_period properties are required. The varargs can be
366 : : * used to specify the limits which apply to this period and which differ from
367 : : * their default values. The varargs are specified in the same format as used
368 : : * by g_object_new(), and the list must be %NULL terminated.
369 : : *
370 : : * Note that any 64-bit varargs must be cast to the correct type (for example,
371 : : * using G_GUINT64_CONSTANT()), or the wrong number of bytes will be put on
372 : : * the varargs list on non-64-bit architectures.
373 : : *
374 : : * All inputs to this function must have been validated with
375 : : * mwt_period_validate() first. It is a programmer error to provide invalid
376 : : * inputs.
377 : : *
378 : : * Returns: (transfer full): a new #MwtPeriod
379 : : * Since: 0.1.0
380 : : */
381 : : MwtPeriod *
382 : 142 : mwt_period_new (GDateTime *start,
383 : : GDateTime *end,
384 : : MwtPeriodRepeatType repeat_type,
385 : : guint repeat_period,
386 : : const gchar *first_property_name,
387 : : ...)
388 : : {
389 : : /* Leave property validation to constructed(). */
390 : :
391 : : /* FIXME: This is mildly horrific. Do something clever with
392 : : * g_object_new_with_properties(). */
393 : 142 : guint64 capacity_limit = G_MAXUINT64;
394 [ + + ]: 142 : if (g_strcmp0 (first_property_name, "capacity-limit") == 0)
395 : : {
396 : : va_list args;
397 : 28 : va_start (args, first_property_name);
398 : 28 : capacity_limit = va_arg (args, guint64);
399 : 28 : va_end (args);
400 : : }
401 : :
402 : 142 : return g_object_new (MWT_TYPE_PERIOD,
403 : : "start", start,
404 : : "end", end,
405 : : "repeat-type", repeat_type,
406 : : "repeat-period", repeat_period,
407 : : "capacity-limit", capacity_limit,
408 : : NULL);
409 : : }
410 : :
411 : : /**
412 : : * mwt_period_get_start:
413 : : * @self: a #MwtPeriod
414 : : *
415 : : * Get the value of #MwtPeriod:start.
416 : : *
417 : : * Returns: start date/time (inclusive)
418 : : * Since: 0.1.0
419 : : */
420 : : GDateTime *
421 : 547 : mwt_period_get_start (MwtPeriod *self)
422 : : {
423 [ - + ]: 547 : g_return_val_if_fail (MWT_IS_PERIOD (self), NULL);
424 : :
425 : 547 : return self->start;
426 : : }
427 : :
428 : : /**
429 : : * mwt_period_get_end:
430 : : * @self: a #MwtPeriod
431 : : *
432 : : * Get the value of #MwtPeriod:end.
433 : : *
434 : : * Returns: end date/time (exclusive)
435 : : * Since: 0.1.0
436 : : */
437 : : GDateTime *
438 : 534 : mwt_period_get_end (MwtPeriod *self)
439 : : {
440 [ - + ]: 534 : g_return_val_if_fail (MWT_IS_PERIOD (self), NULL);
441 : :
442 : 534 : return self->end;
443 : : }
444 : :
445 : : /**
446 : : * mwt_period_get_repeat_type:
447 : : * @self: a #MwtPeriod
448 : : *
449 : : * Get the value of #MwtPeriod:repeat-type.
450 : : *
451 : : * Returns: repeat type
452 : : * Since: 0.1.0
453 : : */
454 : : MwtPeriodRepeatType
455 : 25 : mwt_period_get_repeat_type (MwtPeriod *self)
456 : : {
457 [ - + ]: 25 : g_return_val_if_fail (MWT_IS_PERIOD (self), MWT_PERIOD_REPEAT_NONE);
458 : :
459 [ + + - + ]: 25 : g_assert (self->repeat_type != MWT_PERIOD_REPEAT_NONE ||
460 : : self->repeat_period == 0);
461 : :
462 : 25 : return self->repeat_type;
463 : : }
464 : :
465 : : /**
466 : : * mwt_period_get_repeat_period:
467 : : * @self: a #MwtPeriod
468 : : *
469 : : * Get the value of #MwtPeriod:repeat-period.
470 : : *
471 : : * Returns: repeat period
472 : : * Since: 0.1.0
473 : : */
474 : : guint
475 : 25 : mwt_period_get_repeat_period (MwtPeriod *self)
476 : : {
477 [ - + ]: 25 : g_return_val_if_fail (MWT_IS_PERIOD (self), 0);
478 : :
479 [ + + - + ]: 25 : g_assert (self->repeat_period != 0 ||
480 : : self->repeat_type == MWT_PERIOD_REPEAT_NONE);
481 : :
482 : 25 : return self->repeat_period;
483 : : }
484 : :
485 : : /**
486 : : * mwt_period_get_capacity_limit:
487 : : * @self: a #MwtPeriod
488 : : *
489 : : * Get the value of #MwtPeriod:capacity-limit.
490 : : *
491 : : * Returns: capacity limit, in bytes
492 : : * Since: 0.1.0
493 : : */
494 : : guint64
495 : 120 : mwt_period_get_capacity_limit (MwtPeriod *self)
496 : : {
497 [ - + ]: 120 : g_return_val_if_fail (MWT_IS_PERIOD (self), 0);
498 : :
499 : 120 : return self->capacity_limit;
500 : : }
501 : :
502 : : /* Add @n repeat periods to #MwtPeriod:start and #MwtPeriod:end, and return a
503 : : * new #GDateTime for each of them. @n must be positive.
504 : : *
505 : : * @out_start and @out_end are both (inout), ignoring and freeing whatever old
506 : : * value is passed in, and returning the result.
507 : : *
508 : : * If either of the dates could not be updated (if we’ve hit the limits in the
509 : : * date type, for example), %FALSE will be returned and @out_start and @out_end
510 : : * will be set to %NULL.
511 : : *
512 : : * The #MwtPeriod:repeat-type must not be %MWT_PERIOD_REPEAT_NONE, and the
513 : : * #MwtPeriod:repeat-period must not be zero.
514 : : *
515 : : * @out_was_empty is set to %TRUE (and %FALSE is returned) if the nth recurrence
516 : : * of the period was empty (for example, due to DST adjustments). See
517 : : * get_nth_recurrence_skip_empty() for the recommended way to handle this.
518 : : */
519 : : static gboolean
520 : 1767 : get_nth_recurrence (MwtPeriod *self,
521 : : guint64 n,
522 : : GDateTime **out_start,
523 : : GDateTime **out_end,
524 : : gboolean *out_was_empty)
525 : : {
526 : 1767 : g_autoptr(GDateTime) new_start = NULL;
527 : 3534 : g_autoptr(GDateTime) new_end = NULL;
528 : :
529 [ - + ]: 1767 : g_assert (self->repeat_period != 0);
530 [ - + ]: 1767 : g_assert (n != 0);
531 : :
532 [ + + ]: 1767 : if (n > G_MAXINT / self->repeat_period)
533 : 1 : goto done;
534 : :
535 : 1766 : gint addand = self->repeat_period * n;
536 : :
537 [ + + + + : 1766 : switch (self->repeat_type)
+ - ]
538 : : {
539 : 14 : case MWT_PERIOD_REPEAT_HOUR:
540 : 14 : new_start = g_date_time_add_hours (self->start, addand);
541 : 14 : new_end = g_date_time_add_hours (self->end, addand);
542 : 14 : break;
543 : 1372 : case MWT_PERIOD_REPEAT_DAY:
544 : 1372 : new_start = g_date_time_add_days (self->start, addand);
545 : 1372 : new_end = g_date_time_add_days (self->end, addand);
546 : 1372 : break;
547 : 197 : case MWT_PERIOD_REPEAT_WEEK:
548 : 197 : new_start = g_date_time_add_weeks (self->start, addand);
549 : 197 : new_end = g_date_time_add_weeks (self->end, addand);
550 : 197 : break;
551 : 116 : case MWT_PERIOD_REPEAT_MONTH:
552 : 116 : new_start = g_date_time_add_months (self->start, addand);
553 : 116 : new_end = g_date_time_add_months (self->end, addand);
554 : 116 : break;
555 : 67 : case MWT_PERIOD_REPEAT_YEAR:
556 : 67 : new_start = g_date_time_add_years (self->start, addand);
557 : 67 : new_end = g_date_time_add_years (self->end, addand);
558 : 67 : break;
559 : 0 : case MWT_PERIOD_REPEAT_NONE:
560 : : /* Must be handled by the caller. */
561 : : default:
562 : 0 : g_assert_not_reached ();
563 : : }
564 : :
565 : 1767 : done:
566 : : /* We must not return one date without the other. If one, but not both, of the
567 : : * g_date_time_add_*() calls failed, squash the other result. */
568 [ + + + + ]: 1767 : if (new_start == NULL || new_end == NULL)
569 : : {
570 [ + + ]: 14 : g_clear_pointer (&new_start, g_date_time_unref);
571 [ - + ]: 14 : g_clear_pointer (&new_end, g_date_time_unref);
572 : : }
573 : :
574 : 1767 : gboolean was_empty = FALSE;
575 : :
576 [ + + + - : 3520 : if (new_start != NULL && new_end != NULL &&
+ + ]
577 : 1753 : g_date_time_equal (new_start, new_end))
578 : : {
579 [ + - ]: 16 : g_clear_pointer (&new_start, g_date_time_unref);
580 [ + - ]: 16 : g_clear_pointer (&new_end, g_date_time_unref);
581 : 16 : was_empty = TRUE;
582 : : }
583 : :
584 [ + + + - : 1767 : g_assert (new_start == NULL || new_end == NULL ||
- + ]
585 : : g_date_time_compare (new_start, new_end) < 0);
586 [ + + + - : 1767 : g_assert (!was_empty || (new_start == NULL && new_end == NULL));
+ - ]
587 : :
588 [ + + ]: 1767 : g_clear_pointer (out_start, g_date_time_unref);
589 [ + + ]: 1767 : g_clear_pointer (out_end, g_date_time_unref);
590 : 1767 : *out_start = g_steal_pointer (&new_start);
591 : 1767 : *out_end = g_steal_pointer (&new_end);
592 : 1767 : *out_was_empty = was_empty;
593 : :
594 [ + + + - ]: 1767 : return (*out_start != NULL && *out_end != NULL);
595 : : }
596 : :
597 : : /* Version of get_nth_recurrence() which skips over empty recurrences.
598 : : * @inout_n_skipped_periods must be provided, as the number of recurrences which
599 : : * have already been skipped; it will be updated to include any additional skips
600 : : * from this function call. Otherwise, semantics are the same as
601 : : * get_nth_recurrence(). */
602 : : static gboolean
603 : 1751 : get_nth_recurrence_skip_empty (MwtPeriod *self,
604 : : guint64 n,
605 : : GDateTime **out_start,
606 : : GDateTime **out_end,
607 : : guint64 *inout_n_skipped_periods)
608 : : {
609 : : gboolean retval;
610 : : gboolean was_empty;
611 : 1751 : guint64 n_skipped_periods = *inout_n_skipped_periods;
612 : :
613 : : do
614 : : {
615 : : /* @n_skipped_periods should never be able to overflow, since a guint64
616 : : * can store every second in the range of #GDateTime. */
617 [ - + ]: 1767 : g_assert (n_skipped_periods <= G_MAXUINT64 - n);
618 : :
619 : 1767 : retval = get_nth_recurrence (self, n + n_skipped_periods,
620 : : out_start, out_end, &was_empty);
621 [ + + ]: 1767 : if (was_empty)
622 : 16 : n_skipped_periods++;
623 : : }
624 [ + + ]: 1767 : while (was_empty);
625 : :
626 [ + + ]: 1751 : g_debug ("%s: returning %s, n_skipped_periods: %" G_GUINT64_FORMAT " → %" G_GUINT64_FORMAT,
627 : : G_STRFUNC, retval ? "yes" : "no", *inout_n_skipped_periods, n_skipped_periods);
628 : :
629 : 1751 : *inout_n_skipped_periods = n_skipped_periods;
630 : 1751 : return retval;
631 : : }
632 : :
633 : : /**
634 : : * get_nearest_recurrences:
635 : : * @self: a #MwtPeriod
636 : : * @when: the time to check
637 : : * @out_contains_start: (optional) (nullable) (out) (transfer full): return
638 : : * location for the start time of the recurrence containing @when
639 : : * @out_contains_end: (optional) (nullable) (out) (transfer full): return
640 : : * location for the end time of the recurrence containing @when
641 : : * @out_next_start: (optional) (nullable) (out) (transfer full): return
642 : : * location for the start time of the recurrence after @when
643 : : * @out_next_end: (optional) (nullable) (out) (transfer full): return
644 : : * location for the end time of the recurrence after @when
645 : : *
646 : : * Get the recurrence of the #MwtPeriod which contains @when, and the next
647 : : * recurrence after that. Either or both of these may be %NULL:
648 : : *
649 : : * - The recurrence containing @when may be %NULL if no recurrence of the
650 : : * period actually contains @when.
651 : : * - The next recurrence may be %NULL if the #MwtPeriod:repeat-type is
652 : : * %MWT_PERIOD_REPEAT_NONE.
653 : : * - Or if the next recurrence would exceed the limits of #GDateTime (the year
654 : : * 9999).
655 : : *
656 : : * If a recurrence is returned, both the start and end #GDateTimes are
657 : : * guaranteed to be non-%NULL. The start and end times are guaranteed to be
658 : : * in order, and non-equal. If a recurrence of the period is empty due to
659 : : * falling in DST transition, it is skipped and the next non-empty recurrence is
660 : : * returned.
661 : : */
662 : : static void
663 : 1032 : get_nearest_recurrences (MwtPeriod *self,
664 : : GDateTime *when,
665 : : GDateTime **out_contains_start,
666 : : GDateTime **out_contains_end,
667 : : GDateTime **out_next_start,
668 : : GDateTime **out_next_end)
669 : : {
670 : 1032 : g_autoptr(GDateTime) retval_contains_start = NULL;
671 : 1032 : g_autoptr(GDateTime) retval_contains_end = NULL;
672 : 1032 : g_autoptr(GDateTime) retval_next_start = NULL;
673 : 1032 : g_autoptr(GDateTime) retval_next_end = NULL;
674 : :
675 : : /* We have to allocate these here to avoid memory corruption due to the
676 : : * combination of g_autoptr() and goto. This is unfortunate, since the goto
677 : : * in this function is fairly useful.
678 : : * See: https://blog.fishsoup.net/2015/11/05/attributecleanup-mixed-declarations-and-code-and-goto/ */
679 : 1032 : g_autoptr(GDateTime) start = NULL;
680 : 1032 : g_autoptr(GDateTime) end = NULL;
681 : :
682 : 1032 : guint64 n_skipped_periods = 0;
683 : :
684 : : /* Get the base time if @when is %NULL, or if @when is before the base start
685 : : * time. */
686 [ + + + + ]: 1032 : if (when == NULL || g_date_time_compare (when, self->start) < 0)
687 : : {
688 : 73 : retval_next_start = g_date_time_ref (self->start);
689 : 73 : retval_next_end = g_date_time_ref (self->end);
690 : 73 : goto done;
691 : : }
692 : :
693 : : /* We can assume this from this point onwards. */
694 [ - + ]: 959 : g_assert (when != NULL);
695 : :
696 : : /* Does the base time for the period contain @when? */
697 [ + - + + ]: 1918 : if (g_date_time_compare (self->start, when) <= 0 &&
698 : 959 : g_date_time_compare (when, self->end) < 0)
699 : : {
700 : 124 : retval_contains_start = g_date_time_ref (self->start);
701 : 124 : retval_contains_end = g_date_time_ref (self->end);
702 : :
703 [ + + ]: 124 : if (self->repeat_type != MWT_PERIOD_REPEAT_NONE)
704 : : {
705 : : /* Failure here will result in (@retval_next_start == retval_next_end == NULL),
706 : : * which is what we want. */
707 : 85 : get_nth_recurrence_skip_empty (self, 1,
708 : : &retval_next_start, &retval_next_end, &n_skipped_periods);
709 : : }
710 : :
711 : 124 : goto done;
712 : : }
713 : :
714 : : /* Do recurrences happen at all? */
715 [ + + ]: 835 : if (self->repeat_type == MWT_PERIOD_REPEAT_NONE ||
716 [ - + ]: 816 : self->repeat_period == 0)
717 : 19 : goto done;
718 : :
719 : : /* Firstly, work out a lower bound on the number of periods which could have
720 : : * elapsed between @self->start and @when. We can use this to jump ahead to
721 : : * roughly when the most appropriate recurrence could happen to contain @when,
722 : : * avoiding a load of iterations and #GDateTime allocations. */
723 : : GTimeSpan max_period_span;
724 : :
725 [ + + + + : 816 : switch (self->repeat_type)
+ - ]
726 : : {
727 : 6 : case MWT_PERIOD_REPEAT_HOUR:
728 : 6 : max_period_span = G_TIME_SPAN_HOUR;
729 : 6 : break;
730 : 689 : case MWT_PERIOD_REPEAT_DAY:
731 : 689 : max_period_span = G_TIME_SPAN_DAY;
732 : 689 : break;
733 : 84 : case MWT_PERIOD_REPEAT_WEEK:
734 : 84 : max_period_span = G_TIME_SPAN_DAY * 7;
735 : 84 : break;
736 : 24 : case MWT_PERIOD_REPEAT_MONTH:
737 : : /* The longest month has 31 days. Go for 32 days just in case there’s a
738 : : * DST transition during the month (lengthening it by 1 hour). */
739 : 24 : max_period_span = G_TIME_SPAN_DAY * 32;
740 : 24 : break;
741 : 13 : case MWT_PERIOD_REPEAT_YEAR:
742 : : /* A year is typically 365 days. Go for 367 just in case of added time
743 : : * in DST transitions or it being a leap year. */
744 : 13 : max_period_span = G_TIME_SPAN_DAY * 367;
745 : 13 : break;
746 : 0 : case MWT_PERIOD_REPEAT_NONE:
747 : : /* Handled above. */
748 : : default:
749 : 0 : g_assert_not_reached ();
750 : : }
751 : :
752 : 816 : GTimeSpan diff = g_date_time_difference (when, self->start);
753 [ - + ]: 816 : g_assert (diff >= 0);
754 [ + - + - ]: 816 : g_assert (max_period_span != 0 && self->repeat_period != 0);
755 : 816 : guint64 min_n_periods = ((diff / max_period_span) / self->repeat_period);
756 : :
757 : 816 : g_debug ("%s: diff: %" G_GINT64_FORMAT ", min_n_periods: %" G_GUINT64_FORMAT,
758 : : G_STRFUNC, diff, min_n_periods);
759 : :
760 : 816 : start = g_date_time_ref (self->start);
761 : 816 : end = g_date_time_ref (self->end);
762 : :
763 [ + + + + ]: 1552 : if (min_n_periods > 0 &&
764 : 736 : !get_nth_recurrence_skip_empty (self, min_n_periods,
765 : : &start, &end, &n_skipped_periods))
766 : 2 : goto done;
767 : :
768 : : /* Add periods individually until we either match or overshoot. We need to be
769 : : * careful to always add from the initial date, as addition to dates is not
770 : : * transitive. For example, if a period spans 01:00 to 02:00 one week, and a
771 : : * DST transition happens the next week where the clocks go forward by 1h at
772 : : * 01:00, adding 1 week to the initial @start/@end date/times will produce
773 : : * times of 02:00 to 02:00, not 01:00 to 02:00; adding 1 week more to those
774 : : * date/times will not undo the error. However, adding 2 weeks from the
775 : : * original date/times will avoid the times getting adjusted to avoid the
776 : : * missing DST time at all; the errors will not compound. */
777 [ + + ]: 1235 : for (guint64 i = 1; g_date_time_compare (start, when) <= 0; i++)
778 : : {
779 : : /* @min_n_periods + @i can never really overflow, given that a guint64 can
780 : : * represent every second in the range of a #GDateTime, and the minimum
781 : : * interval we operate in is hours. */
782 [ - + ]: 930 : g_assert (i <= G_MAXUINT64 - min_n_periods);
783 : :
784 : : /* Match now? */
785 [ + - + + ]: 1860 : if (g_date_time_compare (start, when) <= 0 &&
786 : 930 : g_date_time_compare (when, end) < 0)
787 : : {
788 : 502 : retval_contains_start = g_date_time_ref (start);
789 : 502 : retval_contains_end = g_date_time_ref (end);
790 : :
791 : : /* Failure here will result in (@retval_next_start == retval_next_end == NULL),
792 : : * which is what we want. */
793 : 502 : get_nth_recurrence_skip_empty (self, min_n_periods + i,
794 : : &retval_next_start, &retval_next_end, &n_skipped_periods);
795 : :
796 : 502 : goto done;
797 : : }
798 : :
799 [ + + ]: 428 : if (!get_nth_recurrence_skip_empty (self, min_n_periods + i,
800 : : &start, &end, &n_skipped_periods))
801 : 7 : goto done;
802 : : }
803 : :
804 : : /* If we’ve reached this point, we have (@start > @when), so there is no
805 : : * recurrence which contains @when, but we should return the start and end
806 : : * times of the next recurrence. */
807 [ - + ]: 305 : g_assert (g_date_time_compare (start, when) > 0);
808 [ - + ]: 305 : g_assert (retval_contains_start == NULL);
809 [ - + ]: 305 : g_assert (retval_contains_end == NULL);
810 : 305 : retval_next_start = g_date_time_ref (start);
811 : 305 : retval_next_end = g_date_time_ref (end);
812 : 305 : goto done;
813 : :
814 : 1032 : done:
815 [ - + ]: 1032 : g_assert ((retval_contains_start == NULL) == (retval_contains_end == NULL));
816 [ - + ]: 1032 : g_assert ((retval_next_start == NULL) == (retval_next_end == NULL));
817 [ + + + - : 1032 : g_assert (retval_contains_start == NULL || when == NULL ||
- + ]
818 : : g_date_time_compare (retval_contains_start, when) <= 0);
819 [ + + + - : 1032 : g_assert (retval_contains_end == NULL || when == NULL ||
- + ]
820 : : g_date_time_compare (retval_contains_end, when) > 0);
821 [ + + + + : 1032 : g_assert (retval_next_start == NULL || when == NULL ||
- + ]
822 : : g_date_time_compare (retval_next_start, when) > 0);
823 [ + + + + : 1032 : g_assert (retval_next_end == NULL || when == NULL ||
- + ]
824 : : g_date_time_compare (retval_next_end, when) > 0);
825 [ + + + - : 1032 : g_assert (retval_contains_start == NULL || retval_contains_end == NULL ||
- + ]
826 : : g_date_time_compare (retval_contains_start, retval_contains_end) < 0);
827 [ + + + - : 1032 : g_assert (retval_next_start == NULL || retval_next_end == NULL ||
- + ]
828 : : g_date_time_compare (retval_next_start, retval_next_end) < 0);
829 [ + + - + ]: 1032 : g_assert (when != NULL || retval_contains_start == NULL);
830 [ + + - + ]: 1032 : g_assert (when != NULL || retval_contains_end == NULL);
831 [ + + - + ]: 1032 : g_assert (when != NULL || retval_next_start == self->start);
832 [ + + - + ]: 1032 : g_assert (when != NULL || retval_next_end == self->end);
833 : :
834 [ + + ]: 1032 : if (out_contains_start != NULL)
835 : 851 : *out_contains_start = g_steal_pointer (&retval_contains_start);
836 [ + + ]: 1032 : if (out_contains_end != NULL)
837 : 851 : *out_contains_end = g_steal_pointer (&retval_contains_end);
838 [ + + ]: 1032 : if (out_next_start != NULL)
839 : 181 : *out_next_start = g_steal_pointer (&retval_next_start);
840 [ + + ]: 1032 : if (out_next_end != NULL)
841 : 181 : *out_next_end = g_steal_pointer (&retval_next_end);
842 : 1032 : }
843 : :
844 : : /**
845 : : * mwt_period_contains_time:
846 : : * @self: a #MwtPeriod
847 : : * @when: the time to check
848 : : * @out_start: (optional) (out) (transfer full) (nullable): return location for
849 : : * the start time of the recurrence which contains @when
850 : : * @out_end: (optional) (out) (transfer full) (nullable): return location for
851 : : * the end time of the recurrence which contains @when
852 : : *
853 : : * Check whether @when lies within the given #MwtPeriod or any of its
854 : : * recurrences. If it does, @out_start and @out_end will (if provided) will be
855 : : * set to the start and end times of the recurrence which contains @when.
856 : : *
857 : : * If @when does not fall within a recurrence of the #MwtPeriod, @out_start
858 : : * and @out_end will be set to %NULL.
859 : : *
860 : : * Returns: %TRUE if @when lies in the period, %FALSE otherwise
861 : : * Since: 0.1.0
862 : : */
863 : : gboolean
864 : 851 : mwt_period_contains_time (MwtPeriod *self,
865 : : GDateTime *when,
866 : : GDateTime **out_start,
867 : : GDateTime **out_end)
868 : : {
869 [ - + ]: 851 : g_return_val_if_fail (MWT_IS_PERIOD (self), FALSE);
870 [ - + ]: 851 : g_return_val_if_fail (when != NULL, FALSE);
871 : :
872 : 851 : g_autoptr(GDateTime) retval_start = NULL;
873 : 1702 : g_autoptr(GDateTime) retval_end = NULL;
874 : :
875 : : /* Get the recurrence which contains @when, then. If no recurrence contains
876 : : * @when, @retval_start and @retval_end will be set to %NULL. */
877 : 851 : get_nearest_recurrences (self, when, &retval_start, &retval_end, NULL, NULL);
878 : :
879 [ + + + - ]: 851 : gboolean retval = (retval_start != NULL && retval_end != NULL);
880 : :
881 [ + + ]: 851 : if (out_start != NULL)
882 : 96 : *out_start = g_steal_pointer (&retval_start);
883 [ + + ]: 851 : if (out_end != NULL)
884 : 355 : *out_end = g_steal_pointer (&retval_end);
885 : :
886 : 851 : return retval;
887 : : }
888 : :
889 : : /**
890 : : * mwt_period_get_next_recurrence:
891 : : * @self: a #MwtPeriod
892 : : * @after: (nullable): time to get the next recurrence after
893 : : * @out_next_start: (out) (optional) (nullable) (transfer full): return location
894 : : * for the start time of the next recurrence after @after
895 : : * @out_next_end: (out) (optional) (nullable) (transfer full): return location
896 : : * for the end time of the next recurrence after @after
897 : : *
898 : : * Get the start and end time of the first recurrence of #MwtPeriod with a start
899 : : * time greater than @after. If @after is %NULL, this will be the base start and
900 : : * end time of the #MwtPeriod.
901 : : *
902 : : * If #MwtPeriod:repeat-type is %MWT_PERIOD_REPEAT_NONE, and @after is
903 : : * non-%NULL, @out_next_start and @out_next_end will be set to %NULL and %FALSE
904 : : * will be returned.
905 : : *
906 : : * If the first recurrence after @after exceeds the limits of #GDateTime (the
907 : : * end of the year 9999), @out_next_start and @out_next_end will be set to %NULL
908 : : * and %FALSE will be returned.
909 : : *
910 : : * If a recurrence is returned, both @out_next_start and @out_next_end are
911 : : * guaranteed to be non-%NULL (if provided). The returned recurrence is
912 : : * guaranteed to be non-empty: if the next recurrence after @after would be
913 : : * empty due to a DST transition, the first following non-empty recurrence will
914 : : * be returned.
915 : : *
916 : : * Returns: %TRUE if a next recurrence was found, %FALSE otherwise
917 : : * Since: 0.1.0
918 : : */
919 : : gboolean
920 : 181 : mwt_period_get_next_recurrence (MwtPeriod *self,
921 : : GDateTime *after,
922 : : GDateTime **out_next_start,
923 : : GDateTime **out_next_end)
924 : : {
925 [ - + ]: 181 : g_return_val_if_fail (MWT_IS_PERIOD (self), FALSE);
926 : :
927 : 181 : g_autoptr(GDateTime) retval_next_start = NULL;
928 : 362 : g_autoptr(GDateTime) retval_next_end = NULL;
929 : :
930 : : /* Get the recurrence which contains @when, then. If no recurrence contains
931 : : * @when, @retval_start and @retval_end will be set to %NULL. */
932 : 181 : get_nearest_recurrences (self, after, NULL, NULL, &retval_next_start, &retval_next_end);
933 : :
934 [ + + + - ]: 181 : gboolean retval = (retval_next_start != NULL && retval_next_end != NULL);
935 : :
936 [ + - ]: 181 : if (out_next_start != NULL)
937 : 181 : *out_next_start = g_steal_pointer (&retval_next_start);
938 [ + + ]: 181 : if (out_next_end != NULL)
939 : 96 : *out_next_end = g_steal_pointer (&retval_next_end);
940 : :
941 : 181 : return retval;
942 : : }
|