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
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
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
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