티스토리 뷰

반응형

오늘은 지식in 같은 곳에서 가장 쉽게 접하는 질문 중 하나인 윤년을 계산하는 방법에 대해서 이야기해보자. 사실 코드만 써 놓으면 너무 간단하니까 윤년에 대해 몇 가지 이야기를 좀 해보도록 하겠다.

 

윤년이란 무엇인가?

 

1년은 지구가 태양을 한 바퀴도는 것을 일수로 표현한 것으로 우리는 통상 1년 = 365일이라고 알고 있다. 아주 엉뚱한 소리는 아닌데, 실제로 우리가 일상에서 쓰는 1년은 '회귀년'으로 춘분점이 동일한 위치로 돌아오는 데까지 걸리는 시간을 의미한다. 이는 엄밀하게는 365.2422일로 365일 하고도 약 5시간가량이 된다. 이 실제 1년이 365일보다 조금 더 긴 이 차이가 누적이 되어 몇 백 년이 지나면 8월이 한 겨울이 되는 등 실제 계절과 달력이 차이나는 문제가 생긴다. 그래서 지금 우리가 많이 쓰는 그레고리력에서는 몇 가지 예외적인 규칙을 두어 윤년이라는 것을 정했다. 윤년이 되면 28일까지 있는 2월이 29일까지로 하루 늘어난다. 이 윤년은 4년에 한 번으로 정해서 매년 쌓이는 5시간 49분 정도의 차이를 모아 하루를 더하는 것이다.

 

  1. 기본적으로 4년에 한 번 윤년으로 4의 배수가 되는 해는 윤년이다.  이는 매년 쌓이는 시간이 약 6시간쯤 되니 이를 4번 모으면 하루가 되기 때문이다.
  2. 그런데 이렇게 되면 또 4년마다 3시간 정도가 모자란다. 그래서 매 100년은 윤년이 아니다.
  3. 이마저도 누적되면 또 차이가 나기 때문에 다시 400년마다 윤년으로 정한다.

그레고리력이 정하는 윤년의 규칙은 여기까지 이다. 물론 400년 주기에서 보정하는 부분 역시 오차가 있어서 2000년마다 한 번씩은 다시 윤년이 아닌 것으로 정해야 하지만, "그건 그때 가서 알아서 하겠지"라고 딱히 정의하지 않았다.

 

자 그러면 위의 규칙을 다시 정리하면 다음과 같다.

 

  1. 연도가 4로 나눠지는 해는 윤년이다.
  2. 그러나 연도가 100으로 나눠지는 해는 윤년이 아니다.
  3. 그런데 연도가 400으로 나눠지는 해는 윤년이다.

이렇게 해서 세 번의 분기를 판단해야 한다. 그래서 이 규칙을 곧이곧대로 코드로 옮기면 의외로 지저분하다.

def isLeapYear(y):
  # 윤년은 영어로 leap year이다. 
  # leaf year 가 아니다.
  if y % 4 == 0:
    if y % 100 == 0:
      if y % 400 == 0:
        return True  # 매 400년
      return False  # 400년은 아닌 100년
    return True # 100년은 아닌 4년
  return False # 4년이 아님

그런데 여기 등장하는 숫자들을 보자, 400, 100, 4이다. 400은 당연히 100의 배수이다. 그리고 100은 4의 배수이다.  A가 B의 배수일 때 또 다른 Y에 대해서 Y가 B의 배수라면 Y는 A의 배수이다라는 명제가 참임을 알 수 있다. 이의 대우는 "Y가 A의 배수가 아니라면 Y는 B의 배수도 아니다."가 된다.  예를 들어 Y가 400의 배수가 아니라면 4의 배수가 아니라는 점은 자명하다.

 

이 점이 시사하는 바는  큰 숫자로 먼저 나눠보면 더 빠른 결론에 도달할 수 있다는 것이다.

def isLeapYear(y):
  if y % 400 == 0:
    return True
  if y % 100 == 0:
    return False
  if y % 4 == 0:
    return True
  return False

코드 수는 똑같지만, 조금 더 알아보기 쉬워졌다.  자 그럼 위 로직을 말로 설명해보자면 "400의 배수이거나,  100의 배수가 아니면서 4의 배수이면 윤년이다"가 된다.  이건 파이썬의 and/or 연산의 short circuit 성질을 이용해서 한 줄로 표시할 수 있다.

 

shor circuit 이란 A, B의 평가식을 and 나 or 로 연결했을 때, 나머지 하나를 굳이 평가할 필요가 없는 상황에서는 후자를 평가하지 않는다는 것이다.  예를 들어 A and B는 A, B가 모두 참이어야 참이다. 만약 A가 거짓이라면 B를 볼 필요 없이 전체가 거짓임을 알 수 있다. 반대로 A or B는 둘 중 하나만 참이면 참이다. 따라서 A가 참이면 B를 볼 필요가 없다.

 

이 성질을 사용해서 이러한 중첩된 if 문을 간단히 하나의 조건을 결합할 수 있는 상황이 가끔 있는데, 공교롭게도 이 윤년 계산의 케이스가 딱 여기에 해당한다.

 

def isLeapYear(y):
  return y % 400 == 0 or y % 100 > 0 and y % 4 == 0
  #      ~~~~~~~~~~~A    ~~~~~~~~~~B     ~~~~~~~~~C

이 코드는 이렇게 동작한다.

 

  1. y가 400의 배수라면 A가 참이고 B, C를 평가하지 않는다.
  2. y가 400의 배수가 아니라면, B를 평가한다. 만약 B가 참이라면 (y가 100의 배수가 아니라면)  C(4의 배수인지)까지 평가할 것이다. 그러나 B가 거짓이라면 C를 평가할 필요가 없으니 거짓으로 결론날 것이다.

그러니까 and/or 연산의 short circuit 만 알고 있다면 윤년 체크 함수는 단 한줄에 작성할 수 있다는 것이다.

 

----

 

참고로 가독성을 좀 포기한다면 다음과 같이 쓸 수도 있는데, 해석은 각자가...

 

leaf = lamdba y: 400 % ((4, 400)[y%100==0]) == 0
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
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
글 보관함