summaryrefslogtreecommitdiffstats
path: root/kstars/kstars/timezonerule.cpp
blob: 0a711f579abb1ee7958c4b0acd9bc0c5bbd32089 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
/***************************************************************************
                          timezonerule.cpp  -  description
                             -------------------
    begin                : Tue Apr 2 2002
    copyright            : (C) 2002 by Jason Harris
    email                : kstars@30doradus.org
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include <kdebug.h>
#include <klocale.h>

#include "timezonerule.h"
#include "kstarsdatetime.h"

TimeZoneRule::TimeZoneRule() {
	//Build the empty TimeZoneRule.
	StartMonth = 0;
	RevertMonth = 0;
	StartDay = 0;
	RevertDay = 0;
	StartWeek = -1;
	RevertWeek = -1;
	StartTime = TQTime();
	RevertTime = TQTime();
	HourOffset = 0.0;
	dTZ = 0.0;
}

TimeZoneRule::TimeZoneRule( const TQString &smonth, const TQString &sday, const TQTime &stime,
			const TQString &rmonth, const TQString &rday, const TQTime &rtime, const double &dh ) {
	dTZ = 0.0;
	if ( smonth != "0" ) {
		StartMonth = initMonth( smonth );
		RevertMonth = initMonth( rmonth );

		if ( StartMonth && RevertMonth && initDay( sday, StartDay, StartWeek ) &&
					initDay( rday, RevertDay, RevertWeek ) && stime.isValid() && rtime.isValid() ) {
			StartTime = stime;
			RevertTime = rtime;
			HourOffset = dh;
		} else {
			kdWarning() << i18n( "Error parsing TimeZoneRule, setting to empty rule." ) << endl;
			StartMonth = 0;
			RevertMonth = 0;
			StartDay = 0;
			RevertDay = 0;
			StartWeek = -1;
			RevertWeek = -1;
			StartTime = TQTime();
			RevertTime = TQTime();
			HourOffset = 0.0;
		}
	} else { //Empty rule
		StartMonth = 0;
		RevertMonth = 0;
		StartDay = 0;
		RevertDay = 0;
		StartWeek = -1;
		RevertWeek = -1;
		StartTime = TQTime();
		RevertTime = TQTime();
		HourOffset = 0.0;
	}
}

TimeZoneRule::~TimeZoneRule() {
}

void TimeZoneRule::setDST( bool activate ) {
	if ( activate ) {
		kdDebug() << i18n( "Daylight Saving Time active" ) << endl;
		dTZ = HourOffset;
	} else {
		kdDebug() << i18n( "Daylight Saving Time inactive" ) << endl;
		dTZ = 0.0;
	}
}

int TimeZoneRule::initMonth( const TQString &mn ) {
//Check whether the argument is a three-letter English month code.
	TQString ml = mn.lower();
	if ( ml == "jan" ) return 1;
	else if ( ml == "feb" ) return 2;
	else if ( ml == "mar" ) return 3;
	else if ( ml == "apr" ) return 4;
	else if ( ml == "may" ) return 5;
	else if ( ml == "jun" ) return 6;
	else if ( ml == "jul" ) return 7;
	else if ( ml == "aug" ) return 8;
	else if ( ml == "sep" ) return 9;
	else if ( ml == "oct" ) return 10;
	else if ( ml == "nov" ) return 11;
	else if ( ml == "dec" ) return 12;

	kdWarning() << i18n( "Could not parse " ) << mn << i18n( " as a valid month code." ) << endl;
	return false;
}

bool TimeZoneRule::initDay( const TQString &dy, int &Day, int &Week ) {
//Three possible ways to express a day.
//1. simple integer; the calendar date...set Week=0 to indicate that Date is not the day of the week
	bool ok;
	int day = dy.toInt( &ok );
	if ( ok ) {
		Day = day;
		Week = 0;
		return true;
	}

	TQString dl = dy.lower();
//2. 3-letter day of week string, indicating the last of that day of the month
//   ...set Week to 5 to indicate the last weekday of the month
	if ( dl == "mon" ) { Day = 1; Week = 5; return true; }
	else if ( dl == "tue" ) { Day = 2; Week = 5; return true; }
	else if ( dl == "wed" ) { Day = 3; Week = 5; return true; }
	else if ( dl == "thu" ) { Day = 4; Week = 5; return true; }
	else if ( dl == "fri" ) { Day = 5; Week = 5; return true; }
	else if ( dl == "sat" ) { Day = 6; Week = 5; return true; }
	else if ( dl == "sun" ) { Day = 7; Week = 5; return true; }

//3. 1,2 or 3 followed by 3-letter day of week string; this indicates
//   the (1st/2nd/3rd) weekday of the month.
	int wn = dl.left(1).toInt();
	if ( wn >0 && wn <4 ) {
		TQString dm = dl.mid( 1, dl.length() ).lower();
		if ( dm == "mon" ) { Day = 1; Week = wn; return true; }
		else if ( dm == "tue" ) { Day = 2; Week = wn; return true; }
		else if ( dm == "wed" ) { Day = 3; Week = wn; return true; }
		else if ( dm == "thu" ) { Day = 4; Week = wn; return true; }
		else if ( dm == "fri" ) { Day = 5; Week = wn; return true; }
		else if ( dm == "sat" ) { Day = 6; Week = wn; return true; }
		else if ( dm == "sun" ) { Day = 7; Week = wn; return true; }
	}

	kdWarning() << i18n( "Could not parse " ) << dy << i18n( " as a valid day code." ) << endl;
	return false;
}

int TimeZoneRule::findStartDay( const KStarsDateTime &d ) {
	// Determine the calendar date of StartDay for the month and year of the given date.
	ExtDate test;

	// TimeZoneRule is empty, return -1
	if ( isEmptyRule() ) return -1;

	// If StartWeek=0, just return the integer.
	if ( StartWeek==0 ) return StartDay;

	// Since StartWeek was not zero, StartDay is the day of the week, not the calendar date
	else if ( StartWeek==5 ) { // count back from end of month until StartDay is found.
		for ( test = ExtDate( d.date().year(), d.date().month(), d.date().daysInMonth() );
					test.day() > 21; test = test.addDays( -1 ) )
			if ( test.dayOfWeek() == StartDay ) break;
	} else { // Count forward from day 1, 8 or 15 (depending on StartWeek) until correct day of week is found
		for ( test = ExtDate( d.date().year(), d.date().month(), (StartWeek-1)*7 + 1 );
					test.day() < 7*StartWeek; test = test.addDays( 1 ) )
			if ( test.dayOfWeek() == StartDay ) break;
	}
	return test.day();
}

int TimeZoneRule::findRevertDay( const KStarsDateTime &d ) {
	// Determine the calendar date of RevertDay for the month and year of the given date.
	ExtDate test;

	// TimeZoneRule is empty, return -1
	if ( isEmptyRule() ) return -1;

	// If RevertWeek=0, just return the integer.
	if ( RevertWeek==0 ) return RevertDay;

	// Since RevertWeek was not zero, RevertDay is the day of the week, not the calendar date
	else if ( RevertWeek==5 ) { //count back from end of month until RevertDay is found.
		for ( test = ExtDate( d.date().year(), d.date().month(), d.date().daysInMonth() );
					test.day() > 21; test = test.addDays( -1 ) )
			if ( test.dayOfWeek() == RevertDay ) break;
	} else { //Count forward from day 1, 8 or 15 (depending on RevertWeek) until correct day of week is found
		for ( test = ExtDate( d.date().year(), d.date().month(), (RevertWeek-1)*7 + 1 );
					test.day() < 7*RevertWeek; test = test.addDays( 1 ) )
			if ( test.dayOfWeek() == StartDay ) break;
	}
	return test.day();
}

bool TimeZoneRule::isDSTActive( const KStarsDateTime &date ) {
	// The empty rule always returns false
	if ( isEmptyRule() ) return false;

	// First, check whether the month is outside the DST interval.  Note that
	// the interval check is different if StartMonth > RevertMonth (indicating that
	// the DST interval includes the end of the year).
	int month = date.date().month();
	
	if ( StartMonth < RevertMonth ) {
		if ( month < StartMonth || month > RevertMonth ) return false;
	} else {
		if ( month < StartMonth && month > RevertMonth ) return false;
	}
	
	// OK, if the month is equal to StartMonth or Revert Month, we have more
	// detailed checking to do...
	int day = date.date().day();

	if ( month == StartMonth ) {
		int sday = findStartDay( date );
		if ( day < sday ) return false;
		if ( day==sday && date.time() < StartTime ) return false;
	} else if ( month == RevertMonth ) {
		int rday = findRevertDay( date );
		if ( day > rday ) return false;
		if ( day==rday && date.time() > RevertTime ) return false;
	}

	// passed all tests, so we must be in DST.
	return true;
}

void TimeZoneRule::nextDSTChange_LTime( const KStarsDateTime &date ) {
	KStarsDateTime result;

	// return a very remote date if the rule is the empty rule.
	if ( isEmptyRule() ) result = KStarsDateTime( INVALID_DAY );

	else if ( deltaTZ() ) {
		// Next change is reverting back to standard time.

		//y is the year for the next DST Revert date.  It's either the current year, or
		//the next year if the current month is already past RevertMonth
		int y = date.date().year();
		if ( RevertMonth < date.date().month() ) ++y;

		result = KStarsDateTime( ExtDate( y, RevertMonth, 1 ), RevertTime );
		result = KStarsDateTime( ExtDate( y, RevertMonth, findRevertDay( result ) ), RevertTime );

	} else {
		// Next change is starting DST.

		//y is the year for the next DST Start date.  It's either the current year, or
		//the next year if the current month is already past StartMonth
		int y = date.date().year();
		if ( StartMonth < date.date().month() ) ++y;

		result = KStarsDateTime( ExtDate( y, StartMonth, 1 ), StartTime );
		result = KStarsDateTime( ExtDate( y, StartMonth, findStartDay( result ) ), StartTime );
	}

	kdDebug() << i18n( "Next Daylight Savings Time change (Local Time): " ) << result.toString() << endl;
	next_change_ltime = result;
}


void TimeZoneRule::previousDSTChange_LTime( const KStarsDateTime &date ) {
	KStarsDateTime result;

	// return a very remote date if the rule is the empty rule
	if ( isEmptyRule() ) next_change_ltime = KStarsDateTime( INVALID_DAY );

	if ( deltaTZ() ) {
		// Last change was starting DST.

		//y is the year for the previous DST Start date.  It's either the current year, or
		//the previous year if the current month is earlier than StartMonth
		int y = date.date().year();
		if ( StartMonth > date.date().month() ) --y;

		result = KStarsDateTime( ExtDate( y, StartMonth, 1 ), StartTime );
		result = KStarsDateTime( ExtDate( y, StartMonth, findStartDay( result ) ), StartTime );

	} else if ( StartMonth ) {
		//Last change was reverting to standard time.

		//y is the year for the previous DST Start date.  It's either the current year, or
		//the previous year if the current month is earlier than StartMonth
		int y = date.date().year();
		if ( RevertMonth > date.date().month() ) --y;

		result = KStarsDateTime( ExtDate( y, RevertMonth, 1 ), RevertTime );
		result = KStarsDateTime( ExtDate( y, RevertMonth, findRevertDay( result ) ), RevertTime );
	}

	kdDebug() << i18n( "Previous Daylight Savings Time change (Local Time): " ) << result.toString() << endl;
	next_change_ltime = result;
}

/**Convert current local DST change time in universal time */
void TimeZoneRule::nextDSTChange( const KStarsDateTime &local_date, const double TZoffset ) {
	// just decrement timezone offset and hour offset
	KStarsDateTime result = local_date.addSecs( int( (TZoffset + deltaTZ()) * -3600) );

	kdDebug() << i18n( "Next Daylight Savings Time change (UTC): " ) << result.toString() << endl;
	next_change_utc = result;
}

