Формула подсчёта количества дней в месяце

Примечание: данный пост является переводом статьи cmcenroe.me/2014/12/05/days-in-month-formula.html (Часть I), а также авторским к нему дополнением (Часть II). Не следует относиться к материалу серьёзно, а скорее как к разминке для ума, требующей не более чем школьных знаний арифметики и не имеющей практического применения. Всем приятного чтения!

Часть I


Вступление


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

ФормализуяДругими словами, необходимо найти функцию f, такую, что значение f(x) для каждого месяца x, представленного числом от 1 до 12, равняется количеству дней в этом месяце. Таблица значений аргумента и функции1:
x 1 2 3 4 5 6 7 8 9 10 11 12
f(x) 31 28 31 30 31 30 31 31 30 31 30 31

Если у вас возникло желание попробовать самому до прочтения моего решения, то сейчас самое время. Если же вы предпочитаете немедленно увидеть готовый ответ, то посмотрите под спойлер.
Ответ

Ниже следуют мои шаги по нахождению решения.

Математический аппарат


Сначала бегло освежим в памяти два жизненно необходимых в решении этой задачи оператора: целочисленное деление и остаток от деления.

Целочисленное деление это оператор, применяемый во многих языках программирования при делении двух целых чисел и отбрасывающий от частного дробную часть. Я буду изображать его как . Например:

Остаток от деления это оператор, находящий остаток от деления. Во многих языках программирования применяется символ %, я же буду использовать конструкции вида , например:

Замечу, что остаток от деления имеет равный с делением приоритет.

Основы


Итак, применим наш математический аппарат для получения базовой формулы.2 В обычном месяце 30 или 31 день, так что мы можем использовать для получения поочерёдно 1 или 0, а затем просто прибавить к этому числу константу:

Получаем таблицу, полужирным выделены корректные значения:
x 1 2 3 4 5 6 7 8 9 10 11 12
f(x) 31 30 31 30 31 30 31 30 31 30 31 30

Неплохое начало! Уже есть правильные значения для января и для месяцев с марта по июль включительно. Февраль — особый случай, и с ним мы разберёмся чуть позже. После июля для оставшихся месяцев порядок получения 0 и 1 должен быть изменён на обратный.
Для этого мы может прибавить к делимому 1:

x 1 2 3 4 5 6 7 8 9 10 11 12
f(x) 30 31 30 31 30 31 30 31 30 31 30 31

Теперь правильные значения с августа по декабрь, но, как и предполагалось, значения для прочих месяцев неверны. Давайте посмотрим как мы можем объединить эти формулы.

Наложение маски


Для этого необходима кусочно-заданная функция, но — так как мне это показалось скучным — я задумался о другом пути решения, использующем одну часть функции на одном интервале, другую — на другом.
Полагаю, что проще всего будет найти выражение, равное 1 в одной области применения и 0 — в остальной. Метод, в котором умножая аргумент на выражение мы исключаем его из формулы вне области его применения, я назвал «наложением маски», потому такое поведение подобно некой битовой маске.
Для применения этого метода в последней части нашей функции необходимо найти выражение, равное 1 при , и — так как значения аргумента всегда меньше 16 — для этого прекрасно подходит целочисленное деление на 8.
x 1 2 3 4 5 6 7 8 9 10 11 12
x8 0 0 0 0 0 0 0 1 1 1 1 1

Теперь с помощью этой маски, используя в делимом выражение вместо 1, мы можем заменить порядок получения 0 и 1 формуле на обратный:

x 1 2 3 4 5 6 7 8 9 10 11 12
f(x) 31 30 31 30 31 30 31 31 30 31 30 31

Эврика! Всё правильно, кроме февраля. Сюрприз-сюрприз.

Февраль


