Skip to content

Commit

Permalink
Add some methods to parse ISO8601 duration strings
Browse files Browse the repository at this point in the history
(eg '2021-03-23T00:00:00Z/2021-03-24T12:00:00Z/PT12H')
to a list of datetimes
  • Loading branch information
nyalldawson committed Mar 25, 2021
1 parent 1a7fe6a commit 1bb042b
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 0 deletions.
33 changes: 33 additions & 0 deletions python/core/auto_generated/qgstemporalutils.sip.in
Expand Up @@ -10,6 +10,8 @@





class QgsTemporalUtils
{
%Docstring(signature="appended")
Expand Down Expand Up @@ -100,6 +102,37 @@ the number of days in the current month.

.. versionadded:: 3.18
%End

static QList< QDateTime > calculateDateTimesUsingDuration( const QDateTime &start, const QDateTime &end, const QString &duration, bool &ok /Out/, bool &maxValuesExceeded /Out/, int maxValues = -1 );
%Docstring
Calculates a complete list of datetimes between ``start`` and ``end``, using the specified ISO8601 ``duration`` string (eg "PT12H").

:param start: start date time
:param end: end date time
:param duration: ISO8601 duration string
:param maxValues: maximum number of values to return, or -1 to return all values

:return: - calculated list of date times
- ok: will be set to ``True`` if ``duration`` was successfully parsed and date times could be calculated
- maxValuesExceeded: will be set to ``True`` if the maximum number of values to return was exceeded

.. versionadded:: 3.20
%End

static QList< QDateTime > calculateDateTimesFromISO8601( const QString &string, bool &ok /Out/, bool &maxValuesExceeded /Out/, int maxValues = -1 );
%Docstring
Calculates a complete list of datetimes from a ISO8601 ``string`` containing a duration (eg "2021-03-23T00:00:00Z/2021-03-24T12:00:00Z/PT12H").

:param string: ISO8601 compatible string
:param maxValues: maximum number of values to return, or -1 to return all values

:return: - calculated list of date times
- ok: will be set to ``True`` if ``string`` was successfully parsed and date times could be calculated
- maxValuesExceeded: will be set to ``True`` if the maximum number of values to return was exceeded

.. versionadded:: 3.20
%End

};


Expand Down
83 changes: 83 additions & 0 deletions src/core/qgstemporalutils.cpp
Expand Up @@ -209,3 +209,86 @@ QDateTime QgsTemporalUtils::calculateFrameTime( const QDateTime &start, const lo
}
}

QList<QDateTime> QgsTemporalUtils::calculateDateTimesUsingDuration( const QDateTime &start, const QDateTime &end, const QString &duration, bool &ok, bool &maxValuesExceeded, int maxValues )
{
ok = false;
const QgsTimeDuration timeDuration( QgsTimeDuration::fromString( duration, ok ) );
if ( !ok )
return {};

if ( timeDuration.years == 0 && timeDuration.months == 0 && timeDuration.weeks == 0 && timeDuration.days == 0
&& timeDuration.hours == 0 && timeDuration.minutes == 0 && timeDuration.seconds == 0 )
{
ok = false;
return {};
}

QList<QDateTime> res;
QDateTime current = start;
maxValuesExceeded = false;
while ( current <= end )
{
res << current;

if ( maxValues >= 0 && res.size() > maxValues )
{
maxValuesExceeded = true;
break;
}

if ( timeDuration.years )
current = current.addYears( timeDuration.years );
if ( timeDuration.months )
current = current.addMonths( timeDuration.months );
if ( timeDuration.weeks || timeDuration.days )
current = current.addDays( timeDuration.weeks * 7 + timeDuration.days );
if ( timeDuration.hours || timeDuration.minutes || timeDuration.seconds )
current = current.addSecs( timeDuration.hours * 60LL * 60 + timeDuration.minutes * 60 + timeDuration.seconds );
}
return res;
}

QList<QDateTime> QgsTemporalUtils::calculateDateTimesFromISO8601( const QString &string, bool &ok, bool &maxValuesExceeded, int maxValues )
{
ok = false;
maxValuesExceeded = false;
const QStringList parts = string.split( '/' );
if ( parts.length() != 3 )
{
return {};
}

const QDateTime start = QDateTime::fromString( parts.at( 0 ), Qt::ISODate );
if ( !start.isValid() )
return {};
const QDateTime end = QDateTime::fromString( parts.at( 1 ), Qt::ISODate );
if ( !end.isValid() )
return {};

return calculateDateTimesUsingDuration( start, end, parts.at( 2 ), ok, maxValuesExceeded, maxValues );
}

//
// QgsTimeDuration
//

