17 апреля 2013 в 20:33

Даты в JavaScript: количество дней в месяце и некоторые особенности Safari из песочницы

Собственно, сам сниппет


Не так давно столкнулся с задачей, которая позволила бы получить количество дней в указанном месяце в JavaScript. Штатной функции для этого в языке к сожалению нет.

На эту тему был нагуглен один изящный механизм, использующий одну известную особенность многих языков программирования. Если установить несуществующую дату для какого-либо месяца (например 31 апреля), то в результате нашем объекте будет сохранено соответствующее число следующего месяца (в данном случае — 1 мая).

Таким образом, для того, чтобы получить количество дней в указанном месяце, необходимо отнять результат вышеописанной операции из числа 32. То есть, если задать в качестве даты 32 апреля, в результате мы получим 2 мая. Проверим: 32-2=30 — такое количество дней будет в апреле.

	var days_in_april = 32 - new Date(2013, 3, 32).getDate();

Способ применения


Используя прекрассную возможность прототипирования в JavaScript можно расширить встроенный объект языка Date собственным методом:

	Date.prototype.daysInMonth = function() {
		return 32 - new Date(this.getFullYear(), this.getMonth(), 32).getDate();
	};

Таким образом, чтобы получить количество дней в текущем месяце, достаточно будет обратиться непосредственно к Date:

	alert(new Date().daysInMonth());

Однако, как выяснилось со временем на практике — этот способ всё ещё не идеален. По крайней мере не для Safari.

Особенности Safari


Иногда результаты работы этого снипета не совпадали с ожидаемыми, и в Safari вместо привычных 30 и 31 можно было получить единицу. Исследование проблемы показали, что оказывается в Safari не в 100% случаев срабатывает описанная особенность языка при работе с датами. Лучше всего это проиллюстрирует следующий пример:

	<script>
		var date = new Date(1982, 9, 32);
		document.write(date);
	</script>

И вот что мы видим в Chrome 26 и Safari 6 соответственно:

image

Идеальная практика


Чтобы выкрутиться и избежать нагромождения ненужных проверок специально для Safari, достаточно изменить опорное число 32 на 33. Данный способ работает в Safari пока также хорошо, как и в остальных браузерах:

	Date.prototype.daysInMonth = function() {
		return 33 - new Date(this.getFullYear(), this.getMonth(), 33).getDate();
	};
@uoziod
карма
25,7
рейтинг 0,0
Пользователь
Самое читаемое Разработка