В любом месяце 30 или 31 день, кроме февраля с его 28 (високосный год выходит за рамки этой задачи).3 На текущий момент по нашей формуле в нём 30 дней, поэтому неплохо бы вычесть выражение, равное 2 при .
Лучшее что мне удалось придумать это , что накладывает маску на все месяцы после февраля:
x 1 2 3 4 5 6 7 8 9 10 11 12
2 mod x 0 0 2 2 2 2 2 2 2 2 2 2

Изменив базовую константу на 28 с добавлением 2 к остальным месяцам получим формулу:

x 1 2 3 4 5 6 7 8 9 10 11 12
f(x) 29 28 31 30 31 30 31 31 30 31 30 31

К сожалению, январь теперь короче на 2 дня. Но, к счастью, получить выражение, которое будет применяться только для первого месяца очень легко: это округлённое вниз обратное к число. Умножив его на 2 получаем окончательную формулу:

x 1 2 3 4 5 6 7 8 9 10 11 12
f(x) 31 28 31 30 31 30 31 31 30 31 30 31


Послесловие


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

function f(x) { return 28 + (x + Math.floor(x/8)) % 2 + 2 % x + 2 * Math.floor(1/x); }

Часть II


Вступление


В первой части была получена короткая и даже немного изящная формула, основными достоинствами которой являются простота математического аппарата, отсутствие ветвлений и условных выражений, лаконичность. К недостаткам — кроме того, что вы не будете применять её в вашем проекте — можно отнести отсутствие проверки на вискокосный и не високосный год.
Поэтому я поставил перед собой задачу создать функцию f, такую, что значение f(x, y) для каждого месяца x, представленного числом от 1 до 12 и года y, большего 0, равняется количеству дней в месяце x в году y.
Для нетерпеливых под спойлером находится готовый ответ, остальных же прошу следовать за мной.
Ответ


Остаток от деления: mod и ⌊⌋


Для визуальной наглядности договоримся, что в некоторых формулах оператор деления с остатком заменён нижними скобками, там где это показалось мне необходимым:


Високосный год


В високосный год вводится дополнительный день календаря: 29 февраля. Как известно, високосным является год, кратный 4 и не кратный 100, либо кратный 400. Запишем тождественное этому высказыванию выражение:



Для приведения этого выражения в алгебраическое, необходимо применить к результату выражения инъекцию вида:


Что позволит получить 1 при делении без остатка и 0 при делении с остатком, чтобы использовать её в формуле определения количества дней в месяце.

В качестве функции g' можно использовать 1 минус остаток от деления для :
x 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
g'(x) Infinity 1 0 0 0 0 0 0 0 0 0 0 0 0 0

Легко заметить, что увеличив делимое и делитель на 1 мы получим правильную формулу при :
x 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
g'(x) 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0


Таким образом выражение запишем как:

А выражение запишем как:


Применяя этот подход получим следующую функцию g(y), значением которой будет 1, если год високосный, или 0 в обратном случае:

y 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000
g(y) 0 0 1 0 0 0 1 0 0 0 1
y 2000 2100 2200 2300 2400 2500 2600 2700 2800 2900 3000
g(y) 1 0 0 0 1 0 0 0 1 0 0

Полужирным выделены високосные года.

Напоминаю, что в рамках принятой договорённости оператор получения остатка от деления может быть изображен как mod, так и ⌊⌋.

Наложение маски


В формуле часть является поправкой, прибавляющей 2 дня к январю. Если убрать множитель 2 и в числителе заменить 1 на 2, тогда эта формула будет прибавлять 2 дня к январю и 1 день к февралю, что даёт нам ключ к добавлению дня в високосном году. Для наглядности используем в формуле промежуточное значение g(y) и в качестве y используем 2000 (високосный) и 2001 (не високосный) годы:

x 1 2 3 4 5 6 7 8 9 10 11 12
f(x, 2000) 31 29 31 30 31 30 31 31 30 31 30 31
f(x, 2001) 30 28 31 30 31 30 31 31 30 31 30 30

