Возникла такая задача: подсчитать разницу, прошедшую между двумя датами и уметь выводить её в годах, месяцах, днях и т.д., а также в такой форме "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)