[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-core-devel
Subject:    Re: PATCH: time-only support for KDateTime (used in Nepomuk-KDE)
From:       Sebastian =?utf-8?q?Tr=C3=BCg?= <strueg () mandriva ! com>
Date:       2007-03-16 10:52:33
Message-ID: 200703161152.34356.strueg () mandriva ! com
[Download RAW message or body]

On Thursday 15 March 2007 14:12:40 David Jarvie wrote:
> On Thursday 15 March 2007 12:02, Sebastian TrĂ¼g wrote:
> > Hi folks,
> >
> > Aaron asked me if KDateTime already provided the date and time conversion
> > functionality from KMetaData::DateTime. Actually the ISO8601 support in
> > KDateTime is nearly perfect. The only thing missing is the support for
> > time-only instances and parsing of date- and time-only strings like
> >
> > YYYY-MM-DD
> >
> > the attached patch does that. However, it breaks one unit test: with my
> > patch
> > KDateTime does create date-only ISO8601 strings like the one above which
> > is
> > perfectly valid as far as I understood ISO8601. The current
> > implementation appends the SOD time (00:00:00.000) to a date-only string.
> > This would not be
> > that big a problem for Nepomuk-KDE but just a little ugly IMHO.
>
> I did wonder when writing KDateTime whether time-only values should be
> included, and evidently they should after all. :)
>
> I won't be able to try it out until the weekend, but AFAICR date-only
> strings like YYYY-MM-DD *are* handled, provided no time zone or time

well, they are not parsed without my patch.

> offset is specified. My reading of ISO8601 is that if a time zone
> specification/offset is included in the string, a time value must also be
> included. That's why a time of 00:00:00 is appended to the date for a
> date-only value except when it's ClockTime. Unless I've misunderstood the
> standard, I don't think your change is correct.

hmm, I don't really get the use of a timezone for a date-only instance anyway.
Anyway, that would mean that I just have to use ClockTime date-only instances 
in Nepomuk-KDE. That seems reasonable.
I attached a new patch that changes it back to the original behaviour.

> I'm doubtful about the treatment of time-only values which have a TimeZone
> time specification. Without an associated date, a time value is ambiguous
> if a time zone is applied. (Time zones can adjust the meaning of a time by
> up to 2 hours depending on the date in the year. And the meaning is even
> more ambiguous for years before or after the validity of the time zone
> data.) Because of this, I suggest that a time zone specification should be
> treated the same as ClockTime. I would still allow a time zone to be
> specified for record purposes, but it should be disregarded in
> calculations.

Why not just inform the user (via documentation) that calculations with a 
timezone can never be 100% accurate. Because there may be many situations in 
which it makes sense to have time-only values with a time-zone.

> When calculations are performed on time-only values, the result should
> always be either a time-only value or invalid. AFAICS, the necessary mods
> to the code to achieve this haven't been done. For example, in
> KDateTime::addSecs(), it doesn't look as if a time-only value is returned
> when 'this' is time-only, and if the time addition happened to wrap round
> midnight, even the date wouldn't be sot.

I fixed that and also the other addXXX methods. I hope that I did not miss 
anything. I did not write time-only unit tests yet though.

> In KDateTimePrivate::setTimeOnly(), mDt should be set using setDt() to
> ensure that 'ut' and caching members are updated properly.

done.

> In kdatetime.h, the new %f format specification should be described
> sufficiently to not need to look up an external reference to find out what
> output it produces. By all means include the reference, but also explain
> its effects.

also done. I hope an example is enough.

Cheers,
Sebastian

["kdatetime-timeonly.diff" (text/x-diff)]

