Rework QgsRange to better handle temporal ranges

Make a new QgsTemporalRange template class for specific
temporal classes.

Add QgsDateTimeRange class.
This commit is contained in:
Nyall Dawson 2017-04-19 15:45:32 +10:00
parent 18c8c1fe36
commit 23de9a0224
3 changed files with 277 additions and 120 deletions

View File

@ -12,6 +12,7 @@ class QgsDoubleRange
bool includeLower() const;
bool includeUpper() const;
bool isEmpty() const;
bool isSingleton() const;
bool contains( const QgsDoubleRange &other ) const;
bool contains( double element ) const;
bool overlaps( const QgsDoubleRange &other ) const;
@ -31,6 +32,7 @@ class QgsIntRange
bool includeLower() const;
bool includeUpper() const;
bool isEmpty() const;
bool isSingleton() const;
bool contains( const QgsIntRange &other ) const;
bool contains( int element ) const;
bool overlaps( const QgsIntRange &other ) const;
@ -44,14 +46,37 @@ class QgsDateRange
public:
QgsDateRange( QDate lower, QDate upper, bool includeLower = true, bool includeUpper = true );
QDate lower() const;
QDate upper() const;
bool includeLower() const;
bool includeUpper() const;
QgsDateRange( const QDate &lower = QDate(), const QDate &upper = QDate(), bool includeLower = true, bool includeUpper = true );
QDate begin() const;
QDate end() const;
bool includeBeginning() const;
bool includeEnd() const;
bool isEmpty() const;
bool isInstant() const;
bool isInfinite() const;
bool contains( const QgsDateRange &other ) const;
bool contains( QDate element ) const;
bool contains( const QDate &element ) const;
bool overlaps( const QgsDateRange &other ) const;
};
class QgsDateTimeRange
{
%TypeHeaderCode
#include <qgsrange.h>
%End
public:
QgsDateTimeRange( const QDateTime &lower = QDateTime(), const QDateTime &upper = QDateTime(), bool includeLower = true, bool includeUpper = true );
QDateTime begin() const;
QDateTime end() const;
bool includeBeginning() const;
bool includeEnd() const;
bool isEmpty() const;
bool isInstant() const;
bool isInfinite() const;
bool contains( const QgsDateTimeRange &other ) const;
bool contains( const QDateTime &element ) const;
bool overlaps( const QgsDateTimeRange &other ) const;
};

View File

