Teknisk artikkel

Sammenligningskjeder, tomme celler og SUMIF i HotXLS Delphi

HotXLS Delphi Component evaluerer =1<2<3 til FALSE, samme svar som Excel 16 gir, for siden v2.384.3 folder formelparseren sammenligningsoperatorer fra venstre mot høyre: 1<2 blir TRUE, og TRUE<3 er FALSE fordi en boolean rangerer over alle tall. Samme utgivelse gjør en tom operand lik både 0 og "", og lar SUMIF strekke et sumområde på én celle til formen til kriterieområdet. Hvert av disse ser ut som kuriositeter helt til en arbeidsbok beregnet i Delphi er uenig med samme arbeidsbok åpnet i Excel

Uenigheten starter vanligvis med en formel noen skrev på intuisjon. Noen taster =0<B2<100 for å sjekke at et antall er innenfor området, Excel svarer stille FALSE for hver rad, og arket sendes ut med den feilen bakt inn. En beregningsmotor får ikke lov til å reparere brukerens intensjon; jobben dens er å produsere verdien Excel ville produsert, slik at det bufrede resultatet HotXLS skriver inn i filen stemmer med det Excel viser etter en rekalkulering. Før v2.384.3 svarte HotXLS TRUE for den områdesjekken på hver rad, feil i motsatt retning, og en rapport generert på en server ville motsa samme rapport åpnet på en stasjonær

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

Excel gir FALSE fordi den leser en kjede av sammenligninger som (1<2)<3, og den indre TRUE taper så type-rangeringskonkurransen mot tallet 3. Den gamle HotXLS-parseren leste samme tekst som 1<(2<3): TXLSSyntax.Parse_expr i lxFormula.pas parset én operand, så en sammenligningstoken og rekurserte inn i Parse_expr for høyresiden, noe som gjør operatøren høyreassosiativ. Det gir 1<TRUE, og et tall er under en boolean, så resultatet ble TRUE. Feilen 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 fiksen. Regresjonstesten CalculateFormula_ComparisonChainsFoldLeftToRight fester syv slike formler mot verdiene Excel 16 gir, og kjører hver enkelt gjennom begge motorarkitekturene, classic TXLSWorkbook og den XLSX-native TXLSXWorkbook, med Calculate-metoden beskrevet i oversikten over HotXLS formelmotor

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');
  // Det Excel 16 gir:  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 mot det aktive arket og
    // gir Null når arbeidsboken ikke har noe ark i det hele tatt
    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ær for =1<2<3 der den gamle høyreassosiative Parse_expr evaluerte 1<(2<3) som TRUE mens venstre-til-høyre-folderen siden v2.384.3 evaluerer (1<2)<3 som FALSE, avgjort av CompareVariants-rangeringen som setter alle tall under tekst og tekst under boolean, regelen i lxCalc.pas
Begge motorer folder nå sammenligningskjeder fra venstre mot høyre og fester syv formler mot Excel 16 — en boolean rangerer over alle tall, så TRUE som taper til 3 er nøyaktig det som gjør den kjedede områdesjekken til FALSE

Fiksen gjør Parse_expr om til en løkke av samme form som Parse_expr1 allerede bruker for +, - og &. Den parser den første operanden med Parse_expr1, og så lenge neste token er én av =, <>, <, >, <= eller >=, lager den en sammenligningsnode, fester det akkumulerte venstre resultatet som første barn, parser neste operand med Parse_expr1 i stedet for Parse_expr, og gjør den nye noden til venstre resultat for neste runde. To detaljer var lette å få feil da rekursjonen ble omgjort til iterasjon, og begge står i vedlikeholdernotatene: den akkumulerte noden må overlates (lChild := Item; Item := nil) i den rekkefølgen, og feilveien må Exit etter å ha frigjort den halvbygde noden i stedet for å falle ut av løkken og returnere et hengende tre

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

