org.javarosa.core.model.utils.DateUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.javarosa.core.model.utils.DateUtils.java

Source

/*
 * Copyright (C) 2009 JavaRosa
 *
 * 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.
 */

package org.javarosa.core.model.utils;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.util.MathUtils;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormat;

/**
 * Static utility methods for Dates in j2me
 *
 * @author Clayton Sims
 *
 */

public class DateUtils {
    private static final int MONTH_OFFSET = (1 - Calendar.JANUARY);

    public static final int FORMAT_ISO8601 = 1;
    public static final int FORMAT_HUMAN_READABLE_SHORT = 2;
    public static final int FORMAT_HUMAN_READABLE_DAYS_FROM_TODAY = 5;
    //public static final int FORMAT_HUMAN_READABLE_LONG = 3;
    public static final int FORMAT_TIMESTAMP_SUFFIX = 7;

    /** RFC 822 **/
    public static final int FORMAT_TIMESTAMP_HTTP = 9;

    public static final long DAY_IN_MS = 86400000l;

    public DateUtils() {
        super();
    }

    public static class DateFields {
        public DateFields() {
            year = 1970;
            month = 1;
            day = 1;
            hour = 0;
            minute = 0;
            second = 0;
            secTicks = 0;
            dow = 0;

            //            tzStr = "Z";
            //            tzOffset = 0;
        }

        public int year;
        public int month; //1-12
        public int day; //1-31
        public int hour; //0-23
        public int minute; //0-59
        public int second; //0-59
        public int secTicks; //0-999 (ms)

        /** NOTE: CANNOT BE USED TO SPECIFY A DATE **/
        public int dow; //1-7;

        //        public String tzStr;
        //        public int tzOffset; //s ahead of UTC

        DateFields(int year, int month, int day, int hour, int minute, int second, int secTicks, int dow) {
            this.year = year;
            this.month = month;
            this.day = day;
            this.hour = hour;
            this.minute = minute;
            this.second = second;
            this.secTicks = secTicks;
            this.dow = dow;
        }

        public static DateFields of(int year, int month, int day, int hour, int minute, int second, int secTicks) {
            // The official API returns an ISO 8601 day of week
            // with a range of values from 1 for Monday to 7 for Sunday].
            // TODO migrate dow field to a DayOfWeek type to avoid any possible interpretation errors
            int iso8601Dow = new LocalDateTime(year, month, day, hour, minute, second, secTicks).getDayOfWeek();
            int dow = iso8601Dow == 7 ? 0 : iso8601Dow;
            return new DateFields(year, month, day, hour, minute, second, secTicks, dow);
        }

        public boolean check() {
            return (inRange(month, 1, 12) && inRange(day, 1, daysInMonth(month - MONTH_OFFSET, year))
                    && inRange(hour, 0, 23) && inRange(minute, 0, 59) && inRange(second, 0, 59)
                    && inRange(secTicks, 0, 999));
        }
    }

    public static DateFields getFields(Date d) {
        return getFields(d, null);
    }

    public static DateFields getFields(Date d, String timezone) {
        Calendar cd = Calendar.getInstance();
        cd.setTime(d);
        if (timezone != null) {
            cd.setTimeZone(TimeZone.getTimeZone(timezone));
        }

        DateFields fields = new DateFields();
        fields.year = cd.get(Calendar.YEAR);
        fields.month = cd.get(Calendar.MONTH) + MONTH_OFFSET;
        fields.day = cd.get(Calendar.DAY_OF_MONTH);
        fields.hour = cd.get(Calendar.HOUR_OF_DAY);
        fields.minute = cd.get(Calendar.MINUTE);
        fields.second = cd.get(Calendar.SECOND);
        fields.secTicks = cd.get(Calendar.MILLISECOND);
        fields.dow = cd.get(Calendar.DAY_OF_WEEK);

        return fields;
    }

