기술 문서

델파이에서 재계산 없이 Excel 캐시 수식 값 읽기

네이티브 델파이·C++Builder Excel 라이브러리인 HotXLS는 Excel이 수식 옆에 이미 저장해 둔 값을 TryGetCachedFormulaValueIXLSFormulaCacheReader로 읽습니다. 어느 진입점도 계산기를 부르지 않고, 수식 토큰을 디컴파일하지 않고, dirty 상태를 갱신하지 않고, 모델에 아무것도 다시 쓰지 않습니다. 그래서 읽기만 한 통합 문서는 연 것과 정확히 그대로입니다

이것을 이끄는 시나리오는 지루하고 극히 흔합니다. 야간 작업이 남이 만든 수백 개의 통합 문서를 열고 각각에서 합계 열 하나를 뽑아 웨어하우스로 밀어 넣습니다. 합계는 이미 파일 안에 앉아 있습니다. Excel이 계산해서 저장해 둔 것입니다. 그런데 작업이 수식 셀에 값을 묻는 순간, 그 질문에 답이 하나뿐인 라이브러리는 의존성 그래프를 만들어 시트 전체를 평가하고, I/O 바운드여야 할 작업이 계산 벤치마크로 변합니다

수식 셀을 읽는 것은 왜 전체 재계산 비용이 드는가

수식 셀의 값 getter는 값을 만들어 달라는 요청이고, 만드는 유일하게 보편적으로 올바른 방법은 수식을 평가하는 것이기 때문입니다. 통합 문서를 편집하는 애플리케이션에게는 그것이 맞는 기본값이고, 통합 문서를 추출하는 파이프라인에게는 틀린 기본값입니다. 더 나쁘게 평가는 부작용에서 자유롭지 않습니다. 결과를 셀에 다시 쓰고, dirty 플래그를 뒤집고, 함수가 지원되지 않거나 외부 참조가 깨졌을 때 생산 애플리케이션과 다르게 해석될 수 있습니다. 운영팀에 읽기 전용이라고 설명한 작업이 디스크의 것과 더 이상 맞지 않는 통합 문서를 조용히 만들어 내고, 나중에 그것을 저장하는 것이 있다면 디스크의 파일도 바뀝니다

캐시 값 읽기는 계약의 나머지 절반입니다. 그것은 더 좁은 질문 — 생산 애플리케이션이 여기에 무엇을 저장했는가? — 에 답하고 다른 것은 답하기를 거부합니다. 진짜 신선한 숫자가 필요할 때 HotXLS는 여전히 의존성 그래프가 주도하는 증분 재계산을 줍니다. 요점은 추출과 평가가 두 개의 다른 호출이어야지, 두 가지 무드를 가진 하나의 호출이면 안 된다는 것입니다

하나의 셀에 관한 세 개의 직교 사실

결론부터. 캐시 수식 값은 세 개의 독립된 사실을 실고, 그것들을 하나의 Variant로 뭉개면 필요한 정보가 사라집니다. TXLSFormulaCacheInfo는 그것들을 State, Kind, Value로 따로 둡니다. TXLSFormulaCacheState는 다섯 경우에 걸쳐 출처를 기록합니다. xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated, xlfcsInvalidated. TXLSFormulaCacheValueKind는 페이로드를 xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean, xlfcvError로 분류합니다. 이 분리가 존재를 정직하게 보고하게 해 줍니다. 캐시된 blank, 캐시된 빈 문자열, 캐시된 False, 캐시된 0, 캐시된 오류는 전부 진짜 값이므로, 존재는 결코 VarIsEmptyVarIsNull에서 추론될 수 없습니다. TryGetCachedFormulaValuexlfcsLoadedxlfcsCalculated에만 True를 돌려주고, False를 돌려줄 때도 진단 가능한 상태를 채워 줍니다

HotXLS 레코드 TXLSFormulaCacheInfo는 하나의 수식 셀에 관한 세 직교 사실을 따로 둔다. 다섯 경우에 걸친 출처 State, 여섯에 걸친 페이로드 Kind, Variant Value. 그래서 캐시된 blank나 False가 없는 캐시로 오인되지 않는다
출처, 페이로드 타입, 페이로드 값이 따로 남습니다. 캐시된 blank, 0, 빈 문자열, 오류가 자신인 진짜 값으로 보고될 수 있는 유일한 방법입니다
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row, Col은 여기서 모두 1 기반
    if Book.TryGetCachedFormulaValue(1, 12, 5, Info) then
      Writeln('cached value: ', VarToStr(Info.Value))
    else
      Writeln('no usable cache, state ordinal ', Ord(Info.State));
  finally
    Book.Free;
  end;
end;

캐시 값은 왜 없는가

