`Calendar`

Calendar

Elixir에서 달력(calendar), 날짜, 시간, 일시(datetime)를 다룰 때 지켜야 할 책임(규약)을 정의하는 모듈이에요.

이 모듈은 Elixir에서 달력 behaviour를 위한 타입과 최소 구현을 정의합니다. Elixir의 달력 기능의 목표는 완전한 기능을 갖춘 datetime API를 제공하는 대신, 상호 운용(interoperability)을 위한 기반을 제공하는 것이에요.

실제 date, time, datetime 구조체는 Date, Time, NaiveDateTime, DateTime을 참고하세요.

year, month, day 등의 타입은 과잉 정의(overspecified) 되어 있어요. 예를 들어 t:month/0 타입은 1..12 대신 정수로 지정됩니다. 이는 달력마다 한 달의 일 수가 다를 수 있기 때문이에요.

출처: Calendar

본문

핵심 타입들

Calendar 모듈은 다양한 날짜·시간 개념을 표현하는 타입을 정의해요. 모두 "필드를 담은 맵이나 구조체" 형태를 띠며, 필수 필드가 정해져 있습니다.

  • t:date/0calendar, year, month, day 필드를 담은 맵/구조체
  • t:time/0hour, minute, second, microsecond 필드
  • t:naive_datetime/0 — date 필드 + time 필드(타임존 없음)
  • t:datetime/0 — naive datetime에 time_zone, zone_abbr, utc_offset, std_offset까지 포함

몇 가지 기본 단위 타입도 있어요. t:year/0은 정수, t:month/0t:day/0은 양의 정수, t:hour/0, t:minute/0, t:second/0은 음이 아닌 정수입니다. t:week/0은 양의 정수예요. t:time_zone/0은 IANA tz 데이터베이스의 ID(예: Europe/Zurich)를 나타내는 문자열이고, t:zone_abbr/0은 존 약어(예: CET, CEST, BST)를 나타냅니다.

시간대 오프셋을 다루는 타입도 있어요. t:utc_offset/0은 표준 시간의 ISO 초 단위 UTC 오프셋이고, t:std_offset/0은 표준 오프셋(보통 여름 시간에 0이 아님)입니다. 벽시계(wall time)에 쓰이는 UTC로부터의 총 오프셋을 얻으려면 std_offsetutc_offset에 더해야 해요.

특별한 내부 표현 타입도 있습니다.

@type day_fraction() ::
  {parts_in_day :: non_neg_integer(), parts_per_day :: pos_integer()}

t:day_fraction/0은 시간을 하루의 분율로 표현해요(자정에서 시작). parts_in_day은 하루 중 이미 지나간 양을, parts_per_day은 하루가 나뉘는 총 부분 수를 나타냅니다. 그리고:

@type iso_days() :: {days :: integer(), day_fraction()}

t:iso_days/0은 달력 간 변환에 쓰이는 내부 날짜 형식입니다. ISO 8601 표기법으로 0000-01-01+00:00T00:00.000000(프롤렙틱 그레고리력의 기원전 1년 1월 1일 자정으로도 알려짐) 이후 지나간 일수와 마지막 날의 분율 부분까지 포함한 값이에요.

t:microsecond/0은 저장된 정밀도와 함께 마이크로초를 표현합니다.

@type microsecond() :: {value :: non_neg_integer(), precision :: non_neg_integer()}

value는 항상 마이크로초 단위 총값을 나타내고, precision은 외부 형식으로 나타낼 때 사용해야 할 자릿수예요. 정밀도가 0이면 마이크로초를 생략해야 하고, 6이면 value가 사용할 마이크로초 수와 정확히 일치함을 뜻합니다. 예를 들어:

  • {0, 0}은 마이크로초가 없음을 의미
  • {1, 6}은 1µs
  • {1000, 6}은 1000µs(1ms이지만 마이크로초 정밀도로 측정)
  • {1000, 3}은 1ms(밀리초 정밀도로 측정)

그 밖에도 t:era/0(음이 아닌 정수), t:day_of_era/0({day, era} 튜플), t:day_of_week/0(음이 아닌 정수), t:quarter_of_year/0 등이 있습니다.

