Article technique

Doubles XLSX en Delphi : round-trip sans perte à 17 chiffres

HotXLS écrit les valeurs de cellules numériques ordinaires et les caches de formules en XLSX avec 17 chiffres significatifs, et en lecture il corrige lanalyse de la RTL Delphi par un contrôle exact borné décimal vers binary64, si bien quun Double que vous stockez dans une cellule revient bit pour bit après une sauvegarde et une réouverture. Quinze chiffres, le nombre quExcel montre à lécran, ne suffisent pas à identifier une valeur IEEE 754 binary64, et lécart apparaît exactement là où vivent les colonnes de finance et didentifiants : les entiers au-delà de 1015

Le bug qui a forcé le changement avait lair inoffensif. Une formule de feuille INT(-1E15-0.1) sévalue en -1000000000000001, un double parfaitement représentable. HotXLS la calculé correctement, la mis en cache, sauvegardé le fichier, et le cache sur disque lisait -1E15. Excel naurait montré la valeur dorigine quaprès un recalcul, et tout consommateur qui se fie aux valeurs en cache, ce qui est la plupart, recevait silencieusement un nombre décalé de un. Rien ne lançait, rien navertissait, et le fichier validait bien

Pourquoi 15 chiffres significatifs ne suffisent-ils pas pour un double ?

Quinze chiffres décimaux sont garantis pour survivre à un voyage vers binary64 et retour, mais le sens inverse en exige 17. IEEE 754 binary64 a une mantisse de 53 bits, ce qui fait entre 15 et 17 chiffres décimaux selon où la valeur se situe. Toute chaîne décimale dau plus 15 chiffres significatifs se convertit en double et se réimprime inchangée. Un double, en revanche, a besoin de jusquà 17 chiffres avant que son orthographe décimale lidentifie de façon unique, et dans la plage au-dessus de 253 les doubles adjacents sont espacés de 2, tandis quentre 1015 et 253 ils sont espacés de 0.125 à 1

Le FloatToStr de Delphi formate avec 15 chiffres significatifs, soit la même précision quExcel emploie pour laffichage. Cette concordance est précisément pourquoi le défaut se cache si bien : ouvrez le fichier dans Excel et la cellule a exactement lair quelle avait dans votre programme Delphi, parce quExcel aussi ne montre que 15 chiffres. La différence ne devient visible que quand vous comparez le double stocké, soustrayez deux valeurs voisines, ou nourrissez le cache dans quelque chose qui fait de larithmétique. La valeur -1000000000000001 a 16 chiffres significatifs, si bien quarrondir à 15 produit -1000000000000000 et lunité de queue est perdue pour de bon

HotXLS écrit les valeurs de cellules XLSX et les caches de formules avec 17 chiffres significatifs parce que le double représentable -1000000000000001 mis en cache depuis INT(-1E15-0.1) perdait son unité de queue quand stocké avec 15 chiffres en -1E15
Une mantisse binary64 de 53 bits couvre 15 à 17 chiffres décimaux, si bien quune valeur telle que -1000000000000001 exige les 17 chiffres significatifs complets avant que son orthographe décimale soit unique

ECMA-376 Part 1 §18.3.1.96 définit lélément <v> qui porte une valeur de cellule comme texte, et rien dans le schéma ne dicte combien de chiffres un rédacteur doit émettre pour une cellule de nombre. La précision relève entièrement de la responsabilité du rédacteur, voilà pourquoi deux producteurs peuvent tous deux écrire du XLSX valide et diverger quand même sur quel nombre est dans la cellule

Écrire 17 chiffres significatifs depuis Delphi

HotXLS emploie maintenant un invariant pour les cellules numériques : FloatToStrF avec ffGeneral, précision 17, et paramètres de format invariants. La branche dates du rédacteur de cellules employait déjà exactement cette projection, si bien que les nombres ordinaires lont simplement rejointe. De lextérieur rien ne change dans lAPI ; vous affectez un Double ou une formule, sauvegardez, et le token qui atterrit dans <v> est assez précis pour reproduire le double

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');
    // Les valeurs et le cache de formule sont écrits avec 17 chiffres significatifs
    if Book.SaveAs('ledger.xlsx') <> lxOk then
      raise Exception.Create('Save failed');
  finally
    Book.Free;
  end;
end;

