תת-הזרם של PivotCache ב-BIFF שומר את מערך הנתונים השמור של PivotTable בנפרד מהתצוגה שמציגה אותו, ו-HotXLS קורא וכותב את תת-הזרם הזה על ידי בחינה של גופי רשומות במקום לסמוך על מספרי רשומות. ההבחנה הזאת היא כל הסיפור: אותו מספר רשומה נושא שתי פריסות גוף בלתי תואמות בהתאם לכותב שייצר את הקובץ, ולכן הקורא מכריע את התיחום מהגוף של הרשומה הראשונה שנתקל בה
את השכבה הזאת פוגשים ברגע ש-PivotTable צריך לשרוד round-trip. תצוגת pivot בלי ה-cache שלה היא קליפה ריקה, ו-Excel יבנה מחדש את ה-cache מטווח המקור כשהוא פותח את הקובץ, וזה בסדר עד הרגע שבו טווח המקור נעלם, הנתונים הודבקו מתוך query, או שה-workbook הוא סגירה ארכיונית שחייבת להישאר בלי שינוי כשמישהו פותח אותה
שני מבנים, שני מקומות בקובץ
הנתונים השמורים וההגדרה של ה-cache חיים בחלקים שונים של ה-workbook, והדבר הראשון שצריך לעשות נכון הוא לא לבלבל ביניהם. הרשומות השמורות מרכיבות תת-זרם משלהן, שמנוסח ב-[MS-XLS] §2.1.7.12 כ-PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. שים לב למה שנעדר שם: אין BOF בראש ה-production הזה
ההגדרה יושבת דווקא ב-globals של ה-workbook, בתור PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), ממוקמת אחרי רשומות העיצוב ולפני רשומות ה-BoundSheet וה-Country. כך מתואר cache בודד בשני מקומות שמפרידות ביניהן מאות רשומות, והקישור ביניהם הוא מזהה stream שחייב להסכים בשלושה מקומות בבת אחת
כל cache שייך ל-stream תחת _SX_DB_CUR ששמו הוא האיות ההקסדצימלי בן ארבע הספרות באותיות גדולות של המזהה שלו. SXStreamID.idStm, השדה idstm שחוזר בכותרת של ה-SXDB, ושם ה-stream הזה חייבים כולם להתאים. כשאתה מקצה מזהה חדש, שריין קודם כל כל מספר שכבר נקרא מהקובץ, אחרת cache חדש יכול לתבוע מספר ששייך ל-cache ישן שהקורא עוד לא הגיע אליו
עוד מזהה אחד תופס אנשים. הערך iCache בתצוגת pivot הוא המיקום מבוסס-האפס של ה-SXStreamID המתאים ברצף הגלובלי, ולא מזהה cache שאתה בוחר. בכתיבה צריך למפות אותו מאובייקט ה-cache למיקום הפלט בפועל שלו, ותצוגות קיימות צריכות לקבל מספור מחדש יחד איתו, אחרת שדרוג של cache אחד מפנה בשקט תצוגה ל-cache אחר לגמרי
var
Book: TXLSWorkbook;
Cache: TXLSPivotCache;
Field: TXLSPivotCacheField;
V: TXLSPivotCacheValue;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('sales.xls');
Cache := Book.PivotCaches.Add;
Cache.SourceRangeSheet := 'Data';
Cache.SourceFirstRow := 1; Cache.SourceFirstCol := 1;
Cache.SourceLastRow := 500; Cache.SourceLastCol := 6;
Cache.SourceDataType := 1; // SXVS SHEET, MS-XLS 2.4.317
Cache.RefreshOnLoad := False; // סומכים על הרשומות השמורות ב-cache
Cache.SaveData := True;
Field := Cache.AddField('Region', xlpcftString);
V.ValueType := xlpcftString;
V.StrValue := 'North';
Field.FindOrAddItem(V);
Cache.SetRecordCount(0); // מאפסים, ואז קובעים את גודל רשת הרשומות
Cache.SetRecordCount(500);
Book.StorePivotCaches;
finally
Book.Free;
end;
end;
ה-SetRecordCount הכפול אינו אמונה תפלה. RecordCount הוא כתיבת מאפיין פשוטה שלא מקצה זיכרון, ונתיב הגדילה הפנימי מאתחל רק את השורות שנוספו זה עתה, כך ש-cache שהמנייה שלו נקבעה דרך נתיב הכותרת יכול להגיע לרשת אינדקסים באורך אפס. כתיבות ל-RecordIndices נזרקות אז בלי שגיאה. קביעת המנייה לאפס ובחזרה מקימה את הרשת מחדש, וזה חייב לקרות אחרי שכל שדה נוסף, כי רוחב השורה נגזר ממספר השדות
למה מספר רשומה לא יכול לגלות לך את פריסת הגוף?
כי מספרי רשומות ופריסות גוף השתנו בזמנים שונים, ולכן המיפוי ביניהם אינו פונקציה. מספר אחד מקבוצת ה-legacy מופיע רק בקבצים של כותבים ישנים יותר, מה שהופך אותו לאות מהימן בכיוון אחד. מספר אחר הוא באמת דו-משמעי: הוא מופיע גם בקבצים תקינים וגם בטווח גרסאות ביניים שהשתמשו במספר החדש עם פריסת הגוף הישנה
את התיחום צריך לכן להכריע מהגוף, ופעם אחת לכל תת-זרם cache ולא לכל רשומה. HotXLS נועל את הדיאלקט מהאורך של רשומת ה-SXDBB הראשונה בכל תת-זרם. בתיחום של המפרט, SXDBB אחד מחזיק בדיוק רשומת cache אחת, כך שאורכו שווה לרוחב שורה אחת. בתיחום ה-packed הישן יותר, הרשומה הראשונה מחזיקה כמה שורות שרק נכנסות, כך שלכל cache עם יותר משורה אחת הוא בן לפחות שני רוחבי שורה. ההשוואה היא הכרעתית בכל פעם ששתי התחזיות חולקות
כשהן לא חולקות, הקורא לוקח את הקריאה של המפרט, על העיקרון שקבצים שכתב Excel מרובים על פני קבצים שכתב build ביניים. הנקודה העיוורת הזאת צרה מעצם הבנייה שלה, וגם כשהיא בכלל מתרחשת, הקובץ עצמו עדיין מנוגן בייט-בייט. רק האינדקסים הטיפוסיים שהספרייה חושפת לקוד שלך מושפעים
רוחב האינדקס חי ברשומה אחרת
SXDBB (§2.4.276) נושא אינדקס אחד לכל שדה cache שדגל ה-distinct-value שלו מוגדר, לפי סדר השדות, ואת רוחבו של כל אינדקס מחליטים במקום אחר: רשומת השדה SXFDB המתאימה (§2.4.283) מכריזה על דגל short-items, והדגל הזה אומר אם האינדקס תופס שני בייטים או אחד. שתי רשומות, חוזה מרומז אחד, ומשפט בודד במפרט שמחבר ביניהן
הצימוד הזה הוא בדיוק המקום שבו קידוד ביתי משתבש. כותב מוקדם יותר של HotXLS דחס כל שדה למספר הביטים המינימלי, עם ריפוד לגבול בייט בין שורות, מה שנראה סביר בפני עצמו וסותר ישירות את הרוחב שאותו כותב עצמו הכריז זה עתה ב-SXFDB. שדה עם שלושה ערכים נבדלים תואר ברוחב בייט אחד ברשומה אחת ותפס שני ביטים בשנייה. התיקון לא היה לתקן את האריתמטיקה אלא לחלץ את החלטת הרוחב לפונקציה אחת ששני ה-emitters קוראים לה, כך ששתי הרשומות כבר לא יכולות להתרחק זו מזו. זו אותה משפחת פגמים המתוארת בסטייה בהכרזת אורך רשומות BIFF, שבה גודל מוכרז וגוף בפועל נפרדים זה מזה
התוצאה של אי-קריאת הרשומות האלה בכלל שווה ניסוח, כי קל לזלזל בה. כשהקורא דילג על אינדקסי הרשומות, כל cache שנטען מקובץ דיווח אינדקס אפס לכל שדה של כל שורה, כלומר כל שורה הצביעה על הערך הראשון של כל שדה. זו לא רק אינטרוספקציה מופחתת: נתיב הערכת ה-pivot ונתיב המילוי מה-cache לתאים שניהם צורכים את הרשת הזאת. ובדיקת round-trip לא תתפוס את זה, כי cache שעדיין על raw replay נכתב בחזרה מהבייטים המקוריים שלו
// דגלי ה-provenance מגלים לך מה בידך ומה מותר לכתוב מחדש
if Cache.FromRawBlobs then
begin
Writeln('stream id : ', IntToHex(Cache.StreamId, 4));
Writeln('legacy framing : ', Cache.RawFramingIsLegacy);
Writeln('own storage : ', Cache.RawHasStorageStream);
Writeln('model complete : ', Cache.RawModelIsComplete);
// הפקה מחדש היא חסרת-אובדן רק כשלכל רשומה יש מודל כאן
if Cache.CanUpgradeFraming then
Writeln('safe to rewrite with the current emitters');
end;
מתי כתיבה מחדש של cache היא חסרת אובדן?
רק כששלושה תנאים מתקיימים יחד, ו-CanUpgradeFraming הוא המאפיין היחיד שעונה על השאלה. ה-cache חייב להיות עדיין על raw replay, תת-הזרם חייב להיות באחת מהפריסות שהספרייה הזאת כתבה בעבר בצורה שגויה, והקורא חייב לבנות מודל מטופוס ושלם של כל רשומה בו. cache ש-Excel כתב אף פעם לא עומד בתנאים, כי תת-הזרם שלו נושא רשומות של-HotXLS אין עבורן מודל, והפקה מחדש מהמודל הייתה משמיטה אותן
בדיקת השלמות מחמירה משימראה במבט ראשון. רשומה שהקורא שמר רק כבייטים אטומים מסמנת את המודל כבלתי שלם. כך גם מנייה מוכרזת של רשומות נוסחה שה-emitter לא יכול לשחזר, כי הפקה מחדש הייתה כותבת מחדש הכרזה על כמה רשומות נוסחה כהכרזה על אפס, וערך בקובץ שאי אפשר לשחזר שקול לרשומה שאי אפשר לשחזר
שמרנות מכוונת עוברת גם דרך הכותב. אינדקסים נחתכים לטווח החוקי במקום להיות מקודדים כ-sentinel מחוץ לתחום, כי המפרט מגדיר אינדקס אל רצף הערכים הנבדלים ושום דבר מעבר לזה, ותא ריק הוא בעצמו ערך ברצף הזה. גוף של רשומת cache שחורג מתקרת רשומות ה-BIFF אינו נכתב בכלל, מה שהיה דורש אלפי שדות cache ובלתי ניתן להשגה בגבול העמודות של BIFF8 בכל מקרה; ה-fallback הוא ש-Excel מרענן מטווח המקור, שזו התנהגות מוגדרת ולא קובץ פגום
תאריכים נושאים את תלות הבין-רשומתית האחרונה. ההמרה מ-serial לתאריך תלויה במערכת התאריכים של ה-workbook, וה-emitter של הרשומות לא רואה את ה-workbook, ולכן בחירת תאריך הבסיס מועברת כפרמטר שברירת המחדל שלו היא מערכת ה-1900 ונתיב השמירה ברמת ה-workbook מספק אותו. תחת מערכת ה-1900 המספר הסידורי הוא הערך ישירות; מערכת ה-1904 שונה ב-1462 ימים. הטיפול הרחב יותר במספרים סידוריים של תאריכים נמצא במספרים סידוריים של תאריכים, מערכת ה-1904 ופורמטי מספרים
אם אתה עובד בשכבת התצוגה ולא בשכבת ה-cache, הרשומות שמתארות את ה-pivot הנראה מכוסות בקבוצת רשומות ה-PivotTable של BIFF8, והתנהגות צד החישוב בשדות מחושבים, פריטים מחושבים ורענון. שלוש השכבות מגיעות כולן ברכיב הגיליונות של HotXLS ל-Delphi, וזה מה שמאפשר לטעון workbook ישן, לבחון מה ה-cache שלו באמת מכיל, ולהחליט אם כתיבה מחדש שלו בטוחה לפני שעושים זאת