영업일 vs 달력일: 공휴일을 포함한 마감 계산
영업일을 올바르게 세고 더하는 법, 즉 O(1) 평일 공식, 공휴일 달력 문제, 그리고 순진한 코드를 깨뜨리는 포함/제외 끝점과 시간대 가장자리 사례를 정리합니다.
영업일로 표현된 마감은 같은 수의 달력일과 다른 양이고, 둘 사이의 간극이 청구 분쟁, 놓친 신고, 화난 고객이 나오는 곳입니다. 송장의 "Net 30"은 30 달력일을 뜻합니다. "3 영업일 내 발송"은 주말을 뺍니다. 법적 통지의 "5 영업일 내 응답"은 주말 과 공휴일을 빼고, 어느 공휴일인지는 관할에 달렸습니다. 수학은 사소해 보이고 거의 결코 그렇지 않습니다. 영업일의 정의가 지역적이고, 가변적이고, 가장자리 사례로 가득하기 때문입니다.
구분이 중요한 이유
두 단위는 일주일보다 긴 어떤 기간에서든 며칠까지 갈라지고, 계약·SLA·법령이 의도적으로 하나를 고릅니다.
- 지급 조건. "Net 30"은 30 달력일이고, "30 영업일"은 대략 42 달력일입니다. 하나를 다른 것으로 다루면 만기를 1주 반 옮겨 연체료나 위반 조항을 촉발할 수 있습니다.
- SLA. "다음 영업일" 응답의 지원 SLA는 금요일 저녁 티켓이 토요일이 아니라 월요일이 만기라는 뜻이고, 월요일이 공휴일이면 화요일 만기입니다.
- 발송 ETA. 운송사는 운송을 영업일로 견적합니다. 목요일에 주문한 3 영업일 발송은 일요일이 아니라 그다음 화요일에 도착합니다.
- 법적·신고 마감. 법원과 세무 당국은 창을 영업일로 정의하고, 종종 주말이나 공휴일에 떨어지는 마감이 다음 영업일로 굴러가는 규칙을 더합니다. 이것을 틀리는 것은 반올림 오차가 아니라 놓친 신고입니다.
단위를 틀리면 답이 예측 가능한 양만큼 틀리고, 이는 무작위 오류보다 나쁩니다. 돈이나 마감이 걸리기 전까지 아무도 알아채지 못하기 때문입니다.
정의
달력일은 모든 것을 셉니다. 평일, 주말, 공휴일입니다. 두 날짜 사이 달력일 차이는 단순히 날짜 숫자의 뺄셈입니다.
영업일(근무일)은 주말과 공휴일을 뺍니다. 두 가정이 여기 숨어 있고, 둘 다 어딘가에서 틀립니다.
- 주말은 보편적이지 않습니다. 세계 대부분은 토요일과 일요일에 쉬지만, 중동 일부는 금요일–토요일 주말을 쓰고, 몇몇 관할은 목요일–금요일이나 단일 휴일을 써 왔습니다. "주말"은 상수가 아니라 구성 값입니다.
- 공휴일은 지역적입니다. 한 나라의 영업일이 다른 나라에서는 공휴일이고, 한 나라 안에서도 공휴일이 국가적, 지역적, 또는 특정 거래소나 산업 전용일 수 있습니다.
그래서 영업일은 "구성된 주말 날도 관련 달력의 공휴일도 아닌 날"입니다. 아래 모든 것이 그 달력을 올바르게 갖는 데 달려 있습니다.
두 날짜 사이 영업일 세기
순진한 접근은 한 번에 하루씩 돌며 그 날이 평일이고 공휴일이 아닐 때 카운터를 올립니다. 올바르고 읽기 쉽고, 날 수에 O(n)입니다. 몇 주에는 괜찮고, 몇 년 기간에는 낭비입니다.
두 날짜 사이 평일 수에는 O(1) 닫힌 형태 지름길이 있습니다. 핵심 관찰은, 7일의 모든 완전 블록이 정확히 평일 5개를 기여한다는 것입니다. 그래서 기간을 완전 주와 나머지로 나눕니다.
# 반열린 구간 [start, end)의 평일 세기
# (start 포함, end 제외). Mon=0 .. Sun=6.
total_days = end - start # 일 단위
full_weeks = total_days // 7
remainder = total_days % 7 # 0..6 남는 날
weekdays = full_weeks * 5
# 나머지 날만 걷되, start의 요일에서 시작:
for i in 0 .. remainder-1:
if (weekday(start) + i) % 7 < 5: # 0..4가 월..금
weekdays += 1
# 이제 [start, end) 안 평일에 떨어지는 공휴일을 뺀다:
business_days = weekdays - count_weekday_holidays(start, end)
나머지 루프는 기간 길이와 무관하게 최대 여섯 번 돌므로, 전체가 O(1)에 공휴일 검사 비용을 더한 것입니다. 공휴일 데이터가 정렬된 목록이나 날짜로 키 매긴 집합이면, 범위 안 공휴일 세기는 전체 스캔이 아니라 제한된 조회입니다.
끝점을 조심하세요. 위 공식은 반열린 구간 [start, end)를 셉니다. start는
세고 end는 안 셉니다. 양쪽 포함([start, end])은 하루 더이고, 양쪽 제외는 하루
덜입니다. 거의 모든 날짜 수학의 오프바이원 버그는 같은 시스템의 두 부분 사이 끝점
관례 불일치입니다. 한 관례를 고르고, 주석에 적고, 어디서나 적용하세요. 의심스러
우면 반열린 구간이 깔끔하게 합성됩니다. [a, c) 위 카운트는 [a, b)에 [b, c)를
더한 것과 같은데, 닫힌 구간은 그것을 공짜로 주지 않습니다.
공휴일 뺄셈에는 자체 함정이 있습니다. 평일에 떨어지는 공휴일만 빼세요. 토요일에 떨어지는 공휴일은 애초에 평일로 세지 않았으니, 그것을 빼면 과소 계산입니다.
날짜에 N 영업일 더하기
날짜 사이 세기와 날짜를 전진시키기는 다른 연산이고, 위 공식이 "N 영업일 더하기"로 직접 뒤집히지 않습니다. 전진은 걷고-건너뛰기로 깔끔하게 됩니다.
# `start`로부터 N 영업일 뒤 날짜를 반환.
# N > 0이면 앞으로 이동. `start` 자신은 세지 않음.
date = start
added = 0
while added < N:
date = date + 1 day
if is_weekday(date) and not is_holiday(date):
added += 1
return date
이것은 영업일에 O(N)이고, 실무에서 괜찮습니다. 아무도 1만 영업일을 더하지 않습니다. 닫힌 형태 버전이 존재하지만(완전 주를 계산한 다음 나머지를 걷기), 걷고-건너뛰기 형태가 틀리기 어렵고 커스텀 주말이나 공휴일 집합으로 확장하기 쉽습니다.
내재화할 것은 이것입니다. "+5 영업일"은 "+7 달력일"이 아닙니다. 우연을 빼고는요. 월요일에 5 영업일을 더하면 다음 월요일에 닿습니다. 그 사이 공휴일이 없을 때만 7 달력일입니다. 수요일에 5 영업일을 더하면 그다음 수요일에 닿고, 역시 7 달력일인데, 공휴일이 있으면 8일입니다. 주중에 시작하고 범위에 공휴일이 있으면 달력 간극이 9일 이상일 수 있습니다. 관계는 고정 배수가 아닙니다.
공휴일 문제
주말은 고정 규칙입니다. 공휴일은 데이터이고, 데이터는 썩습니다.
- 공휴일은 지역별입니다. 올바른 계산은 관련 관할의 공휴일 집합이 필요하고, "관련"은 나라, 주나 도, 또는 특정 증권거래소나 은행 시스템일 수 있습니다. 미국 연방 달력은 뉴욕 증권거래소 거래 달력과 맞지 않고, 그것은 독일의 한 주(Land)와 맞지 않습니다.
- 관측 이동. 고정 날짜 공휴일이 주말에 떨어지면, 많은 시스템이 인접 평일에 그것을 관측합니다. 토요일 공휴일은 앞의 금요일에, 일요일 공휴일은 뒤의 월요일에. 관측일이 영업일 제외이지 명목 날짜가 아니고, 어느 쪽으로 이동하는지의 규칙 자체가 관할별입니다.
- 이동하는 공휴일. 일부 공휴일은 다른 사건에 상대적으로, 또는 음력이나 태음태양력으로 정의되어 매년 다른 그레고리력 날짜에 떨어집니다. 그 날짜는 단순한 고정 월·일 규칙으로 계산할 수 없고, 그것을 발행하는 기관에서 와야 합니다.
이것이 2026년에 하드코딩한 공휴일 목록이 2028년에 틀리는 이유입니다. 새 공휴일이 선언되고, 일회성 공휴일(선거, 왕실 행사, 국가 애도)이 나타나고, 이동하는 것들이 옮겨 갑니다. 영업일 계산기는 소스에 박힌 상수가 아니라 유지되는 달력, 즉 규칙을 추적하는 라이브러리나 당신이 업데이트하는 데이터 소스가 필요합니다. 우리 공휴일 달력 도구는 바로 그 소스가 되기 위해 존재합니다. 추측하는 대신 살펴볼 수 있는 나라별 집합입니다.
무는 가장자리 사례
- 반일. 일부 달력은 부분 영업일을 포함합니다. 일찍 닫는 거래소, 큰 공휴일 전의 반일입니다. 단위가 온전한 영업일이면, 반일이 하나로 세는지, 0으로, 아니면 분수로 세는지 명시적으로 정하고 문서화하세요.
- 시간대. "영업일 종료"는 시간대 없이 무의미합니다. 누구의 도시에서 오후 5:00 마감인가요? 로스앤젤레스에서 제시간인 제출이 프랑크푸르트에서는 하루 늦을 수 있습니다. 시간대를 무시하는 날짜 수학은 날짜 변경선 반대편 누군가에게 꼬박 하루 어긋난 마감을 만들고, 이는 Unix 타임스탬프와 2038년 문제 에서 논한 같은 부류의 버그입니다.
- 마감 시각. 많은 "영업일" 규칙에 당일 마감 시각이 있습니다. 오후 2:00 전에 넣은 주문은 오늘 발송, 오후 2:00 후는 다음 영업일로 셉니다. 마감 시각은 영업일 산술이 시작하기 전에 사실상 시작 날짜를 옮기므로, 그것을 먼저 적용하세요.
- 굴림 관례. 계산된 마감이 비영업일에 떨어지면, 계약이 그것을 다음 영업일로 앞으로 굴릴지, 이전으로 뒤로 굴릴지, 그대로 둘지 결정합니다. 이것은 세기 자체와 별개 규칙이고 명시돼야 합니다.
산술은 입력이 고정되면 단순합니다. 주말 정의, 공휴일 달력, 끝점 관례, 시간대, 굴림 규칙입니다. 오류는 거의 전적으로 그중 하나를 암묵적으로 두는 데 삽니다.
달력 라이브러리를 엮지 않고 답이 필요하면, 우리 영업일 계산기가 두 날짜 사이 근무일을 세고 유지되는 나라별 공휴일 집합에 대해 영업일을 더하거나 빼며, 끝점 관례가 처리되므로 공휴일 로직을 다시 짓지 않고 마감을 확인할 수 있습니다.