読者です 読者をやめる 読者になる 読者になる

無駄と文化

実用的ブログ

Python で日時計算 ~ 月初とか月末とか N ヵ月前とか

Python で日付計算と言えば datetime モジュールですね。まぁまぁ便利なんですが、標準では 月初月末 を求めることができません。
なのでちょっとしたユーティリティコードとして書いておきました。


N ヶ月後・N ヶ月前

add_month()N ヶ月後の同日同時間 を求める関数です。同様に sub_month()N ヶ月前の同日同時間 を求めます。
2月31日 のように計算結果が存在しない日付になってしまう場合は、同月の末日を返します。

本当は datetime.timedelta を使えれば良かったんですが、datetime.timedelta では N ヶ月差を表現することはできないようです。


月初・月末

現在持っている日付の 月初月末 を求める関数も書きました。
start_of_month() は月初を、end_of_month() は月末を返します。


コード

# -*- coding: utf-8 -*-

from datetime import datetime
from calendar import monthrange


def add_month(_date, month_delta):
    """{month_delta} ヵ月後の同日の日時を返す

    >>> add_month(datetime.datetime(2017, 1, 30, 5, 20, 15), month_delta=1)
    datetime.datetime(2017, 2, 28, 23, 59, 59)

    >>> add_month(datetime.datetime(2017, 1, 15, 5, 20, 15), month_delta=1)
    datetime.datetime(2017, 2, 15, 5, 20, 15)

    >>> add_month(datetime.datetime(2017, 1, 30, 5, 20, 15), month_delta=13)
    datetime.datetime(2018, 2, 28, 23, 59, 59)
    """

    year = _date.year
    month = _date.month
    day = _date.day
    hour = _date.hour
    minute = _date.minute
    second = _date.second
    microsecond = _date.microsecond

    alt_year = year
    alt_month = month + month_delta

    if alt_month > 12:
        month_overflow = alt_month

        alt_year += month_overflow // 12
        alt_month = month_overflow % 12
    elif alt_month <= 0:
        month_overflow = alt_month

        alt_year += (month_overflow - 1) // 12
        alt_month = (month_overflow - 1) % 12 + 1

    try:
        return datetime(alt_year, alt_month, day, hour, minute, second, microsecond)
    except ValueError:
        return end_of_month(datetime(alt_year, alt_month, 1, hour, minute, second, microsecond))

def sub_month(_date, month_delta):
    """{month_delta} ヵ月前の同日の日時を返す

    >>> sub_month(datetime.datetime(2017, 3, 30, 5, 20, 15), month_delta=1)
    datetime.datetime(2017, 2, 28, 23, 59, 59)

    >>> sub_month(datetime.datetime(2017, 3, 15, 5, 20, 15), month_delta=1)
    datetime.datetime(2017, 2, 15, 5, 20, 15)

    >>> sub_month(datetime.datetime(2017, 3, 20, 5, 20, 15), month_delta=5)
    datetime.datetime(2016, 10, 20, 5, 20, 15)
    """

    return add_month(_date, -month_delta)

def start_of_month(_date):
    """その月の月初の時を返す

    >>> start_of_month(datetime.datetime(2017, 3, 15))
    datetime.datetime(2017, 3, 1, 0, 0)

    >>> start_of_month(datetime.datetime(2017, 2, 28, 10, 10, 10))
    datetime.datetime(2017, 2, 1, 0, 0)
    """

    year = _date.year
    month = _date.month

    return datetime(year, month, 1, 0, 0, 0)

def end_of_month(_date):
    """その月の月末の日時を返す

    >>> end_of_month(datetime.datetime(2017, 3, 15))
    datetime.datetime(2017, 3, 31, 23, 59, 59)

    >>> end_of_month(datetime.datetime(2017, 2, 28, 10, 10, 10))
    datetime.datetime(2017, 2, 28, 23, 59, 59)
    """

    year = _date.year
    month = _date.month
    end_of_month = monthrange(year, month)[1]

    return datetime(year, month, end_of_month, 23, 59, 59)


ちなみに、これらの関数名は PHP の良く出来た日付計算ライブラリ Carbon を大いに参考にしています。

一応、Gist があります。

Python で日時計算 ~ 月初とか月末とか N ヵ月前とか · GitHub


私からは以上です。

一手間加えた INSERT - レコードが未登録のとき、登録済みのとき、

DB にレコードを INSERT するとき、一手間加えて 未登録の場合に限って登録登録済みなら一部フィールドだけ上書き などしたくなりますよね。
ここ最近、そのような SQL を書くことが多かったのでメモしておきます。

ちなみに MySQL の独自構文などもバリバリ使っているので、他のベンダーの DB に適用するときは部分的な書き換えが必要かもしれません。


レコードが存在していなかったら新規登録、存在していれば上書き

INSERT INTO `posts`(
    `id`
    ,`title`
    ,`body`
    ,`created_at`
    ,`updated_at`
)
VALUES (
    42
    ,'test title'
    ,'test body'
    ,'1970-01-01 00:00:00'
    ,'1970-01-01 00:00:00'
)
ON DUPLICATE KEY UPDATE
    `title`       = VALUES(`title`)
    ,`body`       = VALUES(`body`)
    ,`updated_at` = VALUES(`updated_at`)

UNIQUE インデックスまたは PRIMARY KEY を重複させるようなレコードを INSERT しようとしたときに、INSERT ではなく UPDATE が実行される。
その場合でも created_at は上書きされない。

参考


レコードが存在していなかったら新規登録、存在していれば何もしない

INSERT INTO `tags` (
    `id`
    ,`name`
)
SELECT
    42
    ,'Technology'
FROM dual
WHERE NOT EXISTS (
    SELECT `id` FROM `tags`
    WHERE `name` = 'Technology'
)

タグ一覧テーブルからタグ名が ‘Technorogy’ であるものを探して、存在しなかった場合に限り INSERT する。
8行目でテーブル名として使われている dual は実際には参照されることのないダミーのテーブル名。

参考