Teknik Makale

Excel Kesir Biçimi: 3.14159 İçin 3 1/7, 3 14/99 Değil

3.14159'u # ??/?? kesir maskesiyle biçimlendirin; Excel, 3 14/99 yedi kattan fazla daha yakınken 3 1/7 gösterir. En yakın kesri arayan bir Delphi dışa aktarımı böylece kullanıcının hiç görmediği bir değer basar. Sebep şu: Excel yalnızca basamaklı kesir yakınsaklarını kabul eder. HotXLS'te TXLSXCell.FormattedText, dolgu boşlukları dahil, aynı 3 1/7'yi döndürür

Excel basamaklı kesir yakınsaklarında durur

Yaklaşık kesir üreten her maske Excel'e aynı işi verir: tamsayıdan sonraki kısım için, eğik çizgiden sonraki basamak yuvası sayısıyla sınırlı bir payda ile bir pay ve payda bulmak. Excel bunu, değeri basamaklı kesir olarak açıp yalnızca yakınsakları tutarak yapar; yakınsak, o açılımı her terden sonra keserek elde ettiğiniz kesirdir. 0.14159 için açılım [0; 7, 15, 1, ...] ile başlar; bu, önce 1/7'yi, sonra 15/106'yı, sonra 16/113'ü verir. 3 tamsayısı hiçbir şeyi değiştirmez. Onunla 3.14159, her pi yaklaşımı tablosunda bulunan aynı 22/7, 333/106 ve 355/113'ten geçer. 106, iki basamaklı bir paydayı çoktan taşırdığından Excel 1/7'de kalır. Daha yakın olan 14/99 ise bir yarı yakınsaktır (semiconvergent): iki komşu yakınsak arasında oturan ara kesirlerden biri. Excel oralara hiç uğramaz. Bununla karşılaşmak için özel bir koda da ihtiyaç yoktur: Hücreleri Biçimlendir iletişim kutusundaki Up to one digit ve Up to two digits türleri, ECMA-376 Part 1 §18.8.30'un öntanımlı sayı biçimi listesindeki 12 ve 13 kimlikli gömülü biçimlerdir; yani # ?/? ile # ??/??

3.14159'un basamaklı kesir yakınsaklarının sayı doğrusu: yakınlaşan eksenlerde 22/7, 333/106 ve 355/113; hangi yakınsağın tutulacağına kesir maskesinin payda sınırları karar verir
Açılımın her kesidi 3.14159'a daha yakın düşer; eğik çizgiden sonraki basamak sayısı, sınır içinde hayatta kalan yakınsağı belirler

Bu fark pi benzeri girdilerle sınırlı değildir. En iyi yaklaşık kesir araması (best approximation) — tipik olarak bir Stern-Brocot yürüyüşü ya da izin verilen her payda üzerinde bir tarama — hangi kesrin en yakın olduğunu sorar; yakınsak döngüsü ise, payda hâlâ sığarken açılımın hangi kesre ilk ulaştığını sorar. Sıradan iki ondalıklı girdilerde iki kural, fark edilecek kadar sık ayrışır ve aşağıdaki koddaki 0.27 tam da böyle bir değerdir. Kodunuz kesirleri, birisinin çalışma kitabıyla karşılaştıracağı bir PDF faturasına, bir HTML raporuna ya da bir CSV'ye yazıyorsa, Excel'in kuralını oraya kopyalayın; daha az isabetli olan kural olduğunu bilerek de olsa. Kâğıtta 3 14/99, Excel'de 3 1/7 gören bir okuyucu, dışa aktarımınızın daha hassas olduğu sonucuna varmaz; ikisinden birinin bozuk olduğu sonucuna varır. Bozuk olan Excel olmayacaktır