    public static Date getDate(DateFields f) {
        return getDate(f, null);
    }

    public static Date getDate(DateFields f, String timezone) {
        LocalDateTime ldt = getLocalDateTime(f);
        return timezone == null ? ldt.toDate() : ldt.toDate(TimeZone.getTimeZone(timezone));
    }

    private static LocalDateTime getLocalDateTime(DateFields f) {
        return new LocalDateTime(f.year, f.month, f.day, f.hour, f.minute, f.second, f.secTicks);
    }

    /* ==== FORMATTING DATES/TIMES TO STANDARD STRINGS ==== */

    public static String formatDateTime(Date d, int format) {
        if (d == null) {
            return "";
        }

        DateFields fields = getFields(d, format == FORMAT_TIMESTAMP_HTTP ? "UTC" : null);

        String delim;
        switch (format) {
        case FORMAT_ISO8601:
            delim = "T";
            break;
        case FORMAT_TIMESTAMP_SUFFIX:
            delim = "";
            break;
        case FORMAT_TIMESTAMP_HTTP:
            delim = " ";
            break;
        default:
            delim = " ";
            break;
        }

        return formatDate(fields, format) + delim + formatTime(fields, format);
    }

    public static String formatDate(Date d, int format) {
        return (d == null ? "" : formatDate(getFields(d, format == FORMAT_TIMESTAMP_HTTP ? "UTC" : null), format));
    }

    public static String formatTime(Date d, int format) {
        return (d == null ? "" : formatTime(getFields(d, format == FORMAT_TIMESTAMP_HTTP ? "UTC" : null), format));
    }

    private static String formatDate(DateFields f, int format) {
        switch (format) {
        case FORMAT_ISO8601:
            return formatDateISO8601(f);
        case FORMAT_HUMAN_READABLE_SHORT:
            return formatDateColloquial(f);
        case FORMAT_HUMAN_READABLE_DAYS_FROM_TODAY:
            return formatDaysFromToday(f);
        case FORMAT_TIMESTAMP_SUFFIX:
            return formatDateSuffix(f);
        case FORMAT_TIMESTAMP_HTTP:
            return formatDateHttp(f);
        default:
            return null;
        }
    }

    private static String formatTime(DateFields f, int format) {
        switch (format) {
        case FORMAT_ISO8601:
            return formatTimeISO8601(f);
        case FORMAT_HUMAN_READABLE_SHORT:
            return formatTimeColloquial(f);
        case FORMAT_TIMESTAMP_SUFFIX:
            return formatTimeSuffix(f);
        case FORMAT_TIMESTAMP_HTTP:
            return formatTimeHttp(f);
        default:
            return null;
        }
    }

    /** RFC 822 **/
    private static String formatDateHttp(DateFields f) {
        return format(f, "%a, %d %b %Y");
    }

    /** RFC 822 **/
    private static String formatTimeHttp(DateFields f) {
        return format(f, "%H:%M:%S GMT");
    }

    private static String formatDateISO8601(DateFields f) {
        return f.year + "-" + intPad(f.month, 2) + "-" + intPad(f.day, 2);
    }

    private static String formatDateColloquial(DateFields f) {
        String year = Integer.valueOf(f.year).toString();

        //Normal Date
        if (year.length() == 4) {
            year = year.substring(2, 4);
        }
        //Otherwise we have an old or bizzarre date, don't try to do anything

        return intPad(f.day, 2) + "/" + intPad(f.month, 2) + "/" + year;
    }

    private static String formatDateSuffix(DateFields f) {
        return f.year + intPad(f.month, 2) + intPad(f.day, 2);
    }

