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'у! Да здравствует повторное использование кода! :)