0.27 için yakınlaştırılmış sayı doğrusu: Excel'in gösterdiği 10/37 yakınsağı, en yakın kesir taramasından gelen daha yakın 17/63'e karşı; ayrışan ve uyuşan örneklerle
Sıradan iki ondalıklı değerlerde yakınsak döngüsü ile en yakın kesir taraması, fark edilecek kadar sık ayrışır; dışa aktarımlar Excel'in kuralını kopyalamalı

Döngünün kendisi kısadır. Eğik çizgiden sonraki basamak yer tutucularını sayın — ister ? ister # ister 0 — bir, iki ya da üç adet için sınır 9, 99 ya da 999 olur. # ??/?? gibi karma bir maske tamsayıyı ayırır ve yalnızca kesirli kısmı açar; ?/? gibi uygunsuz bir maske bütün değeri açar ve 3.14159'un ?/? altında 22/7 çıkmasının nedeni budur. Her adım, eldeki B değerinin tamsayı kısmı A'yı alır ve bir sonraki yakınsağa katlar. Kalan B − A, 5e-8'in altına indiğinde döngü durur; böylece kayan nokta gürültüsü taşımış basit bir kesir yine o kesre düşer. Bir adım paydayı sınırın ötesine ittiğinde Excel önceki yakınsağa geri düşer ve bu düşüş 1/1'e konabilir; karma bir maske bunu tamsayıya taşır, dolayısıyla # ?/? altındaki 12.9999, 13 gösterir. Aşağıda döngü, kaba kuvvet bir en yakın kesir aramasının yanında sade Delphi olarak yazılıdır; böylece iki kuralın kendi sayılarınızda nerede ayrıştığını görebilirsiniz

uses
  SysUtils;

type
  TFraction = record
    Num, Den: Int64;
  end;

// Excelin # ?/?, # ??/?? ve # ???/??? maskelerinin ardındaki yakınsak kuralı.
// X negatif değildir: karma maske için kesirli kısım, uygunsuz maske için
// bütün değer. MaxDen 9, 99 ya da 999'dur.
function ExcelFraction(X: Double; MaxDen: Integer): TFraction;
var
  B, R: Double;
  A, P, Q, P1, Q1, P2, Q2: Int64;
begin
  B := X;
  P2 := 0; Q2 := 1;      // yakınsak k-2
  P1 := 1; Q1 := 0;      // yakınsak k-1
  P := 0;  Q := 1;
  while Q1 < MaxDen do
  begin
    A := Trunc(B);
    P := A * P1 + P2;
    Q := A * Q1 + Q2;
    R := B - A;
    if R < 5E-8 then
      Break;               // kalan sıfır sayılır
    B := 1 / R;
    P2 := P1; Q2 := Q1;
    P1 := P;  Q1 := Q;
  end;
  if Q > MaxDen then       // sınırı aştı: bir yakınsak geri
    if Q1 > MaxDen then
    begin
      P := P2; Q := Q2;
    end
    else
    begin
      P := P1; Q := Q1;
    end;
  Result.Num := P;
  Result.Den := Q;
end;

// Bir "best approximation" yordamının döndürdüğü: D <= MaxDen iken en yakın N/D
function NearestFraction(X: Double; MaxDen: Integer): TFraction;
var
  D: Integer;
  N: Int64;
  Err, BestErr: Double;
begin
  Result.Num := Round(X);
  Result.Den := 1;
  BestErr := Abs(X - Result.Num);
  for D := 2 to MaxDen do
  begin
    N := Round(X * D);
    Err := Abs(X - N / D);
    if Err < BestErr then  // katı: eşitlikte sadeleşmiş biçim kazanır
    begin
      BestErr := Err;
      Result.Num := N;
      Result.Den := D;
    end;
  end;
end;

procedure CompareFractionRules;
const
  Samples: array[0..3] of Double = (0.14159, 0.99, 0.27, 0.5);
var
  I: Integer;
  E, N: TFraction;
