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

مقارنة التواريخ والأوقات

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

مقارنة التواريخ والأوقات

ترتيب القيم الزمنية وقياسها هو أحد أكثر مهام التاريخ والوقت شيوعًا في التطبيقات الحقيقية: ترتيب الأحداث زمنيًا، والتحقّق من مرور موعد الحجز، وحساب عدد الأيام المتبقية حتى الموعد النهائي، أو تقييد إجراء المستخدم مرةً كل 24 ساعة. تمنحك واجهة java.time أداتين مختلفتين لهذا الغرض — دوال المقارنة المنطقية (isBefore وisAfter وisEqual) وأدوات القياس التي يوفّرها ChronoUnit.

دوال المقارنة الثلاث

كل نوع رئيسي من أنواع التاريخ والوقت — LocalDate وLocalTime وLocalDateTime وZonedDateTime وInstant وOffsetDateTime — يُنفّذ واجهة Comparable ويكشف ثلاث دوال منطقية سلسة:

  • isBefore(other) — تعيد true إذا كانت هذه القيمة أسبق صارمًا من other.
  • isAfter(other) — تعيد true إذا كانت هذه القيمة أحدث صارمًا من other.
  • isEqual(other) — تعيد true إذا كانت كلتا القيمتين تمثّلان النقطة ذاتها (أو التاريخ ذاته) على المحور الزمني.

تعمل المقارنات الثلاث على المحور الزمني، لا على هوية الكائن. وهي آمنة من null بمعنى أنّها ترمي NullPointerException فورًا عند تمرير null، ممّا يمنحك فشلًا واضحًا بدلًا من سلوك خاطئ صامت.

import java.time.LocalDate; LocalDate today = LocalDate.of(2026, 6, 9); LocalDate deadline = LocalDate.of(2026, 12, 31); LocalDate birthdate = LocalDate.of(2026, 6, 9); System.out.println(today.isBefore(deadline)); // true System.out.println(today.isAfter(deadline)); // false System.out.println(today.isEqual(birthdate)); // true
لماذا لا نستخدم equals() فقط؟ equals() يعمل أيضًا مع LocalDate، غير أنّ isEqual() هو الواجهة المفضّلة لأنّه يعبّر عن القصد بوضوح. وعلى أنواع مثل ZonedDateTime يكون الفرق جوهريًا: قد تمثّل كائنا ZonedDateTime اللحظة ذاتها لكن بمعرّفَي منطقة مختلفَين — فـisEqual يقارن اللحظة على المحور الزمني بينما يشترط equals تطابق المنطقة أيضًا.

ZonedDateTime ودقة isEqual

يصبح هذا الفرق بالغ الأهمية عند العمل عبر مناطق زمنية مختلفة:

import java.time.ZonedDateTime; import java.time.ZoneId; ZonedDateTime londonNoon = ZonedDateTime.of(2026, 6, 9, 12, 0, 0, 0, ZoneId.of("Europe/London")); ZonedDateTime dubaiAfternoon = ZonedDateTime.of(2026, 6, 9, 15, 0, 0, 0, ZoneId.of("Asia/Dubai")); // لندن UTC+1 الساعة 12:00 = UTC 11:00 // دبي UTC+4 الساعة 15:00 = UTC 11:00 → اللحظة ذاتها! System.out.println(londonNoon.isEqual(dubaiAfternoon)); // true System.out.println(londonNoon.equals(dubaiAfternoon)); // false (مناطق مختلفة)
عند مقارنة أوقات من مناطق زمنية مختلفة، استخدم دائمًا isEqual() (أو حوّل كليهما إلى Instant أولًا). استخدام equals() سيُخبرك بصمت أنّ حدثَين متزامنَين "مختلفان".

قياس الفجوات بـ ChronoUnit

دوال المقارنة تُخبرك فقط بـاتجاه المقارنة. لمعرفة المقدار — كم من الأيام أو الساعات أو الدقائق أو غيرها من الوحدات التقويمية تفصل قيمتَين — تستخدم ChronoUnit.between(start, end).

ChronoUnit هو enum في حزمة java.time.temporal تغطّي ثوابته النطاق الزمني الكامل: NANOS وMICROS وMILLIS وSECONDS وMINUTES وHOURS وHALF_DAYS وDAYS وWEEKS وMONTHS وYEARS وDECADES وCENTURIES وMILLENNIA.

import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; LocalDate start = LocalDate.of(2026, 1, 1); LocalDate end = LocalDate.of(2026, 6, 9); long daysBetween = ChronoUnit.DAYS.between(start, end); // 159 long weeksBetween = ChronoUnit.WEEKS.between(start, end); // 22 long monthsBetween = ChronoUnit.MONTHS.between(start, end); // 5 System.out.println(daysBetween); // 159 System.out.println(weeksBetween); // 22 System.out.println(monthsBetween); // 5

