واجهة التاريخ والوقت

المناطق الزمنية والإزاحات

15 دقيقة الدرس 7 من 13

المناطق الزمنية والإزاحات

كل ما تعلّمته حتى الآن — LocalDate وLocalTime وLocalDateTime وInstant — يشترك في شيء واحد: لا أيٌّ منها وحده يحمل المعلومات الكافية لتثبيت لحظة ما على ساعة الحائط في مدينة بعينها على وجه الأرض. الأصناف التي ندرسها في هذا الدرس تسدّ هذه الفجوة: ZoneId وZonedDateTime وOffsetDateTime.

ما هي المنطقة الزمنية فعلًا؟

المنطقة الزمنية هي مجموعة قواعد مُسمَّاة تحدّد، لكل نقطة في التاريخ، كم ساعة ودقيقة يفصل موقعًا ما عن UTC، وهل يُطبَّق عليه التوقيت الصيفي (DST) ومتى. قاعدة بيانات المناطق الزمنية الخاصة بـ IANA — التي يُضمّنها Java — تُشفّر آلاف هذه مجموعات القواعد. أما الإزاحة الثابتة عن UTC (مثل +05:30) فأبسط: مجرد فارق ثابت دون أي منطق للتوقيت الصيفي.

ZoneId — تسمية المنطقة الزمنية

ZoneId هو المُعرِّف الخفيف لمنطقة زمنية مُسمَّاة. تحصل عليه بالاسم أو من الإعداد الافتراضي للنظام:

import java.time.ZoneId; ZoneId london = ZoneId.of("Europe/London"); ZoneId riyadh = ZoneId.of("Asia/Riyadh"); ZoneId kolkata = ZoneId.of("Asia/Kolkata"); // UTC+5:30 بلا توقيت صيفي ZoneId system = ZoneId.systemDefault(); // يقرأ إعداد TZ في JVM // سرد جميع المعرّفات المتاحة ZoneId.getAvailableZoneIds().stream() .sorted() .limit(10) .forEach(System.out::println);
استخدم دائمًا أسماء مناطق IANA مثل America/New_York وليس EST. الاختصارات المكوّنة من ثلاثة أحرف غامضة — فـCST تُستخدم لكلٍّ من Central Standard Time وChina Standard Time — ولا تُشفّر انتقالات التوقيت الصيفي.

ZonedDateTime — الصورة الكاملة

ZonedDateTime هو أغنى نوع تاريخ-وقت في java.time. يجمع LocalDateTime مع ZoneId والإزاحة الفعلية في تلك اللحظة، لذا يمكن تحويله دائمًا إلى Instant. فكّر فيه كإجابة لسؤال: "ماذا تُظهر ساعة الحائط في مدينة ما الآن؟"

import java.time.ZonedDateTime; import java.time.ZoneId; // اللحظة الحالية في منطقة زمنية محدّدة ZonedDateTime nowInTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo")); ZonedDateTime nowInNewYork = ZonedDateTime.now(ZoneId.of("America/New_York")); System.out.println(nowInTokyo); // مثلًا: 2024-11-15T09:00:00+09:00[Asia/Tokyo] System.out.println(nowInNewYork); // مثلًا: 2024-11-14T19:00:00-05:00[America/New_York] // الإنشاء من LocalDateTime + منطقة زمنية import java.time.LocalDateTime; LocalDateTime meetingLocal = LocalDateTime.of(2025, 3, 10, 14, 30); ZonedDateTime meetingInDubai = ZonedDateTime.of(meetingLocal, ZoneId.of("Asia/Dubai")); System.out.println(meetingInDubai); // 2025-03-10T14:30:00+04:00[Asia/Dubai]

التحويل بين المناطق الزمنية يتمّ بنداء واحد. اللحظة الأصلية تُحفظ، ويتغيّر فقط التمثيل على ساعة الحائط:

ZonedDateTime dubaiMeeting = ZonedDateTime.of( LocalDateTime.of(2025, 6, 15, 10, 0), ZoneId.of("Asia/Dubai")); ZonedDateTime sameMomentInLondon = dubaiMeeting.withZoneSameInstant(ZoneId.of("Europe/London")); ZonedDateTime sameMomentInNY = dubaiMeeting.withZoneSameInstant(ZoneId.of("America/New_York")); System.out.println(dubaiMeeting); // 2025-06-15T10:00:00+04:00[Asia/Dubai] System.out.println(sameMomentInLondon); // 2025-06-15T07:00:00+01:00[Europe/London] System.out.println(sameMomentInNY); // 2025-06-15T02:00:00-04:00[America/New_York]
الفرق بين withZoneSameInstant وwithZoneSameLocal: استخدم withZoneSameInstant عندما تريد التعبير عن اللحظة ذاتها في منطقة مختلفة (تحويلات من قبيل السفر). استخدم withZoneSameLocal عندما تريد الإبقاء على وقت ساعة الحائط وإعادة تفسيره في منطقة مختلفة — مفيد حين تكتشف أن LocalDateTime وُسِم بمنطقة خاطئة.