begin
  for I := Low(Samples) to High(Samples) do
  begin
    E := ExcelFraction(Samples[I], 99);
    N := NearestFraction(Samples[I], 99);
    Writeln(Format('%-8g Excel %d/%d   nearest %d/%d',
      [Samples[I], E.Num, E.Den, N.Num, N.Den]));
  end;
end;

// Beklenen:
// 0.14159  Excel 1/7   nearest 14/99
// 0.99     Excel 98/99   nearest 98/99
// 0.27     Excel 10/37   nearest 17/63
// 0.5      Excel 1/2   nearest 1/2

0.27 satırı listede, çünkü sıradan iki ondalıklı bir girdi de ayrışır: 10/37'ye karşı 17/63; değerde hiçbir egzotik yan yoktur. Kuralların ne kadar sık ayrıştığı verilerinize bağlıdır: 0.5 ya da 0.375 gibi gerçek basit kesirler iki yoldan da aynı çıkar; ölçülen nicelikler ile hesaplanan oranlar çok daha az güvenilirdir. Trunc, Floor'un yerine durur; çünkü girdi asla negatif değildir. Excel işareti kesrin dışında ele alır. Sizin kodunuz da öyle yapmalı. Ve argüman, bir sonraki bölümün somutlaştıracağı nedenle, hücrenin gerçekte sakladığı Double olmalıdır

0.99, iki basamaklı bir kesirde neye dönüşür?

Excel, # ??/?? altında 0.99'u ·98/99 olarak gösterir (bu yazıdaki her · bir boşluk karakterini temsil eder). Sebep ikili kayan noktadır. 0.99 literali tam bir Double değildir ve saklanan değer onun bir tüy altında durur. Böylece 1/(1/0.99 − 1), kabaca 98.9999999999994 olarak değerlendirir, açılımın ikinci terimi 98 olur ve 98/99 yakınsağı sınırın altına sığar. Aynı açılımı tam ondalıklarla yapın: [0; 1, 99] elde edersiniz; işte o 99 paydayı 100'e iter, döngü 1/1'e geri düşer ve hücre 1······ okurdu. Burada Excel'den daha dikkatli olmak hatadır. Tutarları Currency ya da TBcd olarak tutan ya da ondalık dizgiyi veritabanından yeniden ayrıştırıp tam aritmetikle açan bir rapor üreticisi, Excel'in 98/99 yazdığı yere bütün bir 1 basar. Biçimlendiriciye saklanan Double'ı verin. Her ara değeri kayan noktada tutun. Kesir bir gösterim meselesi olarak da kalır: formüller 0.99'u görmeye devam eder ve HotXLS, precision as displayed açıkken bile kesir bölümlerini tam hassasiyetle tutar

Excel kesir maskesi # ??/?? altında 0.99 için iki yollu şema: saklanan Double 98/99'e açılırken tam ondalık aritmetik 1/1'e düşer
Saklanan Double 98/99'e konarken tam ondalık aritmetik 1/1'e düşer — burada Excel'den daha dikkatli olmak hatadır

Excel kesir alanını nasıl doldurur ve hizalar

Excel kesirleri, bir ondalık sütununun dizildiği gibi dizer: pay, yuvalarının içinde sağa yaslıdır; payda sola yaslıdır; böylece eğik çizgi her satırda aynı konuma düşer. Kullanılmayan her yuva, yer tutucusuna göre doldurulur: ? boşluk bırakır, 0 sıfır yazar, # hiçbir şey yazmaz. 3.14159'un # ??/?? altında 3··1/7· okunması budur: bir boşluk maskeden gelen ayraçtır, biri 1'in önündeki boş pay yuvasını, biri de 7'den sonraki boş payda yuvasını doldurur. Tablo, düzen kurallarını kapsayan bir girdi kümesi için Excel'in gösterdiği metni listeler

