Articolo tecnico

Formati frazione di Excel e frazioni continue in HotXLS

Formattate 3.14159 con la maschera di frazione # ??/?? ed Excel mostra 3 1/7, benché 3 14/99 sia più di sette volte più vicino, così un export Delphi che cerca la frazione più vicina stampa un valore che l'utente non vede mai. La ragione è che Excel accetta solo i convergenti della frazione continua; in HotXLS, TXLSXCell.FormattedText restituisce lo stesso 3 1/7, spazi di riempimento compresi

Excel si ferma ai convergenti della frazione continua

Ogni maschera di frazione per approssimare dà ad Excel lo stesso compito: trovare un numeratore e un denominatore per la parte dopo l'intero, col denominatore limitato dal numero di slot per cifre dopo la barra. Excel ci arriva espandendo il valore come frazione continua e tenendo solo i convergenti, le frazioni che si ottengono troncando quell'espansione dopo ciascun termine. Per 0.14159 l'espansione parte con [0; 7, 15, 1, ...], che dà 1/7, poi 15/106, poi 16/113. L'intero 3 non cambia nulla. Con lui 3.14159 passa per gli stessi 22/7, 333/106 e 355/113 che si trovano in ogni tabella delle approssimazioni di pi greco. Dato che 106 sborda già un denominatore a due cifre, Excel tiene 1/7. Il più vicino 14/99 è un semiconvergente, una delle frazioni intermedie che siedono tra due convergenti confinanti. Excel non visita mai quelle. Non serve codice su misura per inciamparci: i tipi Up to one digit e Up to two digits della finestra di dialogo Format Cells sono gli ID di formato predefiniti 12 e 13, # ?/? e # ??/??, nell'elenco dei formati numerici predefiniti di ECMA-376 Part 1 §18.8.30

Retta dei numeri dei convergenti della frazione continua di 3.14159: 22/7, 333/106 e 355/113 su assi che zoomano, con i tetti dei denominatori della maschera di frazione che decidono quale convergente Excel tiene
Ogni troncamento dell'espansione atterra più vicino a 3.14159, e il numero di cifre dopo la barra decide quale convergente sopravvive al tetto

Il divario non riguarda solo input alla pi greco. La migliore approssimazione, tipicamente un percorso Stern-Brocot o una scansione su ogni denominatore ammesso, chiede quale frazione sia la più vicina, mentre il ciclo dei convergenti chiede quale frazione l'espansione raggiunga per prima mentre il denominatore ci sta ancora. Su input ordinari a due decimali le due regole divergono abbastanza spesso da contare, e 0.27 nel codice qui sotto è uno di questi valori. Se il vostro codice scrive frazioni in una fattura PDF, un report HTML o un CSV che qualcuno metterà a confronto col foglio di lavoro, copiate lì la regola di Excel, anche se è la meno precisa. Un lettore che vede 3 14/99 sulla carta e 3 1/7 in Excel non conclude che il vostro export sia più esatto. Conclude che uno dei due è rotto. Non sarà Excel

Retta dei numeri in zoom per 0.27 che mostra il convergente 10/37 che Excel visualizza contro il più vicino 17/63 da una scansione a frazione più vicina, con campioni in disaccordo e in accordo
Su valori ordinari a due decimali il ciclo dei convergenti e la scansione a frazione più vicina divergono abbastanza spesso da contare, così gli export dovrebbero copiare la regola di Excel

Il ciclo in sé è breve. Contate i segnaposto per cifre dopo la barra, che siano ?, # o 0, e il tetto è 9, 99 o 999 per uno, due o tre di loro. Una maschera mista come # ??/?? stacca l'intero ed espande solo la parte frazionaria; una maschera impropria come ?/? espande l'intero valore, ecco perché 3.14159 sotto ?/? esce come 22/7. Ogni passo prende la parte intera A del valore corrente B e la ripiega nel convergente successivo. Il ciclo si ferma appena il resto B − A scende sotto 5e-8, così una frazione semplice più il rumore in floating point atterra comunque su quella frazione. Quando un passo spinge il denominatore oltre il tetto, Excel ripiega sul convergente precedente, e quel ripiego può atterrare su 1/1, che una maschera mista porta nell'intero, così 12.9999 sotto # ?/? mostra 13. Qui sotto il ciclo è scritto in Delphi puro accanto a una ricerca a forza bruta della frazione più vicina, così vedete dove le due divergono sui vostri numeri

uses
  SysUtils;

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

