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

المُدد الزمنية: Duration و Period

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

المُدد الزمنية: Duration و Period

تناول الدرس السابق LocalDate وLocalTime وLocalDateTime بوصفها نقاطًا على خط الزمن. هذا الدرس يتناول كميات الزمن — الفجوة بين نقطتين، أو مقدارًا من الوقت تريد إضافته أو طرحه. توفّر واجهة java.time كلاستين مختلفتين لهذا الغرض، كلٌّ منهما تُمثّل مفهومًا مغايرًا:

  • Duration — كمية زمنية تُقاس بالثواني والنانو ثانية. استخدمها مع LocalTime وLocalDateTime وInstant أو أي كلاس يحمل مكوّن وقت.
  • Period — كمية زمنية تُقاس بالسنوات والأشهر والأيام. استخدمها مع LocalDate أو مكوّن التاريخ في LocalDateTime.
لماذا كلاستان منفصلتان؟ لأن "30 يومًا" و"شهرًا واحدًا" ليستا متكافئتين. قد يحتوي الشهر على 28 أو 29 أو 30 أو 31 يومًا حسب التقويم. تعمل Period بوحدات تقويمية وتتعامل مع هذا التباين بشكل صحيح، في حين تعمل Duration بثوانٍ دقيقة لا علاقة لها بالتقويم. الخلط بينهما يُفضي إلى أخطاء خفيّة — يُجبرك التصميم على الاختيار الواعي.

Duration

تُخزَّن Duration دائمًا على شكل عدد إجمالي من الثواني مع تعديل بالنانو ثانية. تُتيح لك توابع المصنع التعبير عن المقدار بوحدة مقروءة:

import java.time.*; Duration twoHours = Duration.ofHours(2); Duration ninetyMins = Duration.ofMinutes(90); Duration halfSecond = Duration.ofMillis(500); Duration precise = Duration.ofSeconds(45, 250_000_000); // 45.25 ثانية // فحص المكوّنات System.out.println(twoHours.toHours()); // 2 System.out.println(twoHours.toMinutes()); // 120 System.out.println(twoHours.toSeconds()); // 7200 System.out.println(twoHours.toNanos()); // 7200000000000

أقوى توابع المصنع هو Duration.between() الذي يحسب الفجوة بين كائنَين زمنيَّين من النوع ذاته:

LocalTime start = LocalTime.of(9, 30); LocalTime end = LocalTime.of(17, 15); Duration workday = Duration.between(start, end); System.out.println(workday.toHours()); // 7 System.out.println(workday.toMinutesPart()); // 45 (Java 9+) System.out.println(workday); // PT7H45M

يُعيد toMinutesPart() (Java 9) مكوّن الدقائق (0–59) لا المجموع الكلي. هذا هو الأسلوب الاصطلاحي لتنسيق المدة دون إجراء حسابات يدوية.

توابع "part" في Java 9: toHoursPart() وtoMinutesPart() وtoSecondsPart() وtoNanosPart() تُقسّم Duration إلى مكوّناتها المقروءة. قبل Java 9 كان عليك إجراء الحساب يدويًا (مثلًا minutes % 60).

عمليات Duration الحسابية

كلاس Duration غير قابل للتغيير (immutable). كل تابع يُعدّل القيمة يُعيد نسخة جديدة:

Duration d = Duration.ofMinutes(90); Duration longer = d.plusMinutes(30); // 120 دقيقة Duration shorter = d.minusSeconds(60); // 89 دقيقة Duration doubled = d.multipliedBy(2); // 180 دقيقة Duration halved = d.dividedBy(2); // 45 دقيقة boolean negative = d.isNegative(); // false Duration absolute = d.abs(); // مفيد حين يكون اتجاه الطرح مجهولًا

Period

تُخزّن Period السنوات والأشهر والأيام في ثلاثة حقول صحيحة منفصلة — ولا تُحوّل بينها. إضافة مدة شهر واحد إلى 31 يناير تُعطي 28 فبراير (أو 29 في السنة الكبيسة)، لا 31 يومًا لاحقًا، لأن الحساب التقويمي يُطبَّق بشكل صحيح لحظة التطبيق.

