مقاله فنی

کپی بین‌کاربرگی و اتصال مجدد فرمول در HotXLS در Delphi

متد AddCopy در HotXLS یک برگ‌کاری را از یک کاربرگ Excel به کاربرگ دیگری کپی می‌کند با decompile‌کردن هر فرمول روی آن برگه به متن به‌سبک A1 و دوباره کامپایل‌کردن آن متن درون کاربرگ مقصد، به‌جای کپی‌کردن مستقیم درخت فرمول کامپایل‌شده، چون ارجاع‌های سری داده‌ی چارت، شاخص‌های فونت متن غنی، و شماره‌گذاری لینک خارجی همگی به‌طور مستقل درون هر فایل کاربرگی تخصیص داده می‌شوند

این شکست دقیقاً در همان کاربرگی نمایان می‌شود که انتظار دارید: یک job پایان-ماه که یک برگه را از گزارش هر شعبه بیرون می‌کشد و آن را به یک فایل خلاصه پیوست می‌کند. نتیجه را باز کنید و یک چارت جمع‌فرعی اعداد یک شعبه‌ی کاملاً متفاوت را رسم می‌کند، یادداشتی که در منبع پررنگ و قرمز بود دوباره متن ساده‌ی سیاه است، و فرمولی که زمانی یک نرخ مالیات را از یک کاربرگ جستجوی همراه می‌کشید حالا یک عدد یخ‌زده نشان می‌دهد که هیچ‌کس نمی‌تواند توضیح دهد. هیچ چیزی اینجا یک استثنا throw نمی‌کند — فایل باز می‌شود، اعداد باورپذیر به‌نظر می‌رسند، و خرابی همان‌جا می‌نشیند تا کسی یک چارت با عنوان اشتباه که کنارش نشسته را متوجه شود

چرا AddCopy نمی‌تواند فقط درخت فرمول کامپایل‌شده را کپی کند؟

AddCopy نمی‌تواند درخت فرمول کامپایل‌شده را بدون تغییر جابه‌جا کند، چون یک فرمول BIFF کامپایل‌شده متن مستقل نیست — یک دنباله از توکن‌هاست، و چندین تا از آن توکن‌ها اعداد صحیح کوچکی هستند که فقط درون کاربرگی که آن‌ها را تولید کرده به‌درستی حل می‌شوند. یک ارجاع 3D مثل Sheet2!A1:A10 پس از کامپایل‌شدن نام لفظی Sheet2 را حمل نمی‌کند؛ یک فیلد را حمل می‌کند که مشخصات BIFF آن را ixti می‌نامد (HotXLS همان مقدار را در درخت کامپایل‌شده‌ی خودش زیر نام فیلد FExternID نگه می‌دارد)، یک اندیس درون جدول EXTERNSHEET خصوصی آن کاربرگ، شماره‌گذاری‌شده به هر ترتیبی که آن کاربرگ خاص اتفاقاً برگه‌ها و کتاب‌های خارجی‌اش را ثبت کرده. توکن را بدون تغییر به درون کاربرگی منتقل کنید که جدول EXTERNSHEETاش به ترتیبی متفاوت ساخته شده و اندیس ۳ دیگر به‌معنای Sheet2 نیست — به‌معنای هر برگه‌ای است که اتفاقاً اسلات ۳ را آنجا اشغال می‌کند، و اکسل هیچ راهی برای پرچم‌گذاری این اشتباه ندارد، چون تا آنجا که به قالب فایل مربوط می‌شود، فرمول کاملاً درست‌شکل است. این دقیقاً همان شکستی است که TXLSWorksheets.AddCopy برای اجتناب از آن وجود دارد: فراخوانی‌شده از مجموعه برگه‌ی خودِ هرکدام از کاربرگ‌ها در کد Delphi یا C++Builder، این یک برگ‌کاری — مقادیر سلول، قالب‌ها، فرمول‌ها، چارت‌ها، نظرات، ادغام‌ها، تنظیم صفحه، و بیشتر — را از یک کاربرگ منبع که ممکن است همان کاربرگی که آن را روی آن فراخوانی می‌کنید باشد یا نباشد کپی می‌کند، و نتیجه را به مقصد زیر نامی که انتخاب می‌کنید یا یک کپی ابهام‌زدایی‌شده از اصل پیوست می‌کند

