کامپوننت HotXLS Delphi یک سطر ODS را که table:number-rows-repeated و یک ارتفاع سطر دارد بهصورت یک رکورد تکی TXLSXRowHeightRun ذخیره میکند — سطر اول، سطر آخر، یک ارتفاع — نه یک entry ارتفاع به ازای هر سطر تکراری، و استایل سلولهای خالی که آن سطرها ارث میبرند را در یک overlay استایل بازهای میریزد. همین کل دلیل این است که HotXLS 2.382.2 صفحهگستردهای را که دنبالهاش 1,048,530 سطر خالی را تکرار میکند در 0.02 ثانیه باز میکند جایی که 2.382.1 time-out میشد، و دلیل اینکه همان فایل با شمارش تکرار سالم به ODS برمیگردد نه بهصورت یک میلیون سطر لفظی
فایل مورد بحث معمولی است. یک sheet چهاردهستونی با 45 سطر داده در LibreOffice Calc نوشته میشود، بعد همهچیز زیرشان را با یک عنصر توصیف میکند: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. استایل ro1 مقدار style:row-height="0.452cm" را تعیین میکند، و هر <table:table-column> یک table:default-cell-style-name حمل میکند که هر سلول خالی در آن run آن را ارث میبرد. کل content.xml 103 کیلوبایت است. هیچچیز در فایل نمیگوید «گران»؛ گرانی تمامش مال ما بود
چرا یک سطر تکراری، import مربوط به ODS را time-out میکند؟
چون importer قبلاً آن را باز میکرد. در 2.382.1 حلقهٔ پایانی سطر SetRowHeight(RowIndex + i, RowHeight) را یک بار به ازای هر سطر تکراری میچرخاند و هر ارتفاع را در یک لیست رشتهٔ Name=Value کلیدشده با شمارهٔ سطر مینوشت. هر درج در آن لیست یک جستوجوی IndexOfName روی همهٔ چیزهایی که از قبل در آن بود اجرا میکرد، پس یک میلیون ارتفاع یک میلیون اسکن خطی هزینه داشت — همان جستوجوی درجه-دو در لیست که HXLS-005 علیهاش ثبت شد. همزمان OdsCommitRow برای هر ستونی که استایلی را ارث میبرد، روی هر یک از آن سطرهای تکراری، یک شیء سلول مادی میکرد، چون یک سلول خالی استایلدار باز هم سلول شمرده میشد
سمت save نسخهٔ خودش از این مشکل را داشت. فایل LibreOffice بعد از آن تکرار بزرگ با یک سطر ro1 دیگر تمام میشود، پس بالاترین سطر استایلدار درست در ته sheet نشسته بود، و OdsBuildTableXml هر سطر تا آن را میپیمود و عنصرهای <table:table-row> را یکییکی بیرون میداد. حتی workbookی که ارزان import شده بود هم گران مینوشت. fix کردن import بدون fix کردن export فقط time-out را جابهجا میکرد، نه حذف
run ارتفاع سطر در HotXLS چیست؟
یک run کوچکترین چیزی است که میتواند «سطرهای 46 تا 1,048,575 همه 12.81 پوینت ارتفاع دارند» را بدون گفتنش 1,048,530 بار توصیف کند. TXLSXRowHeightRun یک رکورد از FirstRow و LastRow و Height است؛ TXLSXRowHeightRuns یک آرایهٔ دینامیک از آنهاست، و هر TXLSXWorksheet یکی را در FRowHeightRuns کنار فهرست ارتفاع هر-سطر موجود نگه میدارد. هنگام import در ODS، حلقهٔ پایانی سطر حالا بر اساس شمارش تکرار شاخه میزند: شمارش 1 باز هم SetRowHeight را صدا میزند، هر چیزی بزرگتر یک بار برای کل بازه XlsxAssignRowHeightRun را. بازه به XlsxMaxRow که 1,048,576 است کلمپ میشود، پس شمارش تکراری که از sheet رد شود بریده میشود نه رد
XlsxAssignRowHeightRun تنها نویسندهٔ این آرایه است، و runها را طبق ساختار ناهمپوشان نگه میدارد. با گرفتن یک بازهٔ جدید، هر run موجود که کاملاً بیرون آن باشد را کپی میکند، هر runی که با آن همپوشانی داشته باشد را به قطعهٔ قبل و قطعهٔ بعد میشکند، و بعد بازهٔ جدید را وقتی Present درست است ضمیمه میکند — یا وقتی Present نادرست است هیچچیز ضمیمه نمیکند، که همان روشی است که ClearRowHeight با آن یک حفرهٔ تکسطری سوراخ میکند. دو چیز نتیجه میشود. آرایه هرگز بازههای همپوشان ندارد، پس یک lookup میتواند در همان اولین برخورد بایستد. و آرایه هرگز در جا دستکاری نمیشود؛ در هر فراخوانی یک کپی تازه ساخته میشود، که در این اندازهها چیزی هزینه ندارد و یک کلاس کامل از باگهای aliasing را حذف میکند
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// sheetی که سطر دنبالهاش 1,048,530 بار زیر یک استایل سطر تکرار میشود
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// هر دو خواندن از همان run حل میشوند؛ هیچچیز باز نشده
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// یک override تکسطری run را سایه میاندازد بدون شکستنش
Sheet.RowHeight[500000] := 36;
// پاک کردن یک سطر داخل run آن را به دو قطعه میبرد
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // همچنان ارتفاع run
finally
Workbook.Free;
end;
end;
ترتیب lookup همان بخشی است که ارزش بهخاطر سپردن دارد. TXLSXWorksheet.GetRowHeight اول فهرست هر-سطر را بررسی میکند و runها را فقط وقتی مشورت میکند که سطر هیچ entry صریحی نداشته باشد، و HasRowHeight هم همین کار را میکند. پس Sheet.RowHeight[500000] := 36 اصلاً به run دست نمیزند — یک entry به فهرست هر-سطر اضافه میکند و آن entry برنده است چون اول lookup میشود. ClearRowHeight برعکس است: هر entry هر-سطر را برمیدارد و بعد XlsxAssignRowHeightRun را با Present = False صدا میزند، چون یک سطر پاکشده باید «بدون ارتفاع» خوانده شود حتی اگر یک run پوششش بدهد. ClearRowHeights هر دو ساختار را یکجا خالی میکند
استایلهای سلول خالی ارثبرده کجا میروند؟
به یک overlay استایل بازهای به ازای هر ستون، نه به شیءهای سلول. OdsCommitRow برای هر مقدار ستون تصمیم میگیرد که آیا یک خالی فشرده است: سطر بیش از یک بار تکرار میشود، سلول هیچ مقدار و هیچ فرمول و هیچ rich textی ندارد. برای یک خالی فشرده فقط روی اولین سطر آن run یک سلول واقعی میسازد، استایل ارثبرده را رویش اعمال میکند، و بعد همان شش اندیس استایل — font و fill و border و number format و alignment و protection — را بهصورت یک StyleOverlays.Add ثبت میکند که سطرهای دو تا انتهای run در آن ستون را پوشش میدهد. سطرهای بعد از اولی در حلقهٔ مادیسازی کاملاً رد میشوند
تست رگرسیون این شکل را عینی میکند. بعد از باز کردن sheetی که سطر دومش 1,048,575 بار زیر یک استایل پیشفرض ستون توپر تکرار میشود، تأکید میشود که Sheet.Cells.Count زیر 10 است، و Sheet.Cells[700000, 1].FontIndex باز هم به فونت توپر میرسد — overlay همان لحظه که آن مختصات لمس شود استایل را تأمین میکند. این همان مکانیزمی است که در سمت XLSX نمیگذارد یک ستون قالببندیشده ولی خالی یک میلیون سلول هزینه بدهد؛ یادداشت ذخیرهسازی سلول در بلوکهای سطر و overlayهای استایل بازهای پوشش میدهد که overlayها چطور لایه میگیرند و حل میشوند. چیزی که اینجا جدید است این است که importer مربوط به ODS آنها را خودش از روی شمارش تکرار میسازد، بهجای اینکه منتظر بماند برنامهای یک بازه را قالببندی کند
SaveAsODS شمارش تکرار را چطور دوباره مینویسد؟
با شکستن دنبالهٔ خالی sheet فقط جایی که واقعاً چیزی عوض میشود. OdsBuildTableXml حالا دو مرز را ردیابی میکند: contentMaxRow، یعنی آخرین سطری که یک مقدار یا فرمول یا هایپرلینک یا شکست دستی سطر را در خود دارد، و maxRow که علاوه بر آن از سلولهای خالیِ فقط-استایل و ارتفاعهای هر-سطر و LastRow هر run و لبهٔ پایینی هر overlay هم میگذرد. یک سلول خالیِ فقط-استایل دیگر محتوا شمرده نمیشود — TXLSXCells.IsStyleOnlyBlank همان چیزی است که مستثنیاش میکند — پس آن سطر استایلدار انتهایی در فایل LibreOffice از کشیدن مرز محتوا به ته sheet دست برمیدارد
بالای contentMaxRow سطرها دقیقاً مثل قبل یکییکی نوشته میشوند. پایینش، نویسنده nextRow را بهعنوان کوچکترینِ اینها حساب میکند: FirstRow run بعدی، LastRow + 1 run جاری، entry ارتفاع تکسطری بعدی، لبهٔ overlay بعدی، و سلول مادیسازیشدهٔ بعدی. بعد همهچیز از سطر جاری تا nextRow - 1 بهصورت یک <table:table-row> با table:number-rows-repeated برابر اختلاف بیرون داده میشود، همراه با یک <table:table-cell/> به ازای هر ستون با نام استایلی که overlay حلش کرده، وقتی overlay آن ستون را پوشش بدهد. خود استایل سطر از TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec) میآید، که حالا متن ارتفاع — مثلاً 12.81pt — را کنار پرچمهای hidden و page-break در کلید حذف تکراریاش میریزد، پس هر سطر در آن run یک استایل ro<N> با یک پراپرتی تکی 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);
// دنبالهٔ خالی بهصورت مشتی سطر تکراری نوشته میشود، نه یک میلیون
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// override و حفره و run همه از رفتوبرگشت جان سالم میبرند
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); // ارتفاع run
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
تستی که این را تثبیت میکند تأکید میکند استریم ذخیرهشده برای sheetی که run ارتفاعش 1,048,575 سطر را پوشش میدهد و یک override و یک حفره در وسطش دارد زیر 64 کیلوبایت است. دو مرز صادقانه کنار آن عدد جا دارند. اول، یک کاربرگ با هر data validationی contentMaxRow را روی maxRow میگذارد، پس validationها فشردهسازی دنباله را در آن sheet غیرفعال میکنند و دوباره سطر به سطر نوشته میشود. دوم، XLSX هیچ اتریبیوت تکرار ندارد — یک <row> در SpreadsheetML یک سطر را توصیف میکند — پس export کردن یک sheet متکی به run به .xlsx سطرهایی را که run پوشش میدهد میشمارد و روی هر کدام یک اتریبیوت ht مینویسد. مدل در حافظه فشرده میماند؛ فرمت فایل تعیین میکند فایل چه شکلی باشد
حالا هر ویرایشی که سطرها را از نو شماره میکند به runها چه بدهکار است؟
نگهداری. یک بازنمایی جدید از متادیتای سطر فقط وقتی درست است که هر عملیاتی که شمارهٔ سطرها را عوض میکند آن را همراه فهرستهای هر-سطری که کنارشان نشسته جابهجا کند، و این commit هر کدام از آن عملیات را دست میزند. InsertRows و DeleteRows از XlsxShiftRowHeightRuns رد میشوند، که آرایه را از نو میسازد با نگه داشتن آن بخشی از هر run که پیش از نقطهٔ ویرایش میافتد، انداختن هر چیزی که داخل یک پنجرهٔ حذف بیفتد، و دوباره اضافه کردن باقیمانده با جابهجایی به اندازهٔ دلتا — پس runی که روی یک درج سوار شده به دو run با یک شکاف تبدیل میشود، و runی که روی یک حذف سوار شده کوچک میشود. TileRangeAxisMetadata runها را در کل بازهٔ کاشیکاریشده پاک میکند و بعد هر run مبدأ را یک بار به ازای هر کپی در آفستش از نو ثبت میکند. TXLSXWorksheet.CopyFrom و TXLSXSheets.AddCopy یک Copy() از آرایه میگیرند نه اینکه انتسابش بدهند، و همین دلیل است که تست میتواند همهٔ ارتفاعها را روی یک کلون پاک کند و باز هم sheet اصلی را در سطر 1,048,576 سالم پیدا کند
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// درج دو سطر در 500000: override به 500002 و حفره به 500003 جابهجا میشوند
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// دوباره حذفشان کن: همهچیز برمیگردد
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// سطرهای 2..4 را دو بار در sheet کاشی کن؛ ارتفاعهای run هر کپی را دنبال میکنند
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // ارتفاع run
end;
مرزهای سمت خواندن همین وظیفه را دارند. GetUsedRange لبهٔ پایینیاش را به FirstRow و LastRow هر run میرساند، و BuildRowMajorCellOrder حداکثر سطر شامل-متادیتایش را از میان هر run گسترش میدهد تا نویسندهٔ XLSX باز هم از سطرهای فقط-ارتفاع دیدن کند. اگر روزی ساختار سطر-کلید خودت را روی مدل شیء HotXLS اضافه کنی، این چکلیست است: insert و delete و tile و copy و used range و هر سریالایزر. یکی را از دست بده و شکست بیصدا است — ارتفاعها به اندازهٔ تعداد درج لغزیدهاند و هیچچیز استثنا نمیدهد
چه چیزی هر-سطر میماند، و اعداد حالا چه شکلیاند
پرچمهای hidden و سطوح outline و حالت collapsed باز هم باز میشوند. حلقهٔ پایانی سطر SetRowHidden و SetRowOutlineLevel را یک بار به ازای هر سطر تکراری میچرخاند، پس sheetی که یک دنبالهٔ میلیونسطری را پنهان کند، یا آن را داخل یک table:table-row-group تودرتو بگذارد، برای هر کدام از آن اتریبیوتها یک entry هر-سطر میپردازد. تغییر 2.382.2 محدود به همان دو چیزی است که HXLS-005 واقعاً اندازه گرفت — ارتفاعها و استایلهای خالی ارثبرده — و همان تکنیک run اگر روزی فایلی طلبش کند به بقیه هم اعمال میشود. reader مربوط به ODS همچنین به style:use-optimal-row-height عمل نمیکند؛ یک استایل سطر که میگوید «optimal» و یک ارتفاع هم میدهد با همان ارتفاع import میشود
در برابر corpus، conditional-formatting.ods حالا چرخهٔ open و assert و save و reopen و re-assert را در 0.178 ثانیه روی Win32 و 0.158 ثانیه روی Win64 تمام میکند، با خود مرحلهٔ open در 0.020 ثانیه، داخل بودجهای 60 ثانیهای که قبلاً تهش میکرد. رابطهای سطح-workbook که فرمت از آنها رد میشود در قدمبهقدمِ باز کردن و ذخیرهٔ فایلهای ODS توصیف شدهاند، و مجموعهٔ گستردهتر اهرمها برای فایلهای بزرگ در کارایی workbookهای بزرگ؛ خود عنصر سطر در ODF با اتریبیوتهای repeat و styleش در ODF 1.3 Part 3 §9.1.4 مشخص شده
HotXLS فایلهای XLS و XLSX و ODS را از کد بومی Delphi و C++Builder بدون نصب اکسل یا LibreOffice میخواند و مینویسد، و همین دلیل این است که یک تکرار میلیونسطری چیزی است که کتابخانه باید خوب مدل کند نه اینکه به یک فرایند بیرونی بسپارد — صفحهٔ کامپوننت صفحهگستردهٔ HotXLS برای Delphi فرمتهای پشتیبانیشده و نسخههای RAD Studio را فهرست میکند