Bütün bir sütuna başvuran bir tanımlı ad, skaler konumda göründüğünde Excel tarafından tek bir hücre olarak okunur: satır 7'deki =Vertical+1 ifadesi tüm alanı değil, Vertical adının satır 7'ye denk gelen hücresini kasteder. HotXLS Delphi Component bu örtük kesişimi v2.382.4 ile iki düzeyde uyguluyor: değerlendirme sırasında ve bağımlılık çıkarımı sırasında; çünkü 4805 formüllü bir kredi şablonu, değeri doğru bulmanın tek başına yetmediğini gösterdi. Bağımlılık yürüyücüsü adı tüm alanına genişlettiğinde, o alanın herhangi bir hücresini besleyen bir formül var olmayan bir döngü kapatıyor ve TXLSXWorkbook.Recalculate tüm workbook'u reddediyor
Söz konusu şablon standart bir kredi amortisman workbook'u. Her önbelleğe alınmış değer 777'ye zehirlenip tam bir Recalculate koşusu yapıldığında her iki motor mimarisi de 23 döndürdü; bu, döngüsel başvuru kodu olan lxErrorRef demek. 4805 formülün 3842'si bağımsız beklentiyle uyuşmadı, B18 #VALUE! tuttu, E18 hâlâ 777'ydi ve J7'deki ödeme sayısı tamamlanmamış bir bakiye sütunundaki yer tutucuları okumuştu. Tek bir dönüş kodunun arkasında üç ayrı kusur saklanıyordu; bu makale her birini onu düzelten kaynakla birlikte ele alıyor
Bir sütun adına yapılan skaler başvuru neden yanlış bir döngü yaratıyor?
Çünkü bir bağımlılık grafiği yalnızca kenarları bilir ve bir formülden 480 satırlık bir alana giden tek bir kenar aslında 480 kenardır; bunlardan biri, formüle bağımlı bir hücreden geri döner. B1'de =IF(TRUE,Vertical+1,0) olsun, Vertical adı Inputs!$A$1:$A$2 olarak tanımlı olsun ve A2'de =B1+1 yazsın. Excel, B1'i A1+1, A2'yi B1+1 olarak değerlendirir; düz bir zincir. B1'i A1:A2'ye bağımlı olarak kaydeden bir yürüyücü A2'yi B1'in öncülü yapar; A2 zaten B1'i öncül olarak listeliyordur ve HotXLS'te artımlı yeniden hesaplama işini yürüten Kahn kuyruğu iki düğümün de in-degree değerinin sıfıra indiğini hiç görmez. Kredi şablonlarının dokusu tam olarak bu desendir: her dönem satırı bakiye, faiz oranı ve ödeme sayısı için adlandırılmış sütunlara başvurur, her ad tüm planı kapsar ve her satır ayrıca o sütunlara yazar. Adları genişletirseniz grafik tek bir dev strongly connected component olur. Aynı formülleri örtük kesişimle değerlendirirseniz grafik, her satıra bir tane düşen kısa zincirler kümesine dönüşür; ECMA-376 Part 1 §18.17.2'nin tek bir değerin gerektiği yerde tüketilen bir referans operandı için tarif ettiği davranış da budur
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Inputs');
Book.DefinedNames.Add('Vertical', 'Inputs!$A$1:$A$2');
Book.DefinedNames.Add('Alias', '=Vertical');
Sheet.Cells[1, 1].Value := 1;
// Skaler konum: formül satır 1'de olduğu için Vertical A1'e daralıyor
Sheet.Cells[1, 2].Formula := '=IF(TRUE,Vertical+1,0)';
Sheet.Cells[2, 1].Formula := '=B1+1';
// Tanımı başka bir ad olan bir ad da kesişime girer, yani burası A2
Sheet.Cells[2, 2].Formula := '=Alias';
// Referans sınıfı argüman: tüm alan toplanıyor, kesişim yok
Sheet.Cells[3, 2].Formula := '=SUM(Vertical)';
// Satır 6 A1:A2 dışında kalıyor, kesişim boş ve IFERROR bunu yakalıyor
Sheet.Cells[6, 2].Formula := '=IFERROR(Vertical,42)';
if Book.Recalculate = lxOk then
begin
// B1 = 2, A2 = 3, B2 = 3, B3 = 4, B6 = 42
// v2.382.4 öncesinde bu dala ulaşılamıyordu: B1 -> A2 -> B1 bir döngüydü
end;
finally
Book.Free;
end;
end;
HotXLS bir argümanın skaler olduğuna nasıl karar veriyor?
HotXLS yanıtı argümanın biçiminden değil fonksiyon tablosundan okur. TXLSFormula.InitFuncHash içindeki her girdi, THashFunc.SetValue üzerinden isteğe bağlı bir argüman başına sınıf string'iyle kaydedilir: 'IF' girdisi '100' taşır, 'SUMIF' '010', 'VLOOKUP' '1011' taşır ve 'SUM' hiçbir şey taşımaz, bu yüzden tüm argümanları fonksiyon düzeyindeki sınıf 0'a düşer. Yeni TXLSFormula.FunctionArgumentClass(APtg, AArgument) bu baytı THashFuncEntry.ArgClass üzerinden açığa çıkarır ve sonucun 1 olması değer sınıfı demektir. Bunlar [MS-XLS] §2.2.2'nin operand token'larına atadığı üç sınıfın aynısıdır ve encoder zaten onlara bağlıydı: bir referans yazarken ptg'yi $24 + $20 * aClass olarak hesaplar; bu, sınıf 0 için PtgRef, sınıf 1 için PtgRefV ve sınıf 2 için PtgRefA verir. Excel'in yazdığı bir BIFF dosyası bu sınıfı her referans token'ında saklar, yani tablosu spesifikasyonla uyuşan bir motor bu argüman skaler mi sorusunu veriye bakmadan yanıtlayabilir. SUMIFin ortadaki argümanı kriterdir, yani bir değer; birinci ve üçüncü argümanlar ise alan, yani referanstır. SUMPRODUCT fonksiyon düzeyinde sınıf 2, yani array olarak kaydedilir; =SUMPRODUCT(Vertical,Vertical) ifadesinin hâlâ tüm alanı çarpmasının nedeni de budur
Üç fonksiyon ilk argümandan sonrası için kendi tablo girdisine hiç bakmaz. IF (ptg 1), CHOOSE (ptg 100) ve IFERROR (ptg 255) seçtikleri şeyi olduğu gibi geçirir, yani dal argümanları fonksiyonun kendi bulunduğu konumun sınıfını miras alır. G2'deki =CHOOSE(1,Vertical,0) ifadesinin A2'ye çözümlenmesini, hemen yanındaki =SUMIF(Vertical,">0",Vertical) ifadesinin ise hâlâ iki satırı toplamasını sağlayan tek kural bu; bir amortisman planının en çok zorladığı kural da bu, çünkü dönem hücreleri kredinin hâlâ açık olup olmadığını test etmek için IF fonksiyonuna yaslanır
Sınıfı bağımlılık yürüyüşü boyunca taşımak
lxCalc.pas içindeki bağımlılık çıkarıcısı, derlenmiş sözdizimi ağacı üzerinde özyinelemeli bir Walk turudur ve iki kez bulunur: biri workbook başına grafik için TXLSCalculator.ExtractDependencies, diğeri workbook'lar arası grafik için ExtractWorkspaceDependencies. v2.382.4 her iki yürüyücüye iki ek parametre veriyor. AScalar bir formülün kökünde True olarak başlar, her fonksiyon çocuğu için FunctionArgumentClass üzerinden yeniden hesaplanır ve ptg 1, 100 ve 255'in dal argümanları için değişmeden geçirilir. ANameRoot ise yalnızca yürüyücü bir adın derlenmiş tanımına indiğinde True olur ve yalnızca parantezler olan SA_GROUP düğümleri boyunca hayatta kalır; böylece =A1:A2+1 olarak tanımlı bir ad düz bir alanla karıştırılmaz. Bir SA_RANGE düğümünde iki bayrak da True olduğunda AddResolvedRange, bağımlılığı kaydetmeden önce değerlendiricinin kullandığı aynı yardımcıyla alanı daraltır. Yardımcı, tamamını alıntılayacak kadar kısa
function IntersectNamedScalarRange(CurRow, CurCol: Integer;
var Row1, Row2, Col1, Col2: Integer): Boolean;
begin
Result := False;
if (Row1 = Row2) and (Col1 = Col2) then Exit(True); // zaten bir hücre
if (Col1 = Col2) and (CurRow >= Row1) and (CurRow <= Row2) then
begin
Row1 := CurRow; Row2 := CurRow; // tek sütun: bu satırı al
Exit(True);
end;
if (Row1 = Row2) and (CurCol >= Col1) and (CurCol <= Col2) then
begin
Col1 := CurCol; Col2 := CurCol; // tek satır: bu sütunu al
Result := True;
end;
end;
Yardımcının reddettiği her şey — iki boyutlu bir alan, çok sayfalı bir referans ya da satırı adlandırılmış sütunun dışında kalan bir formül — değerlendirme tarafında #VALUE! üretir, grafik tarafında ise hiç bağımlılık kaydetmez; Excel'in boş bir kesişim için yaptığı da budur. Değerlendirme tarafı TXLSCalculator.GetValueItemName içinde yaşar: derlenmiş tanımdan SA_GROUP sarmalayıcılarını soyar ve kök bir SA_RANGE ise GetRangeInfo çağırır, kesişimi alır ve tüm tanımı değerlendirmek yerine tek hücreyi FGetValue üzerinden çeker. Dış referanslar eski yolda kalır, çünkü kesişim uygulanacak yerel bir satır yoktur. Bir adın depolamasının ve kapsamının nereden geldiği tanımlı adlar ve sayfalar arası formüller makalesinde ele alınıyor; buradaki konu yalnızca ad çözümlendikten sonra motorun ne yaptığı
Yarı hesaplanmış bir sütun üzerinde MATCH neden 777 okudu?
Çünkü MATCHin lookup-array argümanı bir scan referansıdır ve scan referansları değerlendirme sırasından bilinçli olarak çıkarılmıştı. Lookup scan makalesi TXLSDepRange.LookupScan alanını getirmiş ve scan kenarlarını sıralamadan çıkararak nelerden vazgeçtiğinizi anlatan bir bölümle kapanmıştı: bir lookup formülü, aralığındaki her hücre yeniden hesaplanmadan çalışabilir ve bayat değerler okuyabilir. Etkileşimli bir oturumda bu bir sonraki turda yakınsar. Zehirlenmiş bir şablonun toplu yeniden hesaplamasında ise yakınsamaz; =MATCH(0.01,Balances,-1)+1 olarak tanımlı PaymentCount, bakiye sütununda hâlâ duran 777 yer tutucularını okudu ve doğru olamayacak bir dönem sayısı döndürdü
TXLSDepGraph.TopoOrder artık scan kenarlarını yumuşak sıralama kenarları olarak ele alıyor. Katı in-degree değerinin yanında bir ScanInDeg dizisi tutuyor; düğüm başına kirli scan öncüllerini sayıyor ve o öncüller üretildikçe azaltıyor — bunun için önceki değişikliğin zaten sakladığı ScanPrecedents, ScanDependents ve ScanPrecedentCount listelerini kullanıyor. Her turda Kahn kuyruğu hazır penceresini tarayıp ScanInDeg değeri sıfır olan ilk düğümü başa alıyor; hazır düğümlerin hepsi hâlâ bir scan öncülü bekliyorsa baştaki düğüm kararlı sırasıyla çıkarılıyor. Scan kenarları katı in-degree değerine hiç girmez, yani kendi sütunu üzerinde kendine başvuran bir VLOOKUP hâlâ geçerlidir; ama bitirilebilecek bir öncülü bekleyebilecek bir lookup artık bekliyor. Bunu sabitleyen regresyon testi LookupScan_WaitsForDirtyFormulaValues üç bakiye hücresini 777'ye zehirler ve PaymentCount değerinin 3 dönmesini bekler, sonra girdiyi sıfıra çevirir ve =IFERROR(PaymentCount,99) ifadesinin #N/A görüp 99 döndürmesini bekler
Dört ondalıklı kırpma nereden geldi?
Delphi Variant aritmetiğinden ve yalnızca iç içe konumlarda. TXLSCalculator.GetValueItem içindeki ikili operatörler, üst düzey bir + veya - işlemini zaten iki Double yerel değişkene kopyalıyordu, bu yüzden =B1-A1 sorunsuzdu. =IF(TRUE,B1-A1,0) içinde ise aynı çıkarma işlemi iki Variant üzerinde Value := Value - SubValue olarak çalıştı ve bir operand Int64 hücre değeri, diğeri Double olduğunda gözlemlediğimiz sonuç dört ondalık basamaklı sabit noktalı bir tip olan Currency oldu; yani 1066.1854641400994 eksi 120 dört ondalığa kırpılmış olarak geri geldi. Her ödemenin bir önceki satırdan bileşik olarak hesaplandığı bir planda bu hata, toplamlara ulaşmadan önce yüzlerce dönemden geçer
// TXLSCalculator.GetValueItem, ikili aritmetik dalı (lxCalc.pas)
if VarIsNull(Value) then Value := 0;
if VarIsNull(SubValue) then SubValue := 0;
// Karışık Int64/Double Variant aritmetiği Currency tipine yükselebilir.
// Tablo aritmetiği kayan nokta hassasiyetini korumak zorundadır.
if VarIsNumeric(Value) then Value := Double(Value);
if VarIsNumeric(SubValue) then SubValue := Double(SubValue);
Bu koruma SA_ADD, SA_SUB, SA_MUL ve SA_DIV için de aynı şekilde, operatörden önce çalışır; Arithmetic_MixedInt64AndDoubleKeepsPrecision regresyonu A1'e Int64(120), B1'e 1066.1854641400994 yazar, ardından iç içe farkı ve toplamı 1E-10, çarpımı ve bölümü 1E-8 ile 1E-12 toleransıyla kontrol eder. HotXLS, RTL'in derleyici sürümleri arasında karışık Variant tiplerine uyguladığı her yükseltme kuralını bildiğini iddia etmiyor; tablo aritmetiğinin IEEE double olduğunu iddia ediyor ve artık operatör görmeden önce her iki operandı da double yapıyor, yani soruyu ortadan kaldırıyor
Düzeltmenin garanti ettiği ve etmediği şeyler
v2.382.4'ten sonra her iki motor mimarisi de zehirlenmiş şablon için lxOk döndürüyor, 4805 önbelleğe alınmış değerin tamamı bağımsız satır satır beklentiyle 1E-7 içinde uyuşuyor ve önbelleklerin gerçekten zehirlendiği, kaynak hash'inin değişmediği ve her formülün hâlâ yerinde olduğu assertion'ları da tutuyor. Bunu başarmak için hiçbir iterasyon açılmadı ve hiçbir hata kodu bastırılmadı. Bir ad üzerinden kurulan gerçek bir döngü — A1'de =B1, B1 hâlâ Vertical okuyor — yine hata döndürür; NamedScalarRanges_IntersectWithoutFalseCycles testi de tam olarak bunu doğrulayarak bitiyor
Sınırları açıkça söylemekte fayda var. Örtük kesişim yalnızca, derlenmiş tanımı parantezler soyulduktan sonra tek bir sayfada tek sütunlu ya da tek satırlı bir alan olan adlar için geçerli; skaler konumdaki iki boyutlu bir ad, tıpkı Excel'de olduğu gibi #VALUE! verir ve tablonun tanımadığı bir fonksiyon FunctionArgumentClass üzerinden sınıf 0 alır, yani ad argümanları yine tam olarak genişletilir. Yumuşak sıralama bir garanti değil tercihtir: yalnızca scan kenarlarından oluşan bir döngü yine kararlı sırada değerlendirilir ve önbellekte ne varsa onu okur; lookup scan makalesinin bilerek kabul ettiği davranış budur. Şablonun tamamına ilişkin sonuç ise başka bir tablo motoruna değil bağımsız bir beklenti betiğine karşı doğrulandı, çünkü referans ofis paketi orijinal şablonu yeniden hesaplamayı 60 saniyelik bütçe içinde bitiremedi. HotXLS, Excel kurulu olmadan XLS, XLSX, ODS ve CSV okuyup yeniden hesaplayan ve yazan yerel bir Delphi ve C++Builder tablo bileşenidir; ad kesişimi, argüman sınıfı tablosu ve yumuşak scan sıralaması hesaplama motoru ortak olduğu için her biçimde geçerlidir ve güncel fonksiyon kapsamı HotXLS Delphi spreadsheet component ürün sayfasında listeleniyor