Teknisk artikel

Sammenligningskæder, tomme celler og SUMIF i HotXLS Delphi

HotXLS Delphi Component evaluerer =1<2<3 som FALSE, samme svar som Excel 16 giver, for siden v2.384.3 folder dens formelparser sammenligningsoperatorer fra venstre mod højre: 1<2 bliver TRUE, og TRUE<3 er FALSE, fordi en boolean rangerer over ethvert tal. Samme release gør en tom operand lig både 0 og "" og lader SUMIF strække et encelle-sumområde til kriterieområdets form. Hvert af punkterne ligner trivia, til en arbejdsbog beregnet i Delphi er uenig med samme arbejdsbog åbnet i Excel

Uenigheden starter normalt med en formel, et menneske har skrevet ud fra intuition. Nogen taster =0<B2<100 for at tjekke, at en mængde er inden for intervallet, Excel svarer stille FALSE på hver række, og arket skibes med den bug indbagt. En beregningsmotor må ikke rette brugerens hensigt; dens job er at levere den værdi, Excel ville levere, så det cachete resultat, HotXLS skriver i filen, matcher, hvad Excel viser efter en rekalkulation. Før v2.384.3 svarede HotXLS TRUE på det intervaltjek på hver række, forkert i den modsatte retning, og en rapport genereret på en server ville modsiges af samme rapport åbnet på en desktop

Hvorfor returnerer =1<2<3 FALSE i Excel?

Excel returnerer FALSE, fordi den læser en kæde af sammenligninger som (1<2)<3, og den indre TRUE så taber type-rangeringskonkurrencen mod tallet 3. Den gamle HotXLS-parser læste samme tekst som 1<(2<3): TXLSSyntax.Parse_expr i lxFormula.pas parsede én operand, så en sammenligningstoken og rekursede ind i Parse_expr for højresiden, hvilket gør operatoren right-associative. Det giver 1<TRUE, og et tal er under en boolean, så resultatet var TRUE. Fejlen er symmetrisk: =3>2>1 er TRUE i Excel og var FALSE i HotXLS, og =1=1=TRUE er TRUE i Excel og var FALSE før fixen. Regressionstesten CalculateFormula_ComparisonChainsFoldLeftToRight fastlåser syv sådanne formler mod de værdier, Excel 16 returnerer, og kører hver eneste gennem begge motorarkitekturer, classic TXLSWorkbook og XLSX-native TXLSXWorkbook, via Calculate-metoden beskrevet i the HotXLS formula engine overview

const
  Formulas: array [0..6] of string = ('=1<2<3', '=3>2>1', '=(1<2)<3',
    '=1<(2<3)', '=1=1=TRUE', '=3>2>1=TRUE', '=1<2<3=FALSE');
  // Hvad Excel 16 returnerer:  FALSE, TRUE, FALSE, TRUE, TRUE, TRUE, TRUE
var
  Classic: IXLSWorkbook;
  Xlsx: TXLSXWorkbook;
  i: Integer;
begin
  Classic := TXLSWorkbook.Create;
  Xlsx := TXLSXWorkbook.Create;
  try
    // TXLSXWorkbook.Calculate evaluerer mod det aktive ark og
    // returnerer Null, når arbejdsbogen slet ikke har et ark
    Xlsx.Sheets.Add('Data');
    for i := 0 to High(Formulas) do
      Writeln(Formulas[i], '  classic=', VarToStr(Classic.Calculate(Formulas[i])),
        '  xlsx=', VarToStr(Xlsx.Calculate(Formulas[i])));
  finally
    Xlsx.Free;
  end;
end;
HotXLS parse-træer for =1<2<3, hvor den gamle right-associative Parse_expr evaluerede 1<(2<3) som TRUE, mens den venstre-til-højre-folder siden v2.384.3 evaluerer (1<2)<3 som FALSE, afgjort af CompareVariants-rangeringen, der sætter ethvert tal under tekst og tekst under boolean, reglen i lxCalc.pas
Begge motorer folder nu sammenligningskæder fra venstre mod højre og fastlåser syv formler mod Excel 16 — en boolean rangerer over ethvert tal, så TRUE, der taber til 3, er netop det, der gør den kædede intervaltjek FALSE

Fixet gør Parse_expr til en løkke af samme form, som Parse_expr1 allerede bruger til +, - og &. Den parser den første operand med Parse_expr1, og mens næste token er en af =, <>, <, >, <= eller >=, opretter den en sammenligningsnode, hæfter det akkumulerede venstre resultat som første barn, parser næste operand med Parse_expr1 frem for Parse_expr og gør den nye node til venstre resultat i næste runde. To detaljer var lette at fikse forkert, da rekursionen blev lavet om til iteration, og begge står i vedligeholdernes noter: den akkumulerede node skal overdrages i rækkefølgen (lChild := Item; Item := nil), og fejlvejen skal Exit efter at have frigjort den halvbyggede node frem for at falde ud af løkken og returnere en hængende graf

