HotXLS, sıradan sayısal hücre değerlerini ve formül önbelleklerini XLSX'e 17 anlamlı basamakla yazar ve okurken Delphi RTL ayrıştırmasını, sınırlı ama kesin bir ondalıktan-binary64'e denetimle düzeltir; böylece hücreye koyduğunuz bir Double, kaydetme ve yeniden açmadan sonra bit bit geri gelir. On beş basamak — Excel'in ekranda gösterdiği sayı — bir IEEE 754 binary64 değerini tanımlamak için yeterli değildir ve boşluk tam olarak finans ile kimlik sütunlarının yaşadığı yerde görünür: 1015'i aşan tamsayılar
Değişikliği zorlayan hata zararsız görünüyordu. Çalışma sayfası formülü INT(-1E15-0.1), tümüyle gösterilebilir bir double olan -1000000000000001'e değerlenir. HotXLS onu doğru hesapladı, önbellekledi, dosyayı kaydetti ve diskteki önbellek -1E15 okuyordu. Excel özgün değeri ancak bir yeniden hesaplamadan sonra gösterirdi ve önbellek değerlerine güvenen her tüketici — çoğu öyledir — sessizce bir fazlası yanlış bir sayı aldı. Hiçbir şey fırlatmadı, hiçbir şey uyarmadı ve dosya doğrulamayı sorunsuz geçti
15 anlamlı basamak bir double için neden yeterli değildir?
On beş ondalık basamağın binary64'e gidip geri dönerken sağ çıktığı garanti edilir ama ters yön 17 ister. IEEE 754 binary64, 53 bitlik bir anlamlı kısma sahiptir; bu, değerin nerede durduğuna göre 15 ile 17 ondalık basamağa denk gelir. En fazla 15 anlamlı basamaklı her ondalık dizge bir double'a dönüşür ve değişmeden geri basılır. Bir double ise ondalık yazımı onu özgün biçimde tanımlamadan önce 17 basamağa kadar ister; 253'ün üzerindeki aralıkta komşu double'lar 2 araylıdır, 1015 ile 253 arasında ise 0.125 ile 1 arasıdır
Delphi'nin FloatToStr'u 15 anlamlı basamakla biçimler; Excel'in görüntüde kullandığı duyarlılığın aynısıdır. Eşleşme, kusurun bu kadar iyi saklanmasının tam da nedenidir: dosyayı Excel'de açın, hücre Delphi programınızda göründüğü gibi görünür; çünkü Excel de yalnızca 15 basamak gösterir. Fark yalnızca saklanan double'ı karşılaştırdığınızda, yakın iki değeri çıkardığınızda ya da önbelleği aritmetik yapan bir şeye verdiğinizde görünür olur. -1000000000000001 değeri 16 anlamlı basamaklıdır; 15'e yuvarlamak -1000000000000000 üretir ve sondaki birim sonsuza dek gider
ECMA-376 Part 1 §18.3.1.96, hücre değerini metin olarak taşıyan <v> öğesini tanımlar ve şemada hiçbir şey, bir yazıcının bir sayı hücresi için kaç basamak yazması gerektiğini dikte etmez. Duyarlılık tümüyle yazıcının sorumluluğudur; iki üreticinin de geçerli XLSX yazıp hücredeki sayının ne olduğu konusunda hâlâ anlaşamamasının nedeni budur
Delphi'den 17 anlamlı basamak yazmak
HotXLS artık sayısal hücreler için tek bir sabiti (invariant) kullanıyor: ffGeneral, 17 duyarlılık ve sabit biçim ayarlarıyla FloatToStrF. Hücre yazıcısının tarih dalı tam olarak o projeksiyonu zaten kullanıyordu; sıradan sayılar ona katıldı. Dışarıdan bakınca API'de hiçbir şey değişmez; bir Double ya da bir formül atarsınız, kaydedersiniz ve <v>'ye düşen belirteç double'ı yeniden üretmeye yeter
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Ledger');
Sheet.Cells[1, 1].Value := 9007199254740991.0; // 2^53 - 1
Sheet.Cells[2, 1].Value := -1000000000000001.0;
Sheet.Cells[3, 1].Formula := 'INT(-1E15-0.1)';
if Book.Recalculate <> lxOk then
raise Exception.Create('Recalculation failed');
// Değerler ile formül önbelleği 17 anlamlı basamakla yazılır
if Book.SaveAs('ledger.xlsx') <> lxOk then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
On yedi basamak bir tavandır, dolgu kuralı değil. ffGeneral sondaki sıfırları atar; 0.5 hâlâ 0.5 olarak yazılır ve küçük tamsayılar kısa kalır; yalnızca gerçekten ek basamaklara ihtiyaç duyan değerler onları alır. Maliyet, önceden kırpılmakta olan hücrelerde birkaç bayttır; piyasaya süreceğiniz en ucuz doğruluk düzeltmesi budur
17 basamak yazmak okurken değerleri neden hâlâ kaybeder?
Yeterli basamak yazmak, yalnızca okuyucu onları en yakın double'a çeviriyorsa işe yarar ve RTL'nin Val'ı bunu her zaman yapmaz. Doğrulama sırasında test makinesindeki RTL hem 9007199254740990.7'yi hem düz tamsayı 9007199254740991'i, komşu ve yanlış binary64 değerine okudu. Her iki dizge de 253-1'e düşmeliydi; biri HotXLS'in üretebileceği 17 basamaklı bir yazım, öteki Excel'in aynı değeri yazma biçimi. Yalnız başına düzeltilmiş bir yazıcı, HotXLS'in kendisinin doğru geri okuyamayacağı bir dosya sevk edecekti
Bu yüzden HotXLS Val'ı kapı bekçisi olarak tutar ve yanıtını düzeltir. Mevcut sayısal dilbilgisi tarih olmayan bir belirteci kabul ettikten sonra kesin bir ayrıştırıcı, ondalığı büyük tamsayılı bir kesir olarak kurar ve RTL adayının iki yanındaki ikili orta noktalarla karşılaştırır. Ondalık alt orta noktanın altına düşerse aday bir ulp aşağı iner; üst orta noktanın üstündeyse bir ulp yukarı çıkar; tam eşitlikte çift anlamlı kısım kazanır — IEEE 754 en yakına çifte yuvarlama kuralı. Adaya en fazla sekiz komşu düzeltmesi tanınır; bu fazlasıyla yeterlidir, çünkü Val bir mil değil bir birim yanılmaktadır
Düzeltme bilinçli olarak sınırlıdır. 48 karakterden uzun, 17 anlamlı basamaktan çok ya da üst büyüklüğü 324'ün üzerinde olan belirteçler onu atlar ve eski ayrıştırıcı sonucunu korur; her dahili başarısızlık da aynı şeye döner. Büyük tamsayılar, o sınırlar içindeki en geniş pay ile paydaya yetecek şekilde 32'şer bitlik 48 limb kullanır. Burada bir taşınabilirlik ayrıntısı önemlidir: limb açıkça UInt32 olarak bildirilmiştir. LongWord bariz seçim gibi görünür ama Delphi'nin Linux64 hedefinde 8 bayttır ve genişliği sessizce ikiye katlanan bir limb türü aritmetikteki her elden ve kaydırmayı bozar
Gidiş-dönüşü bit bit doğrulamak
Bu test için double'ları = ile karşılaştırmak sorun değildir; amaç tam eşitliktir ama bir bit dizilimi karşılaştırması hata raporunu daha kullanışlı yapar: bir ulp'lik sapma, özdeş basılan iki sayı olarak değil ham 64 bitlik değerde 1 farkı olarak görünür
function SameBits(A, B: Double): Boolean;
var
BitsA, BitsB: UInt64;
begin
Move(A, BitsA, SizeOf(BitsA));
Move(B, BitsB, SizeOf(BitsB));
Result := BitsA = BitsB;
end;
procedure CheckReopen(const FileName: string);
const
Expected: array[1..3] of Double =
(9007199254740991.0, -1000000000000001.0, -1000000000000001.0);
var
Book: TXLSXWorkbook;
Row: Integer;
Stored: Double;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) <> lxOk then
raise Exception.Create('Open failed');
for Row := 1 to 3 do
begin
Stored := Book.Sheets[1].Cells[Row, 1].Value;
if not SameBits(Stored, Expected[Row]) then
Writeln('row ', Row, ' drifted: ', FloatToStrF(Stored, ffGeneral, 17, 0));
end;
finally
Book.Free;
end;
end;
3. satır ilginç olanıdır. INT(-1E15-0.1)'in önbelleklenmiş sonucunu yeniden hesaplamadan okur; formül motorunun değeri yeniden hesaplayabildiğini değil önbelleğin kendisinin kayıpsız olduğunu kanıtlar. Bir hatta önbelleklenmiş sonuçlara güveniyorsanız yeniden hesaplamadan önbelleklenmiş formül değerlerini okuma rehberi önbellek durumlarını ve eksik önbelleğin neden asla uydurulmadığını anlatır
Hangi öteki XLSX parçaları sayısal önbellekler taşır?
Hücre yazıcısını düzeltmek birkaç yoldan birini kapattı; kalan her sayısal yazım noktasının ayrı ayrı kontrol edilmesi gerekiyordu. İlk sürüm, Pivot çıktısının değer önbellekleri dahil, çalışma sayfası değerlerini ve formül önbelleklerini kapsadı. Pivot kaynak önbellekleri ile Chart önbellekleri hâlâ 15 basamaklı huniden geçiyordu ve varsayımla değil iki izleyen sürümle kapatıldı
- Pivot kaynak önbelleği:
sharedItemsdeğerleri ile satır içi ve akışlıcacheRecordsyazıcılarının ikisi de artık aynı 17 basamaklı sabiti kullanıyor ve pivot okuma yolu aynı kesin orta nokta düzeltmesini uyguluyor.refreshedDategibi betimleyici öznitelikler özgün yazımlarını korur - Chart önbellekleri: önbelleklenmiş nokta değerlerini kuran tek yazım noktası 17 basamağa geçti ve trendline değerleri ile kategorileri, hata çubuğu doğrulamasını ve iki render yolunu kapsayan altı okuma noktası tek bir kesin ayrıştırma yardımcısından geçiyor
- Sütun genişlikleri ile eksen sınırları gibi öteki her sayısal şey, sonlu olmayan ve subnormal değerleri 0'a süren savunmacı biçimlendiriciden geçmeye devam eder; serileştirilmiş bir parça asla ayrıştırılamayan bir float içermemelidir
En önemli birlikte çalışabilirlik kontrolü Excel'in kendisiyle oldu. Excel 253-1'i 9007199254740991 tamsayısı olarak yazar; HotXLS eşdeğer 17 basamaklı yazımı 9007199254740990.7 yazabilir. İki belirteç de her iki tarafta aynı double'a ayrışır; Excel'de yazılmış, HotXLS tarafından yeniden açılıp yeniden kaydedilmiş ve sonra Excel'de yeniden açılmış bir dosya Pivot önbelleğinde aynı değerleri korur. Test edilecek özellik özdeş yazım değil karşılıklı tanımadır
Bilmeye değer sınırlar ve uç durumlar
Burada kayıpsızlık binary64 değerinin korunduğu anlamına gelir; her değerin gösterilebilir olduğu anlamını değil. Sonlu olmayan sonuçlar hâlâ #NUM! hataları olarak, subnormal'ler hâlâ sıfır olarak, tam olarak önceki gibi kaydedilir. Görüntü de ayrı bir konudur: Excel ekranda 15 anlamlı basamak göstermeye devam edecek; Excel'de 9007199254740993 yazan bir kullanıcı farklı bir saklanan değer alır ve hiçbir yazıcı bunu düzeltemez. Çalışma kitabınız Görüntülendiği gibi duyarlık ayarını kullanıyorsa değerler bilinçli olarak sayı biçimlerine yuvarlanır; bu makalenin konusunun tam tersidir. O modun kuralları HotXLS'in Excel görüntülendiği gibi duyarlık yuvarlamasını nasıl uyguladığında işlenir
Kesin okuyucu ayrıca Val'ın yerini almaz; yalnızca Val'ın zaten kabul ettiği değerleri düzeltir. RTL'nin reddettiği bir belirteç yine reddedilir ve 48 karakter, 17 basamak, 324 üst sınırının dışındaki bir belirteç eski ayrıştırıcının ürettiğini korur. HotXLS ya da Excel tarafından yazılan dosyalar için bu hiç önemli değildir; ikisi de hücre değerleri için böyle belirteçler üretmez ama 40 anlamlı basamaklı elle yazılmış bir XML parçası kesin düzeltmeyle değil RTL'nin duyarlılığıyla okunur
SSS
HotXLS, Excel'in bir hücre için gösterdiği sayıyı değiştirir mi?
Hayır. HotXLS yalnızca saklanan <v> belirtecine kaç basamağın girdiğini değiştirir. Excel hâlâ 15 anlamlı basamak gösterir; hücreler aynı görünür. Fark, altta yatan double'ın artık Delphi kodunuzun atadığıyla eşleşmesidir
17 yerine neden 16 basamak yazılmıyor?
On altı basamak her binary64 değeri için yeterli değildir. Bazı double'lar ondalık yazımları özgün olmadan önce 17 anlamlı basamak ister; 16 basamaklı bir yazıcı, nadir durumlarda iki farklı double'ı yine aynı metne eşler ki değişikliğin kaldırdığı sessiz sapma sınıfının ta kendisi budur
Düzeltmeden önce yazılmış dosyaların yeniden üretilmesi gerekir mi?
Yalnızca 15 basamaktan fazlasına ihtiyaç duyan değerler içeriyorlarsa. O belirteçler son basamaklarını diskte çoktan kaybetti; onları daha doğru okumak hiç yazılmamış olanı geri getiremez. Özgün veriden yeniden kaydetmek ya da formülleri yeniden hesaplayıp yeniden kaydetmek kesin bir dosya üretir
On yedi basamaklı sayısal çıktı, kesin binary64 okuma düzeltmesi ve uyumlu Pivot ile Chart önbellek işlemesi, Delphi, C++Builder ve Lazarus için HotXLS Delphi Spreadsheet Component'in parçasıdır; bileşen Excel ya da OLE otomasyonu kurulu olmadan XLSX okur ve yazar