var
  Summary, Branch: IXLSWorkbook;   // interface-counted: do not Free
begin
  Summary := TXLSWorkbook.Create;
  Branch := TXLSWorkbook.Create;
  Branch.Open('branch-east.xls');

  // Appends a copy of Branch's first sheet onto Summary, renamed to
  // stay unique inside the destination workbook
  Summary.Sheets.AddCopy(Branch.Sheets[1], 'East Detail');
  Summary.SaveAs('consolidated.xls');
end;

راه‌حل: decompile به متن، دوباره کامپایل در مقصد

HotXLS مسئله‌ی اندیس‌گذاری را با اینکه هرگز اجازه ندهد خودِ درخت کامپایل‌شده از مرز کاربرگ عبور کند حل می‌کند. برای هر سلول فرمول در یک کپی بین‌کاربرگی، AddCopy فرمول منبع را به همان متن به‌سبک A1ای که یک کاربر در نوار فرمول اکسل می‌دید decompile می‌کند، سپس آن متن را به کاربرگ مقصد می‌سپارد، که آن را از صفر با استفاده از جداول خودش دوباره به یک درخت تجزیه می‌کند — یک ارجاع واجد-شرایط-با-برگه مثل Data!D2:D100 در آن نقطه صرفاً یک رشته است، و یک رشته در هر کاربرگی همان معنا را دارد، پس اگر مقصد از پیش برگه‌ای به نام Data داشته باشد، ارجاع به‌درستی بدون هیچ ترجمه‌ی اندیسی اصلاً حل می‌شود، چون هرگز اندیس خامی در پرواز نبوده که ترجمه شود. HotXLS فقط زمانی هزینه‌ی این رفت‌وبرگشت را می‌پردازد که مجبور باشد: کپی‌کردن یک برگه درون همان کاربرگ مسیر ارزان‌تری را می‌گیرد که در آن درخت کامپایل‌شده صرفاً در حافظه شبیه‌سازی می‌شود، چون هر اندیسی درونش از پیش معتبر است جایی که می‌ماند، و انحراف متنی فقط زمانی اجرا می‌شود که AddCopy تشخیص دهد منبع و مقصد واقعاً نمونه‌های کاربرگ متفاوتی هستند. ارزش دارد دقیق باشیم درباره‌ی اینکه این بازنویسی چه چیزی نیست، هم. این هیچ ربطی به جابه‌جایی ردیف و ستونی ندارد که وقتی ردیف‌ها را درون یک برگه‌ی تکی درج یا حذف می‌کنید اجرا می‌شود، که یک مقاله‌ی همراه با جزئیات پوشش می‌دهد — آن موتور متن A1 را درجا بازنویسی می‌کند تا سلول‌هایی که چند ردیف بالا یا پایین درون یک کاربرگ جابه‌جا شده‌اند پیگیری کند، درحالی‌که این یکی وقتی یک فرمول کاربرگی را که آن را کامپایل کرده کاملاً ترک می‌کند اجرا می‌شود، جایی که ردیف‌های جابه‌جاشده مسئله نیستند و شماره‌گذاری خصوصی-کاربرگ مسئله است

// Conceptually, this is what AddCopy does for each formula cell: turn
// the compiled tree back into text using the source workbook's own
// tables, then let the destination workbook parse that text back into
// a tree using its own tables, from scratch
FormulaText := SourceBook.GetUnCompiledFormula(SourceFormula, Row, Col, SourceSheetID);
DestFormula := DestBook.GetCompiledFormula(FormulaText, DestSheetID);

اگر مقصد هنوز آن برگه، یا آن نام را نداشته باشد چه؟