TryGetCachedFormulaValueFalse를 돌려주는 이유는 정확히 넷이고, 상태가 어느 것이 해당하는지 말해 줍니다. xlfcsNotFormula는 셀이 리터럴을 담거나 아무것도 담지 않음을 뜻하고, 좌표가 범위 밖인 경우도 같은 답으로 합쳐집니다. xlfcsMissing은 셀이 실제로 수식인데 생산자가 값 페이로드를 저장하지 않았음을 뜻합니다. 생성기가 수식을 쓰고 첫 열림 때 Excel이 결과를 채우게 두는 경우의 흔한 귀결입니다. xlfcsInvalidated는 로드 후 수식 텍스트가 교체되었음을 뜻하므로, 그전에 있던 값은 더 이상 존재하지 않는 표현식을 기술합니다. 반면 xlfcsCalculated는 성공 사례입니다. 파일에서 온 xlfcsLoaded와 달리, 이번 세션에 여러분 코드나 HotXLS 평가기가 만든 값을 표시합니다

없는 캐시에 관한 정직함은 그것을 발라 덮는 것보다 중요합니다. HotXLS는 값을 발명하기를 거부하고, 저장에서도 똑같이 엄격합니다. xlfcsLoadedxlfcsCalculated만 캐시 값을 내보내고, xlfcsMissingxlfcsInvalidated는 낡은 숫자를 파일에 얼려 넣는 대신 수식만 씁니다. 파이프라인에서 남는 분별 있는 응답은 셋입니다. 행을 건너뛰고 갭을 기록하거나, 그 통합 문서 하나를 일부러 재계산하고 비용을 받아들이거나, 평가하고 대조하거나. 평가된 숫자가 생산 애플리케이션이 썼을 것과 어긋난다면, 결과에서 추측하는 대신 두 계산이 갈라지는 지점을 찾는 도구는 수식 평가 트레이서입니다

클래식, OOXML, ODF 엔진을 통틀어 하나의 리더

파이프라인은 방금 연 파일이 BIFF인지 OOXML인지 ODF인지 신경 쓰면 안 됩니다. IXLSFormulaCacheReader는 셋 모두를 위한 단일 읽기 전용 진입점입니다. TXLSWorkbook.CreateFormulaCacheReaderTXLSXWorkbook.CreateFormulaCacheReader 둘 다 각 엔진이 이미 쓰는 스파스 셀 조회 위의 가벼운 어댑터를 돌려주며, 1 기반 시트·행·열 좌표가 동일합니다. 통합 문서 클래스는 일부러 그 인터페이스를 자기 자신이 구현하지 않습니다. 통합 문서에 대한 인터페이스 참조는 소유 의미론을 바꾸고 호출자가 수명 임대를 슬쩍 지나치게 합니다. 대신 통합 문서를 파괴하면 그 임대 안의 날 포인터가 지워지고, 여러분 코드가 여전히 쥔 리더는 해제된 메모리를 역참조하는 대신 다음 쿼리에서 EXLSFormulaCacheReaderInvalidated를 일으킵니다. fail-fast 수명 검사지 동시성 보장이 아닙니다

var
  Reader: IXLSFormulaCacheReader;
  Info: TXLSFormulaCacheInfo;
  Row, Missing, Errors: Integer;
  Total: Double;
begin
  Reader := Book.CreateFormulaCacheReader;
  Total := 0;
  Missing := 0;
  Errors := 0;
  for Row := 2 to LastRow do
    if Reader.TryGetCachedFormulaValue(1, Row, 7, Info) then
    begin
      case Info.Kind of
        xlfcvNumber: Total := Total + Double(Info.Value);
        xlfcvError:  Inc(Errors);
      end;
    end
    else if Info.State = xlfcsMissing then
      Inc(Missing);
  // 계산기는 안 돌았고, dirty 플래그는 안 움직였고, Book은 그대로다
end;

캐시 바이트가 실제로 사는 곳

클래식 .xls 파일에서 캐시는 Formula 레코드의 FormulaValue 필드로, [MS-XLS] §2.5.133이 기술하는 8바이트입니다. 상위 워드가 $FFFF와 같으면 페이로드는 IEEE 754 더블이 아니라 태그가 붙은 variant이고, 레이아웃은 미묘하게 틀리기 쉽습니다. variant 타입은 val[0]에, boolean 또는 BErr 페이로드는 val[2]에 앉고 val[1]은 정의되지 않습니다. HotXLS는 예전에 val[1]에서 페이로드를 읽었습니다. 숫자가 아니라 boolean이나 오류를 캐시하는 특정 파일에서만 표면화되는 off-by-one 부류입니다. 리더와 공유 수식 기록기는 이제 같은 오프셋에 합의하므로, 캐시된 TRUE는 노이즈로 썩는 대신 로드와 저장을 온전히 살아남습니다