Hvordan rangerer HotXLS tal, tekst og booleans i en sammenligning?

HotXLS rangerer blandede typer, som Excel gør: ethvert tal er mindre end enhver tekstværdi, og enhver tekstværdi er mindre end enhver boolean. TXLSCalculator.CompareVariants i lxCalc.pas klassificerer begge operander med GetRetValueType i enumereringen TXLSRetValueType = (xlVariantValue, xlNumberValue, xlStringValue, xlBooleanValue), og når de to klasser adskiller sig, sammenligner den blot deres ordinaler, så deklarationsrækkefølgen af den enum er tværtype-reglen. Inden for én klasse er sammenligningen den naturlige, med ét Excel-specifikt twist for tekst: begge strenge passerer lxUpperCase først, så ="abc"="ABC" er TRUE. Denne rangering er grunden til, at kæderesultatet ikke kan ræsonneres ud uden den. TRUE<3 er ikke en coercion af TRUE til 1, det er en boolean sammenlignet med et tal, og boolean vinder. Datoer er serienumre for motoren (varDate klassificeres som xlNumberValue), så en dato er altid under enhver tekst, inklusive tekst, der tilfældigvis ligner en dato

Hvad er en tom celce lig i en sammenligning?

En tom celle brugt som sammenligningsoperand er lig 0, når modparten er et tal, lig "", når modparten er tekst, og siden v2.384.53 lig FALSE, når modparten er en logisk værdi, så med A1 tom er =A1=0, =A1="" og =A1=FALSE alle TRUE. TXLSCalculator.CompareVarValues, som betjener alle seks sammenligningsoperatorer, substituerer den tomme, før den kalder CompareVariants: er præcis én operand Null, bliver den WideString(''), når partneren er en streng, False, når partneren er en boolean, og 0 ellers. To tomme celler sammenlignes stadig lig hinanden uden substitution. Aritmetikvejen havde altid gjort en tom celle til 0, hvilket er derfor, =A1+1 gav 1, men CompareVariants holdt Null som sin egen laveste rang under ethvert tal, og sammenligningsoperatorerne brugte den rang direkte

var
  Wb: IXLSWorkbook;
  Sh: TXLSWorksheet;
begin
  Wb := TXLSWorkbook.Create;
  Sh := Wb.Sheets.Add;
  Sh.Range['B1', 'B1'].Value := 5;      // A1 efterlades bevidst tom

  Writeln(VarToStr(Wb.Calculate('=A1=0')));    // True
  Writeln(VarToStr(Wb.Calculate('=A1=""')));   // True
  Writeln(VarToStr(Wb.Calculate('=A1<B1')));   // True: den tomme sammenlignes som 0
  Writeln(VarToStr(Wb.Calculate('=A1<0')));    // False; True før v2.384.3
end;
HotXLS substitution af tomme operander i CompareVarValues, hvor en tom A1 sammenlignes lig 0 og tom tekst, mens den gamle Null-rangering gjorde =A1<0 TRUE for hver tom balance, og siden v2.384.53 sammenlignes tom mod boolean som FALSE, så =A1=FALSE er TRUE som i Excel
Substitutionen matcher modpartens type, 0, den tomme streng eller siden v2.384.53 FALSE — den IF, der mærkede hver tom balance som overtrukket, var den gamle Null-rangering, ikke dine data

Den sidste linje er den, der gjorde ondt i praksis. Under den gamle rang var en tom celle mindre end ethvert tal, negative inkluderet, så =IF(A1<0,"overdrawn","ok") mærkede hver tom balancecelle som overtrukket, og =A1=0 var FALSE for en celle, enhver bruger ville beskrive som nul. Én grænse stod tilbage efter v2.384.3: substitutionen valgte kun mellem 0 og den tomme streng, så en tom celle sammenlignet med en boolean blev 0, som rangerer under både TRUE og FALSE, og =A1=FALSE på en tom A1 evaluerede til FALSE. Siden HotXLS 2.384.53 behandles en tom celle sammenlignet med en logisk værdi som FALSE i både XLS- og XLSX-motoren, som Excel gør: med A1 tom returnerer =A1=FALSE og =A1<TRUE TRUE, og =A1=TRUE returnerer FALSE. Det betyder også, at sammenligningen ikke kan skelne tom fra FALSE, hverken i Excel eller HotXLS; skal et ark have den skelnen, så test med ISBLANK eller =A1=""

Hvorfor returnerede SUMIF med encelle-sumområde 0?