QgsTimeDuration QgsTimeDuration::fromString( const QString &string, bool &ok )
{
ok = false;
thread_local QRegularExpression sRx( QStringLiteral( R"(P(?:([\d]+)Y)?(?:([\d]+)M)?(?:([\d]+)W)?(?:([\d]+)D)?(?:T(?:([\d]+)H)?(?:([\d]+)M)?(?:([\d\.]+)S)?)?$)" ) );

const QRegularExpressionMatch match = sRx.match( string );
QgsTimeDuration duration;
if ( match.hasMatch() )
{
ok = true;
duration.years = match.capturedView( 1 ).toInt();
duration.months = match.capturedView( 2 ).toInt();
duration.weeks = match.capturedView( 3 ).toInt();
duration.days = match.capturedView( 4 ).toInt();
duration.hours = match.capturedView( 5 ).toInt();
duration.minutes = match.capturedView( 6 ).toInt();
duration.seconds = match.capturedView( 7 ).toDouble();
}
return duration;
}
65 changes: 65 additions & 0 deletions src/core/qgstemporalutils.h
Expand Up @@ -25,6 +25,46 @@ class QgsMapSettings;
class QgsFeedback;
class QgsMapDecoration;

#ifndef SIP_RUN

/**
* \ingroup core
* \class QgsTimeDuration
* \brief Contains utility methods for working with temporal layers and projects.
*
* Designed for storage of ISO8601 duration values.
*
* \note Not available in Python bindings
* \since QGIS 3.20
*/
class CORE_EXPORT QgsTimeDuration
{
public:

//! Years
int years = 0;
//! Months
int months = 0;
//! Weeks
int weeks = 0;
//! Days
int days = 0;
//! Hours
int hours = 0;
//! Minutes
int minutes = 0;
//! Seconds
double seconds = 0;

/**
* Creates a QgsTimeDuration from a \a string value.
*/
static QgsTimeDuration fromString( const QString &string, bool &ok );

};
#endif


/**
* \ingroup core
* \class QgsTemporalUtils
Expand Down Expand Up @@ -125,6 +165,31 @@ class CORE_EXPORT QgsTemporalUtils
* \since QGIS 3.18
*/
static QDateTime calculateFrameTime( const QDateTime &start, const long long frame, const QgsInterval interval );

/**
* Calculates a complete list of datetimes between \a start and \a end, using the specified ISO8601 \a duration string (eg "PT12H").
* \param start start date time
* \param end end date time
* \param duration ISO8601 duration string
* \param ok will be set to TRUE if \a duration was successfully parsed and date times could be calculated
* \param maxValuesExceeded will be set to TRUE if the maximum number of values to return was exceeded
* \param maxValues maximum number of values to return, or -1 to return all values
* \returns calculated list of date times
* \since QGIS 3.20
*/
static QList< QDateTime > calculateDateTimesUsingDuration( const QDateTime &start, const QDateTime &end, const QString &duration, bool &ok SIP_OUT, bool &maxValuesExceeded SIP_OUT, int maxValues = -1 );

/**
* Calculates a complete list of datetimes from a ISO8601 \a string containing a duration (eg "2021-03-23T00:00:00Z/2021-03-24T12:00:00Z/PT12H").
* \param string ISO8601 compatible string
* \param ok will be set to TRUE if \a string was successfully parsed and date times could be calculated
* \param maxValuesExceeded will be set to TRUE if the maximum number of values to return was exceeded
* \param maxValues maximum number of values to return, or -1 to return all values
* \returns calculated list of date times
* \since QGIS 3.20
*/
static QList< QDateTime > calculateDateTimesFromISO8601( const QString &string, bool &ok SIP_OUT, bool &maxValuesExceeded SIP_OUT, int maxValues = -1 );

};


Expand Down
116 changes: 116 additions & 0 deletions tests/src/python/test_qgstemporalutils.py
Expand Up @@ -144,6 +144,122 @@ def testFrameTimeCalculation(self):
QgsInterval(0.2, unit))
self.assertEqual(f, expected3[unit])

def testCalculateDateTimesUsingDuration(self):
# invalid duration string
vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2021, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 3, 24), QTime(12, 0, 0)), 'xT12H')
self.assertFalse(ok)
# null duration string
vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2021, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 3, 24), QTime(12, 0, 0)), '')
self.assertFalse(ok)

vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2021, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 3, 24), QTime(12, 0, 0)), 'P')
self.assertFalse(ok)

# valid durations
vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2021, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 3, 24), QTime(12, 0, 0)), 'PT12H')
self.assertEqual(vals, [QDateTime(2021, 3, 23, 0, 0),
QDateTime(2021, 3, 23, 12, 0),
QDateTime(2021, 3, 24, 0, 0),
QDateTime(2021, 3, 24, 12, 0)])
self.assertTrue(ok)
self.assertFalse(exceeded)

vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2021, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 3, 24), QTime(12, 0, 0)), 'PT12H', maxValues=2)
self.assertEqual(vals, [QDateTime(2021, 3, 23, 0, 0),
QDateTime(2021, 3, 23, 12, 0),
QDateTime(2021, 3, 24, 0, 0)])
self.assertTrue(ok)
self.assertTrue(exceeded)

vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2021, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 3, 24), QTime(12, 0, 0)), 'PT10H2M5S')
self.assertEqual(vals, [QDateTime(2021, 3, 23, 0, 0), QDateTime(2021, 3, 23, 10, 2, 5),
QDateTime(2021, 3, 23, 20, 4, 10), QDateTime(2021, 3, 24, 6, 6, 15)])
self.assertTrue(ok)
self.assertFalse(exceeded)

vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2010, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 5, 24), QTime(12, 0, 0)), 'P2Y')
self.assertEqual(vals,
[QDateTime(2010, 3, 23, 0, 0), QDateTime(2012, 3, 23, 0, 0), QDateTime(2014, 3, 23, 0, 0),
QDateTime(2016, 3, 23, 0, 0), QDateTime(2018, 3, 23, 0, 0), QDateTime(2020, 3, 23, 0, 0)])
self.assertTrue(ok)
self.assertFalse(exceeded)

vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2020, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 5, 24), QTime(12, 0, 0)), 'P2M')
self.assertEqual(vals,
[QDateTime(2020, 3, 23, 0, 0), QDateTime(2020, 5, 23, 0, 0), QDateTime(2020, 7, 23, 0, 0),
QDateTime(2020, 9, 23, 0, 0), QDateTime(2020, 11, 23, 0, 0), QDateTime(2021, 1, 23, 0, 0),
QDateTime(2021, 3, 23, 0, 0), QDateTime(2021, 5, 23, 0, 0)])
self.assertTrue(ok)
self.assertFalse(exceeded)

vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2021, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 5, 24), QTime(12, 0, 0)), 'P2W')
self.assertEqual(vals, [QDateTime(2021, 3, 23, 0, 0), QDateTime(2021, 4, 6, 0, 0), QDateTime(2021, 4, 20, 0, 0),
QDateTime(2021, 5, 4, 0, 0), QDateTime(2021, 5, 18, 0, 0)])
self.assertTrue(ok)
self.assertFalse(exceeded)

vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2021, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 4, 7), QTime(12, 0, 0)), 'P2D')
self.assertEqual(vals,
[QDateTime(2021, 3, 23, 0, 0), QDateTime(2021, 3, 25, 0, 0), QDateTime(2021, 3, 27, 0, 0),
QDateTime(2021, 3, 29, 0, 0), QDateTime(2021, 3, 31, 0, 0), QDateTime(2021, 4, 2, 0, 0),
QDateTime(2021, 4, 4, 0, 0), QDateTime(2021, 4, 6, 0, 0)])
self.assertTrue(ok)
self.assertFalse(exceeded)

# complex mix
vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesUsingDuration(
QDateTime(QDate(2010, 3, 23), QTime(0, 0, 0)),
QDateTime(QDate(2021, 5, 24), QTime(12, 0, 0)), 'P2Y1M3W4DT5H10M22S')
self.assertEqual(vals, [QDateTime(2010, 3, 23, 0, 0), QDateTime(2012, 5, 18, 5, 10, 22),
QDateTime(2014, 7, 13, 10, 20, 44), QDateTime(2016, 9, 7, 15, 31, 6),
QDateTime(2018, 11, 1, 20, 41, 28), QDateTime(2020, 12, 27, 1, 51, 50)])
self.assertTrue(ok)
self.assertFalse(exceeded)

def testCalculateDateTimesFromISO8601(self):
# invalid duration string
vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesFromISO8601('x')
self.assertFalse(ok)

vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesFromISO8601(
'a-03-23T00:00:00Z/2021-03-24T12:00:00Z/PT12H')
self.assertFalse(ok)
vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesFromISO8601(
'2021-03-23T00:00:00Z/b-03-24T12:00:00Z/PT12H')
self.assertFalse(ok)
vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesFromISO8601(
'2021-03-23T00:00:00Z/2021-03-24T12:00:00Z/xc')
self.assertFalse(ok)

vals, ok, exceeded = QgsTemporalUtils.calculateDateTimesFromISO8601(
'2021-03-23T00:00:00Z/2021-03-24T12:00:00Z/PT12H')
self.assertEqual(vals, [QDateTime(2021, 3, 23, 0, 0, 0, 0, Qt.TimeSpec(1)),
QDateTime(2021, 3, 23, 12, 0, 0, 0, Qt.TimeSpec(1)),
QDateTime(2021, 3, 24, 0, 0, 0, 0, Qt.TimeSpec(1)),
QDateTime(2021, 3, 24, 12, 0, 0, 0, Qt.TimeSpec(1))])
self.assertTrue(ok)
self.assertFalse(exceeded)


if __name__ == '__main__':
unittest.main()

0 comments on commit 1bb042b

Please sign in to comment.