ZonedDateTime — ما هو، العمل مع المناطق الزمنية والتوقيت

المؤلف: IT Sectr نُشر: 2026-07-13 وقت القراءة: 10 دق

ZonedDateTime هي فئة غير قابلة للتغيير من حزمة java.time تخزن التاريخ والوقت مع معلومات المنطقة الزمنية (ZoneId). على عكس LocalDateTime، فإن ZonedDateTime تحدد بشكل فريد لحظة زمنية على الخط الزمني. وفقاً لمواصفات Oracle Java 17 (2024)، تتعامل الفئة بشكل صحيح مع انتقالات التوقيت الصيفي (DST) من خلال قواعد المنطقة من قاعدة بيانات IANA Time Zone Database.

النقاط الرئيسية

  • ZonedDateTime هي فئة غير قابلة للتغيير تجمع بين التاريخ والوقت والمنطقة الزمنية (ZoneId) في كائن واحد.
  • على عكس LocalDateTime، فإن ZonedDateTime تحدد بشكل فريد لحظة زمنية على الخط الزمني وهي مناسبة للأنظمة العالمية.
  • تتعامل الفئة تلقائياً مع انتقالات التوقيت الصيفي (DST) وفقاً لقواعد قاعدة بيانات IANA Time Zone Database.
  • للتحويل بين المناطق الزمنية، يُستخدم الأسلوب withZoneSameInstant(ZoneId).
  • يُوصى بتخزين ZonedDateTime في قواعد البيانات عبر OffsetDateTime أو TIMESTAMP WITH TIME ZONE.

ما هو ZonedDateTime؟

ZonedDateTime هي إحدى الفئات الرئيسية في حزمة java.time، تمثل التاريخ والوقت مع معلومات كاملة عن المنطقة الزمنية. تجمع بين ثلاثة مكونات: LocalDateTime (التاريخ والوقت)، ZoneId (معرف المنطقة)، وZoneOffset (الإزاحة بالنسبة لـ UTC).

على عكس LocalDateTime، الذي يخزن فقط وقت الساعة الحائطية (wall-clock time) دون ربط بالمنطقة، فإن ZonedDateTime تحدد بشكل فريد لحظة زمنية. مثالان متطابقان من LocalDateTime في منطقتين زمنيتين مختلفتين يمثلان لحظتين مختلفتين. مثالان متطابقان من ZonedDateTime — نفس اللحظة.

الفئة غير قابلة للتغيير تماماً وآمنة للخيوط (thread-safe). جميع العمليات الحسابية تُرجع كائناً جديداً. ZonedDateTime تطبق واجهة ChronoZonedDateTime ويمكن استخدامها أينما كانت هناك حاجة للتعامل مع الوقت الزمني في Java.

وفقاً لمواصفات Oracle Java 17، تدعم ZonedDateTime العمل مع أي منطقة من قاعدة بيانات IANA Time Zone Database، والتي تضم أكثر من 600 منطقة زمنية.

ZonedDateTime vs LocalDateTime: ما الفرق؟

الفرق الرئيسي — ZonedDateTime تحتوي على منطقة زمنية، بينما LocalDateTime لا تحتوي. هذا الفرق الجوهري يحدد نطاق استخدام كل فئة.

تُستخدم LocalDateTime للأحداث المحلية: وقت حفلة موسيقية، جدول الدروس، تاريخ الميلاد. إذا حدث حدث في موسكو الساعة 15:00، فإن LocalDateTime ستسجل 15:00 دون أي ربط. إذا نقلت الخادم إلى نيويورك، ستبقى الساعة 15:00 — لكنها ستكون لحظة فيزيائية مختلفة.

تُستخدم ZonedDateTime للبيانات العالمية: سجلات الخادم، الطوابع الزمنية في API، الاجتماعات الدولية. إذا تم تحديد اجتماع الساعة 15:00 MSK، فإن ZonedDateTime ستحتفظ بالوقت والمنطقة معاً. في نيويورك، ستعرض بشكل صحيح كـ 8:00 EST. وفقاً لـ Baeldung (2024)، فإن الاختيار بين LocalDateTime وZonedDateTime هو القرار المعماري الأكثر شيوعاً عند العمل مع التواريخ.

قاعدة عملية: إذا كانت البيانات مخزنة لمنطقة واحدة — استخدم LocalDateTime. إذا كانت البيانات تعبر حدود المناطق الزمنية — استخدم ZonedDateTime. إذا كنت بحاجة لتمثيل لحظة مطلقة — استخدم Instant.

كيف تعمل المنطقة الزمنية في java.time؟

المنطقة الزمنية في java.time ممثلة بالفئة ZoneId. ZoneId هو معرف منطقة بتنسيق «قارة/منطقة»، مثلاً «Europe/Moscow»، «America/New_York»، «Asia/Tokyo». يتم الحصول على ZoneId من خلال الأسلوب الثابت of(String zoneId) أو عبر المنطقة الزمنية الافتراضية للنظام.

ينقسم ZoneId إلى نوعين: fixed offset (إزاحة ثابتة، مثلاً «+03:00») وregion-based (مناطق إقليمية، مثلاً «Europe/London»). المناطق الإقليمية تحتوي على قواعد انتقال التوقيت الصيفي والتغييرات التاريخية. fixed offset هو مجرد إزاحة ثابتة.

للحصول على الإزاحة الحالية لـ ZoneId في لحظة معينة، يُستخدم الأسلوب getRules()، الذي يُرجع ZoneRules. ZoneRules يحتوي على جميع التحولات والإزاحات لمنطقة معينة. هذه هي الآلية الأساسية للمعالجة الصحيحة لـ DST.

جميع المناطق الزمنية تأتي مع JDK عبر ملفات tzdata (IANA Time Zone Database) ويتم تحديثها بانتظام. على Android، يعتمد إصدار tzdata على تحديثات النظام عبر Google Play Services.

إنشاء ZonedDateTime

يمكن إنشاء ZonedDateTime بعدة طرق. الأبسط هو now()، الذي يُرجع الوقت الحالي في المنطقة الزمنية للنظام. النوع now(ZoneId) يسمح بالحصول على الوقت الحالي في منطقة محددة.

الأسلوب of(LocalDateTime, ZoneId) يُنشئ ZonedDateTime من وقت محلي ومنطقة. النوع of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) يُنشئ من المكونات.

يمكن تحويل LocalDateTime إلى ZonedDateTime عبر الأسلوب atZone(ZoneId). Instant — عبر Instant.atZone(ZoneId). Date — عبر Date.toInstant().atZone(ZoneId).

kotlin
val moscowZone = ZoneId.of("Europe/Moscow")

val nowInMoscow = ZonedDateTime.now(moscowZone)

val fromComponents = ZonedDateTime.of(
    2026, 7, 21, 15, 30, 0, 0, moscowZone
)

val fromLocal = LocalDateTime.now().atZone(moscowZone)

val fromInstant = Instant.now().atZone(moscowZone)

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

الأسلوب الرئيسي للتحويل هو withZoneSameInstant(ZoneId). يقوم بتحويل ZonedDateTime إلى منطقة زمنية أخرى مع الحفاظ على نفس اللحظة الزمنية. مثلاً، 15:00 MSK ← 8:00 EST. الأسلوب withZoneSameLocal(ZoneId) يغير المنطقة مع الحفاظ على الوقت المحلي — وهذا يعطي لحظة مختلفة.

للحصول على الإزاحة بالنسبة لـ UTC، يُستخدم الأسلوب getOffset()، الذي يُرجع ZoneOffset. ZoneOffset هو فئة فرعية من ZoneId تمثل إزاحة ثابتة بتنسيق «+HH:mm» أو «-HH:mm».

التحويل إلى Instant يتم عبر الأسلوب toInstant(). Instant هي لحظة زمنية مطلقة، مستقلة عن المنطقة الزمنية. التحويل العكسي هو Instant.atZone(ZoneId).

kotlin
val moscow = ZonedDateTime.of(
    2026, 7, 21, 15, 0, 0, 0,
    ZoneId.of("Europe/Moscow")
)

val newYork = moscow.withZoneSameInstant(
    ZoneId.of("America/New_York")
)

val utcInstant = moscow.toInstant()
val backToMoscow = utcInstant.atZone(ZoneId.of("Europe/Moscow"))

التعامل مع التوقيت الصيفي (DST)

تُنشئ انتقالات التوقيت الصيفي مشكلتين: الفجوات (gaps) والتداخلات (overlaps). تحدث الفجوة في الربيع عندما تُقدم الساعات إلى الأمام — وقت معين غير موجود. يحدث التداخل في الخريف عندما تُؤخر الساعات — نفس الوقت يحدث مرتين.

