معالجة التواريخ والأوقات
تقوم واجهة java.time على مبدأ الثبات (Immutability). لا يمكنك تعديل كائن LocalDate في مكانه؛ كل عملية معالجة تُعيد كائنًا جديدًا تمامًا. يتناول هذا الدرس ثلاث مجموعات من أساليب المعالجة — plus/minus، وwith، وTemporalAdjusters — مع توضيح متى يكون كل منها الخيار الأنسب.
لماذا يهمّنا الثبات هنا؟
في ما قبل java.time، كانت java.util.Date وCalendar قابلة للتعديل، مما أفضى إلى أخطاء يصعب تعقّبها: كانت الأساليب تُعدّل حالة مشتركة، وكانت الكائنات المُمرَّرة بين الخيوط تحتاج إلى مزامنة. تحلّ الواجهة الجديدة كلا المشكلتين. الثمن هو مزيد من الكلام البرمجي — كل عملية تُنتج قيمة جديدة — غير أن آلات JVM الحديثة تُحسّن الكائنات قصيرة العمر بكفاءة عالية، لذا تكاد تكون التكلفة الأدائية منعدمة عمليًا.
النموذج الذهني الأساسي: تعامل مع LocalDate وLocalDateTime وZonedDateTime وأخواتها تمامًا كما تتعامل مع String. لا تُعدّل سلسلة نصية؛ بل تُنتج سلسلة جديدة. الأمر ذاته ينطبق هنا.
plus و minus — التحرك على المحور الزمني
الحاجة الأكثر شيوعًا هي "أعطني تاريخًا بعد N يوم/شهر/سنة من الآن". كل تطبيق لواجهة Temporal يوفّر أساليب مُختصرة من النوع المحدد، وشكلًا عامًا يقبل TemporalAmount.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.Duration;
public class PlusMinusDemo {
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 6, 9);
// أساليب مختصرة من النوع المحدد
LocalDate nextWeek = today.plusDays(7);
LocalDate nextMonth = today.plusMonths(1);
LocalDate nextYear = today.plusYears(1);
LocalDate lastWeek = today.minusDays(7);
LocalDate lastQuarter = today.minusMonths(3);
System.out.println("اليوم: " + today);
System.out.println("الأسبوع القادم: " + nextWeek);
System.out.println("الشهر القادم: " + nextMonth);
System.out.println("السنة القادمة: " + nextYear);
System.out.println("الأسبوع الماضي: " + lastWeek);
System.out.println("الربع الماضي: " + lastQuarter);
// الشكل العام مع TemporalAmount (Period أو Duration)
LocalDate in90Days = today.plus(Period.ofDays(90));
System.out.println("بعد 90 يومًا: " + in90Days);
// LocalDateTime — تدعم plus أيضًا Duration
LocalDateTime now = LocalDateTime.of(2026, 6, 9, 14, 30);
LocalDateTime soon = now.plusHours(2).plusMinutes(45);
System.out.println("قريبًا: " + soon);
}
}
يُمثّل Period مقدارًا مرتبطًا بالتقويم (سنوات، أشهر، أيام). أما Duration فيُمثّل مقدارًا مرتبطًا بالوقت (ساعات، دقائق، ثوانٍ، نانوثوانٍ). يمكن تمرير أيٍّ منهما إلى plus(TemporalAmount) العامة، لكن الكائن الزمني المُستقبِل يجب أن يدعم الحقول ذات الصلة — فاستدعاء LocalDate.plus(Duration.ofHours(3)) يُلقي UnsupportedTemporalTypeException لأن التاريخ لا يحتوي ساعات.
مفاجآت أطوال الأشهر: ينتج عن LocalDate.of(2026, 1, 31).plusMonths(1) القيمة 2026-02-28 وليس 2026-03-03. تقطع java.time النتيجة إلى آخر يوم صالح في الشهر المستهدف (تُسمّى قاعدة نهاية الشهر). هذا هو السلوك التقويمي الصحيح، لكن إذا طرحت الشهر للخلف لاحقًا قد لا تحصل على التاريخ الأصلي. صمِّم منطق الدورة الكاملة مع مراعاة ذلك.
with — استبدال حقل محدد
plus/minus تعديلات نسبية. أما with فهي استبدال مطلق: "اجعل يوم الشهر 1"، "اجعل الساعة 0"، "اجعل السنة 2030". تتوفر في أشكال مُختصرة من النوع المحدد وشكل عام.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
public class WithDemo {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 6, 15);
// أشكال مختصرة من النوع المحدد
LocalDate firstOfMonth = date.withDayOfMonth(1);
LocalDate lastDayOfYear = date.withDayOfYear(365); // 2026 ليست كبيسة
LocalDate sameMonthDay2030 = date.withYear(2030);
System.out.println("أول الشهر: " + firstOfMonth);
System.out.println("آخر يوم في السنة: " + lastDayOfYear);
System.out.println("نفس اليوم في 2030: " + sameMonthDay2030);
// الشكل العام باستخدام ChronoField
LocalDate withMonth = date.with(ChronoField.MONTH_OF_YEAR, 12);
System.out.println("نفس اليوم في ديسمبر: " + withMonth);
// يعمل على LocalDateTime أيضًا
LocalDateTime noon = LocalDateTime.of(2026, 6, 15, 9, 45)
.with(ChronoField.HOUR_OF_DAY, 12)
.with(ChronoField.MINUTE_OF_HOUR, 0);
System.out.println("الظهر: " + noon);
}
}
تقبل with(TemporalField, long) العامة أي ثابت من ChronoField، مما يمنحك تحكمًا دقيقًا — بما في ذلك حقول متخصصة كـALIGNED_WEEK_OF_MONTH أو EPOCH_DAY. تُفضَّل الأشكال المُختصرة في معظم الحالات لوضوحها.
TemporalAdjusters — تعديلات قواعد العمل
كثير من الاحتياجات الواقعية لا يمكن التعبير عنها بـ "أضف N وحدة" أو "اجعل الحقل يساوي X": "آخر جمعة في هذا الشهر"، "يوم العمل التالي"، "أول اثنين بعد تاريخ معين". تُغطي فئة المصنع TemporalAdjusters الحالات الشائعة، ويمكنك كتابة مُعدِّلاتك الخاصة.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class AdjustersDemo {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 6, 9); // الثلاثاء
LocalDate firstDay = date.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
LocalDate firstOfNext = date.with(TemporalAdjusters.firstDayOfNextMonth());
LocalDate nextFriday = date.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
LocalDate prevMonday = date.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
LocalDate firstMonday = date.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
LocalDate lastFriday = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
System.out.println("أول الشهر: " + firstDay);
System.out.println("آخر الشهر: " + lastDay);
System.out.println("أول الشهر القادم: " + firstOfNext);
System.out.println("الجمعة القادمة: " + nextFriday);
System.out.println("الاثنين السابق: " + prevMonday);
System.out.println("أول اثنين في يونيو: " + firstMonday);
System.out.println("آخر جمعة في يونيو: " + lastFriday);
}
}
كتابة TemporalAdjuster مخصص
TemporalAdjuster واجهة وظيفية (Functional Interface) تحتوي أسلوبًا وحيدًا هو adjustInto(Temporal). ولأنها وظيفية، يمكن تطبيقها كتعبير لامدا. مثال كلاسيكي هو مُعدِّل "يوم العمل التالي" الذي يتخطى عطل نهاية الأسبوع.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
public class BusinessDayAdjuster {
// تخطّى السبت والأحد
public static final TemporalAdjuster NEXT_BUSINESS_DAY = (Temporal temporal) -> {
LocalDate date = LocalDate.from(temporal);
do {
date = date.plusDays(1);
} while (date.getDayOfWeek() == DayOfWeek.SATURDAY
|| date.getDayOfWeek() == DayOfWeek.SUNDAY);
return date;
};
public static void main(String[] args) {
LocalDate friday = LocalDate.of(2026, 6, 5); // الجمعة
LocalDate saturday = LocalDate.of(2026, 6, 6); // السبت
LocalDate sunday = LocalDate.of(2026, 6, 7); // الأحد
System.out.println("بعد الجمعة: " + friday.with(NEXT_BUSINESS_DAY)); // الاثنين 2026-06-08
System.out.println("بعد السبت: " + saturday.with(NEXT_BUSINESS_DAY)); // الاثنين 2026-06-08
System.out.println("بعد الأحد: " + sunday.with(NEXT_BUSINESS_DAY)); // الاثنين 2026-06-08
}
}
اجمع المُعدِّلات لقواعد مركّبة. قد تقضي قاعدة موعد السداد بـ "آخر يوم تقويمي في الشهر، فإذا كان عطلة نهاية أسبوع انتقل إلى الجمعة السابقة". اجمع مُعدِّلَين: lastDayOfMonth() ثم مُعدِّل تراجع عطلة نهاية الأسبوع. أبقِ كل مُعدِّل صغيرًا ذا غرض واحد.
اختيار الأداة المناسبة
- استخدم plus/minus حين تتحرك مقدارًا ثابتًا على المحور الزمني (7 أيام من الآن، 3 أشهر للخلف).
- استخدم with حين تحتاج إلى تثبيت حقل محدد على قيمة ملموسة (أول يوم في الشهر، منتصف الليل من هذا اليوم).
- استخدم TemporalAdjusters حين تكون القاعدة مرتبطة بالتقويم أو منطق الأعمال ولا يمكن التعبير عنها بإزاحة ثابتة أو تعيين حقل.
ربط العمليات معًا
لأن كل أسلوب يُعيد كائنًا جديدًا، يمكنك ربط الاستدعاءات بأسلوب سلسلي. الوضوح هو الحدّ الوحيد.
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;
public class ChainingDemo {
public static void main(String[] args) {
// "بداية آخر يوم عمل في الشهر القادم"
LocalDateTime result = LocalDateTime.now()
.plusMonths(1)
.with(TemporalAdjusters.lastDayOfMonth())
.with(TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY))
.withHour(9)
.withMinute(0)
.withSecond(0)
.withNano(0);
System.out.println(result);
}
}
previousOrSame مقابل previous: تُعيد TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY) التاريخ ذاته إن كان جمعة أصلًا؛ بينما تتحرك previous(DayOfWeek.FRIDAY) للخلف يومًا واحدًا على الأقل دائمًا. اختر النوع الصحيح لتتعامل مع الحالة الحدّية بصواب.
الخلاصة
معالجة التواريخ في java.time ثابتة وقابلة للتركيب. تُحرّك plus/minus على المحور الزمني بمقدار ثابت؛ تُثبِّت with حقلًا على قيمة مطلقة؛ تُعبّر TemporalAdjusters عن قواعد تقويمية رفيعة المستوى وقابلة للتوسعة عبر الواجهة الوظيفية TemporalAdjuster. تغطي هذه المجموعات الثلاث معًا كل حالة معالجة تواريخ تكاد تواجهها في كود الإنتاج.