В воскресенье я как обычно бездельничал, просматривая Reddit. Прокручивая щенячьи забавы и плохой юмор программистов, моё внимание привлёк один конкретный пост. Речь шла о баге в calc.exe.


Неверный результат вычисления диапазона дат в Калькуляторе Windows

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

Заинтересовавшись причиной, я сделал то, что вы делаете в таких случаях: попробовал на своей машине, чтобы запостить «У меня всё работает». И повторение ситуации из поста «31 июля ? 31 декабря» на моей машине дало правильный результат «5 месяцев». Но немного потестировав, я обнаружил, что «31 июля – 30 декабря» на самом деле вызывает ошибку. Выводится не совсем корректное значение «5 месяцев, 613566756 недель, 3 дня».

Я ещё не закончил расшатывать программу и тут вспомнил: «О, а разве калькулятор — не одна из тех вещей, для которых Microsoft открыла исходники?» И действительно. Эта ошибка не могла быть слишком сложной, поэтому я подумал, что попробую найти её. Скачать исходники было достаточно просто, и добавление требуемой рабочей нагрузки UWP в Visual Studio также прошло без сучка и задоринки.

Навигация по кодовым базам, с которыми вы не знакомы, — это то, к чему привыкаешь со временем. Особенно когда вы хотите внести вклад в проекты с открытым исходным кодом, где находите баг. Однако незнание XAML или WinRT, конечно, не облегчает дело.

Я открыл файл solution и заглянул в проект “Calculator” в поисках любого файла, который должен иметь отношение к багу. Нашёл DateCalculator.xaml, затем вроде бы подходящий по названию DateDiff_FromDate to DateCalculatorViewModel.cpp и, наконец, DateCalculator.cpp.

Установив точку останова и посмотрев некоторые переменные, я увидел, что конечное значение DateDifference уже неверно. То есть это была не просто ошибка преобразования в строку, а ошибка фактического вычисления.

Фактическое вычисление в упрощённом псевдокоде выглядит примерно так:

DateDifference calculate_difference(start_date, end_date) {
    uint[] diff_types = [year, month, week, day]
    uint[] typical_days_in_type = [365, 31, 7, 1]
    uint[] calculated_difference = [0, 0, 0, 0]
    date temp_pivot_date
    date pivot_date = start_date
    uint days_diff = calculate_days_difference(start_date, end_date)

    for(type in differenceTypes) {
        temp_pivot_date = pivot_date
        uint current_guess = days_diff /typicalDaysInType[type] 
        if(current_guess !=0)
            pivot_date = advance_date_by(pivot_date, type, current_guess)
        
        int diff_remaining
        bool best_guess_hit = false
        do{
            diff_remaining = calculate_days_difference(pivot_date, end_date)
            if(diff_remaining < 0) {
                // pivotDate has gone over the end date; start from the beginning of this unit
                current_guess = current_guess - 1
                pivot_date = temp_pivot_date
                pivot_date = advance_date_by(pivot_date, type, current_guess)
                best_guess_hit = true
            } else if(diff_remaining > 0) {
                // pivot_date is still below the end date
                if(best_guess_hit)
                    break;
                current_guess = current_guess + 1
                pivot_date = advance_date_by(pivot_date, type, 1)
            }
        } while(diff_remaining!=0)

        temp_pivot_date = advance_date_by(temp_pivot_date, type, current_guess)
        pivot_date = temp_pivot_date 
        calculated_difference[type] = current_guess
        days_diff = calculate_days_difference(pivot_date, end_date)
    }
    calculcated_difference[day] = days_diff
    return calculcated_difference
}

Выглядит нормально. В логике проблем нет. По сути, функция делает следующее:

  • отсчитывает полные годы от стартовой даты
  • с момента даты последнего полного года отсчитывает месяцы
  • с момента даты последнего полного месяца отсчитывает недели
  • с момента даты последней полной недели отсчитывает оставшиеся дни

