1 /*
2     SPDX-FileCopyrightText: 2002 Jason Harris <kstars@30doradus.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "timezonerule.h"
8 #include "kstars_debug.h"
9 
10 #include <KLocalizedString>
11 
12 #include <QString>
13 
TimeZoneRule()14 TimeZoneRule::TimeZoneRule()
15 {
16     setEmpty();
17 }
18 
TimeZoneRule(const QString & smonth,const QString & sday,const QTime & stime,const QString & rmonth,const QString & rday,const QTime & rtime,const double & dh)19 TimeZoneRule::TimeZoneRule(const QString &smonth, const QString &sday, const QTime &stime, const QString &rmonth,
20                            const QString &rday, const QTime &rtime, const double &dh)
21 {
22     dTZ = 0.0;
23     if (smonth != "0")
24     {
25         StartMonth  = initMonth(smonth);
26         RevertMonth = initMonth(rmonth);
27 
28         if (StartMonth && RevertMonth && initDay(sday, StartDay, StartWeek) && initDay(rday, RevertDay, RevertWeek) &&
29             stime.isValid() && rtime.isValid())
30         {
31             StartTime  = stime;
32             RevertTime = rtime;
33             HourOffset = dh;
34         }
35         else
36         {
37             qCWarning(KSTARS) << i18n("Error parsing TimeZoneRule, setting to empty rule.");
38             setEmpty();
39         }
40     }
41     else //Empty rule
42     {
43         setEmpty();
44     }
45 }
46 
setEmpty()47 void TimeZoneRule::setEmpty()
48 {
49     StartMonth  = 0;
50     RevertMonth = 0;
51     StartDay    = 0;
52     RevertDay   = 0;
53     StartWeek   = -1;
54     RevertWeek  = -1;
55     StartTime   = QTime();
56     RevertTime  = QTime();
57     HourOffset  = 0.0;
58     dTZ         = 0.0;
59 }
60 
setDST(bool activate)61 void TimeZoneRule::setDST(bool activate)
62 {
63     if (activate)
64     {
65         qCDebug(KSTARS) << "Daylight Saving Time active";
66         dTZ = HourOffset;
67     }
68     else
69     {
70         qCDebug(KSTARS) << "Daylight Saving Time inactive";
71         dTZ = 0.0;
72     }
73 }
74 
initMonth(const QString & mn)75 int TimeZoneRule::initMonth(const QString &mn)
76 {
77     //Check whether the argument is a three-letter English month code.
78     QString ml = mn.toLower();
79     if (ml == "jan")
80         return 1;
81     else if (ml == "feb")
82         return 2;
83     else if (ml == "mar")
84         return 3;
85     else if (ml == "apr")
86         return 4;
87     else if (ml == "may")
88         return 5;
89     else if (ml == "jun")
90         return 6;
91     else if (ml == "jul")
92         return 7;
93     else if (ml == "aug")
94         return 8;
95     else if (ml == "sep")
96         return 9;
97     else if (ml == "oct")
98         return 10;
99     else if (ml == "nov")
100         return 11;
101     else if (ml == "dec")
102         return 12;
103 
104     qCWarning(KSTARS) << i18n("Could not parse %1 as a valid month code.", mn);
105     return 0;
106 }
107 
initDay(const QString & dy,int & Day,int & Week)108 bool TimeZoneRule::initDay(const QString &dy, int &Day, int &Week)
109 {
110     //Three possible ways to express a day.
111     //1. simple integer; the calendar date...set Week=0 to indicate that Date is not the day of the week
112     bool ok;
113     int day = dy.toInt(&ok);
114     if (ok)
115     {
116         Day  = day;
117         Week = 0;
118         return true;
119     }
120 
121     QString dl = dy.toLower();
122     //2. 3-letter day of week string, indicating the last of that day of the month
123     //   ...set Week to 5 to indicate the last weekday of the month
124     if (dl == "mon")
125     {
126         Day  = 1;
127         Week = 5;
128         return true;
129     }
130     else if (dl == "tue")
131     {
132         Day  = 2;
133         Week = 5;
134         return true;
135     }
136     else if (dl == "wed")
137     {
138         Day  = 3;
139         Week = 5;
140         return true;
141     }
142     else if (dl == "thu")
143     {
144         Day  = 4;
145         Week = 5;
146         return true;
147     }
148     else if (dl == "fri")
149     {
150         Day  = 5;
151         Week = 5;
152         return true;
153     }
154     else if (dl == "sat")
155     {
156         Day  = 6;
157         Week = 5;
158         return true;
159     }
160     else if (dl == "sun")
161     {
162         Day  = 7;
163         Week = 5;
164         return true;
165     }
166 
167     //3. 1,2 or 3 followed by 3-letter day of week string; this indicates
168     //   the (1st/2nd/3rd) weekday of the month.
169     int wn = dl.leftRef(1).toInt();
170     if (wn > 0 && wn < 4)
171     {
172         QString dm = dl.mid(1, dl.length()).toLower();
173         if (dm == "mon")
174         {
175             Day  = 1;
176             Week = wn;
177             return true;
178         }
179         else if (dm == "tue")
180         {
181             Day  = 2;
182             Week = wn;
183             return true;
184         }
185         else if (dm == "wed")
186         {
187             Day  = 3;
188             Week = wn;
189             return true;
190         }
191         else if (dm == "thu")
192         {
193             Day  = 4;
194             Week = wn;
195             return true;
196         }
197         else if (dm == "fri")
198         {
199             Day  = 5;
200             Week = wn;
201             return true;
202         }
203         else if (dm == "sat")
204         {
205             Day  = 6;
206             Week = wn;
207             return true;
208         }
209         else if (dm == "sun")
210         {
211             Day  = 7;
212             Week = wn;
213             return true;
214         }
215     }
216 
217     qCWarning(KSTARS) << i18n("Could not parse %1 as a valid day code.", dy);
218     return false;
219 }
220 
findStartDay(const KStarsDateTime & d)221 int TimeZoneRule::findStartDay(const KStarsDateTime &d)
222 {
223     // Determine the calendar date of StartDay for the month and year of the given date.
224     QDate test;
225 
226     // TimeZoneRule is empty, return -1
227     if (isEmptyRule())
228         return -1;
229 
230     // If StartWeek=0, just return the integer.
231     if (StartWeek == 0)
232         return StartDay;
233 
234     // Since StartWeek was not zero, StartDay is the day of the week, not the calendar date
235     else if (StartWeek == 5) // count back from end of month until StartDay is found.
236     {
237         for (test = QDate(d.date().year(), d.date().month(), d.date().daysInMonth()); test.day() > 21;
238              test = test.addDays(-1))
239             if (test.dayOfWeek() == StartDay)
240                 break;
241     }
242     else // Count forward from day 1, 8 or 15 (depending on StartWeek) until correct day of week is found
243     {
244         for (test = QDate(d.date().year(), d.date().month(), (StartWeek - 1) * 7 + 1); test.day() < 7 * StartWeek;
245              test = test.addDays(1))
246             if (test.dayOfWeek() == StartDay)
247                 break;
248     }
249     return test.day();
250 }
251 
findRevertDay(const KStarsDateTime & d)252 int TimeZoneRule::findRevertDay(const KStarsDateTime &d)
253 {
254     // Determine the calendar date of RevertDay for the month and year of the given date.
255     QDate test;
256 
257     // TimeZoneRule is empty, return -1
258     if (isEmptyRule())
259         return -1;
260 
261     // If RevertWeek=0, just return the integer.
262     if (RevertWeek == 0)
263         return RevertDay;
264 
265     // Since RevertWeek was not zero, RevertDay is the day of the week, not the calendar date
266     else if (RevertWeek == 5) //count back from end of month until RevertDay is found.
267     {
268         for (test = QDate(d.date().year(), d.date().month(), d.date().daysInMonth()); test.day() > 21;
269              test = test.addDays(-1))
270             if (test.dayOfWeek() == RevertDay)
271                 break;
272     }
273     else //Count forward from day 1, 8 or 15 (depending on RevertWeek) until correct day of week is found
274     {
275         for (test = QDate(d.date().year(), d.date().month(), (RevertWeek - 1) * 7 + 1); test.day() < 7 * RevertWeek;
276              test = test.addDays(1))
277             if (test.dayOfWeek() == StartDay)
278                 break;
279     }
280     return test.day();
281 }
282 
isDSTActive(const KStarsDateTime & date)283 bool TimeZoneRule::isDSTActive(const KStarsDateTime &date)
284 {
285     // The empty rule always returns false
286     if (isEmptyRule())
287         return false;
288 
289     // First, check whether the month is outside the DST interval.  Note that
290     // the interval check is different if StartMonth > RevertMonth (indicating that
291     // the DST interval includes the end of the year).
292     int month = date.date().month();
293 
294     if (StartMonth < RevertMonth)
295     {
296         if (month < StartMonth || month > RevertMonth)
297             return false;
298     }
299     else
300     {
301         if (month < StartMonth && month > RevertMonth)
302             return false;
303     }
304 
305     // OK, if the month is equal to StartMonth or Revert Month, we have more
306     // detailed checking to do...
307     int day = date.date().day();
308 
309     if (month == StartMonth)
310     {
311         int sday = findStartDay(date);
312         if (day < sday)
313             return false;
314         if (day == sday && date.time() < StartTime)
315             return false;
316     }
317     else if (month == RevertMonth)
318     {
319         int rday = findRevertDay(date);
320         if (day > rday)
321             return false;
322         if (day == rday && date.time() > RevertTime)
323             return false;
324     }
325 
326     // passed all tests, so we must be in DST.
327     return true;
328 }
329 
nextDSTChange_LTime(const KStarsDateTime & date)330 void TimeZoneRule::nextDSTChange_LTime(const KStarsDateTime &date)
331 {
332     KStarsDateTime result;
333 
334     // return an invalid date if the rule is the empty rule.
335     if (isEmptyRule())
336         result = KStarsDateTime(QDateTime());
337 
338     else if (deltaTZ())
339     {
340         // Next change is reverting back to standard time.
341 
342         //y is the year for the next DST Revert date.  It's either the current year, or
343         //the next year if the current month is already past RevertMonth
344         int y = date.date().year();
345         if (RevertMonth < date.date().month())
346             ++y;
347 
348         result = KStarsDateTime(QDate(y, RevertMonth, 1), RevertTime);
349         result = KStarsDateTime(QDate(y, RevertMonth, findRevertDay(result)), RevertTime);
350     }
351     else
352     {
353         // Next change is starting DST.
354 
355         //y is the year for the next DST Start date.  It's either the current year, or
356         //the next year if the current month is already past StartMonth
357         int y = date.date().year();
358         if (StartMonth < date.date().month())
359             ++y;
360 
361         result = KStarsDateTime(QDate(y, StartMonth, 1), StartTime);
362         result = KStarsDateTime(QDate(y, StartMonth, findStartDay(result)), StartTime);
363     }
364 
365     qCDebug(KSTARS) << "Next Daylight Savings Time change (Local Time): " << result.toString();
366     next_change_ltime = result;
367 }
368 
previousDSTChange_LTime(const KStarsDateTime & date)369 void TimeZoneRule::previousDSTChange_LTime(const KStarsDateTime &date)
370 {
371     KStarsDateTime result;
372 
373     // return an invalid date if the rule is the empty rule
374     if (isEmptyRule())
375         next_change_ltime = KStarsDateTime(QDateTime());
376 
377     if (deltaTZ())
378     {
379         // Last change was starting DST.
380 
381         //y is the year for the previous DST Start date.  It's either the current year, or
382         //the previous year if the current month is earlier than StartMonth
383         int y = date.date().year();
384         if (StartMonth > date.date().month())
385             --y;
386 
387         result = KStarsDateTime(QDate(y, StartMonth, 1), StartTime);
388         result = KStarsDateTime(QDate(y, StartMonth, findStartDay(result)), StartTime);
389     }
390     else if (StartMonth)
391     {
392         //Last change was reverting to standard time.
393 
394         //y is the year for the previous DST Start date.  It's either the current year, or
395         //the previous year if the current month is earlier than StartMonth
396         int y = date.date().year();
397         if (RevertMonth > date.date().month())
398             --y;
399 
400         result = KStarsDateTime(QDate(y, RevertMonth, 1), RevertTime);
401         result = KStarsDateTime(QDate(y, RevertMonth, findRevertDay(result)), RevertTime);
402     }
403 
404     qCDebug(KSTARS) << "Previous Daylight Savings Time change (Local Time): " << result.toString();
405     next_change_ltime = result;
406 }
407 
408 /**Convert current local DST change time in universal time */
nextDSTChange(const KStarsDateTime & local_date,const double TZoffset)409 void TimeZoneRule::nextDSTChange(const KStarsDateTime &local_date, const double TZoffset)
410 {
411     // just decrement timezone offset and hour offset
412     KStarsDateTime result = local_date.addSecs(int((TZoffset + deltaTZ()) * -3600));
413 
414     qCDebug(KSTARS) << "Next Daylight Savings Time change (UTC): " << result.toString();
415     next_change_utc = result;
416 }
417 
418 /**Convert current local DST change time in universal time */
previousDSTChange(const KStarsDateTime & local_date,const double TZoffset)419 void TimeZoneRule::previousDSTChange(const KStarsDateTime &local_date, const double TZoffset)
420 {
421     // just decrement timezone offset
422     KStarsDateTime result = local_date.addSecs(int(TZoffset * -3600));
423 
424     // if prev DST change is a revert change, so the revert time is in daylight saving time
425     if (result.date().month() == RevertMonth)
426         result = result.addSecs(int(HourOffset * -3600));
427 
428     qCDebug(KSTARS) << "Previous Daylight Savings Time change (UTC): " << result.toString();
429     next_change_utc = result;
430 }
431 
reset_with_ltime(KStarsDateTime & ltime,const double TZoffset,const bool time_runs_forward,const bool automaticDSTchange)432 void TimeZoneRule::reset_with_ltime(KStarsDateTime &ltime, const double TZoffset, const bool time_runs_forward,
433                                     const bool automaticDSTchange)
434 {
435     /**There are some problems using local time for getting next daylight saving change time.
436     	1. The local time is the start time of DST change. So the local time doesn't exists and must
437     		  corrected.
438     	2. The local time is the revert time. So the local time exists twice.
439     	3. Neither start time nor revert time. There is no problem.
440 
441     	Problem #1 is more complicated and we have to change the local time by reference.
442     	Problem #2 we just have to reset status of DST.
443 
444     	automaticDSTchange should only set to true if DST status changed due to running automatically over
445     	a DST change time. If local time will changed manually the automaticDSTchange should always set to
446     	false, to hold current DST status if possible (just on start and revert time possible).
447     	*/
448 
449     //don't need to do anything for empty rule
450     if (isEmptyRule())
451         return;
452 
453     // check if DST is active before resetting with new time
454     bool wasDSTactive(false);
455 
456     if (deltaTZ() != 0.0)
457     {
458         wasDSTactive = true;
459     }
460 
461     // check if current time is start time, this means if a DST change happened in last hour(s)
462     bool active_with_houroffset = isDSTActive(ltime.addSecs(int(HourOffset * -3600)));
463     bool active_normal          = isDSTActive(ltime);
464 
465     // store a valid local time
466     KStarsDateTime ValidLTime = ltime;
467 
468     if (active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth)
469     {
470         // current time is the start time
471         qCDebug(KSTARS) << "Current time = Starttime: invalid local time due to daylight saving time";
472 
473         // set a correct local time because the current time doesn't exists
474         // if automatic DST change happened, new DST setting is the opposite of current setting
475         if (automaticDSTchange)
476         {
477             // revert DST status
478             setDST(!wasDSTactive);
479             // new setting DST is inactive, so subtract hour offset to new time
480             if (wasDSTactive)
481             {
482                 // DST inactive
483                 ValidLTime = ltime.addSecs(int(HourOffset * -3600));
484             }
485             else
486             {
487                 // DST active
488                 // add hour offset to new time
489                 ValidLTime = ltime.addSecs(int(HourOffset * 3600));
490             }
491         }
492         else // if ( automaticDSTchange )
493         {
494             // no automatic DST change happened, so stay in current DST mode
495             setDST(wasDSTactive);
496             if (wasDSTactive)
497             {
498                 // DST active
499                 // add hour offset to current time, because time doesn't exists
500                 ValidLTime = ltime.addSecs(int(HourOffset * 3600));
501             }
502             else
503             {
504                 // DST inactive
505                 // subtrace hour offset to current time, because time doesn't exists
506                 ValidLTime = ltime.addSecs(int(HourOffset * -3600));
507             }
508         } // else { // if ( automaticDSTchange )
509     }
510     else // if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth )
511     {
512         // If current time was not start time, so check if current time is revert time
513         // this means if a DST change happened in next hour(s)
514         active_with_houroffset = isDSTActive(ltime.addSecs(int(HourOffset * 3600)));
515         if (active_with_houroffset != active_normal && RevertMonth == ValidLTime.date().month())
516         {
517             // current time is the revert time
518             qCDebug(KSTARS) << "Current time = Reverttime";
519 
520             // we don't kneed to change the local time, because local time always exists, but
521             // some times exists twice, so we have to reset DST status
522             if (automaticDSTchange)
523             {
524                 // revert DST status
525                 setDST(!wasDSTactive);
526             }
527             else
528             {
529                 // no automatic DST change so stay in current DST mode
530                 setDST(wasDSTactive);
531             }
532         }
533         else
534         {
535             //Current time was neither starttime nor reverttime, so use normal calculated DST status
536             setDST(active_normal);
537         }
538     } // if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth )
539 
540     //	qDebug() << "Using Valid Local Time = " << ValidLTime.toString();
541 
542     if (time_runs_forward)
543     {
544         // get next DST change time in local time
545         nextDSTChange_LTime(ValidLTime);
546         nextDSTChange(next_change_ltime, TZoffset);
547     }
548     else
549     {
550         // get previous DST change time in local time
551         previousDSTChange_LTime(ValidLTime);
552         previousDSTChange(next_change_ltime, TZoffset);
553     }
554     ltime = ValidLTime;
555 }
556 
equals(TimeZoneRule * r)557 bool TimeZoneRule::equals(TimeZoneRule *r)
558 {
559     if (StartDay == r->StartDay && RevertDay == r->RevertDay && StartWeek == r->StartWeek &&
560         RevertWeek == r->RevertWeek && StartMonth == r->StartMonth && RevertMonth == r->RevertMonth &&
561         StartTime == r->StartTime && RevertTime == r->RevertTime && isEmptyRule() == r->isEmptyRule())
562         return true;
563     else
564         return false;
565 }
566