Excel、SQL、Python、JavaScriptで同じ日付の週番号が違う理由

Pythonで週番号を計算してから同じ日付をExcelで開くと、数字が一致しない。SQLでクエリを実行すると3つ目の結果が出る。さらに検索すると4つ目が見つかる。

これはバグではありません。各ツールが異なる設計判断をしているだけです — しかも多くのツールは十分に目立つ形で説明していません。

一般的に使われる週番号の体系は少なくとも2つあり、それぞれに複数のバリエーションがあります。そして「普遍的なデフォルト」はありません。違いを理解すると、不一致のデバッグは一瞬です。知らないと延々とハマります。

主要な2つの体系

ISO 8601(欧州の多く、国際ビジネス、科学的文脈で利用)

  • 週は月曜〜日曜
  • 週1は「その年の最初の木曜日」を含む週
  • 年は52週または53週
  • 年境界では「週年(week year)」が暦年と異なることがある

US / シンプル体系(北米、スプレッドシートのデフォルトに多い)

  • 週は日曜〜土曜(ロケールにより月曜開始の場合も)
  • 週1は1月1日を含む週
  • 1月1日は必ず週1に入る(曜日に関係なく)
  • 年末の半端な週は次年に移さず、その年の52/53週として扱う

実務上の違いは、12月下旬と1月上旬で最も顕著に出ます。

DateISO weekUS week (Sun start)
December 28, 2025Week 52, 2025Week 53, 2025
December 29, 2025Week 1, 2026Week 53, 2025
December 30, 2025Week 1, 2026Week 53, 2025
December 31, 2025Week 1, 2026Week 53, 2025
January 1, 2026Week 1, 2026Week 1, 2026
January 2, 2026Week 1, 2026Week 1, 2026
January 3, 2026Week 1, 2026Week 1, 2026
January 4, 2026Week 1, 2026Week 2, 2026

2025年12月29〜31日は、ISOでは2026年の週1に入ります — その週の木曜日(1月1日)が2026年にあるためです。一方US体系では、2025年の週53に残ります。

Excel: WEEKNUMISOWEEKNUM

Excelは関数が分かれているので、他のツールより分かりやすい方です。

WEEKNUM(date, [return_type]) — US式(週の開始曜日は選べる)

return_type 引数で週の開始曜日が決まります:

return_type週の開始
1 (既定)日曜
2月曜
11月曜
12火曜
13水曜
14木曜
15金曜
16土曜
17日曜
21月曜(ISO 8601)

return_type = 21 にすると WEEKNUM はISOと同じ挙動になります — ただし説明が弱く、多くのユーザーは存在を知りません。

ISOWEEKNUM(date) — ISO 8601(常に月曜開始、木曜ルール)

Excel 2013で追加されました。ISO週番号を曖昧さなく返します。

=ISOWEEKNUM("2025-12-31")   → 1    (2026年の週1)
=WEEKNUM("2025-12-31", 1)   → 53   (2025年の週53、日曜開始)
=WEEKNUM("2025-12-31", 2)   → 53   (2025年の週53、月曜開始)
=WEEKNUM("2025-12-31", 21)  → 1    (ISO互換、ISOWEEKNUMと同じ)

よくあるミス: ISOベースのシステムと比較しているのに、既定の WEEKNUM を使うこと。ほとんどの日付は合いますが、年末年始だけ黙ってズレます。

Google Sheets: 同じ関数、同じ注意点

Google Sheetsにも WEEKNUMISOWEEKNUM があり、挙動はExcelと同じです。既定の WEEKNUM はUS式(日曜開始)で、ISOWEEKNUM はISO 8601です。

違いとして、Sheetsでは ISOWEEKNUMYEAR を並べて使うと、ISO週年(week year)の扱いがより問題になります。=YEAR("2025-12-31") は2025になりますが、ISO週は2026に属します。ISO週年を返す単一の組み込み関数はないため、計算が必要です:

=IF(ISOWEEKNUM(A1) > 50, IF(MONTH(A1) = 1, YEAR(A1) - 1, YEAR(A1)),
   IF(ISOWEEKNUM(A1) < 3, IF(MONTH(A1) = 12, YEAR(A1) + 1, YEAR(A1)),
   YEAR(A1)))

冗長です。週年をきちんと扱うなら、PythonやSQLへ寄せた方が楽な場合も多いです。

Python: isocalendar() はISO、でも strftime('%W') は違う

Pythonの datetime には2系統あります。

date.isocalendar() — ISO 8601、(year, week, weekday) を返す

from datetime import date

d = date(2025, 12, 31)
d.isocalendar()
# IsoCalendarDate(year=2026, week=1, weekday=3)

ここで返る年は暦年(2025)ではなく ISO週年(2026) です。週をラベリングするなら d.year ではなく iso_year を使う必要があります。

strftime('%W')strftime('%U') — US式(週の開始が違う)

d.strftime('%W')   # 週番号、週の開始は月曜 → '52'
d.strftime('%U')   # 週番号、週の開始は日曜 → '52'
d.strftime('%V')   # ISO週番号 → '01'
d.strftime('%G')   # ISO週年 → '2026'

%V / %G はISOとして正しい組です。%W / %U はUS系で、年境界でISOと食い違います。

よくあるミス: 他システムがISOなのに d.strftime('%W') で週番号を取ること。年の大半は一致し、12月と1月だけ静かにズレます。

# ISO比較には不適 — 年境界でズレる
week = int(d.strftime('%W'))

