HotXLS Delphi Component lưu một dòng ODS mang table:number-rows-repeated cùng chiều cao dòng thành một record TXLSXRowHeightRun duy nhất — dòng đầu, dòng cuối, một chiều cao — thay vì một entry chiều cao cho mỗi dòng lặp, và gộp style của các ô trống mà những dòng đó kế thừa vào một interval style overlay duy nhất. Đó là toàn bộ lý do HotXLS 2.382.2 mở một bảng tính có phần đuôi lặp 1.048.530 dòng trống trong 0,02 giây trong khi 2.382.1 bị timeout, và cũng là lý do cùng tệp đó được lưu ngược về ODS với số lần lặp còn nguyên thay vì thành một triệu dòng theo nghĩa đen
Tệp đang nói tới thì bình thường. LibreOffice Calc ghi một sheet mười bốn cột với 45 dòng dữ liệu, rồi mô tả mọi thứ bên dưới chúng bằng đúng một phần tử: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Style ro1 đặt style:row-height="0.452cm", và mỗi <table:table-column> mang một table:default-cell-style-name mà mọi ô trống trong run đều kế thừa. Cả content.xml nặng 103 KB. Không có gì trong tệp nói rằng nó “đắt”; phần đắt hoàn toàn là của chúng tôi
Vì sao một dòng lặp làm timeout một lần import ODS?
Vì bộ import từng mở rộng nó ra. Ở 2.382.1, phần kết thúc dòng lặp SetRowHeight(RowIndex + i, RowHeight) một lần cho mỗi dòng lặp, ghi từng chiều cao vào một string list Name=Value khóa theo số dòng. Mỗi lần chèn vào list đó lại chạy một phép tra IndexOfName trên mọi thứ đã có trong đó, nên một triệu chiều cao tốn một triệu lượt quét tuyến tính — đúng kiểu tìm kiếm bậc hai trên list mà HXLS-005 được mở để xử lý. Cùng lúc đó OdsCommitRow vật chất hóa một cell object cho mọi cột kế thừa style, trên mỗi dòng lặp, vì một ô trống có style vẫn bị tính là một ô
Phía lưu cũng có phiên bản vấn đề của riêng nó. Tệp LibreOffice kết thúc bằng một dòng ro1 nữa sau lần lặp lớn, nên dòng có style cao nhất nằm tận đáy sheet, và OdsBuildTableXml duyệt từng dòng cho tới đó và phát ra các phần tử <table:table-row> mỗi lần một cái. Kể cả một workbook được import rẻ cũng sẽ ghi đắt. Vá phần import mà không vá phần export thì chỉ dời chỗ timeout đi chứ không xóa nó
Row-height run trong HotXLS là gì?
Một run là thứ nhỏ nhất có thể mô tả “các dòng 46 tới 1.048.575 đều cao 12,81 point” mà không phải nói điều đó 1.048.530 lần. TXLSXRowHeightRun là một record gồm FirstRow, LastRow và Height; TXLSXRowHeightRuns là một mảng động của chúng, và mỗi TXLSXWorksheet giữ một mảng trong FRowHeightRuns bên cạnh danh sách chiều cao theo từng dòng đã có. Khi import ODS, phần kết thúc dòng giờ rẽ nhánh theo số lần lặp: số lần bằng 1 vẫn gọi SetRowHeight, còn lớn hơn thì gọi XlsxAssignRowHeightRun một lần cho cả dải. Dải được kẹp vào XlsxMaxRow, tức 1.048.576, nên một số lần lặp vượt quá sheet sẽ bị cắt bớt chứ không bị từ chối
XlsxAssignRowHeightRun là writer duy nhất của mảng này, và nó giữ các run rời nhau ngay từ cách dựng. Nhận một khoảng mới, nó chép mọi run hiện có nằm hoàn toàn ngoài khoảng đó, cắt bất kỳ run nào chồng lên thành phần trước và phần sau, rồi nối khoảng mới vào khi Present là true — hoặc không nối gì khi Present là false, và đó là cách ClearRowHeight đục một lỗ một dòng. Hai hệ quả đi kèm. Mảng không bao giờ chứa các khoảng chồng nhau, nên một phép tra có thể dừng ngay ở kết quả đầu tiên. Và mảng không bao giờ bị sửa tại chỗ; một bản sao mới được dựng ở mọi lần gọi, tốn không đáng kể ở các kích thước liên quan mà lại xóa sạch cả một lớp bug aliasing
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// Một sheet có dòng đuôi lặp 1.048.530 lần dưới một row style
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// Cả hai lần đọc đều phân giải qua cùng một run; không có gì bị mở rộng
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Một override một dòng che run mà không cắt nó
Sheet.RowHeight[500000] := 36;
// Xóa một dòng bên trong run sẽ cắt run thành hai mảnh
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // vẫn là chiều cao của run
finally
Workbook.Free;
end;
end;
Thứ tự tra cứu là phần đáng nhớ. TXLSXWorksheet.GetRowHeight kiểm tra danh sách theo dòng trước và chỉ hỏi các run khi dòng đó không có entry tường minh, và HasRowHeight cũng vậy. Nên Sheet.RowHeight[500000] := 36 không hề đụng tới run — nó thêm một entry vào danh sách theo dòng, và entry đó thắng vì được tra trước. ClearRowHeight thì ngược lại: nó bỏ mọi entry theo dòng rồi gọi XlsxAssignRowHeightRun với Present = False, vì một dòng đã xóa phải đọc ra là “không có chiều cao” kể cả khi một run phủ lên nó. ClearRowHeights làm rỗng cả hai cấu trúc cùng lúc
Các style ô trống được kế thừa đi đâu?
Vào một interval style overlay cho mỗi cột, chứ không vào các cell object. OdsCommitRow quyết định theo từng cột xem đó có phải một ô trống gọn: dòng lặp nhiều hơn một lần, ô không có giá trị, không có công thức và không có rich text. Với một ô trống gọn, nó chỉ tạo một ô thật ở dòng đầu của run, áp style kế thừa vào đó, rồi đăng ký sáu style index y hệt — font, fill, border, number format, alignment, protection — dưới dạng một StyleOverlays.Add phủ từ dòng thứ hai tới hết run ở cột đó. Các dòng sau dòng đầu bị bỏ qua hoàn toàn trong vòng lặp vật chất hóa
Bài regression làm hình dạng đó cụ thể. Sau khi mở một sheet có dòng thứ hai lặp 1.048.575 lần dưới một default style cột in đậm, Sheet.Cells.Count được assert là dưới 10, và Sheet.Cells[700000, 1].FontIndex vẫn phân giải ra font đậm — overlay cung cấp style ngay khi tọa độ đó bị chạm tới. Đây cũng chính là cơ chế giữ cho một cột có định dạng nhưng rỗng không tốn một triệu ô ở phía XLSX; bài về lưu trữ ô theo row-block và interval style overlay trình bày cách các overlay xếp lớp và phân giải. Điểm mới ở đây là bộ import ODS tự tạo chúng, từ số lần lặp, chứ không chờ ứng dụng định dạng một vùng
SaveAsODS ghi số lần lặp trở lại như thế nào?
Bằng cách chỉ cắt phần đuôi rỗng của sheet ở những chỗ thật sự có thay đổi. OdsBuildTableXml giờ theo dõi hai biên: contentMaxRow, dòng cuối cùng chứa giá trị, công thức, hyperlink hay ngắt dòng thủ công, và maxRow, biên này còn kéo dài qua các ô trống chỉ có style, các chiều cao một dòng, LastRow của mọi run và mép dưới của mọi overlay. Một ô trống chỉ có style không còn bị tính là nội dung — TXLSXCells.IsStyleOnlyBlank là thứ loại nó ra — nên dòng có style nằm ở cuối tệp LibreOffice không còn kéo biên nội dung xuống tận đáy sheet
Phía trên contentMaxRow, các dòng vẫn được ghi từng cái một y như trước. Phía dưới, writer tính nextRow là giá trị nhỏ nhất trong: FirstRow của run kế tiếp, LastRow + 1 của run hiện tại, entry chiều cao một dòng kế tiếp, mép overlay kế tiếp, và ô được vật chất hóa kế tiếp. Toàn bộ từ dòng hiện tại tới nextRow - 1 sau đó được phát ra thành một <table:table-row> với table:number-rows-repeated đặt bằng hiệu số, mang mỗi cột một <table:table-cell/> cùng tên style đã phân giải qua overlay khi có overlay phủ cột đó. Bản thân row style đến từ TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), thứ giờ gộp cả đoạn text chiều cao — ví dụ 12.81pt — vào key khử trùng lặp của nó bên cạnh cờ hidden và page-break, nên mọi dòng trong run dùng chung một style ro<N> với đúng một thuộc tính style:row-height
var
Workbook, Reopened: TXLSXWorkbook;
Saved: TMemoryStream;
begin
Workbook := TXLSXWorkbook.Create;
Reopened := TXLSXWorkbook.Create;
Saved := TMemoryStream.Create;
try
Workbook.OpenODS('conditional-formatting.ods');
Workbook.Sheets[1].RowHeight[500000] := 36;
Workbook.Sheets[1].ClearRowHeight(500001);
// Phần đuôi rỗng được ghi thành một vài dòng lặp, không phải một triệu
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// Override, lỗ đục và run đều sống sót qua vòng 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); // chiều cao của run
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Bài test ghim hành vi này assert rằng stream đã lưu dưới 64 KB với một sheet có height run trải 1.048.575 dòng kèm một override và một lỗ đục ở giữa. Hai ranh giới trung thực cần nói ngay bên cạnh con số đó. Thứ nhất, một worksheet có bất kỳ data validation nào sẽ đặt contentMaxRow bằng maxRow, nên validation tắt việc nén đuôi trên sheet đó và nó lại được ghi từng dòng. Thứ hai, XLSX không có attribute repeat — một <row> của SpreadsheetML mô tả đúng một dòng — nên export một sheet dựa trên run sang .xlsx sẽ liệt kê các dòng mà run phủ và ghi một attribute ht trên mỗi dòng. Model vẫn gọn trong bộ nhớ; định dạng tệp quyết định tệp trông thế nào
Mọi thao tác đánh số lại dòng giờ nợ các run điều gì?
Việc bảo trì. Một cách biểu diễn mới cho metadata dòng chỉ đúng nếu mọi thao tác làm đổi số dòng đều di chuyển nó cùng các danh sách theo dòng nằm cạnh, và commit này chạm vào từng thao tác như vậy. InsertRows và DeleteRows đi qua XlsxShiftRowHeightRuns, thứ dựng lại mảng bằng cách giữ phần của mỗi run nằm trước điểm sửa, bỏ phần rơi vào cửa sổ xóa, và thêm lại phần còn lại đã dịch theo delta — nên một run vắt qua điểm chèn biến thành hai run có khe hở, còn một run vắt qua vùng xóa thì co lại. TileRangeAxisMetadata xóa các run trên cả dải được tile, rồi đăng ký lại từng run nguồn một lần cho mỗi bản sao tại offset của nó. TXLSXWorksheet.CopyFrom và TXLSXSheets.AddCopy lấy một Copy() của mảng chứ không gán thẳng, và đó là lý do bài test có thể xóa sạch chiều cao trên một bản clone mà vẫn thấy sheet gốc nguyên vẹn ở dòng 1.048.576
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Chèn hai dòng tại 500000: override dời sang 500002, lỗ đục sang 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// Xóa chúng lần nữa: mọi thứ dịch về như cũ
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// Tile các dòng 2..4 hai lần xuống dưới sheet; chiều cao của run đi theo mỗi bản sao
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // chiều cao của run
end;
Các biên ở phía đọc cũng có nghĩa vụ tương tự. GetUsedRange nâng mép dưới của nó tới FirstRow và LastRow của mỗi run, còn BuildRowMajorCellOrder kéo dòng tối đa tính cả metadata của nó qua mọi run để writer XLSX vẫn ghé qua những dòng chỉ có chiều cao. Nếu bạn có thêm một cấu trúc khóa theo dòng của riêng mình trên object model HotXLS, đây là danh sách kiểm tra: chèn, xóa, tile, copy, used range, và mọi serializer. Bỏ sót một cái là lỗi âm thầm — chiều cao lệch đi đúng bằng số dòng chèn, và không có gì ném exception cả
Cái gì vẫn theo từng dòng, và các con số giờ ra sao
Cờ hidden, outline level và trạng thái collapsed vẫn bị mở rộng. Phần kết thúc dòng lặp SetRowHidden và SetRowOutlineLevel một lần cho mỗi dòng lặp, nên một sheet ẩn cả phần đuôi một triệu dòng, hay lồng nó trong một table:table-row-group, vẫn trả một entry cho mỗi dòng cho từng attribute đó. Thay đổi ở 2.382.2 chỉ nhắm vào đúng hai thứ mà HXLS-005 thật sự đo — chiều cao và style ô trống kế thừa — và cùng kỹ thuật run đó sẽ áp được cho các thứ khác nếu có tệp nào đòi hỏi. Reader ODS cũng không hành động theo style:use-optimal-row-height; một row style nói “optimal” mà lại cho kèm chiều cao thì được import với chính chiều cao đó
So với corpus, conditional-formatting.ods giờ hoàn tất chu trình mở, assert, lưu, mở lại và assert lại trong 0,178 giây trên Win32 và 0,158 giây trên Win64, với riêng giai đoạn mở là 0,020 giây, nằm gọn trong ngân sách 60 giây mà trước đây nó từng đốt hết. Các interface cấp workbook mà định dạng này đi qua được mô tả trong bài hướng dẫn mở và lưu tệp ODS, còn tập đòn bẩy rộng hơn cho tệp lớn nằm ở hiệu năng workbook lớn; bản thân phần tử row của ODF, với các attribute repeat và style của nó, được quy định ở ODF 1.3 Phần 3 §9.1.4
HotXLS đọc và ghi XLS, XLSX cùng ODS từ code Delphi và C++Builder thuần, không cần cài Excel hay LibreOffice, và đó là lý do một dòng lặp hàng triệu lần là thứ thư viện phải model cho tử tế chứ không thể giao cho một tiến trình bên ngoài — trang component bảng tính HotXLS cho Delphi liệt kê các định dạng hỗ trợ và phiên bản RAD Studio