دوباره‌کامپایل‌کردن AddCopy فقط زمانی موفق می‌شود که کاربرگ مقصد از پیش هر چیزی را که متن فرمول به آن ارجاع می‌دهد داشته باشد، و دو شکافی که در عمل نمایان می‌شوند، یک برگه‌ی هم‌نام که هنوز در این دسته کپی نشده، و یک نام تعریف‌شده در سطح کاربرگ که هرگز اصلاً در مقصد وجود نداشته هستند. HotXLS وقتی دوباره‌کامپایل‌کردن در میانه‌ی کپی یک برگه شکست بخورد یک استثنا raise نمی‌کند — انتساب Value سلول به‌جای آن بی‌سروصدا متن فرمول را به‌عنوان یک رشته‌ی ساده ذخیره می‌کند، یک حالت شکست عمدی و قابل‌بازرسی به‌جای یک حالت خاموش، چون یک سلول فرمول که به‌طور غیرمنتظره متن لفظی‌ای مثل =SUM(Q1!B2:B12) به‌جای یک عدد محاسبه‌شده نشان دهد، همان نشانه‌ای است که چیزی بالادست در کپی حل نشده. پیش از تسلیم‌شدن، AddCopy یک تعمیر امتحان می‌کند: درخت نحوی فرمول شکست‌خورده را می‌پیماید و هر شناسه‌ی نام-تعریف‌شده‌ای که فرمول لمس می‌کند را جمع می‌کند، و برای هر نام در سطح-کاربرگ که در منبع وجود دارد اما هنوز در مقصد نه، آن نام را به آن‌سو کپی می‌کند و همان متن را برای دومین‌بار دوباره کامپایل می‌کند. نام‌های سطح-برگه بیرون از چیزی می‌نشینند که این تعمیر می‌تواند تعمیر کند، چون نامی که فقط برای فرمول‌های یک برگه از کاربرگ منبع قابل‌مشاهده است، هیچ اسلات معادلی برای مهاجرت به آن ندارد، و یک مقصد که از پیش نامی با همان املا در اختیار دارد بدون تغییر رها می‌شود به‌جای بازنویسی‌شدن، بر این فرض که نامی که فراخواننده عمداً از قبل ساخته، همانی است که می‌خواهند رعایت شود. درون یک کاربرگ تکی، جستجوی نام یک فرمول بین-برگه‌ای به‌طور خودکار از دامنه‌ی برگه به دامنه‌ی کاربرگ بالا می‌رود، که همان سازوکاری است که مقاله‌ی نام‌های تعریف‌شده و فرمول‌های بین-برگه‌ای در HotXLS پوشش می‌دهد؛ عبورکردن از یک مرز واقعی کاربرگ آن تور ایمنی را کاملاً حذف می‌کند، و یک نام باید عمداً به‌آن‌سو حمل شود وگرنه فرمولی که به آن بستگی دارد به متن تنزل می‌یابد

ارجاع‌های سری چارت به همان تعمیر نیاز دارند، اما مسیر کد متفاوتی

یک سری چارت در HotXLS که یک بازه‌ی سلول را رسم می‌کند دقیقاً به همان مسئله‌ی شماره‌گذاری یک فرمول سلول معمولی برمی‌خورد، چون یک ارجاع بازه‌ی داده‌ی چارت هم یک جریان توکن فرمول کامپایل‌شده است — مشخصات BIFF رکوردی که آن را حمل می‌کند BRAI می‌نامد (بخش 2.4.51 از [MS-XLS]) — اما AddCopy نمی‌تواند آن را با استفاده‌ی دوباره از مسیر بارگذاری چارت معمولی درست کند، چون آن مسیر دقیقاً همان چیزی است که باگ را می‌سازد. وقتی یک رکورد چارت در روند معمولی بازکردن یک فایل از دیسک تجزیه شود، درخت فرمولش با ترجمه‌ی بایت‌های خام از طریق هر نمونه‌ی محاسبه‌گری که تجزیه را انجام می‌دهد ساخته می‌شود؛ بایت‌های خام BRAI یک چارت منبع را به‌جای آن از طریق بارگذار رکورد معمولی خودِ کاربرگ مقصد تغذیه کنید، و ixtiی جاسازی‌شده در آن بایت‌ها در برابر جدول EXTERNSHEET مقصد حل می‌شود، پس سری بی‌سروصدا به هر برگه‌ای که آن اسلات را آنجا اشغال می‌کند اشاره می‌کند — همان رده‌ی اشتباه از کپی‌کردن درخت کامپایل‌شده‌ی یک سلول بدون تغییر، فقط سخت‌تر برای متوجه‌شدن چون هیچ‌کس فرمول‌های سری چارت را همان‌طور که فرمول‌های سلول را می‌خواند، نمی‌خواند. HotXLS این تله را به‌جای آن با یک مسیر شبیه‌سازی اختصاصی اجتناب می‌کند: TXLSCustomChart.AssignFrom بایت‌های هدر غیرفرمولی خودِ هر رکورد چارت را کلمه‌به‌کلمه کپی می‌کند، سپس بازه‌ی پیوست‌شده را از طریق همان پریمیتیو decompile-و-recompile که برای سلول‌های معمولی استفاده می‌شود بازمی‌سازد، پس درخت جدید از صفر در برابر جدول EXTERNSHEET مقصد ساخته می‌شود، نه اینکه پس از واقعه در برابر آن دوباره تفسیر شود

همان مسئله‌ی شماره‌گذاری، یک شاخص فونت در یک زمان

هر عدد محلی-کاربرگ درون یک چارت یا یک سلول متن-غنی یک فرمول نیست، و یک شاخص فونت همان رده‌ی مسئله در مینیاتور است. run‌های متن غنی، همراه با دو نوع رکورد چارت دیگر که یک برچسب یا فونت محور حمل می‌کنند، یک ارجاع فونت را به‌عنوان یک اندیس عدد صحیح خام درون جدول فونت خودِ کاربرگ مالک ذخیره می‌کنند، و آن اندیس در جدول یک کاربرگ متفاوت هیچ معنایی ندارد — می‌تواند به‌همان راحتی به یک قلم، اندازه، یا رنگ کاملاً متفاوت آنجا اشاره کند. HotXLS این را با مقدار حل می‌کند نه با عدد: ویژگی‌های فونت واقعی در آن اندیس در جدول منبع را جستجو می‌کند، یک ورودی مطابق در جدول فونت مقصد پیدا یا می‌سازد، و اندیس ذخیره‌شده را بازنویسی می‌کند تا به آن اسلات جدید اشاره کند. یک عجیبی قالبی خودِ جستجو را دست‌وپاگیر می‌کند — اندیس روی-فایل از اسلات ۴ عبور می‌کند، شکافی در شماره‌گذاری که بخش 2.5.339 از [MS-XLS] مستند می‌کند، پس کد باید اندیس را یک واحد پیش از مقایسه‌ی فونت‌ها به پایین جابه‌جا کند و یک واحد پیش از نوشتن نتیجه به بالا برگرداند

// The file-numbered font index skips slot 4 (MS-XLS section 2.5.339);
// shift into the in-memory slot, migrate the font by value if the
// destination differs, then shift back before writing the result
if Ifnt >= 5 then
  Dec(Ifnt);
if DestFonts.Key[Ifnt] <> SourceFonts.Key[Ifnt] then
  Ifnt := DestFonts.SetKey(0, SourceFonts.Key[Ifnt]);
if Ifnt >= 4 then
  Inc(Ifnt);

برای فرمولی که از پیش خارج از کاربرگ اشاره می‌کند چه اتفاقی می‌افتد؟

