تابع صيغة مشتركة في XLSX لا يحمل نص صيغة. عنصره <f t="shared" si="N"/> يشير إلى خلية سيد في موضع آخر من الورقة، ويجب أن يعيد القارئ بناء النص بإزاحة صيغة السيد بفارق الصف والعمود. يقوم مكوّن HotXLS لـDelphi وC++Builder بذلك التوسيع عند وقت الفتح، بحيث يُبلغ كل تابع عن صيغة كاملة
إن كنت قد حمّلت يومًا ملف XLSX من العالم الحقيقي في مكتبة خارجية ووجدت عمودًا من ألف صيغة يحمل نصًا في خلية واحدة بالضبط وسلاسل فارغة في الـ999 الأخرى، فقد صادفت هذه السمة من الجانب الخاطئ. لا شيء تالف. الملف يفعل ما يسمح له ECMA-376 بفعله، والقارئ توقف ببساطة عند النقطة التي توقف عندها XML
لماذا خلية الصيغة المشتركة فارغة؟
لأن الصيغة تخزّن الصيغة مرة واحدة عمدًا. في ECMA-376 الجزء 1 وISO/IEC 29500-1، يحمل عنصر <f> (§18.3.1.40) سمة t من نوع ST_CellFormulaType، والقيمة shared تعني أن هذه الخلية تشارك في مجموعة يعرّفها سمة si. خلية واحدة بالضبط في المجموعة، السيد، تحمل أيضًا سمة ref تعطي النطاق الذي تنطبق عليه المجموعة، وتلك الخلية وحدها تحمل نص الصيغة كمحتوى عنصر. كل خلية أخرى في المجموعة تابع. تكرر t="shared" وsi نفسه، ومحتوى عنصرها فارغ. يكتب Excel هذه المجموعات بعدوانية، لأن ملء نازل fill-down عبر عمود من 200,000 صف ينهار من 200,000 سلسلة صيغة إلى سلسلة واحدة زائد 199,999 عنصر عنصر نائب صغير. التوفير حقيقي والكلفة تهبط بالكامل على القارئ: بدون التوسيع، التابع لا معنى له بذاته
الإزاحة ترجمة، لا نسخ نص
تحلّ HotXLS تابعًا بتحديد موقع السيد المسجَّل تحت si نفسه، وحساب فارق الصف والعمود من مرساة السيد إلى الخلية الحالية، وترجمة كل إشارة في صيغة السيد بذلك الفارق. الأبعاد النسبية تتحرك، والأبعاد المطلقة لا تتحرك، والإشارات المختلطة تحرّك نصفها غير المطلق فقط. السلاسل الحرفية النصية تُتخطى كليًا، فصيغة تحتوي مصادفة النص "A1" تبقي ذلك النص دون تغيير في كل تابع
const
// xl/worksheets/sheet1.xml, trimmed to the interesting cells
SheetXml: WideString=
'<row r="1"><c r="A1"><v>1</v></c>'+
'<c r="B1"><f t="shared" si="4" ref="B1:B3">'+
'A1+$A$1+A$1+$A1+"A1"+SUM(A1:A2)</f><v>7</v></c></row>'+
'<row r="2"><c r="B2"><f t="shared" si="4"/><v>8</v></c></row>'+
'<row r="3"><c r="B3"><f t="shared" si="4"></f><v>9</v></c></row>';
var
Wb: TXLSXWorkbook;
Sh: TXLSXWorksheet;
begin
Wb:= TXLSXWorkbook.Create;
try
Wb.Open(FileName);
Sh:= Wb.Sheets[1];
// Master, verbatim
// B1 -> A1+$A$1+A$1+$A1+"A1"+SUM(A1:A2)
// Follower one row down: relative row moves, absolute row frozen,
// the mixed A$1 keeps its row, and the literal stays a literal
// B2 -> A2+$A$1+A$1+$A2+"A1"+SUM(A2:A3)
ShowMessage(Sh.Cells[2, 2].Formula);
finally
Wb.Free;
end;
end;
سمة ref بوابة، لا زخرفة. تابع تقع إحداثياته خارج النطاق المُنطبَق للسيد لا يُوسَّع، لأن الملف عندئذ يقدّم ادعاءً لا تدعمه المجموعة. بالمثل، حين تدفع الإزاحة إشارة فوق الصف واحد أو يسار العمود A، تُصدر HotXLS #REF! لتلك الرمزية token بدل تثبيتها بصمت، وهذا ما كان Excel نفسه سينتجه للتعديل نفسه. هذه الترجمة قريبة من إعادة كتابة الإشارات التي تحدث حين تُدرِج أو تحذف صفوفًا، لكنها ليست الشيء نفسه. لذلك المسار قواعده الخاصة بشأن ما يفعله نطاق حين يقطعه تعديل، وهو مشروح بشكل منفصل في المقال عن تعديل إشارات الصيغ أثناء الإدراج والحذف. التوسيع المشترك أبسط: إنه إزاحة صرفة من مرساة معروفة، تُطبَّق مرة واحدة، عند وقت التحليل
أي أشكال إشارات يجب أن يغطيها المُزيح؟
كلها، وإلا كان التوسيع علّة فقدان بيانات متنكرة. مُزيح ساذج يفهم فقط A1 وA1:B2 سيُفسد أو يُسقط الأشكال الأكثر غرابة، والمصنَّفات الحقيقية مليئة بها. يتعرّف مترجم الصيغة المشتركة في HotXLS على عائلة A1 بأكملها قبل أن يقرر ما يحرّكه. إشارات المصنَّفات الخارجية مثل [Book.xlsx]Sheet1!A1 والإشارات ثلاثية الأبعاد مثل Sheet1:Sheet3!A1 تبقي بادئتها سليمة بينما تُزاح إشارة الخلية اللاحقة. أسماء الأوراق المقتبَسة تنجو، بما فيها الحالة الشائكة حين تُسمَّى الورقة حرفيًا A1، فـ'A1'!A1 يزيح فقط الجزء بعد علامة التعجب. العمود الكامل A:A يحرّك بُعده العمودي فقط ولا شيء غيره؛ الصف الكامل 1:1 يحرّك بُعده الأفقي فقط ولا شيء غيره؛ $A:$A لا يتحرك على الإطلاق. إشارات الجداول المهيكَلة structured مثل Table[A1] تُترَك دون مساس، لأن الجزء بين قوسين اسم عمود، لا إحداثية
// One master, expanded two columns to the right and zero rows down.
// Master D1: A1+A:A+$A:$A
// F1 : C1+C:C+$A:$A
//
// One master, expanded three rows down and zero columns across.
// Master A1: B1+$C$1+"A1"+A:A+1:1+'Data'!A1+LOG10(A1)+Table[A1]+'A1'!A1
// A3 : B3+$C$1+"A1"+A:A+3:3+'Data'!A3+LOG10(A3)+Table[A1]+'A1'!A3
//
// Note what did NOT move in the second line: the absolute $C$1, the
// string literal "A1", the whole column A:A under a pure row delta,
// the function name LOG10, and the structured reference Table[A1]
أسماء الدوال هي الفخ الصامت هنا. ماسح رموز token scanner يلتقط حروفًا يتبعها أرقام سيعيد بسعادة كتابة LOG10 إلى LOG11 صف واحد أسفل. تفرض HotXLS حد إشارة قبل رمز مرشَّح وبعده، فمعرّف يستمر إلى حرف أو رقم أو شرطة سفلية أو نقطة أو قوس افتتاح ليس إشارة خلية. إن كنت تعمل في عائلة الترميز الأخرى، فإن مشكلة الحد نفسها تظهر بشكل مختلف، ويغطي مقال ترميز R1C1 أين يفترق النموذجان
لماذا يبتلع عنصر f ذاتي الإغلاق القيمة التالية؟
لأن عنصرًا ذاتي الإغلاق لا ينتج حدث نهاية-عنصر. هذه أغلى علّة واحدة في السمة بأكملها، وهي ليست خاصة بمحلّل XML واحد. في TXMLReader، يرفع <f t="shared" si="4"/> حدث Element واحدًا بالضبط بـIsEmptyElement مضبوطة إلى True، ولا يرفع أبدًا EndElement المطابق. محلّل يغلق حالة التقاط صيغته فقط عند EndElement يبقى إذن داخل الصيغة، والنص التالي الذي يراه، وهو النتيجة المخزَّنة مؤقتًا داخل <v>، يُلحَق بمخزن الصيغة المؤقت. والأسوأ أن الحالة تنجو عبر حد الخلية، فالخلية التالية التي تملك <f> حقيقيًا يُمتَص نص صيغتها في الخلية السابقة. الإصلاح هو إنهاء حالة الصيغة عند حدث Element نفسه كلما كانت IsEmptyElement True، وتشغيل حل التابع بأكمله هناك بدل الانتظار. هذا يعني قراءة t وsi وref وaca وca من السمات، وتطبيق التوسيع المشترك، وكتابة سمات إعادة الحساب على الخلية، ومسح الحالة المشتركة، كل ذلك داخل الفرع الذي يعالج العنصر الفارغ. لاحظ أن الصيغة تسمح بكلا التهجئتين، <f t="shared" si="4"/> و<f t="shared" si="4"></f>، والثانية ترفع فعلًا EndElement. القارئ الصحيح يجب أن يعالج الزوج بالتماثل نفسه، ولهذا تغطي HotXLS كلا التهجئتين في ملف الانحدار نفسه
قيم si متناثرة وغير مرتَّبة وطابور الانتظار
سمة si عدد صحيح غير موقَّع يوفّره الملف، لا موضع مصفوفة تتحكم به. لا شيء في المخطط يفرض أن تكون الفهارس المشتركة كثيفة، أو تبدأ من صفر، أو تظهر بترتيب تصاعدي، ولا شيء يمنع ملفًا عدائيًا أو غريبًا فحسب من استخدام si="4294967290" على الخلية الأولى. تحديد حجم مصفوفة بحث من أكبر si ملحوظ هو إذن بدائية استنزاف ذاكرة، لا تحسين. تُبقي HotXLS مسار فتح المصنَّف على جدول متناثر مرتَّب بدلًا من ذلك: تُسجَّل المجموعات المشتركة تحت مفتاحها الصحيح في TStringList مرتَّبة، ما يجعل البحث بحثًا ثنائيًا binary search عبر عدد المجموعات الموجودة فعلًا أيًا كان، دون علاقة بالحجم العددي للفهارس. الترتيب هو النصف الثاني من المشكلة. عادة يسبق السيد تابعيه بترتيب المستند، لكن هذه اتفاقية لا قاعدة، فأي تابع يتعذر عليه حل si الخاص به في لحظة تحليله يدخل طابور انتظار pending queue. حين تنتهي الورقة، يُعاد تشغيل الطابور مقابل الجدول المكتمل الآن، ويحل السادة المتأخرون أيتامهم. الخلايا التي لا تجد سيدًا أبدًا تبقي صيغة فارغة، وهي النتيجة الصادقة لملف يشير إلى مجموعة لم يعرّفها قط
توسيع الصيغ المشتركة دون تحميل المصنَّف
تواجه القرّاءات المتدفقة المتطلَّب نفسه تحت ميزانية ذاكرة أضيق كثيرًا، وتحلّها بجدول محلي للورقة. يوسّع كل من TXLSDirectReader وTXLSRowCursor التابعين إلى صيغ كاملة لكل خلية بينما يحافظان على سلوكيهما المحدودَي الذاكرة والإسقاط projection، بحيث ما تزال تمريرة أمامية فقط عبر ورقة من 300 ميغابايت تسلّمك نص صيغة حقيقيًا
var
Reader: TXLSDirectReader;
Cursor: TXLSRowCursor;
begin
// Projection: only rows 2..3, only column A. The master lives in row 1,
// outside the projection, and is still parsed so the followers resolve
Reader:= TXLSDirectReader.Create;
try
Reader.FirstRow:= 2;
Reader.LastRow:= 3;
Reader.IncludeColumn(1);
Reader.OnCell:= HandleCell; // Cell.Formula is fully expanded here
Reader.ReadFile(FileName);
finally
Reader.Free;
end;
// Forward-only row traversal, same expansion
Cursor:= TXLSRowCursor.Create;
try
Cursor.Open(FileName);
if Cursor.FindFirst then
repeat
if Cursor.CellCount > 0 then
WriteLn(Cursor.RowIndex, ': ', Cursor.Cells[0].Formula);
until not Cursor.FindNext;
finally
Cursor.Free;
end;
end;
قيدان ينتجان عن ذلك التصميم. أولًا، الإسقاط لا يمكنه تخطي السيد أبدًا. فلتر صف مضبوط بـFirstRow وLastRow، أو فلتر عمود مبني بـIncludeColumn، قد يتخطى إصدار خلية السيد إلى استدعاء الرجوع الخاص بك، لكن المُحلِّل يجب أن يسجّل مع ذلك si الخاص بها وإحداثيات مرساتها ونطاقها المُنطبَق ونص صيغتها، وإلا فإن كل تابع داخل الإسقاط يُحَل إلى لا شيء. فقط العمل من جانب التابع، الإزاحة وفك ترميز القيمة، آمن التخطي. ثانيًا، الجدول لكل ورقة عمل ودورة حياته يجب إدارتها صراحة: تحتفظ TXLSRowCursor بمثيل واحد طوال مدة تمريرة ورقة وتمسحه عند إعادة التشغيل، وتبديل الورقة، ونهاية الملف، والاستثناء، والإغلاق، بحيث لا يمكن أبدًا لمجموعة مُعرَّفة على الورقة الأولى أن تتسرب إلى الورقة الثانية. ولأن المسار المتدفق حلقة ساخنة hot loop، فهو يستخدم تجزئة عدد صحيح بعنونة مفتوحة open-addressing بدل جدول السلاسل النصية المرتَّب، ما يتجنب تحويل عدد صحيح إلى سلسلة نصية لكل خلية
ماذا يحدث عند الحفظ، وأين الحدود
بمجرد توسيع تابع يصبح صيغة عادية، وتكتبه HotXLS مجددًا كعنصر <f> مستقل دون t="shared" ودون si. رحلة الذهاب والإياب مستقرة وتنجو نتائج <v> المخزَّنة مؤقتًا، لكن المُخرَج أكبر من المدخل لورقة مُشترَكة بكثافة، والتجميع الذي أنشأه Excel لا يُعاد بناؤه عند الحفظ. إن كانت الدقة على مستوى البايت للمجموعات المشتركة تهمك أكثر من امتلاك نص صيغة حقيقي في كل خلية، فهذه هي المقايضة التي تقبلها. جانب XLS مختلف بالمناسبة: يملك سجلّ BIFF8 SHRFMLA ترميزه الخاص وكاتبه الخاص، بمفتاح مجموعة مشتركة على المصنَّف
شيئان ذوا صلة ليسا صيغتين مشتركتين صراحة رغم مشاركتهما عنصر <f>. صيغ مصفوفة CSE القديمة تستخدم t="array" بـref يغطي النطاق المُرسَى، والمصفوفات الديناميكية تستخدم تهجئة t="array" نفسها لكن تُعرَّف بسمة cm تتسلسل عبر cellMetadata إلى سجلّ XLDAPR. معاملة خلية انسكاب spill لمصفوفة ديناميكية كتابع مشترك أو CSE علّة صحة حقيقية، والفصل بينهما مشروح في المقال عن المصفوفات الديناميكية وصيغ الانسكاب. اقرأ الحالات الثلاث كثلاثة محلّلات تصادف أنها تشارك اسم وسم tag، ويبقى الكود صادقًا
توسيع الصيغة المشتركة، والقرّاءات المتدفقة، ومترجم الإشارات الموصوفة هنا تُشحن ضمن مكوّن HotXLS Excel لـDelphi وC++Builder؛ وتحمل صفحة المنتج المرجع الكامل لواجهة برمجة الصيغ والقراءة المباشرة، بما في ذلك خصائص الإسقاط المستخدَمة أعلاه