1 /* -*- mode: C++ ; c-file-style: "stroustrup" -*- *****************************
2  * Qwt Widget Library
3  * Copyright (C) 1997   Josef Wilgen
4  * Copyright (C) 2002   Uwe Rathmann
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the Qwt License, Version 1.0
8  *****************************************************************************/
9 
10 #include "qwt_date.h"
11 #include <qdebug.h>
12 #include <qlocale.h>
13 #include <math.h>
14 #include <limits>
15 #include <limits.h>
16 
17 #if QT_VERSION >= 0x050000
18 
19 typedef qint64 QwtJulianDay;
20 static const QwtJulianDay minJulianDayD = Q_INT64_C( -784350574879 );
21 static const QwtJulianDay maxJulianDayD = Q_INT64_C( 784354017364 );
22 
23 #else
24 
25 // QDate stores the Julian day as unsigned int, but
26 // but it is QDate::fromJulianDay( int ). That's why
27 // we have the range [ 1, INT_MAX ]
28 typedef int QwtJulianDay;
29 static const QwtJulianDay minJulianDayD = 1;
30 static const QwtJulianDay maxJulianDayD = std::numeric_limits<int>::max();
31 
32 #endif
33 
qwtExpandedFormat(const QString & format,const QDateTime & dateTime,QwtDate::Week0Type week0Type)34 static QString qwtExpandedFormat( const QString & format,
35     const QDateTime &dateTime, QwtDate::Week0Type week0Type )
36 {
37     const int week = QwtDate::weekNumber( dateTime.date(), week0Type );
38 
39     QString weekNo;
40     weekNo.setNum( week );
41 
42     QString weekNoWW;
43     if ( weekNo.length() == 1 )
44         weekNoWW += "0";
45 
46     weekNoWW += weekNo;
47 
48     QString fmt = format;
49     fmt.replace( "ww", weekNoWW );
50     fmt.replace( "w", weekNo );
51 
52     if ( week == 1 && dateTime.date().month() != 1 )
53     {
54         // in case of week 1, we might need to increment the year
55 
56         static QString s_yyyy = "yyyy";
57         static QString s_yy = "yy";
58 
59         // week 1 might start in the previous year
60 
61         bool doReplaceYear = fmt.contains( s_yy );
62 
63         if ( doReplaceYear )
64         {
65             if ( fmt.contains( 'M' ) )
66             {
67                 // in case of also having 'M' we have a conflict about
68                 // which year to show
69 
70                 doReplaceYear = false;
71             }
72             else
73             {
74                 // in case of also having 'd' or 'dd' we have a conflict about
75                 // which year to show
76 
77                 int numD = 0;
78 
79                 for ( int i = 0; i < fmt.size(); i++ )
80                 {
81                     if ( fmt[i] == 'd' )
82                     {
83                         numD++;
84                     }
85                     else
86                     {
87                         if ( numD > 0 && numD <= 2 )
88                             break;
89 
90                         numD = 0;
91                     }
92                 }
93 
94                 if ( numD > 0 && numD <= 2 )
95                     doReplaceYear = false;
96             }
97         }
98 
99         if ( doReplaceYear )
100         {
101             const QDate dt( dateTime.date().year() + 1, 1, 1 );
102 
103             if ( fmt.contains( s_yyyy ) )
104             {
105                 fmt.replace( s_yyyy, dt.toString( s_yyyy ) );
106             }
107             else
108             {
109                 fmt.replace( s_yy, dt.toString( s_yyyy ) );
110             }
111         }
112     }
113 
114     return fmt;
115 }
116 
qwtFirstDayOfWeek()117 static inline Qt::DayOfWeek qwtFirstDayOfWeek()
118 {
119 #if QT_VERSION >= 0x040800
120     return QLocale().firstDayOfWeek();
121 #else
122 
123     switch( QLocale().country() )
124     {
125         case QLocale::Maldives:
126             return Qt::Friday;
127 
128         case QLocale::Afghanistan:
129         case QLocale::Algeria:
130         case QLocale::Bahrain:
131         case QLocale::Djibouti:
132         case QLocale::Egypt:
133         case QLocale::Eritrea:
134         case QLocale::Ethiopia:
135         case QLocale::Iran:
136         case QLocale::Iraq:
137         case QLocale::Jordan:
138         case QLocale::Kenya:
139         case QLocale::Kuwait:
140         case QLocale::LibyanArabJamahiriya:
141         case QLocale::Morocco:
142         case QLocale::Oman:
143         case QLocale::Qatar:
144         case QLocale::SaudiArabia:
145         case QLocale::Somalia:
146         case QLocale::Sudan:
147         case QLocale::Tunisia:
148         case QLocale::Yemen:
149             return Qt::Saturday;
150 
151         case QLocale::AmericanSamoa:
152         case QLocale::Argentina:
153         case QLocale::Azerbaijan:
154         case QLocale::Botswana:
155         case QLocale::Canada:
156         case QLocale::China:
157         case QLocale::FaroeIslands:
158         case QLocale::Georgia:
159         case QLocale::Greenland:
160         case QLocale::Guam:
161         case QLocale::HongKong:
162         case QLocale::Iceland:
163         case QLocale::India:
164         case QLocale::Ireland:
165         case QLocale::Israel:
166         case QLocale::Jamaica:
167         case QLocale::Japan:
168         case QLocale::Kyrgyzstan:
169         case QLocale::Lao:
170         case QLocale::Malta:
171         case QLocale::MarshallIslands:
172         case QLocale::Macau:
173         case QLocale::Mongolia:
174         case QLocale::NewZealand:
175         case QLocale::NorthernMarianaIslands:
176         case QLocale::Pakistan:
177         case QLocale::Philippines:
178         case QLocale::RepublicOfKorea:
179         case QLocale::Singapore:
180         case QLocale::SyrianArabRepublic:
181         case QLocale::Taiwan:
182         case QLocale::Thailand:
183         case QLocale::TrinidadAndTobago:
184         case QLocale::UnitedStates:
185         case QLocale::UnitedStatesMinorOutlyingIslands:
186         case QLocale::USVirginIslands:
187         case QLocale::Uzbekistan:
188         case QLocale::Zimbabwe:
189             return Qt::Sunday;
190 
191         default:
192             return Qt::Monday;
193     }
194 #endif
195 }
196 
qwtFloorTime(QwtDate::IntervalType intervalType,QDateTime & dt)197 static inline void qwtFloorTime(
198     QwtDate::IntervalType intervalType, QDateTime &dt )
199 {
200     // when dt is inside the special hour where DST is ending
201     // an hour is no unique. Therefore we have to
202     // use UTC time.
203 
204     const Qt::TimeSpec timeSpec = dt.timeSpec();
205 
206     if ( timeSpec == Qt::LocalTime )
207         dt = dt.toTimeSpec( Qt::UTC );
208 
209     const QTime t = dt.time();
210     switch( intervalType )
211     {
212         case QwtDate::Second:
213         {
214             dt.setTime( QTime( t.hour(), t.minute(), t.second() ) );
215             break;
216         }
217         case QwtDate::Minute:
218         {
219             dt.setTime( QTime( t.hour(), t.minute(), 0 ) );
220             break;
221         }
222         case QwtDate::Hour:
223         {
224             dt.setTime( QTime( t.hour(), 0, 0 ) );
225             break;
226         }
227         default:
228             break;
229     }
230 
231     if ( timeSpec == Qt::LocalTime )
232         dt = dt.toTimeSpec( Qt::LocalTime );
233 }
234 
qwtToTimeSpec(const QDateTime & dt,Qt::TimeSpec spec)235 static inline QDateTime qwtToTimeSpec(
236     const QDateTime &dt, Qt::TimeSpec spec )
237 {
238     if ( dt.timeSpec() == spec )
239         return dt;
240 
241     const qint64 jd = dt.date().toJulianDay();
242     if ( jd < 0 || jd >= INT_MAX )
243     {
244         // the conversion between local time and UTC
245         // is internally limited. To avoid
246         // overflows we simply ignore the difference
247         // for those dates
248 
249         QDateTime dt2 = dt;
250         dt2.setTimeSpec( spec );
251         return dt2;
252     }
253 
254     return dt.toTimeSpec( spec );
255 }
256 
257 #if 0
258 
259 static inline double qwtToJulianDay( int year, int month, int day )
260 {
261     // code from QDate but using doubles to avoid overflows
262     // for large values
263 
264     const int m1 = ( month - 14 ) / 12;
265     const int m2 = ( 367 * ( month - 2 - 12 * m1 ) ) / 12;
266     const double y1 = ::floor( ( 4900.0 + year + m1 ) / 100 );
267 
268     return ::floor( ( 1461.0 * ( year + 4800 + m1 ) ) / 4 ) + m2
269             - ::floor( ( 3 * y1 ) / 4 ) + day - 32075;
270 }
271 
272 static inline qint64 qwtFloorDiv64( qint64 a, int b )
273 {
274     if ( a < 0 )
275         a -= b - 1;
276 
277     return a / b;
278 }
279 
280 static inline qint64 qwtFloorDiv( int a, int b )
281 {
282     if ( a < 0 )
283         a -= b - 1;
284 
285     return a / b;
286 }
287 
288 #endif
289 
qwtToDate(int year,int month=1,int day=1)290 static inline QDate qwtToDate( int year, int month = 1, int day = 1 )
291 {
292 #if QT_VERSION >= 0x050000
293     return QDate( year, month, day );
294 #else
295     if ( year > 100000 )
296     {
297         // code from QDate but using doubles to avoid overflows
298         // for large values
299 
300         const int m1 = ( month - 14 ) / 12;
301         const int m2 = ( 367 * ( month - 2 - 12 * m1 ) ) / 12;
302         const double y1 = ::floor( ( 4900.0 + year + m1 ) / 100 );
303 
304         const double jd = ::floor( ( 1461.0 * ( year + 4800 + m1 ) ) / 4 ) + m2
305             - ::floor( ( 3 * y1 ) / 4 ) + day - 32075;
306 
307         if ( jd > maxJulianDayD )
308         {
309             qWarning() << "qwtToDate: overflow";
310             return QDate();
311         }
312 
313         return QDate::fromJulianDay( static_cast<QwtJulianDay>( jd ) );
314     }
315     else
316     {
317         return QDate( year, month, day );
318     }
319 #endif
320 }
321 
322 /*!
323   Translate from double to QDateTime
324 
325   \param value Number of milliseconds since the epoch,
326                1970-01-01T00:00:00 UTC
327   \param timeSpec Time specification
328   \return Datetime value
329 
330   \sa toDouble(), QDateTime::setMSecsSinceEpoch()
331   \note The return datetime for Qt::OffsetFromUTC will be Qt::UTC
332  */
toDateTime(double value,Qt::TimeSpec timeSpec)333 QDateTime QwtDate::toDateTime( double value, Qt::TimeSpec timeSpec )
334 {
335     const int msecsPerDay = 86400000;
336 
337     const double days = static_cast<qint64>( ::floor( value / msecsPerDay ) );
338 
339     const double jd = QwtDate::JulianDayForEpoch + days;
340     if ( ( jd > maxJulianDayD ) || ( jd < minJulianDayD ) )
341     {
342         qWarning() << "QwtDate::toDateTime: overflow";
343         return QDateTime();
344     }
345 
346     const QDate d = QDate::fromJulianDay( static_cast<QwtJulianDay>( jd ) );
347 
348     const int msecs = static_cast<int>( value - days * msecsPerDay );
349 
350     static const QTime timeNull( 0, 0, 0, 0 );
351 
352     QDateTime dt( d, timeNull.addMSecs( msecs ), Qt::UTC );
353 
354     if ( timeSpec == Qt::LocalTime )
355         dt = qwtToTimeSpec( dt, timeSpec );
356 
357     return dt;
358 }
359 
360 /*!
361   Translate from QDateTime to double
362 
363   \param dateTime Datetime value
364   \return Number of milliseconds since 1970-01-01T00:00:00 UTC has passed.
365 
366   \sa toDateTime(), QDateTime::toMSecsSinceEpoch()
367   \warning For values very far below or above 1970-01-01 UTC rounding errors
368            will happen due to the limited significance of a double.
369  */
toDouble(const QDateTime & dateTime)370 double QwtDate::toDouble( const QDateTime &dateTime )
371 {
372     const int msecsPerDay = 86400000;
373 
374     const QDateTime dt = qwtToTimeSpec( dateTime, Qt::UTC );
375 
376     const double days = dt.date().toJulianDay() - QwtDate::JulianDayForEpoch;
377 
378     const QTime time = dt.time();
379     const double secs = 3600.0 * time.hour() +
380         60.0 * time.minute() + time.second();
381 
382     return days * msecsPerDay + time.msec() + 1000.0 * secs;
383 }
384 
385 /*!
386   Ceil a datetime according the interval type
387 
388   \param dateTime Datetime value
389   \param intervalType Interval type, how to ceil.
390                       F.e. when intervalType = QwtDate::Months, the result
391                       will be ceiled to the next beginning of a month
392   \return Ceiled datetime
393   \sa floor()
394  */
ceil(const QDateTime & dateTime,IntervalType intervalType)395 QDateTime QwtDate::ceil( const QDateTime &dateTime, IntervalType intervalType )
396 {
397     if ( dateTime.date() >= QwtDate::maxDate() )
398         return dateTime;
399 
400     QDateTime dt = dateTime;
401 
402     switch ( intervalType )
403     {
404         case QwtDate::Millisecond:
405         {
406             break;
407         }
408         case QwtDate::Second:
409         {
410             qwtFloorTime( QwtDate::Second, dt );
411             if ( dt < dateTime )
412                 dt = dt.addSecs( 1 );
413 
414             break;
415         }
416         case QwtDate::Minute:
417         {
418             qwtFloorTime( QwtDate::Minute, dt );
419             if ( dt < dateTime )
420                 dt = dt.addSecs( 60 );
421 
422             break;
423         }
424         case QwtDate::Hour:
425         {
426             qwtFloorTime( QwtDate::Hour, dt );
427             if ( dt < dateTime )
428                 dt = dt.addSecs( 3600 );
429 
430             break;
431         }
432         case QwtDate::Day:
433         {
434             dt.setTime( QTime( 0, 0 ) );
435             if ( dt < dateTime )
436                 dt = dt.addDays( 1 );
437 
438             break;
439         }
440         case QwtDate::Week:
441         {
442             dt.setTime( QTime( 0, 0 ) );
443             if ( dt < dateTime )
444                 dt = dt.addDays( 1 );
445 
446             int days = qwtFirstDayOfWeek() - dt.date().dayOfWeek();
447             if ( days < 0 )
448                 days += 7;
449 
450             dt = dt.addDays( days );
451 
452             break;
453         }
454         case QwtDate::Month:
455         {
456             dt.setTime( QTime( 0, 0 ) );
457             dt.setDate( qwtToDate( dateTime.date().year(),
458                 dateTime.date().month() ) );
459 
460             if ( dt < dateTime )
461                 dt = dt.addMonths( 1 );
462 
463             break;
464         }
465         case QwtDate::Year:
466         {
467             dt.setTime( QTime( 0, 0 ) );
468 
469             const QDate d = dateTime.date();
470 
471             int year = d.year();
472             if ( d.month() > 1 || d.day() > 1 || !dateTime.time().isNull() )
473                 year++;
474 
475             if ( year == 0 )
476                 year++; // there is no year 0
477 
478             dt.setDate( qwtToDate( year ) );
479             break;
480         }
481     }
482 
483     return dt;
484 }
485 
486 /*!
487   Floor a datetime according the interval type
488 
489   \param dateTime Datetime value
490   \param intervalType Interval type, how to ceil.
491                       F.e. when intervalType = QwtDate::Months,
492                       the result will be ceiled to the next
493                       beginning of a month
494   \return Floored datetime
495   \sa floor()
496  */
floor(const QDateTime & dateTime,IntervalType intervalType)497 QDateTime QwtDate::floor( const QDateTime &dateTime,
498     IntervalType intervalType )
499 {
500     if ( dateTime.date() <= QwtDate::minDate() )
501         return dateTime;
502 
503     QDateTime dt = dateTime;
504 
505     switch ( intervalType )
506     {
507         case QwtDate::Millisecond:
508         {
509             break;
510         }
511         case QwtDate::Second:
512         case QwtDate::Minute:
513         case QwtDate::Hour:
514         {
515             qwtFloorTime( intervalType, dt );
516             break;
517         }
518         case QwtDate::Day:
519         {
520             dt.setTime( QTime( 0, 0 ) );
521             break;
522         }
523         case QwtDate::Week:
524         {
525             dt.setTime( QTime( 0, 0 ) );
526 
527             int days = dt.date().dayOfWeek() - qwtFirstDayOfWeek();
528             if ( days < 0 )
529                 days += 7;
530 
531             dt = dt.addDays( -days );
532 
533             break;
534         }
535         case QwtDate::Month:
536         {
537             dt.setTime( QTime( 0, 0 ) );
538 
539             const QDate date = qwtToDate( dt.date().year(),
540                 dt.date().month() );
541             dt.setDate( date );
542 
543             break;
544         }
545         case QwtDate::Year:
546         {
547             dt.setTime( QTime( 0, 0 ) );
548 
549             const QDate date = qwtToDate( dt.date().year() );
550             dt.setDate( date );
551 
552             break;
553         }
554     }
555 
556     return dt;
557 }
558 
559 /*!
560   Minimum for the supported date range
561 
562   The range of valid dates depends on how QDate stores the
563   Julian day internally.
564 
565   - For Qt4 it is "Tue Jan 2 -4713"
566   - For Qt5 it is "Thu Jan 1 -2147483648"
567 
568   \return minimum of the date range
569   \sa maxDate()
570  */
minDate()571 QDate QwtDate::minDate()
572 {
573     static QDate date;
574     if ( !date.isValid() )
575         date = QDate::fromJulianDay( minJulianDayD );
576 
577     return date;
578 }
579 
580 /*!
581   Maximum for the supported date range
582 
583   The range of valid dates depends on how QDate stores the
584   Julian day internally.
585 
586   - For Qt4 it is "Tue Jun 3 5874898"
587   - For Qt5 it is "Tue Dec 31 2147483647"
588 
589   \return maximum of the date range
590   \sa minDate()
591   \note The maximum differs between Qt4 and Qt5
592  */
maxDate()593 QDate QwtDate::maxDate()
594 {
595     static QDate date;
596     if ( !date.isValid() )
597         date = QDate::fromJulianDay( maxJulianDayD );
598 
599     return date;
600 }
601 
602 /*!
603   \brief Date of the first day of the first week for a year
604 
605   The first day of a week depends on the current locale
606   ( QLocale::firstDayOfWeek() ).
607 
608   \param year Year
609   \param type Option how to identify the first week
610   \return First day of week 0
611 
612   \sa QLocale::firstDayOfWeek(), weekNumber()
613  */
dateOfWeek0(int year,Week0Type type)614 QDate QwtDate::dateOfWeek0( int year, Week0Type type )
615 {
616     const Qt::DayOfWeek firstDayOfWeek = qwtFirstDayOfWeek();
617 
618     QDate dt0( year, 1, 1 );
619 
620     // floor to the first day of the week
621     int days = dt0.dayOfWeek() - firstDayOfWeek;
622     if ( days < 0 )
623         days += 7;
624 
625     dt0 = dt0.addDays( -days );
626 
627     if ( type == QwtDate::FirstThursday )
628     {
629         // according to ISO 8601 the first week is defined
630         // by the first thursday.
631 
632         int d = Qt::Thursday - firstDayOfWeek;
633         if ( d < 0 )
634             d += 7;
635 
636         if ( dt0.addDays( d ).year() < year )
637             dt0 = dt0.addDays( 7 );
638     }
639 
640     return dt0;
641 }
642 
643 /*!
644   Find the week number of a date
645 
646   - QwtDate::FirstThursday\n
647     Corresponding to ISO 8601 ( see QDate::weekNumber() ).
648 
649   - QwtDate::FirstDay\n
650     Number of weeks that have begun since dateOfWeek0().
651 
652   \param date Date
653   \param type Option how to identify the first week
654 
655   \return Week number, starting with 1
656  */
weekNumber(const QDate & date,Week0Type type)657 int QwtDate::weekNumber( const QDate &date, Week0Type type )
658 {
659     int weekNo;
660 
661     if ( type == QwtDate::FirstDay )
662     {
663         QDate day0;
664 
665         if ( date.month() == 12 && date.day() >= 24 )
666         {
667             // week 1 usually starts in the previous years.
668             // and we have to check if we are already there
669 
670             day0 = dateOfWeek0( date.year() + 1, type );
671             if ( day0.daysTo( date ) < 0 )
672                 day0 = dateOfWeek0( date.year(), type );
673         }
674         else
675         {
676             day0 = dateOfWeek0( date.year(), type );
677         }
678 
679         weekNo = day0.daysTo( date ) / 7 + 1;
680     }
681     else
682     {
683         weekNo = date.weekNumber();
684     }
685 
686     return weekNo;
687 }
688 
689 /*!
690    Offset in seconds from Coordinated Universal Time
691 
692    The offset depends on the time specification of dateTime:
693 
694    - Qt::UTC
695      0, dateTime has no offset
696    - Qt::OffsetFromUTC
697      returns dateTime.utcOffset()
698    - Qt::LocalTime:
699      number of seconds from the UTC
700 
701    For Qt::LocalTime the offset depends on the timezone and
702    daylight savings.
703 
704    \param dateTime Datetime value
705    \return Offset in seconds
706  */
utcOffset(const QDateTime & dateTime)707 int QwtDate::utcOffset( const QDateTime &dateTime )
708 {
709     int seconds = 0;
710 
711     switch( dateTime.timeSpec() )
712     {
713         case Qt::UTC:
714         {
715             break;
716         }
717         case Qt::OffsetFromUTC:
718         {
719             seconds = dateTime.utcOffset();
720             break;
721         }
722         default:
723         {
724             const QDateTime dt1( dateTime.date(), dateTime.time(), Qt::UTC );
725             seconds = dateTime.secsTo( dt1 );
726         }
727     }
728 
729     return seconds;
730 }
731 
732 /*!
733   Translate a datetime into a string
734 
735   Beside the format expressions documented in QDateTime::toString()
736   the following expressions are supported:
737 
738   - w\n
739     week number: ( 1 - 53 )
740   - ww\n
741     week number with a leading zero ( 01 - 53 )
742 
743   As week 1 usually starts in the previous year a special rule
744   is applied for formats, where the year is expected to match the
745   week number - even if the date belongs to the previous year.
746 
747   \param dateTime Datetime value
748   \param format Format string
749   \param week0Type Specification of week 0
750 
751   \return Datetime string
752   \sa QDateTime::toString(), weekNumber(), QwtDateScaleDraw
753  */
toString(const QDateTime & dateTime,const QString & format,Week0Type week0Type)754 QString QwtDate::toString( const QDateTime &dateTime,
755     const QString & format, Week0Type week0Type )
756 {
757     QString fmt = format;
758     if ( fmt.contains( 'w' ) )
759     {
760         fmt = qwtExpandedFormat( fmt, dateTime, week0Type );
761     }
762 
763     return dateTime.toString( fmt );
764 }
765