大体この辺の時期になると隠れてたバグが見つかるやつ・・・
ということで今回は
各データベースでの計算方法と自分がよく使う言語での月末判定を整理しました。
月の演算(Nか月後・Nか月前)と月末調整
対象データベース: ORACLE, SQLSERVER, POSTGRES, MYSQL, MariaDB, SQLite, IBM DB2
考慮点:
- Nか月後・Nか月前の計算
- 指定日が月末の場合は算出結果も月末にする
- うるう年対応
- 11/30の3か月後 → 2/28, 2/28の3か月後 → 5/28 の月末にしないパターンも一応できるやつは考える
1. ORACLE
ORACLEではADD_MONTHS
関数を使用できる。
月末調整も自動で行われるため、基本的にADD_MONTHS
を使えばOK。
-- Nか月後(指定日が月末なら結果も月末)
SELECT ADD_MONTHS(DATE '2025-02-28', 3) FROM dual; -- 2025-05-31
-- Nか月前(指定日が月末なら結果も月末)
SELECT ADD_MONTHS(DATE '2025-02-28', -3) FROM dual; -- 2024-11-30
特記事項
- ORACLEの
ADD_MONTHS
は、月末日の場合は結果も月末になる仕様(2025/2/28 → 2025/5/31)。
- うるう年も正しく処理(2024/2/28 → 2024/5/28)。
- 11/30の3か月後は2/28 になる(2024/11/30 → 2025/2/28)。
2. SQLSERVER
SQLSERVERではDATEADD
関数を使用するが、月末処理は自動で行わなれない。
そのため、月末判定を行い、月末の場合はEOMONTHを使う必要あり。
-- Nか月後(指定日が月末なら結果も月末)
SELECT
CASE
WHEN DAY(@date) = DAY(EOMONTH(@date))
THEN EOMONTH(DATEADD(MONTH, 3, @date))
ELSE DATEADD(MONTH, 3, @date)
END AS result
特記事項
EOMONTH
を使い、月末なら結果も月末にする。
- 2/28 → 5/28 の調整は別途処理が必要(DATEADDの標準仕様では 2/28 → 5/28 にならない)。
3. POSTGRES
PostgreSQLではINTERVAL 'N months'
を使いますが、月末処理を考慮する必要あり。
-- Nか月後(指定日が月末なら結果も月末)
SELECT
CASE
WHEN date_part('day', my_date) = date_part('day', my_date + INTERVAL '1 month' - INTERVAL '1 day')
THEN my_date + INTERVAL '3 months' - INTERVAL '1 day'
ELSE my_date + INTERVAL '3 months'
END AS result
FROM (SELECT DATE '2025-02-28' AS my_date) t;
特記事項
INTERVAL
を使うと、基本的には2/28 → 5/28
になる。
- 月末調整が必要(
+ INTERVAL '1 month' - INTERVAL '1 day'
で判定)。
4. MYSQL / MariaDB
DATE_ADD
関数を使いますが、月末処理を考慮する必要あり。
-- Nか月後(指定日が月末なら結果も月末)
SELECT
CASE
WHEN DAY(@date) = DAY(LAST_DAY(@date))
THEN LAST_DAY(DATE_ADD(@date, INTERVAL 3 MONTH))
ELSE DATE_ADD(@date, INTERVAL 3 MONTH)
END AS result;
特記事項
LAST_DAY
を使い、元の月が月末なら結果も月末にする。
- うるう年や11/30 → 2/28 の処理は問題なし。
5. SQLite
SQLiteではstrftime
を使いますが、月末処理を考慮する必要あり。
-- Nか月後(指定日が月末なら結果も月末)
SELECT
CASE
WHEN strftime('%d', date) = strftime('%d', date, 'start of month', '+1 month', '-1 day')
THEN strftime('%Y-%m-%d', date, '+3 months', 'start of month', '+1 month', '-1 day')
ELSE strftime('%Y-%m-%d', date, '+3 months')
END AS result
FROM (SELECT '2025-02-28' AS date);
特記事項
- SQLiteには
EOMONTH
のような関数がないため、start of month
, +1 month
, -1 day
で月末調整。
- うるう年の処理も適切に行う。
6. IBM DB2
DB2ではADD_MONTHS
が使えるらしい。一度しか見たことないので知識浅め
-- Nか月後(指定日が月末なら結果も月末)
SELECT ADD_MONTHS(DATE('2025-02-28'), 3) FROM SYSIBM.SYSDUMMY1;
特記事項
- ORACLEと同様、DB2の
ADD_MONTHS
は月末を維持する。
- うるう年の調整も問題なし。
まとめ(DBごとの対応表)
DB | 標準関数 | 月末保持 | うるう年対応 |
---|
ORACLE | ADD_MONTHS | ✅ | ✅ |
SQLSERVER | DATEADD + EOMONTH | 要調整 | ❌ (2/28 → 5/28にする場合は追加処理) |
POSTGRES | INTERVAL | 要調整 | ✅ |
MYSQL | DATE_ADD + LAST_DAY | 要調整 | ✅ |
MariaDB | DATE_ADD + LAST_DAY | 要調整 | ✅ |
SQLite | strftime | 要調整 | ✅ |
IBM DB2 | ADD_MONTHS | ✅ | ✅ |
DB系まとめ
- ORACLE, DB2は
ADD_MONTHS
で簡単に処理できる。
- SQLSERVER, POSTGRES, MYSQL, SQLite は月末調整のロジックが必要。
- うるう年対応は
INTERVAL
(Postgres) では問題ないが、SQLSERVERは調整が必要。
◆言語での月末日判定まとめ
これらを利用して上記のDB周りの関数ではどうにもならないのを補助。もしくは言語でどうにかする時に使う。
✅ C#
DateTime.DaysInMonthを使用する
using System;
public class DateUtils
{
public static bool IsEndOfMonth(DateTime date)
{
return date.Day == DateTime.DaysInMonth(date.Year, date.Month);
}
}
// 使用例
Console.WriteLine(DateUtils.IsEndOfMonth(new DateTime(2025, 2, 28))); // True
Console.WriteLine(DateUtils.IsEndOfMonth(new DateTime(2025, 2, 27))); // False
✅ VB.NET
DateTime.DaysInMonthを使用する
Imports System
Module DateUtils
Function IsEndOfMonth(ByVal dateValue As DateTime) As Boolean
Return dateValue.Day = DateTime.DaysInMonth(dateValue.Year, dateValue.Month)
End Function
Sub Main()
Console.WriteLine(IsEndOfMonth(New DateTime(2025, 2, 28))) ' True
Console.WriteLine(IsEndOfMonth(New DateTime(2025, 2, 27))) ' False
End Sub
End Module
✅ Java
lengthOfMonth()を使用する
lengthOfMonth ⇒ 月の長さ(月末日)
getDayOfMonth ⇒ 月の何日目か(2025/02/28⇒28)
import java.time.LocalDate;
public class DateUtils {
public static boolean isEndOfMonth(LocalDate date) {
return date.getDayOfMonth() == date.lengthOfMonth();
}
public static void main(String[] args) {
System.out.println(isEndOfMonth(LocalDate.of(2025, 2, 28))); // True
System.out.println(isEndOfMonth(LocalDate.of(2025, 2, 27))); // False
}
}
✅ Python(ライブラリなし)
28日を基準に計算する。
やってることは他とあまり変わらない。
ライブラリをみたらPythonは calendar
モジュールや relativedelta
を提供する dateutil
ライブラリが使用できる。
使うとよりシンプルに月末日を判定できる。※下記にライブラリを使ってるのを記載するので興味なければ飛ばしてください
from datetime import date
def is_end_of_month(d: date) -> bool:
next_day = d.replace(day=28) + (date(d.year, d.month % 12 + 1, 1) - date(d.year, d.month, 28))
return d == next_day - (next_day - d).days
# 使用例
print(is_end_of_month(date(2025, 2, 28))) # True
print(is_end_of_month(date(2025, 2, 27))) # False
一応、仕組み。間違ってたらすみません。
-
d.replace(day=28)
- 指定日の日付を28日に固定します。
- 理由: 全ての月に28日は存在するため、エラーを防ぐために28日を基準にしている。
-
date(d.year, d.month % 12 + 1, 1)
- 次の月の1日を作成。
d.month % 12 + 1
により、12月の場合は翌年の1月となる。
-
date(d.year, d.month, 28)
-
next_day = d.replace(day=28) + (date(d.year, d.month % 12 + 1, 1) - date(d.year, d.month, 28))
- 次の月の1日との差分を利用し、月末の日付を取得。
(date(d.year, d.month % 12 + 1, 1) - date(d.year, d.month, 28))
→ これにより 月末までの日数 が求まる。
- それを28日に加算することで、月末の日付を導き出す。
-
d == next_day - (next_day - d).days
next_day - (next_day - d).days
によって元の日付が月末かどうかを判定。
- 月末の場合は
d == next_day
となる。
コードの挙動例
入力日付 | next_day | 判定 (d == next_day ) |
---|
2025/02/28 | 2025/02/28 | True |
2025/02/27 | 2025/02/28 | False |
2024/11/30 | 2024/11/30 | True |
2025/01/31 | 2025/01/31 | True |
✅ calendar
モジュールを使う
calendar.monthrange
を使うと、指定した月の最終日が簡単に取得できる。
import calendar
from datetime import date
def is_end_of_month(d: date) -> bool:
last_day = calendar.monthrange(d.year, d.month)[1]
return d.day == last_day
# 使用例
print(is_end_of_month(date(2025, 2, 28))) # True
print(is_end_of_month(date(2025, 2, 27))) # False
解説
calendar.monthrange(year, month)
は (曜日, 月の最終日) を返します。
d.day == last_day
で月末かどうかを判定する。
✅ dateutil.relativedelta
を使う
dateutil
ライブラリの relativedelta
を使うと、簡単に月末日を求めることができる。
from datetime import date
from dateutil.relativedelta import relativedelta
def is_end_of_month(d: date) -> bool:
return (d + relativedelta(days=1)).day == 1
# 使用例
print(is_end_of_month(date(2025, 2, 28))) # True
print(is_end_of_month(date(2025, 2, 27))) # False
解説
relativedelta(days=1)
を使って翌日に移動。
- 翌日が1日なら、元の日付は月末です。
✅ どちらを選ぶべきか?
- 標準ライブラリのみを使用したい →
calendar
モジュール
- 外部ライブラリ (dateutil) を使ってもよい →
relativedelta
を使った方法
シンプルに書きたい場合や、dateutil
を既に使っている場合は relativedelta
が便利。
どちらもパフォーマンス的に大きな差はないっぽいので、用途に合わせて選んでください。
思い出しながら書いた部分もあったり言語バージョンが古かったりするかもなので
上手く動かないときは適宜対応ということでお願いします。