مقارنة التواريخ والأوقات
مقارنة التواريخ والأوقات
ترتيب القيم الزمنية وقياسها هو أحد أكثر مهام التاريخ والوقت شيوعًا في التطبيقات الحقيقية: ترتيب الأحداث زمنيًا، والتحقّق من مرور موعد الحجز، وحساب عدد الأيام المتبقية حتى الموعد النهائي، أو تقييد إجراء المستخدم مرةً كل 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، ممّا يمنحك فشلًا واضحًا بدلًا من سلوك خاطئ صامت.
equals() فقط؟ equals() يعمل أيضًا مع LocalDate، غير أنّ isEqual() هو الواجهة المفضّلة لأنّه يعبّر عن القصد بوضوح. وعلى أنواع مثل ZonedDateTime يكون الفرق جوهريًا: قد تمثّل كائنا ZonedDateTime اللحظة ذاتها لكن بمعرّفَي منطقة مختلفَين — فـisEqual يقارن اللحظة على المحور الزمني بينما يشترط equals تطابق المنطقة أيضًا.
ZonedDateTime ودقة isEqual
يصبح هذا الفرق بالغ الأهمية عند العمل عبر مناطق زمنية مختلفة:
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.
النتيجة دائمًا من نوع long. وتكون سالبة إذا كانت end قبل start، ممّا يجعل تمييز الاتجاه سهلًا دون الحاجة إلى استدعاء منفصل لدالة مقارنة.
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 لإنتاج رسالة واضحة للمستخدم:
المقارنة باستخدام compareTo
لأن كل نوع زمني يُنفّذ Comparable، يمكنك أيضًا استخدام compareTo(other). تعيد عددًا صحيحًا سالبًا أو صفرًا أو موجبًا — نفس عقد Comparable في Java. هذا مفيد بشكل خاص عند ترتيب المجموعات:
LocalDate (أو أي نوع من java.time) بالعامل == أبدًا. إنّها كائنات، و== يختبر هوية المرجع. قد يتشارك كائنا LocalDate يمثّلان التاريخ ذاته المرجعَ نفسه أو لا، تبعًا للتخزين المؤقت. استخدم دائمًا isEqual() أو equals() أو compareTo().
الخلاصة
استخدم isBefore وisAfter وisEqual لطرح أسئلة اتجاهية عن القيم الزمنية؛ وتذكّر أنّ isEqual يقارن النقطة على المحور الزمني بينما يتحقّق equals أيضًا من تطابق المنطقة. استخدم ChronoUnit.UNIT.between(start, end) حين تحتاج إلى عدد رقمي واحد في وحدة محدّدة — يعيد قيمة long مقتطعة وتكون سالبة حين تسبق النهاية البداية. وللتفصيل متعدد المكوّنات، استخدم Period أو Duration بدلًا من ذلك.