Biçim koduDeğerExcel'in gösterdiği
# ?/?0.5·1/2
# ?/?4/31·1/3
# ??/??3.141593··1/7·
# ??/??0.99·98/99
# ?/?1212····
# ?/?00····
# ?/?12.999913····
# #/#1212
0 ?/?0.50·1/2
#,##0 ?/?1234.751,234·3/4
?/?3.1415922/7
?/?00/1
??/??1212/1·

Elle yazılan biçimlendiriciler Excel'den en çok tüm sayılarda sapar. Payı ? kullanan karma bir maske altında Excel tamsayı basamaklarını — sıfır dahil — korur ve ayraç, pay, eğik çizgi ile paydayı her birini bir boşlukla değiştirir; dolayısıyla # ?/? altındaki 12, 12···· olur ve # ??/100 altında ardına yedi boşluk eklenir. Pay yer tutucusunu #'ye çevirin; dolgu gider. Sıfır tamsayı ters yönde işler: # yuvası 0.5 için hiçbir şey basmaz ve ayraç yalnızca bir boşluk olarak hayatta kalır; 0 ?/? basamağı zorlarken uygunsuz maskeler kesri hiçbir zaman düşürmez

Dolguyla ne yapılacağı, metnin nereye gittiğine bağlıdır. Tek aralıklı metin raporları ve sağa yaslı PDF tablo hücreleri için koruyun; orada eğik çizgileri hizalamak işin bütün noktasıdır. CSV ve JSON için kırpın; orada sondaki bir boşluk, 12'yi artık sayı olarak ayrışmayan bir dizgiye çevirir. HTML de bir karar ister; çünkü tarayıcı, hücre white-space: pre kullanmadıkça boşluk dizilerini çöker. Ya onu ayarlayın ya kırpın; kimi satırları kaydıran, kimi kaydırmayan yarım çökmüş dolgudan kaçının

# ?/8 gibi sabit paydalar

1'den 9'a bir basamakla başlayan rakamlarla yazılmış payda, bir sınır değil sabit bir değerdir ve aritmetik baştan aşağı değişir: Excel değeri paydayla çarpar, en yakın tüm paya yuvarlar ve kesri sadeleştirmeden bırakır. 0/8 altında 0.375, 3/8 gösterir ve 0.64, 5/8 gösterir; çünkü 0.64 × 8, 5.12'dir. Hazır As sixteenths türü 0.5'i 8/16 olarak gösterir; teknik resim okuyan bir operatörün beklediği de budur, her sonucu sadeleştiren bir biçimlendiricinin onu 1/2'ye çevirmesi değil. Yuvarlanmış bir pay paydaya ulaşabilir. O zaman karma bir maske onu tamsayıya taşır: # ?/8 altındaki 2.97'nin 3···· okuması beklenir; çünkü 0.97 × 8 = 7.76, tam bir sekize yuvarlanır. İki aile arasında, paydanın okuyucu için ne anlama geldiğine göre seçin. Sabit paydalar, sabit adımlarla ifade edilen her şeye uyar: emperyal uzunluklar, matkap ölçüleri ya da sekizlik eski hisse fiyatları gibi. Kereste ölçüsü okuyan kimse 3/7 inç istemez. Yaklaşık kesir maskeleri ise, hiçbir belirli paydanın anlam taşımadığı oranlara ve ölçülen değerlere uyar

Excel'in kesir metnini Delphi'de FormattedText ile okumak

HotXLS'te döngüye kendiniz nadiren ihtiyaç duyarsınız. TXLSXCell.FormattedText, saklanan değeri hücrenin NumberFormat'ından, lxStandard.pas'ın TXLSFormater.GetDisplayValue'sını kullanarak geçirir; yakınsak seçimi, dolgu ve herhangi bir literal metin tek dizgide birlikte gelir. Aşağıdaki yordam hücrelere değer ve maske çiftleri atar ve her sonucu köşeli parantezler arasında basar; böylece sondaki boşluklar görünür kalır. Yeni bir TXLSXWorkbook çalışma sayfasız başlar; kod bu yüzden bir hücreye dokunmadan önce Sheets.Add çağırır. Sheets.Items ile Cells[Row, Col] ikisi de 1 tabanlıdır

