Introduce GcalRange

GcalRange is a new structure that represents a time range. It is
always inclusive at the start, and exclusive at the end.
This commit is contained in:
Georges Basile Stavracas Neto
2020-04-14 19:08:05 +00:00
parent 6091a14a44
commit dbfebc85fa
5 changed files with 766 additions and 0 deletions
+492
View File
@@ -0,0 +1,492 @@
/* gcal-range.c
*
* Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "gcal-range.h"
#include "gcal-date-time-utils.h"
struct _GcalRange
{
gatomicrefcount ref_count;
GDateTime *range_start;
GDateTime *range_end;
GcalRangeType range_type;
};
G_DEFINE_BOXED_TYPE (GcalRange, gcal_range, gcal_range_ref, gcal_range_unref)
typedef int (*CompareDateTimeFunc) (GDateTime *a,
GDateTime *b);
static CompareDateTimeFunc
get_compare_func (GcalRange *a,
GcalRange *b)
{
if (a->range_type == GCAL_RANGE_DATE_ONLY || b->range_type == GCAL_RANGE_DATE_ONLY)
return gcal_date_time_compare_date;
else
return (CompareDateTimeFunc) g_date_time_compare;
}
/**
* gcal_range_copy:
* @self: a #GcalRange
*
* Makes a deep copy of a #GcalRange.
*
* Returns: (transfer full): A newly created #GcalRange with the same
* contents as @self
*/
GcalRange *
gcal_range_copy (GcalRange *self)
{
GcalRange *copy;
g_return_val_if_fail (self, NULL);
g_return_val_if_fail (!g_atomic_ref_count_compare (&self->ref_count, 0), NULL);
copy = gcal_range_new (self->range_start, self->range_end, self->range_type);
return copy;
}
static void
gcal_range_free (GcalRange *self)
{
g_assert (self);
g_assert (g_atomic_ref_count_compare (&self->ref_count, 0));
gcal_clear_date_time (&self->range_start);
gcal_clear_date_time (&self->range_end);
g_free (self);
}
/**
* gcal_range_ref:
* @self: A #GcalRange
*
* Increments the reference count of @self by one.
*
* Returns: (transfer full): @self
*/
GcalRange *
gcal_range_ref (GcalRange *self)
{
g_return_val_if_fail (self, NULL);
g_return_val_if_fail (!g_atomic_ref_count_compare (&self->ref_count, 0), NULL);
g_atomic_ref_count_inc (&self->ref_count);
return self;
}
/**
* gcal_range_unref:
* @self: A #GcalRange
*
* Decrements the reference count of @self by one, freeing the structure when
* the reference count reaches zero.
*/
void
gcal_range_unref (GcalRange *self)
{
g_return_if_fail (self);
g_return_if_fail (!g_atomic_ref_count_compare (&self->ref_count, 0));
if (g_atomic_ref_count_dec (&self->ref_count))
gcal_range_free (self);
}
/**
* gcal_range_new:
*
* Creates a new #GcalRange.
*
* Returns: (transfer full): A newly created #GcalRange
*/
GcalRange*
gcal_range_new (GDateTime *range_start,
GDateTime *range_end,
GcalRangeType range_type)
{
g_return_val_if_fail (range_start, NULL);
g_return_val_if_fail (range_end, NULL);
g_return_val_if_fail (g_date_time_compare (range_start, range_end) <= 0, NULL);
return gcal_range_new_take (g_date_time_ref (range_start),
g_date_time_ref (range_end),
range_type);
}
/**
* gcal_range_new_take:
* @range_start: (transfer full): a #GDateTime
* @range_end: (transfer full): a #GDateTime
*
* Creates a new #GcalRange and takes ownership of
* @range_start and @range_end.
*
* Returns: (transfer full): A newly created #GcalRange
*/
GcalRange*
gcal_range_new_take (GDateTime *range_start,
GDateTime *range_end,
GcalRangeType range_type)
{
GcalRange *self;
g_return_val_if_fail (range_start, NULL);
g_return_val_if_fail (range_end, NULL);
g_return_val_if_fail (g_date_time_compare (range_start, range_end) <= 0, NULL);
self = g_new0 (GcalRange, 1);
g_atomic_ref_count_init (&self->ref_count);
self->range_start = range_start;
self->range_end = range_end;
self->range_type = range_type;
return self;
}
/**
* gcal_range_get_start:
* @self: a #GcalRange
*
* Retrieves a copy of @self's range start.
*
* Returns: (transfer full): a #GDateTime
*/
GDateTime*
gcal_range_get_start (GcalRange *self)
{
g_return_val_if_fail (self, NULL);
g_return_val_if_fail (!g_atomic_ref_count_compare (&self->ref_count, 0), NULL);
return g_date_time_ref (self->range_start);
}
/**
* gcal_range_get_end:
* @self: a #GcalRange
*
* Retrieves a copy of @self's range end.
*
* Returns: (transfer full): a #GDateTime
*/
GDateTime*
gcal_range_get_end (GcalRange *self)
{
g_return_val_if_fail (self, NULL);
g_return_val_if_fail (!g_atomic_ref_count_compare (&self->ref_count, 0), NULL);
return g_date_time_ref (self->range_end);
}
/**
* gcal_range_calculate_overlap:
* @a: a #GcalRange
* @b: a #GcalRange
* @out_position: (direction out)(nullable): return location for a #GcalRangePosition
*
* Calculates how @a and @b overlap.
*
* The position returned at @out_position is always relative to @a. For example,
* %GCAL_RANGE_AFTER means @a is after @b. The heuristic for the position is:
*
* 1. If @a begins before @b, @a comes before @b.
* 2. If @a and @b begin at precisely the same moment, but @a ends before @b,
* @a comes before @b.
* 3. Otherwise, @b comes before @a.
*
* Returns: the overlap result between @a and @b
*/
GcalRangeOverlap
gcal_range_calculate_overlap (GcalRange *a,
GcalRange *b,
GcalRangePosition *out_position)
{
CompareDateTimeFunc compare_func;
GcalRangePosition position;
GcalRangeOverlap overlap;
gint a_start_b_start_diff;
gint a_end_b_end_diff;
g_return_val_if_fail (a && b, GCAL_RANGE_NO_OVERLAP);
/*
* There are 11 cases that we need to take care of:
*
* 1. Equal
*
* A |------------------------|
* B |------------------------|
*
* 2. Superset
*
* i.
* A |------------------------|
* B |-------------------|
*
* ii.
* A |------------------------|
* B |-------------------|
*
* iii.
* A |------------------------|
* B |-------------------|
*
* 3. Subset
*
* i.
* A |-------------------|
* B |------------------------|
*
* ii.
* A |-------------------|
* B |------------------------|
*
* iii.
* A |-------------------|
* B |------------------------|
*
* 4. Intersection
*
* i.
* A |--------------------|
* B |--------------------|
*
* ii.
* A |--------------------|
* B |--------------------|
*
* 5) No Overlap
*
* i.
* A |------------|
* B |-----------|
*
* ii.
* A |------------|
* B |-----------|
*
*/
compare_func = get_compare_func (a, b);
a_start_b_start_diff = compare_func (a->range_start, b->range_start);
a_end_b_end_diff = compare_func (a->range_end, b->range_end);
if (a_start_b_start_diff == 0 && a_end_b_end_diff == 0)
{
/* Case 1, the easiest */
overlap = GCAL_RANGE_EQUAL;
position = GCAL_RANGE_MATCH;
}
else
{
if (a_start_b_start_diff == 0)
{
if (a_end_b_end_diff > 0)
{
/* Case 2.i */
overlap = GCAL_RANGE_SUPERSET;
position = GCAL_RANGE_AFTER;
}
else /* a_end_b_end_diff < 0 */
{
/* Case 3.i */
overlap = GCAL_RANGE_SUBSET;
position = GCAL_RANGE_BEFORE;
}
}
else if (a_end_b_end_diff == 0)
{
if (a_start_b_start_diff < 0)
{
/* Case 2.iii */
overlap = GCAL_RANGE_SUPERSET;
position = GCAL_RANGE_BEFORE;
}
else /* a_start_b_start_diff > 0 */
{
/* Case 3.iii */
overlap = GCAL_RANGE_SUBSET;
position = GCAL_RANGE_AFTER;
}
}
else /* a_start_b_start_diff != 0 && a_end_b_end_diff != 0 */
{
if (a_start_b_start_diff < 0 && a_end_b_end_diff > 0)
{
/* Case 2.ii */
overlap = GCAL_RANGE_SUPERSET;
position = GCAL_RANGE_BEFORE;
}
else if (a_start_b_start_diff > 0 && a_end_b_end_diff < 0)
{
/* Case 3.ii */
overlap = GCAL_RANGE_SUBSET;
position = GCAL_RANGE_AFTER;
}
else
{
gint a_start_b_end_diff;
gint a_end_b_start_diff;
a_start_b_end_diff = compare_func (a->range_start, b->range_end);
a_end_b_start_diff = compare_func (a->range_end, b->range_start);
/* No overlap cases */
if (a_start_b_end_diff >= 0)
{
/* Case 5.i */
overlap = GCAL_RANGE_NO_OVERLAP;
position = GCAL_RANGE_AFTER;
}
else if (a_end_b_start_diff <= 0)
{
/* Case 5.ii */
overlap = GCAL_RANGE_NO_OVERLAP;
position = GCAL_RANGE_BEFORE;
}
else
{
/* Intersection cases */
if (a_start_b_start_diff < 0 && a_end_b_start_diff > 0 && a_end_b_end_diff < 0)
{
/* Case 4.i */
overlap = GCAL_RANGE_INTERSECTS;
position = GCAL_RANGE_BEFORE;
}
else if (a_start_b_start_diff > 0 && a_start_b_end_diff < 0 && a_end_b_end_diff > 0)
{
/* Case 4.ii */
overlap = GCAL_RANGE_INTERSECTS;
position = GCAL_RANGE_AFTER;
}
else
{
g_assert_not_reached ();
}
}
}
}
}
if (out_position)
*out_position = position;
return overlap;
}
/**
* gcal_range_compare:
* @a: a #GcalRange
* @b: a #GcalRange
*
* Compares @a and @b. See gcal_range_calculate_overlap() for the
* rules of when a range comes before or after.
*
* Returns: -1 is @a comes before @b, 0 if they're equal, or 1 if
* @a comes after @b.
*/
gint
gcal_range_compare (GcalRange *a,
GcalRange *b)
{
CompareDateTimeFunc compare_func;
gint result;
g_return_val_if_fail (a && b, 0);
compare_func = get_compare_func (a, b);
result = compare_func (a->range_start, b->range_start);
if (result == 0)
result = compare_func (b->range_end, a->range_end);
return result;
}
/**
* gcal_range_union:
* @a: a #GcalRange
* @b: a #GcalRange
*
* Creates a new #GcalRange with the union of @a and @b.
*
* Returns: (transfer full): a #GcalRange.
*/
GcalRange*
gcal_range_union (GcalRange *a,
GcalRange *b)
{
CompareDateTimeFunc compare_func;
GcalRangeType range_type;
GDateTime *start;
GDateTime *end;
g_return_val_if_fail (a != NULL, NULL);
g_return_val_if_fail (b != NULL, NULL);
compare_func = get_compare_func (a, b);
if (compare_func (a->range_start, b->range_start) < 0)
start = a->range_start;
else
start = b->range_start;
if (compare_func (a->range_end, b->range_end) > 0)
end = a->range_end;
else
end = b->range_end;
if (a->range_type == GCAL_RANGE_DATE_ONLY || b->range_type == GCAL_RANGE_DATE_ONLY)
range_type = GCAL_RANGE_DATE_ONLY;
else
range_type = GCAL_RANGE_DEFAULT;
return gcal_range_new (start, end, range_type);
}
/**
* gcal_range_to_string:
* @self: a #GcalRange
*
* Formats @self using ISO8601 dates. This is only useful
* for debugging purposes.
*
* Returns: (transfer full): a string representation of @self
*/
gchar*
gcal_range_to_string (GcalRange *self)
{
g_autofree gchar *start_string = NULL;
g_autofree gchar *end_string = NULL;
g_return_val_if_fail (self, NULL);
start_string = g_date_time_format_iso8601 (self->range_start);
end_string = g_date_time_format_iso8601 (self->range_end);
return g_strdup_printf ("[%s | %s)", start_string, end_string);
}
+111
View File
@@ -0,0 +1,111 @@
/* gcal-range.h
*
* Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <glib-object.h>
G_BEGIN_DECLS
#define GCAL_TYPE_RANGE (gcal_range_get_type ())
typedef struct _GcalRange GcalRange;
/**
* GcalRangeOverlap:
*
* @GCAL_RANGE_NO_OVERLAP: the ranges don't overlap
* @GCAL_RANGE_INTERSECTS: the ranges intersect, but have non-intersected areas
* @GCAL_RANGE_SUBSET: the first range is a subset of the second range
* @GCAL_RANGE_EQUAL: the ranges are exactly equal
* @GCAL_RANGE_SUPERSET: the first range is a superset of the second range
*
* The possible results of comparing two ranges.
*/
typedef enum
{
GCAL_RANGE_NO_OVERLAP,
GCAL_RANGE_INTERSECTS,
GCAL_RANGE_SUBSET,
GCAL_RANGE_EQUAL,
GCAL_RANGE_SUPERSET,
} GcalRangeOverlap;
/**
* GcalRangePosition:
*
* @GCAL_RANGE_BEFORE: range @a is before @b
* @GCAL_RANGE_AFTER: range @a is after @b
*
* The position of @a relative to @b. When the ranges are exactly equal, it
* is undefined.
*/
typedef enum
{
GCAL_RANGE_BEFORE = -1,
GCAL_RANGE_MATCH = 0,
GCAL_RANGE_AFTER = 1,
} GcalRangePosition;
/**
* GcalRangeType:
*
* @GCAL_RANGE_DEFAULT: the default (date and time) range type
* @GCAL_RANGE_DATE_ONLY: date-only range
*
* The type of the range. When comparing ranges, if either one of
* them is date-only, times are not considered by the comparison.
*/
typedef enum
{
GCAL_RANGE_DEFAULT,
GCAL_RANGE_DATE_ONLY,
} GcalRangeType;
GType gcal_range_get_type (void) G_GNUC_CONST;
GcalRange* gcal_range_new (GDateTime *range_start,
GDateTime *range_end,
GcalRangeType range_type);
GcalRange* gcal_range_new_take (GDateTime *range_start,
GDateTime *range_end,
GcalRangeType range_type);
GcalRange* gcal_range_copy (GcalRange *self);
GcalRange* gcal_range_ref (GcalRange *self);
void gcal_range_unref (GcalRange *self);
GDateTime* gcal_range_get_start (GcalRange *self);
GDateTime* gcal_range_get_end (GcalRange *self);
GcalRangeType gcal_range_get_range_type (GcalRange *self);
GcalRangeOverlap gcal_range_calculate_overlap (GcalRange *a,
GcalRange *b,
GcalRangePosition *out_position);
gint gcal_range_compare (GcalRange *a,
GcalRange *b);
GcalRange* gcal_range_union (GcalRange *a,
GcalRange *b);
gchar* gcal_range_to_string (GcalRange *self);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (GcalRange, gcal_range_unref)
G_END_DECLS
+1
View File
@@ -96,6 +96,7 @@ sources = files(
'core/gcal-log.c',
'core/gcal-manager.c',
'core/gcal-night-light-monitor.c',
'core/gcal-range.c',
'core/gcal-recurrence.c',
'core/gcal-shell-search-provider.c',
'core/gcal-timeline.c',
+1
View File
@@ -35,6 +35,7 @@ tests = [
'server',
'discoverer',
'event',
'range',
'range-tree',
]
+161
View File
@@ -0,0 +1,161 @@
/* test-range.c
*
* Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "gcal-range.h"
/*********************************************************************************************************************/
static void
range_new (void)
{
g_autoptr (GcalRange) range = NULL;
g_autoptr (GDateTime) start = NULL;
g_autoptr (GDateTime) end = NULL;
start = g_date_time_new_now_utc ();
end = g_date_time_add_seconds (start, 1);
range = gcal_range_new (start, end, GCAL_RANGE_DEFAULT);
g_assert_nonnull (range);
}
/*********************************************************************************************************************/
static void
range_invalid (void)
{
g_autoptr (GcalRange) range = NULL;
g_autoptr (GDateTime) start = NULL;
g_autoptr (GDateTime) end = NULL;
start = g_date_time_new_now_utc ();
end = g_date_time_add_seconds (start, 1);
g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "gcal_range_new: assertion 'range_start' failed");
range = gcal_range_new (NULL, NULL, GCAL_RANGE_DEFAULT);
g_test_assert_expected_messages ();
g_assert_null (range);
g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "gcal_range_new: assertion 'range_start' failed");
range = gcal_range_new (NULL, end, GCAL_RANGE_DEFAULT);
g_test_assert_expected_messages ();
g_assert_null (range);
g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "gcal_range_new: assertion 'range_end' failed");
range = gcal_range_new (start, NULL, GCAL_RANGE_DEFAULT);
g_test_assert_expected_messages ();
g_assert_null (range);
g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, "gcal_range_new: assertion 'g_date_time_compare (range_start, range_end) <= 0' failed");
range = gcal_range_new (end, start, GCAL_RANGE_DEFAULT);
g_test_assert_expected_messages ();
g_assert_null (range);
}
/*********************************************************************************************************************/
typedef struct
{
const gchar *start;
const gchar *end;
} TextRange;
static struct {
TextRange first;
TextRange second;
GcalRangeOverlap expected_overlap;
GcalRangePosition expected_position;
} ranges[] = {
/* Equal */
{
{ "2020-03-05T00:00:00", "2020-03-10T00:00:00" },
{ "2020-03-05T00:00:00", "2020-03-10T00:00:00" },
GCAL_RANGE_EQUAL,
GCAL_RANGE_MATCH,
},
/* No Overlap */
{
{ "2020-03-05T00:00:00", "2020-03-10T00:00:00" },
{ "2020-03-10T00:00:00", "2020-03-15T00:00:00" },
GCAL_RANGE_NO_OVERLAP,
GCAL_RANGE_BEFORE,
},
{
{ "2020-03-10T00:00:00", "2020-03-15T00:00:00" },
{ "2020-03-05T00:00:00", "2020-03-10T00:00:00" },
GCAL_RANGE_NO_OVERLAP,
GCAL_RANGE_AFTER,
},
};
static void
range_calculate_overlap (void)
{
g_autoptr (GTimeZone) utc = NULL;
gint i;
utc = g_time_zone_new_utc ();
for (i = 0; i < G_N_ELEMENTS (ranges); i++)
{
g_autoptr (GDateTime) second_start = NULL;
g_autoptr (GDateTime) second_end = NULL;
g_autoptr (GDateTime) first_start = NULL;
g_autoptr (GDateTime) first_end = NULL;
g_autoptr (GcalRange) range_a = NULL;
g_autoptr (GcalRange) range_b = NULL;
GcalRangePosition position;
GcalRangeOverlap overlap;
first_start = g_date_time_new_from_iso8601 (ranges[i].first.start, utc);
first_end = g_date_time_new_from_iso8601 (ranges[i].first.end, utc);
second_start = g_date_time_new_from_iso8601 (ranges[i].second.start, utc);
second_end = g_date_time_new_from_iso8601 (ranges[i].second.end, utc);
range_a = gcal_range_new (first_start, first_end, GCAL_RANGE_DEFAULT);
g_assert_nonnull (range_a);
range_b = gcal_range_new (second_start, second_end, GCAL_RANGE_DEFAULT);
g_assert_nonnull (range_b);
overlap = gcal_range_calculate_overlap (range_a, range_b, &position);
g_assert_cmpint (overlap, ==, ranges[i].expected_overlap);
g_assert_cmpint (position, ==, ranges[i].expected_position);
}
}
/*********************************************************************************************************************/
gint
main (gint argc,
gchar *argv[])
{
g_setenv ("TZ", "UTC", TRUE);
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/range/new", range_new);
g_test_add_func ("/range/invalid-ranges", range_invalid);
g_test_add_func ("/range/calculate-overlap", range_calculate_overlap);
return g_test_run ();
}