Artykuł techniczny

Powtarzane wiersze ODS jako przedziały wysokości w HotXLS

Arkusze z 45 wierszami danych i czternastoma kolumnami, zapisane jako ODS, kończą się zwykle jednym elementem więcej, niż się spodziewasz: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Ta pojedyncza linia opisuje każdy wiersz od 46 aż po dno siatki. Rozwiń ją dosłownie, jako ten wiersz wypisany 1 048 530 razy, i mały content.xml zamienia się w milion rekordów wierszy, milion wpisów wysokości i, jeśli kolumny niosą domyślny styl komórki, prawie piętnaście milionów ostylowanych pustych komórek

Jak HotXLS zamienia jeden powtarzany wiersz ODS w zwarty stan: element content.xml z table:number-rows-repeated 1048530 i stylem ro1 mapuje się na pojedynczy rekord TXLSXRowHeightRun obejmujący wiersze 46 do 1048575 przy 12.81 pt plus jeden wpis StyleOverlays na zakres kolumn o tym samym stylu, zamiast rozwijania tego elementu w milion wpisów SetRowHeight i obiektów komórek
licznik powtórzeń, wysokość wiersza ro1 i domyślne style kolumn opisują każdy pusty wiersz pod wierszem 45, więc importer może zbudować jeden rekord przedziału i kilka nakładek na zakresy kolumn, nie dotykając miliona współrzędnych

Dlaczego pojedynczy powtarzany wiersz jest drogi w imporcie

Koszt bierze się z rozwijania, a rozwijanie to oczywisty sposób pisania importera. Styl ro1 ustawia style:row-height="0.452cm", czyli 12.81 punktu. Finalizator wiersza, który aplikuje go jednym SetRowHeight(RowIndex + i, RowHeight) na powtarzany wiersz, zapisuje 1 048 530 wpisów do tabeli wysokości per wiersz, jakąkolwiek trzyma model obiektowy. Jeśli ta tabela to kluczowana lista napisów, gdzie każdy zapis najpierw szuka istniejącego klucza, import jest kwadratowy względem licznika powtórzeń, a licznik rzędu miliona to dokładnie ten input, przy którym kwadrat przestaje być teoretyczny

Puste komórki pogarszają sprawę. Każdy <table:table-column> w takim pliku zwykle podaje table:default-cell-style-name i każda pusta komórka w powtarzanym wierszu go dziedziczy. Ostylowana pustka to dla większości modeli obiektowych wciąż komórka. Zmaterializuj jedną na kolumnę na powtarzany wiersz, a rachunek za pamięć przyćmi tabelę wysokości

Strona zapisu ma lustrzany problem. Gdy model powie, że wiersz 1 048 575 ma wysokość, writer przechodzący od wiersza 1 do ostatniego z czymkolwiek wyemituje milion elementów <table:table-row>. Tani import połączony z drogim zapisem tylko przenosi koszt na inny przycisk

Jak HotXLS przechowuje wysokość powtarzanego wiersza

HotXLS Delphi Excel Component przechowuje wysokość powtarzanego wiersza ODS jako przedział: rekord TXLSXRowHeightRun z FirstRow, LastRow i jedną Height. Gdy TXLSXWorkbook.OpenODS kończy wiersz o liczniku powtórzeń 1, woła SetRowHeight jak każdy inny kod. Przy większym liczniku zapisuje cały odcinek jako jeden przedział, więc ogon z przykładu staje się wierszami 46 do 1 048 575 przy 12.81 pt. Licznik powtórzeń wybiegający poza koniec siatki jest ucinany na wierszu 1 048 576 zamiast być odrzucany