Index: kdatetime.cpp
===================================================================
--- kdatetime.cpp	(revision 641810)
+++ kdatetime.cpp	(working copy)
@@ -1,6 +1,7 @@
 /*
     This file is part of the KDE libraries
     Copyright (c) 2005,2006 David Jarvie <software@astrojar.org.uk>
+    Copyright (c) 2007 Sebastian Trueg <trueg@kde.org>
 
     This library is free software; you can redistribute it and/or
     modify it under the terms of the GNU Library General Public
@@ -320,7 +321,8 @@
           status(stValid),
           utcCached(true),
           m2ndOccurrence(false),
-          mDateOnly(false)
+          mDateOnly(false),
+          mTimeOnly(false)
     {
         converted.tz = 0;   // flag time zone conversion as invalid
     }
@@ -332,8 +334,11 @@
           status(stValid),
           utcCached(false),
           m2ndOccurrence(false),
-          mDateOnly(donly)
+          mDateOnly(donly),
+          mTimeOnly(false)
     {
+        if (!mDt.date().isValid())
+            setTimeOnly(true);
         switch (specType)
         {
             case KDateTime::TimeZone:
@@ -364,6 +369,7 @@
           utcCached(rhs.utcCached),
           m2ndOccurrence(rhs.m2ndOccurrence),
           mDateOnly(rhs.mDateOnly),
+          mTimeOnly(rhs.mTimeOnly),
           converted2ndOccur(rhs.converted2ndOccur)
     {}
 
@@ -373,14 +379,16 @@
     KDateTime::Spec spec() const;
     QDateTime utc() const                    { return QDateTime(ut.date, ut.time, \
Qt::UTC); }  bool      dateOnly() const               { return mDateOnly; }
+    bool      timeOnly() const               { return mTimeOnly; }
     bool      secondOccurrence() const       { return m2ndOccurrence; }
     void      setDt(const QDateTime &dt)     { mDt = dt; utcCached = m2ndOccurrence \
= false; }  void      setDtFromUtc(const QDateTime &utcdt);
-    void      setDate(const QDate &d)        { mDt.setDate(d); utcCached = \
m2ndOccurrence = false; } +    void      setDate(const QDate &d)        { \
                mDt.setDate(d); utcCached = mTimeOnly = m2ndOccurrence = false; }
     void      setTime(const QTime &t)        { mDt.setTime(t); utcCached = mDateOnly \
                = m2ndOccurrence = false; }
     void      setDtTimeSpec(Qt::TimeSpec s)  { mDt.setTimeSpec(s); utcCached = \
m2ndOccurrence = false; }  void      setSpec(const KDateTime::Spec&);
     void      setDateOnly(bool d);
+    void      setTimeOnly(bool d);
     int       timeZoneOffset() const;
     QDateTime toUtc(const KTimeZone *local = 0) const;
     QDateTime toZone(const KTimeZone *zone, const KTimeZone *local = 0) const;
@@ -429,6 +437,7 @@
 
 
     static QTime        sod;               // start of day (00:00:00)
+    static QDate        sot;               // start of time (01.01.0000)
 
     /* Because some applications create thousands of instances of KDateTime, this
      * data structure is designed to minimize memory usage. Ensure that all small
@@ -462,11 +471,13 @@
     mutable bool         m2ndOccurrence    : 1; // this is the second occurrence of \
a time zone time  private:
     bool                 mDateOnly         : 1; // true to ignore the time part
+    bool                 mTimeOnly         : 1; // true to ignore the date parts
     mutable bool         converted2ndOccur : 1; // this is the second occurrence of \
'converted' time  };
 
 
 QTime KDateTimePrivate::sod(0,0,0);
+QDate KDateTimePrivate::sot(1,1,1);
 
 KDateTime::Spec KDateTimePrivate::spec() const
 {
@@ -551,6 +562,16 @@
     }
 }
 
+void KDateTimePrivate::setTimeOnly(bool timeOnly)
+{
+    if (timeOnly != mTimeOnly)
+    {
+        mTimeOnly = timeOnly;
+        if (timeOnly && mDt.date() != sot)
+            setDt( QDateTime( sot, mDt.time() ) );
+    }
+}
+
 /* Sets the date/time to a given UTC date/time. The time spec is not changed. */
 void KDateTimePrivate::setDtFromUtc(const QDateTime &utcdt)
 {
@@ -736,6 +757,7 @@
     newd->specType       = KDateTime::TimeZone;
     newd->utcCached      = utcCached;
     newd->mDateOnly      = mDateOnly;
+    newd->mTimeOnly      = mTimeOnly;
     newd->m2ndOccurrence = converted2ndOccur;
     switch (specType)
     {
@@ -771,6 +793,13 @@
         d->setDtTimeSpec(Qt::UTC);
 }
 
+KDateTime::KDateTime(const QTime &time, const Spec &spec)
+: d(new KDateTimePrivate(QDateTime(QDate(), time, Qt::LocalTime), spec, false))
+{
+    if (spec.type() == UTC)
+        d->setDtTimeSpec(Qt::UTC);
+}
+
 KDateTime::KDateTime(const QDate &date, const QTime &time, const Spec &spec)
   : d(new KDateTimePrivate(QDateTime(date, time, Qt::LocalTime), spec))
 {
@@ -816,6 +845,7 @@
 bool      KDateTime::isValid() const            { return d->specType != Invalid  &&  \
d->dt().isValid(); }  bool      KDateTime::outOfRange() const         { return \
d->status == stTooEarly; }  bool      KDateTime::isDateOnly() const         { return \
d->dateOnly(); } +bool      KDateTime::isTimeOnly() const         { return \
d->timeOnly(); }  bool      KDateTime::isLocalZone() const        { return \
d->specType == TimeZone  &&  d->z.tz == KSystemTimeZones::local(); }  bool      \
KDateTime::isClockTime() const        { return d->specType == ClockTime; }  bool      \
KDateTime::isUtc() const              { return d->specType == UTC || (d->specType == \
OffsetFromUTC && d->z.utcOffset == 0); } @@ -1060,6 +1090,10 @@
         result.d->setDate(d->date().addDays(static_cast<int>(msecs / 86400000)));
         return result;
     }
+    if (d->timeOnly())
+    {
+        return KDateTime( QDate(), time().addMSecs(msecs), d->spec());
+    }
     qint64 secs = msecs / 1000;
     int oldms = d->dt().time().msec();
     int ms = oldms  +  static_cast<int>(msecs % 1000);
@@ -1097,6 +1131,10 @@
         result.d->setDate(d->date().addDays(days));
         return result;
     }
