/****************************************************************************** * Qwt Widget Library * Copyright (C) 1997 Josef Wilgen * Copyright (C) 2002 Uwe Rathmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the Qwt License, Version 1.0 *****************************************************************************/ #include "qwt_date.h" #include "qwt_math.h" #include #include #include #if QT_VERSION >= 0x050000 typedef qint64 QwtJulianDay; static const QwtJulianDay minJulianDayD = Q_INT64_C( -784350574879 ); static const QwtJulianDay maxJulianDayD = Q_INT64_C( 784354017364 ); #else // QDate stores the Julian day as unsigned int, but // there is QDate::fromJulianDay( int ). That's why // we have the range [ 1, INT_MAX ] typedef int QwtJulianDay; static const QwtJulianDay minJulianDayD = 1; static const QwtJulianDay maxJulianDayD = std::numeric_limits< int >::max(); #endif static QString qwtExpandedFormat( const QString& format, const QDateTime& dateTime, QwtDate::Week0Type week0Type ) { const int week = QwtDate::weekNumber( dateTime.date(), week0Type ); QString weekNo; weekNo.setNum( week ); QString weekNoWW; if ( weekNo.length() == 1 ) weekNoWW += QLatin1Char( '0' ); weekNoWW += weekNo; QString fmt = format; fmt.replace( QLatin1String( "ww" ), weekNoWW ); fmt.replace( QLatin1Char( 'w' ), weekNo ); if ( week == 1 && dateTime.date().month() != 1 ) { // in case of week 1, we might need to increment the year QLatin1String s_yyyy( "yyyy" ); QLatin1String s_yy( "yy" ); // week 1 might start in the previous year bool doReplaceYear = fmt.contains( s_yy ); if ( doReplaceYear ) { if ( fmt.contains( 'M' ) ) { // in case of also having 'M' we have a conflict about // which year to show doReplaceYear = false; } else { // in case of also having 'd' or 'dd' we have a conflict about // which year to show int numD = 0; for ( int i = 0; i < fmt.size(); i++ ) { if ( fmt[i] == 'd' ) { numD++; } else { if ( numD > 0 && numD <= 2 ) break; numD = 0; } } if ( numD > 0 && numD <= 2 ) doReplaceYear = false; } } if ( doReplaceYear ) { const QDate dt( dateTime.date().year() + 1, 1, 1 ); const QString dtString = QLocale().toString( dt, s_yyyy ); if ( fmt.contains( s_yyyy ) ) { fmt.replace( s_yyyy, dtString ); } else { fmt.replace( s_yy, dtString ); } } } return fmt; } static inline Qt::DayOfWeek qwtFirstDayOfWeek() { return QLocale().firstDayOfWeek(); } static inline void qwtFloorTime( QwtDate::IntervalType intervalType, QDateTime& dt ) { // when dt is inside the special hour where DST is ending // an hour is no unique. Therefore we have to // use UTC time. const Qt::TimeSpec timeSpec = dt.timeSpec(); if ( timeSpec == Qt::LocalTime ) dt = dt.toTimeSpec( Qt::UTC ); const QTime t = dt.time(); switch( intervalType ) { case QwtDate::Second: { dt.setTime( QTime( t.hour(), t.minute(), t.second() ) ); break; } case QwtDate::Minute: { dt.setTime( QTime( t.hour(), t.minute(), 0 ) ); break; } case QwtDate::Hour: { dt.setTime( QTime( t.hour(), 0, 0 ) ); break; } default: break; } if ( timeSpec == Qt::LocalTime ) dt = dt.toTimeSpec( Qt::LocalTime ); } static inline QDateTime qwtToTimeSpec( const QDateTime& dt, Qt::TimeSpec spec ) { if ( dt.timeSpec() == spec ) return dt; const qint64 jd = dt.date().toJulianDay(); if ( jd < 0 || jd >= std::numeric_limits< int >::max() ) { // the conversion between local time and UTC // is internally limited. To avoid // overflows we simply ignore the difference // for those dates QDateTime dt2 = dt; dt2.setTimeSpec( spec ); return dt2; } return dt.toTimeSpec( spec ); } #if 0 static inline double qwtToJulianDay( int year, int month, int day ) { // code from QDate but using doubles to avoid overflows // for large values const int m1 = ( month - 14 ) / 12; const int m2 = ( 367 * ( month - 2 - 12 * m1 ) ) / 12; const double y1 = std::floor( ( 4900.0 + year + m1 ) / 100 ); return std::floor( ( 1461.0 * ( year + 4800 + m1 ) ) / 4 ) + m2 - std::floor( ( 3 * y1 ) / 4 ) + day - 32075; } static inline qint64 qwtFloorDiv64( qint64 a, int b ) { if ( a < 0 ) a -= b - 1; return a / b; } static inline qint64 qwtFloorDiv( int a, int b ) { if ( a < 0 ) a -= b - 1; return a / b; } #endif static inline QDate qwtToDate( int year, int month = 1, int day = 1 ) { #if QT_VERSION >= 0x050000 return QDate( year, month, day ); #else if ( year > 100000 ) { // code from QDate but using doubles to avoid overflows // for large values const int m1 = ( month - 14 ) / 12; const int m2 = ( 367 * ( month - 2 - 12 * m1 ) ) / 12; const double y1 = std::floor( ( 4900.0 + year + m1 ) / 100 ); const double jd = std::floor( ( 1461.0 * ( year + 4800 + m1 ) ) / 4 ) + m2 - std::floor( ( 3 * y1 ) / 4 ) + day - 32075; if ( jd > maxJulianDayD ) { qWarning() << "qwtToDate: overflow"; return QDate(); } return QDate::fromJulianDay( static_cast< QwtJulianDay >( jd ) ); } else { return QDate( year, month, day ); } #endif } /*! Translate from double to QDateTime \param value Number of milliseconds since the epoch, 1970-01-01T00:00:00 UTC \param timeSpec Time specification \return Datetime value \sa toDouble(), QDateTime::setMSecsSinceEpoch() \note The return datetime for Qt::OffsetFromUTC will be Qt::UTC */ QDateTime QwtDate::toDateTime( double value, Qt::TimeSpec timeSpec ) { const int msecsPerDay = 86400000; const double days = static_cast< qint64 >( std::floor( value / msecsPerDay ) ); const double jd = static_cast< qint64 >( QwtDate::JulianDayForEpoch ) + days; if ( ( jd > maxJulianDayD ) || ( jd < minJulianDayD ) ) { qWarning() << "QwtDate::toDateTime: overflow"; return QDateTime(); } const QDate d = QDate::fromJulianDay( static_cast< QwtJulianDay >( jd ) ); const int msecs = static_cast< int >( value - days * msecsPerDay ); static const QTime timeNull( 0, 0, 0, 0 ); QDateTime dt( d, timeNull.addMSecs( msecs ), Qt::UTC ); if ( timeSpec == Qt::LocalTime ) dt = qwtToTimeSpec( dt, timeSpec ); return dt; } /*! Translate from QDateTime to double \param dateTime Datetime value \return Number of milliseconds since 1970-01-01T00:00:00 UTC has passed. \sa toDateTime(), QDateTime::toMSecsSinceEpoch() \warning For values very far below or above 1970-01-01 UTC rounding errors will happen due to the limited significance of a double. */ double QwtDate::toDouble( const QDateTime& dateTime ) { const int msecsPerDay = 86400000; const QDateTime dt = qwtToTimeSpec( dateTime, Qt::UTC ); const double days = dt.date().toJulianDay() - QwtDate::JulianDayForEpoch; const QTime time = dt.time(); const double secs = 3600.0 * time.hour() + 60.0 * time.minute() + time.second(); return days * msecsPerDay + time.msec() + 1000.0 * secs; } /*! Ceil a datetime according the interval type \param dateTime Datetime value \param intervalType Interval type, how to ceil. F.e. when intervalType = QwtDate::Months, the result will be ceiled to the next beginning of a month \return Ceiled datetime \sa floor() */ QDateTime QwtDate::ceil( const QDateTime& dateTime, IntervalType intervalType ) { if ( dateTime.date() >= QwtDate::maxDate() ) return dateTime; QDateTime dt = dateTime; switch ( intervalType ) { case QwtDate::Millisecond: { break; } case QwtDate::Second: { qwtFloorTime( QwtDate::Second, dt ); if ( dt < dateTime ) dt = dt.addSecs( 1 ); break; } case QwtDate::Minute: { qwtFloorTime( QwtDate::Minute, dt ); if ( dt < dateTime ) dt = dt.addSecs( 60 ); break; } case QwtDate::Hour: { qwtFloorTime( QwtDate::Hour, dt ); if ( dt < dateTime ) dt = dt.addSecs( 3600 ); break; } case QwtDate::Day: { dt.setTime( QTime( 0, 0 ) ); if ( dt < dateTime ) dt = dt.addDays( 1 ); break; } case QwtDate::Week: { dt.setTime( QTime( 0, 0 ) ); if ( dt < dateTime ) dt = dt.addDays( 1 ); int days = qwtFirstDayOfWeek() - dt.date().dayOfWeek(); if ( days < 0 ) days += 7; dt = dt.addDays( days ); break; } case QwtDate::Month: { dt.setTime( QTime( 0, 0 ) ); dt.setDate( qwtToDate( dateTime.date().year(), dateTime.date().month() ) ); if ( dt < dateTime ) dt = dt.addMonths( 1 ); break; } case QwtDate::Year: { dt.setTime( QTime( 0, 0 ) ); const QDate d = dateTime.date(); int year = d.year(); if ( d.month() > 1 || d.day() > 1 || !dateTime.time().isNull() ) year++; if ( year == 0 ) year++; // there is no year 0 dt.setDate( qwtToDate( year ) ); break; } } return dt; } /*! Floor a datetime according the interval type \param dateTime Datetime value \param intervalType Interval type, how to ceil. F.e. when intervalType = QwtDate::Months, the result will be ceiled to the next beginning of a month \return Floored datetime \sa floor() */ QDateTime QwtDate::floor( const QDateTime& dateTime, IntervalType intervalType ) { if ( dateTime.date() <= QwtDate::minDate() ) return dateTime; QDateTime dt = dateTime; switch ( intervalType ) { case QwtDate::Millisecond: { break; } case QwtDate::Second: case QwtDate::Minute: case QwtDate::Hour: { qwtFloorTime( intervalType, dt ); break; } case QwtDate::Day: { dt.setTime( QTime( 0, 0 ) ); break; } case QwtDate::Week: { dt.setTime( QTime( 0, 0 ) ); int days = dt.date().dayOfWeek() - qwtFirstDayOfWeek(); if ( days < 0 ) days += 7; dt = dt.addDays( -days ); break; } case QwtDate::Month: { dt.setTime( QTime( 0, 0 ) ); const QDate date = qwtToDate( dt.date().year(), dt.date().month() ); dt.setDate( date ); break; } case QwtDate::Year: { dt.setTime( QTime( 0, 0 ) ); const QDate date = qwtToDate( dt.date().year() ); dt.setDate( date ); break; } } return dt; } /*! Minimum for the supported date range The range of valid dates depends on how QDate stores the Julian day internally. - For Qt4 it is "Tue Jan 2 -4713" - For Qt5 it is "Thu Jan 1 -2147483648" \return minimum of the date range \sa maxDate() */ QDate QwtDate::minDate() { static QDate date; if ( !date.isValid() ) date = QDate::fromJulianDay( minJulianDayD ); return date; } /*! Maximum for the supported date range The range of valid dates depends on how QDate stores the Julian day internally. - For Qt4 it is "Tue Jun 3 5874898" - For Qt5 it is "Tue Dec 31 2147483647" \return maximum of the date range \sa minDate() \note The maximum differs between Qt4 and Qt5 */ QDate QwtDate::maxDate() { static QDate date; if ( !date.isValid() ) date = QDate::fromJulianDay( maxJulianDayD ); return date; } /*! \brief Date of the first day of the first week for a year The first day of a week depends on the current locale ( QLocale::firstDayOfWeek() ). \param year Year \param type Option how to identify the first week \return First day of week 0 \sa QLocale::firstDayOfWeek(), weekNumber() */ QDate QwtDate::dateOfWeek0( int year, Week0Type type ) { const Qt::DayOfWeek firstDayOfWeek = qwtFirstDayOfWeek(); QDate dt0( year, 1, 1 ); // floor to the first day of the week int days = dt0.dayOfWeek() - firstDayOfWeek; if ( days < 0 ) days += 7; dt0 = dt0.addDays( -days ); if ( type == QwtDate::FirstThursday ) { // according to ISO 8601 the first week is defined // by the first Thursday. int d = Qt::Thursday - firstDayOfWeek; if ( d < 0 ) d += 7; if ( dt0.addDays( d ).year() < year ) dt0 = dt0.addDays( 7 ); } return dt0; } /*! Find the week number of a date - QwtDate::FirstThursday\n Corresponding to ISO 8601 ( see QDate::weekNumber() ). - QwtDate::FirstDay\n Number of weeks that have begun since dateOfWeek0(). \param date Date \param type Option how to identify the first week \return Week number, starting with 1 */ int QwtDate::weekNumber( const QDate& date, Week0Type type ) { int weekNo; if ( type == QwtDate::FirstDay ) { QDate day0; if ( date.month() == 12 && date.day() >= 24 ) { // week 1 usually starts in the previous years. // and we have to check if we are already there day0 = dateOfWeek0( date.year() + 1, type ); if ( day0.daysTo( date ) < 0 ) day0 = dateOfWeek0( date.year(), type ); } else { day0 = dateOfWeek0( date.year(), type ); } weekNo = day0.daysTo( date ) / 7 + 1; } else { weekNo = date.weekNumber(); } return weekNo; } /*! Offset in seconds from Coordinated Universal Time The offset depends on the time specification of dateTime: - Qt::UTC 0, dateTime has no offset - Qt::OffsetFromUTC returns dateTime.offsetFromUtc() - Qt::LocalTime: number of seconds from the UTC For Qt::LocalTime the offset depends on the timezone and daylight savings. \param dateTime Datetime value \return Offset in seconds */ int QwtDate::utcOffset( const QDateTime& dateTime ) { int seconds = 0; switch( dateTime.timeSpec() ) { case Qt::UTC: { break; } case Qt::OffsetFromUTC: { #if QT_VERSION >= 0x050200 seconds = dateTime.offsetFromUtc(); #else seconds = dateTime.utcOffset(); #endif break; } default: { const QDateTime dt1( dateTime.date(), dateTime.time(), Qt::UTC ); seconds = dateTime.secsTo( dt1 ); } } return seconds; } /*! Translate a datetime into a string Beside the format expressions documented in QDateTime::toString() the following expressions are supported: - w\n week number: ( 1 - 53 ) - ww\n week number with a leading zero ( 01 - 53 ) As week 1 usually starts in the previous year a special rule is applied for formats, where the year is expected to match the week number - even if the date belongs to the previous year. \param dateTime Datetime value \param format Format string \param week0Type Specification of week 0 \return Datetime string \sa QDateTime::toString(), weekNumber(), QwtDateScaleDraw */ QString QwtDate::toString( const QDateTime& dateTime, const QString& format, Week0Type week0Type ) { QString fmt = format; if ( fmt.contains( 'w' ) ) { fmt = qwtExpandedFormat( fmt, dateTime, week0Type ); } return QLocale().toString( dateTime, fmt ); }