/**Convert current local DST change time in universal time */
void TimeZoneRule::previousDSTChange( const KStarsDateTime &local_date, const double TZoffset ) {
	// just decrement timezone offset
	KStarsDateTime result = local_date.addSecs( int( TZoffset * -3600) );

	// if prev DST change is a revert change, so the revert time is in daylight saving time
	if ( result.date().month() == RevertMonth )
		result = result.addSecs( int(HourOffset * -3600) );
	
	kdDebug() << i18n( "Previous Daylight Savings Time change (UTC): " ) << result.toString() << endl;
	next_change_utc = result;
}

void TimeZoneRule::reset_with_ltime( KStarsDateTime &ltime, const double TZoffset, const bool time_runs_forward,
		const bool automaticDSTchange ) {

/**There are some problems using local time for getting next daylight saving change time.
	1. The local time is the start time of DST change. So the local time doesn't exists and must
		  corrected.
	2. The local time is the revert time. So the local time exists twice.
	3. Neither start time nor revert time. There is no problem.

	Problem #1 is more complicated and we have to change the local time by reference.
	Problem #2 we just have to reset status of DST.
	
	automaticDSTchange should only set to true if DST status changed due to running automatically over
	a DST change time. If local time will changed manually the automaticDSTchange should always set to
	false, to hold current DST status if possible (just on start and revert time possible).
	*/

	//don't need to do anything for empty rule
	if ( isEmptyRule() ) return;

	// check if DST is active before resetting with new time
	bool wasDSTactive(false);
	
	if ( deltaTZ() != 0.0 ) { 
		wasDSTactive = true;
	}

	// check if current time is start time, this means if a DST change happend in last hour(s)
	bool active_with_houroffset = isDSTActive(ltime.addSecs( int(HourOffset * -3600) ) );
	bool active_normal = isDSTActive( ltime );

	// store a valid local time
	KStarsDateTime ValidLTime = ltime;

	if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth ) {
		// current time is the start time
		kdDebug() << "Current time = Starttime: invalid local time due to daylight saving time" << endl;

		// set a correct local time because the current time doesn't exists
		// if automatic DST change happend, new DST setting is the opposite of current setting
		if ( automaticDSTchange ) {
			// revert DST status
			setDST( !wasDSTactive );
			// new setting DST is inactive, so subtract hour offset to new time
			if ( wasDSTactive ) {
				// DST inactive
				ValidLTime = ltime.addSecs( int( HourOffset * - 3600) );
			} else {
				// DST active
				// add hour offset to new time
				ValidLTime = ltime.addSecs( int( HourOffset * 3600) );
			}
		} else {  // if ( automaticDSTchange )
		// no automatic DST change happend, so stay in current DST mode
			setDST( wasDSTactive );
			if ( wasDSTactive ) {
				// DST active
				// add hour offset to current time, because time doesn't exists
				ValidLTime = ltime.addSecs( int( HourOffset * 3600) );
			} else {
				// DST inactive
				// subtrace hour offset to current time, because time doesn't exists
				ValidLTime = ltime.addSecs( int( HourOffset * -3600) );
			}
		}  // else { // if ( automaticDSTchange )

	} else {  // if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth )
		// If current time was not start time, so check if current time is revert time
		// this means if a DST change happend in next hour(s)
		active_with_houroffset = isDSTActive(ltime.addSecs( int(HourOffset * 3600) ) );
		if ( active_with_houroffset != active_normal && RevertMonth == ValidLTime.date().month() ) {
			// current time is the revert time
			kdDebug() << "Current time = Reverttime" << endl;

			// we don't kneed to change the local time, because local time always exists, but
			// some times exists twice, so we have to reset DST status
			if ( automaticDSTchange ) {
				// revert DST status
				setDST( !wasDSTactive );
			} else {
				// no automatic DST change so stay in current DST mode
				setDST( wasDSTactive );
			}

		} else {
			//Current time was neither starttime nor reverttime, so use normal calculated DST status
			setDST( active_normal );
		}
	}  // if ( active_with_houroffset != active_normal && ValidLTime.date().month() == StartMonth )

//	kdDebug() << "Using Valid Local Time = " << ValidLTime.toString() << endl;

	if (time_runs_forward) {
		// get next DST change time in local time
		nextDSTChange_LTime( ValidLTime );
		nextDSTChange( next_change_ltime, TZoffset );
	} else {
		// get previous DST change time in local time
		previousDSTChange_LTime( ValidLTime );
		previousDSTChange( next_change_ltime, TZoffset );
	}
	ltime = ValidLTime;
}


bool TimeZoneRule::equals( TimeZoneRule *r ) {
	if ( StartDay == r->StartDay && RevertDay == r->RevertDay &&
			StartWeek == r->StartWeek && RevertWeek == r->RevertWeek &&
			StartMonth == r->StartMonth && RevertMonth == r->RevertMonth &&
			StartTime == r->StartTime && RevertTime == r->RevertTime &&
			isEmptyRule() == r->isEmptyRule() )
		return true;
	else
		return false;
}