Jedna reguła importera zaskakuje ludzi diffujących wynik względem pliku źródłowego. Aplikacje arkuszowe piszące ODS dają zwykle każdemu stylowi wiersza style:row-height, także wierszom, których nikt nigdy nie zmieniał, i oznaczają je style:use-optimal-row-height="true". Dla wiersza, który nie przechowuje żadnych komórek, wysokość automatyczna to po prostu wysokość domyślna, więc HotXLS nie zapisuje dla niego wysokości. Wiersze, które coś przechowują, zachowują wysokość nawet oznaczoną jako optymalna, bo wiersz z zawijanym tekstem, który urósł, by pomieścić treść, skurczyłby się z powrotem przy ponownym otwarciu skoroszytu jako XLSX. Ten kompromis jest właściwy: wyrzucenie prawdziwej wysokości daje widoczną zmianę układu, a zostawienie redundantnej kosztuje jeden przedział

Odczyt wysokości idzie przez zwykłą właściwość RowHeight. Najpierw sprawdza listę per wiersz i dopiero gdy wiersz nie ma jawnego wpisu, schodzi do przedziałów; HasRowHeight podąża tą samą kolejnością. Przedziały nigdy się nie nakładają, więc fallback zatrzymuje się na pierwszym przedziale zawierającym wiersz

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Spans: TXLSXAxisSpans;
  I: Integer;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // Arkusz, którego ostatni element wiersza powtarza się 1 048 530 razy
    Workbook.OpenODS('tail-repeat.ods');
    Sheet := Workbook.Sheets[1];  // Sheets jest numerowane od 1
    // Zapisane metadane wierszy: najpierw wpisy pojedynczych wierszy, potem przedziały
    Spans := Sheet.GetStoredAxisSpans(True);
    for I := 0 to High(Spans) do
      Writeln(Spans[I].FirstIndex, '..', Spans[I].LastIndex);
    // Oba odczyty pochodzą z tego samego przedziału
    Writeln(Sheet.RowHeight[46]:0:2, ' pt');
    Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
    // Wpis per wiersz jest sprawdzany przed przedziałem, więc wygrywa
    Sheet.RowHeight[500000] := 36;
    // Wyczyszczenie jednego wiersza wewnatrz przedzialu wycina w nim dziurę
    Sheet.ClearRowHeight(500001);
    Writeln(Sheet.HasRowHeight(500001));           // FALSE
    Writeln(Sheet.RowHeight[500002]:0:2, ' pt');   // nadal wysokość przedziału
  finally
    Workbook.Free;
  end;
end;

Dwie edycje w tym przykładzie zachowują się różnie celowo. Sheet.RowHeight[500000] := 36 zostawia przedział w spokoju i dokłada jeden wpis do listy per wiersz, która przysłania przedział, bo jest pytana pierwsza. ClearRowHeight nie może działać tak, bo wyczyszczony wiersz musi zgłaszać brak wysokości, mimo że przedział wciąż go obejmuje. Usuwa więc każdy wpis per wiersz, a potem przebudowuje listę przedziałów: każdy przedział poza czyszczonym wierszem zostaje jak był, przedział, który go zawierał, jest dzielony na część przed i część po, a dla samego wiersza 500 001 nic nie jest zapisywane. GetStoredAxisSpans(True) wypisuje teraz 46..500000 i 500002..1048575 jako dwa osobne przedziały, obok wpisu pojedynczego wiersza dla 500 000. ClearRowHeights opróżnia listę per wiersz i przedziały jednym wywołaniem

Chirurgia przedziałów wysokości wierszy w HotXLS: po OpenODS jeden przedział obejmuje wiersze 46 do 1048575 przy 12.81 pt, wpis per wiersz ustawia wiersz 500000 na 36 pt i wygrywa wyszukiwanie, bo RowHeight najpierw sprawdza listę per wiersz, a ClearRowHeight wiersza 500001 dzieli przedział na dwa rozłączne kawałki wokół dziury
ClearRowHeight zostawia kawałki przedziału poza czyszczonym wierszem i nie zapisuje nic dla samego wiersza, więc przedziały nigdy się nie nakładają, wyszukiwanie może stanąć na pierwszym trafieniu, a nadpisanie w wierszu 500000 zostaje nietknięte

Gdzie trafiają dziedziczone style pustych komórek?

