exelenz.ru

javascript: время на человечьем языке

[Просмотров: 54077] [20:08:36//17-09-2002] [Комментариев:7]

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

Вернуться к началу страницы [Просмотров: 54077] [0.043] [Комментариев:7] Вернуться к началу страницы