技術文章

使用 HotXLS 在 Delphi 中處理 R1C1 公式標記法

HotXLS 透過 TXLSCalculator.GetCompiledFormulaR1C1 編譯以 R1C1 參照標記法寫出的公式,該方法接受 R2C3(絕對參照)、R[-1]C[2](相對位移)與 RC(目前儲存格)等形式,將它們依據公式所在儲存格的列與欄轉換為 A1 標記法,並將結果傳送給處理一般 A1 公式的同一個編譯器;對於需要在數百列中生成相同公式的 Delphi 與 C++Builder 程式碼而言,這一個方法消除了一整類字串串接的錯誤

任何以程式設計方式填滿一欄的人都對這類錯誤非常熟悉;您巡迴列,並針對每一列使用 Format('D%d*E%d', [Row, Row]) 建置一個 A1 公式字串;每一次反覆運算都會將列號拼接至文字中,而列號是唯一改變的部分;只要算錯一次位移、將以 0 為基底的迴圈計數器與以 1 為基底的 A1 列號混淆,或是因為標頭列而位移了資料區塊,該欄中的每一個公式都會指向偏離一列的位置;沒有任何內容會引發錯誤,數字就是錯的;您實際想要的公式是「將我左側的兩個儲存格相乘」,這根本不需要提及列號,而 R1C1 標記法可讓您以這種方式編寫

什麼是 R1C1 標記法以及您應該何時使用它?

R1C1 標記法是以列號和欄號來定址儲存格,而不是以欄字母加上列號,且它將相對參照標記為相對於公式儲存格的明確位移;R2C3 是第 2 列第 3 欄的絕對儲存格,A1 將其拼寫為 $C$2R[-1]C[2] 是位於公式所在位置上方一列、右側兩欄的儲存格;RC 則是公式儲存格本身;括號中的位移是關鍵:不論是哪一個儲存格承載它,R1C1 中的相對參照讀起來都相同,而該相同參照的 A1 轉譯則會隨著每一列而改變

該標記法正好在一個情境中發揮了價值,而這也是個常見的情境:範本生成,其中必須將同一個相對公式置於資料區域的每一列中;在 A1 中,您必須針對每一列重新轉譯公式文字;在 R1C1 中,該文字是個常數;這也更接近試算表檔案格式內部的思維方式:共用公式紀錄將相對參照儲存為相對於主儲存格的列與欄位移,因此逐列的 A1 字串只是您的程式碼合成出的內容,解析器仍要將其拆解回位移;R1C1 跳過了此來回過程;對於互動式的、由人類編寫的公式,A1 仍然是自然的選擇,這就是為什麼它在 HotXLS 的所有地方都保持為預設值

HotXLS 如何編譯 R1C1 公式?

HotXLS 在 v2.175.0 中新增的此功能公開於兩個層級;TXLSCalculator.GetCompiledFormulaR1C1(UncompiledFormula: String; SheetID, CurRow, CurCol: Integer): TXLSCompiledFormula 是大多數程式碼呼叫的方法:它傳回一個準備好進行求值的編譯公式,與其 A1 同胞 GetCompiledFormula 完全相同,但多出了兩個額外參數,用於指名公式所屬儲存格以 0 為基底的列與欄;在其底層,TXLSFormula.GetCompiledR1C1 產生原始語法樹,而獨立函式 R1C1ToA1(const AFormula: String; CurRow, CurCol: Integer): String 執行實際的標記法轉換;該管道刻意設計得很簡單:使用主儲存格座標將 R1C1 文字翻譯為等效的 A1 文字,然後透過現有的引擎編譯 A1 文字——該同一個引擎負責解析 定義名稱與跨工作表參照 並發送 自訂工作表函式

// The formula lives in D5: CurRow = 4, CurCol = 3 (both 0-based)
S := R1C1ToA1('R2C3', 4, 3);
// S = '$C$2'  (absolute row and column)

S := R1C1ToA1('SUM(R[-3]C[0]:R[-1]C[0])', 4, 3);
// S = 'SUM(D2:D4)'  (offsets resolved against D5)

S := R1C1ToA1('ROUND(R[-1]C[0], 2)', 4, 3);
// S = 'ROUND(D4, 2)'  (the R in ROUND is left alone)

使用一個相對公式填滿一欄

成果在迴圈中顯現;將在每一次反覆運算中重新轉譯公式文字的 A1 版本,與公式為常數且僅有主體座標移動的 R1C1 版本進行比較;兩者都透過 TXLSCalculator 編譯並使用 GetValue 求值;計算器在建置時接受一個儲存格提供者回呼,以便評估器可以從您的資料來源提取儲存格值