// La regola dei convergenti dietro le maschere # ?/?, # ??/?? e # ???/??? di Excel
// X non è negativo: la parte frazionaria per una maschera mista, il valore intero
// per una maschera impropria. MaxDen vale 9, 99 o 999.
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;      // convergente k-2
  P1 := 1; Q1 := 0;      // convergente 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;               // il resto conta come zero
    B := 1 / R;
    P2 := P1; Q2 := Q1;
    P1 := P;  Q1 := Q;
  end;
  if Q > MaxDen then       // tetto superato: si ripiega di un convergente
    if Q1 > MaxDen then
    begin
      P := P2; Q := Q2;
    end
    else
    begin
      P := P1; Q := Q1;
    end;
  Result.Num := P;
  Result.Den := Q;
end;

// Cosa restituisce una routine di migliore approssimazione: la N/D più vicina con D <= MaxDen
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  // severo: la forma ridotta vince i pareggi
    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;

// Atteso:
// 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

La riga 0.27 è nell'elenco perché anche un input ordinario a due decimali diverge, 10/37 contro 17/63, senza nulla di esotico nel valore. Quanto spesso le regole divergono dipende dai vostri dati: le frazioni semplici vere come 0.5 o 0.375 escono uguali in entrambi i modi, le grandezze misurate e i rapporti calcolati molto meno affidabilmente. Trunc fa le veci di Floor perché l'input non è mai negativo. Excel tratta il segno fuori dalla frazione. Anche il vostro codice dovrebbe. E l'argomento deve essere il Double che la cella conserva davvero, per un motivo che la sezione successiva rende concreto

Cosa succede a 0.99 in una frazione a due cifre?

Excel mostra 0.99 sotto # ??/?? come ·98/99 (ogni · in questo articolo sta per un carattere spazio). La causa è il floating point binario. Il letterale 0.99 non ha un Double esatto, e il valore conservato siede un pelo più in basso. Così 1/(1/0.99 − 1) restituisce circa 98.9999999999994 invece di 99, il secondo termine dell'espansione diventa 98, e il convergente 98/99 ci sta sotto il tetto. Fate la stessa espansione con decimali esatti e ottenete [0; 1, 99]: quel 99 spinge il denominatore a 100, il ciclo ripiega su 1/1, e la cella leggerebbe 1······. Qui, essere più precisi di Excel è il bug. Un generatore di report che tiene gli importi come Currency o TBcd, o riparsa la stringa decimale dal database e la espande con aritmetica esatta, stampa un bel 1 intero dove Excel stampa 98/99. Date al formattatore il Double conservato. Tenete ogni intermedio in floating point. La frazione resta una questione di visualizzazione, anche: le formule vedono ancora 0.99, e HotXLS mantiene le sezioni di frazione a piena precisione persino in un foglio di lavoro con la precision as displayed attiva

Diagramma a due strade per 0.99 sotto la maschera di frazione di Excel # ??/??: il Double conservato si espande in 98/99 mentre i decimali esatti sbordano il tetto e visualizzano un 1 intero
Il Double conservato atterra su 98/99 mentre l'aritmetica decimale esatta ripiega su 1/1 — essere più precisi di Excel è qui il bug

Come Excel riempie e allinea l'area della frazione

Excel dispone le frazioni come viene disposta una colonna di decimali: il numeratore è allineato a destra nei suoi slot, il denominatore a sinistra, così la barra atterra nella stessa posizione su ogni riga. Ogni slot inutilizzato viene riempito secondo il suo segnaposto. Un ? lascia uno spazio, uno 0 scrive uno zero, un # non scrive niente. Ecco perché 3.14159 sotto # ??/?? legge 3··1/7·: uno spazio è il separatore della maschera, uno riempie lo slot vuoto del numeratore davanti all'1, e uno riempie lo slot vuoto del denominatore dopo il 7. La tabella elenca il testo che Excel visualizza per un insieme di input che copre le regole di layout

Codice di formatoValoreExcel mostra
# ?/?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·

I numeri interi sono dove i formattatori scritti a mano deviano di più da Excel. Sotto una maschera mista il cui numeratore usa ?, Excel conserva le cifre dell'intero, zero compreso, e scambia separatore, numeratore, barra e denominatore per uno spazio ciascuno, così 12 sotto # ?/? diventa 12···· e sotto # ??/100 è seguito da sette spazi. Cambiate il segnaposto del numeratore in # e il riempimento sparisce. Un intero zero funziona al contrario: lo slot # non stampa niente per 0.5 e il separatore sopravvive solo come spazio, mentre 0 ?/? forza la cifra, e le maschere improprie non buttano mai via la frazione

