스프레드시트를 열고 2026-06-19가 표시된 셀을 클릭하면, 수식 입력줄에는 여전히 날짜가 표기되어 있습니다. 하지만 이 동일 셀 정보를 Delphi 프로그램으로 읽어오면 날짜 문자열 대신 실수 데이터인 46192를 리턴 받습니다. 두 해석 모두 맞습니다. Excel은 본래 셀 내부에 날짜 전용 변수를 저장하지 않기 때문입니다. 오직 에포크 기준 경과일 수를 의미하는 날짜 정수(serial number)만을 기입해 두고, 화면 인쇄 장치 측에 해당 숫자를 날짜 서식 문자열로 변환해 묘사하도록 넘버 format 속성을 바인딩해 관리합니다. 셀 데이터 자체에는 시간 형식이 내장되어 있지 않으며, 오직 실수 값과 표시 규칙이 공존하고 있을 뿐이고, 이 표시 규칙이 숫자를 날짜로 분기해 주는 유일한 판별 기준선입니다.
이 이원화 구조가 스프레드시트 라이브러리가 피해야 할 날짜 파싱 오류의 근본 원인입니다. 날짜 숫자값 단독으로는 기준 시작점을 모르므로 진짜 연도를 판독해 낼 수 없습니다. 동일한 경과일 숫자라도 통합 문서의 특정 제어 플래그 변수 값에 따라 정확히 4년의 오차율을 지닌 두 날짜 정보로 다르게 계산됩니다. 또한 서식 정보를 읽어 날짜 무늬가 등재되어 있는지 매칭해 내지 못하면, 읽어 들인 데이터는 단순 실수 데이터 값으로 변환되어 버립니다. HotXLS의 날짜 정보 해석 코드가 이와 같이 설계되어 작동하는 기술적 연유입니다.
날짜 셀의 정의: 실수와 표시 서식의 연합
Excel은 기준 에포크 시점 이후의 경과 일수를 실수 형태로 저장하며, 소수점 이하 자리 정보는 시간 정보(정오 시점은 .5 값)를 묘사하는 데 씁니다. 정수 영역은 온전한 날짜 카운트입니다. 실수 데이터만 떼어 놓고 보면 시간성 여부를 추적할 수 없습니다. 이 실수를 시간으로 매핑해 주는 속성은 ECMA-376 표준이 정의하는 numFmt(숫자 서식 정의 식별자) 정보이며, 서식 문자열이 날짜/시간 포맷 명세 구조를 지녔을 때 비로소 날짜 형태로 인쇄됩니다. 이 서식을 제거해 버리면 일반 실수 수치로만 출력되며 본체 값은 전혀 변하지 않습니다.
이것이 셀 데이터를 취득할 때 반환 형식이 varDate 유형의 시간 타입이 될 수도 있고 순수 실수인 Double 형태로 출력될 수도 있는 이유가 여기에 있으며, 서식 매핑 코드를 읽는 작업이 외부 인코더가 기입한 진짜 시간 속성을 가려내기 위해 꼭 수행되어야 하는 이유입니다. HotXLS가 XLSX 문서를 읽어 들일 때, 각 셀은 실수 Value 데이터 정보와 함께 NumberFormatIndex 식별자 정보를 수반해 TXLSXCell 인스턴스로 전달되며, 이 인덱스 정보를 조회하여 날짜 변환 여부를 결정합니다.
var
Book: TXLSXWorkbook;
Cell: TXLSXCell;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('timesheet.xlsx') <> 1 then
raise Exception.Create('Cannot open workbook');
Cell := Book.Sheets[0].Cells[1, 1]; // row 1, col 1 (1-based)
// Value may arrive as varDate or as a plain numeric serial;
// the format index is the signal that tells them apart.
Writeln('raw value : ', VarToStr(Cell.Value));
Writeln('numFmt idx: ', Cell.NumberFormatIndex);
Writeln('format : ', Cell.NumberFormat);
finally
Book.Free;
end;
end;
1462일의 오차를 지닌 두 가지 에포크 기준점
기본 날짜 체계인 Windows용 Excel 규격은 1899년 12월 31일을 0 값으로 보고 날짜 카운트를 개시하므로, 날짜 정수 1 값은 1900년 1월 1일이 됩니다. 반면 과거 초기 Macintosh 버전 엑셀 사양은 1904년 1월 1일을 기점으로 시작하므로, 날짜 정수 1은 정확히 4년과 1일 뒤의 날짜가 됩니다. 통합 문서는 이 기준점이 어디로 할당되어 있는지 속성 플래그 값으로 파일에 명시해 보관합니다. OOXML 포맷 내에서는 통합 문서 부분 노드 내 date1904 속성 텍스트로 보관되며, HotXLS 컴포넌트는 이를 통합 문서의 Date1904 속성 값으로 매핑해 노출해 줍니다.
두 시작 에포크 간의 실오차 거리는 정확히 1462일입니다. 이는 윤년 366일 하루를 포함한 4개년의 총합 일수인 1461일에, 기점 설정 시 발생하는 오프셋 보정 수치인 1을 더한 수치입니다. 이 상수 수치는 변경 불가능하므로 기억해 두면 편리합니다. 시작 기점 정보 판독을 빠뜨린 채 1904 기반의 날짜 정수 데이터를 1900 기점 공식으로 해석해 대입하면, 모든 날짜가 정확히 4년가량 뒤틀려 출력되므로 데이터가 손상되었다고 착각하기 쉽습니다.
Delphi 고유의 TDateTime 변수 자료형 역시 1900년 개시 기준 체계에 바인딩되어 있으므로, 1904 플래그 파일 유입 시 TDateTime 매핑 함수는 가감 연산으로 1462 오프셋 보정을 처리해야 합니다. 1904 날짜 실수를 읽을 때는 1462를 감산하여 TDateTime에 주입하고, 반대로 1904 명세 파일에 TDateTime 정보를 기록할 때는 생성될 날짜 실수에서 1462를 감산해 파일에 주입해야 엑셀 화면에 목적한 날짜가 유효하게 묘사됩니다. HotXLS는 파일 내부의 Date1904 설정을 판독해 날짜 값 변환 주기에 이 보정 가감산을 자동 처리하므로, 개발자가 TDateTime 정보를 주입하는 것만으로 화면에 동일 날짜를 출력할 수 있습니다.
1900년 윤년 적용 기술 버그의 유산
1900 체계 하에서는 오랜 역사적 버그가 하나 내장되어 있습니다. Excel은 1900년을 윤년으로 강제 판독하여, 1900년 2월 29일을 유효 날짜(날짜 정수 60)로 해독합니다. 그러나 백주년 해는 400으로 나누어 떨어질 때만 윤년으로 선언하는 달력 규칙에 의거해 1900년은 평년입니다. 이 존재하지 않는 유령 날짜(phantom day)가 시스템에 수용된 연유는, 초기 스프레드시트 툴(Lotus 1-2-3)이 윤년 연산 오동작 상태를 보관한 채 유통되었고, Excel이 해당 데이터를 승계하는 과정에서 날짜 계산 충돌 오동작을 피하기 위해 동일하게 이를 구현하기로 호환 결정했기 때문입니다.
이로 인한 영향은 사소하지만 명확합니다. 1900년 3월 1일 이후의 모든 현대적 날짜 정수 값은, 있지도 않은 2월 29일 하루 치가 누적 합산되어 있어 실제 카운트 일수보다 언제나 1만큼 크게 나옵니다. 스프레드시트 라이브러리들은 이 명세상 오류를 바로잡는 대신 고스란히 복제해 구현해야 합니다. Excel 연산 결과와 소수점 오차 없이 동일한 산출 결과를 내는 것이 목적이기 때문입니다. 이를 표준 달력 법에 맞춰 독자적으로 바로잡아 버리면, 해독한 날짜가 엑셀 정보 표시보다 언제나 하루씩 밀려 버리는 더 참혹한 결함에 직면하게 되며, 실무 비즈니스 현장에서 백년 전 날짜 정보를 연산할 확률이 희박하다는 것을 고려하면 오류를 답습해 연계율을 지키는 것이 이익입니다. 1904 체계에는 이 유령 날짜 오류가 탑재되어 있지 않아, 과거 일부 기업들이 1904 방식을 고집해 쓰기도 했습니다.
numFmt를 활용한 날짜 판별 조건식
외부에서 전달받은 문서 내부의 데이터가 실수 값일 때, 이것이 날짜를 표기한 것임을 판별할 유일한 정보는 서식 속성뿐입니다. ECMA-376 표준은 공통 내장 서식 번호(format id) 목록을 규정해 놓았으며, 날짜용 지정 범위 구간이 정해져 있습니다. 14~22번 식별자는 우리에게 친숙한 m/d/yyyy, h:mm 형태 등의 표준 규격 서식 범위이며, 45~47번은 누적 시간 경과 표시 서식입니다. 추가로 CJK 아시아 달력 규격에 대응하기 위한 로컬 서식으로 27~36번 및 50~58번 구간 정보도 규격화되어 수반됩니다. 셀 서식 번호가 이 공통 예약 번호 범주 내에 안착해 있다면 시간 형식으로 판단할 수 있습니다.
내장 번호 정보는 공통 표준 서식만을 해독하며 커스텀 사용자 정의 서식은 판별할 수 없습니다. 사용자가 임의로 서식 코드를 커스텀 선언하면, 공통 번호 범위를 초과하는 신규 서식 번호가 생성되어 파일 내 커스텀 테이블에 등재됩니다. 이 커스텀 서식이 날짜 속성인지를 해독하려면, 서식 문자열 포맷 정보를 읽어 날짜 관련 지시 토큰(token)이 내포되어 있는지 분석해야 합니다. HotXLS는 이 두 가지 유형의 판독 검사를 XlsxNumFmtIsDate 함수로 단일화하여 제공하며, 내장형 범위에 해당할 시 바로 True를 리턴하고, 그 외 커스텀 서식은 XlsxFormatCodeIsDate 구문 파서를 구동해 문자열 내부를 해독해 냅니다. 개발자용 공개 인터페이스는 셀 속성의 NumberFormat 문자열 정보와 NumberFormatIndex 번호값입니다.
서식 문자열을 단순히 d나 m 문자로 검색해서는 안 되는 이유
날짜를 지시하는 지시 문자(날짜 일수 d, 월 m, 년 y, 시 h, 초 s)로 단순히 문자 검색을 시도하면 연산 오동작에 봉착합니다. 서식 문자열 내부에는 날짜 지시자가 아닌 문자 정보들도 혼재해 쓰이기 때문이며, 대표적으로 두 구조 하에서 오동작을 일으킵니다.
첫째는 쌍따옴표로 감싸진 리터럴 문자열 영역입니다. 일반 금액 서식 뒤에 문자를 표기하기 위해 #,##0 "MM"와 같은 서식을 주입할 수 있으며, 이 경우 뒤의 "MM"은 메가(Mega) 단위를 지칭하는 일반 텍스트 문자로 날짜와는 관계없습니다. 하지만 파서가 단순 검색식으로 "M"에 반응해 이 서식을 날짜 형식 셀로 판단해 버리면 오류가 발생합니다. 둘째는 대괄호 지시 영역입니다. 엑셀 서식 문자열 내부에는 대괄호 형태로 표기 색상 지시어([Red]), 대소 가치 비교식([>1000]), 로컬 언어 코드 및 경과 시간 지시자([h], [mm]) 등이 수반됩니다. 대괄호 내부에 들어간 알파벳 문자는 쓰임새에 따라 지시 토큰인 경우도 있고 일반 태그 정보인 경우도 있어, 대괄호 영역 안의 알파벳을 외부의 것과 동일하게 검색해 버리면 정보 유실이나 오독을 피할 수 없습니다.
올바른 문자열 파서는 문자 하나씩 순차 검사하며, 쌍따옴표 리터럴 안 영역에 있는지, 대괄호 중첩 수준 깊이가 어디인지 실시간 계측하며 추적해야 하고, 백슬래시(\)로 이탈(escape) 처리된 문자 해독 논리도 갖추어야 합니다. 쌍따옴표 리터럴 외부 공간 및 대괄호 외곽 구역에 노출된 순수 날짜 지시 문자만이 유효 지시 토큰으로 최종 계측되어 수용되어야 합니다. XlsxFormatCodeIsDate가 이 분석 연산을 수행합니다. 따옴표를 발견하면 문자열 해독 플래그를 올려 따옴표가 닫힐 때까지 문자 탐색을 격리하고, 백슬래시는 다음 한 문자 파싱을 건너뛰게 만들며, 대괄호 카운터를 사용해 [...] 영역 내부에서의 오독을 차단합니다. 덕분에 #,##0 "MM"는 정확히 일반 금액 수치 서식으로 판정되고, 반대로 리터럴 영역 밖에 단 한 글자의 m이나 d가 기입된 극도로 단순한 서식 파일도 날짜 형식 셀로 유효하게 감지해 냅니다.
외부 문서 날짜 판독 실무 예시
위에 기술한 여러 이론은 결국 단 하나의 실무 목표인 '타 엑셀 툴이 저장한 날짜 실수 값을 온전히 신뢰할 수 있는 날짜 객체로 역변환하는 것'으로 귀결됩니다. 날짜 정수로 경과일 카운트를 획득하고, 통합 문서 내의 Date1904 속성 플래그를 읽어 계산할 시작 기점을 결정하며, 셀 서식 정보 번호나 커스텀 서식 코드를 해독하여 진짜 날짜로의 변환 필요성 여부를 결정합니다. 이 세 요소 중 하나라도 생략되면 그럴듯하게 뒤틀린 가짜 날짜 데이터가 입력되어 버립니다.
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Cell: TXLSXCell;
r: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('vendor-export.xlsx') <> 1 then
raise Exception.Create('Cannot open export');
// The 1904 flag is workbook-wide: read it once, apply it to
// every serial the workbook hands back.
if Book.Date1904 then
Writeln('workbook uses the 1904 date system')
else
Writeln('workbook uses the 1900 date system');
Sheet := Book.Sheets[0];
for r := 1 to 10 do
begin
Cell := Sheet.Cells[r, 1];
// A date is only a date when its format says so; the same numeric
// value with a plain format is just a quantity.
Writeln(Format('row %d value=%s numFmt=%d code="%s"',
[r, VarToStr(Cell.Value), Cell.NumberFormatIndex, Cell.NumberFormat]));
end;
finally
Book.Free;
end;
end;
레거시 BIFF 파일 포맷 해석기 설계 시에는 추가적인 함정이 한 가지 더 있습니다. .xls 스트림 구조상 가로로 인접한 여러 실수 데이터 셀은 인쇄 최적화를 위해 단일 다중 레코드(MULRK) 블록 구조로 가두어 보관합니다. 이 밀집 레코드에 등재되어 있어도 날짜 속성은 동일하게 보존되어 있으므로, 판독 루프는 다중 레코드 내부의 서식 인덱스를 각각 파싱하여 날짜 여부를 매칭해 내야 하고, 1904 에포크 보정 역시 매치되는 셀마다 적용해 주어야 합니다. 다중 레코드를 판독하지 않고 일반 개별 레코드만 감시하는 단순한 로더는, 인접 날짜 열 데이터를 일반 정수 어레이 배열로 덮어버리는 오류를 보게 됩니다.
TDateTime 변환 대입 실무 원칙
서식 검증으로 날짜 대상임이 식별되고 Date1904 기점이 확인되면, 그 이후 변환은 연산 공식 대입으로 귀결됩니다. HotXLS 라이브러리 엔진이 사전에 varDate 형식으로 해독한 객체는 추가 조작 없이 즉각 TDateTime 변수에 할당해 활용할 수 있습니다. 엑셀 인코더 오류로 실수 Double 값으로만 반환된 데이터는 1900 개시 기준 계산 대입을 수행하며, 1904 플래그 파일인 경우 1462일을 빼서 TDateTime 시간 오프셋에 매치시킵니다. 반대로 TDateTime 값을 엑셀 셀에 주입할 때는 기본 1900 방식으로 실수 값을 인출해 보관하고, 저장 주기에 1904 플래그 파일인 것을 감지하면 1462 보정 가산을 수행하여 화면 도면이 4년 뒤의 엉뚱한 날짜로 날아가는 오류를 차단합니다.
신규 통합 문서 빌드 시 기점 설정을 명확히 지정해 쓰십시오. 기본 세팅값은 Date1904 = False이며 대부분의 Windows용 엑셀 사양과 부합합니다. Macintosh 전용으로 빌드해 보관해야 하거나 최종 조회 장치가 1904 계산 공식만을 처리하는 수신처일 때에만 이를 True로 켜서 저장하십시오. 4년 오차 오류를 방지하는 대전제는 데이터 작성 시와 조회 시 일관된 기점 변수를 바인딩해 처리하는 습관입니다.
시간 정보는 셀이 머금은 정보 속성 범주 중 극히 일부분입니다. 문서를 장식하는 보조 속성 정보인 제목, 작성자, 생성 시간 스탬프 등의 메타데이터는 통합 문서 속성 가이드 문서에서 확인 가능하며, 메타데이터 기록 시에도 미입력 정보를 0 값으로 판독하는 동일한 TDateTime 계산 공식이 대입됩니다. 또한 날짜 값이 단순 입력 실수가 아닌 수식 연산의 결과물일 때는, 수식 엔진 및 사용자 정의 함수 문서에 정의된 연산식 해석 주기를 거쳐 소수점 실수 데이터가 도출됩니다. 이 모든 기능은 별도의 엑셀 프로그램 자동화(OLE Automation) 연동을 타지 않고 로우 바이너리 단에서 파일을 분석하는 Delphi 및 C++Builder용 HotXLS Component 제품에 내장되어 안정적으로 제공됩니다.