// A1 style: a different formula string for every row
for Row := 1 to 500 do
begin
  FormulaText := Format('D%d*E%d', [Row + 1, Row + 1]);  // 1-based A1 rows
  Compiled := Calc.GetCompiledFormula(FormulaText, 0);
  // ... evaluate, store, free ...
end;
const
  AmountFormula = 'RC[-2]*RC[-1]';  // two cells to the left, same row
var
  Calc: TXLSCalculator;
  Compiled: TXLSCompiledFormula;
  Value: Variant;
  Row: Integer;
begin
  Calc := TXLSCalculator.Create(nil, Provider.GetValue);
  try
    for Row := 1 to 500 do
    begin
      Compiled := Calc.GetCompiledFormulaR1C1(AmountFormula, 0, Row, 5);
      try
        if Calc.GetValue(0, Compiled, Row, 5, Value, 1) = lxOk then
          StoreResult(Row, 5, Value);
      finally
        Compiled.Free;
      end;
    end;
  finally
    Calc.Free;
  end;
end;

R1C1 迴圈在公式文字中完全沒有列算術;'RC[-2]*RC[-1]' 在第 2 列和在第 500 列都代表「同一列向左兩欄,乘以同一列向左一欄」,且如果資料區塊稍後因標頭列而向下移動,公式常數不會改變——僅有迴圈邊界會改變;A1 版本有兩個地方可能會出錯 + 1 調整;而 R1C1 版本則為零個

轉換器接受哪些 R1C1 形式?

R1C1ToA1 轉換器可識別 v2.175.0 中記載的形式:任一方向上相對位移的 R[n]C[m]、絕對列與欄的 RnCm、帶有負位移的 R[-n]C[m]、代表公式儲存格本身的裸 RC,以及諸如 R1C1:R3C3R[-1]C:R[1]C 的範圍;列與欄部分是獨立解析的,因此像 R[1]C3 這樣的混合形式(相對列、絕對欄)也能正常運作,且字母不區分大小寫,因此 r[-1]c[2] 的編譯結果與其大寫同胞相同;帶中括號的部分會成為未錨定的(相對)A1 座標;裸數字則會成為帶有 $ 錨定的絕對座標

有趣的問題是轉換器避免損毀公式中的其他所有內容,因為 R 或 C 是常見字母;它的識別規則是基於 token 的:只有當 R 前面沒有其他字母時,才會被視為參照的開頭,且該候選對象必須接著被完整解析——選用性的列部分、必要的 C、選用性的欄部分——否則文字會原樣還原;這就是為什麼 ROUND(R[-1]C[0], 2) 僅轉換內部參照:ROUND 中的 R 後面是 O 而不是數字、括號或 C,因此解析失敗,函式名稱逐字傳遞;相同的邏輯也保護了 ROW(),且以 C 開頭的函式名稱根本絕不會成為候選對象,交由 R 能啟動參照;v2.175.0 的版本資訊說明字串常值與識別碼也會被跳過;即便如此,如果您的公式中帶引號的常值恰好包含形狀完全像 R1C1 參照的文字,在生產環境中信任它之前,值得檢查一次編譯的輸出

值得知道的座標基底與錨定細節

此 API 內部有兩種約定相遇,理清它們可以避免唯一真正的陷阱;GetCompiledFormulaR1C1CurRowCurCol 參數遵循計算器 API,是以 0 為基底,而標記法內部的數字則是以 1 為基底,與 Excel 顯示的內容相符:R2C3 是第 2 列第 3 欄,也就是 $C$2,而不是 $D$3;如果您的迴圈計數器已經是以 0 為基底,則直接將其作為 CurRow 傳遞;+ 1 的處理存在於轉換器內部,而不是在您的程式碼中

如果編譯後公式的生命週期將長於為其編譯的儲存格,還有一個細節需要注意;當您省略列或欄部分時——例如 RC[-1]R[2]C 形式——省略的座標會解析為主儲存格,並在轉換後的 A1 文字中作為帶有 $ 錨定的絕對座標發出;在求值時這是看不出來的,因為不論哪種方式其值都相同;但相對與絕對錨定控制著稍後插入或刪除列或欄時參照如何移位,這在 公式參照調整的隨附文章 中有所提及;如果您需要座標在結構編輯期間保持相對關係,請明確編寫位移——使用 R[0]C[-1] 而不是 RC[-1]——這樣轉換器就會發出未錨定的 A1 參照

R1C1 編譯是 HotXLS Delphi Excel 元件 中公式引擎的一部分,與 A1 編譯器、重新計算圖和上方顯示的求值 API 並存;如果您的程式碼藉由將公式巡迴向下填滿欄來建置試算表,那麼將這些迴圈從字串拼接的 A1 移動到單一的 R1C1 常數,是目前最經濟實惠的可靠性升級方案之一