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

И вот, глядя на их код, у меня родилась задачка, которую я хочу предложить в качестве развлечения читающим меня питонистам.

Итак, нужно написать универсальную функцию с такой спецификацией:

def move_month(month, year, delta):
    '''
    Принимая номер месяца (1-12), год и дельту (+1 или -1),
    возвращает пару (месяц, год) с соответственно следующим или 
    предыдущим месяцем.
    '''
    # ...
    return new_month, new_year

Основной фан в том, чтобы сделать это элегантно и коротко, при этом совершенно не обязательно хорошо читаемо. Я, собственно, и пишу эту задачку здесь, потому что свое решение из-за нечитаемости в боевой код включать не буду, а совсем ему дать пропасть жалко :-).

В качестве бонуса напишите так, чтобы delta могла быть не только единичной, но и произвольной целой.

Комментарии: 39

  1. odin
    import operator
    
    def move_month(month, year, delta):
        new_year, new_month = map(operator.add, divmod(delta, 12), (year, month))
        return new_mont, new_year
    
  2. Денис

    И коротко (3 операции) и читаемо:

    def move_month(month, year, delta):
        '''
        Принимая номер месяца (1-12), год и дельту,
        возвращает пару (месяц, год) с соответственно следующим или
        предыдущим месяцем.
        '''
        month += delta
        year += month//12
        month %= 12
        return month, year
    
  3. Vlad

    new_month = (month+delta-1)%12+1
    new_year += (month+delta-1)//12

  4. Илья
    def move_month(month, year, delta):
        new_month = (month + delta - 1) % 12 + 1
        new_year = year + (month + delta - 1) / 12
        return new_month, new_year
    
  5. Sergey Bondarenko

    Пример на Groovy, вместе с тестами:

    def moveMonth(month, year, deltha) {
        [(1..12)[(month + deltha - 1) % 12],  (int)(year + (month + deltha - 1) / 12)]
    }
    
    assert moveMonth(1, 1, 1) == [2, 1]
    assert moveMonth(1, 1, 2) == [3, 1]
    assert moveMonth(1, 1, 11) == [12, 1]
    assert moveMonth(1, 1, 12) == [1, 2]
    assert moveMonth(1, 12, 14) == [3, 13]
    
    assert moveMonth(1, 2, -1) == [12, 1]
    assert moveMonth(1, 2, -12) == [1, 1]
    assert moveMonth(1, 4, -25) == [12, 1]
    
  6. diadya_vova

    new_year = (year * 12 + month + delta - 1) // 12
    new_month = (year * 12 + month + delta) - (new_year * 12)

    Для произвольной целой и +1/-1 вроде так.

  7. Абыр
    def move_month(month, year, delta):
        year_delta, new_month = divmod(month -1 + delta, 12)
        return new_month + 1, year + year_delta
    
  8. Илья

    @Денис:

    Ваш код дает не совсем корректные результаты:

    >>> move_month(1, 2009, -1)
    (0, 2009)
    
    >>> move_month(1, 2009, 11)
    (0, 2010)
    
  9. odin

    наврал:

    new_year, new_month = map(operator.add, divmod(month + delta, 12), (year, 0))
    if new_month == 0:
        new_month, new_year = 12, new_year - 1
    

    Денис, перед return надо вставить

    if new_month == 0:
        new_month, new_year = 12, new_year - 1
    

    а то валится на

    >>>move_month(10, 2008, 2)
    (0, 2009)
    
  10. anonymous
    def move_month(month, year, delta):
        d, m = divmod(month + delta, 12)
        return m, year + d
    
  11. Dmytro Shteflyuk

    С Python'ом туго, потому я на руби поизвращаюсь:

    new_year, new_month = year + (new_month = month + delta - 1) / 12, new_month % 12 + 1
    
  12. Иван Сагалаев

    Ба! Не ожидал такого количества так быстро! Но, в общем, да, ключ в том, что операция деления по модулю умеет обрабатывать переполнение, а целочисленная часть деления выдает нужную дельту для года.

    Хотя я, вот, не знал, что они есть уложенные в одну функцию divmod, спасибо :-)

    Кстати, некоторые, кто применял деление к месяцу, забыли, что его надо сначала на -1 подвинуть, чтобы от нуля был.

    Понравилось прикольное решение odin'а с operator.add:

    new_year, new_month = map(operator.add, divmod(month + delta, 12), (year, 0))
    if new_month == 0:
        new_month, new_year = 12, new_year - 1
    

    Но только тут ведь можно без if'а обойтись, убрав единицу у месяца под divmod'ом:

    year, month = map(operator.add, divmod(month - 1 + delta, 12), (year, 1))
    
  13. nasos

    odin, ваш код ужасен. Используйте списковые выражения вместо map - гораздо читабельнее будет и без operator.add можно обойтись.

  14. Иван Сагалаев

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

    В условии задачки было как раз, что читабельность нас не интересует :-). Это все чистая зарядка для мозгов, никакой практичности.

  15. Dmytro Shteflyuk

    Еще вариант на Ruby:

    new_year, new_month = (month + delta - 1).divmod(12).zip([year, 1]).map { |a, b| a + b }
    
  16. Иван Сагалаев

    Кстати, в Питоне-то тоже можно без operator.add обойтись в одну строку:

    year, month = [a + b for a, b in zip(divmod(month - 1 + delta, 12), (year, 1))]
    
  17. Sergey Bondarenko

    И что, все предложенные примеры корректно обрабатывают обратные переходы (при deltha == month, deltha == month -1)?
    Тесты в студию!
    Предлагаю посты кот. не проходят тесты - удалять :)

  18. Sergey Bondarenko

    Сорри, я это имел в виду:
    при deltha == -month, deltha == -month-1

  19. Yura Ivanov

    В питоне не силен... на js:

    var move_month = function(month, year, delta){
    fullyear = year*12+month-1+delta;
    return {month:(fullyear % 12 +1),year:parseInt(fullyear / 12)};}
    

    криво, зато причудливо...

  20. Горбунов Олег

    На правах небольшого подкола:
    у нас в ПХП есть стандартая никсовая функция mktime()
    которая вычисляет даты с любыми смещениями, с учетом високосности и прочих радостей.
    неужели ее в чудо-питоне у вас нет?

  21. Kane

    Почему же нет. time.mktime. Вот чего нет к сожалению так это strtotime (или все же есть?). Можно было б сделать strtotime("+1 month").

  22. odin

    @nasos, каюсь, не самый лучший вариант. а зачем тут списковые выражения?

  23. Денис

    Да, со сдвигом месяца на единицу я забыл.

    Но при этом все варианты, где подсчёты повторяются (например, два раза считается (month+delta-1), мне кажутся не корректными.

    И разумеется в production стоит использовать какую нибудь полезную библиотеку (как в случае с mktime). Но разминка хороша. :)

  24. Dmytro Shteflyuk

    2Денис: У в Ruby on Rails есть библиотека для работы с датами.

    Date.new(2008, 12, 1).advance(:months => -12)
    Date.new(2008, 12, 1).months_since(-12)
    

    Но так не интересно :-)

  25. Иван Сагалаев

    И что, все предложенные примеры корректно обрабатывают обратные переходы (при deltha == month, deltha == month -1)?

    Проверка остается в качестве упражнения читателю :-). На самом деле, первоначально неработающих, кажется, только два — Дениса и anonymous'а, где месяц к 0-based не приводится.

    у нас в ПХП есть стандартая никсовая функция mktime()

    Поскольку она стандартная юниксовая, она, конечно, в Питоне есть. Но я, признаться, не знал, что она умеет приводить в правильную сторону 0 и 13 месяцы :-). Спасибо! С ней другая кривость — она выдает секунды, которые снова надо конвертировать в нормальную дату, а это уже некрасиво.

    а зачем тут списковые выражения?

    Это, видимо, про то, что я приводил в 2:21. Хотя я бы не сказал, что отказ от map сделал это выражение читаемее :-)

  26. odin

    а зачем тут списковые выражения?

    Это, видимо, про то, что я приводил в 2:21. Хотя я бы не сказал, что >отказ от map сделал это выражение читаемее :-)

    согласен, всё-таки, месяц и год несколько не однородные данные, и по смыслу подобный способ их обработки не совсем подходящий

  27. hidded

    В питоне есть одна интересная библиотека для работы с датами — называется dateutil (python-dateutil):

    import dateutil.relativedelta
    import datetime
    datetime.date.today() + dateutil.relativedelta.relativedelta(months=+1) # datetime.date(2009, 2, 22)
    
  28. Александр Шугард
    def move_month(month, year, delta):
        yd, md = divmod(month + delta - 1, 12)
        return range(1,13)[md], year + yd
    

    Так по короче будет (первый комент можно убрать)

    Работает для таких вещей как

    print move_month(12,2005,-24)
    

    Что у многих возвращает 0 в месяце

  29. Логовас

    Мои 5 копеек:

    lambda y,m,x: [((y-1,12),(y,m))[bool(m)] for y,m in [divmod(y*12+m+x,12)]][0]
    
  30. ei-grad

    Логовас, круче чем у вас не смог придумать)))

    а правильно это сделать, имхо, так:

    def move_month(month, year, delta):
        m = month + delta - 1
        return (year * 12 + m) // 12 , m % 12 + 1
    
  31. Игорь Давыденко

    @hidded

    отличная библиотека, очень нравится с ней работать...

  32. Курилов Дмитрий

    def move_month(month, year, delta):
    return (month + delta) % 12 or 12, year + (month + delta - 1) / 12

  33. Виталий Калитов

    Немного не в тему :)
    2odin:
    Зачем импортировать лишний модуль?
    operator.add легко заменить на:
    lambda x,y :x.__add__(y)
    В конце концов, python это и делает при операциях с числами - обращается к их(чисел как объектов) соответствующим методам. ;)
    Я, разумеется, ни разу не хочу никого поучать.

  34. Иван Сагалаев

    Виталий, если уж там lambda, то тогда и __add__ не нужен, можно более естественно: lambda x, y : x + y.

  35. Виталий Калитов

    Иван, точно!
    Чересчур увлекся идеей, да и выспаться не помешало бы. :)

  36. aleo

    Домашка по четвергам?

  37. odin

    2Виталий:

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

  38. Артур

    =)

        def move_month(month, year, delta):
            import os
            if os.name!="posix":
                raise NotImplementedError("Only for posix")
            v=os.popen("date -d'%d-%d-01 %d month' +'%%Y %%m'"%(year,month,delta))
            try:
                new_year, new_month=[int(x) for x in v.readline().split(' ')]
            except ValueError:
                raise ValueError("this date is not supported")
            v.close()
            return(new_month, new_year)
    
  39. Алексей Блинов

    ИМХО Логовас победил :)
    лямбды они вообще малочитаемы, а такие...

Добавить комментарий