Period threeMonths = Period.ofMonths(3); Period oneYear = Period.ofYears(1); Period twoWeeks = Period.ofWeeks(2); // تُخزَّن كـ 14 يومًا Period complex = Period.of(1, 6, 15); // سنة و6 أشهر و15 يومًا System.out.println(complex.getYears()); // 1 System.out.println(complex.getMonths()); // 6 System.out.println(complex.getDays()); // 15 System.out.println(complex); // P1Y6M15D

Period.between()

مثل Duration.between()، يحسب Period.between() الفرق بين قيمتَي LocalDate بوحدات تقويمية:

LocalDate birthdate = LocalDate.of(1995, 6, 15); LocalDate today = LocalDate.of(2026, 6, 9); Period age = Period.between(birthdate, today); System.out.printf("العمر: %d سنة، %d شهرًا، %d يومًا%n", age.getYears(), age.getMonths(), age.getDays()); // العمر: 30 سنة، 11 شهرًا، 25 يومًا
لا تستخدم Duration.between() مع LocalDate. لا يحمل LocalDate مكوّن وقت، لذا تمريره إلى Duration.between() يُطلق استثناء UnsupportedTemporalTypeException. دائمًا طابق الكلاس: Duration للكائنات الحاملة للوقت، وPeriod لكائنات التاريخ فقط.

عمليات Period الحسابية

Period p = Period.of(1, 2, 10); // سنة وشهران و10 أيام Period more = p.plusMonths(3); // P1Y5M10D Period negated = p.negated(); // P-1Y-2M-10D boolean isNeg = p.isNegative(); // false (للقيمة الأصلية) long totalMonths = p.toTotalMonths(); // 14 (السنوات * 12 + الأشهر، الأيام تُهمَل)

لاحظ أن toTotalMonths() يتجاهل حقل الأيام. لا يوجد toTotalDays() لأن ذلك يستلزم معرفة التقويم (الشهر ليس عددًا ثابتًا من الأيام). إذا أردت إجمالي الأيام بين تاريخين، استخدم ChronoUnit.DAYS.between(start, end) عوضًا عن ذلك.

ChronoUnit.between() — بديل أبسط

أحيانًا تحتاج رقمًا واحدًا فحسب: "كم أسبوعًا كاملًا بين هذين التاريخين؟" يجعل ChronoUnit هذا سطرًا واحدًا:

import java.time.temporal.ChronoUnit; LocalDate start = LocalDate.of(2026, 1, 1); LocalDate end = LocalDate.of(2026, 6, 9); long days = ChronoUnit.DAYS.between(start, end); // 159 long weeks = ChronoUnit.WEEKS.between(start, end); // 22 long months = ChronoUnit.MONTHS.between(start, end); // 5

يُعيد هذا دائمًا long واحدًا مُقتطعًا إلى وحدات كاملة. استخدمه حين تحتاج رقمًا قياسيًا لا Period أو Duration منظَّمة.

تطبيق الكميات على كائنات التاريخ والوقت

تُنفّذ كلتا الكلاستين واجهة TemporalAmount، ما يتيح تمريرهما إلى plus() وminus() على أي كائن زمني متوافق:

LocalDate deadline = LocalDate.of(2026, 6, 9); LocalDate extended = deadline.plus(Period.ofWeeks(2)); // 2026-06-23 LocalDate oneYearAgo = deadline.minus(Period.ofYears(1)); // 2025-06-09 LocalDateTime meeting = LocalDateTime.of(2026, 6, 9, 14, 0); LocalDateTime later = meeting.plus(Duration.ofMinutes(90)); // 15:30
قاعدة إبهام: إذا كان متغيّرك LocalDate، استخدم Period. وإذا احتوى على مكوّن وقت (LocalTime أو LocalDateTime أو Instant)، استخدم Duration. قد يتجاوز المُترجم الخلط بينهما أحيانًا، غير أن وقت التشغيل يُطلق استثناءً — لذا يجب أن يصدر الانضباط منك أنت.

الخلاصة

تُمثّل Duration وقت الآلة — عددًا دقيقًا من الثواني والنانو ثانية، مستقلًّا عن التقويم. وتُمثّل Period وقت الإنسان — سنواتٌ وأشهرٌ وأيامٌ تحترم عدم انتظام التقويم. كلتاهما كائنات قيمة غير قابلة للتغيير. استخدم between() لقياس الفجوة، وplus() / minus() لتطبيقهما. وحين تحتاج عددًا قياسيًا واحدًا فحسب، الجأ إلى ChronoUnit.