Tekninen artikkeli

XLSX jaetun kaavan si-laajennus Delphissä: sudenkuopat

XLSX:n jaetun kaavan seuraaja ei kanna kaavatekstiä. Sen <f t="shared" si="N"/>-elementti osoittaa isäntäsoluun muualla laskentataulukossa, ja lukijan täytyy rakentaa teksti uudelleen siirtämällä isäntäkaavaa rivi- ja sarake-erolla. HotXLS-komponentti Delphille ja C++Builderille tekee tuon laajennuksen avaushetkellä, joten jokainen seuraaja raportoi täydellisen kaavan

Jos olet koskaan ladannut oikean maailman XLSX:n kolmannen osapuolen kirjastossa ja huomannut, että tuhannen kaavan sarakkeessa on tekstiä täsmälleen yhdessä solussa ja tyhjät merkkijonot muissa 999:ssä, olet kohdannut tämän ominaisuuden väärältä puolelta. Mikään ei ole turmeltunut. Tiedosto tekee juuri sitä, mitä ECMA-376 sallii sen tehdä, ja lukija yksinkertaisesti pysähtyi siihen kohtaan, mihin XML pysähtyi

Miksi jaetun kaavan solu on tyhjä?

Koska formaatti tallentaa kaavan tarkoituksella vain kerran. ECMA-376 Part 1:ssä ja ISO/IEC 29500-1:ssä <f>-elementti (§18.3.1.40) kantaa t-attribuuttia tyyppiä ST_CellFormulaType, ja arvo shared tarkoittaa, että tämä solu osallistuu ryhmään, joka on tunnistettu si-attribuutilla. Täsmälleen yksi solu ryhmässä, isäntä, kantaa myös ref-attribuuttia, joka antaa alueen, johon ryhmä soveltuu, ja vain tuo solu kantaa kaavatekstin elementtisisältönä. Jokainen muu solu ryhmässä on seuraaja. Se toistaa t="shared":n ja saman si:n, ja sen elementtisisältö on tyhjä. Excel kirjoittaa nämä ryhmät aggressiivisesti, koska täyttö alaspäin 200 000 rivin sarakkeen yli romahtaa 200 000 kaavamerkkijonosta yhdeksi merkkijonoksi plus 199 999 pientä paikkamerkkielementtiä. Säästö on todellinen ja kustannus laskeutuu kokonaan lukijalle: ilman laajennusta seuraajalla ei ole merkitystä omillaan

Siirto on käännös, ei tekstikopio

HotXLS ratkaisee seuraajan paikantamalla saman si:n alla rekisteröidyn isännän, laskemalla rivi- ja sarake-eron isäntäankkurista nykyiseen soluun, ja kääntämällä jokaisen viittauksen isäntäkaavassa tuolla erolla. Suhteelliset ulottuvuudet liikkuvat, absoluuttiset eivät, ja sekaviittaukset liikuttavat vain ei-absoluuttista puoliskoaan. Merkkijonoliteraalit ohitetaan kokonaan, joten kaava, joka sattuu sisältämään tekstin "A1", pitää tuon tekstin muuttumattomana jokaisessa seuraajassa

const
  // xl/worksheets/sheet1.xml, trimmed to the interesting cells
  SheetXml: WideString=
    '<row r="1"><c r="A1"><v>1</v></c>'+
    '<c r="B1"><f t="shared" si="4" ref="B1:B3">'+
    'A1+$A$1+A$1+$A1+&quot;A1&quot;+SUM(A1:A2)</f><v>7</v></c></row>'+
    '<row r="2"><c r="B2"><f t="shared" si="4"/><v>8</v></c></row>'+
    '<row r="3"><c r="B3"><f t="shared" si="4"></f><v>9</v></c></row>';

var
  Wb: TXLSXWorkbook;
  Sh: TXLSXWorksheet;
begin
  Wb:= TXLSXWorkbook.Create;
  try
    Wb.Open(FileName);
    Sh:= Wb.Sheets[1];
    // Master, verbatim
    // B1 -> A1+$A$1+A$1+$A1+"A1"+SUM(A1:A2)
    // Follower one row down: relative row moves, absolute row frozen,
    // the mixed A$1 keeps its row, and the literal stays a literal
    // B2 -> A2+$A$1+A$1+$A2+"A1"+SUM(A2:A3)
    ShowMessage(Sh.Cells[2, 2].Formula);
  finally
    Wb.Free;
  end;