Комментарии (36)

  • +9
    www.datejs.com/
    momentjs.com/
    И, думаю, еще найдется.
    • 0
      отлично, спасибо!)
      но я искал крохотный способ именно для описанной задачи.
      • +4
        Как показывает практика, если начинается работа с датами — то каким-то конкретным случаем она не ограничивается.
        Ну и тот же Momentjs не такой уж и огромный.
        • +1
          за datejs не скажу, но momentjs тоже некорректно срабатывает в этом случае jsfiddle.net/yeaWn/1/
    • +2
      более того к написанию поста меня сподвигла именно особенность Safari
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Если поправят, то должно будет также работать
  • 0
    Отличное и элегантное решение. Можно даже в 140byt.es добавить…
    • +1
      Спасибо за наводку!)
  • 0
    А не надежнее было сделать крохотный массив, а для февраля уж совсем не сложно посчитать.
    • +5
      да new Date(y,m,0).getDate() же…
      • 0
        Только
        new Date(y,m+1,0).getDate()
        если подразумевается, что y и m текущие год и месяц.
    • 0
      Тоже хороший способ! Пожалуй дополню им пост чуть позже.
  • 0
    Еще одна библиотека для работы с датами
    arshaw.com/xdate/
    • 0
      тоже самое, смотрите сами: jsfiddle.net/rsboarder/RAupA/
      • +1
        в общем в пору переименовать слегка статью)
  • +1
    Хочется сказать не про сам способ, а про то, как вы предлагаете его применять:

    > Используя прекрассную возможность прототипирования в JavaScript можно расширить встроенный объект языка Date собственным методом

    Это называется манки-патчинг (monkey patching) и лучше его избегать, потому что код, который вы напишете, рассчитывая на наличие этого патча, может быть использован где-то на стороне (не исключено что даже вами) и вместо желаемого результата, человек получит ошибку.

    Уж лучше написать старую добрую простую функцию или если вам так нравится идея работать с классами, вы можете отнаследовать новый класс от стандартного Date, назвав его, например, ImprovedDate и работать с ним. Тогда, в его прототип вы сможете добавлять какие угодно методы и перекрывать любые стандартные уже объявленные.
    • +2
      Monkey patching это будет называться в случае если daysInMonth уже определен в Date.
      • 0
        Нет большой разницы, изменять в ран-тайме уже существующие методы или добавлять новые. Вот, например, Википедия встает на мою сторону:

        > A monkey patch is a way to extend or modify the run-time code of dynamic languages without altering the original source code.

        Но я не о названии, а о том, что такой путь неправилен и применять его можно только для быстрой отладки в консоли, а для промышленных решений он не годится.
        • +2
          Как у тебя вообще с английским? Внимательно перечитай всю статью, а не первый попавшийся абзац. Там ни слова про «загрязнение» ранее объявленных классов-интерфейсов новыми методами, что является обычной практикой во всех динамических языках с наличием прототипов, модулей или traits, где код собирается по кускам
          • 0
            Может быть всему виной мое плохое знание английского или что-то другое, но я совершенно не могу понять почему добавление новых методов в прототип объекта не считается манки-патчингом и не попадает под определение: extend or modify the run-time code of dynamic languages.

            И еще, я не могу понять откуда у взялось конкретное определение манки-патча как только изменение методов уже существующих классов-интерфейсов. Может быть виной всему то, что я вырываю куски из контекста и никогда не читаю ничего до конца, но вот я вырвал из статьи в вики кусок, в котором говорится «Monkey patching is used to… apply a patch at runtime to the objects in memory, instead of the source code on disk». Если я в прототипе Date объявлю дополнительное свойство, то оно появится у всех инстансов класса Date.

            var myDate = new Date();
            Date.prototype.isMonkeyPatched = true;
            myDate.isMonkeyPatched; // true
            

            Никакого противоречия — действительно код выше модификацирует объекты в памяти, поэтому я не могу понять, почему добавление свойства или метода является манки-патчингом, а изменение — нет.

            А еще, в принципе мне сложно понять, как можно считать вполне себе утилитарный класс Date можно считать классом-интерфейсом, но спишем это еще на какой-нибудь мой недостаток, может быть я вообще не разбираюсь в программировании.

            Я изначально-то говорил не об этом. Мой комментарий был совершенно не про трактовку определений, а про то, что изменения прототипа стандартного класса «на лету» (как бы они ни назывались, манки-патчинг или «загрязнение» ранее объявленных классов-интерфейсов новыми методами) — плохая практика для JavaScript'а и лучше найти любой другой способ, недостатка в которых нет.
            • +1
              // monkey patching valueOf
              Date.prototype.oldValueOf = Date.prototype.valueOf
              Date.prototype.valueOf = function() {
              return parseInt(this.oldValueOf() / 1000);
              }

              vs

              // extending Date
              Date.prototype.valueOfInSeconds = function() {
              return parseInt(this.valueOf() / 1000);
              }

              Заметно разницу? Первый пример — классический манки-патч, влияющий на остальной код, который использует Date#valueOf. Второй пример — динамическое расширение прототипа Date _новым_ методом, которое никоим образом не влияет на остальные инстансы Date (поменялся прототип, а не поведение ранее объявленных методов).
            • +1
              А вот пример изменения в «рантайме» (то, что как раз имелось ввиду в статье):

              var date = new Date()
              date.valueOf() // 1366289455443
              date.valueOf = function() {
              return 1;
              }
              date.valueOf() // 1
  • 0
    Раз такой хороший тред тут образовался — может, кто-нибудь знает библиотеку для работы с датами, которая умеет прибавлять к дате N рабочих дней? А то сейчас приходится использовать самописный костыль (и он меня немного пугает):

    // add numDays to oldDate and return resulting date
    function add_days(oldDate, numDays) {
        var new_date = new Date(oldDate.getFullYear(),oldDate.getMonth(),oldDate.getDate()+parseInt(numDays));
        return new_date;
    }
    
    function add_working_days(to_date, days) {
        // to_date: starting date,
        // days = number of working days to add
        var temp_date = new Date();
        var i = 0;
        var days_to_add = 0;
        while (i < (days)){
            temp_date = add_days(to_date, days_to_add);
            //0 = Sunday, 6 = Saturday, if the date not equals a weekend day then increase by 1
            if ((temp_date.getDay() != 0) && (temp_date.getDay() != 6)) {
                i+=1;
            }
    	days_to_add += 1;
        }
        return add_days(to_date, days_to_add);
    }
    
    • 0
      Вполне нормальное решение, имхо.

      Если, правда, вам не нужно проверять по производственному календарю праздники.
      • 0
        О, вот как называется календарь с нерабочими днями, спасибо за наводку :)
        • 0
          Главное — не забывать, что календарь нерабочих день разнится не только от государства к государству, но и даже внутри одного государства — бывают региональные и профессиональные праздники.
          В итоге к одной простенькой функции получаем в добавок огромный список праздничных дней :)
    • 0
      Например:
      function getDaysByWD(dayOfWeek, num) {
        // на каждые 5 рабочих дней - полная неделя
        var full = parseInt(num / 5) * 7;
        // считаем остаток
        var rest = num % 5;
        // корректировка по выходным, пока ноль
        var d = 0;
        
        // Если суббота - то один день
        if (dayOfWeek == 6) {
          d = 1;
        }
        // а для рабочих дней
        else if (dayOfWeek > 0) {
          // считаем сколько дней осталось до выходных
          daysToWeeknd = 6 - dayOfWeek - 1;
          // если в остатке больше, чем до выходных
          // то нужно посчитать ещё одну пару выходных
          if (rest > daysToWeeknd) {
            d = 2;
          }
        }
      
        return full + rest + d;
      }
      

      или компактная версия:
      function getDaysByWD(w,n,r) {
        return parseInt(n/5)*7+(r=n%5)+(w>0?w<6?r>(5-w)?2:0:1:0);
      }
      

      Возможно требует корректировок.
    • 0
      Если не ошибаюсь, в вашем алгоритме есть ошибка: если к пятнице прибавить один рабочий день, то вернётся суббота. Логичней было бы вернуть понедельник.
  • +2
    А так разве не проще?

    var first = new Date(2011, 3, 1).getTime(),
        second = new Date(2011, 4, 1).getTime();
    console.log((second - first)/86400000);
    • 0
      Не каждые сутки равны 86400000. Есть переходы на/с летнего времени, в такие дни стуки либо на час длинее, либо на час короче! Нужно добавить округление к ближайшему целому.
  • +2
    В JS хорошо поддержана работа с датами — просто не надо выходить за пределы документированности. Никто не гарантирует, что new Date(Y, M, 33) будет работать.

    Как сосчитать число дней в месяце? Берём время первого числа следующего месяца, вычитаем время 1 числа текущего, делим на число микросекунд в сутках.
    var d = +new Date()
    	, thYear = d.getFullYear(), thMonth = d.getMonth()
    	, nextMonth = (thMonth +1) % 12, nextYear = thYear + (thMonth==11);
    var daysInThMonth = (+new Date(nextYear, nextMonth, 1) - new Date(thYear, thMonth, 1))/ 86400000;
    

    Тут можно сократить. Но суть в том, что так совершенно без трюков (не считая +new вместо getTime()) берётся число дней, пользуясь работой с датами.

    А Вы в выводах написали 2 противоречащих утверждения: «Идеальная практика» и «Данный способ работает в Safari пока также хорошо». Способ, «работающий, пока», не может быть идеальным: ).
    • +1
      делим на число микросекунд в сутках.
      27 октября 2013 у вас получится 31.041666666666668 дней в месяце, так как число микросекунд в сутках — не константа :)
      • 0
        Опечатался — миллисекунд.
        Да, нужно Math.round снаружи, чтобы учесть любые переходы на зимнее-летнее время в разных странах.
  • 0
    Промазал, перенёс выше ↑.
  • 0
    я вот такое использую:

    var _date:Date = new Date(year, month+1, 1);
    _date.date--;
    trace(_date.date);

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.