Значения для всех месяцев, кроме января не високосного года верны.

Для исправления этого досадного недоразумения добавим к январю 1 день уже известной нам формулой :

x 1 2 3 4 5 6 7 8 9 10 11 12
f(x, 2000) 32 29 31 30 31 30 31 31 30 31 30 31
f(x, 2001) 31 28 31 30 31 30 31 31 30 31 30 30


Теперь необходимо вычесть 1 день из января в случае високосного года, для чего нам поможет знание того, что для любого x , а .

Тогда формула примет вид:


Или:

x 1 2 3 4 5 6 7 8 9 10 11 12
f(x, 2000) 31 29 31 30 31 30 31 31 30 31 30 31
f(x, 2001) 31 28 31 30 31 30 31 31 30 31 30 30

Заключение


В результате получена уже значительно более громоздкая, но более универсальная формула, которую также можно использовать для получения количества дней в месяце определённого года:

function f(x, y) { return 28 + ((x + Math.floor(x / 8)) % 2) + 2 % x + Math.floor((1 + (1 - (y % 4 + 2) % (y % 4 + 1)) * ((y % 100 + 2) % (y % 100 + 1)) + (1 - (y % 400 + 2) % (y % 400 + 1))) / x) + Math.floor(1/x) - Math.floor(((1 - (y % 4 + 2) % (y % 4 + 1)) * ((y % 100 + 2) % (y % 100 + 1)) + (1 - (y % 400 + 2) % (y % 400 + 1)))/x); }

Пример на C# ideone.com/fANutz.


1. Я не умею пользоваться подобной мнемоникой, поэтому подсмотрел табличку в интернете.
2. «Основы», или «Правило С Многими Исключениями», как и большинство правил.
3. Изначально в римском календаре февраль был последним месяцем года, поэтому есть логика в том, что он короче всех остальных. Также есть логика в добавлении или удалении дня именно в конце года, поэтому его длина является переменной.

Upd. 1:
Альтернативный перевод первой части в сообществе вконтакте.
Upd. 2: Благодаря комментарию keksmen исправлена ошибка в формуле определения високосного года (g(y)) и исправлена итоговая формула.
Share post