달력 behaviour와 콜백

Calendar는 달력(calendar)을 구현하기 위한 behaviour를 정의해요. 실제 날짜·시간 구조체는 Date, Time, NaiveDateTime, DateTime에 담겨 있지만, "각 달력이 지켜야 할 규칙"이 바로 여기 콜백으로 정의되어 있습니다. 주요 콜백을 정리하면:

  • 조회 콜백: days_in_month/2(해당 연·월의 일 수), months_in_year/1(그 해의 월 수), leap_year?/1(윤년 여부 — 윤년 개념을 지원하지 않으면 false를 반환해야 함), valid_date?/3, valid_time?/4
  • 계산 콜백: day_of_week/4(요일, starting_on으로 주 시작 요일 지정), day_of_year/3, day_of_era/3, year_of_era/3, quarter_of_year/3
  • 변환 콜백: naive_datetime_to_iso_days/7naive_datetime_from_iso_days/1, time_to_day_fraction/4time_from_day_fraction/1, iso_days_to_beginning_of_day/1·iso_days_to_end_of_day/1(v1.15.0부터)
  • 문자열 콜백: date_to_string/3, time_to_string/4, naive_datetime_to_string/7, datetime_to_string/11(타임존 포함), 그리고 대응하는 파싱 콜백 parse_date/1, parse_time/1, parse_naive_datetime/1, parse_utc_datetime/1(v1.10.0부터)
  • 이동 콜백 (v1.17.0부터): shift_date/4, shift_time/5, shift_naive_datetime/8 — 주어진 Duration.t()만큼 해당 달력 규칙에 따라 이동
  • 롤오버 콜백: day_rollover_relative_to_midnight_utc/0 — 달력에서 하루가 끝나고 다음 날이 시작되는 순간을 정의. 예를 들어 자정에 새 하루가 시작되면 {0, 1}, 해 뜰 때면 {1, 4}, 정오면 {1, 2}, 해 질 무렵이면 {3, 4}를 반환해요. 두 달력의 롤오버 시각이 같은지 검사해, 같으면 날짜와 naive datetime까지 서로 변환할 수 있습니다.

day_of_week/4 콜백에 대해 조금 더 살펴볼게요.

@callback day_of_week(year(), month(), day(), starting_on :: :default | atom()) ::
  {day_of_week(), first_day_of_week :: non_neg_integer(),
   last_day_of_week :: non_neg_integer()}

starting_on은 주의 시작 요일을 나타내며 모든 달력은 적어도 :default 값을 지원해야 해요. day_of_week의 값은 서수(ordinal)로, 1은 "주의 첫 번째 날"을 뜻하지 "월요일"을 뜻하지는 않아요. first_day_of_weeklast_day_of_week보다 작아야 하고 day_of_week는 그 범위 안에 있어야 한다는 요구사항이 있습니다.

이 모듈이 behaviour를 정의하는 이유가 흥미로운데, Elixir의 달력 기능은 "범용 datetime API"가 아니라 "다른 것들의 기반"을 목표로 하기 때문이에요. 그래서 새 달력(예: 다른 역법)을 만들려면 이 콜백들을 구현하면 됩니다.

타임존 데이터베이스

Calendar는 타임존 데이터베이스를 가져오고 설정하는 함수도 제공합니다.

@spec get_time_zone_database() :: time_zone_database()
@spec put_time_zone_database(time_zone_database()) :: :ok

DateTime 모듈의 많은 함수는 타임존 데이터베이스가 필요해요. 기본적으로 이 모듈은 Calendar.get_time_zone_database/0이 반환하는 기본 타임존 데이터베이스를 사용하며, 그 기본값은 Calendar.UTCOnlyTimeZoneDatabase입니다. 이 데이터베이스는 Etc/UTC datetime만 처리하고 다른 타임존에 대해서는 {:error, :utc_only_time_zone_database}를 반환해요.

다른 타임존 데이터베이스(패키지가 제공하는 것 포함)는 설정으로 기본값을 지정할 수 있습니다:

config :elixir, :time_zone_database, CustomTimeZoneDatabase

