言語ごとの週番号の扱いについて[Python,JavaScript,MySQL]

## はじめに

みなさん、プログラム内で週番号を扱ったことがあるでしょうか。
例えば、2021年の23週目、という形でデータを持つことを考えます。
その場合に注意するべきなのは、週の初めを1周として扱うのか、0週として扱うのか、ということです。
しかも、プログラミング言語ごとに週番号の扱い方が異なるので注意が必要です。
しっかりと理解しておかないと、かなり危険なのでまとめたいと思います。

今回の記事では**週の始まりを月曜日**として考えます。

## Pythonにおける週番号の扱い

Pythonのリファレンスでは以下となっています。

[strftime() と strptime() の書式コード](https://docs.python.org/ja/3/library/datetime.html#strftime-and-strptime-format-codes)

> %W
> 0埋めした10進数で表記した年中の週番号 (週の始まりは月曜日とする)。新年の最初の月曜日に先立つ日は 0週に属するとします。

つまり、1月1日が月曜日かどうかでその年が0週から始まるか、1週から始めるのかが変化する、ということになります。

### Pythonで日付から週番号を取得する場合

では、日付から週番号を取得してみましょう。

```python
from datetime import datetime

# 2021年の1月1日は金曜日
date1 = '2021-01-01'
#2024年の1月1日は月曜日
date2 = '2024-01-01'

# result: date1 week number: 0
print(f'date1 week number: {int(datetime.strptime(date1,"%Y-%m-%d").strftime("%W"))}')

# result: date1 week number: 1
print(f'date2 week number: {int(datetime.strptime(date2,"%Y-%m-%d").strftime("%W"))}')
```

上記のコードの結果を解説すると、2021年1月1日の週番号は0週として取得され、2024年1月1日の週番号は1週として取得される、ということになります。


### Pythonで週番号から日付を取得する場合

次は週番号から週のはじめの日付を取得してみましょう。

先ほどの結果でPythonにおいて2021年は0週から始まり、2024年は1週から始まる、ということが分かりました。
では、2021年と2024年の第2週目の週の始まりの日付を求めてみます。

```python
from datetime import datetime

# 2021年の2週目
date1 = '2021-W2'
# 2024年の2週目
date2 = '2024-W2'

# result: 2021-01-11 00:00:00
print(datetime.strptime(f'{date1}-1', '%Y-W%W-%w'))

# result: 2024-01-08 00:00:00
print(datetime.strptime(f'{date2}-1', '%Y-W%W-%w'))
```

コードでは分かりにくいので、図で説明します。
以下の図では、赤枠が0週目、緑枠が1週目、黄色枠が2週目、となります。

![Snag_2b0fe1b5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/27ecf224-e9da-d2e2-2ace-ba1e8cc9a476.png)

![Snag_2b1011ed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/02b56871-3396-b339-7a8b-1fcff742f498.png)

**1月1日が含まれる週を1週目とする、という考えではなく年の初めの月曜日が含まれる週を1週目とする**、という考えなので、単純に週番号を考えていると週の数え方が年によってずれる、ということが分かるかと思います。

## JavaScriptにおける週番号の扱いについて

JavaScript(以下JSとする)においてライブラリを使わずに日付を扱うことはないかと思うので、今回は[Day.js](https://day.js.org/en/)の場合に絞って解説します。

### Day.jsで日付から週番号を取得する場合

```javascript
const dayjs = require("dayjs");
const weekOfYear = require("dayjs/plugin/weekOfYear");
const updateLocale = require("dayjs/plugin/updateLocale");
dayjs.extend(weekOfYear);
dayjs.extend(weekOfYear);
dayjs.extend(updateLocale);

dayjs.updateLocale("en", {
  weekStart: 1, // OPTIONAL, set the start of a week. If the value is 1, Monday will be the start of week instead of Sunday。
  yearStart: 1, // OPTIONAL, the week that contains Jan 4th is the first week of the year.
});
dayjs.locale("en");

const day1 = "2021-01-01";
const day2 = "2024-01-01";

// result: 1
console.log(dayjs(day1, "YYYY-MM-DD").week());

// result: 1
console.log(dayjs(day2, "YYYY-MM-DD").week());
```

上記結果を見ると、2021年と2024年両方とも1月1日は1週目として取得されています。
つまり、1月1日が含まれる週が第1週目として扱うことができている、ということになります。


### Day.jsで週番号から日付を取得する場合

次は週番号から週の開始の日付を取得する処理を見てみましょう。
以下のコードでは2021年と2024年の第2週目の週の開始の日付を取得します。

```javascript
const dayjs = require("dayjs");
const weekOfYear = require("dayjs/plugin/weekOfYear");
const updateLocale = require("dayjs/plugin/updateLocale");
dayjs.extend(weekOfYear);
dayjs.extend(weekOfYear);
dayjs.extend(updateLocale);

dayjs.updateLocale("en", {
  weekStart: 1, // OPTIONAL, set the start of a week. If the value is 1, Monday will be the start of week instead of Sunday。
  yearStart: 1, // OPTIONAL, the week that contains Jan 4th is the first week of the year.
});
dayjs.locale("en");

const day1 = "2021";
const week1 = 2;
const day2 = "2024";
const week2 = 2;

// result: 2021-01-04
console.log(dayjs(day1, "YYYY").week(week1).startOf('week').format("YYYY-MM-DD"));

// result: 2024-01-08
console.log(dayjs(day2, "YYYY").week(week2).startOf('week').format("YYYY-MM-DD"));
```

コードだと分かりにくいので、図で説明します。

![Snag_2b4843ba.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/9464f683-a53b-5de9-e192-f25890f56e4a.png)

![Snag_2b485639.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/9b399e65-a40b-4d2b-b5da-7a17e76c537d.png)

Day.jsの場合は**1月1日が含まれる週が必ず第1週目として扱うことができている**ことが分かります。


## MySQLにおける週番号の扱いについて

次はMySQLにおける週番号の扱いを見ていきます。

[日付および時間関数 WEEK(date[,mode])](https://dev.mysql.com/doc/refman/8.0/ja/date-and-time-functions.html#function_week)

> この関数は、date に対応する週番号を返します。 2 つの引数を取る形式の WEEK() を使用すると、週が日曜日と月曜日のどちらから始まるのか、および戻り値が 0 から 53までと 1 から 53 までのどちらの範囲内であるのかを指定できます

週番号の0~53か1~53かについては、その年の最初の週を0週とするか、前年の53週(52週の場合もある)とするかどうか、という意味になります。
今回はPythonと合わせるため0~53週とします。

### MySQLで日付から週番号を取得する場合

```sh
SELECT WEEK('2021-01-01',5); # result: 0
SELECT WEEK('2024-01-01',5); # result: 1
```

上記の結果を見ると分かりますが、Pythonと同じく1月1日が月曜日かどうかで結果が変わります。
2021年は0週から始まり、2024年は1週から始まります。


### MySQLで週番号から日付を取得する場合

実際のコードを見てみましょう。

```sh
#2021年の第1週目
SELECT STR_TO_DATE('202101 Monday', '%x%v %W'); # result: 2021-01-04
#2024年の第1週目
SELECT STR_TO_DATE('202401 Monday', '%x%v %W'); # result: 2024-01-01
```

ここで注意すべきは、週番号から日付を取得する場合、月曜日始まりで0周開始の条件で変換することができない、ということです。
理由はDATE_FORMAT関数のmodeが0~3までしか対応していないことが原因です。

[DATE_FORMAT(date,format)](https://dev.mysql.com/doc/refman/5.6/ja/date-and-time-functions.html#function_date-format)

なので、MySQLのWEEK関数の出力を使ってそのままDATE_FORMAT関数を利用しても正しく処理できない可能性がある、ということに注意してください。
※MySQLにあまり精通していないので、他の方法があればコメントでご指摘頂きたいです。

## まとめ

言語ごとに週番号の扱い方が異なります。
ISOで決められている週番号の管理方法もありますが、第1週が前年度の週として扱われたりと人間がロジックを考える場合は少し考えにくい部分が難点かと思っています。
いずれにしても、しっかりと言語ごとの週番号の管理方法を理解しておかないと、思わぬ落とし穴にはまりますのでご注意ください。