    private static String formatTimeISO8601(DateFields f) {
        String time = intPad(f.hour, 2) + ":" + intPad(f.minute, 2) + ":" + intPad(f.second, 2) + "."
                + intPad(f.secTicks, 3);

        //Time Zone ops (1 in the first field corresponds to 'CE' ERA)
        int milliday = ((f.hour * 60 + f.minute) * 60 + f.second) * 1000 + f.secTicks;
        int offset = TimeZone.getDefault().getOffset(1, f.year, f.month - 1, f.day, f.dow, milliday);

        //NOTE: offset is in millis
        if (offset == 0) {
            time += "Z";
        } else {

            //Start with sign
            String offsetSign = offset > 0 ? "+" : "-";

            int value = Math.abs(offset) / 1000 / 60;

            String hrs = intPad(value / 60, 2);
            String mins = ":" + intPad(value % 60, 2);

            time += offsetSign + hrs + mins;
        }
        return time;
    }

    private static String formatTimeColloquial(DateFields f) {
        return intPad(f.hour, 2) + ":" + intPad(f.minute, 2);
    }

    private static String formatTimeSuffix(DateFields f) {
        return intPad(f.hour, 2) + intPad(f.minute, 2) + intPad(f.second, 2);
    }

    public static String format(Date d, String format) {
        return format(getFields(d), format);
    }

    public static String format(DateFields f, String format) {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < format.length(); i++) {
            char c = format.charAt(i);

            if (c == '%') {
                i++;
                if (i >= format.length()) {
                    throw new RuntimeException("date format string ends with %");
                } else {
                    c = format.charAt(i);
                }

                if (c == '%') { //literal '%'
                    sb.append("%");
                } else if (c == 'Y') { //4-digit year
                    sb.append(intPad(f.year, 4));
                } else if (c == 'y') { //2-digit year
                    sb.append(intPad(f.year, 4).substring(2));
                } else if (c == 'm') { //0-padded month
                    sb.append(intPad(f.month, 2));
                } else if (c == 'n') { //numeric month
                    sb.append(f.month);
                } else if (c == 'b') { //short text month
                    sb.append(getLocalDateTime(f).toString(DateTimeFormat.forPattern("MMM")));
                } else if (c == 'd') { //0-padded day of month
                    sb.append(intPad(f.day, 2));
                } else if (c == 'e') { //day of month
                    sb.append(f.day);
                } else if (c == 'H') { //0-padded hour (24-hr time)
                    sb.append(intPad(f.hour, 2));
                } else if (c == 'h') { //hour (24-hr time)
                    sb.append(f.hour);
                } else if (c == 'M') { //0-padded minute
                    sb.append(intPad(f.minute, 2));
                } else if (c == 'S') { //0-padded second
                    sb.append(intPad(f.second, 2));
                } else if (c == '3') { //0-padded millisecond ticks (000-999)
                    sb.append(intPad(f.secTicks, 3));
                } else if (c == 'a') { //Three letter short text day
                    sb.append(getLocalDateTime(f).toString(DateTimeFormat.forPattern("EEE")));
                } else if (c == 'Z' || c == 'A' || c == 'B') {
                    throw new RuntimeException("unsupported escape in date format string [%" + c + "]");
                } else {
                    throw new RuntimeException("unrecognized escape in date format string [%" + c + "]");
                }
            } else {
                sb.append(c);
            }
        }

