Precision as displayed در Excel هر عدد ذخیرهشده را به تعداد اعشاری گرد میکند که فرمت عددیاش نشان میدهد: بخشی از فرمت که با علامت مقدار جور درمیآید، بهعلاوه دو رقم برای هر %، منهای سه رقم برای هر کامای مقیاسکننده هزارگان، با گرد کردن نصف دور از صفر. HotXLS همین قاعده را در هر دو موتور دلفی خود اعمال میکند وقتی TXLSXWorkbook.FullPrecision یا TXLSWorkbook.UseFullPrecision برابر False باشد. این حرف تا وقتی مشتری گزارش ندهد که جمع فاکتورهای صادرشده شما با Excel یک سنت اختلاف دارد، یا ستونی از مدتزمانها با فرمت [ss].00 جمع شده به صفر، ساده به نظر میرسد. هر دو اتفاق افتاد، و هر دو به غلط گرفتن یکی از همین قواعد برمیگشت. از v2.384.57 دو موتور یک پیادهسازی واحد را شریکاند که مقادیر مورد انتظارش در Excel 16 با روشن بودن Workbook.PrecisionAsDisplayed اندازهگیری شده
Precision as displayed دقیقاً چه چیزی را در یک workbook عوض میکند؟
Precision as displayed یک فلگ در سطح workbook است که به موتور محاسبه میگوید اعداد را همانطور که دیده میشوند ذخیره کن، نه همانطور که محاسبه شدهاند. در UI اکسل زیر File و Options و Advanced و «When calculating this workbook» نشسته، با عنوان «Set precision as displayed». روی دیسک یک بیت است. یک فایل BIFF8 آن را در رکورد CalcPrecision حمل میکند ($000E، [MS-XLS] §2.4.35) که فیلد fFullPrec اش برای دقت کامل عادی 1 است و وقتی گزینه روشن است 0. یک پکیج XLSX آن را بهشکل ویژگی fullPrecision روی المان calcPr در workbook.xml حمل میکند، تعریفشده در بخش 1 از ECMA-376، که پیشفرضش true است و fullPrecision="0" گرد کردن را روشن میکند
این فلگ یک ترجیح نمایشی نیست. وقتی تیک را میزنید Excel هشدار میدهد که داده برای همیشه دقت از دست میدهد، و منظورش دارد: مقدارها به دقت نمایشیشان بازنویسی میشوند و رقمهای بریدهشده میروند. بعداً برداشتن تیک رقمهای قدیمی را برنمیگرداند. یک 0.1234 که بهشکل 12.3% دیده میشود برای همیشه میشود 0.123
HotXLS این فلگ را در هر دو فرمت میخواند و مینویسد و در هر دو موتور بیرون میدهد:
-
TXLSXWorkbook.FullPrecision: Booleanروی موتور XLSX، که ازcalcPr/@fullPrecisionخوانده و در همان ذخیره میشود -
TXLSWorkbook.UseFullPrecision: Booleanروی موتور کلاسیک (رویIXLSWorkbookهم هست)، که از رکورد CalcPrecision خوانده و در همان ذخیره میشود - هر دو پیشفرض True دارند، که حالت امن و مخربنبودن است و هم پیشفرض اکسل
اینکه HotXLS گرد کردن را کجا اعمال میکند مهم است. HotXLS در همان نقطهای که مقدار را محاسبه میکند گرد میکند: نتیجه هر فرمول قبل از اینکه بهعنوان مقدار cacheشده سلول ذخیره شود به دقت نمایشیاش گرد میشود، هم در Recalculate و هم در ارزیابی در-صورت-نیاز. ثابتهایی که از طریق Value assign میکنید عیناً همانطور که داده شدهاند ذخیره میشوند. اگر خروجیتان باید چیزی را بازتولید کند که اکسل بعد از زدن تیک ذخیره میکند، آن ثابتها را خودتان قبل از نوشتن گرد کنید، مثلاً با helper ای که پایینتر نشان داده میشود
Excel چطور تصمیم میگیرد چند رقم اعشار نگه دارد؟
Excel تعداد ارقام اعشاری قابل نگهداشت را از همان بخش مشخصی از فرمت استنتاج میکند که مقدار را نمایش میدهد، نه از رشته فرمت بهعنوان یک کل. قواعد پایین در Excel 16 اندازهگیری شدهاند و همان چیزیاند که XlsApplyDisplayedPrecision در lxNumFormat برای هر دو موتور HotXLS پیاده میکند
- بخش را بر اساس علامت انتخاب کنید. فرمت دو-بخشی برای مقادیر منفی از بخش دوم استفاده میکند. فرمت با سه بخش یا بیشتر از دومی برای مقادیر منفی و از سومی برای دقیقاً صفر استفاده میکند. بقیه حالتها بخش اول را به کار میبرند
- جایگاههای اعشار را بشمارید. هر
0و#یا?بعد از ممیز در آن بخش یک رقم قابل نگهداشت اضافه میکند - بهازای هر علامت درصد دو تا اضافه کنید.
0.0%مقدار 0.1234 را 12.3% نشان میدهد، پس مقدار ذخیرهشده یکصدم آن چیزی است که میبینید و سه رقم اعشار نگه میدارد، نه یکی - بهازای هر کامای مقیاسکننده سه تا کم کنید. کامایی که بعد از آخرین جایگاه صحیح بیاید (
0,و0.0,و0,.0) نمایش را بر 1000 تقسیم میکند. 0.0,مقدار 12345.678 را 12.3 نشان میدهد، پس اکسل یک رقم اعشار منهای سه را نگه میدارد، که یک شمارش منفی است: مقدار به صدها گرد میشود و بهعنوان 12300 ذخیره میشود. کامایی که بین جایگاههای صحیح است، مثل#,##0، گروهبندی ساده ارقام است و چیزی را عوض نمیکند - بخشهای غیر-عددی را دست نزنید. بخشهای General و تاریخ و زمان (شامل زمان سپریشده یعنی
[h]و[mm]و[ss])، علمی، کسر و متن، و بخشهای بدون هیچ جایگاه رقمی، همه دقت کامل را نگه میدارند
اندازهگیریشده مقابل Excel 16، این مقادیری است که هر دو موتور HotXLS حالا برای نتیجه یک فرمول در هر فرمت ذخیره میکنند:
| فرمت عددی | مقدار محاسبهشده | مقدار ذخیرهشده | قاعدهای که اعمال میشود |
|---|---|---|---|
0.0% | 0.1234 | 0.123 | یک رقم اعشار بهعلاوه دو تا برای علامت درصد |
0 | 2.5 | 3 | نصف دور از صفر، نه به سمت زوج |
0 | -2.5 | -3 | نصف دور از صفر در سمت منفی هم همینطور |
0.00;(0.0) | -1.2345 | -1.2 | بخش منفی یک رقم اعشار نشان میدهد |
0.00;(0.0) | 1.2345 | 1.23 | بخش مثبت دو رقم اعشار نشان میدهد |
#,##0.0 | 1234.5678 | 1234.6 | کامای گروهبندی، بدون مقیاس |
0.0, | 12345.678 | 12300 | یک رقم اعشار منهای سه: گرد به صدها |
0.0%;(0.00%) | -0.0125 | -0.0125 | بخش منفی دو بهعلاوه دو رقم اعشار نگه میدارد |
0.00 | 1.005 | 1.01 | تحمل خطای بازنمایی دودویی |
0;-0;0.0 | 0.5 | 1 | صفر نیست، پس بخش مثبت تصمیم میگیرد |
سطر آخر یک تله خوشگل است. مقدار 0.5 به عدد صحیح گرد میشود و بخش صفر هیچوقت وارد بازی نمیشود، چون اکسل بخش را قبل از گرد کردن از مقدار محاسبهشده انتخاب میکند. یک محدودیت صادقانه از سمت HotXLS: بخشها فقط با علامت انتخاب میشوند، پس فرمتی که بخشهایش شرطهای براکتدار سفارشی مثل [>=1000] دارند همچنان با علامت جدا میشود. اگر چنین فرمتهایی برایتان مهماند، مقابل اکسل چکشان کنید
چرا 1.005 به 1.01 گرد میشود نه به 1.00؟
Excel مقدار 1.005 را در سلولی با فرمت 0.00 به 1.01 گرد میکند حتی اگر double نزدیک به 1.005 کمی زیر نقطه نیمه باشد، و HotXLS همان را با یک تحمل چند-ulp مچ میکند. لفظ 1.005 در ممیز شناور دودویی قابل بازنمایی نیست. نزدیکترین double طبق IEEE 754 برابر 1.00499999999999989341858963598497211933135986328125 است، و ضربش در 100 میشود 100.49999999999999. یک Floor(x * 100 + 0.5) / 100 کتابدرسیپسند پس برمیگرداند 1.00، که با عددی که کاربر تایپ کرده، با آنچه اکسل نشان میدهد و با آنچه اکسل ذخیره میکند جور نیست
Delphi هم پیچ خودش را اضافه میکند. System.Round حالتهای تساوی را به زوج گرد میکند، پس Round(2.5) برابر 2 و Round(3.5) برابر 4 است. این همان banker's rounding است، پیشفرض معقولی برای آمار و قاعده غلط اینجا: اکسل برای 2.5 در سلولی با فرمت 0 عدد 3 و برای -2.5 عدد -3 را ذخیره میکند. پیادهسازی HotXLS روی مقدار مطلق کار میکند، 0.5 بهعلاوه یک تحمل نسبی برابر 2-51 ضربدر مقدار مقیاسشده اضافه میکند (چند ulp در آن بزرگی، هرگز کمتر از دو ulp عدد 1.0)، میبُرد، برمیگرداند به مقیاس اصلی و علامت را برمیگرداند. تابع زیر یک تصویر مستقلایده از همان اصل است، نه کد کتابخانه، و شمارش ارقام منفی را برای کاماهای مقیاسکننده هم به همان شکل مدیریت میکند:
// طرح مفهومی: گرد کردن نصف دور از صفر به ADigits رقم اعشار،
// با یک تحمل چند-ulp تا 1.005 به 1.01 برسد.
// مقدار ADigits < 0 به دهگان و صدگان و ... گرد میکند ("0.0," میدهد -2)
function RoundAsDisplayed(AValue: Double; ADigits: Integer): Double;
const
Tolerance = 4.440892098500626E-16; // 2^-51, دو ulp عدد 1.0
var
I: Integer;
Scale, Scaled, Eps: Double;
begin
Result := AValue;
if (ADigits < -15) or (ADigits > 14) then
Exit; // فراتر از دقت double: مقدار را دست نزن
Scale := 1;
for I := 1 to Abs(ADigits) do
Scale := Scale * 10;
if ADigits >= 0 then
begin
if Abs(AValue) > 1E300 / Scale then
Exit; // مقیاس کردن overflow میداد
Scaled := Abs(AValue) * Scale;
end
else
Scaled := Abs(AValue) / Scale;
Eps := Scaled * Tolerance;
if Eps < Tolerance then
Eps := Tolerance;
Scaled := Int(Scaled + 0.5 + Eps); // نصف دور از صفر، نه Round()
if ADigits >= 0 then
Result := Scaled / Scale
else
Result := Scaled * Scale;
if AValue < 0 then
Result := -Result;
end;
// RoundAsDisplayed(1.005, 2) = 1.01 (با Floor: مقدار 1.00)
// RoundAsDisplayed(2.5, 0) = 3 (با Round: مقدار 2)
// RoundAsDisplayed(-2.5, 0) = -3
// RoundAsDisplayed(0.1234, 3) = 0.123 ("0.0%": مقدار 1 + 2 رقم)
// RoundAsDisplayed(12345.678, -2) = 12300 ("0.0,": مقدار 1 - 3 رقم)
آن تحمل یک معامله عمدی است. مقداری که واقعاً دو ulp زیر یک پله نیم هم به بالا گرد میشود، اما در آن فاصله تفاوت از خطای بازنمایی قابل تفکیک نیست، و گرفتنش بهعنوان پله نیم همان چیزی است که اعشارهای تایپشده را همانطور که کاربران انتظار دارند رفتار میدهد
قبل از v2.384.57 چه چیزی خراب بود؟
قبل از v2.384.57 موتور XLSX و موتور کلاسیک هر کدام کد precision-as-displayed مال خودشان را داشتند، و هر کدام به شکل متفاوتی غلط بود. اگر با این گزینه روشن workbook تولید میکنید، اینها symptomهایی هستند که در فایلهای ساختهشده توسط buildهای قدیمی دنبالشان بگردید
موتور XLSX: فقط بخش اول، بدون درصد، گرد کردن بانکی
مسیر قدیمی XLSX تعداد اعشار رشته فرمت را بهعنوان یک کل میخواست، که فقط به بخش اول نگاه میکرد و % را نادیده میگرفت، و بعد با Round گرد میکرد. یک 0.1234 با فرمت 0.0% بهعنوان 0.1 ذخیره میشد، که 10% است بهجای 12.3% روی صفحه. یک 2.5 با فرمت 0 بهجای 3، عدد 2 ذخیره میشد. مقادیر منفی در فرمتی مثل 0.00;(0.0) به دو رقم اعشار بخش مثبت گرد میشدند. از v2.384.57 موتور XLSX همان روتین مشترک موتور کلاسیک را صدا میزند، که در همان نسخه پشتیبانی کامای مقیاسکننده را هم گرفته بود
موتور کلاسیک: مقدار TRUE میشد -1
موتور کلاسیک گرد کردنش را با VarIsNumeric نگهبانی میکرد، و VarIsNumeric برای یک Variant از نوع varBoolean مقدار True برمیگرداند. تبدیل آن Variant با Double(V) نتیجهاش -1 است، چون یک True به سبک COM بهشکل -1 ذخیره میشود. فرمولی مثل =A1>0 در سلولی با فرمت 0.00 پس از محاسبه مجدد بهشکل عدد -1 درمیآمد. از v2.384.57 نتیجههای Boolean قبل از هر تست عددی حذف میشوند، و نتیجه منطقی در هر دو موتور نتیجه منطقی میماند
فرمتهای زمان-سپریشده بهعنوان رنگ خوانده میشدند (v2.384.9)
باگ سوم در مدل فرمت عدد بود نه در گرد کردن. parser هر token براکتداری را که شرط نبود رنگ طبقهبندی میکرد، پس [h] و [mm] و [ss] هیچوقت بخششان را تاریخ/زمان علامت نمیزدند. نمایش آسیب نمیدید، چون فرمتبندی روی مسیر جداگانهای اجرا میشود، اما precision as displayed برای رد کردن مقادیر زمانی به همان فلگ تکیه دارد. یک مدتزمان پنجثانیهای برابر 5/86400 از یک روز است، حدود 0.0000579، و فرمتی مثل [ss].00 شبیه یک عدد معمولی دورقم-اعشاری به نظر میرسید، پس با خاموش بودن FullPrecision مدتزمان به 0.00 روز گرد میشد. از v2.384.9 یک ران براکتدار از یک حرف منفرد h یا m یا s بهعنوان token زمان-سپریشده parse میشود و بخش بهعنوان تاریخ/زمان رفتار میشود. همان نسخه تشخیص دقیقه در h:mm را هم فیکس کرد، جایی که دونقطه بین tokenها ساعت را از چشم parser پنهان میکرد
روشن کردن precision as displayed در HotXLS از دلفی
برای گرفتن مقدارهای ذخیرهشده همارز اکسل، فلگ را قبل از محاسبه مجددی که باید به آن احترام بگذارد ست کنید، بعد نتیجههای cacheشده را بخوانید یا ذخیره کنید. روی موتور XLSX مقدار FullPrecision یک فلگ ساده است: عوض کردنش نتیجههایی که یک Recalculate قبلی ذخیره کرده را بیاعتبار نمیکند، پس درست بعد از Create یا Open و قبل از اولین Recalculate ستش کنید. مثال از فرمول استفاده میکند چون HotXLS گرد کردن را همانجا اعمال میکند:
var
Wb: TXLSXWorkbook;
Sh: TXLSXWorksheet;
begin
Wb := TXLSXWorkbook.Create;
try
Sh := Wb.Sheets.Add('Totals');
Sh.Cells[1, 1].Value := 0.1234;
Sh.Cells[2, 1].Value := 2.5;
Sh.Cells[3, 1].Value := 12345.678;
Sh.Cells[1, 2].Formula := '=A1';
Sh.Cells[1, 2].NumberFormat := '0.0%'; // مقدار 12.3% را نشان میدهد
Sh.Cells[2, 2].Formula := '=A2';
Sh.Cells[2, 2].NumberFormat := '0'; // عدد 3 را نشان میدهد
Sh.Cells[3, 2].Formula := '=A3';
Sh.Cells[3, 2].NumberFormat := '0.0,'; // مقدار 12.3 را نشان میدهد (هزارگان)
// باید قبل از اولین Recalculate روی موتور XLSX ست شود
Wb.FullPrecision := False;
Wb.Recalculate;
// نتیجههای cacheشده حالا با Excel 16 جورند: مقدارهای 0.123 و 3 و 12300.
// ثابتهای ستون A دقت کاملشان را نگه میدارند.
Assert(Abs(Double(Sh.Cells[1, 2].Value) - 0.123) < 1E-12);
Assert(Double(Sh.Cells[2, 2].Value) = 3);
Assert(Double(Sh.Cells[3, 2].Value) = 12300);
Wb.SaveAs('totals.xlsx'); // مینویسد <calcPr fullPrecision="0"/>
finally
Wb.Free;
end;
end;
موتور کلاسیک همانطور رفتار میکند، با یک راحتی: assign کردن TXLSWorkbook.UseFullPrecision هر فرمول در گراف وابستگی را dirty علامت میزند، پس Recalculate بعدی کل workbook را زیر قاعده جدید دوباره ارزیابی میکند. عوض کردن یک NumberFormat وقتی گزینه روشن است هم سلولهای فرمولدار مرتبط را dirty میکند، چون حالا فرمت است که مقدار ذخیرهشده را تصمیم میگیرد. توجه کنید Recalculate کلاسیک تعداد سلولهای فرمولداری را که نتوانست ارزیابی کند برمیگرداند، پس صفر یعنی موفقیت:
var
Wb: TXLSWorkbook;
Sh: TXLSWorksheet;
begin
Wb := TXLSWorkbook.Create;
try
Sh := Wb.Sheets.Add;
Sh.Range['A1', 'A1'].Value := -1.2345;
Sh.Range['B1', 'B1'].Formula := '=A1';
Sh.Range['B1', 'B1'].NumberFormat := '0.00;(0.0)';
Sh.Range['C1', 'C1'].Formula := '=A1<0';
Sh.Range['C1', 'C1'].NumberFormat := '0.00';
Wb.UseFullPrecision := False; // همه فرمولها را dirty علامت میزند
if Wb.Recalculate <> 0 then
raise Exception.Create('Some formulas could not be evaluated');
// مقدار B1 برابر -1.2: بخش منفی یعنی "(0.0)" یک رقم اعشار نشان میدهد
// مقدار C1 منطقی و برابر True میماند (buildهای قبل از v2.384.57 عدد -1 ذخیره میکردند)
Wb.SaveAs('report.xls'); // رکورد CalcPrecision با fFullPrec = 0
finally
Wb.Free;
end;
end;
هر دو موتور فلگی را که همراه فایل میآید هم محترم میشمارند. یک workbook ذخیرهشده با گزینه روشن را باز کنید و FullPrecision یا UseFullPrecision از قبل False است، پس یک Recalculate بعد از load دقیقاً همانطور گرد میکند که اکسل میکرد. اگر فقط لازم است اعدادی را که اکسل از قبل ذخیره کرده بخوانید، میتوانید محاسبه مجدد را کلاً رد کنید، همانطور که در خواندن مقادیر cacheشده فرمول بدون محاسبه مجدد توضیح داده شده. برای اینکه شمارههای سریال و فرمتهای تاریخ با مدل فرمتی که چک تاریخ/زمان را میراند چطور کنار هم میآیند، شمارههای سریال تاریخ اکسل، سیستم 1904 و numFmt در دلفی را ببینید
کی precision as displayed را روشن کنیم و کی نه؟
Precision as displayed را فقط وقتی روشن کنید که اعداد ذخیرهشده workbook باید با اعداد نمایشیاش برابر باشند، و قبول کنید که رقمهای اضافی برای همیشه از دست میروند. حالت مشروع کلاسیک یک جدول مالی است که ستونهای مبلغهای گردشده باید با جمع گردشده روی صفحه بخوانند، بدون کسرهای پنهان سنی که جمعی یک واحد آخر خطا تولید کنند. مچ کردن workbook موجود مشتری که از قبل گزینهاش ست شده دلیل خوب دیگر است، و HotXLS فلگ را در round-trip حفظ میکند تا شما بیسروصدا آنها را به دقت کامل برنگردانید
در بیشتر بقیه موقعیتها ازش دوری کنید:
- داده مهندسی و علمی. گرد کردن یک اندازهگیری چون کسی برای گزارش فرمت دورقم-اعشاری انتخاب کرده اطلاعاتی را نابود میکند که هیچ تغییر فرمت بعدی برنمیگرداند
- درصدها با فرمتهای درشت. فرمت
0%فقط دو رقم اعشار از نسبت ذخیرهشده نگه میدارد، پس 0.1234 میشود 0.12، و هر فرمول پاییندستی که سلول را میخواند با 0.12 کار میکند - نمایشهای مقیاسشده. فرمت
0,یا0.0,که برای نمایش هزارگان استفاده میشود مقدار ذخیرهشده را به هزارگان یا صدگان گرد میکند، که بهندرت قصد کسی است که فرمت را انتخاب کرده - قالبهای مشترک. فلگ در سطح کل workbook است. هر کسی که بعداً یک شیت اضافه کند رفتار را به ارث میبرد، معمولاً بیآنکه بداند روشن است
اگر چیزی که واقعاً میخواهید نتیجههای گردشده در چند سلول مشخص است، بهجایش ROUND را داخل همان فرمولها بنویسید. ROUND صریح است، محدود به سلول است، برای هر کسی که فرمول را میخواند دیده میشود، و توسط موتور فرمول HotXLS مثل هر تابع دیگری ارزیابی میشود، بدون هیچ اثر جانبی در سطح workbook
مرجع سریع precision as displayed
- فلگ فایل: رکورد CalcPrecision یعنی
$000EباfFullPrec= 0 در BIFF8 ([MS-XLS] §2.4.35)، وcalcPr fullPrecision="0"در XLSX (بخش 1 از ECMA-376) - سوییچهای HotXLS:
TXLSXWorkbook.FullPrecision := FalseوTXLSWorkbook.UseFullPrecision := False، هر دو پیشفرض True - بخش: با علامت مقدار محاسبهشده انتخاب میشود؛ بخش سوم فقط برای دقیقاً صفر
- ارقام: جایگاههای اعشار، بهعلاوه دو تا برای هر
%، منهای سه برای هر کامای مقیاسکننده؛ شمارش میتواند منفی شود - گرد کردن: نصف دور از صفر با تحمل چند-ulp، پس 2.5 میشود 3، -2.5 میشود -3 و 1.005 میشود 1.01
- رد میشوند: General و تاریخ/زمان و زمان سپریشده، علمی، کسر، متن، Boolean و مقادیر خطا
- دامنه در HotXLS: نتیجههای فرمول موقع محاسبه؛ ثابتها همانطور که assign شدهاند ذخیره میشوند
- موتور XLSX: مقدار
FullPrecisionرا قبل از اولینRecalculateست کنید؛ setter کلاسیک خودش همه فرمولها را دوباره dirty میکند - نسخهها: از v2.384.57 در هر دو موتور با Excel 16 مچ است؛ فرمتهای زمان-سپریشده از v2.384.9 محافظت میشوند
HotXLS کارپوشههای XLS و XLSX را بهطور بومی از Delphi و C++Builder میخواند و مینویسد و محاسبه میکند، از جمله گزینههای محاسبه workbook که اینجا پوشش داده شد. جزئیات و نسخهها و دانلود آزمایشی در صفحه کامپوننت صفحهگسترده HotXLS در Delphi است