HotXLS가 읽는 클래식 XLS Formula 레코드의 8바이트 FormulaValue 필드. 상위 워드가 FFFF와 같지 않으면 IEEE 754 더블이고, 같으면 variant 타입이 val 0에, Boolean 또는 오류 페이로드가 val 2에 앉는다
상위 워드가 FFFF일 때 필드는 태그가 붙은 variant이고 페이로드는 val[2]에 앉으며 val[1]은 정의되지 않습니다. 리더가 예전에 취하던 바이트가 정확히 그것입니다

패키지 포맷에서의 타입 충실성은 자기 함정을 가진 별개의 문제입니다. OOXML에서 캐시 값은 c 요소에 <v>로 달려 있고 t 속성이 ECMA-376 Part 1 §18.3.1.4에 따라 타입을 이름 짓습니다. HotXLS는 t="e"를 곧장 varError Variant로 읽고 저장 때 표준 오류 텍스트로 되돌려 매핑하므로, 오류는 결코 평범한 정수로 가장하지 않습니다. 하지만 델파이 RTL은 여기서 도와주지 않습니다. VarAsType(Integer, varError)는 변환 예외를 일으키기 때문입니다. 동작하는 구성은 TVarData.VTypeTVarData.VError를 직접 설정합니다. 날짜는 반대 방향으로 같은 규율을 따릅니다. t="d"와 ODF 날짜 값 타입은 명시적 타입 선언이고 varDate가 되는 반면, BIFF 숫자 캐시는 날짜 플래그를 전혀 실지 않으므로 Double로 남습니다. HotXLS는 셀 숫자 서식에서 날짜를 추측하지 않습니다. 숫자 서식은 표현이고 캐시는 데이터이기 때문입니다. ODF는 알아둘 가치가 있는 경우를 하나 더 더합니다. office:value-type="void"는 존재하지만 값을 실지 않는 캐시를 표현하고, ODF에는 오류 값 타입이 없으므로 오류처럼 보이는 텍스트는 오류로 승격되지 않고 텍스트로 보존됩니다

function DescribeCache(const Info: TXLSFormulaCacheInfo): string;
begin
  case Info.State of
    xlfcsNotFormula:  Result := 'not a formula cell';
    xlfcsMissing:     Result := 'formula stored with no cached value';
    xlfcsInvalidated: Result := 'formula replaced since load';
  else
    case Info.Kind of
      xlfcvError:    Result := 'error code ' + IntToStr(TVarData(Info.Value).VError);
      xlfcvDateTime: Result := DateTimeToStr(VarToDateTime(Info.Value));
      xlfcvBoolean:  Result := BoolToStr(Info.Value, True);
      xlfcvNumber:   Result := FloatToStr(Double(Info.Value));
      xlfcvString:   Result := VarToStr(Info.Value);
    else
      Result := 'present but blank';
    end;
  end;
end;

공유 수식은 캐시 값을 공유하는가

아니고, 그렇게 가정하는 것이 한 차례 훑기가 열 전체에 같은 숫자를 보고하게 되는 길입니다. OOXML 공유 수식은 수식 표현식과 저장 최적화만 공유합니다. 모든 멤버 셀은 여전히 자기 자신의 <v>를 소유합니다. 그래서 HotXLS는 값 없이 도착한 팔로워에 루트 멤버 캐시를 절대 전파하지 않고, xlfcsMissing으로 로드된 팔로워는 저장과 재열림 뒤에도 xlfcsMissing을 보고합니다. 그룹이 애초에 어떻게 저장되고 확장되는지 짚고 넘어가고 싶다면 공유 수식 si 속성과 그 확장의 메커니즘이 별도로 다뤄집니다. 캐시 읽기에 관한 한 규칙은 한 줄로 줄어듭니다. 모든 셀에게 물어보십시오, 물어보지 않은 것은 아무것도 믿지 마십시오

OOXML 공유 수식 그룹의 HotXLS 뷰. si 속성은 표현식과 저장 레이아웃만 공유하고 모든 멤버 셀은 자기 캐시 값을 소유하므로, 없이 로드된 팔로워는 xlfcsMissing을 계속 보고한다
그룹이 공유하는 것은 표현식이지 숫자가 아니므로, 루트 캐시는 절대 전파되지 않고 값 없이 도착한 멤버는 그 갭을 계속 보고합니다

캐시 값 읽기, 통합 크로스 엔진 리더, 그리고 불러내지 않기로 선택할 수 있는 재계산 엔진은 모두 델파이와 C++Builder용 표준 HotXLS Delphi Spreadsheet Component에 실려 나가며, Excel이나 어떤 OLE 자동화 서버에도 의존하지 않습니다. 제품 페이지는 여기 보인 통합 문서와 리더 진입점의 전체 API 레퍼런스를 싣습니다