فرمولی که پیش از اینکه اصلاً AddCopy را فراخوانی کنید به درون یک کاربرگ سوم دست دراز می‌کند، تنها موردی است که رفت‌وبرگشت متنی نمی‌تواند حمل کند، چون decompiler فرمول-به-متن خودِ HotXLS عمداً متن براکت [Book]Sheet! را برای یک ارجاع خارجی سنتز نمی‌کند، و کامپایلر سمت دیگر هم آن نحو را به‌عنوان ورودی نمی‌پذیرد — پس این یک مورد از طریق یک سازوکار دوم اجرا می‌شود که اصلاً متن را لمس نمی‌کند. وقتی تعمیر مهاجرت-نام که در بالا توصیف شد همچنان یک سلول را به‌عنوان یک رشته رها می‌کند، و کاربرگ منبع یک نام فایل واقعی دارد، AddCopy استراتژی را تغییر می‌دهد: خودِ درخت فرمول کامپایل‌شده را به‌جای متنش deep-copy می‌کند، سپس کپی را به یک پاس اتصال‌مجدد اختصاصی، RebindExternRefsInTree، می‌سپارد، که آن را گره‌به‌گره می‌پیماید. برای هر ارجاع بازه‌ای که پیدا می‌کند، آن پاس ورودی EXTERNSHEET منبع را به یک جفت نام برگه حل می‌کند، و یک ورودی معادل را در جداول ارجاع-خارجی خودِ مقصد ثبت می‌کند، یا دوباره استفاده می‌کند، و اگر مقصد هرگز پیش از این به آن فایل منبع ارجاع نداده باشد، یک لینک کاربرگ-خارجی کاملاً تازه می‌سازد

اینجا جایی است که مسئله‌ی شماره‌گذاری محلی-کاربرگ در تحت‌اللفظی‌ترین حالتش است، چون یک توکن ارجاع خارجی سه مختصات جداگانه را در یک فیلد بسته‌بندی می‌کند و هر یک از آن‌ها خصوصی کاربرگی است که آن را نوشته: کدام کاربرگ خارجی، یک اسلات در فهرست خودِ کتاب‌های خارجی مقصد که به هر ترتیبی که آن کاربرگ اتفاقاً آن‌ها را ثبت کرده تخصیص داده شده؛ کدام برگه درون فهرست برگه‌ی خودِ آن کاربرگ خارجی، ذخیره‌شده به‌عنوان یک اندیس مبنا-۱ که به‌طور مشخص به آن کتاب خارجی محدود است، یک دامنه‌ی شماره‌گذاری کاملاً متفاوت از شناسه‌های برگه‌ی داخلی خودِ مقصد؛ و خودِ بازه‌ی سلول، مختصات ردیف و ستون ساده که به هیچ ترجمه‌ای نیاز ندارند چون در وهله‌ی اول هرگز کاربرگ-نسبی نبوده‌اند. هرکدام از دو تای اول را اشتباه بگیرید و اکسل همچنان فایل را باز می‌کند، همچنان یک فرمول نشان می‌دهد، و آن را در برابر سلول‌های خارجی اشتباه بدون شکایتی ارزیابی می‌کند. یک نوع گره حتی این اتصال‌مجدد در سطح-درخت را هم شکست می‌دهد: یک ارجاع به یک نام تعریف‌شده، یک اندیس درون جدول نام خصوصی خودِ کاربرگش دقیقاً همان‌طور که یک اندیس برگه به EXTERNSHEET خودش خصوصی است، بدون هیچ تعمیر معادل در سطح-درختی در دسترس — همان لحظه‌ای که پیمایش اتصال‌مجدد در هر جای درخت به یک ارجاع نام برمی‌خورد، کل فرمول را رها می‌کند به‌جای اینکه یکی به‌طور جزئی درست بنویسد. حتی وقتی اتصال‌مجدد موفق شود، سلول مقصد یک عدد تازه-دوباره‌محاسبه‌شده نشان نمی‌دهد؛ آن مقداری را نشان می‌دهد که سلول منبع از پیش در زمان کپی داشت، نگه‌داشته‌شده در یک اسلات کش‌شده دقیقاً همان‌طور که خودِ اکسل آخرین-مقدار-شناخته‌شده‌ی هر ارجاع خارجی را کش می‌کند تا زمانی که صراحتاً لینک‌ها را refresh کنید، که پیش‌فرض درستی است، چون دوباره‌محاسبه‌کردن در سراسر یک لینک زنده به درون یک فایل دیگر دقیقاً همان نوع عملیاتی است که می‌خواهید یک‌بار، عمداً، ماشه بکشید، نه در هر بازکردن

