- JavaScript
JavaScript日付計算まとめ|加算・減算・日数差分・月末取得をコードで解説
JavaScriptの日付計算とは、Dateオブジェクトの「年・月・日」を取り出して数値を足し引きし、新しい日付を作る処理です。 日の加算は setDate(getDate() + n)、月末は new Date(年, 月 + 1, 0)、日数の差分は「2つの日付をUTCの0時にそろえてから引き算して 86400000 で割る」が基本形になります。
この記事では、日・週・月・年の加減算、日数・月数・年齢の差分、月初・月末・翌月末、うるう年、営業日、曜日を、そのまま貼って使える関数集としてまとめました。すべて Node.js 22.22.2 で動かし、出力をそのまま載せています。さらに、1月31日に1か月足すと3月になる「月あふれ」、new Date("2026-10-03") がUTC扱いになって日付が1日ずれる現象、夏時間で差分が合わなくなる例を再現し、解決するコードまで示します。最後に、同じ処理を Date / Temporal / date-fns / Day.js で書いた対比表と、Temporal の対応状況(2026年10月時点)も整理します。
JavaScript日付計算の早見表
基準日を 2026年10月3日(土)としたときの書き方と結果です。関数名は、次の章の「コピペ用の関数集」のものです。
| やりたいこと | 書き方(関数集) | 素のDateでの中身 | 結果 |
|---|---|---|---|
| 10日後 | addDays(d, 10) |
new Date(y, m, 日 + 10) |
2026-10-13 |
| 10日前 | addDays(d, -10) |
new Date(y, m, 日 - 10) |
2026-09-23 |
| 2週間後 | addWeeks(d, 2) |
日数に 7 を掛けて足す | 2026-10-17 |
| 3か月後 | addMonths(d, 3) |
1日にしてから月を動かし、月末で丸める | 2027-01-03 |
| 1年後 | addYears(d, 1) |
12か月の加算として扱う | 2027-10-03 |
| 月初 | startOfMonth(d) |
new Date(y, m, 1) |
2026-10-01 |
| 月末 | endOfMonth(d) |
new Date(y, m + 1, 0) |
2026-10-31 |
| 翌月末 | endOfNextMonth(d) |
new Date(y, m + 2, 0) |
2026-11-30 |
| その月の日数 | daysInMonth(2028, 2) |
new Date(y, 月, 0).getDate() |
29 |
| うるう年の判定 | isLeapYear(2028) |
4・100・400で割った余りを見る | true |
| 日数の差分 | diffDays(d, 2027-01-01) |
UTCの0時にそろえて引き算 | 90 |
| 月数の差分 | diffMonths(2026-01-31, d) |
年と月の差から、満たない月を引く | 8 |
| 年齢 | calcAge(1990-10-04, d) |
年の差から、誕生日前なら1引く | 35 |
| 曜日 | weekday(d) |
getDay()(0が日曜) |
土 |
| 5営業日後 | addBusinessDays(d, 5) |
土日と祝日リストを飛ばして数える | 2026-10-09 |
| 文字列から日付 | parseLocal("2026-10-03") |
年・月・日に分けて new Date(y, m - 1, d) |
ローカルの0時 |
最初に押さえる Date の3つの性質
関数集を読む前に、Dateの性質を3つだけ確認します。日付計算でつまずく箇所の多くは、この3つから説明できます。
| 性質 | 内容 | 計算への影響 |
|---|---|---|
| 月は0始まり | getMonth() は1月が0、12月が11 |
表示や new Date(y, m, d) で1ずれやすい |
| 範囲外の値は自動で繰り上がる | 32日は翌月1日、0日は前月の末日になる | 月末取得や日の加算に使える。一方で月あふれの原因にもなる |
| setで始まるメソッドは元の値を書き換える | setDate() などは新しいDateを返さない |
同じDateを別の変数で共有していると、両方変わる |
次のコードで3つの性質を確かめます。
// 自動繰り上がり: 10月32日 / 13番目の月 / 10月0日
console.log(
new Date(2026, 9, 32).toLocaleDateString("ja-JP"),
new Date(2026, 12, 1).toLocaleDateString("ja-JP"),
new Date(2026, 9, 0).toLocaleDateString("ja-JP")
);
// set系は元のDateを書き換える
const a = new Date(2026, 9, 3);
const b = a;
b.setDate(b.getDate() + 7);
console.log(a.toLocaleDateString("ja-JP"), a === b);
実行結果(Node.js 22.22.2、TZ=Asia/Tokyo):
2026/11/1 2027/1/1 2026/9/30
2026/10/10 true
b を7日進めたら a も10月10日になりました。日付計算の関数は「元のDateを変えずに新しいDateを返す」形にしておくと、呼び出し側で安心して使えます。 この記事の関数集はすべてその形です。
コピペ用の関数集(素のDateだけで動く)
外部ライブラリなしで動く関数集です。ファイル名を date-utils.mjs として保存すれば、Node.js でもブラウザ(type="module")でもそのまま読み込めます。
// date-utils.mjs
// 表示用: ローカル時刻で YYYY-MM-DD
const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const WD = ["日", "月", "火", "水", "木", "金", "土"];
// --- 加算・減算(元の Date は変更せず、新しい Date を返す) ---
const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n, d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
const addWeeks = (d, n) => addDays(d, n * 7);
// 月の加算: 月あふれを月末に丸める
const addMonths = (d, n) => {
const r = new Date(d.getTime());
const day = r.getDate();
r.setDate(1); // いったん1日にしてから月を動かす
r.setMonth(r.getMonth() + n);
const last = new Date(r.getFullYear(), r.getMonth() + 1, 0).getDate();
r.setDate(Math.min(day, last)); // 元の日と月末日の小さい方
return r;
};
const addYears = (d, n) => addMonths(d, n * 12);
// --- 月初・月末 ---
const startOfMonth = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
const endOfMonth = (d) => new Date(d.getFullYear(), d.getMonth() + 1, 0);
const endOfNextMonth = (d) => new Date(d.getFullYear(), d.getMonth() + 2, 0);
const daysInMonth = (y, m) => new Date(y, m, 0).getDate(); // m は 1〜12
const isLeapYear = (y) => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
// --- 差分 ---
const toUTCDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
const diffDays = (a, b) => Math.round((toUTCDay(b) - toUTCDay(a)) / 86400000);
const diffMonths = (a, b) => {
let m = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
if (addMonths(a, m) > b) m--; // まだ満たない月は数えない
return m;
};
const calcAge = (birth, today) => {
let age = today.getFullYear() - birth.getFullYear();
const md = (x) => x.getMonth() * 100 + x.getDate();
if (md(today) < md(birth)) age--;
return age;
};
// --- 曜日・営業日 ---
const weekday = (d) => WD[d.getDay()];
const isBusinessDay = (d, holidays = []) => d.getDay() !== 0 && d.getDay() !== 6 && !holidays.includes(fmt(d));
const addBusinessDays = (d, n, holidays = []) => {
let r = new Date(d.getTime());
const step = n >= 0 ? 1 : -1;
let left = Math.abs(n);
while (left > 0) {
r = addDays(r, step);
if (isBusinessDay(r, holidays)) left--;
}
return r;
};
const countBusinessDays = (a, b, holidays = []) => {
let c = 0;
for (let x = addDays(a, 1); toUTCDay(x) <= toUTCDay(b); x = addDays(x, 1)) if (isBusinessDay(x, holidays)) c++;
return c;
};
// "YYYY-MM-DD" をローカルの0時として読む
const parseLocal = (s) => { const [y, m, d] = s.split("-").map(Number); return new Date(y, m - 1, d); };
export { fmt, addDays, addWeeks, addMonths, addYears, startOfMonth, endOfMonth, endOfNextMonth, daysInMonth, isLeapYear, diffDays, diffMonths, calcAge, weekday, isBusinessDay, addBusinessDays, countBusinessDays, parseLocal };
呼び出し側は import * as F from "./date-utils.mjs"; で読み込み、基準日を new Date(2026, 9, 3)(月は0始まりなので 9)として各関数を呼び出します。
全関数を呼び出した実行結果(Node.js 22.22.2、TZ=Asia/Tokyo node run.mjs):
基準日 2026-10-03(土)
addDays(base, 10) 2026-10-13
addDays(base, -10) 2026-09-23
addDays(base, 30) 2026-11-02
addWeeks(base, 2) 2026-10-17
addMonths(base, 3) 2027-01-03
addMonths(base, -10) 2025-12-03
addYears(base, 1) 2027-10-03
addMonths(2026-01-31, 1) 2026-02-28
addMonths(2026-03-31, 1) 2026-04-30
addMonths(2024-01-31, 1) 2024-02-29
addMonths(2026-03-31, -1) 2026-02-28
addYears(2024-02-29, 1) 2025-02-28
addYears(2024-02-29, 4) 2028-02-29
startOfMonth(base) 2026-10-01
endOfMonth(base) 2026-10-31
endOfNextMonth(base) 2026-11-30
endOfMonth(2026-02-10) 2026-02-28
endOfMonth(2028-02-10) 2028-02-29
endOfNextMonth(2026-12-15) 2027-01-31
daysInMonth(2026, 2) 28
daysInMonth(2028, 2) 29
isLeapYear 2024/2026/2100/2000 true false false true
diffDays(2026-10-03, 2027-01-01) 90
diffDays(2026-10-03, 2026-09-01) -32
diffDays(2024-02-28, 2024-03-01) 2
diffMonths(2026-01-31, 2026-10-03) 8
diffMonths(2026-01-31, 2026-02-28) 1
diffMonths(2025-10-04, 2026-10-03) 11
calcAge(1990-10-03, base) 36
calcAge(1990-10-04, base) 35
calcAge(2000-02-29, 2026-02-28) 25
calcAge(2000-02-29, 2026-03-01) 26
weekday(2026-10-03) 土
isBusinessDay(2026-10-03) false
addBusinessDays(base, 5) 2026-10-09
addBusinessDays(base, -3) 2026-09-30
countBusinessDays(10-03, 10-31) 20
countBusinessDays(同, 祝日) 19
parseLocal('2026-10-03') Sat Oct 03 2026 00:00:00 GMT+0900 (Japan Standard Time)
以降の章では、この関数集の中身を「なぜその書き方なのか」とあわせて順に説明します。
日・週・時間を足す、引く
日の加算と減算
日の加減算は、年・月・日を取り出して、日の部分に数値を足した新しいDateを作るのが分かりやすい形です。日が月の日数を超えても、Dateが自動で翌月へ繰り上げます。
関数集の addDays がこの形で、addDays(base, 30) は 2026-11-02(10月は31日までなので自動で11月へ)、addDays(base, -10) は 2026-09-23 になります。
よく見かける d.setDate(d.getDate() + n) も同じ結果になりますが、こちらは d 自体を書き換えます。元の日付を残したいときは new Date(d.getTime()) でコピーしてから呼び出します。
new Date(d.getTime() + n * 86400000) のようにミリ秒を足す方法は、夏時間のある地域では時刻が1時間動くことがあります(後述の夏時間の章で再現します)。「カレンダー上の1日」を足したいときは、日の部分に足す書き方を選びます。
時間・分の加算
時間や分は、経過時間そのものを足したいことが多いので、ミリ秒で計算します。
const addHours = (x, n) => new Date(x.getTime() + n * 3600000);
const addMinutes = (x, n) => new Date(x.getTime() + n * 60000);
const d = new Date(2026, 9, 3, 23, 30);
console.log(addHours(d, 3).toLocaleString("ja-JP"));
console.log(addMinutes(d, -45).toLocaleString("ja-JP"));
実行結果:
2026/10/4 2:30:00
2026/10/3 22:45:00
23時30分に3時間足すと、日付も自動で10月4日に進みます。
月・年を足す:1月31日の1か月後が3月になる「月あふれ」
現象を再現する
月の加算で setMonth(getMonth() + 1) をそのまま使うと、月末付近で意図と違う日付になります。
const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
for (const [y, m, d] of [[2026, 0, 31], [2026, 2, 31], [2024, 0, 31], [2026, 7, 31]]) {
const x = new Date(y, m, d);
const s = fmt(x);
x.setMonth(x.getMonth() + 1);
console.log("setMonth+1:", s, "->", fmt(x));
}
const y = new Date(2024, 1, 29);
y.setFullYear(2025);
console.log("setFullYear:", "2024-02-29 ->", fmt(y));
実行結果:
setMonth+1: 2026-01-31 -> 2026-03-03
setMonth+1: 2026-03-31 -> 2026-05-01
setMonth+1: 2024-01-31 -> 2024-03-02
setMonth+1: 2026-08-31 -> 2026-10-01
setFullYear: 2024-02-29 -> 2025-03-01
1月31日の1か月後が3月3日になりました。 理由は「自動繰り上がり」です。setMonth は月だけを2月に変えるので、内部では「2月31日」という存在しない日付になります。2026年2月は28日までなので、3日分があふれて3月3日に進みます。うるう年の2024年では2月が29日まであるため、あふれは2日分で3月2日です。年の加算でも同じことが起き、2月29日の1年後は3月1日になります。
月末に丸める関数で解決する
多くの場面で期待されるのは「1月31日の1か月後は2月の末日」という結果です。次の手順で実現できます。
- 日をいったん1日にする(あふれが起きない状態にする)
- 月を動かす
- 移動先の月の末日を調べ、「元の日」と「末日」の小さい方を日にする
関数集の addMonths はこの3手順をそのままコードにしたものです。addYears は12か月の加算として addMonths を呼ぶので、2月29日の扱いも同じ規則になります。
直す前と後の比較です(すべて実行して確かめた結果です)。
| 入力 | setMonth / setFullYear をそのまま使う | addMonths / addYears |
|---|---|---|
| 2026-01-31 の1か月後 | 2026-03-03 | 2026-02-28 |
| 2026-03-31 の1か月後 | 2026-05-01 | 2026-04-30 |
| 2024-01-31 の1か月後 | 2024-03-02 | 2024-02-29 |
| 2026-03-31 の1か月前 | 2026-03-03 | 2026-02-28 |
| 2024-02-29 の1年後 | 2025-03-01 | 2025-02-28 |
| 2024-02-29 の4年後 | 2028-02-29 | 2028-02-29 |
後述の Temporal・date-fns・Day.js も、月の加算は同じ「月末に丸める」動きでした。 自作関数の結果がライブラリと一致するので、あとから置き換えるときも結果が変わりません。
なお、月末に丸めた日付にさらに1か月足すと、2月28日 → 3月28日となり、3月31日には戻りません。「毎月末」を順に求めたいときは、足し算を繰り返すのではなく、起点の日付から n か月後を毎回計算するか、次の章の endOfMonth を使います。
月初・月末・翌月末・うるう年を求める
月末は「翌月の0日」
new Date(年, 月, 0) の「0日」は、前の月の末日に繰り上がり(繰り下がり)ます。これを使うと、月ごとの日数を覚えなくても月末が取れます。
| 呼び出し | 結果 | ポイント |
|---|---|---|
endOfMonth(2026-10-03) |
2026-10-31 | 11月0日 = 10月31日 |
endOfMonth(2026-02-10) |
2026-02-28 | 平年の2月 |
endOfMonth(2028-02-10) |
2028-02-29 | うるう年の2月も自動で29日 |
endOfNextMonth(2026-10-03) |
2026-11-30 | 12月0日 = 11月30日 |
endOfNextMonth(2026-12-15) |
2027-01-31 | 月が13以上でも年へ繰り上がる |
daysInMonth(2028, 2) |
29 | 引数の月は1〜12で渡せる |
daysInMonth(y, m) は、月を1〜12で受け取ると new Date(y, m, 0) がちょうど「m月の末日」になる点を利用しています。月が0始まりであることを逆手に取った書き方です。
締め日や支払期日のように「翌月末」「翌々月10日」を求める処理は、この形の組み合わせで書けます。
// 翌々月10日
const d = new Date(2026, 9, 3);
new Date(d.getFullYear(), d.getMonth() + 2, 10); // 2026-12-10
実行すると 2026-12-10 になりました。月に2を足して日を10に固定するだけなので、あふれは起きません。
うるう年の判定
うるう年は「4で割り切れる年。ただし100で割り切れる年は平年、400で割り切れる年はうるう年」です。
実行結果では、2024 → true、2026 → false、2100 → false、2000 → true でした。daysInMonth(y, 2) === 29 でも同じ判定ができます。
日数・月数・年齢の差分を求める
日数の差分
2つのDateを引き算すると、ミリ秒の差が得られます。1日は 86,400,000 ミリ秒なので、割れば日数になります。ただし、時刻が入っていたり夏時間をまたいだりすると端数が出るので、両方を「UTCの0時」にそろえてから引くと安定します。
const toUTCDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
const diffDays = (a, b) => Math.round((toUTCDay(b) - toUTCDay(a)) / 86400000);
diffDays(new Date(2026, 9, 3), new Date(2027, 0, 1)); // 90
diffDays(new Date(2026, 9, 3), new Date(2026, 8, 1)); // -32(過去は負の数)
diffDays(new Date(2024, 1, 28), new Date(2024, 2, 1)); // 2(うるう年の2月29日を含む)
Date.UTC(年, 月, 日) は、ローカルの年月日をUTCの0時とみなしたミリ秒を返します。UTCには夏時間がないので、1日はいつでも 86,400,000 ミリ秒ちょうどです。
時間・分の差分は、そのままミリ秒を割ります。
const e = new Date(2026, 9, 3, 15, 0), s = new Date(2026, 9, 3, 9, 30);
const ms = e - s;
console.log(ms, Math.floor(ms / 3600000) + "時間" + (ms % 3600000) / 60000 + "分");
実行結果:
19800000 5時間30分
月数の差分
月数は「年の差 × 12 + 月の差」を出し、まだ満たない月があれば1引きます。
const diffMonths = (a, b) => {
let m = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
if (addMonths(a, m) > b) m--;
return m;
};
| 開始 | 終了 | 結果 | 補足 |
|---|---|---|---|
| 2026-01-31 | 2026-10-03 | 8 | 9月30日で8か月、10月31日で9か月 |
| 2026-01-31 | 2026-02-28 | 1 | 月末同士は1か月と数える(月末丸めと同じ考え方) |
| 2025-10-04 | 2026-10-03 | 11 | 1日足りないので12か月にならない |
「月末同士を1か月と数えるか」は決めごとです。この関数は addMonths と対になるように作ってあるので、addMonths(a, diffMonths(a, b)) が b を超えないことが保証されます。
年齢の計算
年齢は、年の差を出してから、今年の誕生日がまだ来ていなければ1引きます。
const calcAge = (birth, today) => {
let age = today.getFullYear() - birth.getFullYear();
const md = (x) => x.getMonth() * 100 + x.getDate();
if (md(today) < md(birth)) age--;
return age;
};
| 生年月日 | 基準日 | 結果 |
|---|---|---|
| 1990-10-03 | 2026-10-03 | 36(誕生日当日に加算) |
| 1990-10-04 | 2026-10-03 | 35(誕生日の前日) |
| 2000-02-29 | 2026-02-28 | 25 |
| 2000-02-29 | 2026-03-01 | 26 |
2月29日生まれの人は、この関数では平年の3月1日に年齢が上がります。2月28日に上げたい場合は、比較の前に誕生日を月末で丸める処理を足します。どちらに合わせるかは、扱う制度や仕様書の定義に従ってください。
夏時間で日数の差分が合わない例と対策
日本には夏時間がありませんが、利用者が海外にいる場合や、サーバーのタイムゾーンが海外に設定されている場合は影響を受けます。アメリカ東部(America/New_York)では、2026年3月8日に夏時間が始まり、その日は23時間しかありません。
// dst.mjs 実行: TZ=America/New_York node dst.mjs
console.log("TZ =", process.env.TZ);
const a = new Date(2026, 2, 7), b = new Date(2026, 2, 9); // 3/7 0:00 と 3/9 0:00
const ms = b - a;
console.log("ミリ秒差 / 86400000 =", ms / 86400000);
console.log("Math.floor =", Math.floor(ms / 86400000));
const toUTCDay = (d) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
console.log("diffDays (UTC換算) =", (toUTCDay(b) - toUTCDay(a)) / 86400000);
const c = new Date(2026, 9, 31), d = new Date(2026, 10, 2); // 11/1 に夏時間終了
console.log("秋: ミリ秒差 / 86400000 =", (d - c) / 86400000, " diffDays =", (toUTCDay(d) - toUTCDay(c)) / 86400000);
const x = new Date(2026, 2, 7, 12);
const y = new Date(x.getTime() + 86400000);
console.log("3/7 12:00 + 24時間分のミリ秒 =", y.toString().slice(0, 24));
const z = new Date(2026, 2, 7, 12);
z.setDate(z.getDate() + 1);
console.log("3/7 12:00 に setDate(+1) =", z.toString().slice(0, 24));
実行結果(TZ=America/New_York):
TZ = America/New_York
ミリ秒差 / 86400000 = 1.9583333333333333
Math.floor = 1
diffDays (UTC換算) = 2
秋: ミリ秒差 / 86400000 = 2.0416666666666665 diffDays = 2
3/7 12:00 + 24時間分のミリ秒 = Sun Mar 08 2026 13:00:00
3/7 12:00 に setDate(+1) = Sun Mar 08 2026 12:00:00
同じコードを TZ=Asia/Tokyo で動かすと、ミリ秒差はちょうど 2、どちらの加算も 12:00 のままでした。
日数の差分は UTC にそろえてから、日の加算は日の部分に足す。 この2点を守れば、実行する地域によらず同じ結果になります。開発中のパソコンが日本時間でも、TZ=America/New_York node ファイル名 のように環境変数を付けるだけで、別のタイムゾーンでの動きを確かめられます。
new Date("2026-10-03") で日付が1日ずれる現象
文字列の形で解釈が変わる
new Date() に文字列を渡すとき、ハイフン区切りの日付だけ("2026-10-03")は UTC の0時として解釈されます。 時刻付き("2026-10-03T00:00:00")や、数値を並べる書き方はローカル時刻です。
// trap.mjs
const a = new Date("2026-10-03"), b = new Date("2026-10-03T00:00:00"),
c = new Date("2026/10/03"), e = new Date(2026, 9, 3);
for (const [k, v] of [['new Date("2026-10-03")', a], ['new Date("2026-10-03T00:00:00")', b],
['new Date("2026/10/03")', c], ['new Date(2026, 9, 3)', e]])
console.log(k.padEnd(34), v.toString().slice(0, 33), "| getDate()=" + v.getDate(), "| ISO=" + v.toISOString());
実行結果(TZ=Asia/Tokyo):
new Date("2026-10-03") Sat Oct 03 2026 09:00:00 GMT+0900 | getDate()=3 | ISO=2026-10-03T00:00:00.000Z
new Date("2026-10-03T00:00:00") Sat Oct 03 2026 00:00:00 GMT+0900 | getDate()=3 | ISO=2026-10-02T15:00:00.000Z
new Date("2026/10/03") Sat Oct 03 2026 00:00:00 GMT+0900 | getDate()=3 | ISO=2026-10-02T15:00:00.000Z
new Date(2026, 9, 3) Sat Oct 03 2026 00:00:00 GMT+0900 | getDate()=3 | ISO=2026-10-02T15:00:00.000Z
実行結果(TZ=America/New_York):
new Date("2026-10-03") Fri Oct 02 2026 20:00:00 GMT-0400 | getDate()=2 | ISO=2026-10-03T00:00:00.000Z
(残り3行は、いずれも Sat Oct 03 2026 00:00:00 GMT-0400、getDate()=3)
読み取れることは2つです。
- 日本時間では、
new Date("2026-10-03")は 9:00 になる。 日付は同じ3日ですが、0時のつもりで他の日付と差分を取ると9時間ずれます。 - UTCより遅れている地域(ニューヨークなど)では、
getDate()が 2 になる。 日付そのものが1日前になります。
日本時間で起きやすいのは toISOString() 側のずれ
日本では逆向きのずれがよく起きます。ローカルの0時で作ったDateを toISOString() で文字列にすると、UTCに直されて前日になります。
const m = new Date(2026, 9, 3);
console.log(m.toISOString().slice(0, 10), m.toLocaleDateString("sv-SE"));
実行結果(TZ=Asia/Tokyo):
2026-10-02 2026-10-03
toISOString().slice(0, 10) は 2026-10-02 を返しました。日本時間の0時〜8時59分は、UTCではまだ前日だからです。
解決:読むときも書くときもローカルにそろえる
| 場面 | ずれる書き方 | そろう書き方 |
|---|---|---|
| "YYYY-MM-DD" を読む | new Date("2026-10-03") |
parseLocal("2026-10-03")(年・月・日に分けて渡す) |
| "YYYY-MM-DD" に書き出す | d.toISOString().slice(0, 10) |
fmt(d)(getFullYear などローカルの値で組み立てる) |
| 入力欄(type="date")の値を使う | 値をそのまま new Date() に渡す |
parseLocal(value) に通す |
| サーバーへ日時を送る | ローカルの文字列を送る | toISOString() でUTCのまま送り、表示のときにローカルへ直す |
「日付だけ」を扱う処理はローカルの年月日で読み書きし、「ある瞬間」を扱う処理はUTCのまま持ち回る、と役割を分けると整理しやすくなります。なお、"2026/10/03" のようなスラッシュ区切りは、今回の Node.js 22 ではローカル時刻として読まれましたが、仕様で決められた形式ではないので、parseLocal のように数値で渡す方法が確実です。
曜日と営業日を求める
曜日
getDay() は 0(日曜)〜6(土曜)を返します。配列を引けば日本語の曜日になります。Intl.DateTimeFormat を使う方法もあります。
const m = new Date(2026, 9, 3);
console.log(new Intl.DateTimeFormat("ja-JP", { weekday: "long" }).format(m), m.getDay());
実行結果:
土曜日 6
営業日の加算と日数
営業日は「土日と、祝日リストに含まれる日を飛ばして数える」処理です。祝日はJavaScriptに組み込まれていないので、"YYYY-MM-DD" の配列として渡す形にしています。
const hol = ["2026-10-12"]; // 祝日リスト(例: 2026年10月12日)
const base = new Date(2026, 9, 3); // 土曜日
fmt(addBusinessDays(base, 5)); // 2026-10-09
fmt(addBusinessDays(base, 6)); // 2026-10-12
fmt(addBusinessDays(base, 6, hol)); // 2026-10-13(12日を飛ばす)
fmt(addBusinessDays(base, -3)); // 2026-09-30
countBusinessDays(base, new Date(2026, 9, 31)); // 20
countBusinessDays(base, new Date(2026, 9, 31), hol); // 19
countBusinessDays は「開始日の翌日から終了日まで」を数えます。開始日も含めたい場合は、開始日に addDays(a, -1) を渡します。祝日リストは年ごとに更新が必要なので、設定ファイルやデータベースに置いて差し替えられるようにしておくと運用しやすくなります。
Date / Temporal / date-fns / Day.js の書き方対比表
同じ処理を4つの方法で書き、すべて同じ結果になることを確かめました。検証した版は、date-fns 4.4.0、Day.js 1.11.23、Temporal は @js-temporal/polyfill 0.5.1 です(Node.js 22.22.2 には Temporal が組み込まれていないため、ポリフィルで確認しています)。
| やりたいこと | Date(この記事の関数) | Temporal | date-fns | Day.js | 結果 |
|---|---|---|---|---|---|
| 10日後 | addDays(d, 10) |
t.add({ days: 10 }) |
addDays(d, 10) |
j.add(10, "day") |
2026-10-13 |
| 1/31 の1か月後 | addMonths(d, 1) |
t.add({ months: 1 }) |
addMonths(d, 1) |
j.add(1, "month") |
2026-02-28 |
| 2024-02-29 の1年後 | addYears(d, 1) |
t.add({ years: 1 }) |
addYears(d, 1) |
j.add(1, "year") |
2025-02-28 |
| 月初 | startOfMonth(d) |
t.with({ day: 1 }) |
startOfMonth(d) |
j.startOf("month") |
2026-10-01 |
| 月末 | endOfMonth(d) |
t.with({ day: t.daysInMonth }) |
endOfMonth(d) |
j.endOf("month") |
2026-10-31 |
| 翌月末 | endOfNextMonth(d) |
月初に1か月足してから月末 | endOfMonth(addMonths(d, 1)) |
j.add(1, "month").endOf("month") |
2026-11-30 |
| 日数の差分 | diffDays(a, b) |
a.until(b).days |
differenceInCalendarDays(b, a) |
b.diff(a, "day") |
90 |
| 月数の差分 | diffMonths(a, b) |
a.until(b, { largestUnit: "month" }) |
differenceInMonths(b, a) |
b.diff(a, "month") |
8 |
| 年齢 | calcAge(birth, d) |
birth.until(t, { largestUnit: "year" }).years |
differenceInYears(d, birth) |
j.diff(birth, "year") |
35 |
| その月の日数 | daysInMonth(y, m) |
t.daysInMonth |
getDaysInMonth(d) |
j.daysInMonth() |
31 |
| うるう年 | isLeapYear(y) |
t.inLeapYear |
isLeapYear(d) |
j.isLeapYear()(isLeapYear プラグイン) |
false |
| 曜日 | d.getDay()(0=日) |
t.dayOfWeek(1=月〜7=日) |
getDay(d)(0=日) |
j.day()(0=日) |
土曜 |
| 文字列を読む | parseLocal(s) |
Temporal.PlainDate.from(s) |
parseISO(s) |
dayjs(s) |
ローカルの0時 |
引数の順序に違いがあります。date-fns の差分関数は「後の日付、前の日付」の順、Day.js は「後の日付.diff(前の日付)」、Temporal は「前の日付.until(後の日付)」です。
Temporal のコードと実行結果
// 実行: npm i @js-temporal/polyfill してから TZ=Asia/Tokyo node temporal.mjs
import { Temporal } from "@js-temporal/polyfill";
const p = (k, v) => console.log(k.padEnd(30), v);
const t = Temporal.PlainDate.from("2026-10-03");
p("add({days:10})", t.add({ days: 10 }).toString());
p("subtract({weeks:2})", t.subtract({ weeks: 2 }).toString());
p("1/31 add({months:1})", Temporal.PlainDate.from("2026-01-31").add({ months: 1 }).toString());
p("2024-02-29 add({years:1})", Temporal.PlainDate.from("2024-02-29").add({ years: 1 }).toString());
try { Temporal.PlainDate.from("2026-01-31").add({ months: 1 }, { overflow: "reject" }); }
catch (e) { p("overflow:'reject'", e.name + ": " + e.message); }
p("月初 with({day:1})", t.with({ day: 1 }).toString());
p("月末 with({day:daysInMonth})", t.with({ day: t.daysInMonth }).toString());
p("翌月末", (() => { const n = t.with({ day: 1 }).add({ months: 1 }); return n.with({ day: n.daysInMonth }).toString(); })());
p("daysInMonth / inLeapYear", t.daysInMonth + " / " + t.inLeapYear);
p("dayOfWeek (1=月..7=日)", t.dayOfWeek);
p("until 2027-01-01 (days)", t.until("2027-01-01").days);
p("until (months)", Temporal.PlainDate.from("2026-01-31").until(t, { largestUnit: "month" }).toString());
p("年齢 until largestUnit:year", Temporal.PlainDate.from("1990-10-04").until(t, { largestUnit: "year" }).years);
const z1 = Temporal.ZonedDateTime.from("2026-03-07T12:00[America/New_York]");
p("ZonedDateTime add({days:1})", z1.add({ days: 1 }).toString());
p("ZonedDateTime add({hours:24})", z1.add({ hours: 24 }).toString());
実行結果:
add({days:10}) 2026-10-13
subtract({weeks:2}) 2026-09-19
1/31 add({months:1}) 2026-02-28
2024-02-29 add({years:1}) 2025-02-28
overflow:'reject' RangeError: value out of range: 1 <= 31 <= 28
月初 with({day:1}) 2026-10-01
月末 with({day:daysInMonth}) 2026-10-31
翌月末 2026-11-30
daysInMonth / inLeapYear 31 / false
dayOfWeek (1=月..7=日) 6
until 2027-01-01 (days) 90
until (months) P8M3D
年齢 until largestUnit:year 35
ZonedDateTime add({days:1}) 2026-03-08T12:00:00-04:00[America/New_York]
ZonedDateTime add({hours:24}) 2026-03-08T13:00:00-04:00[America/New_York]
Temporal の特徴がよく出ている箇所を3つ挙げます。
Temporal.PlainDateは「日付だけ」の型で、時刻もタイムゾーンも持ちません。"2026-10-03"を読んでも1日ずれる心配がありません。- 月あふれは標準で月末に丸められ、
{ overflow: "reject" }を付けるとエラーとして受け取れます(エラー文はポリフィル 0.5.1 のもの)。 ZonedDateTimeでは、「1日足す」と「24時間足す」が別の結果になります。夏時間開始日をまたいでも、1日足せば 12:00 のまま、24時間足せば 13:00 です。前の章で手作業で区別した内容が、型と単位で表現されています。
また、月は1始まり(10月は10)で、すべての操作が新しい値を返します。
date-fns のコードと実行結果
npm i date-fns で導入し、import { addDays, addMonths, endOfMonth, differenceInCalendarDays, format, parseISO } from "date-fns"; のように必要な関数だけ読み込みます。基準日 d = new Date(2026, 9, 3) に対して、左の列の式を実行した結果です(表示は format(x, "yyyy-MM-dd"))。
addDays(d,10) 2026-10-13
addMonths(1/31,1) 2026-02-28
addYears(2024-02-29,1) 2025-02-28
startOfMonth / endOfMonth 2026-10-01 / 2026-10-31
翌月末 endOfMonth(addMonths) 2026-11-30
differenceInCalendarDays 90
differenceInMonths 8
differenceInYears (年齢) 35
isLeapYear / getDaysInMonth false / 31
addBusinessDays(d,5) 2026-10-09
parseISO('2026-10-03') Sat Oct 03 2026 00:00:00 GMT+0900
date-fns は素のDateをそのまま受け取り、新しいDateを返す関数の集まりです。parseISO("2026-10-03") はローカルの0時を返すので、new Date("2026-10-03") のUTC扱いを避けられます。addBusinessDays は土日を飛ばします(祝日は扱わないため、祝日を考慮したい場合はこの記事の関数のように自分でリストを渡す形にします)。
Day.js のコードと実行結果
npm i dayjs で導入し、import dayjs from "dayjs"; で読み込みます。j = dayjs("2026-10-03") に対して、左の列の式を実行した結果です(表示は format("YYYY-MM-DD"))。
add(10,'day') 2026-10-13
1/31 add(1,'month') 2026-02-28
2024-02-29 add(1,'year') 2025-02-28
startOf / endOf('month') 2026-10-01 / 2026-10-31
翌月末 2026-11-30
diff(day) 90
diff(month) 8
diff(year) 年齢 35
daysInMonth / day() 31 / 6
dayjs('2026-10-03') Sat Oct 03 2026 00:00:00 GMT+0900
Day.js は dayjs() で包んだオブジェクトにメソッドをつなげていく書き方です。dayjs("2026-10-03") もローカルの0時として読まれました。
Temporal の対応状況(2026年10月時点)
Temporal は、Date の後継として標準化が進められてきた日付・時刻のAPIです。MDN のブラウザー互換データ(npm パッケージ @mdn/browser-compat-data 8.1.4、2026年10月1日付)を今回取得して確認した内容は次のとおりです。
| 実行環境 | 対応した版 | その版の公開日(同データより) |
|---|---|---|
| Firefox | 139 | 2025-05-27 |
| Chrome / Edge | 144 | 2026-01-13(Edge は 2026-01-21) |
| Opera | 128 | 2026-02-26 |
| Node.js | 26.0.0 | 2026-05-05 |
| Deno | 2.7 | 2026-02-25 |
| Bun | 1.4.0 | 2026-08-20 |
| Safari(macOS) | Technology Preview のみ | 正式版は未対応 |
| Safari(iOS) | 未対応 | ― |
MDN の Temporal のページにも、今回開いた時点で「一部の広く使われているブラウザーで動作しないため、Baseline ではない」という趣旨の注記がありました。Chrome・Edge・Firefox と Node.js 26 以降では標準で使え、Safari では正式版の対応待ち、というのが2026年10月時点の状況です。
この記事の検証は Node.js 22.22.2 で行ったため、Temporal はポリフィル経由で動かしています。各ブラウザーや Node.js 26 の組み込み実装での動作は未実行です。
// 組み込みの Temporal があればそれを使い、なければポリフィルを読み込む(Node.js 22 ではポリフィル側で動作)
const T = globalThis.Temporal ?? (await import("@js-temporal/polyfill")).Temporal;
どれを選ぶかの判断表
| 状況 | 選び方 | 理由 |
|---|---|---|
| 依存を増やしたくない、計算の種類が少ない | 素のDate + この記事の関数集 | 20行ほどの関数で足りる。挙動を自分で把握できる |
| 書式化やロケール対応など、周辺の処理も多い | date-fns | 必要な関数だけ読み込める。Dateをそのまま渡せる |
| メソッドをつなげる書き方が好み、軽さを重視 | Day.js | 本体が小さく、機能はプラグインで足す |
| タイムゾーンや夏時間をまたぐ計算が多い | Temporal(必要ならポリフィル) | 日付だけ・時刻付き・タイムゾーン付きが型で分かれている |
| 対象が Chrome / Edge / Firefox と Node.js 26 以降に限られる | Temporal | 標準機能なので追加の読み込みが不要 |
| Safari を含む幅広いブラウザーが対象 | 素のDate か date-fns / Day.js、または Temporal + ポリフィル | 正式版の対応状況に合わせる |
新しく書くコードで月の加算や差分が中心なら、まずは素のDateの関数集で十分です。 タイムゾーンをまたぐ処理が増えてきたら Temporal を検討する、という順番が取り組みやすいでしょう。
つまずきやすい箇所のチェックリスト
| 症状 | 原因 | 対処 |
|---|---|---|
| 月が1つずれて表示される | getMonth() が0始まり |
表示するときに +1、new Date(y, m, d) に渡すときに -1 |
| 1月31日の1か月後が3月になる | setMonth の月あふれ |
addMonths(1日にしてから月を動かし、月末で丸める) |
| 2月29日の1年後が3月1日になる | setFullYear の月あふれ |
addYears(12か月の加算として扱う) |
| 元の日付まで変わった | set系メソッドが元の値を書き換える | new Date(d.getTime()) でコピーしてから操作する |
| 日数の差分が1少ない | 時刻の端数や夏時間 | Date.UTC で0時にそろえてから引く |
| "YYYY-MM-DD" を読むと9時になる、前日になる | 日付だけのISO形式はUTC扱い | parseLocal で年・月・日を数値で渡す |
toISOString() の日付が前日になる |
UTCに変換されている | ローカルの値で組み立てる fmt を使う |
よくある質問
Q. 30日後と1か月後は、どちらで計算すればよいですか?
A. 目的で決まります。「30日間有効」のように日数で決まっているなら addDays(d, 30)、「翌月の同じ日」のように暦で決まっているなら addMonths(d, 1) を使います。2026年10月3日の場合、前者は11月2日、後者は11月3日で、結果が1日違います。
Q. 月末かどうかを判定するにはどうすればよいですか?
A. 「1日足したら月が変わるか」で判定できます。関数集を使うなら fmt(d) === fmt(endOfMonth(d)) でも同じです。月ごとの日数を配列で持つ必要はありません。
Q. Temporal はもう使ってよいですか? A. 2026年10月時点で、Chrome 144・Edge 144・Firefox 139・Node.js 26 以降では標準で使えます(MDN の互換データによる)。Safari の正式版が対象に含まれる場合は、ポリフィルを併用するか、素のDateやライブラリで書いておき、対応が進んだ段階で置き換える方法があります。
まとめ
- 日の加算は日の部分に足す、月末は「翌月の0日」。
new Date(y, m + 1, 0)で月ごとの日数を覚えずに月末が取れます。 - 月・年の加算は月あふれに注意。1月31日の1か月後は
setMonthだと3月3日、月末に丸めるaddMonthsなら2月28日です。 - 日数の差分は
Date.UTCで0時にそろえてから引く。夏時間をまたいでも結果が変わりません。 new Date("2026-10-03")はUTC扱い。日付だけを扱うときは、年・月・日を数値で渡すparseLocalと、ローカルの値で組み立てるfmtを使います。- 営業日は土日と祝日リストを飛ばして数える。祝日は外から渡せる形にしておくと更新が楽です。
- Temporal・date-fns・Day.js の月加算も月末に丸める動きで、この記事の関数集と結果が一致しました。
- Temporal は Chrome 144・Firefox 139・Node.js 26 から標準対応(2026年10月時点、Safari は正式版の対応待ち)。対象環境に合わせて選びます。
参考・出典
- MDN Web Docs「Temporal」(2026年10月3日に閲覧)
- npm パッケージ @mdn/browser-compat-data 8.1.4 の javascript.builtins.Temporal(2026年10月3日に取得)
- npm レジストリの date-fns 4.4.0、dayjs 1.11.23、@js-temporal/polyfill 0.5.1(今回インストールして実行)
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。