File indexing completed on 2024-04-14 14:11:32

0001 /*
0002     SPDX-FileCopyrightText: 2002 Jason Harris <kstars@30doradus.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "timezonerule.h"
0008 #include "kstars_debug.h"
0009 
0010 #include <KLocalizedString>
0011 
0012 #include <QString>
0013 
0014 TimeZoneRule::TimeZoneRule()
0015 {
0016     setEmpty();
0017 }
0018 
0019 TimeZoneRule::TimeZoneRule(const QString &smonth, const QString &sday, const QTime &stime, const QString &rmonth,
0020                            const QString &rday, const QTime &rtime, const double &dh)
0021 {
0022     dTZ = 0.0;
0023     if (smonth != "0")
0024     {
0025         StartMonth  = initMonth(smonth);
0026         RevertMonth = initMonth(rmonth);
0027 
0028         if (StartMonth && RevertMonth && initDay(sday, StartDay, StartWeek) && initDay(rday, RevertDay, RevertWeek) &&
0029             stime.isValid() && rtime.isValid())
0030         {
0031             StartTime  = stime;
0032             RevertTime = rtime;
0033             HourOffset = dh;
0034         }
0035         else
0036         {
0037             qCWarning(KSTARS) << i18n("Error parsing TimeZoneRule, setting to empty rule.");
0038             setEmpty();
0039         }
0040     }
0041     else //Empty rule
0042     {
0043         setEmpty();
0044     }
0045 }
0046 
0047 void TimeZoneRule::setEmpty()
0048 {
0049     StartMonth  = 0;
0050     RevertMonth = 0;
0051     StartDay    = 0;
0052     RevertDay   = 0;
0053     StartWeek   = -1;
0054     RevertWeek  = -1;
0055     StartTime   = QTime();
0056     RevertTime  = QTime();
0057     HourOffset  = 0.0;
0058     dTZ         = 0.0;
0059 }
0060 
0061 void TimeZoneRule::setDST(bool activate)
0062 {
0063     if (activate)
0064     {
0065         qCDebug(KSTARS) << "Daylight Saving Time active";
0066         dTZ = HourOffset;
0067     }
0068     else
0069     {
0070         qCDebug(KSTARS) << "Daylight Saving Time inactive";
0071         dTZ = 0.0;
0072     }
0073 }
0074 
0075 int TimeZoneRule::initMonth(const QString &mn)
0076 {
0077     //Check whether the argument is a three-letter English month code.
0078     QString ml = mn.toLower();
0079     if (ml == "jan")
0080         return 1;
0081     else if (ml == "feb")
0082         return 2;
0083     else if (ml == "mar")
0084         return 3;
0085     else if (ml == "apr")
0086         return 4;
0087     else if (ml == "may")
0088         return 5;
0089     else if (ml == "jun")
0090         return 6;
0091     else if (ml == "jul")
0092         return 7;
0093     else if (ml == "aug")
0094         return 8;
0095     else if (ml == "sep")
0096         return 9;
0097     else if (ml == "oct")
0098         return 10;
0099     else if (ml == "nov")
0100         return 11;
0101     else if (ml == "dec")
0102         return 12;
0103 
0104     qCWarning(KSTARS) << i18n("Could not parse %1 as a valid month code.", mn);
0105     return 0;
0106 }
0107 
0108 bool TimeZoneRule::initDay(const QString &dy, int &Day, int &Week)
0109 {
0110     //Three possible ways to express a day.
0111     //1. simple integer; the calendar date...set Week=0 to indicate that Date is not the day of the week
0112     bool ok;
0113     int day = dy.toInt(&ok);
0114     if (ok)
0115     {
0116         Day  = day;
0117         Week = 0;
0118         return true;
0119     }
0120 
0121     QString dl = dy.toLower();
0122     //2. 3-letter day of week string, indicating the last of that day of the month
0123     //   ...set Week to 5 to indicate the last weekday of the month
0124     if (dl == "mon")
0125     {
0126         Day  = 1;
0127         Week = 5;
0128         return true;
0129     }
0130     else if (dl == "tue")
0131     {
0132         Day  = 2;
0133         Week = 5;
0134         return true;
0135     }
0136     else if (dl == "wed")
0137     {
0138         Day  = 3;
0139         Week = 5;
0140         return true;
0141     }
0142     else if (dl == "thu")
0143     {
0144         Day  = 4;
0145         Week = 5;
0146         return true;
0147     }
0148     else if (dl == "fri")
0149     {
0150         Day  = 5;
0151         Week = 5;
0152         return true;
0153     }
0154     else if (dl == "sat")
0155     {
0156         Day  = 6;
0157         Week = 5;
0158         return true;
0159     }
0160     else if (dl == "sun")
0161     {
0162         Day  = 7;
0163         Week = 5;
0164         return true;
0165     }
0166 
0167     //3. 1,2 or 3 followed by 3-letter day of week string; this indicates
0168     //   the (1st/2nd/3rd) weekday of the month.
0169     int wn = dl.leftRef(1).toInt();
0170     if (wn > 0 && wn < 4)
0171     {
0172         QString dm = dl.mid(1, dl.length()).toLower();
0173         if (dm == "mon")
0174         {
0175             Day  = 1;
0176             Week = wn;
0177             return true;
0178         }
0179         else if (dm == "tue")
0180         {
0181             Day  = 2;
0182             Week = wn;
0183             return true;
0184         }
0185         else if (dm == "wed")
0186         {
0187             Day  = 3;
0188             Week = wn;
0189             return true;
0190         }
0191         else if (dm == "thu")
0192         {
0193             Day  = 4;
0194             Week = wn;
0195             return true;
0196         }
0197         else if (dm == "fri")
0198         {
0199             Day  = 5;
0200             Week = wn;
0201             return true;
0202         }
0203         else if (dm == "sat")
0204         {
0205             Day  = 6;
0206             Week = wn;
0207             return true;
0208         }
0209         else if (dm == "sun")
0210         {
0211             Day  = 7;
0212             Week = wn;
0213             return true;
0214         }
0215     }
0216 
0217     qCWarning(KSTARS) << i18n("Could not parse %1 as a valid day code.", dy);
0218     return false;
0219 }
0220 
0221 int TimeZoneRule::findStartDay(const KStarsDateTime &d)
0222 {
0223     // Determine the calendar date of StartDay for the month and year of the given date.
0224     QDate test;
0225 
0226     // TimeZoneRule is empty, return -1
0227     if (isEmptyRule())
0228         return -1;
0229 
0230     // If StartWeek=0, just return the integer.
0231     if (StartWeek == 0)
0232         return StartDay;
0233 
0234     // Since StartWeek was not zero, StartDay is the day of the week, not the calendar date
0235     else if (StartWeek == 5) // count back from end of month until StartDay is found.
0236     {
0237         for (test = QDate(d.date().year(), d.date().month(), d.date().daysInMonth()); test.day() > 21;
0238              test = test.addDays(-1))
0239             if (test.dayOfWeek() == StartDay)
0240                 break;
0241     }
0242     else // Count forward from day 1, 8 or 15 (depending on StartWeek) until correct day of week is found
0243     {
0244         for (test = QDate(d.date().year(), d.date().month(), (StartWeek - 1) * 7 + 1); test.day() < 7 * StartWeek;
0245              test = test.addDays(1))
0246             if (test.dayOfWeek() == StartDay)
0247                 break;
0248     }
0249     return test.day();
0250 }
0251 
0252 int TimeZoneRule::findRevertDay(const KStarsDateTime &d)
0253 {
0254     // Determine the calendar date of RevertDay for the month and year of the given date.
0255     QDate test;
0256 
0257     // TimeZoneRule is empty, return -1
0258     if (isEmptyRule())
0259         return -1;
0260 
0261     // If RevertWeek=0, just return the integer.
0262     if (RevertWeek == 0)
0263         return RevertDay;
0264 
0265     // Since RevertWeek was not zero, RevertDay is the day of the week, not the calendar date
0266     else if (RevertWeek == 5) //count back from end of month until RevertDay is found.
0267     {
0268         for (test = QDate(d.date().year(), d.date().month(), d.date().daysInMonth()); test.day() > 21;
0269              test = test.addDays(-1))
0270             if (test.dayOfWeek() == RevertDay)
0271                 break;
0272     }
0273     else //Count forward from day 1, 8 or 15 (depending on RevertWeek) until correct day of week is found
0274     {
0275         for (test = QDate(d.date().year(), d.date().month(), (RevertWeek - 1) * 7 + 1); test.day() < 7 * RevertWeek;
0276              test = test.addDays(1))
0277             if (test.dayOfWeek() == StartDay)
0278                 break;
0279     }
0280     return test.day();
0281 }
0282 
0283 bool TimeZoneRule::isDSTActive(const KStarsDateTime &date)
0284 {
0285     // The empty rule always returns false
0286     if (isEmptyRule())
0287         return false;
0288 
0289     // First, check whether the month is outside the DST interval.  Note that
0290     // the interval check is different if StartMonth > RevertMonth (indicating that
0291     // the DST interval includes the end of the year).
0292     int month = date.date().month();
0293 
0294     if (StartMonth < RevertMonth)
0295     {
0296         if (month < StartMonth || month > RevertMonth)
0297             return false;
0298     }
0299     else
0300     {
0301         if (month < StartMonth && month > RevertMonth)
0302             return false;
0303     }
0304 
0305     // OK, if the month is equal to StartMonth or Revert Month, we have more
0306     // detailed checking to do...
0307     int day = date.date().day();
0308 
0309     if (month == StartMonth)
0310     {
0311         int sday = findStartDay(date);
0312         if (day < sday)
0313             return false;
0314         if (day == sday && date.time() < StartTime)
0315             return false;
0316     }
0317     else if (month == RevertMonth)
0318     {
0319         int rday = findRevertDay(date);
0320         if (day > rday)
0321             return false;
0322         if (day == rday && date.time() > RevertTime)
0323             return false;
0324     }
0325 
0326     // passed all tests, so we must be in DST.
0327     return true;
0328 }
0329 
0330 void TimeZoneRule::nextDSTChange_LTime(const KStarsDateTime &date)
0331 {
0332     KStarsDateTime result;
0333 
0334     // return an invalid date if the rule is the empty rule.
0335     if (isEmptyRule())
0336         result = KStarsDateTime(QDateTime());
0337 
0338     else if (deltaTZ())
0339     {
0340         // Next change is reverting back to standard time.
0341 
0342         //y is the year for the next DST Revert date.  It's either the current year, or
0343         //the next year if the current month is already past RevertMonth
0344         int y = date.date().year();
0345         if (RevertMonth < date.date().month())
0346             ++y;
0347 
0348         result = KStarsDateTime(QDate(y, RevertMonth, 1), RevertTime);
0349         result = KStarsDateTime(QDate(y, RevertMonth, findRevertDay(result)), RevertTime);
0350     }
0351     else
0352     {
0353         // Next change is starting DST.
0354 
0355         //y is the year for the next DST Start date.  It's either the current year, or
0356         //the next year if the current month is already past StartMonth
0357         int y = date.date().year();
0358         if (StartMonth < date.date().month())
0359             ++y;
0360 
0361         result = KStarsDateTime(QDate(y, StartMonth, 1), StartTime);
0362         result = KStarsDateTime(QDate(y, StartMonth, findStartDay(result)), StartTime);
0363     }
0364 
0365     qCDebug(KSTARS) << "Next Daylight Savings Time change (Local Time): " << result.toString();
0366     next_change_ltime = result;
0367 }
0368 
0369 void TimeZoneRule::previousDSTChange_LTime(const KStarsDateTime &date)
0370 {
0371     KStarsDateTime result;
0372 
0373     // return an invalid date if the rule is the empty rule
0374     if (isEmptyRule())
0375         next_change_ltime = KStarsDateTime(QDateTime());
0376 
0377     if (deltaTZ())
0378     {
0379         // Last change was starting DST.
0380 
0381         //y is the year for the previous DST Start date.  It's either the current year, or
0382         //the previous year if the current month is earlier than StartMonth
0383         int y = date.date().year();
0384         if (StartMonth > date.date().month())
0385             --y;
0386 
0387         result = KStarsDateTime(QDate(y, StartMonth, 1), StartTime);
0388         result = KStarsDateTime(QDate(y, StartMonth, findStartDay(result)), StartTime);
0389     }
0390     else if (StartMonth)
0391     {
0392         //Last change was reverting to standard time.
0393 
0394         //y is the year for the previous DST Start date.  It's either the current year, or
0395         //the previous year if the current month is earlier than StartMonth
0396         int y = date.date().year();
0397         if (RevertMonth > date.date().month())
0398             --y;
0399 
0400         result = KStarsDateTime(QDate(y, RevertMonth, 1), RevertTime);
0401         result = KStarsDateTime(QDate(y, RevertMonth, findRevertDay(result)), RevertTime);
0402     }
0403 
0404     qCDebug(KSTARS) << "Previous Daylight Savings Time change (Local Time): " << result.toString();
0405     next_change_ltime = result;
0406 }
0407 
0408 /**Convert current local DST change time in universal time */
0409 void TimeZoneRule::nextDSTChange(const KStarsDateTime &local_date, const double TZoffset)
0410 {
0411     // just decrement timezone offset and hour offset
0412     KStarsDateTime result = local_date.addSecs(int((TZoffset + deltaTZ()) * -3600));
0413 
0414     qCDebug(KSTARS) << "Next Daylight Savings Time change (UTC): " << result.toString();
0415     next_change_utc = result;
0416 }
0417 
0418 /**Convert current local DST change time in universal time */
0419 void TimeZoneRule::previousDSTChange(const KStarsDateTime &local_date, const double TZoffset)
0420 {
0421     // just decrement timezone offset
0422     KStarsDateTime result = local_date.addSecs(int(TZoffset * -3600));
0423 
0424     // if prev DST change is a revert change, so the revert time is in daylight saving time
0425     if (result.date().month() == RevertMonth)
0426         result = result.addSecs(int(HourOffset * -3600));
0427 
0428     qCDebug(KSTARS) << "Previous Daylight Savings Time change (UTC): " << result.toString();
0429     next_change_utc = result;
0430 }
0431 
0432 void TimeZoneRule::reset_with_ltime(KStarsDateTime &ltime, const double TZoffset, const bool time_runs_forward,
0433                                     const bool automaticDSTchange)
0434 {
0435     /**There are some problems using local time for getting next daylight saving change time.
0436         1. The local time is the start time of DST change. So the local time doesn't exists and must
0437               corrected.
0438         2. The local time is the revert time. So the local time exists twice.
0439         3. Neither start time nor revert time. There is no problem.
0440 
0441         Problem #1 is more complicated and we have to change the local time by reference.
0442         Problem #2 we just have to reset status of DST.
0443 
0444         automaticDSTchange should only set to true if DST status changed due to running automatically over
0445         a DST change time. If local time will changed manually the automaticDSTchange should always set to
0446         false, to hold current DST status if possible (just on start and revert time possible).
0447         */
0448 
0449     //don't need to do anything for empty rule
0450     if (isEmptyRule())
0451         return;
0452 
0453     // check if DST is active before resetting with new time
0454     bool wasDSTactive(false);
0455 
0456     if (deltaTZ() != 0.0)
0457     {
0458         wasDSTactive = true;
0459     }
0460 
0461     // check if current time is start time, this means if a DST change happened in last hour(s)
0462     bool active_with_houroffset = isDSTActive(ltime.addSecs(int(HourOffset * -3600)));
0463     bool active_normal          = isDSTActive(ltime);
0464 
0465     // store a valid local time
0466     KStarsDateTime ValidLTime = ltime;
0467 
0468     if (active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth)
0469     {
0470         // current time is the start time
0471         qCDebug(KSTARS) << "Current time = Starttime: invalid local time due to daylight saving time";
0472 
0473         // set a correct local time because the current time doesn't exists
0474         // if automatic DST change happened, new DST setting is the opposite of current setting
0475         if (automaticDSTchange)
0476         {
0477             // revert DST status
0478             setDST(!wasDSTactive);
0479             // new setting DST is inactive, so subtract hour offset to new time
0480             if (wasDSTactive)
0481             {
0482                 // DST inactive
0483                 ValidLTime = ltime.addSecs(int(HourOffset * -3600));
0484             }
0485             else
0486             {
0487                 // DST active
0488                 // add hour offset to new time
0489                 ValidLTime = ltime.addSecs(int(HourOffset * 3600));
0490             }
0491         }
0492         else // if ( automaticDSTchange )
0493         {
0494             // no automatic DST change happened, so stay in current DST mode
0495             setDST(wasDSTactive);
0496             if (wasDSTactive)
0497             {
0498                 // DST active
0499                 // add hour offset to current time, because time doesn't exists
0500                 ValidLTime = ltime.addSecs(int(HourOffset * 3600));
0501             }
0502             else
0503             {
0504                 // DST inactive
0505                 // subtrace hour offset to current time, because time doesn't exists
0506                 ValidLTime = ltime.addSecs(int(HourOffset * -3600));
0507             }
0508         } // else { // if ( automaticDSTchange )
0509     }
0510     else // if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth )
0511     {
0512         // If current time was not start time, so check if current time is revert time
0513         // this means if a DST change happened in next hour(s)
0514         active_with_houroffset = isDSTActive(ltime.addSecs(int(HourOffset * 3600)));
0515         if (active_with_houroffset != active_normal && RevertMonth == ValidLTime.date().month())
0516         {
0517             // current time is the revert time
0518             qCDebug(KSTARS) << "Current time = Reverttime";
0519 
0520             // we don't kneed to change the local time, because local time always exists, but
0521             // some times exists twice, so we have to reset DST status
0522             if (automaticDSTchange)
0523             {
0524                 // revert DST status
0525                 setDST(!wasDSTactive);
0526             }
0527             else
0528             {
0529                 // no automatic DST change so stay in current DST mode
0530                 setDST(wasDSTactive);
0531             }
0532         }
0533         else
0534         {
0535             //Current time was neither starttime nor reverttime, so use normal calculated DST status
0536             setDST(active_normal);
0537         }
0538     } // if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth )
0539 
0540     //  qDebug() << Q_FUNC_INFO << "Using Valid Local Time = " << ValidLTime.toString();
0541 
0542     if (time_runs_forward)
0543     {
0544         // get next DST change time in local time
0545         nextDSTChange_LTime(ValidLTime);
0546         nextDSTChange(next_change_ltime, TZoffset);
0547     }
0548     else
0549     {
0550         // get previous DST change time in local time
0551         previousDSTChange_LTime(ValidLTime);
0552         previousDSTChange(next_change_ltime, TZoffset);
0553     }
0554     ltime = ValidLTime;
0555 }
0556 
0557 bool TimeZoneRule::equals(TimeZoneRule *r)
0558 {
0559     if (StartDay == r->StartDay && RevertDay == r->RevertDay && StartWeek == r->StartWeek &&
0560         RevertWeek == r->RevertWeek && StartMonth == r->StartMonth && RevertMonth == r->RevertMonth &&
0561         StartTime == r->StartTime && RevertTime == r->RevertTime && isEmptyRule() == r->isEmptyRule())
0562         return true;
0563     else
0564         return false;
0565 }