@ -27,7 +27,7 @@
* A template based class for storing ranges (lower to upper values).
*
* QgsRange classes represent a range of values of some element type. For instance,
* ranges of QDateTime might be used to represent the ranges of timestamp ranges.
* ranges of int might be used to represent integer ranges.
*
* Ranges can indicate whether the upper and lower values are inclusive or exclusive.
* The inclusivity or exclusivity of bounds is considered when determining things like
@ -86,9 +86,16 @@ template <class T> class CORE_EXPORT QgsRange
/**
* Returns true if the range is empty, ie the lower bound equals (or exceeds) the upper bound
* and either the bounds are exclusive.
* \see isSingleton()
*/
bool isEmpty() const { return mLower > mUpper || ( mUpper == mLower && !( mIncludeLower || mIncludeUpper ) ); }
/**
* Returns true if the range consists only of a single value or instant.
* \see isEmpty()
*/
bool isSingleton() const { return mLower == mUpper && ( mIncludeLower || mIncludeUpper ); }
/**
* Returns true if this range contains another range.
* \see overlaps()
@ -176,6 +183,9 @@ template <class T> class CORE_EXPORT QgsRange
*/
typedef QgsRange< double > QgsDoubleRange;
/**
* QgsRange which stores a range of integer values.
* \since QGIS 3.0
@ -186,137 +196,225 @@ typedef QgsRange< double > QgsDoubleRange;
typedef QgsRange< int > QgsIntRange;
// specialization required to handle invalid QDate bounds
/**
* Returns true if the range is empty, ie the lower bound equals (or exceeds) the upper bound
* and either the bounds are exclusive.
* A range with both invalid lower and upper bounds is considered empty.
* \class QgsTemporalRange
* \ingroup core
* A template based class for storing temporal ranges (beginning to end values).
*
* QgsTemporalRange classes represent a range of values of some temporal type. For instance,
* ranges of QDateTime might be used to represent datetime ranges.
*
* Ranges can indicate whether the upper and lower values are inclusive or exclusive.
* The inclusivity or exclusivity of bounds is considered when determining things like
* whether ranges overlap or during calculation of range intersections.
*
* \since QGIS 3.0
* \see QgsDateRange
* \note not available in Python bindings
*/
template<>
bool QgsRange<QDate>::isEmpty() const
template <class T> class CORE_EXPORT QgsTemporalRange
{
if ( !mLower.isValid() && !mUpper.isValid() )
return true;
public:
if ( mLower.isValid() != mUpper.isValid() )
return false;
/**
* Constructor for QgsTemporalRange. The \a begin and \a end are specified,
* and optionally whether or not these bounds are included in the range.
*/
QgsTemporalRange( const T &begin = T(), const T &end = T(), bool includeBeginning = true, bool includeEnd = true )
: mLower( begin )
, mUpper( end )
, mIncludeLower( includeBeginning )
, mIncludeUpper( includeEnd )
{}
if ( mLower > mUpper )
return true;
/**
* Returns the beginning of the range.
* \see end()
* \see includeBeginning()
*/
T begin() const { return mLower; }
if ( mLower == mUpper && !( mIncludeLower || mIncludeUpper ) )
return true;
/**
* Returns the upper bound of the range.
* \see begin()
* \see includeEnd()
*/
T end() const { return mUpper; }
return false;
}
/**
* Returns true if the beginning is inclusive, or false if the beginning
* is exclusive.
* \see begin()
* \see includeEnd()
*/
bool includeBeginning() const { return mIncludeLower; }
/**
* Returns true if this range contains another range.
*/
template<>
bool QgsRange<QDate>::contains( const QgsRange<QDate> &other ) const
{
if ( !other.mLower.isValid() && mLower.isValid() )
return false;
/**
* Returns true if the end is inclusive, or false if the end is exclusive.
* \see end()
* \see includeBeginning()
*/
bool includeEnd() const { return mIncludeUpper; }
/**
* Returns true if the range consists only of a single instant.
* \see isEmpty()
* \see isInfinite()
*/
bool isInstant() const { return mLower.isValid() && mUpper.isValid() && mLower == mUpper && ( mIncludeLower || mIncludeUpper ); }
/**
* Returns true if the range consists of all possible values.
* \see isEmpty()
* \see isInstant()
*/
bool isInfinite() const
{
return !mLower.isValid() && !mUpper.isValid();
}
/**
* Returns true if the range is empty, ie the beginning equals (or exceeds) the end
* and either of the bounds are exclusive.
* A range with both invalid beginning and end is considered infinite and not empty.
*/
bool isEmpty() const
{
if ( !mLower.isValid() && !mUpper.isValid() )
return false;
if ( mLower.isValid() != mUpper.isValid() )
return false;
if ( mLower > mUpper )
return true;
if ( mLower == mUpper && !( mIncludeLower || mIncludeUpper ) )
return true;
if ( mLower.isValid() )
{
bool lowerOk = ( mIncludeLower && mLower <= other.mLower )
|| ( !mIncludeLower && mLower < other.mLower )
|| ( !mIncludeLower && !other.mIncludeLower && mLower <= other.mLower );
if ( !lowerOk )
return false;
}
}
if ( !other.mUpper.isValid() && mUpper.isValid() )
return false;
/**
* Returns true if this range contains another range.
*/
bool contains( const QgsTemporalRange<T> &other ) const
{
if ( !other.mLower.isValid() && mLower.isValid() )
return false;
if ( mLower.isValid() )
{
bool lowerOk = ( mIncludeLower && mLower <= other.mLower )
|| ( !mIncludeLower && mLower < other.mLower )
|| ( !mIncludeLower && !other.mIncludeLower && mLower <= other.mLower );
if ( !lowerOk )
return false;
}
if ( !other.mUpper.isValid() && mUpper.isValid() )
return false;
if ( mUpper.isValid() )
{
bool upperOk = ( mIncludeUpper && mUpper >= other.mUpper )
|| ( !mIncludeUpper && mUpper > other.mUpper )
|| ( !mIncludeUpper && !other.mIncludeUpper && mUpper >= other.mUpper );
if ( !upperOk )
return false;
}
return true;
}
/**
* Returns true if this range contains a specified \a element.
*/
bool contains( const T &element ) const
{
if ( !element.isValid() )
return false;
if ( mLower.isValid() )
{
bool lowerOk = ( mIncludeLower && mLower <= element )
|| ( !mIncludeLower && mLower < element );
if ( !lowerOk )
return false;
}
if ( mUpper.isValid() )
{
bool upperOk = ( mIncludeUpper && mUpper >= element )
|| ( !mIncludeUpper && mUpper > element );
if ( !upperOk )
return false;
}
return true;
}
/**
* Returns true if this range overlaps another range.
*/
bool overlaps( const QgsTemporalRange<T> &other ) const
{
if ( !mUpper.isValid() && ( ( mIncludeLower && mLower <= other.mUpper ) || ( !mIncludeLower && mLower < other.mUpper ) ) )
return true;
if ( ( ( mIncludeLower && mLower <= other.mLower ) || ( !mIncludeLower && mLower < other.mLower ) )
&& ( ( mIncludeUpper && mUpper >= other.mUpper ) || ( !mIncludeUpper && mUpper > other.mUpper ) ) )
return true;
if ( ( ( mIncludeLower && mLower <= other.mLower ) || ( !mIncludeLower && mLower < other.mLower ) )
&& ( ( mIncludeUpper && mUpper >= other.mLower ) || ( !mIncludeUpper && mUpper > other.mLower ) ) )
return true;
if ( ( ( mIncludeLower && mLower <= other.mUpper ) || ( !mIncludeLower && mLower < other.mUpper ) )
&& ( ( mIncludeUpper && mUpper >= other.mUpper ) || ( !mIncludeUpper && mUpper > other.mUpper ) ) )
return true;
if ( ( ( mIncludeLower && mLower >= other.mLower ) || ( !mIncludeLower && mLower > other.mLower ) )
&& ( ( mIncludeLower && mLower <= other.mUpper ) || ( !mIncludeLower && mLower < other.mUpper ) ) )
return true;
if ( mLower == other.mLower && mUpper == other.mUpper )
return true;
if ( mUpper.isValid() )
{
bool upperOk = ( mIncludeUpper && mUpper >= other.mUpper )
|| ( !mIncludeUpper && mUpper > other.mUpper )
|| ( !mIncludeUpper && !other.mIncludeUpper && mUpper >= other.mUpper );
if ( !upperOk )
return false;
}
}
return true;
}
private:
/**
* Returns true if this range contains a specified \a element.
*/
template<>
bool QgsRange<QDate>::contains( QDate element ) const
{
if ( !element.isValid() )
return false;
T mLower;
T mUpper;
bool mIncludeLower = true;
bool mIncludeUpper = true;
};
if ( mLower.isValid() )
{
bool lowerOk = ( mIncludeLower && mLower <= element )
|| ( !mIncludeLower && mLower < element );
if ( !lowerOk )
return false;
}
if ( mUpper.isValid() )
{
bool upperOk = ( mIncludeUpper && mUpper >= element )
|| ( !mIncludeUpper && mUpper > element );
if ( !upperOk )
return false;
}
return true;
}
/**
* Returns true if this range overlaps another range.
*/
template<>
bool QgsRange<QDate>::overlaps( const QgsRange<QDate> &other ) const
{
if ( !mUpper.isValid() && ( ( mIncludeLower && mLower <= other.mUpper ) || ( !mIncludeLower && mLower < other.mUpper ) ) )
return true;
if ( ( ( mIncludeLower && mLower <= other.mLower ) || ( !mIncludeLower && mLower < other.mLower ) )
&& ( ( mIncludeUpper && mUpper >= other.mUpper ) || ( !mIncludeUpper && mUpper > other.mUpper ) ) )
return true;
if ( ( ( mIncludeLower && mLower <= other.mLower ) || ( !mIncludeLower && mLower < other.mLower ) )
&& ( ( mIncludeUpper && mUpper >= other.mLower ) || ( !mIncludeUpper && mUpper > other.mLower ) ) )
return true;
if ( ( ( mIncludeLower && mLower <= other.mUpper ) || ( !mIncludeLower && mLower < other.mUpper ) )
&& ( ( mIncludeUpper && mUpper >= other.mUpper ) || ( !mIncludeUpper && mUpper > other.mUpper ) ) )
return true;
if ( ( ( mIncludeLower && mLower >= other.mLower ) || ( !mIncludeLower && mLower > other.mLower ) )
&& ( ( mIncludeLower && mLower <= other.mUpper ) || ( !mIncludeLower && mLower < other.mUpper ) ) )
return true;
if ( mLower == other.mLower && mUpper == other.mUpper )
return true;
return false;
}
/**
* QgsRange which stores a range of dates.
*
* Invalid QDates as the lower or upper bound are permitted. In this case,
* Invalid QDates as the beginning or end are permitted. In this case,
* the bound is considered to be infinite. E.g. QgsDateRange(QDate(),QDate(2017,1,1))
* is treated as a range containing all dates before 2017-1-1.
* QgsDateRange(QDate(2017,1,1),QDate()) is treated as a range containing all dates after 2017-1-1.
* \since QGIS 3.0
* \see QgsIntRange
* \see QgsDoubleRange
* \see QgsDateTimeRange
*/
typedef QgsRange< QDate > QgsDateRange;
typedef QgsTemporalRange< QDate > QgsDateRange;
/**
* QgsRange which stores a range of date times.
*
* Invalid QDateTimes as the beginning or end are permitted. In this case,
* the bound is considered to be infinite. E.g. QgsDateTimeRange(QDateTime(),QDateTime(2017,1,1))
* is treated as a range containing all dates before 2017-1-1.
* QgsDateTimeRange(QDateTime(2017,1,1),QDateTime()) is treated as a range containing all dates after 2017-1-1.
* \since QGIS 3.0
* \see QgsDateRange
*/
typedef QgsTemporalRange< QDateTime > QgsDateTimeRange;
#endif // QGSRANGE_H

