HotXLS מאחסנת חותמות הזמן של מאפייני מסמך Excel בתור UTC בתוך הקובץ וחושפת אותן כזמן מקומי דרך ה-API: TXLSWorkbook.CreatedDate ו-LastSavedDate עבור .xls, TXLSXWorkbook.Created ו-Modified עבור .xlsx. מאז v2.384.48 שני המנועים ממירים זמן מקומי ל-UTC בכתיבה וחזרה בקריאה, לפי כללי שעון הקיץ שתקפים בתאריך של החותמת עצמה. הדרך לשם עברה שני תיקונים, ושני הבאגים שרדו מאותה סיבה מביכה: כל round trip אוטומטי עבר, בזמן שחלונית ה-File > Info של Excel הציגה יום שגוי או שעה שגויה. אם קראת את הסקירה שלנו על קביעת מאפייני מסמך Excel בדלפי, זה החלק שבו התאריכים מפסיקים להיות ערכים פשוטים
למה בדיקת שמירה ופתיחה מחדש הסתירה שגיאה של יום?
round trip עצמי הסתיר את השגיאה כי הכותב והקורא חלקו את אותו קבוע שגוי, והטעות ביטלה את עצמה. תאריך בערכת מאפיינים של OLE הוא FILETIME, מנייה של 64 סיביות בטיקים של 100 ננושניות מאז 1601-01-01 UTC ([MS-DTYP] §2.3.3), בזמן ש-TDateTime של דלפי סופר ימים מ-1899-12-30, אותו מוצא סידורי שמכוסה במספרים סידוריים של תאריכי Excel בדלפי ובמערכות 1900 מול 1904. הפער בין שני התקופות הוא 109205 ימים, ואפשר לבדוק את זה בלי לוח שנה: 25569 (ה-Unix epoch כ-TDateTime) בתוספת 109205 נותן 134774, ה-Unix epoch בימים של FILETIME. build של HotXLS לפני v2.384.17 השתמש ב-109206, כך שכל חותמת יצירה ושמירה נכתבה יום מאוחר מדי ונקראה יום מוקדם מדי. סוויטת הבדיקות ראתה את הערך שהיא עצמה הקצתה; Excel ראה את מחר
const
// ימים מתקופת ה-FILETIME (1601-01-01) אל תקופת ה-TDateTime (1899-12-30)
// בדיקה: 25569 + 109205 = 134774, ה-Unix epoch בימים של FILETIME
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// קודם עיגול למילישניות שלמות, ואז קנה מידה לטיקים של 100 ננו.
// קנה מידה ישיר של ה-Double לטיקים הופך 04:00 ל-03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
הערת העיגול בסקיצה הזאת היא הלקח השני, הקטן יותר, מאותו קוד. הכפלה ישירה של TDateTime שברי ב-864,000,000,000 טיקים ליום מאפשרת לשגיאת נקודה צפה בינארית לדלוף אל הספרות התחתונות, וחותמת של בדיוק 04:00 חזרה כ-03:59:59.9999. HotXLS v2.384.48 מעגלת למילישניות שלמות לפני קנה המידה, כך שערכים על גבי השעה העגולה שורדים את המסע בשלמותם. אותה מהדורה הוסיפה את שלב אזור הזמן שהסקיצה הזאת משאירה בכוונה מחוץ לתמונה, כי הקלט כאן כבר UTC
אילו מזהי מאפיינים של SummaryInformation מחזיקים את התאריכים?
בערכת המאפיינים \005SummaryInformation שמוגדרת ב-[MS-OLEPS], זמן היצירה יושב תחת מזהה המאפיין $0C (PIDSI_CREATE_DTM), זמן השמירה האחרונה תחת $0D (PIDSI_LASTSAVE_DTM), וזמן העריכה הכולל תחת $0A (PIDSI_EDITTIME). build ישנים של HotXLS כתבו את חותמת השמירה אל $0E, שהוא PIDSI_PAGECOUNT, כך של-Excel לא הייתה תאריך שמירה להציג ובמקום זאת מאפיין של ספירת עמודים שמחזיק חותמת זמן. מאז v2.384.17 הקורא מכבד גם את הפריסה המורשת הזאת: כש-$0D נעדר ו-$0E נושא VT_FILETIME, הערך נלקח כזמן השמירה האחרונה. כל PROPVARIANT שנקרא גם משוחרר עכשיו עם PropVariantClear, כי קובץ פגום יכול להחנות מחרוזת תחת כל אחד מהמזהים האלה. אם רוצים לראות את ה-streams האלה בעיניים, ההדרכה על קריאת קבצי OLE2 compound בדלפי בלי COM IStorage מראה איך להגיע אליהם
PIDSI_EDITTIME הוא המלכודת בתוך המלכודת. המאפיין מוגדר כ-VT_FILETIME אבל מחזיק משך זמן, את מספר הטיקים הגולמי שחלף בגודל 100 ננו ללא תוספת תקופה. הכותב הישן התייחס אליו כאל תאריך, חילק את EditTimeMinutes ב-1440 ודחף את התוצאה דרך המרת התקופה, כך ש-125 דקות עריכה נחתו בקובץ כ-299 שנים בערך. הקורא הנוכחי מזהה את הקידוד הזה לפי הגודל שלו: אף סשן עריכה אמיתי לא נמשך שלוש מאות שנים, ולכן מכל ערך של 109206 ימים ומעלה מוחסר ההיסט המורש לפני ש-EditTimeMinutes מתמלא
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// ערכי ה-API הם זמן מקומי; הקובץ מאחסן FILETIME של UTC
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS כותבת את מה שאתה מקצה, היא לא חותמת Now
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // משך זמן, נשמר כטיקים גולמיים
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
למה התאריכים ב-XLSX סטו בדיוק בהיסט אזור הזמן?
התאריכים ב-XLSX סטו בהיסט אזור הזמן כי dcterms:created ו-dcterms:modified ב-docProps/core.xml הם ערכי W3CDTF עם תג Z, שמשמעו UTC תחת מודל מאפייני הליבה של ECMA-376 Part 2, ו-HotXLS נהגה לחתום זמן מקומי עם אותו Z מחובר. חוברת שנוצרה ב-09:30 על מכונה ב-UTC+8 נשאה 09:30:00Z, ו-Excel על אותה מכונה עצמה המיר אותו ל-17:30. למנוע הקלאסי היה אותו פגם בדיוק בערכי ה-FILETIME שלו, ומאפייני תאריך מותאמים שנוספו דרך TXLSXWorkbook.CustomProperties.AddDate (נכתבים כ-vt:filetime) חלקו אותו גם כן. מאז v2.384.48 כל שלושת הנתיבים ממירים לפני כתיבה וממירים חזרה בקריאה בכל פעם שהחותמת נושאת Z, ומאז v2.384.59 צד הקריאה מכבד גם שניות שבריות והיסטים מפורשים של +hh:mm / -hh:mm
ההמרה עצמה היא המקום שבו תיקון נאיבי משתבש. LocalFileTimeToFileTime מפעילה את ההיסט שתקף עכשיו, כך שחותמת של ינואר שמומרת ביולי יוצאת עם שעה של סטייה בכל אזור עם שעון קיץ. HotXLS קוראת במקום זאת ל-TzSpecificLocalTimeToSystemTime ול-SystemTimeToTzSpecificLocalTime, שבוחרות שעון חורף או קיץ מתוך התאריך שמומר, וערך לא מוגדר של אפס עובר דרך בלי לגעת, כך שהוא לעולם לא הופך לתאריך של 1899 שהוזז בכמה שעות
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('report-template.xlsx') <> 1 then
raise Exception.Create('Template not available');
Book.Created := EncodeDate(2026, 7, 1) + EncodeTime(9, 30, 0, 0);
Book.Modified := Now;
Book.CustomProperties.AddDate('ApprovedOn',
EncodeDate(2026, 1, 20) + EncodeTime(17, 0, 0, 0));
Book.SaveAs('report.xlsx');
// על מכונה שמוגדרת לשעון מרכז אירופה, core.xml מחזיק עכשיו
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 ביולי), בזמן ש-ApprovedOn נכתב כ-16:00Z (UTC+1 בינואר)
finally
Book.Free;
end;
end;
מה HotXLS לא ממירה בקריאת חותמות זמן?
הקורא של W3CDTF ב-HotXLS ממיר כל צורה עם תג אזור של הפרופיל מאז v2.384.59, והמקרה האחד שהוא עדיין משאיר לבדו הוא שעה בלי אזור. לפני אותה מהדורה ה-parser לקח את 19 התווים הראשונים והמיר מ-UTC רק כשהתו ה-20 היה Z, כך שחותמת עם שניות שבריות (01:30:00.5Z) או היסט מפורש (+08:00) נקראה כזמן מקומי בלי התאמה ונחתה עם סטייה של היסט אזור הזמן. מאז HotXLS 2.384.59, Created, Modified ומאפיינים מותאמים בעלי ערך תאריך מפרסרים שניות שבריות בכל אורך, Z, והיסטים של +hh:mm / -hh:mm, ממירים את רגע הזמן ל-UTC ואז לזמן מקומי, וקוראים חותמת של תאריך בלבד כמו 2026-07-01 כאותו תאריך. חותמת עם שעה אך בלי סמן אזור, שהפרופיל של W3CDTF לא מרשה ו-ECMA-376 Part 2 לא נותן לה כלל, עדיין נקראת כזמן מקומי ללא שינוי, וחותמת שלא מתפרסרת בכלל חוזרת כאפס. חוברות שעוברות דרך Excel תקינות; חבילות שמפיקים אחרים שמשמיטים את האזור ראויות לבדיקה ממוקדת
קבצים שנכתבו על ידי build ישנים של HotXLS הם הגבול הכן השני. חותמת XLSX שנכתבה לפני v2.384.48 הייתה זמן מקומי לבוש ב-Z, ושום דבר בקובץ לא מבדיל אותה מחותמת נכונה, ולכן הקורא הנוכחי מזיז אותה בהיסט אזור הזמן. חותמות FILETIME קלאסיות מאותם build לוקחות את אותה הזזה, ותאריך יצירה שנכתב לפני v2.384.17 נקרא בנוסף יום מאוחר, כי גם את היום העודף של הקבוע הישן אי אפשר לזהות; רק לקידוד זמן העריכה ולמיקום ה-$0E יש חתימה מזהה. קחו בחשבון גם שערך ה-API הוא מקומי למכונה שקוראת, כך ששירות שרץ ב-UTC ושולחן עבודה בטוקיו ידווחו ערכי CreatedDate שונים עבור אותו קובץ, שניהם נכונים
איך בודקים חותמות זמן של מסמכים?
בדקו חותמות זמן של מסמכים מול משהו שהקוד שלכם לא כתב. שני הבאגים האלה עברו בדיקת שמירה ופתיחה מחדש, כי טעות סימטרית בלתי נראית לבדיקה סימטרית. השוו מול חוברת שנשמרה על ידי Excel, או הריצו assert על הבייטים הגולמיים ועל טקסט ה-XML אחרי שמירה, והריצו את הסוויטה על מכונה שמוגדרת לאזור שאינו UTC עם תאריך בדיקה משני צידי מעבר שעון קיץ. build agent שרץ ב-UTC יעביר בשמחה את הקוד הישן והשבור
חותמות זמן של מסמכים קטנות, אבל הן מה שמערכות ניהול רשומות, אינדקסי חיפוש ושובלי ביקורת ממיינים לפיהן, ותאריך שסוטה ביום או בשמונה שעות גרוע מתאריך חסר, כי אף אחד לא מערער עליו. רכיב הגיליונות של HotXLS לדלפי מטפל בחשבון התקופות, במזהי המאפיינים ובהמרת ה-UTC הן עבור .xls והן עבור .xlsx, כך שהקוד שלך יכול להקצות ערכי TDateTime מקומיים פשוטים ולהשאיר את פורמט הקובץ לספרייה