uses
  SysUtils, lxHandleX;

type
  TFractionCase = record
    Mask: string;
    Value: Double;
  end;

const
  Cases: array[0..8] of TFractionCase = (
    (Mask: '# ??/??';        Value: 3.14159),
    (Mask: '# ??/??';        Value: 0.99),
    (Mask: '# ?/?';          Value: 12),
    (Mask: '# ?/?';          Value: 12.9999),
    (Mask: '# #/#';          Value: 12),
    (Mask: '?/?';            Value: 3.14159),
    (Mask: '0/8';            Value: 0.64),
    (Mask: '"x"# ?/?" y"';   Value: -12),
    (Mask: '# ?/?;(# ?/?)';  Value: -1.5));

procedure ShowFractionText;
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Cell: TXLSXCell;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Fractions');   // yeni bir çalışma kitabının henüz sayfası yoktur
    for I := Low(Cases) to High(Cases) do
    begin
      Cell := Sheet.Cells[I + 1, 1];          // satır ve sütunlar 1'den başlar
      Cell.Value := Cases[I].Value;
      Cell.NumberFormat := Cases[I].Mask;
      Writeln(Format('%-16s %9g  [%s]',
        [Cases[I].Mask, Cases[I].Value, Cell.FormattedText]));
    end;
  finally
    Book.Free;
  end;
end;

// Beklenen:
// # ??/??            3.14159  [3  1/7 ]
// # ??/??               0.99  [ 98/99]
// # ?/?                   12  [12    ]
// # ?/?              12.9999  [13    ]
// # #/#                   12  [12]
// ?/?                3.14159  [22/7]
// 0/8                   0.64  [5/8]
// "x"# ?/?" y"           -12  [-x12     y]
// # ?/?;(# ?/?)         -1.5  [(1 1/2)]

Son iki durum, kesri neyin çevrelediğini gösterir. Tırnaklı metin ve ters eğik çizgi kaçışları düz karakter olarak basılır ve bir tüm değerin dolgusu sonekin önüne düşer; dolayısıyla "x"# ?/?" y", -12'yi -x12·····y'ye çevirir. Eksi işareti, öndeki (# ?/?) gibi açık bir negatif bölüm işaretin kendisini yazmadıkça, önek dahil her şeyin önüne gider; kesirler, öbür her Excel sayı biçimiyle aynı bölüm ve eksi işareti kurallarını izler. Değer bir hücreden değil bir veri kümesinden geldiğinde çalışma kitabını atlayın: TXLSFormater'ın parametresiz bir kurucusu vardır ve GetDisplayValue(Value, NumberFormat), FormattedText'in sayısal değerler için yaptığı çağrıdır

uses
  SysUtils, lxStandard;

function FractionText(const AValue: Double; const AMask: WideString): WideString;
var
  Fmt: TXLSFormater;
begin
  Fmt := TXLSFormater.Create;
  try
    Result := Fmt.GetDisplayValue(AValue, AMask);
  finally
    Fmt.Free;
  end;
end;

// FractionText(0.5, '# ?/?')                Beklenen: ' 1/2'
// FractionText(1234.75, '#,##0 ?/?')        Beklenen: '1,234 3/4'
// Trim(FractionText(12, '# ?/?'))           Beklenen: '12'  (CSV dostu)

Delphi kodunuzun Excel dışına bastığı her kesir, yakınsak döngüsünden ya da doğrudan FormattedText'ten, hücrenin kendi Double'ıyla beslenerek gelmelidir. Böylece kâğıttaki 3 1/7 ile ızgaradaki 3 1/7 aynıdır ve HotXLS Delphi spreadsheet component ile o metni, döngüyü hiç yazmadan alırsınız