Dziedziczone style pustych komórek stają się nakładkami stylu: prostokątami na arkuszu, które niosą styl, bez obiektów komórek za nimi. Czytając wiersz, importer przechodzi powtarzane komórki w odcinkach sąsiednich kolumn rozwiązywanych do tego samego stylu. Odcinek, którego styl wygląda dokładnie jak Default, nie zapisuje nic. Odcinek z prawdziwym formatowaniem, na przykład domyślnym wyśrodkowaniem albo pogrubieniem kolumny, staje się jedną wartością wiersza obejmującą wszystkie jego kolumny

Gdy ta wartość wiersza jest zatwierdzana i wiersz się powtarza, HotXLS materializuje pojedynczą komórkę w lewym górnym rogu odcinka, aplikuje do niej dziedziczony styl i rejestruje sześć indeksów stylu (font, wypełnienie, obramowanie, format liczby, wyrównanie, ochrona) przez Sheet.StyleOverlays.Add dla całego prostokąta: każda kolumna w odcinku, od pierwszego powtarzanego wiersza do ostatniego. Dla pozostałych 1 048 529 wierszy nie powstaje nic. Sheet.Cells.Count trzyma się blisko liczby komórek, które faktycznie mają dane

Komórka wewnątrz prostokąta podczepia nakładkę w momencie, gdy twój kod jej dotknie. Po otwarciu arkusza, którego ogon jest formatowany przez pogrubiony domyślny styl kolumny, Sheet.Cells[700000, 1].FontIndex rozwiąże się do pogrubionego fontu, choć komórka na tej współrzędnej wiersz wcześniej nie istniała. Pliki XLSX z sformatowaną, ale pustą kolumną przechodzą przez tę samą maszynerię, a artykuł o przechowywaniu komórek blokami wierszy i nakładkach stylu przedziałowych opisuje, jak kilka nakładek warstwi się i rozwiązuje. Importer ODS różni się tylko tym, skąd bierze nakładki: wyprowadza je z liczników powtórzeń sam

Jak SaveAsODS zapisuje licznik powtórzeń z powrotem

TXLSXWorkbook.SaveAsODS zapisuje każdy odcinek identycznych wierszy jako jeden <table:table-row> z table:number-rows-repeated i kończy odcinek tylko tam, gdzie coś w wierszach się zmienia. Tabela sięga do najniższego wiersza, który coś niesie: wartość, pustą komórkę tylko ze stylem, wysokość pojedynczego wiersza, koniec przedziału wysokości, dolną krawędź nakładki, ukryty albo konspektowy wpis, obszar walidacji lub formatowania warunkowego

Od wiersza 1 writer patrzy w przód na następną granicę. Kandydaci to następna zmaterializowana komórka, początek albo koniec przedziału wysokości, dowolny wpis pojedynczego wiersza (wysokość, flaga ukrycia, poziom konspektu, stan zwinięcia, ręczny podział strony), krawędzie nakładek, zakresy scalone, hiperłącza, komentarze, obrazy oraz krawędzie każdego prostokąta walidacji i formatowania warunkowego. Wszystko do tej granicy to jeden element. Wewnątrz elementu ta sama logika biegnie w poprzek kolumn, więc pusty odcinek staje się jednym <table:table-cell> z table:number-columns-repeated i nazwą stylu, do którego rozwiązuje nakładka

Style wierszy są współdzielone. Wysokość wiersza jest formatowana na tekst, dla przykładu 12.81pt, i ten tekst razem z flagą ukrycia i flagą podziału strony tworzy klucz automatycznego stylu wiersza. Każdy wiersz z tymi samymi trzema wartościami wskazuje ten sam styl ro<N>, który trzyma jedną właściwość style:row-height. Wiersz bez wysokości, widoczny i bez podziału, nie dostaje stylu wiersza wcale