النتيجة دائمًا من نوع long. وتكون سالبة إذا كانت end قبل start، ممّا يجعل تمييز الاتجاه سهلًا دون الحاجة إلى استدعاء منفصل لدالة مقارنة.

LocalDateTime meetingStart = LocalDateTime.of(2026, 6, 9, 9, 0); LocalDateTime meetingEnd = LocalDateTime.of(2026, 6, 9, 11, 30); long hours = ChronoUnit.HOURS.between(meetingStart, meetingEnd); // 2 long minutes = ChronoUnit.MINUTES.between(meetingStart, meetingEnd); // 150 System.out.println(hours); // 2 System.out.println(minutes); // 150
اقتطاع لا تقريب. تعيد ChronoUnit.HOURS.between على فترة مدتها 2.5 ساعة القيمة 2 لا 3. النتيجة دائمًا مقتطعة نحو الصفر. إذا احتجت إلى التقريب، أضف نصف الوحدة إلى المقسوم عليه قبل القسمة.

ChronoUnit مقابل Duration/Period

تعرف بالفعل Duration وPeriod من الدرس الرابع. الفرق الجوهري:

  • Duration.between(a, b) وPeriod.between(a, b) تمنحانك كائنًا منظّمًا بمكوّنات متعددة (مثلًا Period تساوي "1 سنة، 3 أشهر، يومان").
  • ChronoUnit.DAYS.between(a, b) تمنحك رقمًا طويلًا واحدًا — العدد الكلي في وحدة واحدة فقط. لا تُفكّك إلى سنوات وأشهر.

استخدم ChronoUnit حين تحتاج إلى رقم واحد (مثلًا "أيام حتى انتهاء الصلاحية")، وكلًا من Period وDuration حين تحتاج إلى تفصيل مقروء للإنسان ("1 سنة، 3 أشهر، يومان").

مثال عملي: التحقّق من الحجز

فيما يلي مقطع واقعي يجمع الأداتين — دوال المقارنة لتفعيل المنطق وChronoUnit لإنتاج رسالة واضحة للمستخدم:

import java.time.LocalDate; import java.time.temporal.ChronoUnit; public class BookingValidator { public static String checkBooking(LocalDate checkIn, LocalDate checkOut) { LocalDate today = LocalDate.now(); if (!checkIn.isAfter(today)) { return "يجب أن يكون تاريخ الوصول في المستقبل."; } if (!checkOut.isAfter(checkIn)) { return "يجب أن يكون تاريخ المغادرة بعد تاريخ الوصول."; } long nights = ChronoUnit.DAYS.between(checkIn, checkOut); long daysUntilCheckIn = ChronoUnit.DAYS.between(today, checkIn); return String.format( "الحجز صالح: %d ليلة، الوصول بعد %d يوم.", nights, daysUntilCheckIn); } public static void main(String[] args) { System.out.println(checkBooking( LocalDate.of(2026, 7, 1), LocalDate.of(2026, 7, 5))); // الحجز صالح: 4 ليالٍ، الوصول بعد 22 يومًا. } }

المقارنة باستخدام compareTo

لأن كل نوع زمني يُنفّذ Comparable، يمكنك أيضًا استخدام compareTo(other). تعيد عددًا صحيحًا سالبًا أو صفرًا أو موجبًا — نفس عقد Comparable في Java. هذا مفيد بشكل خاص عند ترتيب المجموعات:

import java.time.LocalDate; import java.util.Arrays; import java.util.List; List<LocalDate> dates = Arrays.asList( LocalDate.of(2026, 12, 1), LocalDate.of(2026, 3, 15), LocalDate.of(2026, 6, 9) ); List<LocalDate> sorted = dates.stream() .sorted() // يستخدم Comparable — لا يحتاج إلى comparator .toList(); sorted.forEach(System.out::println); // 2026-03-15 // 2026-06-09 // 2026-12-01
لا تقارن LocalDate (أو أي نوع من java.time) بالعامل == أبدًا. إنّها كائنات، و== يختبر هوية المرجع. قد يتشارك كائنا LocalDate يمثّلان التاريخ ذاته المرجعَ نفسه أو لا، تبعًا للتخزين المؤقت. استخدم دائمًا isEqual() أو equals() أو compareTo().

الخلاصة

استخدم isBefore وisAfter وisEqual لطرح أسئلة اتجاهية عن القيم الزمنية؛ وتذكّر أنّ isEqual يقارن النقطة على المحور الزمني بينما يتحقّق equals أيضًا من تطابق المنطقة. استخدم ChronoUnit.UNIT.between(start, end) حين تحتاج إلى عدد رقمي واحد في وحدة محدّدة — يعيد قيمة long مقتطعة وتكون سالبة حين تسبق النهاية البداية. وللتفصيل متعدد المكوّنات، استخدم Period أو Duration بدلًا من ذلك.