0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日付計算で地味にハマる3つ——「◯年◯ヶ月◯日」の桁借り・月末の求め方・和暦(昭和平成)変換をライブラリなしで

0
Posted at

「2つの日付の差は何日か」だけなら引き算で終わる。だが「◯年◯ヶ月◯日」で出せ、和暦(昭和◯年)でも入力させろ、となると途端に面倒になる。date-fnsdayjs を入れれば済む話だが、この程度なら標準の Date で十分書ける——ただし地味にハマるポイントが3つある。

日付の差・◯日後/前・年齢(和暦対応)を計算するツールをぱんだツールズの1機能として作った。ライブラリなし・ブラウザ完結。

この記事では、日付計算を素の Date で実装するときに踏む3つの落とし穴——「◯年◯ヶ月◯日」差分の桁借り、月末日の求め方、和暦変換——を解説する。

ハマり1:「◯年◯ヶ月◯日」は桁借りが要る

合計日数は単純で、ミリ秒の差を1日のミリ秒で割るだけ。

const msPerDay = 1000 * 60 * 60 * 24
const totalDays = Math.round((to.getTime() - from.getTime()) / msPerDay)

面倒なのは「2年3ヶ月15日」のような複合表示。年・月・日を別々に引き算すると、日や月がマイナスになる。これを桁借りで繰り上げる必要がある。筆算の繰り下がりと同じ考え方。

let years = absTo.getFullYear() - absFrom.getFullYear()
let months = absTo.getMonth() - absFrom.getMonth()
let days = absTo.getDate() - absFrom.getDate()

if (days < 0) {
  months -= 1
  // 「終了日の前月」の日数を借りてくる
  const prevMonth = new Date(absTo.getFullYear(), absTo.getMonth(), 0)
  days += prevMonth.getDate()
}
if (months < 0) {
  years -= 1
  months += 12
}

days がマイナスなら、months から1ヶ月借りてその月の日数を days に足す。months がマイナスなら years から12ヶ月借りる。ここで「借りてくる日数」を固定の30日にしてはいけないのが肝で、2月は28/29日、各月は28〜31日とバラバラ。借りるのは必ず「実際のその月の日数」でないと、1月31日 → 3月1日 のような月またぎがズレる。

その「実際の月の日数」をどう取るかが、次のハマりポイント。

ハマり2:月末日は new Date(y, month, 0) で取る

JavaScript の Date には「その月の日数」を返す API がない。だがコンストラクタの日に 0 を渡すと前月の末日になるという挙動を使うと一発で取れる。

// 「日=0」は前月の最終日に正規化される
new Date(2024, 2, 0).getDate()  // → 29(2024年2月は閏年なので29日)
new Date(2023, 2, 0).getDate()  // → 28(2023年2月)
new Date(2024, 1, 0).getDate()  // → 31(1月)

new Date(y, m, d)d0 を入れると「m 月の0日目」=「m-1 月の最終日」に丸められる。月インデックスは0始まりなので、new Date(2024, 2, 0) は「3月(index 2)の0日目」=2月29日。この getDate() がそのまま「2月の日数」になる。

上の桁借りコードの new Date(absTo.getFullYear(), absTo.getMonth(), 0) は、「終了日の月インデックス」をそのまま渡すことで「終了日の前月の末日」を取り、その日数を借りている。閏年判定を自分で書かなくても、Date の正規化が閏年を勝手に処理してくれる。

同じ理屈で、◯日後 の計算も「日にそのまま加算」すれば月またぎ・年またぎが自動で正規化される。

const resultDate = new Date(baseDate.getTime() + daysNum * 24 * 60 * 60 * 1000)

ちなみにこれが「30日後」と「1ヶ月後」が別物になる理由でもある。日数ベースの加算は常に固定日数だが、「1ヶ月後」は月の長さに依存する。1月31日 + 1ヶ月 は2月31日が存在しないので2月28日(or 29日)になる。日付計算では「何を単位にするか」で答えが変わる。

ハマり3:和暦変換は「改元日」を正確に持つ

和暦(令和・平成・昭和…)対応は、元号ごとの正確な改元日をテーブルに持つのが出発点。「昭和は1926年から」では足りない。昭和の開始は1926年12月25日で、それ以前の1926年は大正15年。改元日を持たないと境界の年で1年ズレる。

const GENGO = [
  { name: '令和', startYear: 2019, startMonth: 5,  startDay: 1  },
  { name: '平成', startYear: 1989, startMonth: 1,  startDay: 8  },
  { name: '昭和', startYear: 1926, startMonth: 12, startDay: 25 },
  { name: '大正', startYear: 1912, startMonth: 7,  startDay: 30 },
  { name: '明治', startYear: 1868, startMonth: 1,  startDay: 25 },
] as const

和暦→西暦は 開始年 + 元号年 - 1。昭和65年なら 1926 + 65 - 1 = 1990年。ただし「昭和元年の1月」のような存在しない日付を弾く必要がある(昭和は12月25日始まりなので昭和元年に1月は無い)。改元日より前ならエラーにする。

function gengoToDate(gengo: GengoName, nen: number, month: number, day: number): Date | null {
  const g = GENGO.find((x) => x.name === gengo)
  if (!g) return null
  const year = g.startYear + nen - 1
  const date = new Date(year, month - 1, day)
  if (isNaN(date.getTime())) return null
  // 元号の開始日より前は存在しない日付
  const gengoStart = new Date(g.startYear, g.startMonth - 1, g.startDay)
  if (date < gengoStart) return null
  return date
}

new Date(...)Invalid DateisNaN(date.getTime()))になるケースと、改元日より前のケースの両方を null で弾く。これで「昭和元年3月」のような実在しない和暦を入力バリデーションで検出できる。

設計の小ネタ:年齢計算は差分計算の使い回し

年齢計算は専用ロジックを書かなくていい。「生年月日から今日までの差」の年部分が満年齢そのものなので、ハマり1で作った calcDiff をそのまま再利用できる。

const diff = calcDiff(birthDate, today)
const age = diff.years   // ◯年◯ヶ月◯日 の「年」が満年齢

calcDiff は桁借りで「◯年◯ヶ月◯日」を正確に出すので、その years を取れば「誕生日が来たかどうか」も含めて正しい満年齢になる。月日がまだ来ていなければ桁借りで years が1減るため、単純な 今年 - 生年 のズレ(誕生日前なのに1歳多い)が起きない。差分計算をきちんと作っておくと、年齢計算が無料でついてくる。

まとめ

日付計算は「引き算するだけ」に見えて、複合表示・月末・和暦で地味に詰まる。

  • 「◯年◯ヶ月◯日」は年・月・日を引いてから桁借りで繰り上げる。借りる日数は固定30日でなく実際の月の日数
  • 月の日数は new Date(y, month, 0).getDate()。日に 0 を渡すと前月末日に正規化され、閏年も自動処理
  • ◯日後getTime() への加算で月・年またぎが自動正規化。だから「30日後」と「1ヶ月後」は別物
  • 和暦は正確な改元日テーブルを持ち、開始年 + 元号年 - 1。改元日より前と Invalid Date を弾いて実在しない日付を検出
  • 年齢計算は差分計算の years を使い回すだけ。桁借りのおかげで誕生日前後も正確

Date の正規化挙動(特に「日=0で前月末日」)を知っていると、閏年判定すら自分で書かずに済む。ライブラリを入れる前に、標準の Date でどこまで戦えるか見極める良い題材だった。

ぱんだツールズ では他にも PDF・画像・CSV・テキスト処理など、開発者向けのツールを多数公開している。全部無料・登録不要・ブラウザ完結で使える。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?