# ISOとして正しい
iso_year, iso_week, _ = d.isocalendar()

JavaScript: 組み込みがないので自前実装が必要

JavaScriptの Date には週番号の組み込みメソッドがありません。Date.getDay() は日曜=0〜土曜=6を返します。自前で計算するか、date-fnsdayjs のようなライブラリを使います。

ISO週の手動計算:

function isoWeek(date) {
  const d = new Date(date)
  d.setHours(0, 0, 0, 0)
  // その週の木曜日が年を決める
  d.setDate(d.getDate() + 3 - (d.getDay() + 6) % 7)
  const jan4 = new Date(d.getFullYear(), 0, 4)
  return 1 + Math.round(((d - jan4) / 86400000 - 3 + (jan4.getDay() + 6) % 7) / 7)
}

function isoWeekYear(date) {
  const d = new Date(date)
  d.setDate(d.getDate() + 3 - (d.getDay() + 6) % 7)
  return d.getFullYear()
}

isoWeek(new Date('2025-12-31'))      // 1
isoWeekYear(new Date('2025-12-31'))  // 2026

date-fns を使う場合:

import { getISOWeek, getISOWeekYear } from 'date-fns'

getISOWeek(new Date('2025-12-31'))      // 1
getISOWeekYear(new Date('2025-12-31'))  // 2026

よくあるミス: Math.ceil(dayOfYear / 7) のような簡易計算を「週番号」と呼ぶこと。これはISOでもUSでもなく、別の(そして誤った)体系になります。

SQL: データベースによって違う

主要DBは週番号の扱いがそれぞれ異なります。

PostgreSQL — 既定でISO

SELECT EXTRACT(week FROM DATE '2025-12-31');
-- 1(ISO週)

SELECT DATE_PART('week', DATE '2025-12-31');
-- 1(同じ、ISO)

-- ISO週年
SELECT EXTRACT(isoyear FROM DATE '2025-12-31');
-- 2026

PostgreSQLの EXTRACT(week ...) はISO 8601に従います。週年は isoyear で取れます。

MySQL / MariaDB — 複数モード

-- mode 3 が ISO 8601(週は月曜開始、週1は木曜ルール)
SELECT WEEK('2025-12-31', 3);   -- 1

-- mode 0(既定)は US式、日曜開始
SELECT WEEK('2025-12-31', 0);   -- 53
SELECT WEEK('2025-12-31');      -- 53(既定 mode 0)

-- YEARWEEK は年+週の組み合わせを返す
SELECT YEARWEEK('2025-12-31', 3);  -- 202601

MySQLのmode引数は重要です(0–7)。mode 3がISOで、既定(mode 0)はUS式です。DBを切り替えたときにバグの温床になります。

SQL Server — 既定はISOではない

-- 既定の DATEPART(week, ...) はISOではない
SELECT DATEPART(week, '2025-12-31');   -- 53

-- ISO週: isowk または iso_week を使う
SELECT DATEPART(isowk, '2025-12-31');  -- 1
SELECT DATEPART(iso_week, '2025-12-31');  -- 1(同じ)

SQL Serverの DATEPART(week, ...) はUS式です。欧州系のシステムと突き合わせるなら必ず isowk を使います。

SQLite — 週関数がない

SQLiteには WEEKNUMEXTRACT(week ...) がありません。strftime を使います:

-- '%W' は月曜開始のUS式
SELECT strftime('%W', '2025-12-31');   -- 52

-- ISO週はワークアラウンドが必要
SELECT (strftime('%j', date('2025-12-31', '-3 days', 'weekday 4')) - 1) / 7 + 1;
-- 1

ISOの回避策は、その週の木曜日を基準に数えます。正しいですが直感的ではありません。

チートシート

ToolISO weekUS weekNotes
ExcelISOWEEKNUM() or WEEKNUM(d, 21)WEEKNUM() defaultISOWEEKNUMは2013で追加
Google SheetsISOWEEKNUM()WEEKNUM() defaultExcelと同じ
Pythondate.isocalendar()[1] or %V%W (Mon) / %U (Sun)ISO週年は%G
JavaScriptManual or date-fns getISOWeek()Manual組み込みなし
PostgreSQLEXTRACT(week ...)Not built-in既定でISO
MySQLWEEK(d, 3)WEEK(d) or WEEK(d, 0)mode 3 = ISO
SQL ServerDATEPART(isowk, d)DATEPART(week, d)既定はUS
SQLiteWorkaround requiredstrftime('%W', d)ネイティブなし

実務で不一致を避ける方法

1つの体系を選び、全体で強制する。 新規システムならISOをデフォルトにするのが無難です。国際標準化されていて、多くの現代ライブラリが採用しています。

ISO週年を週番号とセットで保存する。 2026年の週1と2025年の週1は別の週です。週番号だけ(1)を保存すると年がないと曖昧になります。

年境界ケースを明示的にチェックする。 12月28〜31日、1月1〜3日がズレの発生点です。デプロイ前にこの範囲の日付でパイプラインをテストしてください。

迷ったら、フル日付を保存して比較する。 週番号は表示やレポート用であり、主キーやJOINキーには向きません。週番号でシステム間JOINをするなら、まず「その週の月曜日」のようなカノニカルな日付に変換してから比較するのが安全です。

ISO週番号計算ツール を使えば、年全体のカレンダービューも含め、任意の日付の正しいISO週番号を確認できます。