التوقيت الصيفي والفجوات والتداخلات

عندما تُنشئ انتقالات التوقيت الصيفي فجوة (تقدّم الساعات مع تخطّي ساعة) أو تداخلًا (تأخير الساعات مع تكرار ساعة)، يعالجها ZonedDateTime تلقائيًا: يُزيح الوقت إلى ما بعد الفجوة، ويختار الإزاحة الأبكر عند التداخل. يمكنك التحكّم الدقيق عبر ZoneRules إن احتجت، لكن السلوك الافتراضي صحيح في معظم الحالات.

import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; // في المنطقة الشرقية الأمريكية، 2025-03-09 02:30 غير موجود (فجوة: تقفز 02:00 → 03:00) LocalDateTime gapTime = LocalDateTime.of(2025, 3, 9, 2, 30); ZonedDateTime adjusted = ZonedDateTime.of(gapTime, ZoneId.of("America/New_York")); // Java تُعيد ضبطه تلقائيًا إلى 03:30 EDT System.out.println(adjusted); // 2025-03-09T03:30:00-04:00[America/New_York]

OffsetDateTime — بسيط وثابت وقابل للنقل

OffsetDateTime يُقرن LocalDateTime بإزاحة ثابتة عن UTC (مثل +03:00) لكنه لا يحمل أي قواعد منطقة زمنية — لا توقيت صيفي، لا انتقالات تاريخية. هو أقل تعبيرًا من ZonedDateTime، لكن ذلك ميزته في سياقات معينة:

  • تخزين قواعد البيانات: عمود TIMESTAMP WITH TIME ZONE في SQL يخزن إزاحة لا اسم منطقة. OffsetDateTime يُعيَّن عليه بشكل نظيف عبر JDBC.
  • التسلسل والواجهات البرمجية: صيغة ISO-8601 على الشبكة (2025-06-15T10:00:00+04:00) مفهومة عالميًا وتُقرأ وتُكتب بدقة مع OffsetDateTime.
  • القيم الثابتة: لا توجد قواعد توقيت صيفي يمكن أن تُعيد كتابة الإزاحة بعد تحديث قواعد المنطقة — مناسب لسجلات التدقيق ونمط تسجيل الأحداث.
import java.time.OffsetDateTime; import java.time.ZoneOffset; // اللحظة الحالية بإزاحة ثابتة OffsetDateTime now = OffsetDateTime.now(ZoneOffset.of("+03:00")); System.out.println(now); // 2025-06-15T13:00:00+03:00 // الإنشاء الصريح OffsetDateTime event = OffsetDateTime.of(2025, 9, 1, 9, 0, 0, 0, ZoneOffset.of("+04:00")); // التحويل إلى UTC OffsetDateTime utc = event.withOffsetSameInstant(ZoneOffset.UTC); System.out.println(utc); // 2025-09-01T05:00:00Z // التحويل إلى ZonedDateTime إن احتجت لاحقًا لحسابات مع التوقيت الصيفي ZonedDateTime zoned = event.atZoneSameInstant(ZoneId.of("Asia/Dubai"));

اختيار النوع المناسب

  • استخدم ZonedDateTime عند جدولة أحداث، أو عرض الأوقات للمستخدمين في مناطقهم، أو إجراء حسابات تقويمية (إضافة شهر، تخطّي إلى يوم عمل) تستلزم احترام التوقيت الصيفي.
  • استخدم OffsetDateTime للحفظ الدائم (أعمدة قواعد البيانات، واجهات JSON) وسجلات التدقيق حيث تريد طابعًا زمنيًا مكتفيًا بذاته وغير مبهم.
  • استخدم Instant (تمّت دراسته سابقًا) للطوابع الزمنية الآلية البحتة — المهام الخلفية، مقاييس الأداء، أي شيء لا يراه المستخدم على شكل ساعة حائط.
لا تخزّن ZonedDateTime كنص في قاعدة البيانات دون تفكير دقيق. اسم المنطقة مثل America/New_York هو مؤشّر إلى قاعدة بيانات IANA. إن غيّرت حكومة ما قواعد التوقيت الصيفي، قد تتغيّر تفسيرات الأسماء الموجودة. الأفضل هو التخزين كـInstant UTC أو OffsetDateTime بتوقيت UTC، وتطبيق المنطقة الزمنية فقط عند العرض.

الخلاصة

ZoneId يُعرِّف مجموعة قواعد مُسمَّاة من قاعدة بيانات IANA. ZonedDateTime = LocalDateTime + ZoneId + الإزاحة الفعلية: هو الاختيار الصحيح للجدولة الموجّهة للمستخدم مع الدعم الكامل للتوقيت الصيفي. OffsetDateTime = LocalDateTime + ZoneOffset ثابت: هو الاختيار الصحيح للواجهات البرمجية وقواعد البيانات وسجلات التدقيق. التحويل بين المناطق يتمّ بـwithZoneSameInstant؛ والتحويل بين تمثيلات الإزاحة بـwithOffsetSameInstant. اختر النوع الذي يناسب حالة استخدامك — فهي ليست قابلة للتبادل.