понедельник, 23 ноября 2009 г.

Подсчет временной разницы между двумя датами

Возникла такая задача: подсчитать разницу, прошедшую между двумя датами и уметь выводить её в годах, месяцах, днях и т.д., а также в такой форме "1 день 2 месяца 3 года".

При решении данной задачи нужно учитывать, что нельзя просто взять и вычесть количество миллисекунд одной даты из другой. Таким образом мы не можем в точности посчитать ничего, т.к. существует переход на летнее/зимнее время. Например, мы вычтем кол-во мс из даты, созданной на 30 марта 2009 года(установим также 0 часов, минут, секунд и миллисекунд) и 29 марта 2009 года(также обнулим лишние поля). Если перевести получившееся количество миллисекунд в часы, то мы получим 23 часа, а должно быть 24. Почему? Потому что именно в это время у нас переводят на зимнее время. Соответственно, необходимо учитывать данный факт.

То есть для вычисления мс, прошедших между двумя датами нужно использовать следующий метод:
public static long getDiffInMillis(Calendar c1, Calendar c2)
    {
        // учитываем перевод времени
        long time1 = c1.getTimeInMillis() + c1.getTimeZone().getOffset(c1.getTimeInMillis());
        long time2 = c2.getTimeInMillis() + c2.getTimeZone().getOffset(c2.getTimeInMillis());
        return Math.abs(time1 - time2);
    }

Таким образом мы легко получим кол-во прошедших часов, минут и секунд. Но если мы хотим получить кол-во прошедших дней, не учитывая часы, минуты и прочее, то нам необходим следующий метод:
public static int getDiffInDays(Calendar c1, Calendar c2)
    {
        Calendar gc1 = (Calendar)c1.clone();
        Calendar gc2 = (Calendar)c2.clone();

        // обнуляем часы, минуты и секунды
        gc1.set(Calendar.HOUR, 0);
        gc2.set(Calendar.HOUR, 0);
        gc1.set(Calendar.SECOND, 0);
        gc2.set(Calendar.SECOND, 0);
        gc1.set(Calendar.MILLISECOND, 0);
        gc2.set(Calendar.MILLISECOND, 0);

        return (int)(getDiffInMillis(c1, c2) / MILLISECONDS_IN_A_DAY);
    }

Но используя данную методику нельзя сказать сколько прошло месяцев и лет. Для этого мы будем использовать следующий метод:
/**
     * 

Высчитывает разницу между двумя временами, в зависимости от значения field. * field - это константное значение из java.util.Calendar, обозначающее * конкретную временную метрику(год, месяц и т.д.).

* *

Устанавливает поля, заданные в массиве clearFields равными 0.

* * @param field * константное значение из java.util.Calendar(YEAR, MONTH...) * @param c1 * первая дата * @param c2 * вторая дата * @param clearFields * поля даты, которые не учитываются при сравнении. Задаются массивом констант из из java.util.Calendar. * Например, можно не учитывать миллисекунды, секунды и часы. Данные поля будут обнулены. * @return * разницу между c1 и c2 в метрике, заданной field */ public static int getTimeDifference(int field, Calendar c1, Calendar c2, int... clearFields) { Calendar gc1, gc2; if (c2.after(c1)) { gc1 = (Calendar)c1.clone(); gc2 = (Calendar)c2.clone(); } else { gc1 = (Calendar)c2.clone(); gc2 = (Calendar)c1.clone(); } if (clearFields != null) { // очищаем поля, которые мы не будем учитывать при сравнении дат for (int clearField : clearFields) { gc1.clear(clearField); gc2.clear(clearField); } } int count = 0; for (gc1.add(field, 1); gc1.compareTo(gc2) <= 0; gc1.add(field, 1)) { count++; } return count; }
Как мы видим, данный метод просто пытается прибавить 1 к заданному временному промежутку. В clearFields мы задаем поля, которые необходимо обнулить. Таким образом мы можем найти разницу не только в годах и месяцах, но и в чем угодно, но это будет неэффективно. Следующей задачей является вывод нужного слова для временной единицы. Например, мы говорим прошло "5 лет", "2 месяца", "1 год", "11 секунд" и т.д. Оказывается, что все названия временных промежутков подчиняются единому закону. Достаточно всего лишь трех слов :). Этот закон выражен данным кодом:
/**
     * Возвращает слово, добавляющееся к временной единице, когда указывается сколько прошло
     * временных единиц.
     * В русском языке для временных единиц достаточно трех слов.
     * Значение time берется по модулю.
     * 
     * @param time
     *            количество временной единицы
     * @param timeUnitName1
     *            Название временной единицы, которое используется с 1. Например, 1 "минута".
     * @param timeUnitName2
     *            Название временной единицы, которое используется с 2. Например, 2 "минуты".
     * @param timeUnitName5
     *            Название временной единицы, которое используется с 5. Например, 5 "минут".
     * @return
     *         слово для единицы времени
     */
    public static String getTimeUnitPeriodName(long time, String timeUnitName1, String timeUnitName2, String timeUnitName5)
    {
        String result;
        time = Math.abs(time); // для отрицательных чисел
        int small = (int)(time % 10);
        int middle = (int)(time % 100);

        // если заканчивается на 11, то всегда оформляется словом timeUnitName5
        if (small == 1 && middle != 11)
        {
            result = timeUnitName1;
        }
        // если оканчиваются на 2, 3, 4, за исключением 12, 13 и 14
        else if (small >= 2 && small <= 4 && (middle < 12 || middle > 14))
        {
            result = timeUnitName2;
        }
        else
        {
            result = timeUnitName5;
        }

        return result;
    }
Оказалось, что так устроен наш язык, что особыми временными единицами являются 11, 12, 13 и 14(точнее оканчивающимся на эти числа). Остальное подчиняется общим правилам. То ли дело в английском языке. Теперь мы можем считать промежуток между двумя датами в любой временной единице и находить для данной единицы нужное слово. Осталась ещё одна нерешенная задача: как выводить временной промежуток в формате "X дней Y месяцев Z лет", но делать это так, чтобы у нас не было ситуации "12 дней 13 месяцев 1 год", а было например так "12 дней 1 месяц 2 года". Т.е. у нас не может быть больше 12 месяцев или больше 31 дня. Данную задачу мы решим следующим образом. Будем находить разницу в годах и затем вычитать данную разницу из конца промежутка. Дальше поступим также для месяца. Оставшиеся дни же просто выведем. Вот кусок кода(from и to - экземпляры Calendar):
// если конечная дата больше, то меняем их местами
        if (from.after(to))
        {
            to = from;
        }

        int diffInYears = DateUtils.getDiffInYears(from, to);
        to.add(Calendar.YEAR, -diffInYears); // вычитаем учтенные года

        int diffInMonths = DateUtils.getDiffInMonths(from, to);
        to.add(Calendar.MONTH, -diffInMonths); // вычитаем учтенные месяцы

        int diffInDays = DateUtils.getDiffInDays(from, to);
Не забудьте, что вы вычитаете из даты, поэтому нужно клонировать переданный экземпляр Calendar. Ну а далее форматируем полученные числа по своему желанию.

p.s. также не забывайте указывать первый день недели на понедельник(в России) - calendar.setFirstDayOfWeek(Calendar.MONDAY)

2 комментария:

  1. Так по простому, через миллисекунды делать нельзя. Не особо вникал - високосные года учитываются? Плюс, еще есть замечательной свойство когда раз в несколько лет то ли секунду, то ли миллисекунду добавляют.

    Лучше всего для этих целей использовать проверенные решения вроде joda times.

    ОтветитьУдалить