+    if (d->timeOnly())
+    {
+        return KDateTime( QDate(), time().addSecs(secs), d->spec());
+    }
     if (d->specType == ClockTime)
     {
         QDateTime qdt = d->dt();
@@ -1110,6 +1148,8 @@
 
 KDateTime KDateTime::addDays(int days) const
 {
+    if (d->timeOnly())
+        return *this;
     KDateTime result(*this);
     result.d->setDate(d->date().addDays(days));
     return result;
@@ -1117,6 +1157,8 @@
 
 KDateTime KDateTime::addMonths(int months) const
 {
+    if (d->timeOnly())
+        return *this;
     KDateTime result(*this);
     result.d->setDate(d->date().addMonths(months));
     return result;
@@ -1124,6 +1166,8 @@
 
 KDateTime KDateTime::addYears(int years) const
 {
+    if (d->timeOnly())
+        return *this;
     KDateTime result(*this);
     result.d->setDate(d->date().addYears(years));
     return result;
@@ -1169,6 +1213,8 @@
 {
     if (!isValid() || !t2.isValid())
         return 0;
+    if (d->timeOnly() || t2.d->timeOnly())
+        return 0;
     if (d->dateOnly())
     {
         QDate dat = t2.d->dateOnly() ? t2.d->date() : \
t2.toTimeSpec(d->spec()).d->date(); @@ -1231,6 +1277,9 @@
 
 KDateTime::Comparison KDateTime::compare(const KDateTime &other) const
 {
+    if (d->timeOnly() != other.d->timeOnly())
+        return Impossible;
+
     QDateTime start1, start2;
     bool conv = (!d->equalSpec(*other.d) || d->secondOccurrence() != \
other.d->secondOccurrence());  if (conv)
@@ -1305,6 +1354,8 @@
         return true;   // the two instances share the same data
     if (d->dateOnly() != other.d->dateOnly())
         return false;
+    if (d->timeOnly() != other.d->timeOnly())
+        return false;
     if (d->equalSpec(*other.d))
     {
         // Both instances are in the same time zone, so compare directly
@@ -1450,6 +1501,17 @@
                     num = d->dt().time().second();
                     numLength = 2;
                     break;
+                case 'f':
+                    if (d->dt().time().msec())
+                    {
+                        // Comma is preferred by ISO8601 as the decimal point \
symbol, +                        // so use it unless '.' is the symbol used in this \
locale. +                        result += (KGlobal::locale()->decimalSymbol() == \
QLatin1String(".")) ? QLatin1Char('.') : QLatin1Char(','); +                        \
result += s.sprintf("%03d", d->dt().time().msec()); +                        while \
(result.endsWith("0")) +                            \
result.truncate(result.length()-1); +                    }
+                    break;
                 case 'P':     // am/pm
                 {
                     bool am = (d->dt().time().hour() < 12);
@@ -1652,11 +1714,14 @@
                 year = -year;
             }
             QString s;
-            result += s.sprintf("%04d-%02d-%02d",
-                                year, d->date().month(), d->date().day());
+            if (!d->timeOnly())
+                result += s.sprintf("%04d-%02d-%02d",
+                                    year, d->date().month(), d->date().day());
             if (!d->dateOnly()  ||  d->specType != ClockTime)
             {
-                result += s.sprintf("T%02d:%02d:%02d",
+                if (!d->timeOnly())
+                    result += 'T';
+                result += s.sprintf("%02d:%02d:%02d",
                                     d->dt().time().hour(), d->dt().time().minute(), \
d->dt().time().second());  if (d->dt().time().msec())
                 {
@@ -1664,6 +1729,9 @@
                     // so use it unless '.' is the symbol used in this locale.
                     result += (KGlobal::locale()->decimalSymbol() == \
QLatin1String(".")) ? QLatin1Char('.') : QLatin1Char(',');  result += \
s.sprintf("%03d", d->dt().time().msec()); +                    // The fraction cannot \
end with a 0 +                    while (result.endsWith("0"))
+                        result.truncate(result.length()-1);
                 }
             }
             if (d->specType == UTC)
@@ -1888,6 +1956,7 @@
              * to those interchanging the data to agree on a scheme.
              */
             bool dateOnly = false;
+            bool timeOnly = false;
             // Check first for the extended format of ISO 8601
             QRegExp rx("^([+-])?(\\d{4,})-(\\d\\d\\d|\\d\\d-\\d\\d)[T \
](\\d\\d)(?::(\\d\\d)(?::(\\d\\d)(?:(?:\\.|,)(\\d+))?)?)?(Z|([+-])(\\d\\d)(?::(\\d\\d))?)?$");
  if (str.indexOf(rx))
@@ -1910,7 +1979,16 @@
                             {
                                 rx = QRegExp("^([+-])?(\\d{4})(\\d{3})$");
                                 if (str.indexOf(rx))
-                                    break;
+                                {
+                                    dateOnly = false;
+                                    timeOnly = true;
+                                    // time only
+                                    rx = \
QRegExp("^(\\d\\d)(?::(\\d\\d)(?::(\\d\\d)(?:(?:\\.|,)(\\d+))?)?)?(Z|([+-])(\\d\\d)(?::(\\d\\d))?)?$");
 +                                    if (str.indexOf(rx))
+                                    {
+                                        break;
+                                    }
+                                }
                             }
                         }
                     }
@@ -1924,65 +2002,72 @@
             int second = 0;
             int msecs  = 0;
             bool leapSecond = false;
-            int year = parts[2].toInt(&ok);
-            if (!ok)
-                break;
+            int year = 0;
+            int i = 1;
+            if (!timeOnly) {
+                i += 3;
+                year = parts[2].toInt(&ok);
+                if (!ok)
+                    break;
+            }
             if (parts[1] == QLatin1String("-"))
                 year = -year;
             if (!dateOnly)
             {
-                hour = parts[4].toInt(&ok);
+                hour = parts[i].toInt(&ok);
                 if (!ok)
                     break;
-                if (!parts[5].isEmpty())
+                if (!parts[i+1].isEmpty())
                 {
-                    minute = parts[5].toInt(&ok);
+                    minute = parts[i+1].toInt(&ok);
                     if (!ok)
                         break;
                 }
-                if (!parts[6].isEmpty())
+                if (!parts[i+2].isEmpty())
                 {
-                    second = parts[6].toInt(&ok);
+                    second = parts[i+2].toInt(&ok);
                     if (!ok)
                         break;
                 }
                 leapSecond = (second == 60);
                 if (leapSecond)
                     second = 59;   // apparently a leap second - validate below, \
                once time zone is known
-                if (!parts[7].isEmpty())
+                if (!parts[i+3].isEmpty())
                 {
-                    QString ms = parts[7] + QLatin1String("00");
+                    QString ms = parts[i+3] + QLatin1String("00");
                     ms.truncate(3);
                     msecs = ms.toInt(&ok);
                     if (!ok)
                         break;
                 }
             }
-            int month, day;
             Status invalid = stValid;
-            if (parts[3].length() == 3)
-            {
-                // A day of the year is specified
-                day = parts[3].toInt(&ok);
-                if (!ok || day < 1 || day > 366)
-                    break;
-                d = checkDate(year, 1, 1, invalid).addDays(day - 1);   // convert \
                date, and check for out-of-range
-                if (!d.isValid()  ||  (!invalid && d.year() != year))
-                    break;
-                day   = d.day();
-                month = d.month();
+            if (!timeOnly) {
+                int month, day;
+                if (parts[3].length() == 3)
+                {
+                    // A day of the year is specified
+                    day = parts[3].toInt(&ok);
+                    if (!ok || day < 1 || day > 366)
+                        break;
+                    d = checkDate(year, 1, 1, invalid).addDays(day - 1);   // \
convert date, and check for out-of-range +                    if (!d.isValid()  ||  \
(!invalid && d.year() != year)) +                        break;
+                    day   = d.day();
+                    month = d.month();
+                }
+                else
+                {
+                    // A month and day are specified
+                    month = parts[3].left(2).toInt(&ok);
+                    day   = parts[3].right(2).toInt(&ok1);
+                    if (!ok || !ok1)
+                        break;
+                    d = checkDate(year, month, day, invalid);   // convert date, and \
check for out-of-range +                    if (!d.isValid())
+                        break;
+                }
             }
-            else
-            {
-                // A month and day are specified
-                month = parts[3].left(2).toInt(&ok);
-                day   = parts[3].right(2).toInt(&ok1);
-                if (!ok || !ok1)
-                    break;
-                d = checkDate(year, month, day, invalid);   // convert date, and \
                check for out-of-range
-                if (!d.isValid())
-                    break;
-            }
             if (dateOnly)
             {
                 if (invalid)
@@ -1993,7 +2078,7 @@
                 }
                 return KDateTime(d, Spec(ClockTime));
             }
-            if (hour == 24  && !minute && !second && !msecs)
+            if (!timeOnly && hour == 24  && !minute && !second && !msecs)
             {
                 // A time of 24:00:00 is allowed by ISO 8601, and means midnight at \
the end of the day  d = d.addDays(1);
@@ -2003,7 +2088,7 @@
             QTime t(hour, minute, second, msecs);
             if (!t.isValid())
                 break;
-            if (parts[8].isEmpty())
+            if (parts[i+4].isEmpty())
             {
                 // No UTC offset is specified. Don't try to validate leap seconds.
                 if (invalid)
@@ -2015,19 +2100,19 @@
                 return KDateTime(d, t, KDateTimePrivate::fromStringDefault());
             }
             int offset = 0;
-            SpecType spec = (parts[8] == QLatin1String("Z")) ? UTC : OffsetFromUTC;
+            SpecType spec = (parts[i+4] == QLatin1String("Z")) ? UTC : \
OffsetFromUTC;  if (spec == OffsetFromUTC)
             {
-                offset = parts[10].toInt(&ok) * 3600;
+                offset = parts[i+6].toInt(&ok) * 3600;
                 if (!ok)
                     break;
-                if (!parts[11].isEmpty())
+                if (!parts[i+7].isEmpty())
                 {
-                    offset += parts[11].toInt(&ok) * 60;
+                    offset += parts[i+7].toInt(&ok) * 60;
                     if (!ok)
                         break;
                 }
-                if (parts[9] == QLatin1String("-"))
+                if (parts[i+5] == QLatin1String("-"))
                 {
                     offset = -offset;
                     if (!offset && negZero)
Index: kdatetime.h
===================================================================
--- kdatetime.h	(revision 641810)
+++ kdatetime.h	(working copy)
@@ -1,6 +1,7 @@
 /*
     This file is part of the KDE libraries
     Copyright (c) 2005,2006 David Jarvie <software@astrojar.org.uk>
+    Copyright (c) 2007 Sebastian Trueg <trueg@kde.org>
 
     This library is free software; you can redistribute it and/or
     modify it under the terms of the GNU Library General Public
@@ -48,7 +49,8 @@
  * The class KDateTime combines a date and time with support for an
  * associated time zone or UTC offset. When manipulating KDateTime objects,
  * their time zones or UTC offsets are automatically taken into account. KDateTime
- * can also be set to represent a date-only value with no associated time.
+ * can also be set to represent a date-only value with no associated time or
+ * a time-only value with no associated date.
  *
  * The class uses QDateTime internally to represent date/time values, and
  * therefore can only be used for dates in the Gregorian calendar, with a year
@@ -460,6 +462,9 @@
      */
     enum Comparison
     {
+        Impossible = 0x0,  /**< A proper comparision is not possible. This happens \
if one +                            *   of the instances is time-only but the other \
one is not. +                            */
         Before  = 0x01, /**< This KDateTime is strictly earlier than the other,
                          *   i.e. e1 < s2.
                          */
@@ -524,6 +529,25 @@
     explicit KDateTime(const QDate &date, const Spec &spec = Spec(LocalZone));
 
     /**
+     * Constructs a time-only value expressed in a given time specification. The
+     * date is set to 01.01.0001
+     *
+     * The instance is initialised according to the time specification type of
+     * @p spec as follows:
+     * - @c UTC           : date is stored as UTC.
+     * - @c OffsetFromUTC : date is a local time at the specified offset
+     *                      from UTC.
+     * - @c TimeZone      : date is a local time in the specified time zone.
+     * - @c LocalZone     : date is a local date in the current system time
+     *                      zone.
+     * - @c ClockTime     : time zones are ignored.
+     *
+     * @param time time in the time zone indicated by @p spec
+     * @param spec time specification
+     */
+    explicit KDateTime(const QTime &time, const Spec &spec = Spec(LocalZone));
+
+    /**
      * Constructs a date/time expressed as specified by @p spec.
      *
      * @p date and @p time are interpreted and stored according to the value of
@@ -611,6 +635,13 @@
     bool isDateOnly() const;
 
     /**
+     * Returns if the instance represents a date/time or a time-only value.
+     *
+     * @return @c true if time-only, @c false if date and time
+     */
+    bool isTimeOnly() const;
+
+    /**
      * Returns the date part of the date/time. The value returned should be
      * interpreted in terms of the instance's time zone or UTC offset.
      *
@@ -1167,6 +1198,10 @@
      * - %S   seconds (00 - 59)
      * - %:S  seconds preceded with ':', but omitted if seconds value is zero
      * - %:s  milliseconds, 3 digits (000 - 999)
+     * - %f   fraction of seconds (as used by XML Schema Part 2: Datatypes Second \
Edition:  +     *        http://www.w3.org/TR/xmlschema-2). Example: 500 msecs will \
be formatted as .5. +     *        So the main difference to %:s is that it can be \
less than 3 digits long and +     *        never ends in a 0.
      * - %P   "am" or "pm" in the current locale, or if undefined there, in English
      * - %p   "AM" or "PM" in the current locale, or if undefined there, in English
      * - %:P  "am" or "pm"



[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic