مقاله فنی

سطرهای تکراری ODS به‌صورت run ارتفاع در HotXLS برای Delphi

کامپوننت 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 کیلوبایت است. هیچ‌چیز در فایل نمی‌گوید «گران»؛ گرانی تمامش مال ما بود

چگونه HotXLS یک سطر تکراری ODS را به حالت فشرده تبدیل می‌کند: عنصر content.xml با table:number-rows-repeated برابر 1048530 و استایل ro1 به یک رکورد تکی TXLSXRowHeightRun که سطرهای 46 تا 1048575 را در 12.81 pt پوشش می‌دهد به‌علاوهٔ یک entry در StyleOverlays به ازای هر ستون نگاشت می‌شود، در حالی که نسخهٔ 2.382.1 همان عنصر را به یک میلیون entry SetRowHeight و شیء سلول باز می‌کرد
شمارش تکرار و ارتفاع سطر ro1 و استایل‌های پیش‌فرض ستون‌ها هر سطر خالی زیر سطر 45 را توصیف می‌کنند، پس importer می‌تواند بدون دست زدن به یک میلیون مختصات، یک رکورد run و overlayهای هر-ستون بسازد

چرا یک سطر تکراری، 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 هر دو ساختار را یک‌جا خالی می‌کند

جراحی run ارتفاع سطر در HotXLS: بعد از OpenODS یک run سطرهای 46 تا 1048575 را در 12.81 pt پوشش می‌دهد در حالی که یک entry هر-سطر سطر 500000 را روی 36 pt می‌گذارد و lookup را می‌برد چون GetRowHeight اول فهرست هر-سطر را بررسی می‌کند، و ClearRowHeight روی سطر 500001 آن run را به دو قطعهٔ ناهم‌پوشان دور حفره می‌شکند
XlsxAssignRowHeightRun قطعه‌های بیرون بازهٔ پاک‌شده را کپی می‌کند و برای خود بازه هیچ‌چیز ضمیمه نمی‌کند، پس runها طبق ساختار ناهم‌پوشان می‌مانند و یک lookup می‌تواند در همان اولین برخورد بایستد، در حالی که override روی سطر 500000 دست‌نخورده می‌ماند

استایل‌های سلول خالی ارث‌برده کجا می‌روند؟

به یک 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 شریک می‌شود

آنچه SaveAsODS برای یک sheet متکی به run می‌نویسد: contentMaxRow در سطر 45 که مقدارها تمام می‌شوند می‌ایستد در حالی که maxRow از میان run ارتفاع و overrideهایش می‌گذرد، سطرهای بالای مرز یکی‌یکی نوشته می‌شوند، و دنباله به‌صورت عنصرهای تکراری table-row بیرون داده می‌شود که استایل سطرشان از RowStyleFor می‌آید و استایل سلول‌هایشان از طریق overlayها حل می‌شود
هر عنصر تکراری یک کشش یکنواخت را پوشش می‌دهد و در لبهٔ run بعدی، entry ارتفاع، لبهٔ overlay یا سلول مادی‌سازی‌شدهٔ بعدی می‌ایستد، پس یک sheet بدون validation به‌صورت مشتی عنصر save می‌شود در حالی که validationها یا یک export به XLSX به ازای هر سطر هزینه می‌دهند
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 را فهرست می‌کند