또는 Calendar.put_time_zone_database/1을 호출해도 됩니다. 커스텀 타임존 데이터베이스에 대한 자세한 내용은 Calendar.TimeZoneDatabase를 참고하세요.

@spec compatible_calendars?(calendar(), calendar()) :: boolean()

compatible_calendars?/2는 두 달력이 새 하루를 시작하는 순간이 같은지 반환해요. 같지 않으면 datetime과 time만 변환할 수 있고, 같으면 date와 naive datetime도 변환할 수 있습니다.

strftime/3 — 포맷 출력

Calendar의 또 다른 핵심 기능은 strftime/3입니다. 이 함수는 주어진 date, time, datetime을 문자열로 포매팅해요.

@spec strftime(map(), String.t(), strftime_opts()) :: String.t()

datetime은 Calendar 타입(Time, Date, NaiveDateTime, DateTime)이거나, 포매팅에 필요한 모든 관련 필드를 담은 맵이면 됩니다. 예를 들어 %Y로 연도를 포매팅한다면 datetime에 :year 필드가 있어야 해요. 따라서 %Y를 기대하는 포맷에 Time이나 :year 필드가 없는 맵을 넘기면 오류가 발생합니다.

포매팅 문법은 %<padding><width><format> 형태입니다. %는 포맷 섹션의 시작, <padding>은 패딩(-: 패딩 없음, _: 공백, 0: 0으로 채움), <width>는 최소 너비(최대 1024), <format>은 실제 포맷 코드예요.

주요 포맷 코드는 다음과 같습니다.

포맷 설명 예(ISO)
a 요일 약어 Mon
A 요일 전체 이름 Monday
b 월 약어 Jan
B 월 전체 이름 January
c 선호하는 날짜+시간 표현 2018-10-17 12:34:56
d 월의 일 01, 31
f 마이크로초(자기 정밀도를 너비·패딩에 사용) 000000, 999999, 0123
H 24시간제 시 00, 23
I 12시간제 시 01, 12
j 연중 일수 001, 366
m 01, 12
M 00, 59
p "AM"/"PM"(정오는 "PM", 자정은 "AM") AM, PM
P "am"/"pm" am, pm
q 분기 1, 2, 3, 4
s Epoch(1970-01-01 00:00:00+0000 UTC) 이후 초 1565888877
S 00, 59, 60
u 요일(1=월요일, 7=일요일) 1, 7
x 선호하는 날짜(시간 없이) 표현 2018-10-17
X 선호하는 시간(날짜 없이) 표현 12:34:56
y 연도 2자리 01, 86, 18
Y 연도 -0001, 1986
z UTC 타임존 오프셋 +hhmm/-hhmm(naive면 빈 문자열) +0300, -0530
Z 타임존 약어(naive면 빈 문자열) CET, BRST
% 리터럴 "%" 문자 %

그 외의 문자는 유효하지 않은 포맷으로 해석되어 오류를 일으켜요. %f는 너비와 패딩 수정자를 지원하지 않고, 구조체의 microseconds 필드 정밀도(최소 1)로 마이크로초를 절단해 포매팅합니다.

사용자 옵션으로 :preferred_datetime, :preferred_date, :preferred_time, :am_pm_names, :month_names, :abbreviated_month_names, :day_of_week_names, :abbreviated_day_of_week_names을 제공할 수 있어요. 이 옵션들로 언어별 이름을 주입할 수 있습니다.

iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%y-%m-%d %I:%M:%S %p")
"19-08-26 01:52:06 PM"

iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%a, %B %d %Y")
"Mon, August 26 2019"

마지막으로 truncate/2(v1.6.0부터)는 마이크로초 튜플을 주어진 정밀도(:microsecond, :millisecond, :second)로 절단한 값을 반환합니다.

더 알아보기

  • 실제 날짜·시간 구조체는 Date, Time, NaiveDateTime, DateTime 문서를 참고하세요.
  • 커스텀 타임존 데이터베이스를 만드는 방법은 Calendar.TimeZoneDatabase에서 다룹니다.
  • 기본 제공되는 ISO 달력 구현은 Calendar.ISO를 참고하세요.
  • 이 모듈의 완전한 콜백·타입 목록은 Elixir hexdocs에서 확인할 수 있어요.