- JAVA
Java 日付の差分計算|ChronoUnit・Period・Duration
Java で2つの日付の差を求めるには、総日数や総月数なら ChronoUnit.DAYS.between(開始, 終了)、「1年2か月5日」のような年月日の組なら Period.between、時間・分・秒なら Duration.between を使います。 3つは名前が似ていますが、返す値の意味が異なります。たとえば Period.getDays() は総日数ではなく、「年と月を取り除いた残りの日数」です。
この記事では、同じ2つの日付を3つの方法に通した結果を1枚の比較表にしました。月末、2月29日、逆順、夏時間、型の組み合わせで起きる例外まで、すべて実行結果つきで確認します。
確認環境:Ubuntu 24.04 / OpenJDK 21.0.10。出力はすべて java ファイル名.java で実行した結果です。
やりたいこと別の早見表
| やりたいこと | 書き方 | 結果の例 |
|---|---|---|
| 総日数 | ChronoUnit.DAYS.between(a, b) |
2026-01-15 → 2027-03-20 で 429 |
| 総月数(切り捨て) | ChronoUnit.MONTHS.between(a, b) |
同じ2日付で 14 |
| 満年数・年齢 | ChronoUnit.YEARS.between(a, b) または Period.between(a, b).getYears() |
同じ2日付で 1 |
| 「○年○か月○日」 | Period.between(a, b) の getYears() getMonths() getDays() |
1年2か月5日(P1Y2M5D) |
| 時間・分・秒の差 | Duration.between(日時1, 日時2) |
PT57H15M20S → 2日9時間15分20秒 |
| LocalDate どうしを Duration で | Duration.between(a.atStartOfDay(), b.atStartOfDay()) |
PT48H |
| 両端を含む日数 | ChronoUnit.DAYS.between(a, b) + 1 |
1/1〜1/31 で 31 |
| 差の絶対値 | Math.abs(ChronoUnit.DAYS.between(a, b)) |
-282 → 282 |
| 夏時間を考慮した時間差 | Duration.between(ZonedDateTime, ZonedDateTime) |
24時間ではなく 23時間/25時間 |
| 土日を除く日数 | a.datesUntil(b).filter(平日).count() |
2026年10月で 22 |
どの方法も「開始を含み、終了を含まない」数え方です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java 日付の差分計算|ChronoUnit・Period・Duration」
3つの方法を同じ日付で比べる
| 方法 | 返すもの | 受け取れる型 | 向いている用途 |
|---|---|---|---|
ChronoUnit.XXX.between |
指定した単位での整数(long) | LocalDate、LocalDateTime、ZonedDateTime、Instant など | 総日数、総月数、満年数 |
Period.between |
年・月・日の組(Period) | LocalDate のみ | 年齢、「○年○か月○日」の表示 |
Duration.between |
秒とナノ秒の量(Duration) | 時刻を持つ型(LocalDateTime、ZonedDateTime、Instant、LocalTime) | 経過時間、所要時間 |
import java.time.Duration;
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class Compare {
public static void main(String[] args) {
String[][] pairs = {
{"2026-01-01", "2026-01-31"}, {"2026-01-31", "2026-02-28"}, {"2026-01-31", "2026-03-01"},
{"2026-01-15", "2027-03-20"}, {"2024-02-29", "2025-02-28"}, {"2024-02-29", "2028-02-29"},
{"2025-10-03", "2026-10-03"}, {"2026-10-03", "2025-12-25"},
};
System.out.println("| 開始 → 終了 | DAYS | MONTHS | YEARS | Period | getDays() | Duration |");
System.out.println("|---|---|---|---|---|---|---|");
for (String[] p : pairs) {
LocalDate a = LocalDate.parse(p[0]);
LocalDate b = LocalDate.parse(p[1]);
Period period = Period.between(a, b);
// Duration は時刻が必要なので、その日の 0時(LocalDateTime)に直してから渡す
Duration duration = Duration.between(a.atStartOfDay(), b.atStartOfDay());
System.out.println("| " + a + " → " + b
+ " | " + ChronoUnit.DAYS.between(a, b)
+ " | " + ChronoUnit.MONTHS.between(a, b)
+ " | " + ChronoUnit.YEARS.between(a, b)
+ " | " + period
+ " | " + period.getDays()
+ " | " + duration + " |");
}
}
}
実行結果(表の形で出力しています):
| 開始 → 終了 | DAYS | MONTHS | YEARS | Period | getDays() | Duration |
|---|---|---|---|---|---|---|
| 2026-01-01 → 2026-01-31 | 30 | 0 | 0 | P30D | 30 | PT720H |
| 2026-01-31 → 2026-02-28 | 28 | 0 | 0 | P28D | 28 | PT672H |
| 2026-01-31 → 2026-03-01 | 29 | 1 | 0 | P1M1D | 1 | PT696H |
| 2026-01-15 → 2027-03-20 | 429 | 14 | 1 | P1Y2M5D | 5 | PT10296H |
| 2024-02-29 → 2025-02-28 | 365 | 11 | 0 | P11M30D | 30 | PT8760H |
| 2024-02-29 → 2028-02-29 | 1461 | 48 | 4 | P4Y | 0 | PT35064H |
| 2025-10-03 → 2026-10-03 | 365 | 12 | 1 | P1Y | 0 | PT8760H |
| 2026-10-03 → 2025-12-25 | -282 | -9 | 0 | P-9M-9D | -9 | PT-6768H |
この表から読み取れることは次のとおりです。
getDays()は総日数とは別の値です。 2026-01-15 → 2027-03-20 は総日数 429 に対してgetDays()は 5、4年ちょうどの行は 1461 に対して 0 です。- 1/31 → 2/28 は MONTHS が 0。 終了日の「日」(28)が開始日の「日」(31)に届かないため、1か月と数えません。
- 2024-02-29 → 2025-02-28 は YEARS が 0(Period は P11M30D)。総日数は 365 でも、満1年にはなりません。
- Duration の1日は常に24時間として計算されます(PT720H は 30日 × 24時間)。
Period.getDays() は総日数ではない
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class PeriodDays {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2026, 1, 15);
LocalDate end = LocalDate.of(2027, 3, 20);
Period p = Period.between(start, end);
System.out.println("Period = " + p);
System.out.println("getYears() = " + p.getYears());
System.out.println("getMonths() = " + p.getMonths());
System.out.println("getDays() = " + p.getDays() + " ← 「1年2か月と5日」の 5");
System.out.println("toTotalMonths() = " + p.toTotalMonths());
System.out.println("総日数 DAYS = " + ChronoUnit.DAYS.between(start, end));
System.out.println("総月数 MONTHS = " + ChronoUnit.MONTHS.between(start, end));
System.out.println("start.until(end) = " + start.until(end));
System.out.println("until(end, DAYS) = " + start.until(end, ChronoUnit.DAYS));
System.out.printf("表示用 = %d年%dか月%d日%n", p.getYears(), p.getMonths(), p.getDays());
// Period は足し戻せる:start + Period = end
System.out.println("start.plus(p) = " + start.plus(p));
// ofDays で作った Period は月に繰り上がらない
Period d45 = Period.ofDays(45);
System.out.println("Period.ofDays(45) = " + d45 + " / normalized() = " + d45.normalized());
Period m14 = Period.ofMonths(14);
System.out.println("Period.ofMonths(14) = " + m14 + " / normalized() = " + m14.normalized());
}
}
実行結果:
Period = P1Y2M5D
getYears() = 1
getMonths() = 2
getDays() = 5 ← 「1年2か月と5日」の 5
toTotalMonths() = 14
総日数 DAYS = 429
総月数 MONTHS = 14
start.until(end) = P1Y2M5D
until(end, DAYS) = 429
表示用 = 1年2か月5日
start.plus(p) = 2027-03-20
Period.ofDays(45) = P45D / normalized() = P45D
Period.ofMonths(14) = P14M / normalized() = P1Y2M
Period は「年・月・日」の3つの数を別々に持つ型です。公式Javadoc では、Period.between は「まず完全な月数を取り除き、残りの日数を計算し、月数を12か月=1年として年と月に分ける」と説明されています。
| ほしい値 | 使うもの |
|---|---|
| 総日数 | ChronoUnit.DAYS.between(a, b) |
| 総月数 | ChronoUnit.MONTHS.between(a, b) または period.toTotalMonths() |
| 「○年○か月○日」の各部分 | getYears() getMonths() getDays() |
normalized() は月を年へ繰り上げますが(P14M → P1Y2M)、日を月へは繰り上げません(P45D のまま)。1か月の日数が月によって違うためです。
月末・2月29日・切り上げ・逆順の動きを確認する
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class Edge {
static int age(LocalDate birthday, LocalDate today) {
return Period.between(birthday, today).getYears();
}
// 1日でも端数があれば 1か月と数える(月数の切り上げ)
static long monthsCeil(LocalDate start, LocalDate end) {
long months = ChronoUnit.MONTHS.between(start, end);
return start.plusMonths(months).isBefore(end) ? months + 1 : months;
}
public static void main(String[] args) {
System.out.println("[1] 1/31 → 2/28 は「0か月と28日」");
LocalDate jan31 = LocalDate.of(2026, 1, 31);
LocalDate feb28 = LocalDate.of(2026, 2, 28);
System.out.println("MONTHS.between = " + ChronoUnit.MONTHS.between(jan31, feb28));
System.out.println("Period.between = " + Period.between(jan31, feb28));
System.out.println("1/31.plusMonths(1) = " + jan31.plusMonths(1) + " ← 加算は月末に丸められる");
System.out.println("1/28 → 2/28 = " + Period.between(LocalDate.of(2026, 1, 28), feb28));
System.out.println("2/28 → 3/31 = " + Period.between(feb28, LocalDate.of(2026, 3, 31)));
System.out.println("[2] 2月29日生まれの年齢");
LocalDate birthday = LocalDate.of(2000, 2, 29);
for (String s : new String[] {"2026-02-27", "2026-02-28", "2026-03-01", "2028-02-28", "2028-02-29"}) {
LocalDate today = LocalDate.parse(s);
System.out.println(s + " 時点 : " + age(birthday, today) + "歳 (YEARS.between="
+ ChronoUnit.YEARS.between(birthday, today) + ")");
}
System.out.println("[3] 月数の切り捨てと切り上げ");
LocalDate start = LocalDate.of(2026, 1, 15);
for (String s : new String[] {"2026-02-14", "2026-02-15", "2026-02-16", "2026-04-15"}) {
LocalDate end = LocalDate.parse(s);
System.out.println("1/15 → " + s + " : 切り捨て=" + ChronoUnit.MONTHS.between(start, end)
+ " 切り上げ=" + monthsCeil(start, end));
}
System.out.println("[4] 逆順はマイナス");
LocalDate a = LocalDate.of(2026, 10, 3);
LocalDate b = LocalDate.of(2025, 12, 25);
long days = ChronoUnit.DAYS.between(a, b);
Period p = Period.between(a, b);
System.out.println("DAYS.between = " + days + " / Math.abs = " + Math.abs(days));
System.out.println("Period.between = " + p + " / isNegative = " + p.isNegative());
System.out.println("Period.negated() = " + p.negated());
System.out.println("[5] 両端を含めて数える(1日〜31日は31日間)");
LocalDate first = LocalDate.of(2026, 1, 1);
LocalDate last = LocalDate.of(2026, 1, 31);
System.out.println("between = " + ChronoUnit.DAYS.between(first, last));
System.out.println("between + 1 = " + (ChronoUnit.DAYS.between(first, last) + 1));
System.out.println("終了日の翌日まで = " + ChronoUnit.DAYS.between(first, last.plusDays(1)));
System.out.println("[6] うるう年をまたぐ1年間の日数");
System.out.println("2023-03-01 → 2024-03-01 = " + ChronoUnit.DAYS.between(LocalDate.of(2023, 3, 1), LocalDate.of(2024, 3, 1)));
System.out.println("2025-03-01 → 2026-03-01 = " + ChronoUnit.DAYS.between(LocalDate.of(2025, 3, 1), LocalDate.of(2026, 3, 1)));
}
}
実行結果:
[1] 1/31 → 2/28 は「0か月と28日」
MONTHS.between = 0
Period.between = P28D
1/31.plusMonths(1) = 2026-02-28 ← 加算は月末に丸められる
1/28 → 2/28 = P1M
2/28 → 3/31 = P1M3D
[2] 2月29日生まれの年齢
2026-02-27 時点 : 25歳 (YEARS.between=25)
2026-02-28 時点 : 25歳 (YEARS.between=25)
2026-03-01 時点 : 26歳 (YEARS.between=26)
2028-02-28 時点 : 27歳 (YEARS.between=27)
2028-02-29 時点 : 28歳 (YEARS.between=28)
[3] 月数の切り捨てと切り上げ
1/15 → 2026-02-14 : 切り捨て=0 切り上げ=1
1/15 → 2026-02-15 : 切り捨て=1 切り上げ=1
1/15 → 2026-02-16 : 切り捨て=1 切り上げ=2
1/15 → 2026-04-15 : 切り捨て=3 切り上げ=3
[4] 逆順はマイナス
DAYS.between = -282 / Math.abs = 282
Period.between = P-9M-9D / isNegative = true
Period.negated() = P9M9D
[5] 両端を含めて数える(1日〜31日は31日間)
between = 30
between + 1 = 31
終了日の翌日まで = 31
[6] うるう年をまたぐ1年間の日数
2023-03-01 → 2024-03-01 = 366
2025-03-01 → 2026-03-01 = 365
1/31 → 2/28 と月末の扱い
差分の計算は「終了日の日 ≧ 開始日の日」のときだけ1か月と数えます。 一方、加算の plusMonths(1) は 1/31 を 2/28 に丸めます。そのため「1/31 に1か月足すと 2/28」でも「1/31 から 2/28 までは0か月」となり、足し算と引き算が対称になりません。月末どうしを「1か月」と数えたい場合は、開始日が月末かどうかを start.lengthOfMonth() == start.getDayOfMonth() で判定し、独自の規則として実装します。
2月29日生まれの年齢
Period.between(誕生日, 今日).getYears() と ChronoUnit.YEARS.between は同じ値になります。2000-02-29 生まれの場合、うるう年ではない 2026年は 2/28 の時点で 25、3/1 に 26 になりました。 うるう年の 2028年は 2/29 に 28 になります。2/28 の時点で年齢を加算したい規則がある場合は、その規則をコードで明示します。
月数の切り上げ
MONTHS.between は切り捨てです。端数が1日でもあれば1か月と数える場合は、start.plusMonths(months) が終了日より前かどうかで 1 を足します(コード中の monthsCeil)。1/15 → 2/16 は切り捨てで 1、切り上げで 2 になりました。
逆順はマイナスになる
開始と終了を逆に渡すと、例外にはならず、マイナスの値が返ります。Period は年・月・日のすべてに同じ符号が付きます(P-9M-9D)。順序が決まっていない入力には Math.abs や period.negated()、Duration なら abs() を使います。
LocalDate に Duration を使うと UnsupportedTemporalTypeException
型の組み合わせで起きる例外と、時刻を含む型での数え方を確認します。
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.function.Supplier;
public class Errors {
static void run(String label, Supplier<Object> s) {
try {
System.out.println(label + " → " + s.get());
} catch (RuntimeException e) {
System.out.println(label + " → " + e.getClass().getName() + ": " + e.getMessage());
}
}
public static void main(String[] args) {
LocalDate d1 = LocalDate.of(2026, 10, 1);
LocalDate d2 = LocalDate.of(2026, 10, 3);
LocalDateTime t1 = LocalDateTime.of(2026, 10, 1, 23, 0);
LocalDateTime t2 = LocalDateTime.of(2026, 10, 2, 1, 0);
System.out.println("[1] LocalDate に Duration・時間の単位を使う");
run("Duration.between(LocalDate, LocalDate)", () -> Duration.between(d1, d2));
run("ChronoUnit.HOURS.between(LocalDate, LocalDate)", () -> ChronoUnit.HOURS.between(d1, d2));
run("直し方: atStartOfDay() にしてから", () -> Duration.between(d1.atStartOfDay(), d2.atStartOfDay()));
run("直し方: DAYS.between * 24", () -> ChronoUnit.DAYS.between(d1, d2) * 24);
System.out.println("[2] 型が混ざったとき(終了側が開始側の型に変換される)");
run("DAYS.between(LocalDate, LocalDateTime)", () -> ChronoUnit.DAYS.between(d1, t2));
run("DAYS.between(LocalDateTime, LocalDate)", () -> ChronoUnit.DAYS.between(t1, d2));
System.out.println("[3] LocalDateTime の DAYS は「24時間たったか」で数える");
run("10/1 23:00 → 10/2 01:00 の DAYS", () -> ChronoUnit.DAYS.between(t1, t2));
run("HOURS", () -> ChronoUnit.HOURS.between(t1, t2));
run("日付で数える: toLocalDate() 同士", () -> ChronoUnit.DAYS.between(t1.toLocalDate(), t2.toLocalDate()));
System.out.println("[4] Duration の取り出し");
Duration d = Duration.between(LocalDateTime.of(2026, 10, 1, 9, 30), LocalDateTime.of(2026, 10, 3, 18, 45, 20));
run("Duration", () -> d);
run("toDays / toHours / toMinutes", () -> d.toDays() + " / " + d.toHours() + " / " + d.toMinutes());
run("表示用", () -> String.format("%d日%d時間%d分%d秒", d.toDaysPart(), d.toHoursPart(), d.toMinutesPart(), d.toSecondsPart()));
}
}
実行結果:
[1] LocalDate に Duration・時間の単位を使う
Duration.between(LocalDate, LocalDate) → java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
ChronoUnit.HOURS.between(LocalDate, LocalDate) → java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Hours
直し方: atStartOfDay() にしてから → PT48H
直し方: DAYS.between * 24 → 48
[2] 型が混ざったとき(終了側が開始側の型に変換される)
DAYS.between(LocalDate, LocalDateTime) → 1
DAYS.between(LocalDateTime, LocalDate) → java.time.DateTimeException: Unable to obtain LocalDateTime from TemporalAccessor: 2026-10-03 of type java.time.LocalDate
[3] LocalDateTime の DAYS は「24時間たったか」で数える
10/1 23:00 → 10/2 01:00 の DAYS → 0
HOURS → 2
日付で数える: toLocalDate() 同士 → 1
[4] Duration の取り出し
Duration → PT57H15M20S
toDays / toHours / toMinutes → 2 / 57 / 3435
表示用 → 2日9時間15分20秒
| 起きること | 理由 | こうするとよい |
|---|---|---|
Duration.between(LocalDate, LocalDate) が Unsupported unit: Seconds |
Duration は秒の単位を扱える型が必要で、LocalDate は時刻を持たない | atStartOfDay() で LocalDateTime にする。日数だけなら ChronoUnit.DAYS.between |
HOURS.between(LocalDate, LocalDate) が Unsupported unit: Hours |
同上 | DAYS.between(...) * 24、または時刻を持つ型で計算 |
DAYS.between(LocalDateTime, LocalDate) が DateTimeException |
終了側を開始側の型(LocalDateTime)に変換できない | 両方を同じ型にそろえる |
| 23:00 → 翌 01:00 の DAYS が 0 | 単位が「完全に」経過した回数を数える(24時間たっていない) | 日付の差なら toLocalDate() どうしで計算 |
Period.between は引数の型が LocalDate に決まっているため、LocalDateTime を渡すとコンパイルの段階で止まります。toLocalDate() で日付にしてから渡します。
Duration から日・時間・分を取り出すときは、toHours() は総時間(57)、toHoursPart() は日を除いた残りの時間(9) です。表示には toDaysPart() toHoursPart() toMinutesPart() toSecondsPart() を組み合わせます。
夏時間のあるゾーンでは LocalDateTime と ZonedDateTime で時間差が変わる
LocalDateTime はタイムゾーンを持たないため、カレンダーと時計の数字だけで差を計算します。ZonedDateTime は実際に経過した時間で計算します。夏時間の切り替えをまたぐ2つの日時で比べます。
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
public class Dst {
static void show(String label, LocalDateTime from, LocalDateTime to, ZoneId zone) {
ZonedDateTime zf = from.atZone(zone);
ZonedDateTime zt = to.atZone(zone);
System.out.println(label + "(" + zone + ")");
System.out.println(" " + zf + " → " + zt);
System.out.println(" LocalDateTime : HOURS=" + ChronoUnit.HOURS.between(from, to)
+ " Duration=" + Duration.between(from, to));
System.out.println(" ZonedDateTime : HOURS=" + ChronoUnit.HOURS.between(zf, zt)
+ " Duration=" + Duration.between(zf, zt)
+ " DAYS=" + ChronoUnit.DAYS.between(zf, zt));
}
public static void main(String[] args) {
ZoneId ny = ZoneId.of("America/New_York");
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
// 2026年の夏時間:3月8日に始まり、11月1日に終わる
show("夏時間の開始をまたぐ", LocalDateTime.of(2026, 3, 7, 12, 0), LocalDateTime.of(2026, 3, 8, 12, 0), ny);
show("夏時間の終了をまたぐ", LocalDateTime.of(2026, 10, 31, 12, 0), LocalDateTime.of(2026, 11, 1, 12, 0), ny);
show("夏時間のないゾーン", LocalDateTime.of(2026, 3, 7, 12, 0), LocalDateTime.of(2026, 3, 8, 12, 0), tokyo);
// 存在しない時刻(2:30)は、切り替え後の時刻にずれる
System.out.println("3/8 02:30 を New_York で解釈 → " + LocalDateTime.of(2026, 3, 8, 2, 30).atZone(ny));
// 異なるゾーンどうしの差は、同じ瞬間に直して計算される
ZonedDateTime a = ZonedDateTime.of(2026, 10, 3, 9, 0, 0, 0, tokyo);
ZonedDateTime b = ZonedDateTime.of(2026, 10, 3, 9, 0, 0, 0, ny);
System.out.println("東京 9:00 → New_York 9:00 = " + Duration.between(a, b));
}
}
実行結果:
夏時間の開始をまたぐ(America/New_York)
2026-03-07T12:00-05:00[America/New_York] → 2026-03-08T12:00-04:00[America/New_York]
LocalDateTime : HOURS=24 Duration=PT24H
ZonedDateTime : HOURS=23 Duration=PT23H DAYS=1
夏時間の終了をまたぐ(America/New_York)
2026-10-31T12:00-04:00[America/New_York] → 2026-11-01T12:00-05:00[America/New_York]
LocalDateTime : HOURS=24 Duration=PT24H
ZonedDateTime : HOURS=25 Duration=PT25H DAYS=1
夏時間のないゾーン(Asia/Tokyo)
2026-03-07T12:00+09:00[Asia/Tokyo] → 2026-03-08T12:00+09:00[Asia/Tokyo]
LocalDateTime : HOURS=24 Duration=PT24H
ZonedDateTime : HOURS=24 Duration=PT24H DAYS=1
3/8 02:30 を New_York で解釈 → 2026-03-08T03:30-04:00[America/New_York]
東京 9:00 → New_York 9:00 = PT13H
| 区間(New_York) | LocalDateTime | ZonedDateTime | DAYS(ZonedDateTime) |
|---|---|---|---|
| 3/7 12:00 → 3/8 12:00(夏時間の開始) | 24時間 | 23時間 | 1 |
| 10/31 12:00 → 11/1 12:00(夏時間の終了) | 24時間 | 25時間 | 1 |
実際に経過した時間が必要なら ZonedDateTime か Instant、カレンダー上の日数が必要なら DAYS や LocalDate を使います。ZonedDateTime の DAYS.between は、23時間や25時間の日でも 1 と数えました。日本時間(Asia/Tokyo)では、今回の区間でどちらの型でも 24時間です。サーバーのタイムゾーンが UTC、利用者が別の地域、という構成では、どのゾーンの「日付」で差を取るのかを先に決めて、atZone(zone).toLocalDate() で日付にしてから計算すると結果が安定します。
営業日数(土日を除く日数)を数える
標準ライブラリには営業日を数えるメソッドが無いため、自分で書きます。3つの方法と、「n 営業日後」を求めるコードです。
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Set;
public class BusinessDays {
static boolean isWeekend(LocalDate d) {
DayOfWeek w = d.getDayOfWeek();
return w == DayOfWeek.SATURDAY || w == DayOfWeek.SUNDAY;
}
// 方法1:1日ずつ数える(start を含み、end を含まない)
static long countLoop(LocalDate start, LocalDate end) {
long count = 0;
for (LocalDate d = start; d.isBefore(end); d = d.plusDays(1)) {
if (!isWeekend(d)) count++;
}
return count;
}
// 方法2:datesUntil(Java 9 以降)で Stream にする。休みの日の一覧も除外できる
static long countStream(LocalDate start, LocalDate end, Set<LocalDate> holidays) {
return start.datesUntil(end)
.filter(d -> !isWeekend(d))
.filter(d -> !holidays.contains(d))
.count();
}
// 方法3:週単位で計算する(期間が長くても一定の手数)
static long countFormula(LocalDate start, LocalDate end) {
long days = ChronoUnit.DAYS.between(start, end);
long weeks = days / 7;
long count = weeks * 5;
LocalDate d = start.plusWeeks(weeks);
while (d.isBefore(end)) {
if (!isWeekend(d)) count++;
d = d.plusDays(1);
}
return count;
}
public static void main(String[] args) {
LocalDate start = LocalDate.of(2026, 10, 1); // 木曜
LocalDate end = LocalDate.of(2026, 11, 1); // 10月の1か月分(11/1 は含まない)
Set<LocalDate> holidays = Set.of(LocalDate.of(2026, 10, 12), LocalDate.of(2026, 10, 30));
System.out.println("全日数 = " + ChronoUnit.DAYS.between(start, end));
System.out.println("土日を除く(ループ) = " + countLoop(start, end));
System.out.println("土日を除く(Stream) = " + countStream(start, end, Set.of()));
System.out.println("土日を除く(週単位) = " + countFormula(start, end));
System.out.println("休みの日も除く = " + countStream(start, end, holidays));
LocalDate longEnd = LocalDate.of(2036, 10, 1);
System.out.println("10年分 ループ=" + countLoop(start, longEnd) + " 週単位=" + countFormula(start, longEnd));
// 3営業日後の日付を求める
LocalDate d = LocalDate.of(2026, 10, 2); // 金曜
int remaining = 3;
while (remaining > 0) {
d = d.plusDays(1);
if (!isWeekend(d)) remaining--;
}
System.out.println("10/2(金) の3営業日後 = " + d + " (" + d.getDayOfWeek() + ")");
}
}
実行結果:
全日数 = 31
土日を除く(ループ) = 22
土日を除く(Stream) = 22
土日を除く(週単位) = 22
休みの日も除く = 20
10年分 ループ=2609 週単位=2609
10/2(金) の3営業日後 = 2026-10-07 (WEDNESDAY)
| 方法 | 特徴 | 向いている場面 |
|---|---|---|
| ループ | 読みやすい。どの Java バージョンでも書ける | 数か月〜数年の範囲 |
datesUntil と Stream |
休みの日の一覧(Set)を filter で足しやすい |
祝日や独自の休みを除きたいとき |
| 週単位の計算 | 期間の長さに関係なく、ループは最大6回 | 大量の行で繰り返し計算するとき |
祝日は年によって変わるため、Set などで外から渡す形にしておくと、データの差し替えだけで対応できます。3つの方法が同じ結果(22日、10年分で 2609日)になることも出力で確認できます。
どれを使うかの判断表
| 状況 | 選ぶもの |
|---|---|
| 「あと何日」「何日経過」 | ChronoUnit.DAYS.between(LocalDate) |
| 年齢、勤続年数 | ChronoUnit.YEARS.between または Period.between(...).getYears() |
| 「○年○か月○日」の表示 | Period.between |
| 契約月数など、端数を1か月と数える | MONTHS.between + 切り上げの判定 |
| 処理時間、滞在時間 | Duration.between(Instant や LocalDateTime) |
| 地域をまたぐ・夏時間がある | ZonedDateTime または Instant で Duration.between |
| 平日だけ数える | datesUntil と filter、または週単位の計算 |
よくある質問
Q. ChronoUnit.DAYS.between と Period.between(...).getDays() は同じですか? A. 別の値です。前者は総日数、後者は年と月を取り除いた残りの日数です。2026-01-15 → 2027-03-20 では 429 と 5 になりました。1か月未満の区間ではたまたま同じ値になるため、テストでは月をまたぐ日付で確認します。
Q. start.until(end) と between の違いは?
A. 同じ計算です。start.until(end) は Period.between(start, end)、start.until(end, ChronoUnit.DAYS) は ChronoUnit.DAYS.between(start, end) と同じ結果になります(公式Javadoc にも等価と書かれています)。
Q. 終了日も含めて数えたいときは?
A. 結果に 1 を足すか、終了日に plusDays(1) してから渡します。1/1〜1/31 は between では 30、両端を含めると 31 です。
Q. LocalDateTime どうしで日数を数えると 1日少なくなります。
A. DAYS.between は24時間が完全に経過した回数を数えるためです。10/1 23:00 → 10/2 01:00 は 0 になります。カレンダー上の日付の差がほしいときは、toLocalDate() で日付にしてから計算します。
まとめ
- 総日数・総月数・満年数は
ChronoUnit.XXX.between、年月日の組はPeriod.between、時間の量はDuration.between Period.getDays()は総日数ではなく、年と月を除いた残りの日数- 月は「終了日の日 ≧ 開始日の日」で1か月。1/31 → 2/28 は 0か月28日
- 2/29 生まれは、うるう年でない年は 3/1 に年齢が増える(Period・YEARS の計算結果)
- 逆順はマイナス。例外にはならないので、必要なら
Math.absやnegated() - LocalDate に Duration や HOURS を使うと UnsupportedTemporalTypeException。
atStartOfDay()で時刻を付ける - 夏時間のあるゾーンでは、ZonedDateTime の時間差が 23時間/25時間になる
- 土日を除く日数は
datesUntilとfilterで書け、休みの日の Set も除外できる
参考・出典
- Java SE 21 API ドキュメント「ChronoUnit」(between の数え方、until との等価) https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/temporal/ChronoUnit.html
- Java SE 21 API ドキュメント「Period」(between の計算方法) https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Period.html
- Java SE 21 API ドキュメント「Duration」(between が必要とする単位、1日=24時間) https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Duration.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java 日付の差分計算|ChronoUnit・Period・Duration」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。