Dix-sept chiffres est un plafond, pas une règle de remplissage. ffGeneral laisse tomber les zéros de queue, si bien que 0.5 sécrit toujours 0.5 et les petits entiers restent courts ; seules les valeurs qui ont réellement besoin des chiffres supplémentaires les obtiennent. Le coût est quelques octets sur les cellules qui étaient auparavant tronquées, soit la correction dexactitude la moins chère que vous publierez jamais

Pourquoi écrire 17 chiffres perd-il encore des valeurs en lecture ?

Écrire assez de chiffres naide que si le lecteur les convertit vers le double le plus proche, et le Val de la RTL ne fait pas toujours cela. Pendant la vérification, la RTL sur la machine de test lisait aussi bien 9007199254740990.7 que lentier simple 9007199254740991 dans la valeur binary64 adjacente, fausse. Les deux chaînes devraient atterrir sur 253-1 ; lune est une orthographe à 17 chiffres que HotXLS peut produire, lautre est comment Excel écrit la même valeur. Un rédacteur corrigé en isolement aurait livré un fichier que HotXLS lui-même ne pouvait pas relire correctement

HotXLS garde donc Val comme gardien et corrige sa réponse. Après que la grammaire numérique existante accepte un token non date, un analyseur exact construit le décimal comme une fraction en grand entier et le compare aux points milieux binaires de part et dautre du candidat de la RTL. Si le décimal tombe sous le point milieu inférieur, le candidat descend dun ulp ; au-dessus du point milieu supérieur, il monte ; sur une égalité exacte la mantisse paire lemporte, ce qui est la règle IEEE 754 darrondi au plus proche pair. Le candidat a droit à au plus huit ajustements de voisin, ce qui suffit largement parce que Val se trompe dune unité, pas dun mile

La correction est délibérément bornée. Les tokens de plus de 48 caractères, avec plus de 17 chiffres significatifs, ou avec une magnitude dexposant au-dessus de 324 la sautent et gardent le résultat de lanalyseur historique, et tout échec interne retombe sur la même chose. Les grands entiers emploient 48 membres de 32 bits chacun, assez pour le numérateur et le dénominateur les plus larges à lintérieur de ces bornes. Un détail de portabilité compte ici : les membres sont déclarés explicitement UInt32. LongWord semble le choix évident, mais il fait 8 octets sur la cible Linux64 de Delphi, et un type de membre qui double silencieusement de largeur casse chaque retenue et décalage de larithmétique

HotXLS corrige le résultat Val de la RTL Delphi pour les nombres XLSX à 17 chiffres en comparant le décimal comme une fraction en grand entier aux points milieux binaires et en se déplaçant dun ulp vers larrondi IEEE 754 au plus proche pair
Le 9007199254740990.7 comme lentier simple quExcel écrit doivent atterrir sur 2^53 - 1, si bien que le lecteur exact ajuste au plus huit voisins et garde lanalyse historique au-delà de ses bornes de 48 caractères et exposant 324

Vérifier le round-trip bit pour bit

Comparer les doubles avec = va pour ce test, parce que le but est légalité exacte, mais une comparaison de motifs de bits rend le rapport déchec plus utile : une dérive dun ulp apparaît comme une différence de 1 dans la valeur brute 64 bits plutôt que comme deux nombres qui dimpriment identiquement

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;

La ligne 3 est celle qui intéresse. Elle lit le résultat en cache de INT(-1E15-0.1) sans recalculer, si bien quelle prouve que le cache lui-même est sans perte plutôt que le moteur de formules étant capable de recalculer la valeur. Si vous dépendez de résultats en cache dans un pipeline, le guide séparé sur la lecture de valeurs de formules en cache sans recalcul couvre les états du cache et pourquoi un cache manquant nest jamais inventé

Quelles autres parties XLSX portent des caches numériques ?

Corriger le rédacteur de cellules a fermé un chemin sur plusieurs, et chaque point décriture numérique restant a dû être vérifié pour son compte. La première version couvrait les valeurs de feuilles et les caches de formules, y compris les caches de valeurs de sortie Pivot. Les caches sources Pivot et les caches Chart passaient encore par lentonnoir à 15 chiffres et ont été fermés en deux versions de suivi plutôt que par supposition

  • Cache source Pivot : les valeurs sharedItems et les deux rédacteurs cacheRecords en ligne et en flux emploient maintenant le même invariant à 17 chiffres, et le chemin de lecture de pivot applique la même correction exacte de point milieu. Les attributs descriptifs tels que refreshedDate gardent leur orthographe dorigine
  • Caches Chart : lunique point décriture qui construit les valeurs de points en cache est passé à 17 chiffres, et les six points de lecture, couvrant valeurs et catégories de trendlines, validation des barres derreur et deux chemins de rendu, passent tous par un helper danalyse exacte
  • Tout le reste de numérique, tel que largeurs de colonnes et bornes daxes, passe encore par le formateur défensif qui vide les valeurs non finies et subnormales vers 0, parce quune partie sérialisée ne doit jamais contenir un float non analysable