HotXLS rangerer blandede typer slik Excel gjør: alle tall er mindre enn alle tekstverdier, og alle tekstverdier er mindre enn alle booleans. TXLSCalculator.CompareVariants i lxCalc.pas klassifiserer begge operandene med GetRetValueType inn i enumerasjonen TXLSRetValueType = (xlVariantValue, xlNumberValue, xlStringValue, xlBooleanValue), og når de to klassene er forskjellige, sammenligner den bare ordinalene deres, så deklarasjonsrekkefølgen til den enum-en er kryss-type-regelen. Inne i én klasse er sammenligningen den naturlige, med ett Excel-spesifikt tvist for tekst: begge strengene går gjennom lxUpperCase først, så ="abc"="ABC" er TRUE. Denne rangeringen er grunnen til at kjederesultatet ikke kan ressoneres om uten den. TRUE<3 er ikke en tvangskonvertering av TRUE til 1, det er en boolean sammenlignet med et tall, og boolean-en vinner. Datoer er serialtall for motoren (varDate klassifiseres som xlNumberValue), så en dato er alltid under enhver tekst, inkludert tekst som tilfeldigvis ser ut som en dato

Hva er en tom celle lik i en sammenligning?

En tom celle brukt som sammenligningsoperand er lik 0 når den andre siden er et tall, lik "" når den andre siden er tekst, og siden v2.384.53 lik FALSE når den andre siden er en logisk verdi, så med A1 tom er =A1=0, =A1="" og =A1=FALSE alle TRUE. TXLSCalculator.CompareVarValues, som betjener alle seks sammenligningsoperatørene, erstatter den tomme før den kaller CompareVariants: er nøyaktig én operand Null, blir den WideString('') når partneren er en streng, False når partneren er en boolean, og 0 ellers. To tomme celler sammenlignes fortsatt like med hverandre uten substitusjon. Aritmetikk-veien har alltid gjort en tom celle om til 0, det er derfor =A1+1 ga 1, men CompareVariants beholder Null som sin egen laveste rang, under alle tall, og sammenligningsoperatørene brukte den rangen direkte

var
  Wb: IXLSWorkbook;
  Sh: TXLSWorksheet;
begin
  Wb := TXLSWorkbook.Create;
  Sh := Wb.Sheets.Add;
  Sh.Range['B1', 'B1'].Value := 5;      // A1 blir med vilje latt 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 substitusjon av tom operand i CompareVarValues der en tom A1 sammenlignes lik med 0 og med tom tekst, mens den gamle Null-rangeringen gjorde =A1<0 TRUE for hver tom saldo, og siden v2.384.53 sammenlignes tom mot boolean som FALSE, slik at =A1=FALSE er TRUE som i Excel
Substitusjonen matcher den andre operandens type, 0, den tomme strengen eller siden v2.384.53 FALSE — IF-en som merket hver tom saldo som overtrukket, var den gamle Null-rangeringen, ikke dataene dine

Siste linje er den som gjorde vondt i praksis. Under den gamle rangen var en tom celle mindre enn alle tall, negative inkludert, så =IF(A1<0,"overdrawn","ok") merket hver tom saldo-celle som overtrukket, og =A1=0 var FALSE for en celle enhver bruker ville beskrive som null. Én grense sto igjen etter v2.384.3: substitusjonen valgte bare mellom 0 og den tomme strengen, så en tom celle sammenlignet med en boolean ble 0, som rangerer under både TRUE og FALSE, og =A1=FALSE på en tom A1 evaluerte til FALSE. Siden HotXLS 2.384.53 behandles en tom celle sammenlignet med en logisk verdi som FALSE i både XLS- og XLSX-motoren, slik Excel gjør: med A1 tom gir =A1=FALSE og =A1<TRUE TRUE og =A1=TRUE gir FALSE. Det betyr også at sammenligningen ikke kan skille tom fra FALSE, i Excel eller i HotXLS; trenger et ark det skillet, test med ISBLANK eller =A1=""

Hvorfor ga SUMIF med et sumområde på én celle 0?