        return sb.toString();
    }

    /* ==== PARSING DATES/TIMES FROM STANDARD STRINGS ==== */

    public static Date parseDateTime(String str) {
        DateFields fields = new DateFields();
        int i = str.indexOf("T");
        if (i != -1) {
            if (!parseDate(str.substring(0, i), fields) || !parseTime(str.substring(i + 1), fields)) {
                return null;
            }
        } else {
            if (!parseDate(str, fields)) {
                return null;
            }
        }
        return getDate(fields);
    }

    public static Date parseDate(String str) {
        DateFields fields = new DateFields();
        if (!parseDate(str, fields)) {
            return null;
        }
        return getDate(fields);
    }

    public static Date parseTime(String str) {
        DateFields fields = getFields(new Date());
        fields.second = 0;
        fields.secTicks = 0;
        if (!parseTime(str, fields)) {
            return null;
        }
        return getDate(fields);
    }

    public static Date parseTimeWithFixedDate(String str, DateFields fields) {
        if (!parseTime(str, fields)) {
            return null;
        }
        return getDate(fields);
    }

    private static boolean parseDate(String dateStr, DateFields f) {
        List<String> pieces = split(dateStr, "-", false);
        if (pieces.size() != 3)
            return false;

        try {
            f.year = Integer.parseInt((String) pieces.get(0));
            f.month = Integer.parseInt((String) pieces.get(1));
            f.day = Integer.parseInt((String) pieces.get(2));
        } catch (NumberFormatException nfe) {
            return false;
        }

        return f.check();
    }

    private static boolean parseTime(String timeStr, DateFields f) {
        //get timezone information first. Make a Datefields set for the possible offset
        //NOTE: DO NOT DO DIRECT COMPUTATIONS AGAINST THIS. It's a holder for hour/minute
        //data only, but has data in other fields
        DateFields timeOffset = null;

        if (timeStr.charAt(timeStr.length() - 1) == 'Z') {
            //UTC!

            //Clean up string for later processing
            timeStr = timeStr.substring(0, timeStr.length() - 1);
            timeOffset = new DateFields();
        } else if (timeStr.indexOf("+") != -1 || timeStr.indexOf("-") != -1) {
            timeOffset = new DateFields();

            List<String> pieces = split(timeStr, "+", false);

            //We're going to add the Offset straight up to get UTC
            //so we need to invert the sign on the offset string
            int offsetSign = -1;

            if (pieces.size() > 1) {
                //offsetSign is already correct
            } else {
                pieces = split(timeStr, "-", false);
                offsetSign = 1;
            }

            timeStr = pieces.get(0);

            String offset = pieces.get(1);
            String hours = offset;
            if (offset.indexOf(":") != -1) {
                List<String> tzPieces = split(offset, ":", false);
                hours = tzPieces.get(0);
                int mins = Integer.parseInt(tzPieces.get(1));
                timeOffset.minute = mins * offsetSign;
            }
            timeOffset.hour = Integer.parseInt(hours) * offsetSign;
        }

        //Do the actual parse for the real time values;
        if (!parseRawTime(timeStr, f)) {
            return false;
        }

        if (!(f.check())) {
            return false;
        }

        //Time is good, if there was no timezone info, just return that;
        if (timeOffset == null) {
            return true;
        }

        //Now apply any relevant offsets from the timezone.
        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

        long msecOffset = (((60 * timeOffset.hour) + timeOffset.minute) * 60 * 1000L);
        c.setTime(new Date(DateUtils.getDate(f, "UTC").getTime() + msecOffset));

        //c is now in the timezone of the parsed value, so put
        //it in the local timezone.

        c.setTimeZone(TimeZone.getDefault());
        long four = c.get(Calendar.HOUR);

        DateFields adjusted = getFields(c.getTime());

        // time zone adjustment may +/- across midnight
        // which can result in +/- across a year
        f.year = adjusted.year;
        f.month = adjusted.month;
        f.day = adjusted.day;
        f.dow = adjusted.dow;
        f.hour = adjusted.hour;
        f.minute = adjusted.minute;
        f.second = adjusted.second;
        f.secTicks = adjusted.secTicks;

        return f.check();
    }

    /**
     * Parse the raw components of time (hh:mm:ss) with no timezone information
     *
     * @param timeStr
     * @param f
     * @return
     */
    private static boolean parseRawTime(String timeStr, DateFields f) {
        List<String> pieces = split(timeStr, ":", false);
        if (pieces.size() != 2 && pieces.size() != 3)
            return false;

        try {
            f.hour = Integer.parseInt((String) pieces.get(0));
            f.minute = Integer.parseInt((String) pieces.get(1));

            if (pieces.size() == 3) {
                String secStr = (String) pieces.get(2);
                int i;
                for (i = 0; i < secStr.length(); i++) {
                    char c = secStr.charAt(i);
                    if (!Character.isDigit(c) && c != '.')
                        break;
                }
                secStr = secStr.substring(0, i);

                int idxDec = secStr.indexOf('.');
                if (idxDec == -1) {
                    if (secStr.length() == 0) {
                        f.second = 0;
                    } else {
                        f.second = Integer.parseInt(secStr);
                    }
                    f.secTicks = 0;
                } else {
                    String secPart = secStr.substring(0, idxDec);
                    if (secPart.length() == 0) {
                        f.second = 0;
                    } else {
                        f.second = Integer.parseInt(secPart);
                    }
                    String secTickStr = secStr.substring(idxDec + 1);
                    if (secTickStr.length() > 0) {
                        f.secTicks = Integer.parseInt(secTickStr);
                    } else {
                        f.secTicks = 0;
                    }
                }

                double fsec = Double.parseDouble(secStr);
                f.second = (int) fsec;
                f.secTicks = (int) (1000.0 * fsec - 1000.0 * f.second);
            }
        } catch (NumberFormatException nfe) {
            return false;
        }

        return f.check();
    }

    /* ==== DATE UTILITY FUNCTIONS ==== */

    public static Date getDate(int year, int month, int day) {
        DateFields f = new DateFields();
        f.year = year;
        f.month = month;
        f.day = day;
        return (f.check() ? getDate(f) : null);
    }

    /**
     *
     * @return new Date object with same date but time set to midnight (in current timezone)
     */
    public static Date roundDate(Date d) {
        if (d == null)
            return null;
        DateFields f = getFields(d);
        return getDate(f.year, f.month, f.day);
    }

    public static Date today() {
        return roundDate(new Date());
    }

    /* ==== CALENDAR FUNCTIONS ==== */

    /**
     * Returns the fractional time within the local day.
     *
     * @param d
     * @return
     */
    public static double decimalTimeOfLocalDay(Date d) {
        long milli = d.getTime();
        // time is local time.
        // We want to obtain milliseconds from start of local day.
        // the Math.floor() function below will do milliseconds from
        // start of UTC day. Adjust back to UTC time-of-day.
        Calendar c = Calendar.getInstance(TimeZone.getDefault());
        long milliOff = (c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET));
        milli += milliOff;
        // and now convert to fractional day.
        Double v = ((double) milli) / DAY_IN_MS;
        return v - Math.floor(v);
    }

    /**
     * Returns the number of days in the month given for
     * a given year.
     * @param month The month to be tested
     * @param year The year in which the month is to be tested
     * @return the number of days in the given month on the given
     * year.
     */
    public static int daysInMonth(int month, int year) {
        if (month == Calendar.APRIL || month == Calendar.JUNE || month == Calendar.SEPTEMBER
                || month == Calendar.NOVEMBER) {
            return 30;
        } else if (month == Calendar.FEBRUARY) {
            return 28 + (isLeap(year) ? 1 : 0);
        } else {
            return 31;
        }
    }

    /**
     * Determines whether a year is a leap year in the
     * proleptic Gregorian calendar.
     *
     * @param year The year to be tested
     * @return True, if the year given is a leap year,
     * false otherwise.
     */
    public static boolean isLeap(int year) {
        return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
    }

    /* ==== Parsing to Human Text ==== */

    /**
     * Provides text representing a span of time.
     *
     * @param f The fields for the date to be compared against the current date.
     * @return a string which is a human readable representation of the difference between
     * the provided date and the current date.
     */
    private static String formatDaysFromToday(DateFields f) {
        String daysAgoStr = "";
        Date d = DateUtils.getDate(f);
        int daysAgo = DateUtils.daysSinceEpoch(new Date()) - DateUtils.daysSinceEpoch(d);

        if (daysAgo == 0) {
            return Localization.get("date.today");
        } else if (daysAgo == 1) {
            return Localization.get("date.yesterday");
        } else if (daysAgo == 2) {
            return Localization.get("date.twoago", new String[] { String.valueOf(daysAgo) });
        } else if (daysAgo > 2 && daysAgo <= 6) {
            return Localization.get("date.nago", new String[] { String.valueOf(daysAgo) });
        } else if (daysAgo == -1) {
            return Localization.get("date.tomorrow");
        } else if (daysAgo < -1 && daysAgo >= -6) {
            return Localization.get("date.nfromnow", new String[] { String.valueOf(-daysAgo) });
        } else {
            return DateUtils.formatDate(f, DateUtils.FORMAT_HUMAN_READABLE_SHORT);
        }
    }

    /* ==== DATE OPERATIONS ==== */

    /**
     * Creates a Date object representing the amount of time between the
     * reference date, and the given parameters.
     * @param ref The starting reference date
     * @param type "week", or "month", representing the time period which is to be returned.
     * @param start "sun", "mon", ... etc. representing the start of the time period.
     * @param beginning true=return first day of period, false=return last day of period
     * @param includeToday Whether to include the current date in the returned calculation
     * @param nAgo How many periods ago. 1=most recent period, 0=period in progress
     * @return a Date object representing the amount of time between the
     * reference date, and the given parameters.
     */
    public static Date getPastPeriodDate(Date ref, String type, String start, boolean beginning,
            boolean includeToday, int nAgo) {
        Date d = null;

        if (type.equals("week")) {
            //1 week period
            //start: day of week that starts period
            //beginning: true=return first day of period, false=return last day of period
            //includeToday: whether today's date can count as the last day of the period
            //nAgo: how many periods ago; 1=most recent period, 0=period in progress

            int target_dow = -1, current_dow = -1, diff;
            int offset = (includeToday ? 1 : 0);

            if (start.equals("sun"))
                target_dow = 0;
            else if (start.equals("mon"))
                target_dow = 1;
            else if (start.equals("tue"))
                target_dow = 2;
            else if (start.equals("wed"))
                target_dow = 3;
            else if (start.equals("thu"))
                target_dow = 4;
            else if (start.equals("fri"))
                target_dow = 5;
            else if (start.equals("sat"))
                target_dow = 6;

            if (target_dow == -1)
                throw new RuntimeException();

            Calendar cd = Calendar.getInstance();
            cd.setTime(ref);

            switch (cd.get(Calendar.DAY_OF_WEEK)) {
            case Calendar.SUNDAY:
                current_dow = 0;
                break;
            case Calendar.MONDAY:
                current_dow = 1;
                break;
            case Calendar.TUESDAY:
                current_dow = 2;
                break;
            case Calendar.WEDNESDAY:
                current_dow = 3;
                break;
            case Calendar.THURSDAY:
                current_dow = 4;
                break;
            case Calendar.FRIDAY:
                current_dow = 5;
                break;
            case Calendar.SATURDAY:
                current_dow = 6;
                break;
            default:
                throw new RuntimeException(); //something is wrong
            }

            diff = (((current_dow - target_dow) + (7 + offset)) % 7 - offset) + (7 * nAgo) - (beginning ? 0 : 6); //booyah
            d = new Date(ref.getTime() - diff * DAY_IN_MS);
        } else if (type.equals("month")) {
            //not supported
        } else {
            throw new IllegalArgumentException();
        }

        return d;
    }

    /**
     * Gets the number of months separating the two dates.
     * @param earlierDate The earlier date, chronologically
     * @param laterDate The later date, chronologically
     * @return the number of months separating the two dates.
     */
    public static int getMonthsDifference(Date earlierDate, Date laterDate) {
        Date span = new Date(laterDate.getTime() - earlierDate.getTime());
        Date firstDate = new Date(0);
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(firstDate);
        int firstYear = calendar.get(Calendar.YEAR);
        int firstMonth = calendar.get(Calendar.MONTH);

        calendar.setTime(span);
        int spanYear = calendar.get(Calendar.YEAR);
        int spanMonth = calendar.get(Calendar.MONTH);
        int months = (spanYear - firstYear) * 12 + (spanMonth - firstMonth);
        return months;
    }

    /**
     * @param date the date object to be analyzed
     * @return The number of days (as a double precision floating point) since the Epoch
     */
    public static int daysSinceEpoch(Date date) {
        return dateDiff(getDate(1970, 1, 1), date);
    }

    public static Double fractionalDaysSinceEpoch(Date a) {
        return new Double((a.getTime() - getDate(1970, 1, 1).getTime()) / (double) DAY_IN_MS);
    }

    /**
     * add n days to date d
     *
     * @param d
     * @param n
     * @return
     */
    public static Date dateAdd(Date d, int n) {
        return roundDate(new Date(roundDate(d).getTime() + DAY_IN_MS * n + DAY_IN_MS / 2));
        //half-day offset is needed to handle differing DST offsets!
    }

    /**
     * return the number of days between a and b, positive if b is later than a
     *
     * @param a
     * @param b
     * @return # days difference
     */
    public static int dateDiff(Date a, Date b) {
        return (int) MathUtils.divLongNotSuck(roundDate(b).getTime() - roundDate(a).getTime() + DAY_IN_MS / 2,
                DAY_IN_MS);
        //half-day offset is needed to handle differing DST offsets!
    }

    /* ==== UTILITY ==== */

    /**
     * Tokenizes a string based on the given delimiter string
     * @param str The string to be split
     * @param delimiter The delimeter to be used
     * @return An array of strings contained in original which were
     * seperated by the delimeter
     */
    public static List<String> split(String str, String delimiter, boolean combineMultipleDelimiters) {

        int index = str.indexOf(delimiter);
        List<String> pieces = new ArrayList<String>(index + 1);
        while (index >= 0) {
            pieces.add(str.substring(0, index));
            str = str.substring(index + delimiter.length());
            index = str.indexOf(delimiter);
        }
        pieces.add(str);

        if (combineMultipleDelimiters) {
            for (int i = 0; i < pieces.size(); i++) {
                if (((String) pieces.get(i)).length() == 0) {
                    pieces.remove(i);
                    i--;
                }
            }
        }

        return pieces;
    }

    /**
     * Converts an integer to a string, ensuring that the string
     * contains a certain number of digits
     * @param n The integer to be converted
     * @param pad The length of the string to be returned
     * @return A string representing n, which has pad - #digits(n)
     * 0's preceding the number.
     */
    public static String intPad(int n, int pad) {
        String s = String.valueOf(n);
        while (s.length() < pad)
            s = "0" + s;
        return s;
    }

    private static boolean inRange(int x, int min, int max) {
        return (x >= min && x <= max);
    }

    /* ==== GARBAGE (backward compatibility; too lazy to remove them now) ==== */

    public static String formatDateToTimeStamp(Date date) {
        return formatDateTime(date, FORMAT_ISO8601);
    }

    public static String getShortStringValue(Date val) {
        return formatDate(val, FORMAT_HUMAN_READABLE_SHORT);
    }

    public static String getXMLStringValue(Date val) {
        return formatDate(val, FORMAT_ISO8601);
    }

    public static String get24HourTimeFromDate(Date d) {
        return formatTime(d, FORMAT_HUMAN_READABLE_SHORT);
    }

    public static Date getDateFromString(String value) {
        return parseDate(value);
    }

    public static Date getDateTimeFromString(String value) {
        return parseDateTime(value);
    }

    public static boolean stringContains(String string, String substring) {
        if (string == null || substring == null) {
            return false;
        }
        if (string.indexOf(substring) == -1) {
            return false;
        } else {
            return true;
        }
    }

}