Co SaveAsODS zapisuje dla arkusza opartego na przedziałach: wiersze 1 do 45 trzymają wartości i są zapisywane po jednym elemencie na wiersz, ogon do wiersza 1048575 jest emitowany jako powtarzane elementy table-row dzielące się na krawędziach przedziałów, nadpisań per wiersz i krawędziach nakładek, wiersze o tej samej wysokości współdzielą jeden automatyczny styl wiersza, a style komórek rozwiązują się przez nakładki
każdy powtarzany element obejmuje jeden jednolity odcinek i zatrzymuje się na następnej krawędzi przedziału, wpisie wysokości, krawędzi nakładki, obszarze reguły albo zmaterializowanej komórce, więc pusty ogon zapisuje się jako garść elementów, podczas gdy eksport XLSX pisze po jednym elemencie wiersza na każdy objęty wiersz
var
  Workbook, Reopened: TXLSXWorkbook;
  Saved: TMemoryStream;
begin
  Workbook := TXLSXWorkbook.Create;
  Reopened := TXLSXWorkbook.Create;
  Saved := TMemoryStream.Create;
  try
    Workbook.OpenODS('tail-repeat.ods');
    Workbook.Sheets[1].RowHeight[500000] := 36;
    Workbook.Sheets[1].ClearRowHeight(500001);
    // Ogon jest zapisywany jako kilka powtarzanych elementów wierszy
    Workbook.SaveAsODS(Saved);
    Writeln('ODS stream: ', Saved.Size, ' bytes');
    Saved.Position := 0;
    Reopened.OpenODS(Saved);
    // Oczekiwane: nadpisanie, dziura i oba kawałki przedziału przeżywają round trip
    Writeln(Reopened.Sheets[1].RowHeight[500000]:0:2);   // 36.00
    Writeln(Reopened.Sheets[1].HasRowHeight(500001));    // FALSE
    Writeln(Reopened.Sheets[1].RowHeight[500002]:0:2);   // wysokość przedziału
  finally
    Saved.Free;
    Reopened.Free;
    Workbook.Free;
  end;
end;

Dla arkusza wyżej ogon dzieli się na cztery kawałki: wiersze 46 do 499 999 przy wysokości przedziału, wiersz 500 000 przy 36 pt, wiersz 500 001 bez stylu wiersza i wiersze 500 002 do 1 048 575 znowu przy wysokości przedziału. Każda kolumna, której domyślny styl zostawił nakładkę, może dodać podział tam, gdzie nakładka się kończy. 45 wierszy danych nad ogonem jest zapisywanych po jednym elemencie na wiersz, bo każdy z nich trzyma komórki

XLSX to inna historia. SpreadsheetML nie ma atrybutu powtórzenia, a element <row> opisuje dokładnie jeden wiersz, więc zapis arkusza opartego na przedziałach jako .xlsx wylicza wiersze objęte przedziałem i pisze ht oraz customHeight="1" na każdym z nich. Model pozostaje zwarty w pamięci; format docelowy decyduje, jak duży wyjdzie plik. Jeśli skoroszyt przychodzi jako ODS i ma tylko wyjść jako ODS, nigdy nie płaci tego kosztu per wiersz

Edycje, które przenumerowują wiersze

Druga reprezentacja wysokości wierszy jest poprawna tylko wtedy, gdy każda operacja przesuwająca wiersze przesuwa ją też. TXLSXWorksheet.InsertRows i DeleteRows przesuwają przedziały razem z listami per wiersz: część przedziału przed punktem edycji zostaje na miejscu, wiersze wewnątrz usuwanego okna wypadają, a reszta przesuwa się o liczbę wstawionych albo usuniętych. Przedział rozpięty nad wstawieniem staje się więc dwoma przedziałami z przerwą niewymiarowanych wierszy pomiędzy, a przedział rozpięty nad usunięciem staje się krótszy