SUMIF returnerede 0, fordi HotXLS klemte iterationen til det mindste af de to områder, mens Excel beholder kriterieområdets form og kun bruger sumområdet til dets øverste venstre celle. =SUMIF(A1:A10,">5",B1) betyder derfor B1:B10 i Excel, en bekvemmelighed, mange håndbyggede templates er afhængige af. Den delte worker TXLSCalculator.GetValueItemRange2 krympede tidligere sine række- og kolonneantaller til værdiområdets, hvilket reducerede eksemplet til en enkelt test af A1 mod B1. v2.384.3 fjerner klemmen: løkken går nu igennem kriterieområdet og læser hver værdi med samme offset fra sumområdets øverste venstre hjørne. Fordi CalcSumIF og CalcAverageIF begge kalder den worker, får AVERAGEIF samme resize, og et sumområde større end kriterieområdet trimmes til kriterieformen af samme grund. Kriterieargumentet i midten er et value-class-argument, og de to yderste er reference-class, den skelnen, der er dækket i artiklen om implicit intersection og argumentklasser

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Row: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Sales');
    for Row := 1 to 10 do
    begin
      Sheet.Cells[Row, 1].Value := Row;          // kriteriekolonne: 1..10
      Sheet.Cells[Row, 2].Value := Row * 100;    // beløb: 100..1000
    end;
    Sheet.Cells[1, 4].Formula := '=SUMIF(A1:A10,">5",B1)';     // encelle-sumområde
    Sheet.Cells[2, 4].Formula := '=SUMIF(A1:A10,">5",B1:B10)'; // eksplicit sumområde
    if Book.Recalculate = lxOk then
      // Både D1 og D2 er 4000 (600+700+800+900+1000); D1 var 0 før v2.384.3
      Writeln(VarToStr(Sheet.Cells[1, 4].Value), ' ', VarToStr(Sheet.Cells[2, 4].Value));
  finally
    Book.Free;
  end;
end;
HotXLS SUMIF- og AVERAGEIF-resize, hvor =SUMIF(A1:A10,">5",B1) går igennem det ti-rækkers kriterieområde og læser B1 til B10 med matchende offsets gennem CalcSumIF-workeren til et resultat på 4000, i stedet for at klemme til encelle-sumområdet, der returnerede 0 før v2.384.3
Excel låner kun sumområdets øverste venstre hjørne og beholder kriterieformen, så en håndbygget template, der giver B1 med, mener B1:B10 — den delte worker går nu igennem alle ti offsets og trimmer et overdimensioneret område på samme måde

INDIRECT og YEARFRAC: to mere stille korrektioner

INDIRECT respekterer nu sit andet argument, og tekst efter en gyldig reference er en fejl i stedet for at blive ignoreret. Med a1 FALSE parses teksten som absolut R1C1, så =INDIRECT("R2C3",FALSE) læser C2; den gamle kode ignorerede flaget, læste "R2" som kolonne R, række 2, og returnerede stille den forkerte celle. Flaget dispatches på sin varianttype (boolean, tal eller tekst), for at konvertere en strengvariant direkte til Double rejser en exception. Relativ R1C1-tekst som R[1]C[1] returnerer #REF!, da INDIRECT ikke har nogen formelcelle-origin at opløse den mod, og A1-tekst med efterfølgende tegn, "B2 junk", returnerer også #REF!. YEARFRAC med basis 0 anvender nu de NASD-regler for sidste-februar, som DAYS360 allerede implementerede: er begge datoer den sidste dag i februar, bliver slutdagen 30, og derefter bliver en start på den sidste dag i februar 30. Fra 2024-02-29 til 2025-02-28 er tællingen nu 360 dage, en brøk på præcis 1, hvor den tidligere Days360US talte 359

Hvad garanterer disse fixes, og hvad var lærdommen?

Sammenligningskæde-adfærden er garanteret af en test, der sammenligner begge motorer med værdier målt i Excel 16, og den test findes, fordi den første beskrivelse af fixen var forkert. v2.384.3-release-noten sagde oprindeligt, at venstre-til-højre-folding gjorde =1<2<3 TRUE, hvilket er præcis, hvad den gamle right-associative parser producerede, og det modsatte af, hvad både Excel og den nye kode returnerer. Ingen havde evalueret eksemplet; det var skrevet ud fra intuitionen "1 er mindre end 2 er mindre end 3". Noten blev rettet, og syv-formel-testen tilføjet i en opfølgende commit, og den regel, der kom ud af det, gælder alle, der dokumenterer spreadsheet-semantik: kør eksemplet i Excel, før du skriver den forventede værdi ned. Substitutionen af tomme operander og SUMIF-resize følger samme Excel-adfærd, inklusive tom-versus-boolean-tilfældet siden v2.384.53, og betingede aggregater, der også skal springe filtrerede eller skjulte rækker over, følger de separate regler i artiklen om SUBTOTAL og AGGREGATE og skjulte rækker

HotXLS er en native Delphi- og C++Builder-spreadsheet-komponent, der læser, rekalkulerer og skriver XLS, XLSX, ODS og CSV uden Excel installeret, og sammenlignings-, tomme- og SUMIF-reglerne beskrevet her bor i beregningsmotoren, som begge arbejdsbogsarkitekturer deler. Den fulde funktionsliste og licensmulighederne står på produktsiden for HotXLS Delphi spreadsheet component