end;

ref-attribuutti on portti, ei koristetta. Seuraajaa, jonka koordinaatit putoavat isännän sovellettavan alueen ulkopuolelle, ei laajenneta, koska tiedosto tekee silloin väitteen, jota ryhmä ei tue. Samoin, kun siirto työntäisi viittauksen rivin yksi yläpuolelle tai sarakkeen A vasemmalle, HotXLS tuottaa #REF!:n tuolle tokenille sen sijaan, että se hiljaa puristaisi sen, mikä on juuri se, mitä Excel itse tuottaisi samalle muokkaukselle. Tämä käännös on läheinen serkku, muttei sama asia kuin viittauksen uudelleenkirjoitus, joka tapahtuu kun lisäät tai poistat rivejä. Tuolla polulla on omat sääntönsä siitä, mitä alue tekee, kun muokkaus leikkaa sen läpi, ja se kuvataan erikseen artikkelissa kaavaviittausten säätämisestä lisäyksen ja poiston aikana. Jaettu laajennus on yksinkertaisempi: se on puhdas siirtymä tunnetusta ankkurista, sovellettuna kerran, jäsennyshetkellä

Mitkä viittausmuodot siirtäjän täytyy kattaa?

Kaikki niistä, tai laajennus on naamioitu datanmenetysbugi. Naiivi siirtäjä, joka ymmärtää vain A1:n ja A1:B2:n, turmelee tai pudottaa eksoottisemmat muodot, ja todelliset työkirjat ovat niitä täynnä. HotXLS:n jaetun kaavan kääntäjä tunnistaa koko A1-perheen ennen kuin se päättää, mitä siirtää. Ulkoiset työkirjaviittaukset kuten [Book.xlsx]Sheet1!A1 ja 3D-viittaukset kuten Sheet1:Sheet3!A1 pitävät etuliitteensä koskemattomana, kun taas perässä oleva soluviittaus siirtyy. Lainatut laskentataulukkonimet selviävät, mukaan lukien ilkeä tapaus, jossa laskentataulukko on kirjaimellisesti nimetty A1, joten 'A1'!A1 siirtää vain huutomerkin jälkeisen osan. Koko sarake A:A liikuttaa sarakeulottuvuuttaan eikä mitään muuta; koko rivi 1:1 liikuttaa riviulottuvuuttaan eikä mitään muuta; $A:$A ei liiku lainkaan. Strukturoidut taulukkoviittaukset kuten Table[A1] jätetään koskemattomiksi, koska hakasulkeissa oleva osa on sarakenimi, ei koordinaatti

// One master, expanded two columns to the right and zero rows down.
// Master D1: A1+A:A+$A:$A
// F1       : C1+C:C+$A:$A
//
// One master, expanded three rows down and zero columns across.
// Master A1: B1+$C$1+"A1"+A:A+1:1+'Data'!A1+LOG10(A1)+Table[A1]+'A1'!A1
// A3       : B3+$C$1+"A1"+A:A+3:3+'Data'!A3+LOG10(A3)+Table[A1]+'A1'!A3
//
// Note what did NOT move in the second line: the absolute $C$1, the
// string literal "A1", the whole column A:A under a pure row delta,
// the function name LOG10, and the structured reference Table[A1]

Funktionimet ovat hiljainen ansa täällä. Token-skanneri, joka nappaa kirjaimia joita seuraa numerot, kirjoittaisi mielellään LOG10:n uudelleen muotoon LOG11 yhtä riviä alempana. HotXLS vaatii viittausrajan ennen ehdokastokenia ja sen jälkeen, joten tunniste, joka jatkuu kirjaimeen, numeroon, alaviivaan, pisteeseen tai avaavaan sulkuun, ei ole soluviittaus. Jos työskentelet toisessa merkintäperheessä, sama rajaongelma näyttäytyy eri tavalla, ja R1C1-merkintäartikkeli kattaa, mihin nämä kaksi mallia eroavat toisistaan

Miksi itsensä sulkeva f-elementti nielaisee seuraavan arvon?

