2 * Copyright (C) 2011 Morphoss Ltd
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as
6 * published by the Free Software Foundation, either version 3 of the
7 * License, or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 package com
.morphoss
.acal
.acaltime
;
21 import java
.io
.Serializable
;
22 import java
.text
.DateFormat
;
23 import java
.text
.SimpleDateFormat
;
24 import java
.util
.Comparator
;
25 import java
.util
.Date
;
26 import java
.util
.TimeZone
;
27 import java
.util
.regex
.Matcher
;
28 import java
.util
.regex
.Pattern
;
30 import android
.os
.Parcel
;
31 import android
.os
.Parcelable
;
32 import android
.util
.Log
;
34 import com
.morphoss
.acal
.Constants
;
35 import com
.morphoss
.acal
.HashCodeUtil
;
36 import com
.morphoss
.acal
.StaticHelpers
;
37 import com
.morphoss
.acal
.davacal
.AcalProperty
;
38 import com
.morphoss
.acal
.davacal
.PropertyName
;
39 import com
.morphoss
.acal
.davacal
.VCalendar
;
42 * <h1>AcalDateTime</h1>
44 * This is a class for handling dates, with times, possibly with timezones.
47 * What! I hear you say: "ANOTHER date time class!!!" Er: yes. You see the Date and/or
48 * Calendar classes in Java bring along with them a large amount of baggage. In many
49 * cases this baggage is mighty useful, but for us it was causing problems, in particular
50 * it was causing problems with certainty around exactly when midnight occurred with some
51 * weird millisecond unreliability. The Java classes also aren't exactly keen on letting
52 * one control exactly when you care about timezones, and they don't seem to want to be
53 * helpful in allowing floating times. Bugger. And believe me: I tried.
56 * This class does include support for leap seconds, for setting the week start day and
57 * calculating week numbers according to ISO8601. The getMillis() and setMillis() functions
58 * are designed to round-trip through Java's native classes which do not support leap seconds,
59 * so the setEpoch()/getEpoch() methods are not *Millis/1000. Months are <em>not</em> zero-
60 * based here, so '1' is January. It seems likely that this is more memory-efficient than
61 * the native code, but while I've endeavoured to make it fast my lack of knowledge of
62 * Java may well have screwed that up.
65 * This class <em>does not</em> attempt to deal with iCalendar-style timezones. It assumes
66 * that events will have timezone names which will contain a recognisable Olson substring
67 * name and uses the Java timezone as a base. We might have to revisit this at some future
68 * point to attempt deeper recognition of timezone information, but it's a good first cut.
74 * @author Morphoss Ltd
77 public class AcalDateTime
implements Parcelable
, Serializable
, Cloneable
, Comparable
<AcalDateTime
> {
79 private static final String TAG
= "AcalDateTime";
80 private static final long serialVersionUID
= 1L;
82 private static final Pattern isoDatePattern
= Pattern
.compile(
83 "^((?:[123]\\d)?\\d{2})" + // 1 = year
84 "-?(0[1-9]|1[0-2])" + // 2 = month
85 "-?([0-3]\\d)" + // 3 = day
87 "([0-2]\\d)" + // 4 = hour
88 ":?([0-5]\\d)" + // 5 = minute
89 ":?([0-6]\\d)" + // 6 = second
90 "(Z)? *" + // 7 = UTC indicator
91 "([aApP]\\.?[mM])?" + // 8 = am/pm indicator
93 "(...(?::\\d\\d|.)?|" + // 9 = non-Olson timezone
94 "((?:Antarctica|America|Africa|Atlantic|Asia|Australia|Indian|Europe|Pacific|US)/(?:(?:[^/]+)/)?[^/]+)" +
95 ")?" // 10 = Olson timezone
98 public static final TimeZone UTC
= TimeZone
.getTimeZone("UTC");
100 public static final int SECONDS_IN_DAY
= 86400;
101 public static final int SECONDS_IN_HOUR
= 3600;
102 public static final int SECONDS_IN_MINUTE
= 60;
103 public static final int DAYS_IN_YEAR
= 365;
106 public static final short MIN_YEAR_VALUE
= 1582;
107 public static final short MAX_YEAR_VALUE
= 32766;
109 public static final long MAX_EPOCH_VALUE
= (long) ((long) (MAX_YEAR_VALUE
- 1971L) * SECONDS_IN_DAY
* DAYS_IN_YEAR
);
110 public static final long MIN_EPOCH_VALUE
= (long) ((long) (MIN_YEAR_VALUE
- 1971L) * SECONDS_IN_DAY
* DAYS_IN_YEAR
);
112 public static final AcalDateTime MIN
= new AcalDateTime(MIN_YEAR_VALUE
, 1, 1, 0, 0, 0, null);
113 public static final AcalDateTime MAX
= new AcalDateTime(MAX_YEAR_VALUE
, 12, 31, 23, 59, 59, null);
115 public static final short MONDAY
= 0;
116 public static final short TUESDAY
= 1;
117 public static final short WEDNESDAY
= 2;
118 public static final short THURSDAY
= 3;
119 public static final short FRIDAY
= 4;
120 public static final short SATURDAY
= 5;
121 public static final short SUNDAY
= 6;
123 public static final short JANUARY
= 1;
124 public static final short FEBRUARY
= 2;
125 public static final short MARCH
= 3;
126 public static final short APRIL
= 4;
127 public static final short MAY
= 5;
128 public static final short JUNE
= 6;
129 public static final short JULY
= 7;
130 public static final short AUGUST
= 8;
131 public static final short SEPTEMBER
= 9;
132 public static final short OCTOBER
= 10;
133 public static final short NOVEMBER
= 11;
134 public static final short DECEMBER
= 12;
136 protected static final short YEAR_NOT_SET
= Short
.MIN_VALUE
;
137 protected short year
= YEAR_NOT_SET
;
138 protected short month
;
140 protected short hour
= 0;
141 protected short minute
= 0;
142 protected short second
= 0;
144 protected short weekStart
= 0;
145 protected boolean isDate
= false;
147 protected static final long EPOCH_NOT_SET
= Long
.MIN_VALUE
;
148 protected long epoch
= EPOCH_NOT_SET
;
150 protected TimeZone tz
= null;
151 protected String tzName
= null;
156 * Construct a new AcalDateTime which will be floating, but with the current 'clock' time
157 * in the current timezone. This will mean that you should call the setTimeZone() method
158 * to anchor this to a timezone.
161 public AcalDateTime() {
162 epoch
= (System
.currentTimeMillis() + TimeZone
.getDefault().getOffset(System
.currentTimeMillis())) / 1000;
163 if ( Constants
.debugDateTime
) checkEpoch();
169 * Return a floating time which will represent the specified milliseconds from Epoch. This
170 * will mean that you should call the shiftTimeZone() method to anchor this to a timezone.
172 * @param millisecondsSinceEpoch
175 public static AcalDateTime
fromMillis(long millisecondsSinceEpoch
) {
176 AcalDateTime ret
= new AcalDateTime();
177 ret
.epoch
= millisecondsSinceEpoch
/ 1000;
178 if ( Constants
.debugDateTime
) ret
.checkEpoch();
185 * Return a localised time which will represent the specified milliseconds from Epoch.
187 * @param millisecondsSinceEpoch
190 public static AcalDateTime
localTimeFromMillis(long millisecondsSinceEpoch
, boolean fromFloating
) {
191 AcalDateTime ret
= fromMillis(millisecondsSinceEpoch
);
192 if ( fromFloating
) {
194 ret
.tzName
= UTC
.getID();
195 ret
.setTimeZone(TimeZone
.getDefault().getID());
198 ret
.shiftTimeZone(TimeZone
.getDefault().getID());
205 * @param yy Between 1582 and 32767
206 * @param mm Between 1 and 12
207 * @param dd Between 1 and the number of days in the month
208 * @param hh Between 0 and 23
209 * @param minute Between 0 and 59
210 * @param second Between 0 and 61 (to allow for possible leap second times)
213 public AcalDateTime( int yy
, int mm
, int dd
, int hh
, int minute
, int second
, String tzName
) {
214 if ( yy
< MIN_YEAR_VALUE
|| yy
> MAX_YEAR_VALUE
) throw new IllegalArgumentException();
216 if ( mm
< 1 || mm
> 12 ) throw new IllegalArgumentException();
218 if ( dd
< 1 || dd
> monthDays(yy
,mm
) ) throw new IllegalArgumentException();
220 if ( hh
< 0 || hh
> 23 )
221 throw new IllegalArgumentException();
223 if ( minute
< 0 || minute
> 59 )
224 throw new IllegalArgumentException();
225 this.minute
= (short) minute
;
226 if ( second
< 0 || second
> 59 ) throw new IllegalArgumentException();
227 this.second
= (short) second
;
229 if ( tzName
!= null ) {
230 tz
= TimeZone
.getTimeZone(tzName
);
231 if ( tz
!= null ) this.tzName
= tzName
;
233 epoch
= EPOCH_NOT_SET
;
237 * Construct from an AcalProperty Object, returning null if it is invalid in some way.
239 public static AcalDateTime
fromAcalProperty(AcalProperty prop
) {
240 if ( prop
== null ) return null;
242 return AcalDateTime
.fromIcalendar(prop
.getValue(),prop
.getParam("VALUE"),prop
.getParam("TZID"));
244 catch ( NullPointerException e
) {}
245 catch ( IllegalArgumentException e
) {}
250 * Returns the number of days in a year/month pair
253 * @return the number of days in the month
255 public static int monthDays(int year
, int month
) {
256 if (month
== 4 || month
== 6 || month
== 9 || month
== 11) return 30;
257 else if (month
!= 2) return 31;
258 return 28 + leapDay(year
);
263 * Returns a 1 if this year is a leap year, otherwise a 0
265 * @return 1 if this is a leap year, 0 otherwise
267 private static int leapDay(int year
) {
268 if ( (year
% 4) == 0 && ((year
% 100) != 0 || (year
% 400) == 0) ) return 1;
275 * Returns a count of the number of leap days between epoch and January 1st of this
276 * year. Years in the range 1800 to 59999 should be OK, or beyond that to some extent,
277 * but we don't, frankly, give a damn outside that range.
280 * @return The number of leap days between 1970-01-01 and January 1st of the specified year.
282 private static int epochLeapDays(int year
) {
285 return 7 - ((year
/ 4) - (year
/100) + (year
/400));
288 if ( year
< 130 ) return (year
/ 4);
291 return 8 + ((year
/ 4) - (year
/ 100) + (year
/ 400));
297 * Construct an AcalDate from a string. This will only handle parsing an ISO format string
298 * at present, which includes parsing an iCalendar DATE-TIIME or DATE string as they are subsets
302 * Strings that we can handle look like this kind of thing:
305 * <li>2001-12-15 14:23:15+13:00</li>
306 * <li>20011215T012315Z</li>
307 * <li>2001-12-15</li>
308 * <li>2001-12-15 14:23:15 Pacific/Auckland</li>
313 * @return A new AcalDate object.
314 * @throws IllegalArgumentException
316 public static AcalDateTime
fromString(String dateString
) throws IllegalArgumentException
{
317 if ( dateString
== null )
318 throw new IllegalArgumentException("Date may not be null.");
320 Matcher m
= isoDatePattern
.matcher(dateString
);
321 if ( ! m
.matches() ) {
322 throw new IllegalArgumentException("Date '" + dateString
+ "' is not in a recognised format.");
325 AcalDateTime newDateTime
= null;
327 int year
= Integer
.parseInt(m
.group(1));
328 int month
= Integer
.parseInt(m
.group(2));
329 int day
= Integer
.parseInt(m
.group(3));
334 if ( m
.group(4) != null && !m
.group(4).equals("") ) {
335 hour
= Integer
.parseInt(m
.group(4));
336 minute
= Integer
.parseInt(m
.group(5));
337 second
= Integer
.parseInt(m
.group(6));
338 if ( m
.group(8) != null && m
.group(8).equalsIgnoreCase("p") ) hour
+= 12;
339 newDateTime
= new AcalDateTime(year
, month
, day
, hour
, minute
, second
, null);
340 if (m
.group(7) != null && m
.group(7).equals("Z") ) {
341 newDateTime
.tz
= UTC
;
342 newDateTime
.tzName
= "UTC";
344 else if (m
.group(10) != null && !m
.group(10).equals("") ) {
345 newDateTime
.overwriteTimeZone(m
.group(10));
347 else if ( m
.group(9) != null && m
.group(9).equals("") ) {
348 // This is unlikely to be even close to working, since for this
349 // format we only got an offset, and all we could really do is guess
350 // what that might mean, in any case.
351 newDateTime
.overwriteTimeZone(m
.group(9));
354 else if (m
.matches() ) {
355 newDateTime
= new AcalDateTime(year
, month
, day
, 0, 0, 0, null);
356 newDateTime
.isDate
= true;
358 if ( Constants
.debugDateTime
) newDateTime
.checkEpoch();
364 * Simply write the named timezone to the current object making no attempt to adjust
365 * the validity of the current date / time information.
368 private void overwriteTimeZone( String newTzName
) {
369 if ( newTzName
== null || newTzName
.equals("")) {
374 tzName
= VCalendar
.staticGetOlsonName(newTzName
);
375 tz
= TimeZone
.getTimeZone(tzName
);
376 if ( tz
== null ) tzName
= null;
380 * hashCode for Serializable support.
382 public int hashCode() {
383 int result
= HashCodeUtil
.SEED
;
384 result
= HashCodeUtil
.hash( result
, this.tzName
);
385 result
= HashCodeUtil
.hash( result
, this.getEpoch() );
392 * Parse an input in RFC5545 format represented as an AcalProperty object and
393 * return a date localised to any TZID supplied, and expanded to a midnight time for a VALUE=DATE parameter.
396 * It is expected that this will be called something like:<br/>
398 * new AcalDateTime = AcalDateTime.fromProperty( dateProperty.Value(), dateProperty.getParam("VALUE"), dateProperty.getParam("TZID") );
400 * It's fine for either of the second two to be null. The first one can also be null, but you'll
401 * just get a null back in that case.
404 * @param dateString - The content of an iCalendar DATE-TIME property
405 * @param isDateParam - the VALUE parameter from an iCalendar DATE-TIME value
406 * @param tzIdParam - the TZID parameter from an iCalendar DATE-TIME value
407 * @return A shiny new AcalDateTime
409 public static AcalDateTime
fromIcalendar(String dateString
, String isDateParam
, String tzIdParam
) {
411 if ( dateString
== null || dateString
.equals("") ) return null;
413 AcalDateTime result
= fromString( dateString
);
415 if ( isDateParam
!= null ) result
.isDate
= isDateParam
.equalsIgnoreCase("DATE");
416 if ( tzIdParam
!= null ) result
.overwriteTimeZone(tzIdParam
);
417 if ( Constants
.debugDateTime
) result
.checkEpoch();
424 * Rerurn the year of this date, sometime after 1582.
428 public short getYear() {
429 if ( year
== YEAR_NOT_SET
) calculateDateTime();
436 * Try to set the year for this date. If the resulting date would be invalid
437 * then return false and don't change the date.
440 * @return true if a legal date would result, and has been set.
442 public synchronized boolean setYear( int yy
) {
443 if ( yy
< MIN_YEAR_VALUE
|| yy
> MAX_YEAR_VALUE
) throw new IllegalArgumentException();
444 if ( year
== YEAR_NOT_SET
) calculateDateTime();
445 if ( Constants
.debugDateTime
) checkEpoch();
446 if ( day
> monthDays(yy
,month
) ) return false;
447 if ( yy
== year
) return true;
449 epoch
= EPOCH_NOT_SET
;
455 * Rerurn the month of this date, 1 to 12
459 public short getMonth() {
460 if ( year
== YEAR_NOT_SET
) calculateDateTime();
467 * Try to set the month for this date. If the resulting date would be invalid
468 * then return false and don't change the date.
471 * @return true if a legal date would result, and has been set.
473 public synchronized boolean setMonth( int mm
) {
474 if ( mm
< 1 || mm
> 12 ) throw new IllegalArgumentException();
475 if ( year
== YEAR_NOT_SET
) calculateDateTime();
476 if ( Constants
.debugDateTime
) checkEpoch();
477 if ( mm
== month
) return true;
478 if ( day
> monthDays(year
,mm
) ) return false;
480 epoch
= EPOCH_NOT_SET
;
487 * Rerurn the day in month of this date, sometime after 1582.
491 public short getMonthDay() {
492 if ( year
== YEAR_NOT_SET
) calculateDateTime();
499 * Get the week of the month. Much simpler than the calculation of the week of the year,
500 * since we want to use this for things like 1st tuesday, 3rd thursday, etc.
504 public short getMonthWeek() {
505 return (short) (1 + (getMonthDay() / 7));
511 * Try to set the day of the month for this date. If the resulting date would be invalid
512 * then return false and don't change the date. The day of month may be negative, in which
513 * case it will be counted backwards with -1 being the last day of the month.
516 * @return true if a legal date would result, and has been set.
518 public synchronized boolean setMonthDay( int newDay
) {
519 if ( newDay
== 0 || newDay
< -31 || newDay
> 31 ) throw new IllegalArgumentException();
520 if ( Constants
.debugDateTime
) checkEpoch();
521 if ( year
== YEAR_NOT_SET
) calculateDateTime();
522 if ( newDay
< 0 ) newDay
+= 1 + monthDays(year
,month
); // backwards from end of month
523 if ( newDay
> monthDays(year
,month
) ) return false;
524 if ( day
== newDay
) return true;
525 day
= (short) newDay
;
526 epoch
= EPOCH_NOT_SET
;
532 * <p>Set the year, month and day of this AcalDateTime.</p>
533 * <p>If the day is invalid for the month in question it will be coerced to the maximum
534 * for that actual month (i.e. 31st of Feb will be coerced to 29th or 28th depending on the
535 * year, 31st April will become 30th, etc.)</p>
536 * <p>If you want the adjustment to fail you should call setMonthDay() instead, which also
537 * handles setting negative days as offsets from the end of the month, and this method does not.</p>
538 * @param newYear The year to set
539 * @param newMonth The month to set, from 1 to 12
540 * @param newDay The day to try and set, from 1 to 31
541 * @return this, for chaining.
543 public synchronized AcalDateTime
setYearMonthDay(int newYear
, int newMonth
, int newDay
) {
544 if ( newYear
< MIN_YEAR_VALUE
|| newYear
> MAX_YEAR_VALUE
) throw new IllegalArgumentException("Year must be between "+MIN_YEAR_VALUE
+" and "+ MAX_YEAR_VALUE
);
545 if ( newMonth
< 1 || newMonth
> 12 ) throw new IllegalArgumentException("Month must be from 1 to 12");
546 if ( newDay
< 1 || newDay
> 31 ) throw new IllegalArgumentException("Day must be from 1 to 31");
547 if ( newDay
> monthDays(year
,month
) ) newDay
= monthDays(year
,month
);
548 if ( year
== YEAR_NOT_SET
) calculateDateTime();
549 year
= (short) newYear
;
550 month
= (short) newMonth
;
551 day
= (short) newDay
;
552 epoch
= EPOCH_NOT_SET
;
559 * Returns the day of year. January the first is 1
561 * @return the day of year.
563 public short getYearDay() {
564 if ( year
== YEAR_NOT_SET
) calculateDateTime();
565 final int[] daysBeforeMonth
= { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
566 return (short) (day
+ (month
> 2 ?
leapDay(year
) : 0) + daysBeforeMonth
[month
-1]);
572 * Try to set the day of the year for this date. If the resulting date would be invalid
573 * then return false and don't change the date. The day of year may be negative, in which
574 * case it will be counted backwards with -1 being the last day of the year.
577 * @return true if a legal date would result, and has been set.
579 public synchronized boolean setYearDay( int yearDay
) {
580 if ( yearDay
== 0 || yearDay
< -366 || yearDay
> 366 ) throw new IllegalArgumentException();
581 if ( Constants
.debugDateTime
) checkEpoch();
582 if ( year
== YEAR_NOT_SET
) calculateDateTime();
583 if ( month
>= JANUARY
&& month
<= DECEMBER
&& yearDay
== getYearDay() ) return true;
584 int daysInYear
= DAYS_IN_YEAR
+ leapDay(year
);
586 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
587 // Log.v(TAG,"Got negative yearDay " + yearDay + " Will use: " + (yearDay + daysInYear + 1) );
588 yearDay
+= daysInYear
;
591 if ( yearDay
< 1 || yearDay
> daysInYear
) return false;
593 privateSetYearDay((short) yearDay
, (short) daysInYear
);
595 epoch
= EPOCH_NOT_SET
;
601 private void privateSetYearDay( short yearDay
, short daysInYear
) {
602 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
603 // Log.v(TAG,"Setting year day for " + yearDay + " with " + daysInYear + " days in year " + year );
604 if ( yearDay
< 60 ) {
605 if ( yearDay
< 32 ) {
611 day
= (short) (yearDay
- 31);
615 // Others are fixed relative to the end of the year
616 // Approximate binary nesting of if will minimise # of comparisons
617 // The really efficient way to code this would be an array of what month each day of the
618 yearDay
= (short) (daysInYear
- yearDay
);
619 if ( yearDay
< 184 ) {
620 if ( yearDay
< 92 ) {
621 if ( yearDay
< 31 ) { month
= DECEMBER
; day
= (short) ( 31 - yearDay
); }
622 else if ( yearDay
< 61 ) { month
= NOVEMBER
; day
= (short) ( 61 - yearDay
); }
623 else { month
= OCTOBER
; day
= (short) ( 92 - yearDay
); }
626 if ( yearDay
< 122 ) { month
= SEPTEMBER
; day
= (short) (122 - yearDay
); }
627 else if ( yearDay
< 153 ) { month
= AUGUST
; day
= (short) (153 - yearDay
); }
628 else { month
= JULY
; day
= (short) (184 - yearDay
); }
632 if ( yearDay
< 275 ) {
633 if ( yearDay
< 214 ) { month
= JUNE
; day
= (short) (214 - yearDay
); }
634 else if ( yearDay
< 245 ) { month
= MAY
; day
= (short) (245 - yearDay
); }
635 else { month
= APRIL
; day
= (short) (275 - yearDay
); }
638 if ( yearDay
< 306 ) { month
= MARCH
; day
= (short) (306 - yearDay
); }
639 else { month
= FEBRUARY
; day
= 29; }
643 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
644 // Log.v(TAG,"Got " + year + "-" + month + "-" + day );
649 * Get the day as numbers of days since Jan 1st 1970
652 public long getEpochDay() {
653 if ( epoch
!= EPOCH_NOT_SET
) {
654 long offset
= (tz
!= null ? tz
.getOffset(epoch
*1000) / 1000 : 0);
655 return (long) Math
.floor((epoch
+offset
) / SECONDS_IN_DAY
);
658 // Otherwise work it out from the date fields.
660 ((year
- 1970) * DAYS_IN_YEAR
)
661 + epochLeapDays(year
)
662 + ( getYearDay() - 1 )
668 * Set the day as numbers of days since Jan 1st 1970
671 public synchronized void setEpochDay( long newEpochDay
) {
672 if ( Constants
.debugDateTime
) checkEpoch();
673 if ( epoch
== EPOCH_NOT_SET
) calculateEpoch();
674 epoch
= (newEpochDay
* SECONDS_IN_DAY
) + getDaySecond();
680 * Get the day of week.
683 public short getWeekDay() {
684 return (short) ((getEpochDay() - 4) % 7);
690 * Set the day of week, taking the weekStart into account. In some circumstances the
691 * generated date might be invalid (monday before 1st jan, sunday after 31st december)
692 * and in that case the method will return false.
695 * @return true If a valid day of week was found within the year
697 public boolean setWeekDay( short weekDay
) {
698 if ( weekDay
< MONDAY
|| weekDay
> SUNDAY
) throw new IllegalArgumentException();
699 short firstOfWeek
= (short) (getYearDay() - ((getWeekDay() + 7 - weekStart
) % 7));
700 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
701 // Log.v(TAG,"First day of week is " + firstOfWeek + " for year day: " + getYearDay() + " which is weekDay " + getWeekDay() );
702 short newDay
= (short) (firstOfWeek
+ ((weekDay
+ 7 - weekStart
) % 7));
703 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
704 // Log.v(TAG,"New day of week is " + newDay + " for week day: " + weekDay + " and weekStart: " + weekStart );
705 if ( newDay
< 1 || newDay
> 366 ) return false;
706 if ( Constants
.debugDateTime
) checkEpoch();
707 return setYearDay( newDay
);
713 * Get the week of the year. The first week of the year is the one which contains at least
714 * four days. There might be 53 weeks in a year, e.g. if it started on a saturday or sunday
715 * and finished on a monday or tuesday (or equivalent for non-Monday weekstart).
718 * This function will return 0 for a week of the year which does not contain the 4th day of the
719 * year but which does contain the first. By specification that is the 52nd/53rd week of the
720 * previous year, but we have no way of indicating previous year except by that. Similarly it
721 * will return 53 for that week of the year containing 31st December, which does not also
722 * contain the 28th. Strictly, that would be week 1 of the following year.
725 * See RFC5545 section 3.3.10, page 41.
729 public short getYearWeek() {
730 short firstOfNextWeek
= (short) (7 + getYearDay() - ((getWeekDay() +(7 - weekStart
)) % 7));
731 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
732 // Log.v(TAG, "Getting week of " + year + "-" + month + "-" + day + " YearDay is " + getYearDay()
733 // + " first of next week is " + firstOfNextWeek );
734 int weekOfYear
= ((firstOfNextWeek
/ 7) + (((firstOfNextWeek
) % 7) > 4 ?
1 : 0));
735 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
736 // Log.v(TAG, "Got week of " + weekOfYear + " from " + Integer.toString(firstOfNextWeek / 7)
737 // + " and " + Integer.toString((firstOfNextWeek % 7) > 4 ? 1 : 0) );
738 return (short) weekOfYear
;
744 * Set the week number, from 0 to 53, where 0 is a week containing 1st January but not containing
751 public boolean setYearWeek( short weekNo
) {
752 if ( weekNo
< 0 || weekNo
> 53 ) throw new IllegalArgumentException();
753 int newDay
= getYearDay() + ((weekNo
- getYearWeek()) * 7);
754 if ( newDay
< 1 ) return false;
755 return setYearDay( newDay
);
761 * Rerurn the hour of this date.
765 public short getHour() {
766 if ( year
== YEAR_NOT_SET
) calculateDateTime();
773 * Try to set the hour for this date. Will throw an exception if the hour is not valid.
778 public synchronized AcalDateTime
setHour( int newHour
) {
779 if ( newHour
< 0 || newHour
> 23 ) throw new IllegalArgumentException();
780 if ( Constants
.debugDateTime
) checkEpoch();
781 if ( year
== YEAR_NOT_SET
) calculateDateTime();
782 if ( hour
== newHour
) return this;
783 if ( epoch
!= EPOCH_NOT_SET
) epoch
+= (newHour
- hour
) * SECONDS_IN_HOUR
;
784 hour
= (short) newHour
;
785 if ( Constants
.debugDateTime
) checkEpoch();
792 * Rerurn the minute of this date.
796 public short getMinute() {
797 if ( year
== YEAR_NOT_SET
) calculateDateTime();
804 * Try to set the minute for this date. Will throw an exception if the minute is not valid.
809 public synchronized AcalDateTime
setMinute( int newMinute
) {
810 if ( newMinute
< 0 || newMinute
> 59 ) throw new IllegalArgumentException();
811 if ( Constants
.debugDateTime
) checkEpoch();
812 if ( year
== YEAR_NOT_SET
) calculateDateTime();
813 if ( minute
== newMinute
) return this;
814 if ( epoch
!= EPOCH_NOT_SET
) epoch
+= (newMinute
- minute
) * SECONDS_IN_MINUTE
;
815 minute
= (short) newMinute
;
816 if ( Constants
.debugDateTime
) checkEpoch();
823 * Rerurn the second of this date.
827 public short getSecond() {
828 if ( year
== YEAR_NOT_SET
) calculateDateTime();
835 * Try to set the second (within the hour) for this date. Will throw an exception if the second
836 * is outside the range 0 - 60. Returns false if second is 60 but the resulting time was not a
840 * @return true, unless you try and set a leap second that doesn't exist.
842 public synchronized AcalDateTime
setSecond( int newSecond
) {
843 if ( newSecond
< 0 || newSecond
> 59 ) throw new IllegalArgumentException();
844 if ( Constants
.debugDateTime
) checkEpoch();
845 if ( year
== YEAR_NOT_SET
) calculateDateTime();
846 if ( second
== newSecond
) return this;
847 if ( epoch
!= EPOCH_NOT_SET
) epoch
+= newSecond
- second
;
848 second
= (short) newSecond
;
849 if ( Constants
.debugDateTime
) checkEpoch();
856 * Get the second for this date, from 0 to 86401.
860 public int getDaySecond( ) {
861 if ( year
== YEAR_NOT_SET
) calculateDateTime();
862 return (hour
* SECONDS_IN_HOUR
) + (minute
* 60) + second
;
868 * Try to set the second for this date. Will throw an exception if it's outside the
872 * @return this, for chaining
874 public synchronized AcalDateTime
setDaySecond( int newSecond
) {
875 if ( Constants
.debugDateTime
) checkEpoch();
876 if ( newSecond
< 0 || newSecond
>= SECONDS_IN_DAY
) throw new IllegalArgumentException("Attempt to setDaySecond("+Integer
.toString(newSecond
)+")");
877 if ( year
== YEAR_NOT_SET
) calculateDateTime();
878 short newHour
= (short) (newSecond
/ SECONDS_IN_HOUR
);
879 short newMinute
= (short) ((newSecond
% SECONDS_IN_HOUR
) / 60);
881 if ( hour
== newHour
&& minute
== newMinute
&& second
== newSecond
) return this;
882 if ( epoch
!= EPOCH_NOT_SET
) {
883 epoch
+= ((newHour
- hour
) * SECONDS_IN_HOUR
)
884 + ((newMinute
- minute
) * SECONDS_IN_MINUTE
)
885 + (newSecond
- second
);
887 hour
= (short) newHour
;
888 minute
= (short) newMinute
;
889 second
= (short) newSecond
;
890 if ( Constants
.debugDateTime
) checkEpoch();
897 * Get the timezone, which may be null.
899 * @return timezone object or null
901 public TimeZone
getTimeZone() {
908 * Get the timezone ID, which may be null, but which is hopefully an Olson name
910 * @return timezone name
912 public String
getTimeZoneId() {
913 if ( tz
== null ) return null;
920 * Set the timezone for this date, keeping the date & time constant.
922 * @return this, for chaining.
924 public synchronized AcalDateTime
setTimeZone( String newTz
) {
925 if ( tzName
== newTz
|| (tzName
!= null && tzName
.equals(newTz
)) ) return this;
926 if ( year
== YEAR_NOT_SET
) calculateDateTime(); // Because we're going to invalidate the epoch...
927 this.overwriteTimeZone(newTz
);
928 epoch
= EPOCH_NOT_SET
;
935 * Set the timezone for this date, shifting the clock time to keep the UTC epoch constant.
937 * @return this, for chaining.
939 public synchronized AcalDateTime
shiftTimeZone( String newTz
) {
940 if ( tzName
== newTz
|| (tzName
!= null && tzName
.equals(newTz
)) ) return this;
941 if ( epoch
== EPOCH_NOT_SET
) calculateEpoch(); // Because we're going to invalidate the date...
942 this.overwriteTimeZone(newTz
);
944 if ( Constants
.debugDateTime
) calculateDateTime();
951 * Apply the local time to this DateTime. If the time is currently floating we will
952 * use setTimeZone to keep the clock time constant, otherwise we will call shiftTimeZone
953 * to keep the epoch constant.
955 * @return this, for chaining.
957 public synchronized AcalDateTime
applyLocalTimeZone() {
958 String newTimeZone
= TimeZone
.getDefault().getID();
959 // if ( Constants.LOG_VERBOSE && isFloating() ) { // && Constants.debugDateTime )
960 // Log.println(Constants.LOGV,TAG,"Applying local ("+newTimeZone+") to date which is "+(tzName==null?"floating":tzName));
961 // Log.w(TAG,Log.getStackTraceString(new Exception("convert from floating.")));
964 return setTimeZone(newTimeZone
);
966 return shiftTimeZone(newTimeZone
);
971 * Get the flag that marks this as a date.
972 * @return whether this is marked as a date.
974 public boolean isDate() {
980 * Set the flag that marks this as a date. If it is marked as a date
981 * it will print as a date via fmtIcal() and toPropertyString() methods.
982 * @return this, for chaining.
984 public AcalDateTime
setAsDate(boolean newValue
) {
991 * Returns a number of milliseconds from epoch. Seconds really, since it will always
992 * be an exact multiple of 1000. This function is less accurate than getEpoch, since it
993 * removes the leap seconds from the result for compatibility with the standard Java
995 * @return milliseconds since epoch, without leap seconds.
997 public long getMillis() {
998 return (getEpoch() * 1000L);
1004 * Set the current time to the given milliSeconds from epoch. Note that we round to
1005 * the nearest second. We assume that these milliseconds don't include leap seconds
1006 * for comatibility with the standard Java libraries which know not of these things.
1008 * @param milliSeconds
1009 * @return this, for chaining.
1011 public AcalDateTime
setMillis( long milliSeconds
) {
1012 setEpoch( Math
.round(milliSeconds
/ 1000.0) );
1018 * Returns a number of seconds from epoch, including leap seconds (up to 2008-12-31)
1021 public long getEpoch() {
1022 if ( epoch
== EPOCH_NOT_SET
) calculateEpoch();
1028 * Set the current time to the given seconds from epoch, which we assume includes leap
1030 * @param newEpoch, the new epoch time to set
1031 * @return this, for chaining
1033 public synchronized AcalDateTime
setEpoch(long newEpoch
) {
1035 if ( epoch
> MAX_EPOCH_VALUE
) epoch
= MAX_EPOCH_VALUE
;
1036 if ( epoch
< MIN_EPOCH_VALUE
) epoch
= MIN_EPOCH_VALUE
;
1037 year
= YEAR_NOT_SET
;
1038 if ( Constants
.debugDateTime
) calculateDateTime();
1044 public static final short YEAR
= 1001;
1045 public static final short MONTH_OF_YEAR
= 1002;
1046 public static final short MONTH
= 1002;
1047 public static final short DAY_OF_MONTH
= 1003;
1048 public static final short WEEK_OF_MONTH
= 1004;
1049 public static final short DAY
= 1005;
1050 public static final short DAY_OF_YEAR
= 1006;
1051 public static final short DAY_OF_EPOCH
= 1007;
1052 public static final short HOUR
= 1020;
1053 public static final short MINUTE
= 1021;
1054 public static final short SECOND
= 1022;
1055 public static final short SECOND_OF_DAY
= 1023;
1056 public static final short WEEK_OF_YEAR
= 1030;
1057 public static final short DAY_OF_WEEK
= 1031;
1058 private static final short NEED_EPOCH_SET
= 1099;
1059 public static final short EPOCH
= 1101;
1063 * Get some field from the date.
1068 public int get(short whatToGet
) {
1069 if ( year
== YEAR_NOT_SET
)
1070 calculateDateTime();
1072 switch( whatToGet
) {
1073 case YEAR
: return year
;
1074 case MONTH_OF_YEAR
: return month
;
1075 case DAY_OF_MONTH
: return day
;
1076 case WEEK_OF_MONTH
: return getMonthWeek();
1077 case DAY_OF_YEAR
: return getYearDay();
1078 case HOUR
: return hour
;
1079 case MINUTE
: return minute
;
1080 case SECOND
: return second
;
1081 case SECOND_OF_DAY
: return getDaySecond();
1082 case WEEK_OF_YEAR
: return getYearWeek();
1083 case DAY_OF_WEEK
: return getWeekDay();
1085 throw new IllegalArgumentException();
1089 public boolean set(short whatToSet
, int setTo
) {
1090 if ( year
== YEAR_NOT_SET
&& whatToSet
< NEED_EPOCH_SET
)
1091 calculateDateTime();
1093 switch( whatToSet
) {
1094 case YEAR
: return setYear((int) setTo
);
1095 case MONTH_OF_YEAR
: return setMonth((int) setTo
);
1096 case DAY_OF_MONTH
: return setMonthDay((int) setTo
);
1097 case DAY_OF_YEAR
: return setYearDay((int) setTo
);
1098 case HOUR
: setHour((int) setTo
); return true;
1099 case MINUTE
: setMinute((int) setTo
); return true;
1100 case SECOND
: setSecond((int) setTo
); return true;
1101 case SECOND_OF_DAY
: setDaySecond((int) setTo
); return true;
1102 case WEEK_OF_YEAR
: setYearWeek((short) setTo
); return true;
1103 case DAY_OF_WEEK
: return setWeekDay((short) setTo
);
1105 throw new IllegalArgumentException();
1109 public int getActualMaximum(short whatToGet
) {
1110 if ( year
== YEAR_NOT_SET
&& whatToGet
< NEED_EPOCH_SET
)
1111 calculateDateTime();
1112 switch( whatToGet
) {
1113 case YEAR
: return MAX_YEAR_VALUE
;
1114 case MONTH_OF_YEAR
: return 12;
1115 case DAY_OF_MONTH
: return monthDays(year
,month
);
1116 case DAY_OF_YEAR
: return DAYS_IN_YEAR
+ leapDay(year
);
1117 case HOUR
: return 23;
1118 case MINUTE
: return 59;
1119 case SECOND
: return 59;
1120 case SECOND_OF_DAY
: return SECONDS_IN_DAY
;
1121 case DAY_OF_WEEK
: return SUNDAY
;
1123 throw new IllegalArgumentException();
1128 * Calculates the internal epoch value, because we don't do that unless we need it.
1130 protected synchronized void calculateEpoch() {
1131 if ( year
== YEAR_NOT_SET
) throw new IllegalStateException("Uninitialised object");
1132 epoch
= (((year
- 1970) * DAYS_IN_YEAR
) + epochLeapDays(year
) + ( getYearDay() - 1 )) * SECONDS_IN_DAY
;
1133 epoch
+= (hour
* SECONDS_IN_HOUR
) + (minute
* 60) + second
;
1134 if ( tz
== null ) return;
1135 long offset
= tz
.getOffset(this.getMillis()) / 1000;
1136 if ( offset
== 0 ) return;
1138 if ( offset
== tz
.getOffset(this.getMillis()) / 1000 ) return;
1139 epoch
+= (offset
- (tz
.getOffset(this.getMillis()) / 1000));
1143 final private static short TWENTY_YEARS
= ((4 * DAYS_IN_YEAR
) + 1) * 5;
1144 final private static short FORTY_YEARS
= ((4 * DAYS_IN_YEAR
) + 1) * 10;
1147 * Calculates the date + time values on the basis of the epoch value. We're lazy though
1148 * so we only calculate this if we have to.
1150 protected synchronized void calculateDateTime() {
1151 if ( epoch
== EPOCH_NOT_SET
) throw new IllegalStateException("Uninitialised object");
1152 if ( epoch
> MAX_EPOCH_VALUE
) epoch
= MAX_EPOCH_VALUE
;
1153 if ( epoch
< MIN_EPOCH_VALUE
) epoch
= MIN_EPOCH_VALUE
;
1154 long nDays
= (epoch
/ SECONDS_IN_DAY
);
1155 long nSeconds
= epoch
% SECONDS_IN_DAY
;
1157 if ( nSeconds
< 0 ) {
1158 nSeconds
+= SECONDS_IN_DAY
;
1163 // TODO: Work out a quicker way. We should be able to do this with
1164 // arithmetic directly
1165 short daysInYear
= 0;
1167 if ( nDays
> FORTY_YEARS
) { nDays
-= FORTY_YEARS
; year
+= 40; }
1168 if ( nDays
> TWENTY_YEARS
) { nDays
-= TWENTY_YEARS
; year
+= 20; }
1170 while( nDays
>= (daysInYear
= (short) (DAYS_IN_YEAR
+ leapDay(year
))) ) {
1171 nDays
-= daysInYear
;
1176 if ( nDays
< - FORTY_YEARS
) { nDays
+= FORTY_YEARS
; year
-= 40; }
1177 if ( nDays
< - TWENTY_YEARS
) { nDays
+= TWENTY_YEARS
; year
-= 20; }
1178 while( nDays
< 0 ) {
1180 daysInYear
= (short) (DAYS_IN_YEAR
+ leapDay(year
));
1181 nDays
+= daysInYear
;
1188 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
1189 // Log.v(TAG, "Setting year days to " + (nDays + 1) + ", seconds to " + nSeconds );
1190 privateSetYearDay((short) ++nDays
, daysInYear
);
1192 hour
= (short) (nSeconds
/ SECONDS_IN_HOUR
);
1193 minute
= (short) ((nSeconds
% SECONDS_IN_HOUR
) / 60);
1194 second
= (short) (nSeconds
% 60);
1197 if ( Constants
.debugDateTime
) checkSanity();
1201 private void checkEpoch() {
1202 if ( year
!= YEAR_NOT_SET
&& epoch
!= EPOCH_NOT_SET
) {
1203 long saveEpoch
= epoch
;
1205 if ( saveEpoch
!= epoch
) {
1207 throw new Exception();
1209 catch (Exception e
) {
1210 Log
.w(TAG
,"SUSPICIOUS: " + year
+ "-" + month
+ "-" + day
1211 + " T " + hour
+ ":" + minute
+ ":" + second
);
1212 Log
.w(TAG
, "Epoch "+saveEpoch
+" did not equal calculated epoch "+epoch
);
1213 Log
.w(TAG
, "Difference is "+(saveEpoch
- epoch
)+" - "
1214 +((saveEpoch
- epoch
)/86400) + " days, "
1215 +((saveEpoch
- epoch
)%86400) + " seconds " );
1216 Log
.i(TAG
,Log
.getStackTraceString(e
));
1222 private void checkSanity() {
1223 if ( year
< 1950 || year
> 2050
1224 || month
< 1 || month
> 12
1225 || day
< 0 || day
> 31
1226 || hour
< 0 || hour
> 23
1227 || minute
< 0 || minute
> 59
1228 || second
< 0 || second
> 59
1230 Log
.w(TAG
,"SUSPICIOUS: " + year
+ "-" + month
+ "-" + day
1231 + " T " + hour
+ ":" + minute
+ ":" + second
);
1232 Log
.i(TAG
,Log
.getStackTraceString(new Exception(this.toPropertyString(PropertyName
.INVALID
))));
1239 * Localises the date + time (which is assumed to actually represent some UTC value) to the
1240 * currently set timezone. Typically we need to do this after we've calculated the UTC date
1241 * and time from the epoch.
1244 private void localiseToZone() {
1245 if ( tz
== null ) return;
1246 long offset
= tz
.getOffset(this.getMillis()) / 1000;
1247 if ( offset
== 0 ) return;
1248 hour
+= (offset
/ SECONDS_IN_HOUR
);
1249 minute
+= ((offset
% SECONDS_IN_HOUR
) / 60);
1250 second
+= (offset
% 60);
1255 private void fixupTimeFields() {
1256 if ( second
< 0 ) { second
+= 60; minute
--; }
1257 else if ( second
> 59 ) { second
-= 60; minute
++; }
1258 if ( minute
< 0 ) { minute
+= 60; hour
--; }
1259 else if ( minute
> 59 ) { minute
-= 60; hour
++; }
1260 if ( hour
< 0 ) { hour
+= 24; day
--; }
1261 else if ( hour
> 23 ) { hour
-= 24; day
++; }
1269 day
+= monthDays(year
,month
);
1272 else while ( day
> monthDays(year
,month
) ) {
1273 day
-= monthDays(year
,month
);
1285 * Format the output as an iCalendar date / date-time string, with a trailling 'Z' if the zone is
1286 * UTC. This happens a lot, so we're trying to be as fast as possible.
1288 * @return The string.
1290 public String
fmtIcal() {
1291 if ( year
== YEAR_NOT_SET
) calculateDateTime();
1293 StringBuilder ret
= new StringBuilder(Integer
.toString(year
));
1294 if ( month
< 10 ) ret
.append("0");
1296 if ( day
< 10 ) ret
.append("0");
1299 if ( isDate
) return ret
.toString();
1302 if ( hour
< 10 ) ret
.append("0");
1304 if ( minute
< 10 ) ret
.append("0");
1306 if ( second
< 10 ) ret
.append("0");
1309 if ( tz
!= null && tzName
!= null && tzName
.equals("UTC") ) ret
.append('Z');
1311 return ret
.toString();
1317 * Returns an iCalendar property string for this DateTime value, given the name
1318 * for the property. For example if the 'name' is 'RECURRENCE-ID' we might get
1320 * <pre>RECURRENCE-ID;VALUE=DATE:20110107</pre>
1322 * <pre>RECURRENCE-ID;TZID=Pacific/Auckland:20110107T162347</pre>
1323 * etc, as appropriate for the DateTime value.
1326 * The iCalendar property string is <em>not</em> wrapped at 75 octets. You will need to
1327 * do that yourself... :-)
1330 * @return a string which is formatted like an iCalendar property.
1332 public String
toPropertyString( PropertyName name
) {
1333 StringBuilder ret
= new StringBuilder(name
.toString());
1335 ret
.append(";VALUE=DATE");
1336 // VALUE=DATE *MUST NOT* contain a TZID (RFC5545 3.2.19)
1338 else if ( tz
!= null && !tzName
.equals("UTC") ) {
1339 ret
.append(";TZID=");
1343 ret
.append(fmtIcal());
1344 return ret
.toString();
1348 public String
toString() {
1349 StringBuilder ret
= new StringBuilder( (epoch
== EPOCH_NOT_SET ?
"EPOCH_NOT_SET" : Long
.toString(epoch
)) );
1351 ret
.append(fmtIcal());
1352 if ( tz
!= null && !tzName
.equals("UTC") ) {
1356 return ret
.toString();
1360 * Compare this AcalDateTime to another. If this is earlier than the other return a negative
1361 * integer and if this is after return a positive integer. If they are the same return 0.
1363 * @return -1, 1 or 0
1366 public int compareTo( AcalDateTime another
) {
1367 if ( this == another
) return 0;
1368 if ( Constants
.debugDateTime
) checkEpoch();
1369 if ( this.epoch
== EPOCH_NOT_SET
) this.calculateEpoch();
1370 if ( another
.epoch
== EPOCH_NOT_SET
) another
.calculateEpoch();
1371 return ( this.epoch
== another
.epoch ?
0 : (this.epoch
< another
.epoch ?
-1 : 1));
1376 public boolean equals( Object another
) {
1377 if ( another
== null || !(another
instanceof AcalDateTime
) ) return false;
1378 if ( this == another
) return true;
1379 return (this.compareTo((AcalDateTime
)another
) == 0);
1384 * Checks whether this date is before some other date.
1385 * @param another AcalDateTime
1386 * @return true, iff this date is earlier than the other date, false otherwise, including if the other date is null
1388 public boolean before( AcalDateTime another
) {
1389 if ( another
== null ) return false;
1390 return (this.compareTo(another
) < 0);
1395 * Checks whether this date is after some other date.
1397 * @return true, iff this date is later than the other date, false otherwise, including if the other date is null
1399 public boolean after( AcalDateTime another
) {
1400 if ( another
== null ) return false;
1401 return (this.compareTo(another
) > 0);
1406 * Returns a java.util.Date representation of this AcalDateTime. Particularly useful
1407 * with SimpleDateFormat to produce localised.
1410 public Date
toJavaDate() {
1411 if ( year
== YEAR_NOT_SET
) calculateDateTime();
1412 return new Date(year
-1900, month
-1, day
, hour
, minute
, second
);
1415 public synchronized AcalDateTime
clone() {
1416 // if ( Constants.debugDateTime ) checkEpoch();
1418 if ( year
== YEAR_NOT_SET
) {
1419 c
= new AcalDateTime();
1420 c
.year
= YEAR_NOT_SET
;
1425 c
= new AcalDateTime( year
, month
, day
, hour
, minute
, second
, tzName
);
1427 catch ( IllegalArgumentException e
) {
1428 Log
.e(TAG
, "Some part of this date is wrong: "+year
+"-"+month
+"-"+day
+" "+hour
+":"+minute
+":"+second
);
1429 Log
.e(TAG
, Log
.getStackTraceString(e
));
1430 c
= new AcalDateTime();
1432 c
.month
= (month
< 1 ?
1 : (month
> 12 ?
12 : month
));
1433 c
.day
= (short) (day
< 1 ?
1 : (day
> monthDays(year
,month
) ?
monthDays(year
,month
) : day
));
1434 c
.hour
= (hour
< 0 ?
0 : (hour
> 23 ?
23 : hour
));
1435 c
.minute
= (minute
< 0 ?
0 : (minute
> 59 ?
59 : minute
));
1436 c
.second
= (second
< 0 ?
0 : (second
> 59 ?
59 : second
));
1440 c
.weekStart
= weekStart
;
1446 if ( Constants
.debugDateTime
) c
.checkEpoch();
1452 * Adds (or subtracts) a number of seconds from this AcalDateTime
1454 * @return The current object, for chaining.
1456 public synchronized AcalDateTime
addSeconds(long delta
) {
1457 if ( Constants
.debugDateTime
) checkEpoch();
1458 // final long MAX_SECONDS_DELTA = SECONDS_IN_DAY * 28;
1459 // final long MIN_SECONDS_DELTA = -MAX_SECONDS_DELTA;
1461 if ( epoch
== EPOCH_NOT_SET
) {
1463 if ( MIN_SECONDS_DELTA < delta && delta < MAX_SECONDS_DELTA ) {
1465 day += (delta / SECONDS_IN_DAY) + (delta < 0 ? -1 : 0);
1466 hour += (delta % SECONDS_IN_DAY) / SECONDS_IN_HOUR;
1467 minute += (delta % SECONDS_IN_HOUR) / SECONDS_IN_MINUTE;
1468 second += (delta % SECONDS_IN_MINUTE);
1476 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
1477 // Log.v(TAG,"Adding "+delta+" to "+epoch);
1479 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
1480 // Log.v(TAG,"Got epoch of "+epoch);
1481 year
= YEAR_NOT_SET
;
1482 if ( Constants
.debugDateTime
) calculateDateTime();
1488 * Adds an integer number of days to this AcalDateTime. And like all integers 'days'
1489 * could be negative, and that's OK too.
1492 public synchronized AcalDateTime
addDays(int days
) {
1493 if ( Constants
.debugDateTime
) checkEpoch();
1494 if ( year
== YEAR_NOT_SET
) calculateDateTime();
1496 int tmpInt
= this.day
+ days
;
1497 if ( tmpInt
> 0 && tmpInt
<= monthDays(year
,month
) ) {
1498 this.epoch
= EPOCH_NOT_SET
;
1499 if ( epoch
!= EPOCH_NOT_SET
) {
1500 Log
.w(TAG
,"Adding " + Integer
.toString(days
) + " days to "
1501 +fmtIcal()+", epoch ("+epoch
+"). Day "
1502 +day
+", new day "+tmpInt
);
1503 this.epoch
+= ((long) days
* SECONDS_IN_DAY
);
1505 this.day
= (short) tmpInt
;
1506 if ( Constants
.debugDateTime
) checkEpoch();
1509 tmpInt
= this.getYearDay() + days
;
1510 if ( tmpInt
> 0 && tmpInt
<= (DAYS_IN_YEAR
+ leapDay(year
)) ) {
1515 if ( tz
!= null ) tmpInt
= this.getDaySecond();
1516 if ( epoch
== EPOCH_NOT_SET
) calculateEpoch();
1517 epoch
+= days
* SECONDS_IN_DAY
;
1518 year
= YEAR_NOT_SET
;
1520 calculateDateTime();
1521 if ( getDaySecond() != tmpInt
) {
1522 // There was a DST boundary between the two times, so fix it up.
1523 tmpInt
-= getDaySecond();
1524 if ( tmpInt
< (SECONDS_IN_HOUR
* -12) ) tmpInt
+= SECONDS_IN_DAY
;
1525 else if ( tmpInt
> (SECONDS_IN_HOUR
* 12) ) tmpInt
-= SECONDS_IN_DAY
;
1529 if ( Constants
.debugDateTime
) checkEpoch();
1535 * Clones the supplied AcalDateTime and then adds an integer number of days to
1536 * the cloned value. And like all integers 'days' could be negative, and that's OK too.
1541 public static AcalDateTime
addDays(AcalDateTime c
, int days
) {
1542 AcalDateTime r
= (AcalDateTime
) c
.clone();
1548 public AcalDateTime
addMonths(int months
) {
1549 if ( Constants
.debugDateTime
) checkEpoch();
1550 if ( year
== YEAR_NOT_SET
) calculateDateTime();
1554 if ( month
< 0 ) year
--;
1557 if ( day
> monthDays(year
,month
) ) day
= (short) monthDays(year
,month
);
1558 epoch
= EPOCH_NOT_SET
;
1563 public boolean isFloating() {
1568 public AcalDuration
getDurationTo(AcalDateTime end
) {
1569 AcalDuration ret
= new AcalDuration();
1570 if ( end
== null ) {
1571 if ( isDate
) ret
.setDuration(1, 0);
1574 if ( epoch
== EPOCH_NOT_SET
) calculateEpoch();
1575 if ( end
.epoch
== EPOCH_NOT_SET
) end
.calculateEpoch();
1576 long seconds
= end
.epoch
- epoch
;
1577 long days
= (seconds
/ SECONDS_IN_DAY
);
1578 seconds
%= SECONDS_IN_DAY
;
1579 ret
.setDuration((int) days
, (int) seconds
);
1581 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
1582 // Log.v(TAG,"Duration from "+this.fmtIcal()+" to "+(end == null ? "null": end.fmtIcal()+ " = " + ret.toString()));
1587 public AcalDateTime
addDuration(AcalDuration relativeTime
) {
1588 if ( Constants
.debugDateTime
) checkEpoch();
1589 if ( relativeTime
.days
!= 0 ) this.addDays(relativeTime
.days
);
1590 if ( relativeTime
.seconds
!= 0 ) this.addSeconds(relativeTime
.seconds
);
1595 public static AcalDateTime
addDuration(AcalDateTime start
, AcalDuration relativeTime
) {
1596 if ( Constants
.debugDateTime
) start
.checkEpoch();
1597 AcalDateTime newTime
= start
.clone();
1598 newTime
.addDuration(relativeTime
);
1604 * We do it this way to more easily get a localised month name.
1608 public static String
getMonthName(int month
) {
1609 final SimpleDateFormat monthFormatter
= new SimpleDateFormat("MMMM");
1610 String monthName
= monthFormatter
.format(new Date(2011,(month
- 1),1));
1611 return monthName
.substring(0, 1).toUpperCase() + monthName
.substring(1);
1614 public String
getMonthName() {
1615 return getMonthName(month
);
1618 public static String
fmtMonthYear(AcalDateTime calendar
) {
1619 if (calendar
.year
== YEAR_NOT_SET
) calendar
.calculateDateTime();
1620 return getMonthName(calendar
.month
) + " " + calendar
.get(YEAR
);
1625 * Works out what suffix a date should have. English only.
1631 public static String
getSuffix(int num
) {
1632 if (num
% 100 > 3 && num
% 100 < 21) return "th";
1634 case 1: return "st";
1635 case 2: return "nd";
1636 case 3: return "rd";
1637 default: return "th";
1641 public static String
fmtDayMonthYear(AcalDateTime c
) {
1642 final DateFormat dateFormatter
= DateFormat
.getDateInstance(DateFormat
.LONG
);
1643 return StaticHelpers
.capitaliseWords(dateFormatter
.format(c
.toJavaDate()));
1647 public static boolean isWithinMonth(AcalDateTime selectedDate
, AcalDateTime displayedMonth
) {
1648 return displayedMonth
.getMonth() == selectedDate
.getMonth();
1652 public AcalDateTime
setWeekStart(short wDay
) {
1653 if ( wDay
< MONDAY
|| wDay
> SUNDAY
) throw new IllegalArgumentException();
1658 public static class AcalDateTimeSorter
implements Comparator
<AcalDateTime
> {
1660 public int compare(AcalDateTime arg0
, AcalDateTime arg1
) {
1661 if ( arg0
.before(arg1
) ) return -1;
1662 else if ( arg0
.after(arg1
)) return 1;
1667 public static AcalDateTime
unwrapParcel( Parcel in
) {
1668 AcalDateTime dt
= new AcalDateTime();
1669 dt
.epoch
= in
.readLong();
1670 dt
.weekStart
= (short) in
.readInt();
1671 dt
.isDate
= (in
.readByte() == 'D');
1672 boolean tzIsSet
= (in
.readByte() == '1');
1673 if ( tzIsSet
) dt
.overwriteTimeZone(in
.readString());
1674 dt
.year
= AcalDateTime
.YEAR_NOT_SET
;
1678 public static final Parcelable
.Creator
<AcalDateTime
> CREATOR
= new Parcelable
.Creator
<AcalDateTime
>() {
1679 public AcalDateTime
createFromParcel(Parcel in
) {
1680 return unwrapParcel(in
);
1683 public AcalDateTime
[] newArray(int size
) {
1684 return new AcalDateTime
[size
];
1689 public int describeContents() {
1694 public void writeToParcel(Parcel dest
, int flags
) {
1695 if ( epoch
== EPOCH_NOT_SET
) calculateEpoch();
1696 dest
.writeLong(epoch
);
1697 dest
.writeInt(weekStart
);
1698 dest
.writeByte((byte) (this.isDate ?
'D' : 'T'));
1699 dest
.writeByte((byte) (tzName
== null ?
'0' : '1'));
1700 if ( tzName
!= null ) dest
.writeString(tzName
);
1704 public AcalProperty
asProperty(String propertyName
) {
1705 AcalProperty ret
= new AcalProperty(propertyName
, fmtIcal());
1706 if ( isDate
) ret
.setParam("VALUE", "DATE");
1707 else if ( tz
!= null && !tzName
.equals("UTC") ) ret
.setParam("TZID",tzName
);
1712 public AcalProperty
asProperty(PropertyName pName
) {
1713 return asProperty(pName
.toString());