Сабстрім PivotCache у BIFF зберігає кешований набір даних PivotTable окремо від подання, яке його показує, і HotXLS читає та пише той сабстрім, інспектуючи тіла записів, а не довіряючи номерам записів. Ця відмінність — уся історія: той самий номер запису несе дві несумісні розкладки тіла залежно від того, який райтер продукував файл, тож читач вирішує фреймування з першого тіла запису, яке бачить
Ви зустрічаєте цей шар у мить, коли PivotTable мусить пережити рейс туди й назад. Pivot-подання без свого кешу — оболонка, і Excel перебудує кеш з вихідного діапазону, коли відкриє файл, що добре аж до моменту, коли вихідний діапазон зник, дані були вставлені із запиту чи книга — архівне закриття, яке не повинно змінюватися, коли хтось його відкриє
Дві структури, два місця у файлі
Кешовані дані й визначення кешу живуть у різних частинах книги, і змішати їх — перше, що треба зробити правильно. Кешовані записи формують власний сабстрім, даний у [MS-XLS] §2.1.7.12 як PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Зверніть увагу, чого немає: BOF на чолі тієї продукції відсутній
Визначення сидить натомість у глобальних даних книги, як PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), розташоване після записів форматування й перед записами BoundSheet і Country. Тож один кеш описаний у двох місцях, розділених сотнями записів, і зв'язок між ними — ідентифікатор потоку, який мусить збігатися в трьох місцях одразу
Кожен кеш належить потоку під _SX_DB_CUR, чиє ім'я — чотирицифрове прописне шістнадцяткове написання його ідентифікатора. SXStreamID.idStm, поле idstm, повторене в заголовку SXDB, і те ім'я потоку мусять усі збігатися. Коли ви виділяєте новий ідентифікатор, спершу зарезервуйте кожен номер, уже прочитаний з файлу, інакше новий кеш може заявити номер, що належить старішому кешу, до якого читач ще не дійшов
Ще один ідентифікатор ловить людей. Значення iCache в pivot-поданні — це нуль-базована позиція відповідного SXStreamID у глобальній послідовності, а не ідентифікатор кешу, який ви можете обрати. При записі його треба мапити з об'єкта кешу на його фактичну вихідну позицію, а наявні подання — перенумеровувати разом із ним, інакше оновлення одного кешу мовчки наводить подання на інший
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.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 — простий запис властивості, який не виділяє пам'ять, і внутрішній шлях росту ініціалізує лише новододані рядки, тож кеш, чия кількість була виставлена через заголовковий шлях, може закінчити з нульовою сіткою індексів. Записи в RecordIndices тоді викидаються без помилки. Виставлення кількості в нуль і назад відновлює сітку, і це мусить статися після того, як кожне поле додано, бо ширина рядка приходить від кількості полів
Чому номер запису не може сказати вам розкладку тіла?
Бо номери записів і розкладки тіл змінювалися в різний час, тож відображення між ними — не функція. Один номер у легасі-наборі з'являється лише у файлах від старіших райтерів, що робить його надійним сигналом в один бік. Інший номер справді двозначний: він з'являється і в правильних файлах, і в діапазоні проміжних версій, що використовували новий номер зі старою розкладкою тіла
Тому фреймування мусить вирішуватися з тіла, і раз на сабстрім кешу, а не на запис. HotXLS зафіксовує діалект з довжини першого запису SXDBB у кожному сабстрімі. У фреймуванні специфікації один SXDBB тримає рівно один кешовий запис, тож його довжина дорівнює одній ширині рядка. У старішому пакованому фреймуванні перший запис тримає стільки рядків, скільки влізло, тож для будь-якого кешу з більше ніж одним рядком це щонайменше дві ширини рядка. Порівняння вирішальне щоразу, коли два передбачення розходяться
Коли вони не розходяться, читач бере прочитання специфікації, за принципом, що файли, написані Excel, переважають файли, написані проміжною збіркою. Та сліпа зона вузька за конструкцією і, коли вона все ж трапляється, файл сам досі програвється байт у байт. Постраждають лише типізовані індекси, виставлені викликаючим
Ширина індексу живе в іншому записі
SXDBB (§2.4.276) несе один індекс на кешове поле, у якого виставлено прапорець різних значень, у порядку полів, і ширина кожного індексу вирішується деінде: відповідний запис поля SXFDB (§2.4.283) декларує прапорець коротких елементів, і той прапорець каже, чи індекс займає два байти, чи один. Два записи, один імпліцитний контракт і одне речення в специфікації, що їх з'єднує
Це зчеплення — рівно те місце, де саморобне кодування помиляється. Раніший райтер HotXLS паковав кожне поле в мінімальну кількість бітів, вистилаючи до межі байта між рядками, що захищається ізольовано і прямо суперечить ширині, яку той самий райтер щойно декларував у SXFDB. Поле з трьома різними значеннями описувалося як завширшки один байт в одному записі й займало два біти в іншому. Фікс полягав не в тому, щоб виправити арифметику, а в тому, щоб витягти рішення про ширину в одну функцію, яку викликають обидва емітери, тож два записи більше не можуть роз'їхатися. Це той самий клас дефекту, описаний у дрейфі декларації довжини запису BIFF, де декларований розмір і фактичне тіло розходяться
Наслідок того, щоб не читати ці записи взагалі, варто сформулювати, бо його легко недооцінити. Коли читач пропускав індекси записів, кожен кеш, завантажений з файлу, повідомляв індекс нуль для кожного поля кожного рядка, а це означає, що кожен рядок вказував на перше значення кожного поля. Це не просто збідніла інтроспекція: шлях оцінювання pivot і шлях заповнення кеш-у-комірку обидва споживають ту сітку. І round-trip тест не може це виявити, бо кеш, що досі на сирому відтворенні, пишеться назад із його оригінальних байтів
// Прапорці походження кажуть, що ви тримаєте і що можна переписати
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;
Коли переписування кешу без втрат?
Лише коли три умови тримаються разом, і CanUpgradeFraming — єдина властивість, що відповідає на питання. Кеш мусить досі бути на сирому відтворенні, сабстрім мусить бути в одному з фреймувань, які ця бібліотека раніше писала неправильно, і читач мусив збудувати повну типізовану модель кожного запису в ньому. Кеш, написаний Excel, ніколи не кваліфікується, бо його сабстрім несе записи, для яких HotXLS не має моделі, і перевипуск з моделі їх втратив би
Тест повноти суворіший, ніж здається спершу. Запис, який читач утримував лише як непрозорі байти, позначає модель неповною. Так само і декларована кількість записів формул, яку емітер не може відтворити, бо перевипуск переписав би декларацію кількох записів формул як декларацію жодного, а значення у файлі, яке не можна відтворити, еквівалентне запису, який не можна відтворити
Навмисний консерватизм пронизує й райтера. Індекси стискаються в легальний діапазон, а не кодуються як out-of-band сентинел, бо специфікація визначає індекс у послідовність різних значень і нічого іншого, а порожня комірка сама є значенням у тій послідовності. Тіло кешового запису, що перевищує стелю запису BIFF, взагалі не пишеться, що вимагало б тисяч кешових полів і все одно недосяжне в межах ліміту колонок BIFF8; fallback у тому, що Excel оновлює з вихідного діапазону, а це визначена поведінка, а не пошкоджений файл
Дати несуть останню міжзаписову залежність. Конвертація серіал-у-дату залежить від системи дат книги, і емітер записів не бачить книгу, тож вибір базової дати передається як параметр, що має дефолт на систему 1900 і постачається шляхом збереження на рівні книги. За системи 1900 серійний номер — значення безпосередньо; система 1904 відрізняється на 1462 дні. Ширше поводження з серіалами дат — у серіалах дат, системі 1904 і числових форматах
Якщо ви працюєте на шарі подання, а не кешу, записи, що описують видимий pivot, покриті в наборі записів PivotTable BIFF8, а поведінка на боці обчислення — у обчислюваних полях, обчислюваних елементах та оновленні. Усі три шари постачаються в HotXLS Delphi spreadsheet component, що й робить можливим завантажити легасі-книгу, роздивитися, що її кеш справді містить, і вирішити, чи безпечно її переписувати, перш ніж робити це