Koska itsensä sulkeva elementti ei tuota loppuelementtitapahtumaa. Tämä on koko ominaisuuden kallein yksittäinen bugi, eikä se ole ominaista millekään tietylle XML-jäsentäjälle. TXMLReader:ssä <f t="shared" si="4"/> nostaa täsmälleen yhden Element-tapahtuman, jossa IsEmptyElement on asetettu True:ksi, eikä koskaan nosta vastaavaa EndElement-tapahtumaa. Jäsentäjä, joka sulkee kaavan tallennustilansa vain EndElement:issa, pysyy siis kaavan sisällä, ja seuraava teksti, jonka se näkee, mikä on välimuistiin tallennettu tulos <v>:n sisällä, liitetään kaavapuskuriin. Pahempaa, tila selviää solurajan yli, joten seuraava solu, joka omistaa todellisen <f>:n, saa kaavatekstinsä imaistuksi edelliseen soluun. Korjaus on lopettaa kaavatila itse Element-tapahtumassa aina, kun IsEmptyElement on True, ja ajaa koko seuraajan ratkaisu siinä sen sijaan, että odotettaisiin. Se tarkoittaa t:n, si:n, ref:n, aca:n ja ca:n lukemista attribuuteista, jaetun laajennuksen soveltamista, uudelleenlaskennan attribuuttien kirjoittamista soluun, ja jaetun tilan tyhjentämistä, kaiken tämän tyhjän elementin käsittelevän haaran sisällä. Huomaa, että formaatti sallii molemmat kirjoitusasut, <f t="shared" si="4"/> ja <f t="shared" si="4"></f>, ja jälkimmäinen todella nostaa EndElement:in. Oikean lukijan täytyy käsitellä pari identtisesti, minkä vuoksi HotXLS kattaa molemmat kirjoitusasut samassa regressiotiedostossa

Harvat, järjestämättömät si-arvot ja odottava jono

si-attribuutti on tiedoston antama etumerkitön kokonaisluku, ei taulukkopaikka, jota hallitset. Mikään skeemassa ei vaadi jaettujen indeksien olevan tiheitä, alkavan nollasta, tai esiintyvän nousevassa järjestyksessä, eikä mikään estä vihamielistä tai vain outoa tiedostoa käyttämästä si="4294967290":tä ensimmäisessä solussa. Hakutaulukon mitoittaminen suurimman havaitun si:n mukaan on siis muistinloppumisalkeisosa, ei optimointi. HotXLS pitää työkirjan avauspolun sen sijaan lajitellulla harvalla taulukolla: jaetut ryhmät rekisteröidään kokonaislukuavaimensa alle lajitellussa TStringList:issa, mikä tekee hausta binäärihaun sen yli, kuinka monta ryhmää todella on olemassa, ilman suhdetta indeksien numeeriseen kokoon. Järjestys on ongelman toinen puolisko. Isäntä yleensä edeltää seuraajiaan dokumenttijärjestyksessä, mutta se on käytäntö eikä sääntö, joten mikä tahansa seuraaja, joka ei pysty ratkaisemaan si:tään sillä hetkellä, kun se jäsennetään, menee odottavaan jonoon. Kun laskentataulukko valmistuu, jono toistetaan nyt täydellistä taulukkoa vasten, ja myöhäiset isännät ratkaisevat orpojaan. Solut, jotka eivät koskaan löydä isäntää, pitävät tyhjän kaavan, mikä on rehellinen lopputulos tiedostolle, joka viittaa ryhmään, jota se ei koskaan määritellyt

Jaettujen kaavojen laajentaminen lataamatta työkirjaa

Suoratoistavat lukijat kohtaavat saman vaatimuksen paljon tiukemmalla muistibudjetilla, ja ne ratkaisevat sen laskentataulukkokohtaisella taulukolla. TXLSDirectReader ja TXLSRowCursor laajentavat molemmat seuraajat täydellisiksi solukohtaisiksi kaavoiksi säilyttäen samalla rajatun muistin ja projektiokäyttäytymisensä, joten eteenpäin-vain-läpikäynti 300 MB:n laskentataulukon yli antaa sinulle silti todellisen kaavatekstin

var
  Reader: TXLSDirectReader;
  Cursor: TXLSRowCursor;