SUMIF ga 0 fordi HotXLS klemte iterasjonen inn på det minste av de to områdene, mens Excel beholder formen til kriterieområdet og bare bruker sumområdet til øvre venstre celle. =SUMIF(A1:A10,">5",B1) betyr derfor B1:B10 i Excel, en bekvemmelighet mange håndbygde maler støtter seg på. Den delte arbeideren TXLSCalculator.GetValueItemRange2 krympet rad- og kolonneantallet sitt til verdiområdets, noe som reduserte eksempelet til én enkelt test av A1 mot B1. v2.384.3 fjerner klemmen: løkken går nå gjennom kriterieområdet og leser hver verdi med samme offset fra sumområdets øvre venstre hjørne. Etter som CalcSumIF og CalcAverageIF begge kaller den arbeideren, får AVERAGEIF samme størrelsesendring, og et sumområde større enn kriterieområdet trimmes til kriterieformen av samme grunn. Kriterieargumentet i midten er et argument av verdiklasse og de to ytterste er av referanseklasse, skillet dekket i artikkelen om implisitt skjæringspunkt 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øp: 100..1000
    end;
    Sheet.Cells[1, 4].Formula := '=SUMIF(A1:A10,">5",B1)';     // sumområde på én celle
    Sheet.Cells[2, 4].Formula := '=SUMIF(A1:A10,">5",B1:B10)'; // eksplisitt 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-størrelsesendring der =SUMIF(A1:A10,">5",B1) går gjennom det ti rader høye kriterieområdet og leser B1 til B10 med matchende offsetter gjennom CalcSumIF-arbeideren til et resultat på 4000, i stedet for å klemme seg til sumområdet på én celle som ga 0 før v2.384.3
Excel låner bare sumområdets øvre venstre hjørne og beholder kriterieformen, så en håndbygd mal som sender B1, mener B1:B10 — den delte arbeideren går nå gjennom alle ti offsetter og trimmer et for stort område på samme måte

INDIRECT og YEARFRAC: to stillere korreksjoner

INDIRECT ærer nå sitt andre argument, og tekst etter en gyldig referanse er en feil i stedet for å bli ignorert. Med a1 FALSE parses teksten som absolutt R1C1, så =INDIRECT("R2C3",FALSE) leser C2; den gamle koden ignorerte flagget, leste «R2» som kolonne R, rad 2, og returnerte stille feil celle. Flagget dispatches på varianttypen sin (boolean, tall eller tekst), for å konvertere en strengvariant rett til Double reiser et unntak. Relativ R1C1-tekst som R[1]C[1] gir #REF!, siden INDIRECT ikke har noe formelcelle-origo å løse den mot, og A1-tekst med etterfølgende tegn, "B2 junk", gir #REF! også. YEARFRAC med basis 0 anvender nå NASD-reglene for siste-februar som DAYS360 allerede implementerte: er begge datoer den siste dagen i februar, blir sluttdagen 30, og en start på den siste dagen i februar blir så 30. Fra 2024-02-29 til 2025-02-28 er antallet nå 360 dager, en brøkdel på nøyaktig 1, der den tidligere Days360US telte 359

Hva garanterer disse fiksene, og hva var lærdommen?

Sammenligningskjede-oppførselen garanteres av en test som sammenligner begge motorer med verdier målt i Excel 16, og den testen finnes fordi den første beskrivelsen av fiksen var gal. Utgivelsesnotatet for v2.384.3 sa opprinnelig at venstre-til-høyre-folding gjorde =1<2<3 til TRUE, noe som er nøyaktig hva den gamle høyreassosiative parseren produserte og det motsatte av hva både Excel og den nye koden gir. Ingen hadde evaluert eksempelet; det var skrevet ut fra intuisjonen om at «1 er mindre enn 2 er mindre enn 3». Notatet ble korrigert og syv-formel-testen lagt til i en oppfølgings-commit, og regelen som kom ut av det, gjelder enhver som dokumenterer regnearksemantikk: kjør eksempelet i Excel før du skriver ned forventet verdi. Substitusjonen av tomme operander og SUMIF-størrelsesendringen følger samme Excel-oppførsel, inkludert tom-mot-boolean-tilfellet siden v2.384.53, og betingede aggregater som også må hoppe over filtrerte eller skjulte rader følger de separate reglene i artikkelen om skjulte rader for SUBTOTAL og AGGREGATE

HotXLS er en naturlig Delphi- og C++Builder-regnearkkomponent som leser, rekalkulerer og skriver XLS, XLSX, ODS og CSV uten Excel installert, og sammenlignings-, tom- og SUMIF-reglene beskrevet her bor i beregningsmotoren begge arbeidsbokarkitekturene deler. Full funksjonsliste og lisensalternativer finner du på produktsiden for HotXLS Delphi regnearkkomponent