Le contrôle dinteropérabilité qui comptait le plus était avec Excel lui-même. Excel écrit 253-1 comme lentier 9007199254740991 ; HotXLS peut écrire lorthographe à 17 chiffres équivalente 9007199254740990.7. Les deux tokens sanalysent vers le même double des deux côtés, si bien quun fichier rédigé dans Excel, rouvert et resauvegardé par HotXLS, puis rouvert dans Excel, garde les mêmes valeurs dans son cache Pivot. La reconnaissance mutuelle, pas lorthographe identique, est la propriété à tester

Excel écrit 2^53 - 1 comme lentier 9007199254740991 tandis que HotXLS peut écrire lorthographe à 17 chiffres 9007199254740990.7, et les deux tokens XLSX sanalysent vers le même double en Delphi et Excel
Un classeur qui voyage dExcel au travers dune resauvegarde HotXLS et retour garde ses valeurs de cache Pivot bit pour bit, parce que les deux lecteurs acceptent lune ou lautre orthographe de la valeur

Limites et cas limites à connaître

Sans perte signifie ici que la valeur binary64 est préservée, pas que chaque valeur est représentable. Les résultats non finis se sauvegardent toujours comme erreurs #NUM! et les subnormales se sauvegardent toujours comme zéro, exactement comme avant. Lafichage est aussi une préoccupation séparée : Excel continuera de montrer 15 chiffres significatifs à lécran, si bien quun utilisateur tapant 9007199254740993 dans Excel obtient quand même une valeur stockée différente, et aucun rédacteur ne peut corriger cela. Si votre classeur emploie Set precision as displayed, les valeurs sont délibérément arrondies à leur format de nombre, ce qui est lopposé de ce dont cet article parle ; les règles de ce mode sont couvertes dans comment HotXLS applique larrondi precision as displayed dExcel

Le lecteur exact ne remplace pas non plus Val ; il corrige seulement les valeurs que Val a déjà acceptées. Un token que la RTL rejette reste rejeté, et un token hors des bornes de 48 caractères, 17 chiffres et exposant 324 garde ce que lanalyseur historique a produit. Pour les fichiers écrits par HotXLS ou Excel cela ne compte jamais, parce que ni lun ni lautre ne produit de tels tokens pour des valeurs de cellules, mais une partie XML artisanale avec 40 chiffres significatifs se lit avec la précision de la RTL, pas avec la correction exacte

FAQ

HotXLS change-t-il le nombre quExcel affiche pour une cellule ?

Non. HotXLS ne change que combien de chiffres entrent dans le token <v> stocké. Excel affiche toujours 15 chiffres significatifs, si bien que les cellules ont la même apparence ; la différence est que le double sous-jacent correspond maintenant à ce que votre code Delphi a affecté

Pourquoi ne pas écrire simplement 16 chiffres au lieu de 17 ?

Seize chiffres ne suffisent pas pour toute valeur binary64. Certains doubles ont besoin de 17 chiffres significatifs avant que leur orthographe décimale soit unique, si bien quun rédacteur à 16 chiffres ferait encore correspondre deux doubles distincts au même texte dans de rares cas, soit exactement la classe de dérive silencieuse que le changement retire

Les fichiers écrits avant la correction doivent-ils être régénérés ?

Seulement sils contiennent des valeurs qui exigeaient plus de 15 chiffres. Ces tokens ont déjà perdu leurs derniers chiffres sur disque et les lire plus précisément ne peut pas restaurer ce qui na jamais été écrit. Resauvegarder depuis les données dorigine, ou recalculer les formules et sauvegarder à nouveau, produit un fichier exact

La sortie numérique à 17 chiffres, la correction exacte de lecture binary64 et la prise en charge assortie des caches Pivot et Chart font partie du HotXLS Delphi Spreadsheet Component pour Delphi, C++Builder et Lazarus, qui lit et écrit du XLSX sans Excel ni automatisation OLE installés