Считаем рабочие дни с Moment.js

    Несколько месяцев назад я опубликовал плагин к Moment.js позволяющий рассчитать: сколько это N рабочих дней от сегодня в календарных днях? какая дата будет спустя N рабочих дней от заданной даты? сколько рабочих дней в заданном диапазоне? Возможность сконфигурировать рабочие дни и исключения в виде праздников — имеется.

    Плагин можно найти на github: https://github.com/andruhon/moment-weekday-calc

    Плагин можно установить через bower и npm:
    bower install moment-weekday-calc
    

    npm install moment-weekday-calc
    

    Плагин добавляет несколько функций в Moment.js:
    • int weekdayCalc — считает сколько «рабочих» дней в заданном диапазоне
    • date addWorkdays — находит дату спустя N «рабочих» (пн-пт) дней
    • int workdaysToCalendarDays — конвертирует рабочие дни в календарные
    • date addWeekdaysFromSet — добавляет дни из заданного множества к заданной дате
    • int weekdaysFromSetToCalendarDays — конвертирует дни из заданного множетсва в календартные дни

    Каждая из функций доступна с префиксом iso, такие функции используют множество рабочих дней начинающееся с понедельника (1-7), функции без префикса используют американский формат начинающийся с воскресенья (0-6).

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

    Использование:
    Привожу примеры только для функций с префиксом iso.

    Сколько пятниц между 14 и 23 февраля?
    moment('14 Feb 2014').isoWeekdayCalc('23 Feb 2014',[5]); //2  
    
    (в данном случае начало диапазона берётся из объекта moment, из которого мы вызываем функцию)

    Сколько рабочих дней, без учёта праздников в с 1 апреля 2015 года по 31 марта 2016?
    moment().isoWeekdayCalc('1 Apr 2015','31 Mar 2016',[1,2,3,4,5]); //262  
    
    (здесь объект moment не содержит даты, поэтому начальная дата задаётся в качестве первого аргумента)

    А если учесть пару праздников?
    moment().isoWeekdayCalc('1 Apr 2015','31 Mar 2016',[1,2,3,4,5],['6 Apr 2015','7 Apr 2015']); //260 
    

    Вызов с объектом:
    moment().isoWeekdayCalc({  
      rangeStart: '1 Apr 2015',  
      rangeEnd: '31 Mar 2016',  
      weekdays: [1,2,3,4,5],  
      exclusions: ['6 Apr 2015','7 Apr 2015']  
    }) //260
    

    Что за дата будет спустя 5 рабочих дней после 2 февраля, если работать без выходных?
    moment('2015-02-02').isoAddWeekdaysFromSet(5, [1,2,3,4,5,7]); //2015-02-08
    

    5 рабочих дней после 4 мая, с учётом 9го мая?
    moment('2015-05-04').isoAddWeekdaysFromSet({  
      'workdays': 5,  
      'weekdays': [1,2,3,4,5,6],  
      'exclusions': ['2015-05-09']  
    }); //2015-05-11 
    

    11 рабочих дней после 10 октября в календарных днях, рабочие дни — среда-воскресенье:
    moment('2015-10-05').isoWeekdaysFromSetToCalendarDays(11, [3,4,5,6,7], ['2015-10-15']) //17
    

    Подробнее в README на гитхабе.

    Заранее спасибо за здравую критику. Надеюсь, что плагин будет кому-нибудь полезен.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 21

      0
      Думаю тут не помешала бы интеграция с одним из плагинов для диапазонов: http://momentjs.com/docs/#/plugins/range/
      +1
      Такая же штука на питоне :)
      Код
      #!/usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/bin/python3.4
      # -*- coding: utf-8 -*-
      __author__ = 'admin'
      
      api_url = "http://basicdata.ru/api/json/calend/"
      from functools import lru_cache
      import calendar
      import datetime
      import urllib.request
      import json
      
      
      def flatten(a):
          if isinstance(a, list):
              for b in a:
                  for x in flatten(b):
                      yield x
          else:
              yield a
      
      
      # Генерирует расписание на заданный год по умолчанию
      # Все дни с понедельника по пятницу отмечаются рабочими
      # Все субботы и воскресенья - выходными
      def generate_default_calendar(year):
          return group_by_month(
              map(lambda x: (x[0], x[1] < 6),
                  map(lambda x: (x[0], x[1] + 1),
                      filter(lambda x: x[0] > 0,
                             flatten(calendar.Calendar.yeardays2calendar(calendar.Calendar(), year))
                             )
                      )
                  )
          )
      
      
      # Разделяет дни на группы по месяцам
      def group_by_month_inner(items):
          month = []
          for day, flag in items:
              if month and month[-1][0] > day:
                  # new month starting
                  yield month
                  month = []
              month.append((day, flag))
          if month:
              yield month
      
      
      def group_by_month(items):
          return list(group_by_month_inner(items))
      
      
      # Загружает дни-исключения
      @lru_cache(maxsize=None)
      def load_exceptions(apiurl, year):
          return json.loads(urllib.request.urlopen(apiurl).read().decode('utf8'))["data"][str(year)]
      
      
      # Меняет значения структуры по умолчанию для дней-исключений
      
      def apply_exceptions(months, exc):
          i = 0
          for m in months:
              newm = []
              i += 1
              for d in m:
                  if str(i) in exc and str(d[0]) in exc[str(i)]:
                      d = (d[0], exc[str(i)][str(d[0])]["isWorking"] != 2)
                  newm.append(d)
              yield newm
      
      
      # Удаляет все выходные, конвертирует кортежи в простые дни месяца
      def filter_holidays(months):
          for m in months:
              yield list(
                  map(
                      lambda x: x[0],
                      filter(
                          lambda x: x[1],
                          m
                      )
                  )
              )
      
      
      # Получает все рабочие дни за определенные месяц/год в виде массива. Если указать месяц, вернет только его
      def get_workdays(year=None, month=None):
          if year is None and month is None:
              year = datetime.datetime.now().year
              month = datetime.datetime.now().month
          if month is None:
              return list(filter_holidays(apply_exceptions(generate_default_calendar(year), load_exceptions(api_url, year))))
          else:
              return get_workdays(year)[month - 1]
      
      
      # Считает кол-в рабочих дней в году/месяце
      @lru_cache(maxsize=None)
      def count_workdays(year=None, month=None):
          if month is None and year is None:
              return len(get_workdays())
          elif month is not None:
              return len(get_workdays(year, month))
          else:
              return sum(list(map(lambda x: len(x), get_workdays(year))), 0)
      
      @lru_cache(maxsize=None)
      def _get_expected_hours(year, month, day):
          return len(list(filter(lambda x: x < day, get_workdays()))) * 8
      # Возвращает сколько часов ты уже должен был отработать
      def get_expected_hours():
          return _get_expected_hours(datetime.datetime.now().year, datetime.datetime.now().month, datetime.datetime.now().day)
      
      
      # Считает заработанные деньги исходя из зарплаты и кол-ва отработанны часов
      @lru_cache(maxsize=None)
      def earned(salary, hours):
          return hours / (count_workdays(datetime.datetime.now().year, datetime.datetime.now().month) * 8) * salary
      
      
      def print_earned_with_stats(hours, salary=None):
          if salary is None:
              salary = 50000
          real = earned(salary, hours)
          expected = earned(salary, get_expected_hours())
          print("Earned: ", real, " Expected: ", expected)
          if real > expected:
              print("Well done, you've already earned extra ", real - expected, " money -", hours - get_expected_hours(),
                    " extra hours worked")
          elif real < expected:
              print("You should work extra ", get_expected_hours() - hours, " hours to catch schedule")
          else:
              print("Going on schedule!")
      
      if __name__ == '__main__':
          import sys
          _salary = None
          _hours = None
          if len(sys.argv) > 1:
              _hours = int(sys.argv[1])
          if len(sys.argv) > 2:
              _salary = int(sys.argv[2])
          if _hours is None:
              _hours = 8
          print_earned_with_stats(_hours, _salary)
      
      

        +1
        А чего не на гитхабе? Вещь то годная.
          0
          Пока нету времени на такое, учебу с работой и то еле совмещаю)))
        0
        Вот вам сразу реквест — хотите, чтобы ваш плагин использовали — грузите исключения сами :)
        Советую basicdata.ru/api/json/calend Описание формата: basicdata.ru/api/calend
          0
          Спасибо. Стран слишком много в мире — надо будет подумать, как это сделать универсально.
            0
            Ну тут все просто — конечный результат вам от любого api нужен один — чтобы оно выдавало дни-исключения. Соответственно, можно из любого формата апи приводить к этому, универсальному, и работать с ним. Тогда использующему ваш модуль в очередной стране надо будет только найти апи для нее и реализовать функцию конвертации
              0
              Быть может заранее заготовить пакеты для стран, которые можно будет установить отдельно? Ну, допустим moment-weekday-calculator-public-holidays-ru и т.д. Как считаете?
                0
                ну в принципе имеет смысл, вряд ли большинство будет с ним работать более чем в одной стране.
                  0
                  Ок. Спасибо. Почешу репу, как время опять будет.
                    +1
                    Я порекомендовал глянуть в сторону ics.
                    Парсится слёту, один вопрос — не забыть раз в год утянуть свежий список.

                    Мне нужен был датский, первый из интернетов —
                    www.officeholidays.com/ics/ics_country.php?tbl_country=Denmark
                    На самом деле, это серьёзный вопрос, кому доверять, и не стоит брать вот так вот первый попавшийся.
                      0
                      Спасибо.
              0
              Кстати еще я не уверен, но вы похоже не учли вариант, что иногда праздничные дни делают рабочими(или если рабочий день в список исключений положить, он станет выходным?)
                0
                Тут всё конкретно — плагин принимает исключения для рабочих дней. Если выпадает на выходные, то просто ничего не меняет. На данный момент это остаётся на откуп разработчику — считать «mondaized» этот выходной или нет.
                  0
                  По-хорошему нужны два списка исключений: для рабочих и для выходных дней. Иногда рабочие дни переносят на выходной.
                    0
                    Сделаю, если кто-то запросит и найдётся на это время.
                0
                В некоторых странах еще и по отдельным провинциям/землям разные праздники. А есть и плавающие праздники, зависящие от даты пасхи.
                0
                Будьте осторожны при использовании этого API! Там неправильные данные, например basicdata.ru говорит, что 20 февраля 2015 — сокращенный на 1 час рабочий день, а на самом деле это не так.
                  0
                  Ну сокращенные на 1 час я не смотрел, а по кол-ву рабочих/нерабочих дней у них все правильно высчитывалось(сокращенные за рабочий считал)
                    0
                    Да, проблема может быть, если мы будем считать не количество рабочих дней, а количество рабочих часов. На всякий случай предупредил, сам чуть не наступил на эти грабли.

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