O precision as displayed do Excel arredonda cada número armazenado para as casas decimais que o number format dele mostra: a seção do formato que casa com o sinal do valor, duas casas extras por %, três a menos por vírgula de escala de milhares, arredondando metade para longe do zero. O HotXLS aplica a mesma regra nos seus dois motores Delphi quando TXLSXWorkbook.FullPrecision ou TXLSWorkbook.UseFullPrecision é False. Isso parece coisa de uma linha só até um cliente relatar que os totais da sua fatura exportada divergem do Excel por um centavo, ou que uma coluna de durações em [ss].00 colapsou para zero. Os dois aconteceram, e os dois remontam a errar uma dessas regras. Desde a v2.384.57 os dois motores compartilham uma implementação única cujos valores esperados foram medidos no Excel 16 com Workbook.PrecisionAsDisplayed ligado
O que o precision as displayed realmente muda num workbook?
O precision as displayed é uma única flag a nível de workbook que diz ao motor de cálculo para armazenar os números como eles aparentam, não como foram calculados. Na UI do Excel fica em Arquivo, Opções, Avançado, "Ao calcular esta pasta de trabalho", como "Definir precisão como exibido". No disco é um bit. Um arquivo BIFF8 o carrega no registro CalcPrecision ($000E, [MS-XLS] §2.4.35), cujo campo fFullPrec é 1 para precisão total normal e 0 quando a opção está ligada. Um pacote XLSX o carrega como o atributo fullPrecision do elemento calcPr no workbook.xml, definido no ECMA-376 Parte 1, em que o padrão é true e fullPrecision="0" liga o arredondamento
A flag não é uma preferência de exibição. Quando você marca a caixa, o Excel avisa que os dados perderão precisão permanentemente, e é pra valer: os valores são reescritos para a precisão exibida deles, e os dígitos cortados se foram. Desmarcar a caixa depois não traz os dígitos antigos de volta. Um 0.1234 exibido como 12.3% vira 0.123 para sempre
O HotXLS lê e grava a flag nos dois formatos e a expõe nos dois motores:
TXLSXWorkbook.FullPrecision: Booleanno motor XLSX, carregada de e salva emcalcPr/@fullPrecisionTXLSWorkbook.UseFullPrecision: Booleanno motor clássico (também emIXLSWorkbook), carregada de e salva no registro CalcPrecision- Ambas têm True como padrão, que é o modo seguro, não destrutivo, e o padrão do Excel
Onde o HotXLS aplica o arredondamento importa. O HotXLS arredonda no ponto em que calcula um valor: cada resultado de fórmula é arredondado para a precisão exibida dele antes de ser armazenado como valor em cache da célula, durante o Recalculate e durante a avaliação sob demanda. Constantes que você atribui pelo Value são armazenadas exatamente como dadas. Se a sua saída precisa reproduzir o que o Excel armazena depois que a caixa é marcada, arredonde essas constantes você mesmo antes de gravá-las, por exemplo com o helper mostrado mais adiante
Como o Excel decide quantas casas decimais manter?
O Excel deriva o número de casas mantidas da seção específica do formato que exibe o valor, não da string de formato como um todo. As regras abaixo foram medidas no Excel 16 e são o que a XlsApplyDisplayedPrecision no lxNumFormat implementa para os dois motores do HotXLS
- Escolha a seção pelo sinal. Um formato de duas seções usa a segunda seção para valores negativos. Um formato com três ou mais seções usa a segunda para negativos e a terceira para exatamente zero. Todo o resto usa a primeira seção
- Conte os placeholders decimais. Cada
0,#ou?depois do ponto decimal nessa seção adiciona uma casa mantida - Some duas por símbolo de porcentagem. O
0.0%mostra 0.1234 como 12.3%, então o valor armazenado é um centésimo do que você vê e mantém três casas, não uma - Subtraia três por vírgula de escala. Uma vírgula depois do último placeholder inteiro (
0,,0.0,,0,.0) divide a exibição por 1000. O0.0,mostra 12345.678 como 12.3, então o Excel mantém uma casa menos três, o que é uma contagem negativa: o valor é arredondado para as centenas e armazenado como 12300. Uma vírgula entre placeholders inteiros, como em#,##0, é agrupamento de dígitos comum e não muda nada - Deixe seções não numéricas em paz. Seções General, de data e hora (incluindo tempo decorrido
[h],[mm]e[ss]), científicas, de fração e de texto, e seções sem placeholder de dígito nenhum mantêm precisão total
Medido contra o Excel 16, estes são os valores que os dois motores do HotXLS agora armazenam para um resultado de fórmula em cada formato:
| Formato de número | Valor calculado | Valor armazenado | Regra aplicada |
|---|---|---|---|
0.0% | 0.1234 | 0.123 | Uma casa mais duas pelo símbolo de porcentagem |
0 | 2.5 | 3 | Metade para longe do zero, não para o par |
0 | -2.5 | -3 | Metade para longe do zero no lado negativo também |
0.00;(0.0) | -1.2345 | -1.2 | A seção negativa mostra uma casa |
0.00;(0.0) | 1.2345 | 1.23 | A seção positiva mostra duas casas |
#,##0.0 | 1234.5678 | 1234.6 | Vírgula de agrupamento, sem escala |
0.0, | 12345.678 | 12300 | Uma casa menos três: arredonda para centenas |
0.0%;(0.00%) | -0.0125 | -0.0125 | A seção negativa mantém duas mais duas casas |
0.00 | 1.005 | 1.01 | Tolerância para erro de representação binária |
0;-0;0.0 | 0.5 | 1 | Não é zero, então a seção positiva decide |
A última linha é uma armadilha boa. O valor 0.5 arredonda para um número inteiro, e a seção de zero nunca entra em cena, porque o Excel escolhe a seção pelo valor calculado antes de arredondar. Uma limitação honesta do lado do HotXLS: as seções são escolhidas só pelo sinal, então um formato cujas seções carregam condições entre colchetes customizadas como [>=1000] ainda é dividido pelo sinal. Confira esses formatos contra o Excel se eles importam para você
Por que 1.005 arredonda para 1.01 e não para 1.00?
O Excel arredonda 1.005 numa célula 0.00 para 1.01 embora o double mais próximo de 1.005 fique ligeiramente abaixo do ponto médio, e o HotXLS acompanha isso com uma tolerância de poucos ulps. O literal 1.005 não é representável em ponto flutuante binário. O double IEEE 754 mais próximo é 1.00499999999999989341858963598497211933135986328125, e multiplicar por 100 dá 100.49999999999999. Um Floor(x * 100 + 0.5) / 100 de livro portanto devolve 1.00, o que diverge do número que o usuário digitou, do que o Excel mostra e do que o Excel armazena
O Delphi adiciona o próprio twist. O System.Round arredonda empates para o par, então Round(2.5) é 2 e Round(3.5) é 4. Esse é o banker's rounding, um padrão sensato para estatística e a regra errada aqui: o Excel armazena 3 para 2.5 numa célula 0 e -3 para -2.5. A implementação do HotXLS trabalha no valor absoluto, soma 0.5 mais uma tolerância relativa de 2-51 vezes o valor escalado (poucos ulps nessa magnitude, nunca menos que dois ulps de 1.0), trunca, reescala e restaura o sinal. A função seguinte é uma ilustração autossuficiente desse princípio, não o código da biblioteca, e ela trata contagens de dígitos negativas para vírgulas de escala do mesmo jeito:
// Esboço de princípio: arredonda metade para longe do zero a ADigits casas,
// com tolerância de poucos ulps para que 1.005 chegue a 1.01.
// ADigits < 0 arredonda para dezenas, centenas, ... ("0.0," dá -2)
function RoundAsDisplayed(AValue: Double; ADigits: Integer): Double;
const
Tolerance = 4.440892098500626E-16; // 2^-51, dois ulps de 1.0
var
I: Integer;
Scale, Scaled, Eps: Double;
begin
Result := AValue;
if (ADigits < -15) or (ADigits > 14) then
Exit; // além da precisão de double: deixe o valor em paz
Scale := 1;
for I := 1 to Abs(ADigits) do
Scale := Scale * 10;
if ADigits >= 0 then
begin
if Abs(AValue) > 1E300 / Scale then
Exit; // escalar transbordaria
Scaled := Abs(AValue) * Scale;
end
else
Scaled := Abs(AValue) / Scale;
Eps := Scaled * Tolerance;
if Eps < Tolerance then
Eps := Tolerance;
Scaled := Int(Scaled + 0.5 + Eps); // metade para longe do zero, não Round()
if ADigits >= 0 then
Result := Scaled / Scale
else
Result := Scaled * Scale;
if AValue < 0 then
Result := -Result;
end;
// RoundAsDisplayed(1.005, 2) = 1.01 (via Floor: 1.00)
// RoundAsDisplayed(2.5, 0) = 3 (Round: 2)
// RoundAsDisplayed(-2.5, 0) = -3
// RoundAsDisplayed(0.1234, 3) = 0.123 ("0.0%": 1 + 2 dígitos)
// RoundAsDisplayed(12345.678, -2) = 12300 ("0.0,": 1 - 3 dígitos)
A tolerância é um trade-off deliberado. Um valor genuinamente dois ulps abaixo de um meio passo também arredonda para cima, mas a essa distância a diferença é indistinguível de erro de representação, e tratá-lo como meio passo é o que faz decimais digitados se comportar do jeito que os usuários esperam
O que dava errado antes da v2.384.57?
Antes da v2.384.57 o motor XLSX e o motor clássico tinham cada um o próprio código de precision-as-displayed, e cada um errava de um jeito diferente. Se você produz workbooks com a opção ligada, estes são os sintomas a procurar em arquivos gerados por builds antigas
Motor XLSX: só a primeira seção, sem porcentagem, banker's rounding
O caminho XLSX antigo pedia a contagem de decimais da string de formato como um todo, o que olhava só a primeira seção e ignorava %, depois arredondava com Round. Um 0.1234 em 0.0% era armazenado como 0.1, que é 10% em vez dos 12.3% na tela. Um 2.5 em 0 era armazenado como 2 em vez de 3. Valores negativos num formato como 0.00;(0.0) eram arredondados para as duas casas da seção positiva. Desde a v2.384.57 o motor XLSX chama a mesma rotina compartilhada do motor clássico, que também ganhou suporte a vírgula de escala nesse release
Motor clássico: TRUE virava -1
O motor clássico guardava o arredondamento dele com VarIsNumeric, e VarIsNumeric devolve True para um Variant varBoolean. Converter esse Variant com Double(V) produz -1, porque um Boolean True à moda COM é armazenado como -1. Uma fórmula como =A1>0 numa célula formatada 0.00 portanto saía da recalculação como o número -1. Desde a v2.384.57 resultados Boolean são excluídos antes de qualquer teste numérico, e um resultado lógico continua lógico nos dois motores
Formatos de tempo decorrido lidos como cores (v2.384.9)
O terceiro bug ficava no modelo de number format, não no arredondamento. O parser classificava todo token entre colchetes que não era uma condição como cor, então [h], [mm] e [ss] nunca marcavam a seção deles como data/hora. A exibição não era afetada, porque a formatação roda num caminho separado, mas o precision as displayed depende dessa flag para pular valores de tempo. Uma duração de cinco segundos é 5/86400 de um dia, cerca de 0.0000579, e um formato como [ss].00 parecia um número comum de duas casas, então com FullPrecision desligada a duração era arredondada para 0.00 dias. Desde a v2.384.9 uma sequência entre colchetes de uma única letra h, m ou s é parseada como token de tempo decorrido e a seção é tratada como data/hora. O mesmo release corrigiu a detecção de minutos no h:mm, em que os dois pontos entre os tokens escondiam a hora do parser
Ativar precision as displayed no HotXLS a partir do Delphi
Para obter valores armazenados equivalentes aos do Excel, defina a flag antes da recalculação que deve honrá-la, depois leia os resultados em cache ou salve. No motor XLSX, o FullPrecision é uma flag simples: mudá-lo não invalida resultados que um Recalculate anterior já armazenou, então defina-o logo após o Create ou Open e antes do primeiro Recalculate. O exemplo usa fórmulas porque é aí que o HotXLS aplica o arredondamento:
var
Wb: TXLSXWorkbook;
Sh: TXLSXWorksheet;
begin
Wb := TXLSXWorkbook.Create;
try
Sh := Wb.Sheets.Add('Totals');
Sh.Cells[1, 1].Value := 0.1234;
Sh.Cells[2, 1].Value := 2.5;
Sh.Cells[3, 1].Value := 12345.678;
Sh.Cells[1, 2].Formula := '=A1';
Sh.Cells[1, 2].NumberFormat := '0.0%'; // mostra 12.3%
Sh.Cells[2, 2].Formula := '=A2';
Sh.Cells[2, 2].NumberFormat := '0'; // mostra 3
Sh.Cells[3, 2].Formula := '=A3';
Sh.Cells[3, 2].NumberFormat := '0.0,'; // mostra 12.3 (milhares)
// Precisa ser definido antes do primeiro Recalculate no motor XLSX
Wb.FullPrecision := False;
Wb.Recalculate;
// Resultados em cache agora batem com o Excel 16: 0.123, 3 e 12300.
// As constantes na coluna A mantêm a precisão total.
Assert(Abs(Double(Sh.Cells[1, 2].Value) - 0.123) < 1E-12);
Assert(Double(Sh.Cells[2, 2].Value) = 3);
Assert(Double(Sh.Cells[3, 2].Value) = 12300);
Wb.SaveAs('totals.xlsx'); // grava <calcPr fullPrecision="0"/>
finally
Wb.Free;
end;
end;
O motor clássico se comporta igual, com uma conveniência: atribuir ao TXLSWorkbook.UseFullPrecision marca toda fórmula do grafo de dependências como dirty, então o próximo Recalculate reavalia o workbook inteiro sob a nova regra. Mudar um NumberFormat com a opção ligada também marca as células de fórmula afetadas como dirty, porque o formato agora decide o valor armazenado. Note que o Recalculate clássico devolve o número de células de fórmula que não conseguiu avaliar, então zero significa sucesso:
var
Wb: TXLSWorkbook;
Sh: TXLSWorksheet;
begin
Wb := TXLSWorkbook.Create;
try
Sh := Wb.Sheets.Add;
Sh.Range['A1', 'A1'].Value := -1.2345;
Sh.Range['B1', 'B1'].Formula := '=A1';
Sh.Range['B1', 'B1'].NumberFormat := '0.00;(0.0)';
Sh.Range['C1', 'C1'].Formula := '=A1<0';
Sh.Range['C1', 'C1'].NumberFormat := '0.00';
Wb.UseFullPrecision := False; // marca toda fórmula como dirty
if Wb.Recalculate <> 0 then
raise Exception.Create('Some formulas could not be evaluated');
// B1 = -1.2: a seção negativa "(0.0)" mostra uma casa decimal
// C1 continua Boolean True (builds antes da v2.384.57 gravavam -1)
Wb.SaveAs('report.xls'); // registro CalcPrecision com fFullPrec = 0
finally
Wb.Free;
end;
end;
Os dois motores também honram a flag que vem com um arquivo. Abra um workbook salvo com a opção ligada e o FullPrecision ou UseFullPrecision já está False, então um Recalculate depois de carregar arredonda exatamente como o Excel faria. Se você só precisa ler os números que o Excel já armazenou, pode pular a recalculação por completo, como descrito em ler valores de fórmula em cache sem recalculação. Para como números de série e formatos de data interagem com o modelo de formato que dirige a checagem de data/hora, veja números de série de data do Excel, o sistema 1904 e numFmt em Delphi
Quando ligar o precision as displayed, e quando não?
Ligue o precision as displayed só quando os números armazenados do workbook precisam igualar os números exibidos, e você aceita perder os dígitos extras para sempre. O caso legítimo clássico é uma planilha financeira em que colunas de valores arredondados precisam somar o total arredondado na tela, sem frações escondidas de centavo produzindo um total que erra por um no último dígito. Reproduzir um workbook existente de um cliente que já tem a opção marcada é o outro bom motivo, e o HotXLS preserva a flag no round-trip para que você não os retorne silenciosamente à precisão total
Evite-o na maioria das outras situações:
- Dados de engenharia e científicos. Arredondar uma medição porque alguém escolheu um formato de duas casas para um relatório destrói informação que nenhuma mudança de formato posterior restaura
- Porcentagens com formatos grosseiros. Um formato
0%mantém só duas casas decimais da razão armazenada, então 0.1234 vira 0.12, e toda fórmula a jusante que lê a célula trabalha com 0.12 - Exibições escaladas. Um formato
0,ou0.0,usado para mostrar milhares arredonda o valor armazenado para os milhares ou centenas, o que raramente é o que quem escolheu o formato pretendia - Templates compartilhados. A flag vale para o workbook inteiro. Qualquer um que depois adicionar uma planilha herda o comportamento, normalmente sem saber que ele está ligado
Se o que você realmente quer são resultados arredondados em algumas células específicas, escreva ROUND nessas fórmulas em vez disso. O ROUND é explícito, local à célula, visível para quem lê a fórmula, e avaliado pelo motor de fórmulas do HotXLS como qualquer outra função, sem efeitos colaterais no workbook inteiro
Referência rápida do precision as displayed
- Flag no arquivo: CalcPrecision
$000EcomfFullPrec= 0 em BIFF8 ([MS-XLS] §2.4.35),calcPr fullPrecision="0"em XLSX (ECMA-376 Parte 1) - Interruptores do HotXLS:
TXLSXWorkbook.FullPrecision := FalseeTXLSWorkbook.UseFullPrecision := False, ambos True por padrão - Seção: escolhida pelo sinal do valor calculado; terceira seção só para exatamente zero
- Dígitos: placeholders decimais, mais dois por
%, menos três por vírgula de escala; a contagem pode ser negativa - Arredondamento: metade para longe do zero com tolerância de poucos ulps, então 2.5 dá 3, -2.5 dá -3 e 1.005 dá 1.01
- Ignorados: General, data/hora e tempo decorrido, científico, fração, texto, Boolean e valores de erro
- Escopo no HotXLS: resultados de fórmula à medida que são calculados; constantes são armazenadas como atribuídas
- Motor XLSX: defina
FullPrecisionantes do primeiroRecalculate; o setter clássico remarca todas as fórmulas como dirty por conta própria - Versões: igualado ao Excel 16 nos dois motores desde a v2.384.57; formatos de tempo decorrido protegidos desde a v2.384.9
O HotXLS lê, grava e calcula workbooks XLS e XLSX nativamente do Delphi e do C++Builder, incluindo as opções de cálculo de workbook cobertas aqui. Detalhes, edições e o download da versão trial estão na página do componente de planilha HotXLS para Delphi