Даты в 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();
	};
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 37
    +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/
                +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);

                                  0
                                  Спасибо за статью. Очень помогли. Ранее расчет вел с помощью другого алгоритма, который имеет в себе ряд изъянов.

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

                                  Самое читаемое