begin
  // Projection: only rows 2..3, only column A. The master lives in row 1,
  // outside the projection, and is still parsed so the followers resolve
  Reader:= TXLSDirectReader.Create;
  try
    Reader.FirstRow:= 2;
    Reader.LastRow:= 3;
    Reader.IncludeColumn(1);
    Reader.OnCell:= HandleCell;   // Cell.Formula is fully expanded here
    Reader.ReadFile(FileName);
  finally
    Reader.Free;
  end;

  // Forward-only row traversal, same expansion
  Cursor:= TXLSRowCursor.Create;
  try
    Cursor.Open(FileName);
    if Cursor.FindFirst then
      repeat
        if Cursor.CellCount > 0 then
          WriteLn(Cursor.RowIndex, ': ', Cursor.Cells[0].Formula);
      until not Cursor.FindNext;
  finally
    Cursor.Free;
  end;
end;

Kaksi rajoitetta seuraa tästä suunnittelusta. Ensinnäkin, projektio ei voi koskaan ohittaa isäntää. Rivisuodin, joka on asetettu FirstRow:lla ja LastRow:lla, tai sarakesuodin, joka on rakennettu IncludeColumn:lla, voi ohittaa isäntäsolun tuottamisen takaisinkutsullesi, mutta jäsentäjän täytyy silti tallentaa sen si, ankkurikoordinaatit, sovellettava alue ja kaavateksti, muuten jokainen projektion sisällä oleva seuraaja ratkeaa tyhjäksi. Vain seuraajapuolen työ, siirto ja arvon dekoodaus, on turvallista ohittaa. Toiseksi, taulukko on laskentataulukkokohtainen, ja sen elinaikaa täytyy hallita eksplisiittisesti: TXLSRowCursor pitää yhden instanssin laskentataulukon läpikäynnin ajan ja tyhjentää sen uudelleenkäynnistyksessä, laskentataulukon vaihdossa, tiedoston lopussa, poikkeuksessa ja sulkemisessa, joten yhdellä laskentataulukolla määritelty ryhmä ei koskaan voi vuotaa toiseen. Koska suoratoistava polku on kuuma silmukka, se käyttää avoimen osoitteistuksen kokonaislukutiivistettä lajitellun merkkijonotaulukon sijaan, mikä välttää kokonaisluku-merkkijono-muunnoksen solua kohti

Mitä tapahtuu tallennuksessa, ja missä rajat ovat

Kun seuraaja on laajennettu, se on tavallinen kaava, ja HotXLS kirjoittaa sen takaisin itsenäisenä <f>-elementtinä ilman t="shared":a ja ilman si:tä. Edestakainen kierto on vakaa ja välimuistiin tallennetut <v>-tulokset selviävät, mutta tuloste on suurempi kuin syöte raskaasti jaetulle laskentataulukolle, eikä Excelin luomaa ryhmittelyä rekonstruoida tallennuksessa. Jos jaettujen ryhmien tavutason uskollisuus on sinulle tärkeämpää kuin todellisen kaavatekstin saaminen jokaiseen soluun, tämä on kompromissi, jonka hyväksyt. XLS-puoli on muuten erilainen: BIFF8 SHRFMLA-tietueella on oma koodauksensa ja oma kirjoittajansa, jaettu-ryhmä-kytkimellä työkirjassa

Kaksi toisiinsa liittyvää asiaa eivät ole eksplisiittisesti jaettuja kaavoja, vaikka ne jakavat <f>-elementin. Vanhat CSE-taulukkokaavat käyttävät t="array":a ref:n kanssa, joka kattaa ankkuroidun alueen, ja dynaamiset taulukot käyttävät samaa t="array"-kirjoitusasua mutta ne tunnistetaan cm-attribuutilla, joka ketjuttaa cellMetadata:n kautta XLDAPR-tietueeseen. Dynaamisen taulukon vuotosolun käsitteleminen jaettuna tai CSE-seuraajana on aito oikeellisuusbugi, ja erottelu käsitellään artikkelissa dynaamisista taulukoista ja vuotokaavoista. Lue nämä kolme tapausta kolmena jäsentäjänä, jotka sattuvat jakamaan tagimerkin, ja koodi pysyy rehellisenä

Tässä kuvattu jaetun kaavan laajennus, suoratoistavat lukijat ja viittauskääntäjä toimitetaan osana HotXLS Excel -komponenttia Delphille ja C++Builderille; tuotesivu sisältää täydellisen kaava- ja suoraluku-API-viitteen, mukaan lukien edellä käytetyt projektio-ominaisuudet