На самом деле проблема заключается в предположении, что последовательный запуск

date = advance_date_by(date, month, somenumber)
date = advance_date_by(date, month, 1)

равен

date = advance_date_by(date, month, somenumber + 1)

Обычно это одно и то же. Но возникает вопрос: «Если вы попали на 31-е число месяца, в следующем месяце 30 дней, вы прибавляете один месяц, то куда попадёте?»

Похоже, для Windows.Globalization.Calendar.AddMonths(Int32) ответ будет «на 30-е число».

А это значит, что:
«31 июля + 4 месяца = 30 ноября»
«30 ноября + 1 месяц = 30 декабря»
«31 июля + 5 месяцев = 31 декабря»

Таким образом, операция AddMonths не является ни дистрибутивной (с AddMonth-умножением), ни коммутативной, ни ассоциативной. Какой вообще-то должна быть операция «сложения». Разве не весело работать со временем и календарями?

Почему в данном случае ошибка задания диапазона приводит к такому огромному числу недель? Как вы могли догадаться, это возникает из-за того, что days_diff является беззнаковым типом. Это превращает -1 дней в огромное количество, которое затем передаётся на следующую итерацию цикла с неделями. Которая затем пытается исправить ситуацию, уменьшая current_guess, но не уменьшая беззнаковую переменную.

Что ж, это был интересный способ провести воскресенье. Я создал пулл-запрос на Github с минимальным «исправлением». Я ставлю «исправление» в кавычки, потому что теперь вычисление выглядит так:



Думаю, технически это правильный результат, если считать, что «31 июля + 4 месяца = 30 ноября». Хотя такой вариант не совсем согласуется с человеческой интуицией о разнице дат. Но в любом случае это менее неправильно, чем было.

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


  1. inoyakaigor
    19.06.2019 12:04

    Когда исходники были закрытыми подобные посты читать было интереснее. Раньше ведь как:
    1) Присоединился дебаггером к процессу
    2) *какая-то магия с ассемблером*
    3) Профит!
    А сейчас что? Скачал исходники, поставил току останова и отладил. Скукота!


    1. developerxyz
      19.06.2019 12:38

      Так ведь исходники могут не совпадать с версией, которая уже установлена.
      Компилировать чужие исходники (иногда свои) бывает очень трудно.
      И надо знать, куда ставить breakpoint.


      1. inoyakaigor
        19.06.2019 14:37

        Я писал это с некоей долей иронии если что)


    1. Alexey2005
      19.06.2019 15:40
      +1

      Да зачастую в дебаггере с дизассемблером баг найти не в пример проще, чем в исходниках. Которые разбиты на 100500 файлов, и алгоритм размазан по нескольким десяткам из них. Вот так смотришь, что куда передаётся — а там интерфейсы поверх интерфейсов, и тонны абстрактных фабрик фабрик, за которыми понять, как и где конкретно происходит собственно расчёт, не так-то просто. Создаётся впечатление, что код на 99% состоит из «воздуха», который реально ни во что не компилируется.


      1. barbanel
        19.06.2019 16:12

        Создаётся впечатление, что код на 99% состоит из «воздуха», который реально ни во что не компилируется.
        Такой же точно калькулятор, написанный в девяностых, был бы раз в сто меньше, как по объему бинарника, так и по потребляемой памяти.
        У меня впечатление что этот код не только компилируется, но еще и пару миллионов пустых циклов добавляет.


        1. Victor_koly
          19.06.2019 18:18
          +1

          Калькулятор Win XP мог посчитать 250000! (по логике — суммирование логарифмов с достаточной точностью). Но функции типа a^b считал до куда меньшего предела.
          А вот в Win 7 уже не воспринимает результаты размером 1010000 и более.


      1. ainoneko
        20.06.2019 05:32

        Создаётся впечатление, что код на 99% состоит из «воздуха», который реально ни во что не компилируется.
        Вы только что описали ДНК?


      1. vlivyur
        20.06.2019 11:14

        Думаю у этого калькулятора и внутри ассемблерного кода фабрика фабрик с воздухом внутри.


    1. undbsd
      19.06.2019 16:27

      ну почему сразу «какая-то магия», просто переписал калькулятор с нуля и готово :D


  1. manyakRus
    19.06.2019 13:03
    +1

    «31 июля + 4 месяца = 30 ноября»
    до исправления было лучше — сразу видно что ошибка.
    а щас никто не заметит ошибку и будет использовать неправильный результат :(


    1. Deerenaros
      19.06.2019 14:04

      Математически операция складывания месяцев с конкретным числом — это бред. Без уточнений по крайне менее. Мы можем взять месяц как стандартные 30 дней (что является математическим округлением среднего ~30.44), тогда, например, мы будем "пропускать" февраль: 31 января + 1 месяц = 1~2 марта. Можно "обрезать" месяц, тогда операция теряет ассоциативность: (31 января + 1 месяц) + 1 месяц != 31 января + (1 месяц + 1 месяц). Можно "сохранять" число при обрезании, но тогда повляются "странные" элементы: 28</28> февраля, 28</29> февраля, 28</30> февраля, 28</31> февраля (да и стремление максимально узаконить такие операции — странная, так как есть же ещё разные календари со свойствами транзитивности — всё это учитывать… непонятно зачем).


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


      1. manyakRus
        19.06.2019 14:13
        +1

        "… Не факт, что это ошибка"
        — такая же команда есть в языках программирования, например в 1С Предприятие.
        Лень проверять, но думаю что 1С скажет что это точно ошибка :)


        1. Deerenaros
          19.06.2019 15:24

          Лучше проверить. Я проверил на python — у него в timedelta нельзя определить именно месяц. А вычитание конкретных дат приводит к конкретному количеству дней.


          1. Vasia529
            20.06.2019 07:20

            dateutil.relativedelta


        1. Golickoff
          20.06.2019 07:47

          Дата = Дата("20190731");
          Дата = ДобавитьМесяц(Дата, 4); //30.11.2019 0:00:00


          1. kuza2000
            20.06.2019 11:02

            1C язык запросов:
            ВЫБРАТЬ ДОБАВИТЬКДАТЕ(ДАТАВРЕМЯ(2019, 7, 31), МЕСЯЦ, 4) КАК Поле1
            Результат: 30.11.2019 0:00:00

            Ну вот, теперь в 1С все проверили :)


      1. Eldhenn
        19.06.2019 14:36

        > Математически операция складывания месяцев с конкретным числом — это бред

        А «следующая зарплата через месяц после 30 января» — тоже бред?


        1. InChaos
          19.06.2019 14:43
          +1

          С обычной человеческой точки зрения это нормально, 5 февраля + месяц = 5 марта, и даже неважно високосный год или нет.
          Но с математической это действительно бред, т.к. понятие месяц = неизвестная величина (28, 29, 30, 31), и в этом случае можно прибавлять только кол-во дней или недель (часов, минут и т.д.), т.е. строго детерминированные величины.


          1. Goron_Dekar
            20.06.2019 08:07

            А что с человеческой точки зрения будет 31 января + месяц?


            1. artoym
              20.06.2019 09:18

              Если с точки зрения «месяцев», то «31 января» стоит читать как «конец января», значит через месяц — это «конец февраля», ну и это равно «28 (или 29) февраля».
              Если же с точки зрения «дней», то надо определить значение «месяц» в днях. Думаю для большинства месяц = 30 дней, что даёт нам 1-2 марта.


              1. transcengopher
                20.06.2019 15:35

                Не согласен.
                Когда вы прибавляете к дате один месяц, то вы прибавляете следующий месяц, а не средний.
                Потому, для 31 января длина следующего месяца не 30, а 28 (29) дней, и именно 28 (29) дней и следует прибавить. Получим 28 (29) февраля.


                1. Lissov
                  20.06.2019 16:16

                  то вы прибавляете следующий месяц

                  Или же текущий. 1 июня + 1 месяц — я ожидаю 1-е июля, а не 2-е (в июле 31 день, в июне 30).


                  1. ainoneko
                    20.06.2019 17:05

                    «Прибавить длину текущего месяца (будет то же число месяца, если оно есть в следующем месяце), но остановиться на последнем дне при переполнении»?


                    1. Lissov
                      20.06.2019 18:35

                      Вообще мне кажется логичным проинформировать пользователя. Например, надписью «результат был скорректирован» и явно описать, в чём проблема. Выдавать возможно неверный результат как будто он правильный — не лучшая идея.
                      Всё таки стоит исходить из того, что нет однозначно верного ответа, что такое «через месяц после 31 января». И так как калькулятор не может знать, что ожидает пользователь, любой ответ может оказаться неверным для задачи пользователя.


        1. Deerenaros
          19.06.2019 15:22

          Может показаться странно, но я ни разу не слышал — через месяц после 30 января. Через месяц — да, но это подразумевает уже само по себе ± пара дней. Но вообще контекст в таких вещах плохо работает и часто уточняют — в конце контекст.следующий_месяц, и даже в этом случае 146% переспросят "то есть в январе?". А именно такая формулировка встречается разве что в анекдотах про математиков (и задачах по спортивному программированию).


        1. FYR
          19.06.2019 15:50

          Нет не бред, потому что используют не просто «месяц», а «календарный месяц». Что уже сложнее ибо привязанно к календарю. И да скорее всего 28.01 +1 месяц = 28.02 а [29-31].01 + 1 месяц = 01.03


          1. eranthis
            19.06.2019 17:33

            В операциях с месяцами основная ошибка — это попытка «взвесить» месяц в днях, что по определению невозможно, да и в корне неверно. Чтобы избежать неоднозначности, этого делать не нужно. В этом плане, очень правильно эта математика реализована, например, в PostgreSQL. Чтобы понять логику прибавления месяцев, проще всего взять пример зарплаты. Если заплата выплачивается каждый месяц, то месяц не может быть пропущен по определению. Иными словами, при добавлении месяца к любой дате в январе должна быть дата в феврале, но никак не в марте. Отсюда правда вытекают такие порой неочевидные моменты, как например:
            28/29/30/31 января + 1 месяц = 28 февраля
            (дата + N месяцев) - N месяцев не всегда равно дата
            (дата + 1 месяц) + 1 месяц не всегда равно дата + 2 месяца
            и т.п.


            1. slonpts
              19.06.2019 20:44

              В таком случае это очень плохая идея — называть такую операцию «сложением».

              Потому что сложение коммутативно, ассоциативно и дистрибутивно (с умножением), и все слишком сильно к этому привыкли.

              Лучше назвать ее как-нибудь по-другому, и не пытаться использовать здесь знаки "+" и "-", чтобы не вводить людей в заблуждение.


              1. tbl
                20.06.2019 00:05

                но люди привыкли к дате прибавлять и вычитать периоды, поэтому "+" и "-", хоть это и не совсем те же ассоциативные операции


              1. karavan_750
                20.06.2019 02:08

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


                1. slonpts
                  20.06.2019 03:11

                  ОК, если ввести 2 типа time и timeinterval, то можно ввести операцию их сложения.

                  Тогда можно определить значения типа timeinterval: '1 second', '1 day', '1 week'.
                  Но нельзя определить значения '1 month', '1 year', '1 century'.

                  И снова все будет работать.


                  1. eranthis
                    20.06.2019 13:46

                    Почему нельзя определить месяц? Это единичный интервал, для которого год и столетие кратные. Выразить эти интервалы в днях нельзя, но это не мешает их определению.


              1. kuza2000
                20.06.2019 11:08

                Очень во многих языках операция конкатенации строк обозначается символом "+", что тоже ничего общего не имеет со сложением.


              1. eranthis
                20.06.2019 13:41

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


        1. AC130
          19.06.2019 21:18

          Не бред, но и не операция сложения, как в комментариях уже указали.


      1. KarasikovSergey
        20.06.2019 10:32

        Математически операция складывания месяцев с конкретным числом — это бред. Без уточнений по крайне менее.

        Не бред, а недостаточная проработка логики. Если уж введена сама возможность складывать месяцы, то при выполнении такой операции должны фоном проводиться проверки: заданное условие DD.MM.YYYY проверяется на високосность, месяц точки отсчёта, таким образом прибавка 3х месяцев учитывает календарный состав следующих за заданным трёх месяцев и система точно знает — сколько там на самом деле дней. Это не так сложно, календарь всегда доступен для обращения за актуальными данными.


      1. vlivyur
        20.06.2019 11:28

        Когда дело касается дат и времени, ожидаемо что правила математики перестают работать. Можно ещё вспомнить что к дате прибавить 1 год 1 месяц и 1 день не то же самое, что к той же дате прибавить 1 месяц 1 день 1 год.


      1. ksr123
        20.06.2019 23:21

        Тут важно, для чего считать. Месяц на бытовом уровне может быть примерным, особенно если речь идет о десятилетия.


        А если о платежах каких-то — то может быть важен каждый день.


  1. KvanTTT
    19.06.2019 13:22
    -2

    Но это всегда может быть какой-то перевёрнутый бит каким-то высокоэнергетическим лучом от какого-то дружественного космического соседа.

    Я конечно понимаю, что это шутка, но вероятность того, что из-за этого повредится какой-то бит в памяти компьютера крайне мала, не говоря о вероятности повреждения конкретного. Скорее всего произойдте просто синий экран и компьютер перезгарузится. Ну или в какой-нибудь картинке пиксель или блок пикселей неправильно отобразится.


    1. InChaos
      19.06.2019 14:38
      +1

      В расчете на один бит вероятность очень мала, а если взять сервер с терабайтами памяти то вероятность далека не такая маленькая, поэтому и используют ECC память. Насчет синего экрана, то как раз процент занимаемой памяти процессами очень мал, относительно данных на таких огромных объемах, и скорее подпортятся данные, чем использующий их процесс.
      Исследования, проведенные IBM в 1990-х годах, показывают, что компьютеры обычно испытывают около одной ошибки, вызванной космическим лучом, на 256 мегабайт оперативной памяти в месяц.


      1. KvanTTT
        19.06.2019 15:22

        Ок, наверное был не прав. Кстати, на хабре уже была статья про статистику отказов в серверной памяти.


        1. InChaos
          20.06.2019 10:45

          О, спасибо, годная статейка.


        1. Lissov
          20.06.2019 16:14

          Была даже статья (не могу найти) с переводом доклада с конференции, где это использовалось как реально работающий метод взлома. Зарегистрировать кучу похожих на google доменов с отличием в 1 бит, и из миллиардов запросов некоторые попадают.


    1. izuware
      19.06.2019 15:30
      +1

      Лично видел 2 + 2 = 5. В прошлом веке на 286м процессоре шлейфом от флоповода перекрыло вентилятор.


      1. amarao
        19.06.2019 15:58
        +1

        Вентилятор на 286? Откуда?


        1. Zagrebelion
          19.06.2019 16:22
          +1

          В блоках питания вроде бы были.


          1. MacIn
            19.06.2019 19:20

            Да, но он направлен наружу и в XT и в AT корпусах.


        1. izuware
          21.06.2019 10:29

          Точно помню кулер на процессоре, и рабочее место расчетника лимитов, на котором стоял писюк желтой сборки. А вот могли поставить чтото дороже 286/287 или нет — мой склероз сообщать отказывается.


      1. Dmitri-D
        19.06.2019 16:49

        даже если по мнению процессора 2 + 2 уже 5, сообщить об этом он не сможет, будет не в состоянии )))


    1. CryptoPirate
      19.06.2019 16:05

      Не совсем так. Есть атака RawHammer, есть ещё классная атака где в URL по одному биту меняют Bit-squatting. Ситуаций, когда меняется один бит очень много.


  1. Dmitri-D
    19.06.2019 16:45
    +1

    любопытно, а 29 февраля + 1год это будет 28е февраля?


    1. alizar
      19.06.2019 16:55

      Конечно!


      1. developerxyz
        19.06.2019 17:49

        Но тут появляется другая ошибка — ошибка локализации. Не «365 дни», а «365 дней».


        1. N0Good
          19.06.2019 19:14

          Да что вы знаете об ошибках локализации калькулятора =) Вот мой калькулятор:
          image


  1. Fragster
    19.06.2019 18:44

    Я как-то делал через долю месяца + округление.


  1. danghyan
    19.06.2019 18:50

    1. Калькулятор нужен что бы работать на человека, а не на машину. Поэтому угождать надо человеку и 5 мая + месяц должно быть 5 июня… В целом математическую операцию +месяц в программировании можно свести к: берем число месяца, прибавляем 1, вставляем обратно.
    2. В разнице между дат в программах всегда надо учитывать что именно за даты, какого года. Дальше уже понятно что +30 дней это +30 дней, а плюс месяц это плюс месяц…

    В целом очень молодец.


    1. masai
      19.06.2019 20:16
      +1

      Поэтому угождать надо человеку и 5 мая + месяц должно быть 5 июня…

      А чему в таком случае будет равно «31 января + 1 месяц»?


      1. TRTHHRTS
        19.06.2019 20:47

        31 февраля, очевидно.
        И срабатывает валидация результата, которая говорит, что как-бы нет такой даты.


      1. begin_end
        19.06.2019 20:59

        Тогда должна действовать поправка: если такого числа в новом месяце нет, берем раннее ближайшее существующее. Кстати, с +год для високосных и февраля то же нужно.


      1. Osnovjansky
        20.06.2019 09:01

        Выше уже обсудили, что как правило, 31 января + 1 месяц = 28/29 февраля, в зависимости от года.
        Куда интереснее, чему должно быть равно (31 января + 1 месяц) + 1 месяц. Нужно ли помнить предысторию получения текущего значения даты.


        1. yea
          20.06.2019 10:58

          (31 января + 1 месяц) + 1 месяц = 28/29 марта
          31 января + (1 месяц + 1 месяц) = 31 января + 2 месяца = 31 марта

          Достаточно логично, как по мне. Ассоциативность придётся закопать, конечно, но у нас ведь тут особое сложение.


          1. krylov_sn
            20.06.2019 12:36

            такая же логика в древнем FoxPro)


          1. Osnovjansky
            20.06.2019 12:38

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


        1. masai
          20.06.2019 12:11

          Выше уже обсудили

          Ага, вижу. Я просто задал вопрос, когда обсуждения ещё не было.


          как правило, 31 января + 1 месяц = 28/29 февраля, в зависимости от года.
          Куда интереснее, чему должно быть равно (31 января + 1 месяц) + 1 месяц. Нужно ли помнить предысторию получения текущего значения даты.

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


  1. stuq1
    19.06.2019 22:06
    +1

    В старом калькуляторе из Windows 7 есть аналогичный баг, но выдается немного иной ответ

    Изображение
    image


  1. red_andr
    19.06.2019 23:17

    Ох уж эти вечные проблемы с годом в 365,2425 дней. Когда я работал с климатическими моделями мы просто использовали 360-дневный год. 12 месяцев по 30 дней и всё. Красота!


    1. sumanai
      20.06.2019 00:05
      +4

      То то прогнозы погоды врут!


      1. red_andr
        20.06.2019 17:31
        +1

        Ну погоду то считают метеорологические модели, а не климатические. У них нормальный земной календарь.


  1. Porohovnik
    20.06.2019 00:04
    +2

    А почему некто не догадался представить мечюсяцы как замкнутый двухсторонний цикл?
    И прибавление месяца, это просто переход на новое значение в списке…
    С годом тоде самое, но список не замкнутый
    Пример 1:
    10 февраля 2018


    • 5 месяцев
      должно работать вот как:
      Сохраняем в буфер количество дней
      Передвигаем замкнутый список месяцев на 5 позиций возвращаем дни.

    Пример 2:
    10 февраля 2018


    • 5 месяцев 25дней
      Повторяем все что в примере 1, а дальше
      Сохраняем сумму дней в переменную
      После чего
      Вытягиваем из двух связного списка количество дней в получившимся месяце
      И вычитаем из получившегося дней, если разность больше 0 переключаем месяц и добавляем разность, если меньше просто добавляем сумму дней

    Вроде всё логично кроме одного понятия: сначала прибавлять дни, а уже потом месяцы или наоборот?
    Лучший вариант-сделать галочку, что переключает это состояние


  1. prostofilya
    20.06.2019 06:14

    А в чём проблема брать количество дней тех месяцев, которые мы складываем?
    Пример: 20 июня 2019 + 3 месяца.
    1) Берём текущий месяц + 2 следующих (июнь, июль, август).
    2) Берём количество дней в этих месяцах и складываем (30 + 31 + 31, 92).
    3) Прибавляем количество дней к дате отсчёта, предварительно преобразуя начальную дату в дни (01.01.1970), смотря как в каком ЯП реализована работа с датами.
    4) Преобразуем кол-во дней в дату.


  1. tuxi
    20.06.2019 06:40

    Уххх, а еще есть боль от номеров недель. "Надо сравнить продажи за 2 года с группировкой по номерам недель. ....wtf!!! почему один год у тебя начинается с последних чисел декабря предыдущего, а другой не с первого января????" :)


    1. prostofilya
      20.06.2019 07:44

      Что-то не совсем понял, какие могут быть проблемы с номерами недель?


      1. m1rko Автор
        20.06.2019 07:50

        Предположим, что неделя должна начинаться с понедельника. Тогда если год начинается посреди недели, то первая неделя года начнётся в прошлом году. То же самое с месяцами. Первая неделя месяца начинается в прошлом месяце, отсюда и проблемы.


        1. prostofilya
          20.06.2019 07:56

          Ну это уже головная боль того, кто ставил задачу.


          1. tuxi
            20.06.2019 08:09

            Не совсем. «Бизнесу» часто нужно сравнить продажи по производственным неделям, но в рамках календарного года. И вот тут начинается самое интересное.


            1. prostofilya
              20.06.2019 08:20

              Как бы там бизнесу не было нужно, календарь не пододвинется. Если аналитики не могут в анализ информации и в грамотную постановку задач, хотя бы опуститься с недель до дней недели на необходимых участках, то это всё же их вина/проблема.


          1. alizar
            20.06.2019 08:17

            Ну он ставит задачу, типа, «Сравнить показатели первой недели месяца с предыдущим»


            1. prostofilya
              20.06.2019 08:25

              Я обязательно уточню что считать первой неделей месяца. Считаем с первого понедельника месяца? -Ок, не проблема.


              1. alizar
                20.06.2019 08:45
                +1

                При суммировании финансовых результатов всех недель месяца и всех дней месяца получатся разные цифры. Бизнес может выдвинуть претензии, что ваша программа глючная.


                1. prostofilya
                  20.06.2019 08:58

                  А это та контора, где надо семь перпендикулярных линий прозрачным маркером нарисовать? У меня шмотки собраны, резюме разослано.


                  1. tuxi
                    20.06.2019 09:40

                    Вы слишком категоричны. Данная закавыка может возникнуть в любой конторе, в любой стране и никто не говорит, что она не решаема. Я говорю, что она решается через некоторую боль и страдания.
                    И разработчик все же должен включать мозги, не имплементировать тупо по "наиподробнейшему" ТЗ. Иначе это мартышка, а не девелопер


                    1. prostofilya
                      20.06.2019 10:09
                      +1

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


                      1. ainoneko
                        20.06.2019 17:10
                        +1

                        "Девятый вагон — это тот, который сразу после восьмого, а не тот, который перед десятым".


                        1. tuxi
                          20.06.2019 22:12

                          Очень точная характеристика ситуации, как я мог забыть эту эпохальную вещь :) прям в точку!



    1. Odrin
      20.06.2019 11:19

      Первая неделя года — это неделя, которая включает в себя 4 января (в странах где неделя начинается с понедельника).


      1. tuxi
        20.06.2019 11:45

        У нас в стране есть такое понятие как «производственный календарь»
        В его контексте, бывают не только недели которые начинаются с прошлого года, но даже года с разным кол-вом этих самых недель.
        Эта тема не столько однозначная как может показаться на первый взгляд.


  1. mikserok
    20.06.2019 07:50

    У меня на семерке работает без аномалий — 5 месяцев и 6 дней. Видимо у автора виндовс 10.
    image


    1. Skerrigan
      20.06.2019 07:53

      Оффтоп не по теме
      Эх, прекрасное стекло в интерфейсе. Как же я тоскую по нему…


    1. Symphel
      20.06.2019 08:45
      +2

      Но ведь это неверно, результат меньше 5 месяцев


    1. anton9843
      20.06.2019 08:54
      +2

      По идее между 31.07 и 30.12 не должно быть больше 5 месяцев
      Если по человечески подумать, то 31.07 + 5 месяцем, это 31.12,
      а тут 30.12. По мне это 5 месяцев без одного дня,
      а не 5 месяцев и 6 дней


      1. unC0Rr
        20.06.2019 11:23

        Кроме того, 152 дня никак не соответствуют 5 месяцам и 6 дням. В месяце получается 29,2 дня в таком случае.


    1. almaredan
      20.06.2019 12:23

      Похоже от 30-г декабря до 31-го -6 дней
      image


    1. developerxyz
      21.06.2019 11:04

      Как раз 5 месяцев 6 дней — аномалия.
      31.07.19 + 5m6d = 06.01.20
      30.12.19 - 5m6d = 24.07.19


  1. Alex023
    20.06.2019 12:05

    Да у калькулятора и перевод на русский знатный: From — От, To — КОМУ.
    КОМУ, Карл! :)


  1. Eldhenn
    20.06.2019 13:40

    И до сих пор никто не вспомнил про таймзоны, летнее время, советский революционный календарь и особенности счёта времени при движении вокруг света. А кто-то даже до сих пор настолько наивен, что недели, дни, часы и минуты считает точно определёнными и неизменными величинами.


    1. vlivyur
      20.06.2019 14:39

      Такая статья уже была на Хабре.


  1. transcengopher
    20.06.2019 15:24

    В Java:

    var first  = LocalDate.of(2019, JULY, 31);
    var second = LocalDate.of(2019, DECEMBER, 30);
    
    print(Period.between(first, second));
    


    Выведет P4M30D, что очень близко с результатом после починки (т.к. P30D равен P4W2D). Может это и «неправильно» — но неправильно скорее думать об этой операции как о каноничном сложении. Зато теперь калькулятор выдаёт тот же ответ, что многие другие приложения (в частности, написанные на Java, да).


  1. sergey-b
    22.06.2019 15:55

    Смотрите, похоже, с годами тоже аналогичная проблема проявляется


    28.02.2016



    1. vlivyur
      22.06.2019 16:55

      Вроде всё правильно.


      1. developerxyz
        22.06.2019 17:52

        Тогда от 28 февраля 2016 года до 29 февраля 2016 года всего 0 дней, таким образом мы доказали, что 28 и 29 февраля 2016 года — один и тот же день.