평소 크게 신경을 쓰던 문제는 아닌데 이번에 관련 작업을 하면서 이게 간단한 문제는 아니라는 것을 알게 됐다. 이 문제가 어려운 이유는 그 해의 몇 주차인지에 대해서는 ISO 표준이 있는데, 월의 몇 주차인지에 대해서는 표준이 없기 때문이다. 아마 표준이 있었으면 이미 날짜 관련 함수 중에 있었을 것이다.

본격적으로 이야기를 꺼내기 전에 주(week)에 관한 ISO 표준 중 관련 사항을 알아 보자.

  1. 한 주의 시작은 월요일이다.

    달력에 일요일부터 표시하고 있기 때문에 한 주 시작이 일요일 같지만 월요일이 표준이라고 한다. 즉, 한 주는 월요일로 시작 해 일요일로 끝난다.

  2. 년도의 주차는 해당 주의 목요일 년도에 따라간다.

    예를 들어 1월 1일이 목요일이면, 12월 31일(수), 12월 30일(화), 12월 29일(월)은 전년도에 포함되지 않고 이번 년도의 1주차에 포함된다. 반대로 1월 1일이 금요일이면 1월 1일(금), 1월 2일(토), 1월 3일(일)은 전년도 마지막 주차에 포함된다.

그럼 월의 주차를 계산하는 방법은 어떨까?

아주 다양한 계산법들이 있지만 그 중 널리 쓰이는 계산법을 몇가지 살펴보자.

  1. 년의 주차 계산과 동일한 방법

    즉, 목요일이 어느 달에 속하느냐를 따져 전달에 마지막 주차에 포함시키거나 이번 달 첫 주차에 포함시킨다.

  2. 월요일을 기준으로 하는 방법

    월요일에 어느 달에 포함되느냐에 따른다. 월요일이 31일이면 그 뒤에 나오는 1, 2, 3, 4, 5, 6일은 마지막 주차에 포함시킨다.

  3. 각 달에 포함하는 방법

    30일이 월요일인 경우 30일(월), 31일(화)는 전 달의 마지막 주차로 계산하고, 1일(수), 2일(목), 3일(금), 4일(토), 5일(일)은 이번 달 첫 주차로 계산하는 방법이다.

그럼 어떤 방법으로 월의 주차를 계산해야 할까? 여기서는 첫번째 년의 주차 계산과 동일한 방법으로 계산하기로 하자. 이렇게 한 이유는 다른 계산법으로 하는 경우 년의 주차로는 1 주차면서 12월의 마지막 주차로 계산되는 불일치가 나타날 수 있기 때문이다. 이런 불일치는 다른 파생적인 문제와 예외 처리가 필요 할 수 있기 때문에, 년의 주차 계산과 동일한 방법으로 월의 주차를 계산하려고 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
class DateTimeExtra extends DateTime {
    public function weekofmonth() {
        $thursday = $this->thursday();

        if ( $thursday->format('n') != $this->format('n') ) {
            $date = $thursday;
        }
        else {
            $date = $this;
        }

        $firstday = clone $date;
        $firstday->sub(new DateInterval('P'.($date->format('j') - 1).'D'));
        $thursday_of_firstday = $firstday->thursday();
        if ( $thursday_of_firstday->format('n') != $firstday->format('n') ) {
            // 이번 달 첫번째 날을 포함한 주의 목요일의 월과 첫번째 날의 월이
            // 다른 경우는 첫번째 날이 이전 달의 마지막 주에 포함되어 있다는
            // 것이므로 월의 주차 기준이 되는 주를 한 주 뒤로 미룬다.
            $firstday->add(new DateInterval('P1W'));
        }

        $month = $date->format('n');
        $weekofmonth = $date->format('W') - $firstday->format('W') + 1;

        return array(intval($month), intval($weekofmonth));
    }

    private function thursday() {
        $thursday = clone $this;
        $dayofweek = $this->format('N');

        if ( $dayofweek < 4 ) {
            $thursday->add(new DateInterval('P'.(4 - $dayofweek).'D'));
        }

        if ( $dayofweek > 4 ) {
            $thursday->sub(new DateInterval('P'.($dayofweek - 4).'D'));
        }

        return $thursday;
    }
}
?>

월의 주차의 계산은 이렇게 했다. 우선 특정일이 속한 주의 목요일과 특정일이 속한 월의 1일이 속한 주의 목요일을 구한다. 그리고 이 둘의 월을 비교한다. 서로 다르면 이번 달의 1일이 속한 주가 전 달의 마지막 주차에 포함된 것이므로 이번 달의 시작인 년의 주차를 한 주 미룬다. 같으면 이번 달 1일이 포함된 주차가 이번 달의 첫 주차이므로 그대로 사용한다. 이 둘의 년의 주차를 빼주면 특정일의 해당 월의 주차를 구할 수 있다.