تتعامل ZonedDateTime مع هذه الحالات من خلال استراتيجية الحل (resolve). عند إنشاء كائن أثناء فجوة، تقوم java.time تلقائياً بتحريك الوقت بمقدار الإزاحة. عند الإنشاء أثناء تداخل، يتم اختيار الخيار الأول (قبل الانتقال). يمكن تغيير هذا السلوك عبر withZoneSameInstant.

يمكنك التحقق مما إذا كان الوقت في DST عبر zone.getRules().isDaylightSavings(instant). يُظهر الأسلوب getOffset() الإزاحة الفعلية للحظة معينة، ويُظهر getRules().getDaylightSavings(instant) مقدار تعديل DST بالميلي ثانية.

kotlin
fun checkDST(zdt: ZonedDateTime) {
    val rules = zdt.getZone().getRules()
    val instant = zdt.toInstant()

    if (rules.isDaylightSavings(instant)) {
        val dstAmount = rules.getDaylightSavings(instant)
        Log.d("التوقيت الصيفي", "إزاحة التوقيت الصيفي: $dstAmount")
    }
}

تنسيق ZonedDateTime

لتنسيق ZonedDateTime يُستخدم DateTimeFormatter. يتضمن تنسيق ISO القياسي التاريخ والوقت والإزاحة: «2026-07-21T15:30:00+03:00[Europe/Moscow]». التنسيقات المحددة مسبقاً: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.

لإخراج مترجم محلياً، استخدم DateTimeFormatter.ofLocalizedDateTime(FormatStyle). يمكن أن يكون FormatStyle SHORT, MEDIUM, LONG, FULL. LONG يتضمن اسم المنطقة («MSK»)، FULL يتضمن الاسم الكامل («Moscow Standard Time»).

هام: عند تحليل سلسلة مع ZonedDateTime، يجب أن يحتوي التنسيق على معلومات المنطقة أو الإزاحة. إذا لم يتم تحديد المنطقة، استخدم LocalDateTime.parse() ثم atZone().

kotlin
val zdt = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))

val iso = zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)

val custom = DateTimeFormatter
    .ofPattern("dd.MM.yyyy HH:mm z")
val formatted = zdt.format(custom)

val parsed = ZonedDateTime.parse(
    "2026-07-21T15:30:00+03:00",
    DateTimeFormatter.ISO_OFFSET_DATE_TIME
)

ZonedDateTime في Android: أمثلة عملية

المثال الأول — عرض وقت اجتماع للمستخدم في منطقته الزمنية المحلية. يرسل الخادم ZonedDateTime بصيغة UTC، ويقوم العميل بالتحويل إلى المنطقة الزمنية المحلية للجهاز.

kotlin
fun displayMeetingTime(
    serverUtc: ZonedDateTime
): String {
    val deviceZone = ZoneId.systemDefault()
    val localTime = serverUtc.withZoneSameInstant(deviceZone)
    val formatter = DateTimeFormatter
        .ofPattern("dd.MM.yyyy HH:mm z")
    return localTime.format(formatter)
}

المثال الثاني — حساب الوقت المتبقي حتى الحدث التالي مع مراعاة المنطقة الزمنية. نستخدم ZonedDateTime لوقت الخادم وDuration.between() لحساب الفرق.

kotlin
fun timeUntilEvent(eventTime: ZonedDateTime): String {
    val now = ZonedDateTime.now()
    val duration = Duration.between(now, eventTime)

    val hours = duration.toHours()
    val minutes = duration.toMinutes() % 60
    return "Remaining $hours h $minutes min"
}

المثال الثالث — العمل مع API Retrofit. يُرجع الخادم سلسلة ISO-8601 مع منطقة. نستخدم محللاً مخصصاً للتحويل إلى ZonedDateTime.

kotlin
data class EventResponse(
    @JsonAdapter(ZonedDateTimeAdapter::class)
    val eventTime: ZonedDateTime
)

class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
    override fun fromJson(reader: JsonReader): ZonedDateTime? {
        return ZonedDateTime.parse(
            reader.nextString()
        )
    }
}

الأخطاء الشائعة عند العمل مع المناطق الزمنية

الخطأ الأول — استخدام ZoneId.systemDefault() في كود الخادم. قد تختلف المنطقة الزمنية للخادم عن منطقة العميل، واستخدام منطقة النظام على الخادم يؤدي إلى حسابات غير صحيحة. حدد المنطقة دائماً بشكل صريح أو استخدم UTC كمرجع.