View File

@ -48,6 +48,21 @@ class TestQgsIntRange(unittest.TestCase):
range = QgsIntRange(1, -1)
self.assertTrue(range.isEmpty())
def testIsSingleton(self):
range = QgsIntRange(1, 1)
self.assertTrue(range.isSingleton())
range = QgsIntRange(1, 10)
self.assertFalse(range.isSingleton())
range = QgsIntRange(1, 1, False, False)
# should not be singleton because 1 is NOT included
self.assertFalse(range.isSingleton())
# invalid range is not singleton
range = QgsIntRange(1, -1)
self.assertFalse(range.isSingleton())
def testContains(self):
# includes both ends
range = QgsIntRange(0, 10)
@ -179,18 +194,18 @@ class TestQgsDateRange(unittest.TestCase):
def testGetters(self):
range = QgsDateRange(QDate(2010, 3, 1), QDate(2010, 6, 2))
self.assertEqual(range.lower(), QDate(2010, 3, 1))
self.assertEqual(range.upper(), QDate(2010, 6, 2))
self.assertTrue(range.includeLower())
self.assertTrue(range.includeUpper())
self.assertEqual(range.begin(), QDate(2010, 3, 1))
self.assertEqual(range.end(), QDate(2010, 6, 2))
self.assertTrue(range.includeBeginning())
self.assertTrue(range.includeEnd())
range = QgsDateRange(QDate(), QDate(2010, 6, 2))
self.assertFalse(range.lower().isValid())
self.assertEqual(range.upper(), QDate(2010, 6, 2))
self.assertFalse(range.begin().isValid())
self.assertEqual(range.end(), QDate(2010, 6, 2))
range = QgsDateRange(QDate(2010, 3, 1), QDate())
self.assertEqual(range.lower(), QDate(2010, 3, 1))
self.assertFalse(range.upper().isValid())
self.assertEqual(range.begin(), QDate(2010, 3, 1))
self.assertFalse(range.end().isValid())
def testIsEmpty(self):
range = QgsDateRange(QDate(2010, 3, 1), QDate(2010, 6, 2))
@ -314,6 +329,25 @@ class TestQgsDateRange(unittest.TestCase):
self.assertFalse(range.overlaps(QgsDateRange(QDate(2009, 4, 1), QDate(2009, 8, 5))))
self.assertTrue(range.overlaps(QgsDateRange(QDate(2019, 4, 1), QDate(2019, 8, 5))))
def testIsInstant(self):
self.assertFalse(QgsDateRange(QDate(2010, 3, 1), QDate(2010, 6, 2)).isInstant())
self.assertTrue(QgsDateRange(QDate(2010, 3, 1), QDate(2010, 3, 1)).isInstant())
self.assertFalse(QgsDateRange(QDate(2010, 3, 1), QDate(2010, 3, 1), False, False).isInstant())
self.assertFalse(QgsDateRange(QDate(), QDate()).isInstant())
def testIsInfinite(self):
self.assertFalse(QgsDateRange(QDate(2010, 3, 1), QDate(2010, 6, 2)).isInfinite())
self.assertFalse(QgsDateRange(QDate(2010, 3, 1), QDate(2010, 3, 1)).isInfinite())
self.assertFalse(QgsDateRange(QDate(2010, 3, 1), QDate(2010, 3, 1), False, False).isInfinite())
self.assertTrue(QgsDateRange(QDate(), QDate()).isInfinite())
def testIsEmpty(self):
self.assertFalse(QgsDateRange(QDate(2010, 3, 1), QDate(2010, 6, 2)).isEmpty())
self.assertFalse(QgsDateRange(QDate(2010, 3, 1), QDate(2010, 3, 1)).isEmpty())
self.assertTrue(QgsDateRange(QDate(2010, 3, 1), QDate(2010, 3, 1), False, False).isEmpty())
self.assertTrue(QgsDateRange(QDate(2017, 3, 1), QDate(2010, 6, 2)).isEmpty())
self.assertFalse(QgsDateRange(QDate(), QDate()).isEmpty())
if __name__ == "__main__":
unittest.main()