Excel 365 وقتی فایل یک فرمول مثل =SUM(A1:B1*{10,100}) را بهصورت فرمول معمولی ذخیره کرده باشد، داخلش @ درج میکند و #VALUE! نشان میدهد، چون Excel در آن حالت تقاطع ضمنی legacy را روی تکتک عملوندهای عملگر اعمال میکند. از v2.384.68 به بعد، HotXLS Delphi Component این فرمولهای آرایهای با عملگر را همانطور که Excel 365 انجام میدهد ذخیره میکند: در XLSX بهشکل فرمولهای آرایه پویای تکسلولی و در XLS بهشکل فرمولهای آرایهای تکسلولی
این symptom از code review جان سالم به در میبرد. سرویس Delphi شما یک workbook مینویسد، HotXLS آن را دوباره محاسبه میکند و برای =SUM(A1:B1*{10,100}) مقدار 210 را کش میکند، و مشتری فایل را در Excel 16 باز میکند و در نوار فرمول =SUM(@A1:B1*@{10,100}) و در سلول #VALUE! میبیند. هیچجای فایل کج نیست. چیزی که غایب است metadataای است که به Excel میگوید این فرمول زیر قواعد آرایه پویا نوشته شده، و بدون آن Excel به مدل ارزیابی قبل از آرایه پویا برمیگردد
چرا Excel 365 به فرمولی که HotXLS درست محاسبه کرده علامت @ اضافه میکند؟
Excel 365 علامت @ را اضافه میکند چون فرمولی که علامتگذاری آرایه پویا ندارد طبق تعریف یک فرمول legacy است، و فرمولهای legacy هرجا که یک عملگر انتظار تکمقدار دارد یک محدوده چندسلولی را به یک سلول میکاهند. این کاهش همان تقاطع ضمنی است: Excel سلولی از محدوده را برمیدارد که با سطر فرمول (برای محدوده عمودی) یا ستون فرمول (برای محدوده افقی) مشترک است، و اگر چنین سلولی وجود نداشته باشد نتیجه #VALUE! میشود. Excel 365 همین معنا را برای فرمولهای old-style نگه میدارد و @ را نمایش میدهد تا این کاهش دیده شود
=SUM(A1:B1*{10,100}) را در E5 بگذارید تا خوانش legacy آشکار شود. A1:B1 یک محدوده افقی است، فرمول در ستون E نشسته، محدوده هیچ سلولی در ستون E ندارد، پس @A1:B1 برابر #VALUE! است و کل SUM همان را به ارث میبرد. زیر قواعد آرایه پویا همان متن ضرب را المان به المان انجام میدهد، 1 × 10 + 2 × 100، و 210 برمیگرداند. موتور فرمول HotXLS از نسخههای v2.384.61 و v2.384.63 به شیوه آرایه پویا ارزیابی میکرد؛ فقط فرمت فایل این را اعلام نمیکرد. با A1:B2 که 1 و 2 و 3 و 4 را نگه میدارد، این فرمولهای آزمایشی و خروجیشان در Excel 16 هستند:
| فرمول | نتیجه HotXLS | Excel 16، ذخیرهشده بهعنوان فرمول معمولی | ذخیره از v2.384.68 به بعد |
|---|---|---|---|
=SUM(A1:B1*{10,100}) | 210 | #VALUE! | آرایه پویا، Excel نشان میدهد 210 |
=SUM((A1:B2>2)*1) | 2 | تقاطع ضمنی، نتیجه غلط یا خطا | آرایه پویا، Excel نشان میدهد 2 |
=SUMPRODUCT((A1:B2>2)*1) | 2 | تقاطع ضمنی، نتیجه غلط یا خطا | آرایه پویا، Excel نشان میدهد 2 |
=MAX(A1:B2-1) | 3 | تقاطع ضمنی، نتیجه غلط یا خطا | آرایه پویا، Excel نشان میدهد 3 |
=SUM(A1:B2) | 10 | 10 | فرمول معمولی، بدون تغییر |
سطر آخر بهاندازه چهار سطر اول اهمیت دارد. SUM(A1:B2) یک محدوده را مستقیم به پارامتری از تابع میدهد که ارجاع میپذیرد، پس هیچ عملگری هرگز محدوده چندسلولی نمیبیند و هیچ تقاطعی در کار نیست. خود Excel 365 هم آن فرمول را بهصورت فرمول معمولی ذخیره میکند، و HotXLS هم همین کار را میکند
HotXLS فرمولهای آرایهای با عملگر را چطور در XLSX و XLS ذخیره میکند
HotXLS یک فرمول آرایهای با عملگر را در XLSX بهشکل آرایه پویای تکسلولی مینویسد: المنت <c> مقدار cm="1" را حمل میکند، فرمول <f t="array" ref="E5"> است، و پکیج xl/metadata.xml میگیرد با یک نوع metadata از جنس XLDAPR که extension آن dynamicArrayProperties fDynamic="1" را نگه میدارد. ویژگی cm یک اندیس یکمبنا به بلاک cellMetadata در همان part است، و رکورد XLDAPR پشتش همان چیزی است که به Excel میگوید «این را زیر قواعد آرایه پویا ارزیابی کن». این همان ساختاری است که Excel 16 وقتی همان فرمول را تایپ و ذخیره میکنید مینویسد، و target layout هم از همین راه مشخص شد
در XLS هیچ part متادیتایی وجود ندارد، پس HotXLS از تنها سازهای استفاده میکند که BIFF8 برای ارزیابی آرایه دارد: فرمول آرایهای تکسلولی. سلول یک رکورد FORMULA میگیرد که token stream آن یک PtgExp منفرد است که به خود سلول اشاره میکند، و بعد از آن یک رکورد ARRAY یعنی $0221 میآید که فرمول parseشده واقعی را روی بازه تکسلولی حمل میکند. Excel 365 فرمولهای آرایه پویا را به همان شکل در XLS مینویسد، و یک نسخه قدیمیتر Excel که فایل را باز کند یک فرمول آرایهای کلاسیک با Ctrl+Shift+Enter میبیند
هیچ API جدیدی در کار نیست. علامتگذاری همان موقع اتفاق میافتد که فرمول را از طریق API معمولی سلول assign کنید، در هر دو موتور. در سمت XLSX این TXLSXCell.Formula است:
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 1;
Sheet.Cells[1, 2].Value := 2;
Sheet.Cells[2, 1].Value := 3;
Sheet.Cells[2, 2].Value := 4;
// عملگر روی یک محدوده یا آرایه درونخطی: بهصورت آرایه پویا ذخیره میشود
Sheet.Cells[5, 5].Formula := '=SUM(A1:B1*{10,100})';
Sheet.Cells[6, 5].Formula := '=SUM((A1:B2>2)*1)';
// محدودهای که مستقیم به یک تابع داده میشود: یک <f> معمولی میماند
Sheet.Cells[7, 5].Formula := '=SUM(A1:B2)';
if Book.Recalculate = lxOk then
Writeln(VarToStr(Sheet.Cells[5, 5].Value)); // 210
// ریشه آرایه متن خودش را بدون علامت = ابتدایی نگه میدارد
Writeln(Sheet.Cells[5, 5].Formula); // SUM(A1:B1*{10,100})
Book.SaveAs('probe.xlsx'); // E5 و E6 مقدار cm="1" + t="array" میگیرند
finally
Book.Free;
end;
end;
بعد از تبدیل، TXLSXCell.Formula متن را بدون = برمیگرداند، همان شکلی که TXLSXRange.SetDynamicArrayFormula ذخیره میکند، پس کدی که بعد از assign رشتههای فرمول را مقایسه میکند باید علامت = ابتدایی را normalize کند
موتور کلاسیک همان قاعده را از طریق IXLSRange.Formula روی یک سلول منفرد دنبال میکند. assign کردن فرمول آن را در درون به مسیر آرایه تکسلولی هدایت میکند، پس XLS ذخیرهشده جفت FORMULA بهعلاوه ARRAY را دارد:
var
Wb: IXLSWorkbook;
Sh: TXLSWorksheet;
begin
Wb := TXLSWorkbook.Create;
Sh := Wb.Sheets.Add;
Sh.Range['A1', 'A1'].Value := 1;
Sh.Range['B1', 'B1'].Value := 2;
Sh.Range['A2', 'A2'].Value := 3;
Sh.Range['B2', 'B2'].Value := 4;
Sh.Range['E5', 'E5'].Formula := '=SUM(A1:B1*{10,100})'; // رکورد ARRAY
Sh.Range['E6', 'E6'].Formula := '=MAX(A1:B2-1)'; // رکورد ARRAY
Sh.Range['E7', 'E7'].Formula := '=SUM(A1:B2)'; // FORMULA معمولی
Writeln(VarToStr(Sh.Range['E5', 'E5'].Value)); // 210
Writeln(VarToStr(Sh.Range['E6', 'E6'].Value)); // 3
Wb.SaveAs('probe.xls');
end;
اگر بهجای یک جمع اسکالر دنبال لنگر کردن یک نتیجه چندسلولی هستید، APIهای صریح همچنان ابزار درستاند: SetArrayFormula برای یک مستطیل از پیش اندازهگرفته، همانطور که در فرمولهای سرریز آرایه پویا در دلفی با HotXLS توضیح داده شده، یا TXLSXRange.SetDynamicArrayFormula برای وقتی که علامتگذاری آرایه پویای XLSX را روی بازهای که خودتان اندازه میگیرید میخواهید. مسیر خودکار این مقاله فقط فرمولهایی را پوشش میدهد که در یک سلول تایپ میشوند
HotXLS کدام فرمولها را بهعنوان آرایه پویا علامت میزند؟
HotXLS یک فرمول را فقط وقتی علامت میزند که یکی از عملگرها زیردرخت عملوندی داشته باشد که آرایه تولید میکند. این چک روی syntax tree کامپایلشده اجرا میشود، و یک عملوند وقتی آرایه تولید میکند که محدودهای چندسلولی باشد، یک ثابت آرایه درونخطی باشد، یا یک عبارت عملگری دیگر که خودش چنین عملوندی دارد. پرانتزها شفافاند. عملگرهایی که شمرده میشوند عبارتاند از عملگرهای حسابی یعنی + - * / ^، الحاق یعنی &، شش مقایسه، مثبت و منفی یگانی، و درصد:
A1:B1*{10,100}،(A1:B2>2)*1،--(B1:B2>0)وA1:B2-1علامت میخورند، هر جای فرمول که باشند، از جمله داخل SUMPRODUCTSUM(A1:B2)وSUMPRODUCT(A1:A2,{1;10})علامت نمیخورند، چون محدوده و آرایه مستقیم وارد آرگومان تابع میشوند و هیچ عملگری به آنها دست نمیزندA1*2یاSUM(A1,B1)*2علامت نمیخورند: ارجاعهای تکسلولی و خروجی تابعها از دید این چک اسکالرند
سه مرز عمدی است. اول، علامتگذاری فقط وقتی رخ میدهد که فرمول از طریق API وارد شود؛ یعنی TXLSXCell.Formula در موتور XLSX و assign کردن Formula یا Value تکسلولی در موتور کلاسیک. فرمولهایی که از فایل load میشوند دقیقاً همانطور که پیدا شدهاند نوشته میشوند، چون یک فرمول legacy از تولیدکنندهای دیگر ممکن است عمداً به تقاطع ضمنی وابسته باشد. دوم، متنی که نه : دارد نه { بدون کامپایل دوباره رد میشود. سوم، فرمولی که سرریز میکرد، مثل =A1:B1*2 بهتنهایی، بهعنوان آرایه پویای تکسلولی علامت میخورد که همانجا که گذاشتهایدش لنگر است. HotXLS آن را سرریز نمیکند، و Excel دفعه بعد که دوباره محاسبه کند نتیجه را به سلولهای مجاور گسترش میدهد
این قاعده عملوند برادر قاعده کلاس-آرگومان است که در تقاطع ضمنی نامهای تعریفشده در HotXLS برای Delphi پوشش داده شده. آن مقاله درباره پارامترهای تابع است که با کلاس value اعلان شدهاند؛ این یکی درباره عملگرهاست که در مدل legacy همیشه مقدار میخواهند
در موتور محاسبه چه چیزی عوض شد تا نتایج یکی شوند
فیکس ذخیرهسازی در v2.384.68 بر این تکیه دارد که موتور فرمول HotXLS از قبل مقادیر Excel 365 را برمیگرداند، که خودش چند فیکس قبلی در هر دو موتور لازم داشت. پیداترینشان SUMPRODUCT بود: تا v2.384.61 فقط دو محدوده ساده یا بیشتر میپذیرفت، پس SUMPRODUCT((B1:B2>0)*1) و SUMPRODUCT(--(B1:B2>0)) و حتی SUMPRODUCT(B1:B2) تکآرگومان هم #N/A برمیگرداندند. HotXLS حالا آرگومانهای عبارتی را با قواعد Excel المان به المان ارزیابی میکند:
- هر آرگومان باید دقیقاً شکل یکسانی داشته باشد، یک اسکالر 1 × 1 حساب میشود، وگرنه نتیجه
#VALUE!است - یک مقدار خطا داخل هر آرگومان، خودش بهعنوان نتیجه برگردانده میشود
- المانهای متنی و منطقی صفر حساب میشوند، پس برای تبدیل TRUE به 1 هنوز
(B1:B2>0)*1یا--لازم است - آرگومانهایی که همه محدودههای سادهاند همان حلقه استریمی اصلی را نگه میدارند، پس محدودههای بزرگ بهشکل آرایه materialize نمیشوند
خانواده SUM یعنی SUM و COUNT و AVERAGE و MIN و MAX و COUNTA وقتی آرگومانش یک عبارت عملگری روی محدوده باشد از همان ارزیاب المان به المان استفاده میکند، پس =SUM((B1:B2>0)*1) هر دو سطر را میشمارد نه فقط سلول اول را. v2.384.62 کاری کرد عملگر تقاطع با فاصله مستطیل مشترک دو ارجاع را برگرداند، با #NULL! وقتی همپوشان نیستند، پس =SUM(A1:B2 B1:B2) برابر 6 است نه 2 و نتیجه میتواند به پارامترهای ارجاعی مثل ROWS و INDEX بخورد. v2.384.63 ثابتهای آرایه درونخطی مثل {1,2;3,4} (کاما ستون جدا میکند، سمیکالن سطر) و اجتماع ارجاعها مثل (A1:B2,D4) را به parser اضافه کرد. مقایسههای المان به المان هم به یک المان خالی نوع سمت دیگر را میدهند، FALSE مقابل یک منطقی، هماهنگ با قاعده اسکالر از v2.384.53 که در زنجیرههای مقایسه، سلولهای خالی و SUMIF در HotXLS دلفی توضیح داده شده
var
V: Variant;
begin
// Book همان TXLSXWorkbook از مثال اول است؛
// شیت فعالش A1:B2 برابر 1 و 2 و 3 و 4 را نگه میدارد
V := Book.Calculate('=SUMPRODUCT((A1:B2>2)*1)'); // 2
V := Book.Calculate('=SUMPRODUCT(A1:B2)'); // 10، آرگومان تکی
V := Book.Calculate('=SUMPRODUCT(A1:A2,{1;10})'); // 31 = 1*1 + 3*10
V := Book.Calculate('=SUM(A1:B2 B1:B2)'); // 6، محدوده مشترک B1:B2
V := Book.Calculate('=SUM((A1:B2,B1:B2))'); // 16، همپوشانی دوبار شمرده میشود
V := Book.Calculate('=ROWS({1,2,3;4,5,6})'); // 2
V := Book.Calculate('=TRUE*1'); // 1، قبل از v2.384.61 برابر -1 بود
end;
TXLSXWorkbook.Calculate یک رشته فرمول را روی شیت فعال بدون ذخیره کردن ارزیابی میکند، راهی سریع برای چک کردن رفتار موتور. یک هشدار درباره خود @: HotXLS از گذشته @ بین دو ارجاع را بهعنوان تقاطع باینری میپذیرفت، و حالا آن شکل را با معناشناسی تقاطع واقعی ارزیابی میکند. در Excel 365 علامت @ یک پیشوند یگانی تقاطع ضمنی است. در متن فرمول @ ننویسید و انتظار معنای Excel را داشته باشید؛ برای تقاطع از فاصله استفاده کنید و بگذارید قواعد ذخیرهسازی بالا معناشناسی آرایه پویا را مدیریت کنند
چرا Excel از باز کردن فایل سر باز میزد یا مقدار غلط محاسبه میکرد؟
قانع کردن Excel برای پذیرش علامتگذاری آرایه پویا سه فیکس لازم داشت که هیچ تست round-trip با خودشان نمیتوانست بگیردشان، چون HotXLS در همه حالات خروجی خودش را درست میخواند. هر کدام با باز کردن خروجی HotXLS در Excel 16 و عوض کردن یک متغیر در هر بار پیدا شد:
- GUID ثابت extension باید کاملاً با حروف کوچک باشد. مقدار
ext uriدرxl/metadata.xmlباید دقیقاً{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}باشد. یک template قدیمی HotXLS آن را با حروف بزرگوکوچک مخلوط نوشته بود، و Excel 16 از باز کردن کل پکیج سر باز میکرد، نه فقط آن سلول. workbookهایی که قبل از v2.384.68 باTXLSXRange.SetDynamicArrayFormulaساخته شده بودند همین مشکل را داشتند - متن ریشه آرایه علامت
=ابتدایی ندارد. نویسنده XLSX متن ذخیرهشده ریشه آرایه را عیناً در<f>میریزد. اگر سلول تبدیلشده=خودش را نگه میداشت، المنت میشد<f t="array" ref="E5">=SUM(...)</f>که Excel در زمان open آن را هم رد میکند. HotXLS همان موقع تبدیل آن را حذف میکند، و برای همینTXLSXCell.Formulaبدون آن برمیگردد Double(True)در Delphi برابر -1 است. تبدیل Variant قرارداد COM را دنبال میکند که در آن TRUE یعنی همه بیتها یک، وVarIsNumeric(True)هم True برمیگرداند. قبل از v2.384.61 این باعث میشد=TRUE*1برابر -1 شود و المانهای منطقی آرایه بهعنوان عدد طبقهبندی شوند، پس مقایسهای مثل(B1:B2>0)=TRUEغلط از آب درمیآمد. HotXLS حالا قبل از اینکه یک Variant را در حساب اسکالر و حساب آرایه و طبقهبندی المان آرایه عدد بگیردvarBooleanرا چک میکند، و TRUE برابر 1 حساب میشود
کلاسهای عملوند BIFF8: جزئیات در سطح byte برای پیادهکنندههای فرمت
در BIFF8 هر token عملوند کلاس عملوندش را در خود بایت token حمل میکند، و Excel به آن کلاس بیشتر از ساختار فرمول اعتماد میکند. [MS-XLS] کلاس را فیلد دوبیتی PtgDataType در بیتهای 5 و 6 token تعریف میکند: 1 برای ارجاع، 2 برای مقدار، 3 برای آرایه. پنج بیت پایین نام token را میگویند، پس همان ارجاع area سه املای متفاوت دارد:
| Token | کلاس ارجاع | کلاس مقدار | کلاس آرایه |
|---|---|---|---|
PtgRef | $24 | $44 | $64 |
PtgArea | $25 | $45 | $65 |
PtgArray | $20 | $40 | $60 |
HotXLS سهتای اینها را در جای جای کد غلط نوشته بود، و هر کدام در Excel symptom متمایزی تولید میکرد در حالی که در HotXLS بدون خطا خوانده میشد:
- ثابتهای آرایه با کلاس ارجاع. انکودر کلاس را از context انتخاب میکرد، و پارامترهای SUM یا ROWS کلاس ارجاع دارند، پس
=SUM({1,2})باPtgArrayبرابر$20نوشته میشد. Excel کل فرمول را بهشکل=#N/Aنشان میداد. یک ثابت آرایه هرگز نمیتواند ارجاع باشد، پس از v2.384.63 HotXLS هرجا context کلاس ارجاع میخواهد کلاس آرایه یعنی$60مینویسد - عملوندهای
PtgIsectوPtgUnionبا کلاس مقدار. عملگرهای باینری عملوند کلاس مقدار میگرفتند، که برای*درست است اما برای عملگرهای ارجاعی غلط. با areaهای$45قبل ازPtgIsectیعنی$0F، Excel =SUM(A1:B2 B1:B2)را مثل=SUM(@A1:B2 @B1:B2)میخواند و#VALUE!برمیگرداند. از v2.384.62 عملوندهایPtgIsectوPtgUnionیعنی$10با کلاس ارجاع یعنی$25نوشته میشوند - عملوندهای کلاس مقدار داخل رکورد ARRAY. Excel حتی داخل یک فرمول آرایهای وقتی عملوند کلاس مقدار باشد تقاطع ضمنی اعمال میکند. HotXLS آنجا
$45مینوشت، پس فرمول آرایه تکسلولی=SUM(A1:B1*{10,100})در Excel به 10 ارزیابی میشد. از v2.384.68 به بعد token stream یک رکورد ARRAY هر ارجاع کلاس-مقدار و ثابت آرایه را به کلاس آرایه یعنی$65و$60ارتقا میدهد، که همان چیزی است که Excel مینویسد
یک reader که بیتهای کلاس را نادیده میگیرد هر سه را بیدردسر round-trip میکند، پس اگر BIFF8 writer خودتان را نگهداری میکنید، بیتهای کلاس هر token عملوند را با یک فایل ذخیرهشده توسط Excel از همان فرمول مقایسه کنید، نه فقط با شماره tokenها
مرجع سریع
- Excel 365 وقتی یک عملگر در فرمول معمولی و بدون علامت یک محدوده چندسلولی یا آرایه درونخطی بگیرد
@نشان میدهد - HotXLS از v2.384.68 به بعد چنین فرمولهایی را بهشکل آرایه پویای تکسلولی در XLSX یعنی
cm="1"وt="array"و metadata از نوعXLDAPR، و بهشکل فرمول آرایه تکسلولی در XLS یعنی FORMULA باPtgExpبهعلاوه ARRAY یعنی$0221ذخیره میکند - فقط عملوندهای عملگر شمرده میشوند؛ محدودهای که مستقیم به آرگومان تابع میرود فرمول معمولی میماند
- فقط فرمولهایی که از طریق
TXLSXCell.FormulaیاFormula/Valueکلاسیک تکسلولی وارد شدهاند علامت میخورند؛ فرمولهای loadشده دستنخورده میمانند - سلول ریشه تبدیلشده بدون علامت
=ابتدایی برمیگردد - GUID یعنی
ext uriمربوط به آرایه پویا باید حروف کوچک باشد وگرنه Excel پکیج را رد میکند - در Delphi مقدار
Double(True)برابر -1 است؛ قبل از تبدیل عددیvarBooleanرا تست کنید - BIFF8: ثابتهای آرایه هرگز کلاس ارجاع، عملوندهای
PtgIsect/PtgUnionبا کلاس ارجاع، و عملوندهای رکورد ARRAY با کلاس آرایه
HotXLS workbookهای XLS و XLSX را بهصورت بومی از Delphi و C++Builder میخواند و مینویسد و محاسبه میکند و فرمولهای آرایهای با عملگر را طوری ذخیره میکند که Excel 365 آنها را با همان مقادیری که HotXLS محاسبه کرده باز کند. برای نسخهها، مستندات و دانلود آزمایشی کامپوننت صفحهگسترده HotXLS در Delphi را ببینید