الخطأ الثاني — تجاهل DST عند حساب المدة. يتعامل Duration.between() بشكل صحيح مع التحولات، ولكن إذا طرحت الطوابع الزمنية يدوياً، فقد يتسبب التوقيت الصيفي في خطأ قدره ساعة واحدة. استخدم ChronoUnit.HOURS.between() بدلاً من الحسابات اليدوية.

الخطأ الثالث — الخلط بين withZoneSameInstant وwithZoneSameLocal. الأول يغير المنطقة مع الحفاظ على اللحظة — يتغير الوقت. الثاني يغير المنطقة مع الحفاظ على الوقت المحلي — تتغير اللحظة. اختيار الأسلوب الخاطئ هو أحد أكثر الأخطاء شيوعاً وفقاً SonarSource (2024).

الخطأ الرابع — افتراض أن المنطقة الزمنية للجهاز هي دائماً نفس المنطقة الزمنية للمستخدم. قد يكون المستخدم مسافراً ويتوقع أن يُظهر التطبيق الوقت في منطقته «الأصلية» بدلاً من المنطقة الحالية. في هذه الحالة، قدم اختيار المنطقة عبر الواجهة.

الأسئلة الشائعة

ما الفرق بين ZonedDateTime وOffsetDateTime؟

ZonedDateTime تحتوي على معرف منطقة إقليمي (مثلاً «Europe/Moscow») وتتعامل مع DST. OffsetDateTime تخزن فقط إزاحة ثابتة (+03:00) بدون قواعد إقليمية. لتخزين قاعدة البيانات، يُوصى باستخدام OffsetDateTime.

كيف أحصل على الوقت الحالي بصيغة UTC عبر ZonedDateTime؟

استخدم ZonedDateTime.now(ZoneOffset.UTC) أو Instant.now().atZone(ZoneOffset.UTC). كلا الخيارين يُرجعان اللحظة الحالية بإزاحة صفرية. للحصول على طابع زمني بسيط، استخدم Instant.now() بدون ربط بمنطقة.

هل يمكن تسلسل ZonedDateTime عبر Gson أو Moshi؟

نعم، ولكن يتطلب محولاً مخصصاً. Gson لا يدعم ZonedDateTime افتراضياً. Moshi يدعمه عبر Rfc3339DateJsonAdapter. يُوصى باستخدام Kotlinx Serialization أو مكتبة JavaTimeModule لـ Jackson.

كيف أتعامل مع حالة عندما يقع الوقت في فجوة DST؟

تقوم java.time تلقائياً بتحريك الوقت للأمام بمقدار الإزاحة. مثلاً، إذا كانت الساعة 02:30 غير موجودة عند تقديم الساعات إلى 03:00، فإن ZonedDateTime ستنشئ كائناً عند 03:30. يمكنك التحقق من وجود فجوة عبر ZoneRules.getTransition(instant).

لماذا لا يُوصى باستخدام ZonedDateTime لقواعد بيانات SQL؟

JDBC 4.2 يدعم OffsetDateTime ولكن ليس ZonedDateTime مباشرة. ZonedDateTime تحتوي على منطقة إقليمية ليس لها مقابل في SQL. يُوصى بتخزين OffsetDateTime أو Instant، وتخزين المنطقة في عمود منفصل.

الملخص

  • ZonedDateTime هي فئة غير قابلة للتغيير للتاريخ والوقت مع المنطقة الزمنية، تتعامل بشكل صحيح مع DST عبر قاعدة بيانات IANA Time Zone Database.
  • الفرق الرئيسي عن LocalDateTime هو وجود المنطقة، مما يجعل ZonedDateTime معرفاً فريداً للحظة زمنية.
  • للتحويل بين المناطق، استخدم withZoneSameInstant()، الذي يحافظ على اللحظة، وليس withZoneSameLocal.
  • أثناء انتقالات التوقيت الصيفي، تحل java.time تلقائياً الفجوات والتداخلات عبر قواعد المناطق المدمجة.
  • لتخزين قاعدة البيانات، استخدم OffsetDateTime أو خزّن Instant وZoneId بشكل منفصل.
  • في Android، لتحويل ZonedDateTime إلى الوقت المحلي للجهاز، استخدم ZoneId.systemDefault() مع withZoneSameInstant.
  • لتسلسل JSON، يلزم محول مخصص — استخدم Kotlinx Serialization أو Jackson JavaTimeModule.

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا