Data Type Java

/*
 * Copyright (C) 2007 The Android Open Source Project
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
import java.util.Calendar;
/**
 * Encapsulates a truck-load of commonly used date functions.
 * 
 * @author barclay
 */
public final class DateUtil {
  private Calendar mCal;
  private DateUtil.Period mSpan;
  private int mSpanOffset = 0;
  private boolean mUnitChange = false;
  /** Number of milliseconds in a second */
  public static final long SECOND_MS = 1000;
  /** Number of milliseconds in a minute */
  public static final long MINUTE_MS = SECOND_MS * 60;
  /** Number of milliseconds in an hour */
  public static final long HOUR_MS = MINUTE_MS * 60;
  /** Number of milliseconds in a morning or evening (1/2 day) */
  public static final long AMPM_MS = HOUR_MS * 12;
  /** Number of milliseconds in a day */
  public static final long DAY_MS = HOUR_MS * 24;
  /** Number of milliseconds in a week */
  public static final long WEEK_MS = DAY_MS * 7;
  /** Number of milliseconds in a year */
  public static final long YEAR_MS = WEEK_MS * 52;
  /** Number of milliseconds in a quarter (as defined by 1/4 of a year) */
  public static final long QUARTER_MS = WEEK_MS * 13;
  /** Number of milliseconds in a month (as defined by 1/12 of a year) */
  public static final long MONTH_MS = YEAR_MS / 12;
  /**
   * Encapsulation of a date broken down by both milliseconds since epoch (as
   * defined by the system), and year, month, day, hour, minute, and second. The
   * reason for storing both is essentially to cache information and glue the
   * variable together into a single item. Purely convenience.
   * 
   * @author barclay
   */
  public static class DateItem {
    public int mYear = 0;
    public int mMonth = 0;
    public int mDay = 0;
    public int mHour = 0;
    public int mMinute = 0;
    public int mSecond = 0;
    public long mMillis = 0;
    /**
     * Set all the fields of the DateItem to the date/time represented by the
     * current value of the Calendar passed in.
     * 
     * @param c
     *          The Calendar that's the source data.
     */
    public void setTo(Calendar c) {
      mYear = c.get(Calendar.YEAR);
      mMonth = c.get(Calendar.MONTH);
      mDay = c.get(Calendar.DAY_OF_MONTH);
      mHour = c.get(Calendar.HOUR_OF_DAY);
      mMinute = c.get(Calendar.MINUTE);
      mSecond = c.get(Calendar.SECOND);
      mMillis = c.getTimeInMillis();
    }
    /**
     * Compares all the fields of the DateItem to another DateItem. All fields
     * are compared, instead of just the millisecond field, in the event that
     * all the fields are not in sync for some reason.
     * 
     * @param other
     * @return true if the two DateItems are equal in all fields, else false.
     */
    public boolean isEqual(DateItem other) {
      if (this.mYear == other.mYear && this.mMonth == other.mMonth
          && this.mDay == other.mDay && this.mHour == other.mHour
          && this.mMinute == other.mMinute && this.mSecond == other.mSecond
          && this.mMillis == other.mMillis)
        return true;
      return false;
    }
  }
  private DateItem mBase;
  private DateItem mCursor;
  /**
   * Code shoulder reference these Periods when refering to spans of time
   * instead of Calendar.*, as Calendar doesn't support the notion of a strict
   * QUARTER, and we use WEEK slightly differently.
   * 
   * @author barclay
   */
  public enum Period {
    MINUTE, HOUR, AMPM, DAY, WEEK, MONTH, QUARTER, YEAR
  }
  public Period[] PERIODS = { Period.MINUTE, Period.HOUR, Period.AMPM,
      Period.DAY, Period.WEEK, Period.MONTH, Period.QUARTER, Period.YEAR };
  public static final String[] MONTHS = { "Jan", "Feb", "Mar", "Apr", "May",
      "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
  public static final String[] DAYS = { "Sun", "M", "Tu", "W", "Th", "F",
      "Sat", };
  public static final String[] QUARTERS = { "Q1", "Q2", "Q3", "Q4", };
  /**
   * Constructor. Instantiates a Calendar member variable to prevent having to
   * continually fetch an instance, which is moderately expensive, and create
   * cursor used to walk a timeline.
   */
  public DateUtil() {
    mCal = Calendar.getInstance();
    mBase = new DateItem();
    mCursor = new DateItem();
  }
  /**
   * Returns the internal Calendar object in it's current state.
   * 
   * @return The Calendar.
   */
  public Calendar getCalendar() {
    return mCal;
  }
  /**
   * Used when going to stride along a timeline. This sets the start time of the
   * walk.
   * 
   * @param ms
   *          The start time in milliseconds since epoch.
   */
  public void setBaseTime(long ms) {
    mBase.mMillis = ms;
    millisToComponent(mBase);
    copyDate(mBase, mCursor);
  }
  /**
   * Returns the milliseconds until the next Period, as based on the difference
   * between the current cursor and the Period. If the current cursor is at the
   * start of a Period, ignoring milliseconds, 0 is returned.
   * 
   * @param u
   * @return milliseconds until next period.
   */
  public long msToNextPeriod(Period u) {
    long ms = 0;
    switch (u) {
      case YEAR:
        mCal.set(mCursor.mYear + 1, 0, 0, 0, 0, 0);
        ms = mCal.getTimeInMillis() - mCursor.mMillis;
        if (ms == YEAR_MS)
          ms = 0;
        break;
      case QUARTER:
        if (mCursor.mMonth >= 9)
          mCal.set(mCursor.mYear + 1, 0, 0, 0, 0, 0);
        else if (mCursor.mMonth >= 6)
          mCal.set(mCursor.mYear, 9, 0, 0, 0, 0);
        else if (mCursor.mMonth >= 3)
          mCal.set(mCursor.mYear, 6, 0, 0, 0, 0);
        else
          mCal.set(mCursor.mYear, 3, 0, 0, 0, 0);
        ms = mCal.getTimeInMillis() - mCursor.mMillis;
        if (ms == QUARTER_MS)
          ms = 0;
        break;
      case MONTH:
        if (mCursor.mMonth == 11)
          mCal.set(mCursor.mYear + 1, 0, 0, 0, 0, 0);
        else
          mCal.set(mCursor.mYear, mCursor.mMonth + 1, 0, 0, 0, 0);
        ms = mCal.getTimeInMillis() - mCursor.mMillis;
        if (ms == MONTH_MS)
          ms = 0;
        break;
      case WEEK:
        mCal.setTimeInMillis(mCursor.mMillis);
        int first = mCal.getFirstDayOfWeek();
        mCal.add(Calendar.WEEK_OF_YEAR, 1);
        mCal.set(Calendar.DAY_OF_WEEK, first);
        mCal.set(Calendar.HOUR_OF_DAY, 0);
        mCal.set(Calendar.MINUTE, 0);
        mCal.set(Calendar.SECOND, 0);
        mCal.set(Calendar.MILLISECOND, 0);
        ms = mCal.getTimeInMillis() - mCursor.mMillis;
        if (ms == WEEK_MS)
          ms = 0;
        break;
      case DAY:
        if (mCursor.mMinute == 0 && mCursor.mHour == 0)
          return 0;
        ms = ((60 - mCursor.mMinute) + (60 * (24 - mCursor.mHour))) * MINUTE_MS;
        break;
      case AMPM:
        if (mCursor.mMinute == 0 && (mCursor.mHour == 0 || mCursor.mHour == 12))
          return 0;
        ms = ((60 - mCursor.mMinute) + (60 * (24 - (mCursor.mHour % 12))))
            * MINUTE_MS;
        break;
      case HOUR:
        if (mCursor.mMinute == 0)
          return 0;
        ms = (60 - mCursor.mMinute) * MINUTE_MS;
        break;
      case MINUTE:
      default:
        if (mCursor.mSecond == 0)
          return 0;
        ms = (60 - mCursor.mSecond) * SECOND_MS;
        break;
    }
    return ms;
  }
  /**
   * Sets an offset of the internal marker recording the class of time spanned
   * (as a Period). This offset is indexes into the Period enum, e.g., if the
   * span is calculated to be Period.YEAR with an offset of 0, then it will be
   * calculated to be Period.MONTH with an offset of -2.
   * 
   * @param milliStart
   *          The milliseconds since epoch in start time, inclusive.
   * @param milliEnd
   *          The milliseconds since epoch in end time, inclusive.
   */
  public void setSpanOffset(int offset) {
    mSpanOffset = offset;
  }
  /**
   * Sets the internal marker recording the class of time spanned (as a Period)
   * for the range of time specified. This is used to determine how to generate
   * labels while striding through time. If milliStart == milliEnd, the span
   * will be set to the smallest known span.
   * 
   * @param milliStart
   *          The milliseconds since epoch in start time, inclusive.
   * @param milliEnd
   *          The milliseconds since epoch in end time, inclusive.
   */
  public void setSpan(long milliStart, long milliEnd) {
    int index = 0;
    long range = milliEnd - milliStart;
    if (range == 0)
      range = 1;
    if (range < 0)
      range = -range;
    if (range > (long) (DateUtil.YEAR_MS * 3)) {
      index = DateUtil.Period.YEAR.ordinal();
    } else if (range > (long) (DateUtil.QUARTER_MS * 6)) {
      index = DateUtil.Period.QUARTER.ordinal();
    } else if (range > (long) (DateUtil.MONTH_MS * 6)) {
      index = DateUtil.Period.MONTH.ordinal();
    } else if (range > (long) (DateUtil.WEEK_MS * 4)) {
      index = DateUtil.Period.WEEK.ordinal();
    } else if (range > (long) (DateUtil.DAY_MS * 5)) {
      index = DateUtil.Period.DAY.ordinal();
    } else if (range > (long) (DateUtil.HOUR_MS * 24)) {
      index = DateUtil.Period.AMPM.ordinal();
    } else if (range > (long) (DateUtil.HOUR_MS * 5)) {
      index = DateUtil.Period.HOUR.ordinal();
    } else {
      index = DateUtil.Period.MINUTE.ordinal();
    }
    index += mSpanOffset;
    if (index < 0)
      index = 0;
    else if (index >= PERIODS.length)
      index = PERIODS.length - 1;
    mSpan = PERIODS[index];
    return;
  }
  /**
   * Returns the span calculated by {@link #setSpan(long, long)}
   * 
   * @return The span as a DateUtil.Period
   * @see DateUtil#Period
   */
  public DateUtil.Period getSpan() {
    return mSpan;
  }
  /**
   * Returns the selected Calendar.* field of the time under the current cursor
   * when striding.
   * 
   * @param p
   *          The Period in which to format the output.
   * @return The field datum.
   */
  public int get(int field) {
    return mCal.get(field);
  }
  /**
   * Returns an array of two strings yielding a textual representation of the
   * time under the current cursor when striding. Neither string will be null,
   * but either may be the empty ("") string. Typically, the second string will
   * be empty rather than the first, and will contain additional information
   * about the label, such as the the month when the days roll over into the
   * next month, or the day of the week. This method sets an internal marker
   * recording if current label has rolled past a period boundary, such as from
   * one week to the next or one year to the next, which is queryable via
   * {@link #isUnitChanged()}
   * 
   * @param p
   *          The Period in which to format the output.
   * @return String[2], containing two description strings of the date/time. The
   *         first string will be withing the Period p, and the
   *         second is typically auxiliary information.
   */
  public String[] getLabel(Period p) {
    String[] strings = new String[2];
    int minute;
    int hour;
    int day;
    int month;
    int year;
    int dow;
    mUnitChange = false;
    switch (p) {
      case YEAR:
        strings[0] = "" + mCal.get(Calendar.YEAR);
        strings[1] = "";
        break;
      case QUARTER:
        year = mCal.get(Calendar.YEAR);
        month = mCal.get(Calendar.MONTH);
        if (month >= 9)
          strings[0] = QUARTERS[3];
        else if (month >= 6)
          strings[0] = QUARTERS[2];
        else if (month >= 3)
          strings[0] = QUARTERS[1];
        else
          strings[0] = QUARTERS[0];
        strings[1] = "";
        if (year != mBase.mYear) {
          strings[1] = "" + mCal.get(Calendar.YEAR);
          mUnitChange = true;
        }
        break;
      case MONTH:
        year = mCal.get(Calendar.YEAR);
        month = mCal.get(Calendar.MONTH);
        strings[0] = MONTHS[month];
        if (year != mBase.mYear) {
          strings[1] = "" + mCal.get(Calendar.YEAR);
          mUnitChange = true;
        } else {
          strings[1] = "";
        }
        break;
      case WEEK:
      case DAY:
        month = mCal.get(Calendar.MONTH);
        day = mCal.get(Calendar.DAY_OF_MONTH);
        strings[0] = "" + day;
        if (month != mBase.mMonth) {
          strings[1] = MONTHS[month];
          mUnitChange = true;
        } else {
          dow = mCal.get(Calendar.DAY_OF_WEEK);
          strings[1] = DAYS[dow - 1];
          if (dow == 1)
            mUnitChange = true;
        }
        break;
      case AMPM:
      case HOUR:
        day = mCal.get(Calendar.DAY_OF_MONTH);
        hour = mCal.get(Calendar.HOUR_OF_DAY);
        if (hour == 0) {
          strings[0] = "12a";
          strings[1] = "midnight";
        } else if (hour == 12) {
          strings[0] = "12p";
          strings[1] = "noon";
        } else if (hour > 11) {
          strings[0] = (hour - 12) + "p";
          strings[1] = "";
        } else {
          strings[0] = hour + "a";
          strings[1] = "";
        }
        if (day != mBase.mDay) {
          dow = mCal.get(Calendar.DAY_OF_WEEK);
          strings[0] = mCal.get(Calendar.MONTH) + 1 + "/" + day;
          strings[1] = DAYS[dow - 1];
          mUnitChange = true;
        }
        break;
      case MINUTE:
      default:
        minute = mCal.get(Calendar.MINUTE);
        hour = mCal.get(Calendar.HOUR_OF_DAY);
        strings[0] = l2pad(minute);
        strings[1] = "";
        if (hour != mBase.mHour) {
          if (hour == 0) {
            day = mCal.get(Calendar.DAY_OF_MONTH);
            dow = mCal.get(Calendar.DAY_OF_WEEK);
            strings[0] = mCal.get(Calendar.MONTH) + 1 + "/" + day;
            strings[1] = DAYS[dow - 1];
          } else if (hour == 12) {
            strings[0] = "12";
            strings[1] = "noon";
          } else if (hour > 11) {
            strings[0] = (hour - 12) + "p";
          } else {
            strings[0] = hour + "a";
          }
          mUnitChange = true;
        } else
          break;
    }
    return strings;
  }
  /**
   * Advances the internal cursor milliseconds in time.
   * 
   * @param milliseconds
   *          The number of milliseconds to advance.
   */
  public void advanceInMs(long milliseconds) {
    copyDate(mCursor, mBase);
    mCal.setTimeInMillis(mCursor.mMillis);
    mCal.add(Calendar.MILLISECOND, (int) milliseconds);
    mCursor.mMillis = mCal.getTimeInMillis();
  }
  /**
   * Advances the internal cursor step units of Period
   * p in time. Note that for MONTH and QUARTER, this works out to
   * 1 and 3 months respectively, as defined by the Calendar class and based on
   * the current cursor, not precisely MONTH_MS or QUARTER_MS milliseconds.
   * 
   * @param p
   *          The DateUtil.Period unit.
   * @param step
   *          The number of Period units to advance.
   */
  public void advance(Period p, int step) {
    copyDate(mCursor, mBase);
    switch (p) {
      case YEAR:
        mCal.setTimeInMillis(mCursor.mMillis);
        mCal.add(Calendar.YEAR, step);
        break;
      case QUARTER:
        mCal.setTimeInMillis(mCursor.mMillis);
        mCal.add(Calendar.MONTH, step * 3);
        break;
      case MONTH:
        mCal.setTimeInMillis(mCursor.mMillis);
        mCal.add(Calendar.MONTH, step);
        break;
      case WEEK:
        mCal.setTimeInMillis(mCursor.mMillis);
        mCal.add(Calendar.WEEK_OF_YEAR, step);
        break;
      case DAY:
        mCal.setTimeInMillis(mCursor.mMillis);
        mCal.add(Calendar.DAY_OF_MONTH, step);
        break;
      case HOUR:
        mCal.setTimeInMillis(mCursor.mMillis);
        mCal.add(Calendar.HOUR_OF_DAY, step);
        break;
      case MINUTE:
      default:
        mCal.setTimeInMillis(mCursor.mMillis);
        mCal.add(Calendar.MINUTE, step);
        break;
    }
    mCursor.mMillis = mCal.getTimeInMillis();
    millisToComponent(mCursor);
    return;
  }
  /**
   * Return whether or not the last getLabel() noted a rollover from one period
   * to another, as determine by the Period passed to getLabel().
   * 
   * @return boolean
   * @see #getLabel(Period)
   */
  public boolean isUnitChanged() {
    return mUnitChange;
  }
  /**
   * Returns the average number of milliseconds in a Period. These are constant.
   * 
   * @param u
   * @return the number of millseconds
   * @see #YEAR_MS
   * @see #QUARTER_MS
   * @see #MONTH_MS
   * @see #DAY_MS
   * @see #HOUR_MS
   * @see #MINUTE_MS
   */
  public long msInPeriod(Period u) {
    long ms = 0;
    switch (u) {
      case YEAR:
        ms = YEAR_MS;
        break;
      case QUARTER:
        ms = QUARTER_MS;
        break;
      case MONTH:
        ms = MONTH_MS;
        break;
      case WEEK:
        ms = WEEK_MS;
        break;
      case DAY:
        ms = DAY_MS;
        break;
      case AMPM:
        ms = AMPM_MS;
        break;
      case HOUR:
        ms = HOUR_MS;
        break;
      case MINUTE:
      default:
        ms = MINUTE_MS;
        break;
    }
    return ms;
  }
  /**
   * Some external entities still use Calendar.* fields to do some of their own
   * date calculations, so this provides a mapping from DateUtil.*_MS to the
   * closest Calendar.* field. Note that if the milliseconds is not one of the
   * DateUtil constants, the smallest known field will be returned.
   * 
   * @param millis
   *          The DateUtil.*_MS field to map from.
   * @return The int representing the closest Calendar.* field.
   */
  public static int mapLongToCal(long millis) {
    if (millis == YEAR_MS)
      return Calendar.YEAR;
    else if (millis == QUARTER_MS)
      return Calendar.MONTH; // There is no Calendar.QUARTER, return MONTH
    else if (millis == MONTH_MS)
      return Calendar.MONTH;
    else if (millis == WEEK_MS)
      return Calendar.WEEK_OF_YEAR;
    else if (millis == DAY_MS)
      return Calendar.DAY_OF_MONTH;
    else if (millis == AMPM_MS)
      return Calendar.AM_PM;
    else if (millis == HOUR_MS)
      return Calendar.HOUR_OF_DAY;
    return Calendar.MINUTE;
  }
  /**
   * Provide a mapping from number of millisecond (DateUtil.*_MS) to a
   * DateUtil.Period. Note that if the milliseconds is not one of the DateUtil
   * constants, the smallest known field will be returned.
   * 
   * @param millis
   *          The DateUtil.*_MS field to map from.
   * @return The Period enum representing the associated DateUtil.Period.
   */
  public static Period mapLongToPeriod(long millis) {
    if (millis == YEAR_MS)
      return Period.YEAR;
    else if (millis == QUARTER_MS)
      return Period.QUARTER;
    else if (millis == MONTH_MS)
      return Period.MONTH;
    else if (millis == WEEK_MS)
      return Period.WEEK;
    else if (millis == DAY_MS)
      return Period.DAY;
    else if (millis == AMPM_MS)
      return Period.AMPM;
    else if (millis == HOUR_MS)
      return Period.HOUR;
    return Period.MINUTE;
  }
  /**
   * Provide a mapping from a Period to the number of millisecond
   * (DateUtil.*_MS)
   * 
   * @param The
   *          Period enum representing the associated DateUtil.Period.
   * @return A String describing the period..
   */
  public static String mapPeriodToString(Period p) {
    if (p == Period.YEAR)
      return "year";
    if (p == Period.QUARTER)
      return "quarter";
    if (p == Period.MONTH)
      return "month";
    if (p == Period.WEEK)
      return "week";
    if (p == Period.DAY)
      return "day";
    if (p == Period.AMPM)
      return "am/pm";
    if (p == Period.HOUR)
      return "hour";
    return "minute";
  }
  /**
   * Provide a mapping from string to a Period.
   * 
   * @param s
   *          The string to map from. Case insensitive.
   * @return The associated DateUtil.Period
   */
  public static Period mapStringToPeriod(String s) {
    if (s.toLowerCase().equals("year"))
      return Period.YEAR;
    if (s.toLowerCase().equals("quarter"))
      return Period.QUARTER;
    if (s.toLowerCase().equals("month"))
      return Period.MONTH;
    if (s.toLowerCase().equals("week"))
      return Period.WEEK;
    if (s.toLowerCase().equals("day"))
      return Period.DAY;
    if (s.toLowerCase().equals("am/pm"))
      return Period.AMPM;
    if (s.toLowerCase().equals("hour"))
      return Period.HOUR;
    return Period.MINUTE;
  }
  /**
   * Provide a mapping from a Period to the number of millisecond
   * (DateUtil.*_MS)
   * 
   * @param millis
   *          The DateUtil.*_MS field to map from.
   * @param The
   *          Period enum representing the associated DateUtil.Period.
   * @return the DateUtil.*_MS constant representing the number of milliseconds
   *         in the period.
   */
  public static long mapPeriodToLong(Period p) {
    if (p == Period.YEAR)
      return YEAR_MS;
    if (p == Period.QUARTER)
      return QUARTER_MS;
    if (p == Period.MONTH)
      return MONTH_MS;
    if (p == Period.WEEK)
      return WEEK_MS;
    if (p == Period.DAY)
      return DAY_MS;
    if (p == Period.AMPM)
      return AMPM_MS;
    if (p == Period.HOUR)
      return HOUR_MS;
    return MINUTE_MS;
  }
  /**
   * Returns a description of the milliseconds, scaled to the largest unit and
   * rounded to the default number of decimal places, with the associated label
   * (e.g., "years", "weeks", etc.)
   * 
   * @param millis
   *          The milliseconds since epoch to format.
   * @return The descriptive string.
   */
  public static String toString(float millis) {
    if (millis > YEAR_MS) {
      return Round(millis / YEAR_MS) + " years";
    } else if (millis > QUARTER_MS) {
      return Round(millis / QUARTER_MS) + " quarters";
    } else if (millis > MONTH_MS) {
      return Round(millis / MONTH_MS) + " months";
    } else if (millis > WEEK_MS) {
      return Round(millis / WEEK_MS) + " weeks";
    } else if (millis > DAY_MS) {
      return Round(millis / DAY_MS) + " days";
    } else if (millis > HOUR_MS) {
      return Round(millis / HOUR_MS) + " hours";
    } else { // if (millis > MINUTE_MS) {
      return Round(millis / MINUTE_MS) + " minutes";
    }
  }
  /**
   * Returns a description of the square root of the milliseconds, scaled to the
   * largest unit and rounded to the default number of decimal places, with the
   * associated label (e.g., "years", "weeks", etc.). Note this is only used for
   * displaying the variance, as the variance the a squared value, so this tests
   * (millis > (unit^2)) ? and displays the value (millis/(unit^2)). Otherwise
   * it is identical to {@link #toString(float)}.
   * 
   * @param millis
   *          The (squared) milliseconds since epoch to format.
   * @return The descriptive string.
   */
  public static String toStringSquared(float millis) {
    if (millis > (float) YEAR_MS * (float) YEAR_MS) {
      return Round(millis / ((float) YEAR_MS * (float) YEAR_MS))
          + " years";
    } else if (millis > (float) QUARTER_MS * (float) QUARTER_MS) {
      return Round(millis / ((float) QUARTER_MS * (float) QUARTER_MS))
          + " quarters";
    } else if (millis > (float) MONTH_MS * (float) MONTH_MS) {
      return Round(millis / ((float) MONTH_MS * (float) MONTH_MS))
          + " months";
    } else if (millis > (float) WEEK_MS * (float) WEEK_MS) {
      return Round(millis / ((float) WEEK_MS * (float) WEEK_MS))
          + " weeks";
    } else if (millis > (float) DAY_MS * (float) DAY_MS) {
      return Round(millis / ((float) DAY_MS * (float) DAY_MS)) + " days";
    } else if (millis > (float) HOUR_MS * (float) HOUR_MS) {
      return Round(millis / ((float) HOUR_MS * (float) HOUR_MS))
          + " hours";
    } else { // if (millis > MINUTE_MS) {
      return Round(millis / ((float) MINUTE_MS * (float) MINUTE_MS))
          + " minutes";
    }
  }
  /**
   * Default number of decimal places to round to:
   */
  public static final int DECIMAL_PLACES = 2;
  /**
   * Round a float to the default number of decimal places.
   * 
   * @param value
   *          The value to round.
   * @return The rounded value as a float.
   * @see #DECIMAL_PLACES
   */
  public static float Round(float value) {
    return Round(value, DECIMAL_PLACES);
  }
  /**
   * Round a float to the specified number of decimal places.
   * 
   * @param value
   *          The value to round.
   * @param places
   *          The number of decimal points.
   * @return The rounded value as a float.
   */
  public static float Round(float value, int places) {
    float p = (float) Math.pow(10, places);
    value = value * p;
    float tmp = Math.round(value);
    return (float) tmp / p;
  }
  /**
   * Returns the "timestamp" string representation of the time in milliseconds:
   * yyyy/mm/dd HH:MM:SS
   * 
   * @param millis
   *          The milliseconds since epoch to format.
   * @return The timestamp string.
   */
  public static String toTimestamp(long millis) {
    Calendar c = Calendar.getInstance();
    c.setTimeInMillis(millis);
    return DateUtil.toTimestamp(c);
  }
  /**
   * Returns the "short timestamp" string representation of the time in
   * milliseconds: HH:MM:SS
   * 
   * @param millis
   *          The milliseconds since epoch to format.
   * @return The short timestamp string.
   */
  public static String toShortTimestamp(long millis) {
    Calendar c = Calendar.getInstance();
    c.setTimeInMillis(millis);
    return DateUtil.toShortTimestamp(c);
  }
  /**
   * Utility routine for padding zeros on the left side of an integer out to two
   * digits, since string concatenations this small are much more efficient that
   * using String.format("%02d",foo).
   * 
   * @param i
   *          The integer to format.
   * @return A zero-padded string representation of the integer.
   */
  private static String l2pad(int i) {
    if (i < 10)
      return "0" + i;
    return "" + i;
  }
  /**
   * Returns a "timestamp" formated string representing the time:
   * "yyyy/mm/dd HH:MM:SS"
   * 
   * @param d
   *          The DateItem to format.
   * @return The timestamp string.
   */
  public static String toTimestamp(DateItem d) {
    return d.mYear + "/" + l2pad(d.mMonth + 1) + "/" + l2pad(d.mDay) + " "
        + l2pad(d.mHour) + ":" + l2pad(d.mMinute) + ":" + l2pad(d.mSecond);
  }
  /**
   * Returns a "timestamp" formated string representing the time:
   * "yyyy/mm/dd HH:MM:SS"
   * 
   * @param d
   *          The Calendar to format.
   * @return The timestamp string.
   */
  public static String toTimestamp(Calendar cal) {
    return cal.get(Calendar.YEAR) + "/" + l2pad(cal.get(Calendar.MONTH) + 1)
        + "/" + l2pad(cal.get(Calendar.DAY_OF_MONTH)) + " "
        + l2pad(cal.get(Calendar.HOUR_OF_DAY)) + ":"
        + l2pad(cal.get(Calendar.MINUTE)) + ":"
        + l2pad(cal.get(Calendar.SECOND));
  }
  /**
   * Returns a "short timestamp" formated string representing the time:
   * "HH:MM:SS"
   * 
   * @param d
   *          The Calendar to format.
   * @return The timestamp string.
   */
  public static String toShortTimestamp(Calendar cal) {
    return l2pad(cal.get(Calendar.HOUR_OF_DAY)) + ":"
        + l2pad(cal.get(Calendar.MINUTE)) + ":"
        + l2pad(cal.get(Calendar.SECOND));
  }
  /**
   * Returns a (generally) filesystem-safe formated string representing the
   * time: "yyyy-mm-dd_HH.MM.SS"
   * 
   * @param d
   *          The Calendar to format.
   * @return The timestamp string.
   */
  public static String toFSTimestamp(Calendar cal) {
    return cal.get(Calendar.YEAR) + "-" + l2pad(cal.get(Calendar.MONTH) + 1)
        + "-" + l2pad(cal.get(Calendar.DAY_OF_MONTH)) + "_"
        + l2pad(cal.get(Calendar.HOUR_OF_DAY)) + "."
        + l2pad(cal.get(Calendar.MINUTE)) + "."
        + l2pad(cal.get(Calendar.SECOND));
  }
  /**
   * Returns true if the two calendars represent dates that fall in the same
   * year, else false.
   * 
   * @param c1
   *          Calendar one.
   * @param c2
   *          Calendar two.
   * @return boolean.
   */
  public static boolean inSameYear(Calendar c1, Calendar c2) {
    if (c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR))
      return true;
    return false;
  }
  /**
   * Returns true if the two calendars represent dates that fall in the same
   * quarter, else false. A quarter here is defined as the each group of three
   * consecutive months, starting with the month designated as the first month
   * by the Calendar package. Thus, it is not defined as the average number of
   * milliseconds in a quarter, which would be {@link #YEAR_MS}/4.
   * 
   * @param c1
   *          Calendar one.
   * @param c2
   *          Calendar two.
   * @return boolean.
   */
  public static boolean inSameQuarter(Calendar c1, Calendar c2) {
    if (inSameYear(c1, c2)) {
      int m1 = c1.get(Calendar.MONTH);
      int m2 = c2.get(Calendar.MONTH);
      if (m1 >= 9 && m2 >= 9)
        return true;
      if (m1 >= 6 && m1 < 9 && m2 >= 6 && m2 < 9)
        return true;
      if (m1 >= 3 && m1 < 6 && m2 >= 3 && m2 < 6)
        return true;
      if (m1 >= 0 && m1 < 3 && m2 >= 0 && m2 < 3)
        return true;
    }
    return false;
  }
  /**
   * Returns true if the two calendars represent dates that fall in the same
   * month, else false.
   * 
   * @param c1
   *          Calendar one.
   * @param c2
   *          Calendar two.
   * @return boolean.
   */
  public static boolean inSameMonth(Calendar c1, Calendar c2) {
    if (inSameYear(c1, c2)
        && (c1.get(Calendar.MONTH) == c2.get(Calendar.MONTH)))
      return true;
    return false;
  }
  /**
   * Returns true if the two calendars represent dates that fall in the same
   * week, else false. A week here is defined by the Calendar.WEEK_OF_YEAR
   * package. Special provisions have been made to test weeks than may span the
   * end/beginning of a year, and returning true if the two calendars are
   * specifying dates within such a week, despite Calendar.WEEK_OF_YEAR being
   * unequal for the two Calendars.
   * 
   * @param c1
   *          Calendar one.
   * @param c2
   *          Calendar two.
   * @return boolean.
   */
  public static boolean inSameWeek(Calendar c1, Calendar c2) {
    if (inSameYear(c1, c2)
        && (c1.get(Calendar.WEEK_OF_YEAR) == c2.get(Calendar.WEEK_OF_YEAR)))
      return true;
    Calendar tmp;
    if (c1.before(c2)) {
      tmp = c2;
      c2 = c1;
      c1 = tmp;
    }
    int c1week = c1.get(Calendar.WEEK_OF_YEAR);
    int c2week = c1.get(Calendar.WEEK_OF_YEAR);
    if (c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR) + 1) {
      if (c1week == c1.getActualMinimum(Calendar.WEEK_OF_YEAR)
          && c2week == c2.getActualMaximum(Calendar.WEEK_OF_YEAR)) {
        tmp = (Calendar) c2.clone();
        tmp.add(Calendar.DAY_OF_YEAR, 7);
        if (tmp.get(Calendar.WEEK_OF_YEAR) > c1week)
          return true;
      }
    }
    return false;
  }
  /**
   * Returns true if the two calendars represent dates that fall in the same
   * day, else false.
   * 
   * @param c1
   *          Calendar one.
   * @param c2
   *          Calendar two.
   * @return boolean.
   */
  public static boolean inSameDay(Calendar c1, Calendar c2) {
    if (inSameYear(c1, c2)
        && (c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR)))
      return true;
    return false;
  }
  /**
   * Returns true if the two calendars represent dates that fall in the same
   * morning or evening, as defined by [midnight,noon) and [noon,midnight), else
   * false.
   * 
   * @param c1
   *          Calendar one.
   * @param c2
   *          Calendar two.
   * @return boolean.
   */
  public static boolean inSameAMPM(Calendar c1, Calendar c2) {
    if (inSameDay(c1, c2) && (c1.get(Calendar.AM_PM) == c2.get(Calendar.AM_PM)))
      return true;
    return false;
  }
  /**
   * Returns true if the two calendars represent dates that fall in the same
   * hour, else false.
   * 
   * @param c1
   *          Calendar one.
   * @param c2
   *          Calendar two.
   * @return boolean.
   */
  public static boolean inSameHour(Calendar c1, Calendar c2) {
    if (inSameDay(c1, c2)
        && (c1.get(Calendar.HOUR_OF_DAY) == c2.get(Calendar.HOUR_OF_DAY)))
      return true;
    return false;
  }
  /**
   * Returns true if the two calendars represent dates that fall in the same
   * period, else false.
   * 
   * @param aggregationMillis
   *          The period as specified in milliseconds, e.g., DateUtil.YEAR_MS
   * @param c1
   *          Calendar one.
   * @param c2
   *          Calendar two.
   * @return boolean.
   */
  public static boolean inSamePeriod(Calendar c1, Calendar c2,
      long aggregationMillis) {
    if (aggregationMillis == 0)
      return false;
    if ((aggregationMillis == YEAR_MS && inSameYear(c1, c2))
        || (aggregationMillis == QUARTER_MS && inSameQuarter(c1, c2))
        || (aggregationMillis == MONTH_MS && inSameMonth(c1, c2))
        || (aggregationMillis == WEEK_MS && inSameWeek(c1, c2))
        || (aggregationMillis == DAY_MS && inSameDay(c1, c2))
        || (aggregationMillis == AMPM_MS && inSameAMPM(c1, c2))
        || (aggregationMillis == HOUR_MS && inSameHour(c1, c2))) {
      return true;
    }
    return false;
  }
  /**
   * Sets the date/time of the Calendar object to the beginning of the Period by
   * setting all fields smaller than the specified period to the minimum value.
   * 
   * @param c
   *          The calendar to set.
   * @param p
   *          The DateUtil.Period to set.
   */
  public static void setToPeriodStart(Calendar c, Period p) {
    switch (p) {
      case YEAR:
        c.set(Calendar.MONTH, 0);
      case MONTH:
        c.set(Calendar.DAY_OF_MONTH, 1);
      case DAY:
        c.set(Calendar.HOUR_OF_DAY, 0);
      case HOUR:
        c.set(Calendar.MINUTE, 0);
      case MINUTE:
        c.set(Calendar.SECOND, 0);
        c.set(Calendar.MILLISECOND, 0);
        break;
      case WEEK:
        c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());
        c.set(Calendar.HOUR_OF_DAY, 0);
        c.set(Calendar.MINUTE, 0);
        c.set(Calendar.SECOND, 0);
        c.set(Calendar.MILLISECOND, 0);
        break;
      case AMPM:
        if (c.get(Calendar.AM_PM) == Calendar.AM)
          c.set(Calendar.HOUR_OF_DAY, 0);
        else
          c.set(Calendar.HOUR_OF_DAY, 12);
        c.set(Calendar.MINUTE, 0);
        c.set(Calendar.SECOND, 0);
        c.set(Calendar.MILLISECOND, 0);
        break;
      case QUARTER:
        int month = c.get(Calendar.MONTH);
        if (month >= 9)
          c.set(Calendar.MONTH, 9);
        else if (month >= 9)
          c.set(Calendar.MONTH, 6);
        else if (month >= 9)
          c.set(Calendar.MONTH, 3);
        else
          c.set(Calendar.MONTH, 0);
        c.set(Calendar.DAY_OF_MONTH, 0);
        c.set(Calendar.HOUR_OF_DAY, 0);
        c.set(Calendar.MINUTE, 0);
        c.set(Calendar.SECOND, 0);
        c.set(Calendar.MILLISECOND, 0);
        break;
    }
    return;
  }
  /**
   * Utility routine to set each DateTime component field to that specified by
   * the DateItem's millisecond field.
   * 
   * @param d
   *          The DateItem to modify.
   */
  private void millisToComponent(DateItem d) {
    mCal.setTimeInMillis(d.mMillis);
    d.mYear = mCal.get(Calendar.YEAR);
    d.mMonth = mCal.get(Calendar.MONTH);
    d.mDay = mCal.get(Calendar.DAY_OF_MONTH);
    d.mHour = mCal.get(Calendar.HOUR_OF_DAY);
    d.mMinute = mCal.get(Calendar.MINUTE);
  }
  /**
   * Copy all member variable of one DateItem to that of another DateItem.
   * 
   * @param src
   *          The DateItem to copy from.
   * @param dst
   *          The DateItem to copy to.
   */
  private void copyDate(DateItem src, DateItem dst) {
    dst.mYear = src.mYear;
    dst.mMonth = src.mMonth;
    dst.mDay = src.mDay;
    dst.mHour = src.mHour;
    dst.mMinute = src.mMinute;
    dst.mMillis = src.mMillis;
  }
}