این طراحی چه هزینه‌ای برای شما دارد

ماشین‌آلات decompile-و-recompile در AddCopy رایگان نیست، و هزینه ارزش برنامه‌ریزی پیش از اسکریپت‌کردن یک job تحکیم بزرگ را دارد، نه بعد از آن. کپی‌کردن یک برگه درون همان کاربرگ مسیر ارزان را می‌گیرد، یک شبیه‌سازی مستقیم درون-حافظه‌ای از درخت کامپایل‌شده، چون هر اندیسی درونش از پیش در کاربرگی که در آن می‌ماند معتبر است؛ یک کپی بین-کاربرگی به‌جای آن هزینه‌ی یک تجزیه‌ی واقعی روی هر سلول فرمول می‌پردازد، decompile به متن و سپس کامپایل دوباره‌ی آن متن از هیچ، و درحالی‌که تفاوت روی یک برگه با چند دوجین فرمول ارزش اندازه‌گیری ندارد، یک کاربرگ منبع با ده‌ها هزار سلول فرمول، کپی‌شده به‌عنوان یک برگه از میان دوجین‌ها در یک job دسته‌ای، باید انتظار داشته باشد که دوباره‌کامپایل‌شدن بر زمان اجرا مسلط شود نه I/O فایل پیرامون آن. ترتیب کپی به دلیل دومی فراتر از سرعت اهمیت دارد: فرمولی که به برگه‌ای ارجاع می‌دهد که AddCopy هنوز در این دسته به آن نرسیده، دوباره‌کامپایل‌شدنش را به همان دلیلی شکست می‌دهد که فرمولی که به یک برگه‌ی واقعاً ناموجود ارجاع می‌دهد شکست می‌خورد، پس یک job که برگه‌ی B را پیش از فرمول برگه‌ی A که به آن بستگی دارد کپی کند، خواهد دید که آن فرمول دقیقاً همان‌طور که در بالا توصیف شد تنزل می‌یابد، متن رشته‌ای یا یک fallback لینک-خارجی که درست به همان فایل منبعی که همین الان از آن آمده اشاره می‌کند. و چون هر کاربرگ منبع در یک دسته‌ی تحکیم معمولاً مستقلاً نوشته شده، ارزش دارد صراحتاً برای همان یک حالت شکست که هیچ فایل منبع تکی هرگز نمی‌توانست از آن هشدار دهد تست کنید — پنج کاربرگ شعبه که هرکدام اعداد یک شعبه‌ی همتا را جمع می‌زنند می‌توانند بدون اینکه هیچ فایل منبع تکی هرگز یکی داشته باشد، به یک ارجاع دایره‌ای واقعی درون کاربرگ خلاصه ترکیب شوند، یک چرخه‌ای که فقط زمانی وجود دارد که هر برگه در همان‌جا فرود آمده باشد و دوباره‌محاسبه در سراسر مجموعه‌ی ترکیب‌شده اجرا شود

کپی‌کردن برگ‌کاری بین‌کاربرگی به‌عنوان رفتار استاندارد AddCopy در کامپوننت Excel از HotXLS برای Delphi برای Delphi و C++Builder عرضه می‌شود؛ صفحه‌ی محصول مرجع کامل API برگ‌کاری و کاربرگ را حمل می‌کند، از جمله رفتار چارت، متن-غنی، و ارجاع-خارجی که در اینجا توصیف شد