Duration

Duration

기간(duration)을 다루는 struct와 함수를 제공하는 모듈이에요. Duration struct는 시간 척도(time scale) 단위들의 모음을 나타내며, 기간의 조작과 계산을 가능하게 해 줍니다.

날짜·시간 척도 단위는 정수로 표현되므로 양수와 음수 값을 모두 가질 수 있어요. 마이크로초는 {microsecond, precision} 튜플로 표현되는데, 이는 Time, DateTime, NaiveDateTime처럼 시간을 구현하는 다른 달력 타입과의 호환성을 보장해요.

출처: Duration

본문

시프트(Shifting)

Elixir 표준 라이브러리에서 기간의 가장 흔한 용도는 달력 타입을 **"시프트(shift)"**하는 거예요.

iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]

위 예시에서 Date.shift/2는 단위를 자동으로 Duration struct로 변환하지만, 직접 struct를 넘길 수도 있어요.

iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]

시프트가 산술 연산이 아니라는 점을 꼭 기억하세요. 예를 들어 date + 1 month + 1 monthdate + 2 months와 같은 결과를 주지 않아요. 예를 볼게요.

iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]

iex> ~D[2016-01-31] |> Date.shift(month: 2)
~D[2016-03-31]

보시다시피 결과가 달라요. 그래서 기간 연산을 "add"가 아니라 "shift"라고 부르는 거예요. 2016-01-31에 한 달을 더하면 2016-02-29가 되고, 다시 한 달을 더하면 2016-03-31 대신 2016-03-29가 되기 때문이에요.

특히 기간을 Calendar.ISO 타입에 적용할 때는 다음 규칙을 따릅니다.

  • 큰 단위(연, 월)가 작은 단위(주, 시, 일 등)보다 먼저 적용돼요.
  • 단위는 적용되기 전에 월(:year, :month), 초(:week, :day, :hour, :minute, :second), 마이크로초(:microsecond)로 통합(collapse)돼요.
  • 1년은 12개월, 1주는 7일과 같아요. 따라서 4주는 1개월과 같지 않습니다.
  • 존재하지 않는 날짜의 경우 결과는 가장 가까운 유효한 날짜로 내림됩니다.

shift/2 함수들은 달력을 인식하므로, 윤년과 적용되는 시간대의 DST를 고려해 항상 유효한 날짜·시간을 반환한다는 보장이 있어요.

간격(Intervals)

Elixir의 기간은 스트림 연산과 결합해 간격(interval)을 만들 수 있어요. 예를 들어 2024년 4월 17일부터 시작해 다음 세 개의 수요일을 가져오려면:

iex> ~D[2024-04-17] |> Stream.iterate(&Date.shift(&1, week: 1)) |> Enum.take(3)
[~D[2024-04-17], ~D[2024-04-24], ~D[2024-05-01]]

하지만 이번에도, 기간을 시프트하는 건 산술이 아니라는 점을 기억하는 게 중요해요. 그래서 무엇을 달성하고 싶은지에 따라 이 모듈의 함수를 쓸 수도 있어요. 아래 두 예시의 결과를 비교해 보세요.

# Adding one month after the other
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.iterate(date, fn prev_date -> Date.shift(prev_date, duration) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-29]]

# Multiplying durations by an index
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.from_index(fn i -> Date.shift(date, Duration.multiply(duration, i)) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-31]]

두 번째 예시는 날짜를 차례로 시프트하는 대신 기간에 대해 연산을 수행하므로 일관되게 매달 마지막 날을 가리킵니다.

기간 비교하기(Comparing durations)

기간을 정확히 비교하려면 특정 필드만 비교하거나 참조 시간 시점(reference time instant)을 사용해야 해요. 일부 필드가 다른 필드에 상대적이기 때문이에요. 예를 들어 1개월이 30일과 같다고 말할 수 있지만, 두 기간을 모두 ~D[2015-02-01]에 더하면 결과가 달라져요. 그 달은 28일밖에 없거든요.

그래서 기간을 비교하고 싶다면 한 가지 방법은 Date.shift/2(또는 DateTime.shift/2 등)를 쓴 뒤 날짜를 비교하는 거예요.

iex> date = ~D[2015-02-01]
iex> Date.compare(Date.shift(date, month: 1), Date.shift(date, day: 30))
:lt

또는 to_timeout/1을 사용해 기간을 고정 단위로 변환할 수 있어요. 이 함수는 주 이하의 기간만 지원하고 month나 year 필드가 설정되어 있으면 예외를 던집니다.

iex> to_timeout(hour: 24) == to_timeout(day: 1)
true

Summary(요약)

Types

  • duration()%Duration{} struct 또는 유효한 기간 단위 쌍의 키워드 리스트를 지정하는 기간 타입
  • t() — 기간 struct 타입
  • to_string_opts()Duration.to_string/2의 옵션
  • unit_pair() — 유효한 기간 단위 키와 값의 쌍을 지정하는 단위 쌍 타입

Functions

  • add(d1, d2) — 주어진 기간 d1d2의 단위들을 더해요.
  • from_iso8601(string)ISO 8601 형식의 기간 문자열을 Duration struct로 파싱해요.
  • from_iso8601!(string)from_iso8601/1과 같지만 실패 시 ArgumentError를 던져요.
  • multiply(duration, integer)duration 단위들을 주어진 integer로 곱해요.
  • negate(duration)duration 단위들을 부정(negate)해요.
  • new!(duration) — 주어진 unit_pairs로 새 Duration struct를 만들어요.
  • subtract(d1, d2) — 주어진 기간 d1d2의 단위들을 뺍니다.
  • to_iso8601(duration) — 주어진 durationISO 8601-2:2019 형식의 문자열로 변환해요.
  • to_string(duration, opts \\\\ []) — 주어진 duration을 사람이 읽기 좋은 표현으로 변환해요.

더 알아보기