Another way to create AcalDateTime where you can advise it is floating.
[acal.git] / src / com / morphoss / acal / acaltime / AcalDateTime.java
blob1fcafa9a41d5f20938b00616bcaca0a7727f37a1
1 /*
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;
41 /**
42 * <h1>AcalDateTime</h1>
43 * <p>
44 * This is a class for handling dates, with times, possibly with timezones.
45 * </p>
46 * <p>
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.
54 * </p>
55 * <p>
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.
63 * </p>
64 * <p>
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.
69 * </p>
70 * <p>
71 * Enjoy!
72 * </p>
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
86 "(?:[T ]" +
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
92 ")?" +
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;
139 protected short day;
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;
155 * <p>
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.
159 * </p>
161 public AcalDateTime() {
162 epoch = (System.currentTimeMillis() + TimeZone.getDefault().getOffset(System.currentTimeMillis())) / 1000;
163 if ( Constants.debugDateTime ) checkEpoch();
168 * <p>
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.
171 * </p>
172 * @param millisecondsSinceEpoch
173 * @return
175 public static AcalDateTime fromMillis(long millisecondsSinceEpoch) {
176 AcalDateTime ret = new AcalDateTime();
177 ret.epoch = millisecondsSinceEpoch / 1000;
178 if ( Constants.debugDateTime ) ret.checkEpoch();
179 return ret;
184 * <p>
185 * Return a localised time which will represent the specified milliseconds from Epoch.
186 * </p>
187 * @param millisecondsSinceEpoch
188 * @return
190 public static AcalDateTime localTimeFromMillis(long millisecondsSinceEpoch, boolean fromFloating) {
191 AcalDateTime ret = fromMillis(millisecondsSinceEpoch);
192 if ( fromFloating ) {
193 ret.tz = UTC;
194 ret.tzName = UTC.getID();
195 ret.setTimeZone(TimeZone.getDefault().getID());
197 else
198 ret.shiftTimeZone(TimeZone.getDefault().getID());
199 return ret;
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)
211 * @param tz
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();
215 year = (short) yy;
216 if ( mm < 1 || mm > 12 ) throw new IllegalArgumentException();
217 month = (short) mm;
218 if ( dd < 1 || dd > monthDays(yy,mm) ) throw new IllegalArgumentException();
219 day = (short) dd;
220 if ( hh < 0 || hh > 23 )
221 throw new IllegalArgumentException();
222 hour = (short) hh;
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;
241 try {
242 return AcalDateTime.fromIcalendar(prop.getValue(),prop.getParam("VALUE"),prop.getParam("TZID"));
244 catch ( NullPointerException e ) {}
245 catch ( IllegalArgumentException e ) {}
246 return null;
250 * Returns the number of days in a year/month pair
251 * @param year
252 * @param month
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
264 * @param year
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;
269 return 0;
274 * <p>
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.
278 * </p>
279 * @param year
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) {
283 if ( year < 1972 ) {
284 year = 1999 - year;
285 return 7 - ((year / 4) - (year/100) + (year/400));
287 year -= 1969;
288 if ( year < 130 ) return (year / 4);
290 year -= 32;
291 return 8 + ((year / 4) - (year / 100) + (year / 400));
296 * <p>
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
299 * of the ISO format.
300 * </p>
301 * <p>
302 * Strings that we can handle look like this kind of thing:
303 * </p>
304 * <ul>
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>
309 * <li>20011215</li>
310 * </ul>
312 * @param datestring
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));
330 int hour = 0;
331 int minute = 0;
332 int second = 0;
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();
359 return newDateTime;
364 * Simply write the named timezone to the current object making no attempt to adjust
365 * the validity of the current date / time information.
366 * @param tzName
368 private void overwriteTimeZone( String newTzName ) {
369 if ( newTzName == null || newTzName.equals("")) {
370 tzName = null;
371 tz = null;
372 return;
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() );
386 return result;
391 * <p>
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.
394 * </p>
395 * <p>
396 * It is expected that this will be called something like:<br/>
397 * <pre>
398 * new AcalDateTime = AcalDateTime.fromProperty( dateProperty.Value(), dateProperty.getParam("VALUE"), dateProperty.getParam("TZID") );
399 * </pre>
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.
402 * </p>
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();
418 return result;
423 * <p>
424 * Rerurn the year of this date, sometime after 1582.
425 * </p>
426 * @return year
428 public short getYear() {
429 if ( year == YEAR_NOT_SET ) calculateDateTime();
430 return year;
435 * <p>
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.
438 * </p>
439 * @param yy
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;
448 year = (short) yy;
449 epoch = EPOCH_NOT_SET;
450 return true;
454 * <p>
455 * Rerurn the month of this date, 1 to 12
456 * </p>
457 * @return month
459 public short getMonth() {
460 if ( year == YEAR_NOT_SET ) calculateDateTime();
461 return month;
466 * <p>
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.
469 * </p>
470 * @param mm
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;
479 month = (short) mm;
480 epoch = EPOCH_NOT_SET;
481 return true;
486 * <p>
487 * Rerurn the day in month of this date, sometime after 1582.
488 * </p>
489 * @return year
491 public short getMonthDay() {
492 if ( year == YEAR_NOT_SET ) calculateDateTime();
493 return day;
498 * <p>
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.
501 * </p>
502 * @return
504 public short getMonthWeek() {
505 return (short) (1 + (getMonthDay() / 7));
510 * <p>
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.
514 * </p>
515 * @param monthDay
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;
527 return true;
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;
553 return this;
558 * <p>
559 * Returns the day of year. January the first is 1
560 * </p>
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]);
571 * <p>
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.
575 * </p>
576 * @param yearDay
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);
585 if ( yearDay < 0 ) {
586 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
587 // Log.v(TAG,"Got negative yearDay " + yearDay + " Will use: " + (yearDay + daysInYear + 1) );
588 yearDay += daysInYear;
589 yearDay++;
591 if ( yearDay < 1 || yearDay > daysInYear ) return false;
593 privateSetYearDay((short) yearDay, (short) daysInYear);
595 epoch = EPOCH_NOT_SET;
597 return true;
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 ) {
606 month = JANUARY;
607 day = yearDay;
609 else {
610 month = FEBRUARY;
611 day = (short) (yearDay - 31);
614 else {
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); }
625 else {
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); }
631 else {
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); }
637 else {
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
650 * @return
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.
659 return (long) (
660 ((year - 1970) * DAYS_IN_YEAR)
661 + epochLeapDays(year)
662 + ( getYearDay() - 1 )
668 * Set the day as numbers of days since Jan 1st 1970
669 * @return
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();
675 year = YEAR_NOT_SET;
680 * Get the day of week.
681 * @return
683 public short getWeekDay() {
684 return (short) ((getEpochDay() - 4) % 7);
689 * <p>
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.
693 * </p>
694 * @param weekDay
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 );
712 * <p>
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).
716 * </p>
717 * <p>
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.
723 * </p>
724 * <p>
725 * See RFC5545 section 3.3.10, page 41.
726 * </p>
727 * @return
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;
743 * <p>
744 * Set the week number, from 0 to 53, where 0 is a week containing 1st January but not containing
745 * the 4th.
746 * </p>
747 * @see getYearWeek
748 * @param weekNo
749 * @return
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 );
760 * <p>
761 * Rerurn the hour of this date.
762 * </p>
763 * @return hour
765 public short getHour() {
766 if ( year == YEAR_NOT_SET ) calculateDateTime();
767 return hour;
772 * <p>
773 * Try to set the hour for this date. Will throw an exception if the hour is not valid.
774 * </p>
775 * @param newHour
776 * @return true.
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();
786 return this;
791 * <p>
792 * Rerurn the minute of this date.
793 * </p>
794 * @return minute
796 public short getMinute() {
797 if ( year == YEAR_NOT_SET ) calculateDateTime();
798 return minute;
803 * <p>
804 * Try to set the minute for this date. Will throw an exception if the minute is not valid.
805 * </p>
806 * @param newMinute
807 * @return true.
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();
817 return this;
822 * <p>
823 * Rerurn the second of this date.
824 * </p>
825 * @return second
827 public short getSecond() {
828 if ( year == YEAR_NOT_SET ) calculateDateTime();
829 return second;
834 * <p>
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
837 * leap second.
838 * </p>
839 * @param newSecond
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();
850 return this;
855 * <p>
856 * Get the second for this date, from 0 to 86401.
857 * </p>
858 * @return 0 to 86401
860 public int getDaySecond( ) {
861 if ( year == YEAR_NOT_SET ) calculateDateTime();
862 return (hour * SECONDS_IN_HOUR) + (minute * 60) + second;
867 * <p>
868 * Try to set the second for this date. Will throw an exception if it's outside the
869 * range 0 - 86399.
870 * </p>
871 * @param newSecond
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);
880 newSecond %= 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();
891 return this;
896 * <p>
897 * Get the timezone, which may be null.
898 * </p>
899 * @return timezone object or null
901 public TimeZone getTimeZone() {
902 return tz;
907 * <p>
908 * Get the timezone ID, which may be null, but which is hopefully an Olson name
909 * </p>
910 * @return timezone name
912 public String getTimeZoneId() {
913 if ( tz == null ) return null;
914 return tzName;
919 * <p>
920 * Set the timezone for this date, keeping the date & time constant.
921 * </p>
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;
929 return this;
934 * <p>
935 * Set the timezone for this date, shifting the clock time to keep the UTC epoch constant.
936 * </p>
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);
943 year = YEAR_NOT_SET;
944 if ( Constants.debugDateTime ) calculateDateTime();
945 return this;
950 * <p>
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.
954 * </p>
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.")));
962 // }
963 if ( isFloating() )
964 return setTimeZone(newTimeZone);
965 else
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() {
975 return 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) {
985 isDate = newValue;
986 return this;
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
994 * libraries.
995 * @return milliseconds since epoch, without leap seconds.
997 public long getMillis() {
998 return (getEpoch() * 1000L);
1003 * <p>
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.
1007 * </p>
1008 * @param milliSeconds
1009 * @return this, for chaining.
1011 public AcalDateTime setMillis( long milliSeconds ) {
1012 setEpoch( Math.round(milliSeconds / 1000.0) );
1013 return this;
1018 * Returns a number of seconds from epoch, including leap seconds (up to 2008-12-31)
1019 * @return
1021 public long getEpoch() {
1022 if ( epoch == EPOCH_NOT_SET ) calculateEpoch();
1023 return epoch;
1028 * Set the current time to the given seconds from epoch, which we assume includes leap
1029 * seconds.
1030 * @param newEpoch, the new epoch time to set
1031 * @return this, for chaining
1033 public synchronized AcalDateTime setEpoch(long newEpoch) {
1034 epoch = 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();
1039 return this;
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;
1062 * <p>
1063 * Get some field from the date.
1064 * </p>
1065 * @param whatToGet
1066 * @return
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;
1137 epoch -= offset;
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;
1159 nDays -= 1;
1162 year = 1970;
1163 // TODO: Work out a quicker way. We should be able to do this with
1164 // arithmetic directly
1165 short daysInYear = 0;
1166 if ( nDays > 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;
1172 year++;
1175 else {
1176 if ( nDays < - FORTY_YEARS ) { nDays += FORTY_YEARS; year -= 40; }
1177 if ( nDays < - TWENTY_YEARS ) { nDays += TWENTY_YEARS; year -= 20; }
1178 while( nDays < 0 ) {
1179 year--;
1180 daysInYear = (short) (DAYS_IN_YEAR + leapDay(year));
1181 nDays += daysInYear;
1185 hour = 0;
1186 minute = 0;
1187 second = 0;
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);
1195 localiseToZone();
1196 // checkEpoch();
1197 if ( Constants.debugDateTime ) checkSanity();
1201 private void checkEpoch() {
1202 if ( year != YEAR_NOT_SET && epoch != EPOCH_NOT_SET ) {
1203 long saveEpoch = epoch;
1204 calculateEpoch();
1205 if ( saveEpoch != epoch ) {
1206 try {
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))));
1238 * <p>
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.
1242 * </p>
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);
1251 fixupTimeFields();
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++; }
1262 if ( day < 1 ) {
1263 while( day < 1 ) {
1264 month--;
1265 if ( month < 1 ) {
1266 year--;
1267 month += 12;
1269 day += monthDays(year,month);
1272 else while ( day > monthDays(year,month) ) {
1273 day -= monthDays(year,month);
1274 month++;
1275 if ( month > 12 ) {
1276 month -= 12;
1277 year++;
1284 * <p>
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.
1287 * </p>
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");
1295 ret.append(month);
1296 if ( day < 10 ) ret.append("0");
1297 ret.append(day);
1299 if ( isDate ) return ret.toString();
1301 ret.append("T");
1302 if ( hour < 10 ) ret.append("0");
1303 ret.append(hour);
1304 if ( minute < 10 ) ret.append("0");
1305 ret.append(minute);
1306 if ( second < 10 ) ret.append("0");
1307 ret.append(second);
1309 if ( tz != null && tzName != null && tzName.equals("UTC") ) ret.append('Z');
1311 return ret.toString();
1316 * <p>
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
1319 * a string like:
1320 * <pre>RECURRENCE-ID;VALUE=DATE:20110107</pre>
1321 * or
1322 * <pre>RECURRENCE-ID;TZID=Pacific/Auckland:20110107T162347</pre>
1323 * etc, as appropriate for the DateTime value.
1324 * </p>
1325 * <p>
1326 * The iCalendar property string is <em>not</em> wrapped at 75 octets. You will need to
1327 * do that yourself... :-)
1328 * </p>
1329 * @param name
1330 * @return a string which is formatted like an iCalendar property.
1332 public String toPropertyString( PropertyName name ) {
1333 StringBuilder ret = new StringBuilder(name.toString());
1334 if ( isDate ) {
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=");
1340 ret.append(tzName);
1342 ret.append(":");
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)) );
1350 ret.append(" - ");
1351 ret.append(fmtIcal());
1352 if ( tz != null && !tzName.equals("UTC") ) {
1353 ret.append(" ");
1354 ret.append(tzName);
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.
1362 * @param another
1363 * @return -1, 1 or 0
1365 @Override
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));
1375 @Override
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.
1396 * @param another
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.
1408 * @return
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();
1417 AcalDateTime c;
1418 if ( year == YEAR_NOT_SET ) {
1419 c = new AcalDateTime();
1420 c.year = YEAR_NOT_SET;
1422 else {
1423 calculateEpoch();
1424 try {
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();
1431 c.year = year;
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;
1441 c.isDate = isDate;
1443 c.epoch = epoch;
1444 c.tzName = tzName;
1445 c.tz = tz;
1446 if ( Constants.debugDateTime ) c.checkEpoch();
1447 return c;
1452 * Adds (or subtracts) a number of seconds from this AcalDateTime
1453 * @param delta
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 ) {
1464 epoch += 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);
1469 fixupTimeFields();
1470 return;
1473 calculateEpoch();
1476 // if ( Constants.debugDateTime && Constants.LOG_VERBOSE )
1477 // Log.v(TAG,"Adding "+delta+" to "+epoch);
1478 epoch += delta;
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();
1483 return this;
1488 * Adds an integer number of days to this AcalDateTime. And like all integers 'days'
1489 * could be negative, and that's OK too.
1490 * @param days
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();
1507 return this;
1509 tmpInt = this.getYearDay() + days;
1510 if ( tmpInt > 0 && tmpInt <= (DAYS_IN_YEAR + leapDay(year)) ) {
1511 setYearDay(tmpInt);
1512 return this;
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;
1519 if ( tz != null ) {
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;
1526 epoch += tmpInt;
1529 if ( Constants.debugDateTime ) checkEpoch();
1530 return this;
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.
1537 * @param c
1538 * @param days
1539 * @return
1541 public static AcalDateTime addDays(AcalDateTime c, int days) {
1542 AcalDateTime r = (AcalDateTime) c.clone();
1543 r.addDays(days);
1544 return r;
1548 public AcalDateTime addMonths(int months) {
1549 if ( Constants.debugDateTime ) checkEpoch();
1550 if ( year == YEAR_NOT_SET ) calculateDateTime();
1551 month--;
1552 month += months;
1553 year += month / 12;
1554 if ( month < 0 ) year--;
1555 month %= 12;
1556 month++;
1557 if ( day > monthDays(year,month) ) day = (short) monthDays(year,month);
1558 epoch = EPOCH_NOT_SET;
1559 return this;
1563 public boolean isFloating() {
1564 return tz == null;
1568 public AcalDuration getDurationTo(AcalDateTime end) {
1569 AcalDuration ret = new AcalDuration();
1570 if ( end == null ) {
1571 if ( isDate ) ret.setDuration(1, 0);
1573 else {
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()));
1583 return ret;
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);
1591 return this;
1595 public static AcalDateTime addDuration(AcalDateTime start, AcalDuration relativeTime) {
1596 if ( Constants.debugDateTime ) start.checkEpoch();
1597 AcalDateTime newTime = start.clone();
1598 newTime.addDuration(relativeTime);
1599 return newTime;
1604 * We do it this way to more easily get a localised month name.
1605 * @param month
1606 * @return
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);
1624 * <p>
1625 * Works out what suffix a date should have. English only.
1626 * </p>
1628 * @param num
1629 * @return
1631 public static String getSuffix(int num) {
1632 if (num % 100 > 3 && num % 100 < 21) return "th";
1633 switch (num % 10) {
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();
1654 weekStart = wDay;
1655 return this;
1658 public static class AcalDateTimeSorter implements Comparator<AcalDateTime> {
1659 @Override
1660 public int compare(AcalDateTime arg0, AcalDateTime arg1) {
1661 if ( arg0.before(arg1) ) return -1;
1662 else if ( arg0.after(arg1)) return 1;
1663 else return 0;
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;
1675 return dt;
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];
1688 @Override
1689 public int describeContents() {
1690 return 0;
1693 @Override
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);
1708 return ret;
1712 public AcalProperty asProperty(PropertyName pName) {
1713 return asProperty(pName.toString());