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/period.h>
30 : : #include <libmogwai-tariff/tariff.h>
31 : : #include <string.h>
32 : :
33 : :
34 [ + + ]: 44 : G_DEFINE_QUARK (MwtTariffError, mwt_tariff_error)
35 : :
36 : : static void mwt_tariff_constructed (GObject *object);
37 : : static void mwt_tariff_dispose (GObject *object);
38 : : static void mwt_tariff_get_property (GObject *object,
39 : : guint property_id,
40 : : GValue *value,
41 : : GParamSpec *pspec);
42 : : static void mwt_tariff_set_property (GObject *object,
43 : : guint property_id,
44 : : const GValue *value,
45 : : GParamSpec *pspec);
46 : :
47 : : /**
48 : : * MwtTariff:
49 : : *
50 : : * A representation of a network tariff. Tariffs are represented as a non-empty
51 : : * set of time periods (#MwtPeriod), each of which has a constant set of
52 : : * properties, such as bandwidth or capacity limits which apply over that
53 : : * period.
54 : : *
55 : : * The periods in a tariff must be non-overlapping, in the sense that if a
56 : : * period intersects another period at all, it must be entirely contained
57 : : * within that period. Put another way, all pairs of periods are either
58 : : * disjoint, or one strictly contains the other. The properties which apply to a
59 : : * given date/time are selected from the shortest period which contains that
60 : : * date/time — see mwt_tariff_lookup_period().
61 : : *
62 : : * The valid relative positions of any two periods are as follows, where the
63 : : * first line gives the first period (A), and all subsequent lines are the valid
64 : : * positions for a second period (B):
65 : : *
66 : : * |[
67 : : * A: ▀▀▀
68 : : * B: ▀▀▀
69 : : * ▀▀▀
70 : : * ▀▀▀
71 : : * ▀▀▀
72 : : * ▀
73 : : * ▀
74 : : * ▀
75 : : * ▀▀▀▀▀
76 : : * ▀▀▀▀
77 : : * ▀▀▀▀
78 : : * ]|
79 : : *
80 : : * Similarly, here are the invalid relative positions:
81 : : *
82 : : * |[
83 : : * A: ▀▀▀
84 : : * B: ▀▀▀
85 : : * ▀▀▀
86 : : * ▀▀▀
87 : : * ]|
88 : : *
89 : : * The periods in a tariff must also be ordered by decreasing span, and then by
90 : : * increasing start date/time. Two periods are not allowed to be equal in span
91 : : * and start date/time. There must be at least one period in a tariff.
92 : : *
93 : : * The #MwtTariff class is immutable once loaded or constructed.
94 : : *
95 : : * Since: 0.1.0
96 : : */
97 : : struct _MwtTariff
98 : : {
99 : : GObject parent;
100 : :
101 : : gchar *name; /* (owned) */
102 : : GPtrArray *periods; /* (element-type MwtPeriod) (owned) */
103 : : };
104 : :
105 : : typedef enum
106 : : {
107 : : PROP_NAME = 1,
108 : : PROP_PERIODS,
109 : : } MwtTariffProperty;
110 : :
111 [ + + + - : 538 : G_DEFINE_TYPE (MwtTariff, mwt_tariff, G_TYPE_OBJECT)
+ + ]
112 : :
113 : : static void
114 : 12 : mwt_tariff_class_init (MwtTariffClass *klass)
115 : : {
116 : 12 : GObjectClass *object_class = (GObjectClass *) klass;
117 : 12 : GParamSpec *props[PROP_PERIODS + 1] = { NULL, };
118 : :
119 : 12 : object_class->constructed = mwt_tariff_constructed;
120 : 12 : object_class->dispose = mwt_tariff_dispose;
121 : 12 : object_class->get_property = mwt_tariff_get_property;
122 : 12 : object_class->set_property = mwt_tariff_set_property;
123 : :
124 : : /**
125 : : * MwtTariff:name:
126 : : *
127 : : * Unique name of the tariff. This is for identifying the tariff, and is not
128 : : * necessarily human readable. It must be non-empty, a valid filename (so must
129 : : * not contain `/` or `\\`), and valid UTF-8. It must also only contain
130 : : * characters which are valid for internationalised domain names, as per
131 : : * RFC 3491.
132 : : *
133 : : * Since: 0.1.0
134 : : */
135 : 12 : props[PROP_NAME] =
136 : 12 : g_param_spec_string ("name", "Name",
137 : : "Unique name of the tariff.",
138 : : NULL,
139 : : G_PARAM_READWRITE |
140 : : G_PARAM_CONSTRUCT_ONLY |
141 : : G_PARAM_STATIC_STRINGS);
142 : :
143 : : /**
144 : : * MwtTariff:periods: (element-type MwtPeriod)
145 : : *
146 : : * The set of #MwtPeriods in the tariff. See the documentation for #MwtTariff
147 : : * for the requirements about periods being non-overlapping, etc.
148 : : *
149 : : * The periods are guaranteed to be ordered by decreasing time span, and then
150 : : * by increasing start date/time.
151 : : *
152 : : * Since: 0.1.0
153 : : */
154 : 12 : props[PROP_PERIODS] =
155 : 12 : g_param_spec_boxed ("periods", "Periods",
156 : : "The set of #MwtPeriods in the tariff.",
157 : : G_TYPE_PTR_ARRAY,
158 : : G_PARAM_READWRITE |
159 : : G_PARAM_CONSTRUCT_ONLY |
160 : : G_PARAM_STATIC_STRINGS);
161 : :
162 : 12 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
163 : 12 : }
164 : :
165 : : static void
166 : 27 : mwt_tariff_init (MwtTariff *self)
167 : : {
168 : : /* Nothing to do here. */
169 : 27 : }
170 : :
171 : : static void
172 : 27 : mwt_tariff_constructed (GObject *object)
173 : : {
174 : 27 : MwtTariff *self = MWT_TARIFF (object);
175 : :
176 : 27 : G_OBJECT_CLASS (mwt_tariff_parent_class)->constructed (object);
177 : :
178 : : /* Validate the properties. */
179 [ - + ]: 27 : g_assert (mwt_tariff_validate (self->name, self->periods, NULL));
180 : 27 : }
181 : :
182 : : static void
183 : 27 : mwt_tariff_dispose (GObject *object)
184 : : {
185 : 27 : MwtTariff *self = MWT_TARIFF (object);
186 : :
187 [ + - ]: 27 : g_clear_pointer (&self->name, g_free);
188 [ + - ]: 27 : g_clear_pointer (&self->periods, g_ptr_array_unref);
189 : :
190 : : /* Chain up to the parent class */
191 : 27 : G_OBJECT_CLASS (mwt_tariff_parent_class)->dispose (object);
192 : 27 : }
193 : :
194 : : static void
195 : 2 : mwt_tariff_get_property (GObject *object,
196 : : guint property_id,
197 : : GValue *value,
198 : : GParamSpec *pspec)
199 : : {
200 : 2 : MwtTariff *self = MWT_TARIFF (object);
201 : :
202 [ + + - ]: 2 : switch ((MwtTariffProperty) property_id)
203 : : {
204 : 1 : case PROP_NAME:
205 : 1 : g_value_set_string (value, self->name);
206 : 1 : break;
207 : 1 : case PROP_PERIODS:
208 : 1 : g_value_set_boxed (value, self->periods);
209 : 1 : break;
210 : 0 : default:
211 : 0 : g_assert_not_reached ();
212 : : }
213 : 2 : }
214 : :
215 : : static void
216 : 54 : mwt_tariff_set_property (GObject *object,
217 : : guint property_id,
218 : : const GValue *value,
219 : : GParamSpec *pspec)
220 : : {
221 : 54 : MwtTariff *self = MWT_TARIFF (object);
222 : :
223 [ + + - ]: 54 : switch ((MwtTariffProperty) property_id)
224 : : {
225 : 27 : case PROP_NAME:
226 : : /* Construct only. */
227 [ - + ]: 27 : g_assert (self->name == NULL);
228 : 27 : self->name = g_value_dup_string (value);
229 : 27 : break;
230 : 27 : case PROP_PERIODS:
231 : : /* Construct only. */
232 [ - + ]: 27 : g_assert (self->periods == NULL);
233 : 27 : self->periods = g_value_dup_boxed (value);
234 : 27 : break;
235 : 0 : default:
236 : 0 : g_assert_not_reached ();
237 : : }
238 : 54 : }
239 : :
240 : : /* ∀ p_1, p_2 ∈ self->periods.
241 : : * ¬ (p_1.start < p_2.start ∧
242 : : * p_1.end > p_2.start ∧
243 : : * p_1.end < p_2.end) ∧
244 : : * ¬ (p_1.start = p_2.start ∧
245 : : * p_1.end = p_2.end)
246 : : */
247 : : static gboolean
248 : 40 : mwt_tariff_are_periods_nonoverlapping (GPtrArray *periods)
249 : : {
250 : : /* FIXME: This is O(N^2) at the moment. We assume there are not many periods. */
251 : : /* FIXME: This needs to expand recurrences, since we could have two periods
252 : : * whose spans are identical, but whose start times differ by the repeat period
253 : : * of one of them. */
254 [ + + ]: 100 : for (gsize i = 0; i < periods->len; i++)
255 : : {
256 : 60 : MwtPeriod *p_1 = g_ptr_array_index (periods, i);
257 : 60 : GDateTime *p_1_start = mwt_period_get_start (p_1);
258 : 60 : GDateTime *p_1_end = mwt_period_get_end (p_1);
259 : :
260 [ + + ]: 160 : for (gsize j = 0; j < periods->len; j++)
261 : : {
262 : 100 : MwtPeriod *p_2 = g_ptr_array_index (periods, j);
263 : 100 : GDateTime *p_2_start = mwt_period_get_start (p_2);
264 : 100 : GDateTime *p_2_end = mwt_period_get_end (p_2);
265 : :
266 [ + + ]: 100 : if (i == j)
267 : 60 : continue;
268 : :
269 : : /* p_1: ▀▀▀
270 : : * p_2: ▀▀▀
271 : : * or (when i and j have swapped)
272 : : * p_1: ▀▀▀
273 : : * p_2: ▀▀▀
274 : : */
275 [ + + + + ]: 58 : if (g_date_time_compare (p_1_start, p_2_start) < 0 &&
276 [ - + ]: 35 : g_date_time_compare (p_1_end, p_2_start) > 0 &&
277 : 17 : g_date_time_compare (p_1_end, p_2_end) < 0)
278 : 0 : return FALSE;
279 : :
280 : : /* p_1: ▀▀▀
281 : : * p_2: ▀▀▀
282 : : */
283 [ + + - + ]: 44 : if (g_date_time_compare (p_1_start, p_2_start) == 0 &&
284 : 4 : g_date_time_compare (p_1_end, p_2_end) == 0)
285 : 0 : return FALSE;
286 : : }
287 : : }
288 : :
289 : 40 : return TRUE;
290 : : }
291 : :
292 : : /* Periods must be ordered by decreasing time span, and then by increasing start
293 : : * date/time. */
294 : : static gboolean
295 : 40 : mwt_tariff_are_periods_ordered (GPtrArray *periods)
296 : : {
297 [ + + ]: 60 : for (gsize i = 1; i < periods->len; i++)
298 : : {
299 : 20 : MwtPeriod *p1 = g_ptr_array_index (periods, i - 1);
300 : 20 : MwtPeriod *p2 = g_ptr_array_index (periods, i);
301 : 20 : GDateTime *p1_start = mwt_period_get_start (p1);
302 : 20 : GDateTime *p1_end = mwt_period_get_end (p1);
303 : 20 : GDateTime *p2_start = mwt_period_get_start (p2);
304 : 20 : GDateTime *p2_end = mwt_period_get_end (p2);
305 : :
306 : 20 : GTimeSpan p1_span = g_date_time_difference (p1_end, p1_start);
307 : 20 : GTimeSpan p2_span = g_date_time_difference (p2_end, p2_start);
308 : :
309 [ + - + + ]: 20 : if (p1_span < p2_span ||
310 [ - + ]: 1 : (p1_span == p2_span && g_date_time_compare (p1_start, p2_start) >= 0))
311 : 0 : return FALSE;
312 : : }
313 : :
314 : 40 : return TRUE;
315 : : }
316 : :
317 : : /**
318 : : * mwt_tariff_validate:
319 : : * @naeme: (nullable): tariff name (see #MwtTariff:name)
320 : : * @periods: (nullable): periods in the tariff (see #MwtTariff:periods)
321 : : * @error: return location for a #GError, or %NULL
322 : : *
323 : : * Validate the given #MwtTariff properties, returning %MWT_TARIFF_ERROR_INVALID
324 : : * if any of them are invalid. All inputs are allowed to the property arguments
325 : : * (except @error): no inputs are a programmer error.
326 : : *
327 : : * It is guaranteed that if this function returns %TRUE for a given set of
328 : : * inputs, mwt_tariff_new() will succeed for those inputs.
329 : : *
330 : : * Returns: %TRUE if valid, %FALSE otherwise
331 : : * Since: 0.1.0
332 : : */
333 : : gboolean
334 : 54 : mwt_tariff_validate (const gchar *name,
335 : : GPtrArray *periods,
336 : : GError **error)
337 : : {
338 [ + + - + ]: 54 : g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
339 : :
340 [ + + ]: 54 : if (!mwt_tariff_validate_name (name))
341 : : {
342 : 9 : g_set_error_literal (error, MWT_TARIFF_ERROR, MWT_TARIFF_ERROR_INVALID,
343 : : _("Invalid tariff name."));
344 : 9 : return FALSE;
345 : : }
346 : :
347 [ + - ]: 45 : if (periods == NULL ||
348 [ + + + - ]: 85 : periods->len == 0 ||
349 [ - + ]: 80 : !mwt_tariff_are_periods_nonoverlapping (periods) ||
350 : 40 : !mwt_tariff_are_periods_ordered (periods))
351 : : {
352 : 5 : g_set_error_literal (error, MWT_TARIFF_ERROR, MWT_TARIFF_ERROR_INVALID,
353 : : _("Invalid tariff periods."));
354 : 5 : return FALSE;
355 : : }
356 : :
357 : 40 : return TRUE;
358 : : }
359 : :
360 : : /**
361 : : * mwt_tariff_new:
362 : : * @start: tariff name (see #MwtTariff:name)
363 : : * @periods: periods in the tariff (see #MwtTariff:periods)
364 : : *
365 : : * Create a #MwtTariff object with the given properties.
366 : : *
367 : : * All inputs to this function must have been validated with
368 : : * mwt_tariff_validate() first. It is a programmer error to provide invalid
369 : : * inputs.
370 : : *
371 : : * Returns: (transfer full): a new #MwtTariff
372 : : * Since: 0.1.0
373 : : */
374 : : MwtTariff *
375 : 27 : mwt_tariff_new (const gchar *name,
376 : : GPtrArray *periods)
377 : : {
378 : : /* Leave property validation to constructed(). */
379 : 27 : return g_object_new (MWT_TYPE_TARIFF,
380 : : "name", name,
381 : : "periods", periods,
382 : : NULL);
383 : : }
384 : :
385 : : /**
386 : : * mwt_tariff_get_start:
387 : : * @self: a #MwtTariff
388 : : *
389 : : * Get the value of #MwtTariff:name.
390 : : *
391 : : * Returns: tariff name
392 : : * Since: 0.1.0
393 : : */
394 : : const gchar *
395 : 9 : mwt_tariff_get_name (MwtTariff *self)
396 : : {
397 [ - + ]: 9 : g_return_val_if_fail (MWT_IS_TARIFF (self), NULL);
398 : :
399 : 9 : return self->name;
400 : : }
401 : :
402 : : /**
403 : : * mwt_tariff_get_periods:
404 : : * @self: a #MwtTariff
405 : : *
406 : : * Get the value of #MwtTariff:periods.
407 : : *
408 : : * Returns: (element-type MwtPeriod): periods in the tariff
409 : : * Since: 0.1.0
410 : : */
411 : : GPtrArray *
412 : 14 : mwt_tariff_get_periods (MwtTariff *self)
413 : : {
414 [ - + ]: 14 : g_return_val_if_fail (MWT_IS_TARIFF (self), NULL);
415 : :
416 : 14 : return self->periods;
417 : : }
418 : :
419 : : /**
420 : : * mwt_tariff_lookup_period:
421 : : * @self: a #MwtTariff
422 : : * @when: the date/time to look up
423 : : *
424 : : * Look up the #MwtPeriod which applies to the given date/time. If @when lies
425 : : * outside the overall start and end times of the tariff, %NULL will be
426 : : * returned. It is up to the caller to treat this appropriately (for example, by
427 : : * disallowing all downloads).
428 : : *
429 : : * This will expand the recurrences of each period in order to find matches.
430 : : *
431 : : * Returns: (transfer none) (nullable): period covering @when, or %NULL if no
432 : : * periods are relevant
433 : : * Since: 0.1.0
434 : : */
435 : : MwtPeriod *
436 : 200 : mwt_tariff_lookup_period (MwtTariff *self,
437 : : GDateTime *when)
438 : : {
439 [ - + ]: 200 : g_return_val_if_fail (MWT_IS_TARIFF (self), NULL);
440 [ - + ]: 200 : g_return_val_if_fail (when != NULL, NULL);
441 : :
442 : : /* Find the set of periods which overlap @when, including taking recurrences
443 : : * into account. */
444 : : /* FIXME: We don’t expect there to be many periods in a tariff. If there are,
445 : : * this algorithm could be improved to use an interval tree or similar to
446 : : * improve performance. */
447 : 200 : g_autoptr(GPtrArray) matches = g_ptr_array_new_with_free_func (NULL);
448 : :
449 [ + + ]: 563 : for (gsize i = 0; i < self->periods->len; i++)
450 : : {
451 : 363 : MwtPeriod *period = g_ptr_array_index (self->periods, i);
452 : :
453 [ + + ]: 363 : if (mwt_period_contains_time (period, when, NULL, NULL))
454 : 209 : g_ptr_array_add (matches, period);
455 : : }
456 : :
457 : : /* Pick the shortest period. There should be no ties here, since overlapping
458 : : * periods are disallowed. */
459 : 200 : MwtPeriod *shortest_period = NULL;
460 : 200 : GTimeSpan shortest_period_duration = G_MAXINT64;
461 [ + + ]: 409 : for (gsize i = 0; i < matches->len; i++)
462 : : {
463 : 209 : MwtPeriod *period = g_ptr_array_index (matches, i);
464 : 209 : GDateTime *start = mwt_period_get_start (period);
465 : 209 : GDateTime *end = mwt_period_get_end (period);
466 : 209 : GTimeSpan duration = g_date_time_difference (end, start);
467 : :
468 [ + + - + ]: 209 : g_assert (shortest_period == NULL || duration != shortest_period_duration);
469 : :
470 [ + + + - ]: 209 : if (shortest_period == NULL || duration < shortest_period_duration)
471 : : {
472 : 209 : shortest_period = period;
473 : 209 : shortest_period_duration = duration;
474 : : }
475 : : }
476 : :
477 : 200 : return shortest_period;
478 : : }
479 : :
480 : : static void
481 : 13 : update_earliest (GDateTime **earliest,
482 : : MwtPeriod **earliest_from_period,
483 : : MwtPeriod **earliest_to_period,
484 : : GDateTime *maybe_earlier,
485 : : MwtPeriod *maybe_earlier_from_period,
486 : : MwtPeriod *maybe_earlier_to_period)
487 : : {
488 : : /* Update @earliest if @maybe_earlier is the same time, as we sort
489 : : * self->periods by decreasing timespan, and the shorter periods always take
490 : : * priority over the longer ones. */
491 [ + + + + ]: 13 : if (*earliest == NULL || g_date_time_compare (maybe_earlier, *earliest) <= 0)
492 : : {
493 [ + + ]: 9 : g_clear_pointer (earliest, g_date_time_unref);
494 : 9 : *earliest = g_date_time_ref (maybe_earlier);
495 : 9 : *earliest_from_period = maybe_earlier_from_period;
496 : 9 : *earliest_to_period = maybe_earlier_to_period;
497 : : }
498 : 13 : }
499 : :
500 : : typedef struct
501 : : {
502 : : GDateTime *when; /* (owned) */
503 : : enum
504 : : {
505 : : TRANSITION_FROM,
506 : : TRANSITION_TO,
507 : : } type;
508 : : gsize period_index;
509 : : MwtPeriod *period; /* (unowned) */
510 : : } TransitionData;
511 : :
512 : : static void
513 : 259 : transition_data_clear (TransitionData *data)
514 : : {
515 [ + + ]: 259 : g_clear_pointer (&data->when, g_date_time_unref);
516 : 259 : }
517 : :
518 : : static gint
519 : 115 : transition_data_sort (const TransitionData *a,
520 : : const TransitionData *b)
521 : : {
522 : 115 : gint when_comparison = g_date_time_compare (a->when, b->when);
523 : :
524 [ + + ]: 115 : if (when_comparison != 0)
525 : 108 : return when_comparison;
526 : :
527 : : /* Order %TRANSITION_FROM before %TRANSITION_TO. */
528 [ + + + + ]: 7 : if (a->type == TRANSITION_FROM && b->type == TRANSITION_TO)
529 : 2 : return -1;
530 [ + + + + ]: 5 : else if (a->type == TRANSITION_TO && b->type == TRANSITION_FROM)
531 : 2 : return 1;
532 : :
533 : : /* If the transition types are equal, order by length/start time. That’s
534 : : * equivalent to ordering by the reverse index of @period in self->periods,
535 : : * due to the sort order we guarantee on the latter. */
536 [ - + ]: 3 : if (a->period_index > b->period_index)
537 : 0 : return -1;
538 [ + - ]: 3 : else if (a->period_index < b->period_index)
539 : 3 : return 1;
540 : :
541 : 0 : g_assert_not_reached ();
542 : : }
543 : :
544 : : /* Get the next transition from @transitions after @data_index whose date/time
545 : : * is greater than that of @data. If no such transition exists, return %NULL. */
546 : : static const TransitionData *
547 : 105 : get_next_transition (GArray *transitions,
548 : : gsize data_index,
549 : : const TransitionData *data)
550 : : {
551 [ - + ]: 105 : if (data_index == G_MAXSIZE)
552 : 0 : return NULL;
553 : :
554 [ + + ]: 110 : for (gsize next_data_index = data_index + 1; next_data_index < transitions->len; next_data_index++)
555 : : {
556 : 88 : const TransitionData *next_data =
557 : 88 : &g_array_index (transitions, TransitionData, next_data_index);
558 : :
559 [ + + ]: 88 : if (!g_date_time_equal (next_data->when, data->when))
560 : 83 : return next_data;
561 : : }
562 : :
563 : 22 : return NULL;
564 : : }
565 : :
566 : : /**
567 : : * mwt_tariff_get_next_transition:
568 : : * @self: a #MwtTariff
569 : : * @after: (nullable): time to get the next transition after
570 : : * @out_from_period: (out) (optional) (nullable) (transfer none): return
571 : : * location for the period being transitioned out of
572 : : * @out_to_period: (out) (optional) (nullable) (transfer none): return location
573 : : * for the period being transitioned in to
574 : : *
575 : : * Get the date and time of the first transition between periods after @after in
576 : : * this #MwtTariff, and return the periods being transitioned out of and in to
577 : : * in @out_from_period and @out_to_period.
578 : : *
579 : : * If @after is %NULL, the first transition in the tariff is returned:
580 : : * @out_from_period is guaranteed to be %NULL, @out_to_period is guaranteed to
581 : : * be non-%NULL, and a non-%NULL value is guaranteed to be returned.
582 : : *
583 : : * Either or both of @out_from_period and @out_to_period may be %NULL, if the
584 : : * next transition is into the first tariff of the period, out of the last
585 : : * tariff of the period; or if there are no more transitions after @after. It is
586 : : * possible for @out_from_period and @out_to_period to be set to the same
587 : : * #MwtPeriod instance, if one recurrence of the period ends when the next
588 : : * begins.
589 : : *
590 : : * If a non-%NULL date and time is returned, at least one of @out_from_period
591 : : * and @out_to_period (if provided) are guaranteed to be non-%NULL.
592 : : *
593 : : * Returns: (nullable) (transfer full): date and time of the next transition,
594 : : * or %NULL if there are no more transitions after @after
595 : : * Since: 0.1.0
596 : : */
597 : : GDateTime *
598 : 151 : mwt_tariff_get_next_transition (MwtTariff *self,
599 : : GDateTime *after,
600 : : MwtPeriod **out_from_period,
601 : : MwtPeriod **out_to_period)
602 : : {
603 [ - + ]: 151 : g_return_val_if_fail (MWT_IS_TARIFF (self), NULL);
604 : :
605 : : /* If (@after == NULL), we need to get the first transition. */
606 [ + + ]: 151 : if (after == NULL)
607 : : {
608 : 16 : g_autoptr(GDateTime) first_transition = NULL;
609 : 8 : MwtPeriod *first_from_period = NULL;
610 : 8 : MwtPeriod *first_to_period = NULL;
611 : :
612 : : /* Periods are stored ordered by time span first, so we can’t just use the
613 : : * first one. */
614 [ + + ]: 21 : for (gsize i = 0; i < self->periods->len; i++)
615 : : {
616 : 13 : MwtPeriod *period = g_ptr_array_index (self->periods, i);
617 : :
618 : 13 : update_earliest (&first_transition, &first_from_period, &first_to_period,
619 : : mwt_period_get_start (period), NULL, period);
620 : : }
621 : :
622 [ - + ]: 8 : g_assert (first_transition != NULL);
623 [ - + ]: 8 : g_assert (first_from_period == NULL);
624 [ - + ]: 8 : g_assert (first_to_period != NULL);
625 [ - + ]: 8 : g_assert (mwt_period_contains_time (first_to_period, first_transition, NULL, NULL));
626 : :
627 [ + - ]: 8 : if (out_from_period)
628 : 8 : *out_from_period = first_from_period;
629 [ + - ]: 8 : if (out_to_period)
630 : 8 : *out_to_period = first_to_period;
631 : :
632 : 8 : return g_steal_pointer (&first_transition);
633 : : }
634 : :
635 : : /* Otherwise, get the next transition for each of the periods in the tariff.
636 : : * For each period, if the period contains @after, then we know its next
637 : : * transition will be FROM that period to another. Otherwise, if the period
638 : : * has another recurrence after @after, we know its next transition will be
639 : : * TO that period from another. Build this up into an array of
640 : : * #TransitionData. */
641 : 143 : g_autoptr(GDateTime) next_transition = NULL;
642 : 143 : MwtPeriod *next_from_period = NULL;
643 : 143 : MwtPeriod *next_to_period = NULL;
644 : :
645 : 143 : g_autoptr(GArray) transitions = g_array_sized_new (FALSE, FALSE,
646 : : sizeof (TransitionData),
647 : 143 : self->periods->len);
648 : 143 : g_array_set_clear_func (transitions, (GDestroyNotify) transition_data_clear);
649 : :
650 [ + + ]: 402 : for (gsize i = 0; i < self->periods->len; i++)
651 : : {
652 : 259 : MwtPeriod *period = g_ptr_array_index (self->periods, i);
653 : :
654 : 259 : g_array_set_size (transitions, transitions->len + 1);
655 : 259 : TransitionData *data = &g_array_index (transitions, TransitionData,
656 : : transitions->len - 1);
657 : :
658 [ + + ]: 259 : if (mwt_period_contains_time (period, after, NULL, &data->when))
659 : : {
660 [ - + ]: 174 : g_assert (g_date_time_compare (data->when, after) > 0);
661 : 174 : data->type = TRANSITION_FROM;
662 : 174 : data->period_index = i;
663 : 174 : data->period = period;
664 : : }
665 [ + + ]: 85 : else if (mwt_period_get_next_recurrence (period, after, &data->when, NULL))
666 : : {
667 [ - + ]: 80 : g_assert (g_date_time_compare (data->when, after) > 0);
668 : 80 : data->type = TRANSITION_TO;
669 : 80 : data->period_index = i;
670 : 80 : data->period = period;
671 : : }
672 : : else
673 : : {
674 : : /* This period has no more transitions; erase the prospective entry
675 : : * from @transitions. */
676 : 5 : g_array_set_size (transitions, transitions->len - 1);
677 : : }
678 : : }
679 : :
680 : : /* Is @transitions empty? */
681 [ + + ]: 143 : if (transitions->len == 0)
682 : 4 : goto done;
683 : :
684 : : /* Sort the transitions. */
685 : 139 : g_array_sort (transitions, (GCompareFunc) transition_data_sort);
686 : :
687 : : /* All the transitions in @transitions are guaranteed to be equal to or later
688 : : * than @after. So far, the @next_transition_data only contains *one* of the
689 : : * periods which the transition enters/leaves. Now we need to work out what
690 : : * the other period is. It may be %NULL. */
691 : 139 : const TransitionData *next_transition_data =
692 : 139 : &g_array_index (transitions, TransitionData, 0);
693 [ - + ]: 139 : g_assert (g_date_time_compare (after, next_transition_data->when) < 0);
694 : :
695 [ + + - ]: 139 : switch (next_transition_data->type)
696 : : {
697 : 105 : case TRANSITION_FROM:
698 : : {
699 : : /* In this case, @next_transition_data concerns a transition FROM one
700 : : * period to another. The #TransitionData.when field is the end
701 : : * date/time of a recurrence of the period given in
702 : : * #TransitionData.period. Find the following transition. If it’s also
703 : : * a FROM transition, we have one period nested within another, and
704 : : * the transition we care about (@next_transition_data) is FROM the
705 : : * inner to the outer. If it’s a TO transition instead, there could be
706 : : * several different valid arrangements of periods, and the easiest
707 : : * way to find the period the transition is going to is by calling
708 : : * mwt_tariff_lookup_period() for the end of the current one. */
709 : : const TransitionData *following_transition_data =
710 : 105 : get_next_transition (transitions, 0, next_transition_data);
711 : 105 : next_transition = g_date_time_ref (next_transition_data->when);
712 : 105 : next_from_period = next_transition_data->period;
713 [ + + ]: 105 : if (following_transition_data != NULL &&
714 [ + + ]: 83 : following_transition_data->type == TRANSITION_FROM)
715 : 48 : next_to_period = following_transition_data->period;
716 : : else
717 : 57 : next_to_period = mwt_tariff_lookup_period (self, next_transition);
718 : : }
719 : 105 : break;
720 : 34 : case TRANSITION_TO:
721 : : {
722 : 34 : next_transition = g_date_time_ref (next_transition_data->when);
723 : 34 : next_to_period = next_transition_data->period;
724 : :
725 : : /* In this case, we could have two transitions, FROM and TO, with
726 : : * @after sitting between them. There could be a period which spans
727 : : * from before FROM to after TO. To detect this, we’d have to walk
728 : : * backwards through the list of transitions (potentially needing
729 : : * to generate new ones), bracketing them, until we found an
730 : : * unmatched TO transition. It seems easier to do this hack. I am
731 : : * not happy with this hack, but it does mean saving a lot of code
732 : : * and testing. The only algorithmically nice fix for this is to
733 : : * rewrite everything to use an interval tree.
734 : : * FIXME: Should we put lower bounds on the permitted lengths of periods? */
735 : 68 : g_autoptr(GDateTime) before_next_transition = g_date_time_add_seconds (next_transition, -1.0);
736 : 34 : next_from_period = mwt_tariff_lookup_period (self, before_next_transition);
737 : : }
738 : 34 : break;
739 : 0 : default:
740 : 0 : g_assert_not_reached ();
741 : : }
742 : :
743 : 143 : done:
744 [ + + - + ]: 143 : g_assert (next_transition != NULL || next_from_period == NULL);
745 [ + + - + ]: 143 : g_assert (next_transition != NULL || next_to_period == NULL);
746 [ + + + + : 143 : g_assert (next_transition == NULL ||
- + ]
747 : : (next_from_period != NULL || next_to_period != NULL));
748 [ + + - + ]: 143 : g_assert (next_transition == NULL ||
749 : : g_date_time_compare (next_transition, after) > 0);
750 [ + + + + : 143 : g_assert (next_transition == NULL || next_to_period == NULL ||
- + ]
751 : : mwt_period_contains_time (next_to_period, next_transition, NULL, NULL));
752 : :
753 [ + + ]: 143 : if (out_from_period != NULL)
754 : 45 : *out_from_period = next_from_period;
755 [ + + ]: 143 : if (out_to_period != NULL)
756 : 45 : *out_to_period = next_to_period;
757 : :
758 : 143 : return g_steal_pointer (&next_transition);
759 : : }
760 : :
761 : : /**
762 : : * mwt_tariff_validate_name:
763 : : * @name: (nullable): string to validate
764 : : *
765 : : * Validate the given @name string to see if it is a valid name for a tariff.
766 : : * Any input is accepted (not a programming error), including %NULL.
767 : : *
768 : : * See #MwtTariff:name for the specification of valid names.
769 : : *
770 : : * Returns: %TRUE if @name is valid, %FALSE otherwise
771 : : * Since: 0.1.0
772 : : */
773 : : gboolean
774 : 61 : mwt_tariff_validate_name (const gchar *name)
775 : : {
776 [ + + - + ]: 61 : if (name == NULL || *name == '\0')
777 : 9 : return FALSE;
778 [ - + ]: 52 : if (!g_utf8_validate (name, -1, NULL))
779 : 0 : return FALSE;
780 [ + - ]: 52 : if (strchr (name, '/') != NULL ||
781 [ - + ]: 52 : strchr (name, '\\') != NULL)
782 : 0 : return FALSE;
783 : :
784 : : /* Abuse g_hostname_to_ascii() for its IDN validation. */
785 : 104 : g_autofree gchar *ascii = g_hostname_to_ascii (name);
786 [ - + ]: 52 : if (ascii == NULL)
787 : 0 : return FALSE;
788 : :
789 : 52 : return TRUE;
790 : : }
|