Comments 55

    0
    Вообще забавно, но на практике, эффективнее использовать что-то типа
    function getDays(month)
    {
        switch (month) {
        case 1:
        case 3:
        case 5:
        case 7:
        case 8:
        case 10:
        case 12:
           days = 31;
           break;
        case 4:
        case 6:
        case 9:
        case 11:
           days = 30;
           break;
        case 2:
           days = 28;
           break;
        default:
            days = -1;
        }
        return days;
    }

    при этом, в февральской ветке можно добавить проверку на високосность.
      +22
      или [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month — 1]
        +10
        Можно ещё 28 + ((0x2eeeecc >> (2*month)) & 3). Ваш вариант выиграет на x86-64, мой — на x86 просто, хотя разница там копеечная.

        А вышеупомянутый вариант со switch'ом будет всё-таки разика в полтора медленнее… хотя так ли это всё важно?
          0
          Почему это switch будет медленный? Ныне компиляторы оптимизируют switch только в путь. В данном случае свитч скорее всего будет оптимизирован в таблицу и соответственно будет иметь константную сложность.
            0
            не быстрее: ветвления,… вычисление адреса, выборка,…
              0
              Не будет тут switch оптимизирован в таблицу. Обработку defaut'а в таблицу всё равно не засунешь, её всё равно придётся отдельно делать, в с учётом того, что вариантов немного компилятор, скорее всего, сгенерирует одно-два сравнения сверх этого.

              Ни до моего варианта, ни до варианта deNULL'а ни один известных мне компилятор не додумался.
              +4
              Ваш вариант самый крутой, умножение заменить сдвигом еще. Вариант с массивом чисел самый очевидный, но закодировать количества дней в 32-битное число — просто и гениально, в стиле олдскульной оптимизации, когда каждый байт был на счету.
                +2
                Умножение я предоставил оптимизировать компилятору. Умножения на константу — это то, что они делают прекрасно. В большинстве случаев компилятор заменит его на сложение и задействует при этом LEA, чтобы регистр не портить.
                +5
                Почему-то никто не обратил внимания на ошибку в константе.
                days = 28 + ((0x3bbeecc >> (month * 2)) & 3);
                И таки идеальный вариант!
              0
              Если не учитывать високосность, то для практики проще всего что-то такое:
              function getDays(m){
               return m === 2 ? 28 : 30 + (m > 7 ? m+1 : m) % 2;
              }
              
                +5
                Условный переход детектед
              +17
              Требую вариант с преобразованием Фурье ))
                +28
                Как-же вы запарили… Да читают, читают ваши теги!
                  +3
                  Самый простой способ запомнить (без всяких костяшек и мнемонических правил) — помнить о двух цезарях.
                  При Юлие Цезаре всё было просто: все нечётные месяцы были по 31 дню. Потом пришёл Август; в августе стало 31 день, и начиная с августа чётные месяцы стали по 31 дню.
                    +6
                    Занятно, но сам с детства использовал «правило костяшек». Если считать костяшки пальцев на руках и выемки между ними слева направо, то костяшка будет указывать на 31 день, а выемка на 30(или 28 в феврале). При этом переход с левой руки на правую как раз попадет на июль-август.
                      +7
                      А теперь открытие — костяшки можно считать и справа налево и вообще только на одной руке, потому что они симметричны :)
                      +3
                      Ещё можно помнить о Тиберие, который — по легенде* — пресёк традицию переименования месяцев в честь императоров.

                      * Когда к Тиберию пришёл сенат с заискивающим предложением назвать в его честь месяц в календаре, тот, засмеявшись, бросил им в ответ:
                      — Глупцы! Что же вы предложите 13-му императору?!
                        +2
                        И вы действительно считаете что это запомнить проще чем костяшки?
                          0
                          Проведите эксперимент: нужно определить сколько дней в ноябре. Ноябрь -> 11-й месяц -> нечётный и больше 7 => 30 дней. В уме быстрее выйдет, чем все костяшки пересчитывать.
                            +1
                            Каждый по своему это считает. Для меня есть четыре с половиной «реперные» точки: январь и декабрь (все знают что начинает и кончается год «длинными» месяцами) и вышеупомянутые «июль» и «август» (которые тоже оба длинные потому что император Август не хотел, чтобы его месяц был короче, чем у предшественника). Ну и февраль, как «очень особый» случай (там и от года количество дней зависит). Ну и чего там осталось? 2-3 шага от любой реперной точки — и мы добираемся до любого месяца.

                            Ваш метод хорош, если помнить какой по номеру каждый месяц, но я таких людей немного знаю. Особенно если учесть что названия врут (судя по названию тот же Ноябрь как бы должен бы быть девятым, да? правда на чётность это не влияет, но всё же...). Всё равно считать нужно откуда-то, а «реперных точек» меньше (что январь первый и декабрь последний, двенадцатый, все знают, а вот что посредине — уже нужно «на пальцах» считать).

                            Так-то самый быстрый способ — просто запомнить где сколько дней, но если мы тут что-то обсуждаем, то, похоже, что с запоминаем у нас всё не слишком хорошо, не так ли?
                        +1
                        Это круто!
                          +11
                          Можно также воспользоваться интерполяцией Лагранжа:
                          sage: PolynomialRing(QQ, 'x').lagrange_polynomial(zip(range(1, 13), (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)))
                          
                          -11/907200*x^11 + 163/181440*x^10 - 37/1260*x^9 + 13481/24192*x^8 - 2055371/302400*x^7 + 240683/4320*x^6 - 28268521/90720*x^5 + 85774775/72576*x^4 - 446998571/151200*x^3 + 46351537/10080*x^2 - 221017/56*x + 1416
                          
                            +6
                            Или, если смущают дроби, можно сделать по модулю например 37:

                            sage: PolynomialRing(GF(37), 'x').lagrange_polynomial(zip(range(1, 13), (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)), algorithm="neville")[-1]
                            
                            16*x^11 + 12*x^10 + 4*x^8 + 21*x^7 + 29*x^6 + 10*x^5 + 12*x^4 + 34*x^3 + 26*x^2 + 5*x + 10
                            
                              +3
                              О! спасибо! Очевидным ответом был «интерполяционый полином». Я всё ждал, когда кто-то запостит формулу.
                                +1
                                Так не честно! :)
                                У вас информации в формуле заметно больше, чем в самом ряду. В статье более компактный вариант.
                                  0
                                  А теперь посчитайте с учетом високосного года :)
                                +1
                                Интересно, насколько хорошо справится с задачей генетический алгоритм, если скормить ему достаточно квадратных скобок, операций взятия остатка и небольших целых чисел
                                +4
                                31-[2*{3/7*(x-1)}]-2*[1/(1+(x-2)^2)]
                                  +3
                                  Другой механизм описан в книге Романовского «Микрокалькуляторы в рассказах и играх». Пара «месяц-год» переводится к нумерации «с марта» (март = 1, февраль = 12), а затем [30,59*месяц] + день − 30.

                                  Правда, ПМК — зверь особый, ветвление там работает медленнее и расходует больше команд, чем умножение. А целую часть даже на Б3-34 можно вычислить через КИПr; r — любой свободный регистр памяти не менее 7 (0…3 автодекрементные, 4…6 автоинкрементные).
                                    +1
                                    Немного офтопа. Я ухитрялся на МК-61 на экзамене обращать матрицу 3×3. Заняло это всю память, для вычисления миноров по-чёрному использовались автоинкремент и автодекремент — поэтому матрица вписывалась не в логичные регистры 1…9, а в 7, 8, 9, 1, 2, 3, A, B, C.
                                      0
                                      Насколько я помню, существовали и алгоритмы и для матриц 4x4. Только они были уже интерактивные — эпизодически запрашивали ввод данных, не умещавшихся в памяти. Для МК-52 были возможны и ещё более сложные алгоритмы, не влезающие в 104 (вроде) ячейки оперативной памяти — там была возможность ручной подгрузки программ из энергонезависимой памяти. Были также и библиотеки программ и во внешних ПЗУ. Также можно было сохранять/восстанавливать данные оперативной памяти. Но необходимость делать это вбивая адреса вручную, ручное переключение режимов записи и чрезвычайно долгий процесс сохранения/восстановления убивал все желание работать. Старались обходится более отзывчивыми алгоритмами, разбиением задач на части и записью/восстановлением данных с помощью карандаша и бумаги.
                                      +1
                                      Давно думал написать статью на Хабр в духе «как сделать вычисление дня недели по дате на советском программируемом калькуляторе в 40-50 шагов памяти», но посчитал, что мелко и никому не интересно. А тут только из одного месяца такое развели :-)
                                        +3
                                        Ну теперь-то вы сделаете статью?
                                    +7
                                    30^(x&1)^(x/8)^(4/x&2)
                                      0
                                      Красиво!
                                      На шарпе можно скобки убрать:
                                      30 ^ m & 1 ^ m / 8 ^ 4 / m & 2;
                                      
                                        +1
                                        Для сравнения, в предложенной в статье формуле 9 операций
                                        28 + (m + m / 8) % 2 + 2 % m + 1 / m * 2;
                                        
                                        а у вас только 7. Интересно, можно ли компактнее?
                                          +1
                                          Есть несколько вариантов с 6 действиями:

                                          ((m+m/8)|30)-(4/m&2)
                                          31-(102/m&1)-(4/m&2)
                                          ((~8)>>m&30)|(3-102/m&5)

                                          Соптимизировать до пяти ни один не удалось.
                                          ~8 за действие не считается — это константа.
                                            +1
                                            Может я чего не понимаю в колбасных обрезках, но в предложенной мною (и исправленной withkittens) формуле всего четыре действия… или там как-то по другому считать нужно?
                                              0
                                              Я тоже не понимаю. В формуле со сдвигом их действительно четыре. Возможно, смущает слишком магическая константа?
                                                +1
                                                Я думаю тут скорее психология: мы тут думаем над всякими чётностями/нечётностми, цезарями и прочим, а оказывается что самый оптимальный вариант — «спресовать» данные о длинах и всё, думать ни о чём не нужно. Обидна, да?
                                                +1
                                                Ваше решение хорошее. Но, решение включает полную таблицу, пусть и свернутую в 32битную константу. Поиск альтернативных функций интересен сам по себе, как реализация переборных алгоритмов.
                                        +8
                                        Если бы не февраль, то:

                                          –1
                                          Самое красивое и эстетичное из всех =)
                                            +3
                                            Но без февраля.
                                          +1
                                          Можно вот так еще:
                                          31
                                          -
                                          (3) * (m == 2)
                                          +
                                          (1) * (m == 2) * ((y % 4 == 0) - (y % 100 == 0) + (y % 400 == 0))
                                          -
                                          (1) * (m == 4 || m == 6 || m == 9 || m == 11)
                                          
                                            +2
                                            Вы уверены, что хорошо проверили свои выкладки?

                                            Функция
                                            image
                                            Спокойно утверждает 2100 год как високосный, не смотря на оговорку о том, что это не так.

                                            И далее, функция
                                            image
                                            Ведет себя так же некорректно:
                                            f(2,2100)===29 //true
                                            
                                              0
                                              Вы правы, я вижу ошибку. Я поспешно воспользовался этой формулой. Вместо неё следовало использовать следующую формулу:

                                              (1 - (y % 4 + 2) % (y % 4 + 1)) * ((y % 100 + 2) % (y % 100 + 1)) + (1 - (y % 400 + 2) % (y % 400 + 1))



                                              ideone.com/VDHV3z

                                              Я внесу соответствующие исправления в пост.
                                              0
                                              Октябрь 1582?
                                                0
                                                Благодаря комментарию keksmen исправлена ошибка в формуле определения високосного года (g(y)) и исправлена итоговая формула.
                                                  0
                                                  С високосным годом (для C/C++):

                                                  31-(102/m&1)-((4/m&2)>>((!y%4)-(!y%100)+(!y%400)))

                                                  Операция !y — это не условный переход. В зависимости от желания компилятора он компилируется либо в
                                                      neg ecx
                                                      sbb eax,eax
                                                      inc eax
                                                  

                                                  либо в
                                                      xor eax,eax
                                                      test ecx,ecx
                                                      sete al
                                                  
                                                    0
                                                    Удостоверьтесь, что не допустили ошибки. Когда я выполняю ваш код, в феврале всегда 28 дней.

                                                    ideone.com/QaTplf
                                                      +1
                                                      Да, скобки не там.

                                                      31-(102/m&1)-((4/m&2)>>(!(y%4)-!(y%100)+!(y%400)))
                                                    0
                                                    Date.prototype.isLeapYear = function () {
                                                        var y = this.getFullYear();
                                                        return !(y % 4) && !!(y % 100) || !(y % 400)
                                                    };
                                                    
                                                    Date.prototype.getDaysInMonth = function () {
                                                        // adapted from ZX-Spectrum BIOS :-)
                                                        return 28 + ((this.isLeapYear() << 2 | 15662003) >> (this.getMonth() << 1) & 3)
                                                    };
                                                    

                                                    Only users with full accounts can post comments. Log in, please.