javascript: время на человечьем языке
В процессе оформления раздела /me на exelenz.ru
мне потребовалось считать время, оставшееся до определенного
события и выводить его в виде правильных фраз на русском языке. Задача
достаточно простая, решать ее, очевидно, надо было на javascript. К моему
удивлению, готовых решений в сети не нашлось, так что я написал свою версию.
Итак, конкретная задача: выводить возраст и время до дня
рождения с точностью до секунды. То есть, получить хочется что-то вроде этого:
Прошло с момента рождения: 24 года 1 месяц 30 дней 5 часов 26 минут и 45 секунд
До ближайшего дня рождения: 10 месяцев 18 часов 33 минуты и 15 секунд
Задача естественным образом разбивается на две подзадачи: собственно вычисление
времени и его форматирование. Для начала решим вторую задачу.
|
Итак, мы хотим получить функцию, которая принимает на входе год, месяц, день, час,
минуту и секунду, в общем, время, которое надо перевести на русский язык, и
выдает текстовою строку содержащую это самое отформатированное время.
Интересно, вот вы так, навскидку, можете сказать, по каким правилам осуществляется
такой перевод? Когда надо сказать "год", когда "лет", а когда "года"?
Yole утверждает, что это знают все :) Я не
знал. Впрочем, правила действительно оказались несложными. Рассмотрим на
примере года. Берем год по модулю 10, если получилась единица, то правильной
формой будет "год" , если от 2 до 4, то "года", иначе "лет". Дальше, берем год
по модулю 100, если получилось число от 11 до 19, то правильной формой будет
"лет". Все. Для месяцев, дней и всех прочих единиц измерения времени правила
одинаковы. Единственное, что еще осталось сделать, это вставить союз "и" перед
последней ненулевой компонентой времени. Да, понятно, что компоненты с нулевым
значением показывать не надо. То есть не "1 год 0 дней 1 час и 0 минут", а "1 год
и 1 час".
Для начала алгоритм воплотился вот в такой вот код:
function DaysLeftText (Year, Month, Day, Hour, Minute, Second) {
Years = new Array ('лет','год', 'года', 'года', 'года',
'лет', 'лет', 'лет', 'лет', 'лет')
Months = new Array ('месяцев','месяц', 'месяца', 'месяца',
'месяца', 'месяцев', 'месяцев', 'месяцев', 'месяцев', 'месяцев')
Days = new Array ('дней','день', 'дня', 'дня', 'дня', 'дней',
'дней', 'дней', 'дней', 'дней')
Hours = new Array ('часов','час', 'часа', 'часа', 'часа',
'часов', 'часов', 'часов', 'часов', 'часов')
Minutes = new Array ('минут','минута', 'минуты', 'минуты',
'минуты', 'минут', 'минут', 'минут', 'минут', 'минут')
Seconds = new Array ('секунд','секунда', 'секунды', 'секунды',
'секунды', 'секунд', 'секунд', 'секунд', 'секунд', 'секунд')
tYear = Years [((Year%100)>=10 && (Year%100)<=19) ? 0 : Year%10]
tMonth = Months [((Month%100)>=10 && (Month%100)<=19) ? 0 : Month%10]
tDay = Days [((Day%100)>=10 && (Day%100)<=19) ? 0 : Day%10]
tHour = Hours [((Hour%100)>=11 && (Hour%100)<=19) ? 0 : Hour%10]
tMinute = Minutes[((Minute%100)>=11 && (Minute%100)<=19) ? 0 : Minute%10]
tSecond = Seconds[((Second%100)>=11 && (Second%100)<=19) ? 0 : Second%10]
preValues = new Array (Year, Month, Day, Hour, Minute, Second)
preTexts = new Array (tYear, tMonth, tDay, tHour, tMinute, tSecond)
numPartsPresent=0
for (i=5; i>=0; i--) if (preValues[i]!=0) numPartsPresent++
text=''
for (i=5; i>=0; i--) {
if (preValues[i]!=0) {
text=preValues[i]+' '+preTexts[i]+' '+text
if (numPartsPresent>1) {
text=' и '+text
numPartsPresent=0
}
}
}
return text
}
Yole, увидевший его, вскричал в ужасе - "Кощунство! Это же чистокровный
code duplication, устрани его немедленно!". Убоявшись бога Чистокода, и
Мартина Фаулера, пророка его, я покаялся. Yole отпустил мне мой грех и
наложил на меня епитимью - совершить Extract Method и набрать сто раз фразу
"чистосердечный рефакторинг очищает". В результате код действительно стал
лучше:
function TimeToString (value, str1, str2, str5) {
if (!value) return 0
mod = value % 10
if ((value%100)>=10 && (value%100)<=19) return str5
if (mod == 1) return str1
if (mod >= 2 && mod <= 4) return str2
return str5
}
function DaysLeftText (Year, Month, Day, Hour, Minute, Second) {
tYear = TimeToString (Year, 'год', 'года', 'лет')
tMonth = TimeToString (Month, 'месяц', 'месяца', 'месяцев')
tDay = TimeToString (Day, 'день', 'дня', 'дней')
tHour = TimeToString (Hour, 'час', 'часа', 'часов')
tMinute = TimeToString (Minute, 'минута', 'минуты', 'минут')
tSecond = TimeToString (Second, 'секунда', 'секунды', 'секунд')
preValues = new Array (Year, Month, Day, Hour, Minute, Second)
preTexts = new Array (tYear, tMonth, tDay, tHour, tMinute, tSecond)
numPartsPresent=0
for (i=5; i>=0; i--) if (preValues[i]!=0) numPartsPresent++
text=''
for (i=5; i>=0; i--) {
if (preValues[i]!=0) {
text=preValues[i]+' '+preTexts[i]+' '+text
if (numPartsPresent>1) {
text='и '+text
numPartsPresent=0
}
}
}
return text
}
Соответственно, такой вот вызов: DaysLeftText (2002, 0, 22, 11, 36, 12), вернет
такую строку: "2002 года 22 дня 11 часов 36 минут и 12 секунд".
|
Далее, надо посчитать, сколько времени прошло с момента рождения и сколько
осталось до ближайшего дня рождения. Понятно, что первое считается, как
Сейчас-МоментРождения, а второе, как БлижайшийДР-Сейчас. Единственная проблема
в том, что требуется уметь выполнять операцию вычитания над датами. Вычитать
друг из друга стандартные яваскриптовые даты по понятным причинам не стоит, так
что пишем свое. Для всех компонент обработка однотипна, начинаем с младшей,
при переходе через ноль вычитаем единицу из следующей по старшинству,
и прибавляем максимальное значение к той, которая обрабатывается. Максимальное
значение - это константа, единственное исключение - количество дней в месяце.
Надо еще не забыть учесть високосные года. Да, понятно, что иметь функцию с
12 параметрами не очень-то хочется, так что стоит упаковать все компоненты
а массивы. Вот код, который выполняет операцию вычитания над датами:
function IsLeapYear (Year) {
return ((Year % 4) == 0) && (((Year % 100) != 0) || ((Year % 400) == 0))
}
function DaysPerMonth (Year, Month) {
DaysInMonth = new Array (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
if (Month == -1) Month = 11
days=DaysInMonth [Month]
if ((Month == 1) && IsLeapYear(Year)) {days++}
return days
}
function DiffDateTime (DTa, DTb) {
maxValues = new Array (0, 12, 0, 24, 60, 60)
for (i=5; i>=0; i--) {
if (DTa[i]0) {
DTa[i-1]--
DTa[i] += maxValues[i] - DTb[i]
if (i==2) DTa[i] += DaysPerMonth (DTa[0], DTa[1])
} else {
DTa[i] -= DTb[i]
}
}
return DTa
}
|
Собственно, теперь остается только нарисовать формочку, куда будут выводиться
нужные данные, вызывать через некий интервал пересчет дат и обновлять значения
полей этой формы. Форма, например, может выглядеть так:
<form name="dates">
Прошло с момента рождения:<br>
<input type="text" name="passed">
Осталось до ближайшего дня рождения:<br>
<input type="text" name="topass">
</form>
А код, который высчитывает даты и обновляет поля формы, вот так:
function OnTimer () {
Now = new Date()
nowYear=Now.getYear ()
if (nowYear<200) nowYear=nowYear+1900
// Time since my birth
DTa = new Array (nowYear, Now.getMonth (), Now.getDate (),
Now.getHours (), Now.getMinutes (), Now.getSeconds ())
DTb = new Array (1978, 6, 17, 12, 0, 0)
DTc = DiffDateTime (DTa, DTb)
// Time to my nearest birtday
nextBDY=nowYear
if ((Now.getMonth ()>6) || (Now.getMonth()==6 && Now.getDay()>17))
nextBDY++
DTa = new Array (nowYear, Now.getMonth (), Now.getDate (),
Now.getHours (), Now.getMinutes (), Now.getSeconds ())
DTb = new Array (nextBDY, 6, 17, 12, 0, 0)
DTd = DiffDateTime (DTb, DTa)
document.dates.passed.value=DaysLeftText (DTc[0],DTc[1],DTc[2],
DTc[3],DTc[4],DTc[5])
document.dates.topass.value=DaysLeftText (DTd[0],DTd[1],DTd[2],
DTd[3],DTd[4],DTd[5])
}
setInterval("OnTimer()",500);
На этом все, дело сделано. Вы можете
скачать исходный текст и/или
посмотреть, как он работает.
|
Скрипт проверен и работает в Internet Explorer, Netscape Navigator начиная с
версии 4, разного рода Мозиллах и в Опере.
Надеюсь, что этот код сбережет вам немного времени. Смерть всемирному bit
bucket'у! Да здравствует повторное использование кода! :)
|