Cosa fare con quel riempimento dipende da dove va il testo. Tenetelo per i report di testo a spaziatura fissa e per le celle di tabelle PDF allineate a destra. Allineare le barre è tutto il punto, lì. Tagliatelo per CSV e JSON, dove uno spazio finale trasforma 12 in una stringa che non si lascia più interpretare come numero. Anche HTML richiede una decisione, perché il browser collassa le sequenze di spazi a meno che la cella usi white-space: pre; impostatelo o tagliate, ma evitate un riempimento mezzo collassato che disallinea alcune righe e altre no

Denominatori fissi come # ?/8

Un denominatore scritto come cifre da 1 a 9 è un valore fisso invece che un tetto. L'aritmetica cambia alla radice: Excel moltiplica il valore per il denominatore, arrotonda al numeratore intero più vicino e lascia la frazione non ridotta. Sotto 0/8, 0.375 mostra 3/8 e 0.64 mostra 5/8, perché 0.64 × 8 fa 5.12. Il tipo di serie As sixteenths mostra 0.5 come 8/16, che è ciò che si attende un meccanico che legge un disegno tecnico e ciò che un formattatore che riduce ogni risultato trasformerebbe in 1/2. Un numeratore arrotondato può raggiungere il denominatore. Allora una maschera mista lo porta nell'intero: 2.97 sotto # ?/8 deve leggere 3····, visto che 0.97 × 8 = 7.76 si arrotonda a un otto pieno. Scegliete tra le due famiglie in base a cosa il denominatore significa per il lettore. I denominatori fissi stanno bene a tutto ciò che si cita a passi fissi, come lunghezze imperiali, punte di trapano o vecchi prezzi di azioni in ottavi. Nessuno che legge dimensioni di legname vuole 3/7 di pollice. Le maschere approssimanti stanno bene a rapporti e grandezze misurate dove nessun denominatore particolare porta significato

Leggere il testo frazione di Excel in Delphi con FormattedText

In HotXLS di rado vi serve il ciclo per conto vostro. TXLSXCell.FormattedText renderizza il valore conservato attraverso il NumberFormat della cella usando TXLSFormater.GetDisplayValue da lxStandard.pas, così la scelta del convergente, il riempimento e ogni testo letterale arrivano insieme in una sola stringa. La procedura sotto assegna coppie di valore e maschera alle celle e stampa ogni risultato tra parentesi quadre, così gli spazi finali restano visibili. Un nuovo TXLSXWorkbook parte senza fogli di lavoro, ecco perché il codice chiama Sheets.Add prima di toccare una cella; Sheets.Items e Cells[Row, Col] sono entrambi a base 1

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');   // un nuovo workbook non ha ancora fogli
    for I := Low(Cases) to High(Cases) do
    begin
      Cell := Sheet.Cells[I + 1, 1];          // righe e colonne partono da 1
      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;

// Atteso:
// # ??/??            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)]

Gli ultimi due casi mostrano cosa circonda la frazione. Il testo tra virgolette e le escape con backslash stampano come caratteri semplici, e il riempimento di un valore intero atterra prima del suffisso, così "x"# ?/?" y" trasforma -12 in -x12·····y. Il segno meno va davanti a tutto, prefisso compreso, a meno che una sezione negativa esplicita come (# ?/?) non scriva il segno per conto suo; le frazioni seguono le stesse regole di sezioni e di segno meno di ogni altro formato numerico di Excel. Quando il valore arriva da un dataset anziché da una cella, saltate il workbook: TXLSFormater ha un costruttore senza parametri, e GetDisplayValue(Value, NumberFormat) è la chiamata che FormattedText fa per i valori numerici

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, # ?/?)                Atteso: uno spazio poi 1/2
// FractionText(1234.75, #,##0 ?/?)        Atteso: 1,234 3/4
// Trim(FractionText(12, # ?/?))           Atteso: 12 senza spazi, adatto a CSV

Ogni frazione che il vostro codice Delphi stampa fuori da Excel dovrebbe venire dal ciclo dei convergenti o direttamente da FormattedText, nutrita col Double della cella stessa. Così il 3 1/7 sulla carta è il 3 1/7 nella griglia, e con lo strumento fogli di calcolo HotXLS per Delphi quel testo lo avete senza scrivere il ciclo affatto