procedure ShiftAroundTheRun(Sheet: TXLSXWorksheet);
begin
  Sheet.RowHeight[500000] := 36;
  Sheet.ClearRowHeight(500001);
  // Dwa nowe wiersze na 500000: nadpisanie idzie na 500002, dziura na 500003
  Sheet.InsertRows(500000, 2);
  Writeln(Sheet.RowHeight[500002]:0:2);  // 36.00
  Writeln(Sheet.HasRowHeight(500003));   // FALSE
  Writeln(Sheet.HasRowHeight(500000));   // FALSE: wstawione wiersze nie mają wysokości
  // Usuń je znowu: nadpisanie i dziura wracają na stare numery wierszy
  Sheet.DeleteRows(500000, 2);
  Writeln(Sheet.RowHeight[500000]:0:2);  // 36.00
  Writeln(Sheet.HasRowHeight(500001));   // FALSE
  Writeln(Sheet.UsedRange.Row2);         // zawiera koniec przesuniętego przedziału
end;

Round trip nie zostawia ogona dokładnie tam, gdzie był. Wstawienie pcha ogonowy przedział z 500 002..1 048 575 na 500 004..1 048 577, a siatka kończy się na wierszu 1 048 576, więc część za tym wierszem jest ucinana. Usunięcie przesuwa potem to, co zostało, o dwa w górę, i przedział kończy się na wierszu 1 048 574: wiersz 1 048 575 nie ma już zapisanej wysokości. Jeśli ostatnie wiersze siatki są dla ciebie ważne, ustaw ich wysokość ponownie po parze wstawienie-usunięcie. Wiersze wstawione w środek przedziału też nie podczepiają jego wysokości: otwierają przerwę niewymiarowanych wierszy i nie dziedziczą wysokości przedziału, dlatego HasRowHeight(500000) wyżej wypisało FALSE

Pozostałe operacje przesuwające wiersze podążają tym samym wzorcem. TileRangeAxisMetadata czyści przedziały na całym kafelkowanym spanie, a potem rejestruje każdy źródłowy przedział raz na kopię przy jej offsecie. TXLSXWorksheet.CopyFrom i TXLSXSheets.AddCopy dają nowemu arkuszowi własną kopię listy przedziałów, więc ClearRowHeights na klonie zostawia wysokości oryginału na miejscu. Po stronie odczytu UsedRange sięga do pierwszego i ostatniego wiersza każdego przedziału, a przejście po wierszach w writerze XLSX przechodzi przez przedziały, więc wiersze tylko z wysokością są wciąż zapisywane

Ukryte wiersze i poziomy konspektu są trzymane per wiersz

Wysokości wierszy i dziedziczone style pustych komórek to dwie rzeczy, które zwijają się w przedziały i nakładki. Flagi ukrycia, poziomy konspektu i stan zwinięcia są zapisywane raz na wiersz. Powtarzany wiersz ukryty albo siedzący wewnątrz <table:table-row-group> produkuje po jednym wpisie na powtarzany wiersz dla każdego z tych atrybutów, a przy zapisie każdy taki wpis jest własną granicą, więc ukryty odcinek jest zapisywany po jednym <table:table-row> na wiersz

W szablonach, którymi sterujesz, trzymaj ukrytą i grupowaną część arkusza tak ciasno, jak wymaga danych. Ukrycie dwudziestu wierszy pod raportem nic nie kosztuje. Ukrycie wszystkiego od wiersza 46 po dno siatki znaczy milion wpisów przy wczytaniu i milion elementów wierszy przy zapisie, a jedyna widoczna różnica to taka, że nikt nie przesunie widoku za raport

Poziom skoroszytowy formatu, w tym które funkcje ODS mapują się na które części modelu obiektowego, opisuje przewodnik po otwieraniu i zapisywaniu plików ODS, a inne dźwignie dla dużych plików są zebrane pod wydajnością dużych skoroszytów. Sam element wiersza, z atrybutami powtórzenia i stylu, jest zdefiniowany w rozdziale table ODF 1.3 Part 3

Powtórzenie rzędu miliona wierszy to szczegół formatu, który natywny czytnik musi dobrze modelować, bo nie ma pakietu biurowego w tle, który by to wchłonął. Strona komponentu arkuszkowego HotXLS dla Delphi wypisuje funkcje XLS, XLSX i ODS, które czyta i zapisuje, wraz ze wspieranymi wersjami Delphi i C++Buildera