مشروع: جدولة الفعاليات
على مدار هذا الدرس التعليمي تعلّمت كل قطعة رئيسية من java.time: التواريخ والأوقات المحلية والمُقيَّدة بمنطقة زمنية، واللحظات الآنية، والمدد، والفترات، والتنسيق، والتحليل، والتوافق مع الأنواع القديمة. في هذا الدرس الختامي ستجمع كل هذه القطع في تطبيق جدولة فعاليات صغير وواقعي قادر على تخزين الأحداث وعرضها بشكل صحيح عبر مناطق زمنية مختلفة والعثور على الأحداث القادمة وتنسيق المخرجات للمستخدمين.
هذا مشروع مقصود في تركيزه — نحو مئتَي سطر من Java الاصطلاحية — حتى تتمكن من رؤية كيفية ترابط كل مفهوم دون الضياع في ضجيج الأطر البرمجية.
ما الذي يجب أن يفعله المجدوِل
- قبول اسم الحدث وتاريخ ووقت البداية و
ZoneId (المنطقة الزمنية التي أُنشئ فيها الحدث أو سيقام بها).
- تخزين الأحداث داخليًا كقيم
Instant حتى تكون محايدة من حيث المنطقة الزمنية في قاعدة البيانات أو على القرص.
- عرض أي حدث في أي منطقة زمنية مطلوبة مع التحويل الفوري.
- سرد جميع الأحداث التي تقع في الأيام الـ N القادمة من منظور المُستدعي.
- تنسيق المخرجات بصيغة ISO-8601 القابلة للقراءة آليًا وبصيغة بشرية حساسة للغة.
سجل الحدث
ابدأ بنموذج بيانات نظيف. سجل Java 17 مثالي هنا — غير قابل للتغيير، ومضغوط، ويولّد تلقائيًا equals وhashCode وtoString.
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public record Event(String name, Instant start, ZoneId originZone) {
/** عرض هذا الحدث في المنطقة الزمنية المستهدفة المعطاة واللغة المحددة. */
public String displayIn(ZoneId targetZone, Locale locale) {
ZonedDateTime zdt = start.atZone(targetZone);
DateTimeFormatter fmt = DateTimeFormatter
.ofPattern("EEEE, dd MMMM yyyy 'at' HH:mm z", locale);
return name + " — " + zdt.format(fmt);
}
/** سلسلة ISO-8601 قابلة للقراءة آليًا (UTC دائمًا). */
public String toIso() {
return start.toString(); // مثال: 2025-11-15T09:00:00Z
}
}
لماذا نخزّن كـ Instant؟ الـ Instant هو نقطة واحدة على الخط الزمني العالمي — لا توجد له منطقة زمنية. تخزين الأحداث بهذه الطريقة يعني أنك لن تقارن عن طريق الخطأ وقت نيويورك بوقت طوكيو دون تحويل صريح. تُسجَّل المنطقة الزمنية بشكل منفصل في originZone للتدقيق فحسب ("أين أُدخل هذا الحدث؟")، وليس للحساب.
خدمة EventScheduler
يحتوي المجدوِل على قائمة من الأحداث ويوفّر عمليات خاصة بالنطاق. لاحظ كيف يلجأ كل أسلوب إلى النوع الصحيح تمامًا من java.time.
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
public class EventScheduler {
private final List<Event> events = new ArrayList<>();
/**
* إضافة حدث. يُزوّد المُستدعي بوقت البداية المحلي
* والمنطقة الزمنية التي يُعبَّر فيها عنه؛ نُحوِّل إلى Instant فورًا.
*/
public void addEvent(String name,
LocalDateTime localStart,
ZoneId zone) {
Instant start = localStart.atZone(zone).toInstant();
events.add(new Event(name, start, zone));
}
/**
* إعادة جميع الأحداث التي تبدأ خلال الأيام [days] القادمة،
* مُقيَّمةً من [now] في المنطقة الزمنية [viewerZone].
*/
public List<Event> upcomingEvents(ZonedDateTime now, int days) {
Instant from = now.toInstant();
Instant to = now.plusDays(days).toInstant();
return events.stream()
.filter(e -> !e.start().isBefore(from)
&& e.start().isBefore(to))
.sorted(Comparator.comparing(Event::start))
.collect(Collectors.toList());
}
/**
* طباعة جميع الأحداث، مع عرض كل منها في المنطقة الزمنية للمشاهد.
*/
public void printAll(ZoneId viewerZone, Locale locale) {
if (events.isEmpty()) {
System.out.println("لا توجد أحداث مجدولة.");
return;
}
events.stream()
.sorted(Comparator.comparing(Event::start))
.forEach(e -> System.out.println(e.displayIn(viewerZone, locale)));
}
/** كم من الوقت حتى (أو منذ) حدث معين، كسلسلة بشرية. */
public String timeUntil(Event event, ZonedDateTime now) {
Duration duration = Duration.between(now.toInstant(), event.start());
long hours = duration.toHours();
long minutes = duration.toMinutesPart();
if (duration.isNegative()) {
return event.name() + " حدث منذ "
+ Math.abs(hours) + "س " + Math.abs(minutes) + "د";
}
return event.name() + " يبدأ خلال " + hours + "س " + minutes + "د";
}
}
استخدم التدفقات للتصفية على Instant. تُقارن Instant.isBefore() وInstant.isAfter() نقاطًا مطلقة في الوقت — فهما دائمًا لا لبس فيهما بصرف النظر عن تغييرات التوقيت الصيفي. لا تُقارِن كائنات ZonedDateTime بـ < أو >؛ استخدم compareTo أو حوّل إلى Instant أولًا.
تحليل مدخلات المستخدم
التطبيقات الحقيقية تستقبل سلاسل التاريخ والوقت من النماذج أو واجهات برمجة التطبيقات. أضف أسلوب مصنع يُحلِّل سلسلة يُزوّدها المستخدم مثل "2025-11-15 09:00" وسلسلة منطقة زمنية مثل "America/New_York":
import java.time.format.DateTimeParseException;
public static Optional<Event> parse(String name,
String dateTimeStr,
String zoneStr) {
DateTimeFormatter parser =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
try {
ZoneId zone = ZoneId.of(zoneStr);
LocalDateTime ldt = LocalDateTime.parse(dateTimeStr, parser);
Instant start = ldt.atZone(zone).toInstant();
return Optional.of(new Event(name, start, zone));
} catch (DateTimeParseException | java.time.zone.ZoneRulesException ex) {
System.err.println("مدخلات خاطئة: " + ex.getMessage());
return Optional.empty();
}
}
تحقّق دائمًا من سلاسل المنطقة الزمنية عند التحليل. ZoneId.of("America/NewYork") (بدون شرطة سفلية) يرمي ZoneRulesException في وقت التشغيل. لفّ بناء المنطقة الزمنية في try-catch وأعد Optional أو ارمِ استثناءً خاصًا بالنطاق برسالة واضحة. لا تدع سلسلة منطقة زمنية خاطئة تتخذ UTC افتراضيًا بصمت — هذا يُخفي الأخطاء لأشهر.
تجميع كل شيء — عرض توضيحي رئيسي
public class Main {
public static void main(String[] args) {
EventScheduler scheduler = new EventScheduler();
// مؤتمر في نيويورك
scheduler.addEvent(
"Java Summit",
LocalDateTime.of(2025, 11, 15, 9, 0),
ZoneId.of("America/New_York")
);
// ندوة عبر الإنترنت مستضافة في لندن
scheduler.addEvent(
"Cloud Architecture Webinar",
LocalDateTime.of(2025, 11, 15, 14, 0),
ZoneId.of("Europe/London")
);
// اجتماع فريق في طوكيو
scheduler.addEvent(
"Asia-Pacific Team Sync",
LocalDateTime.of(2025, 11, 16, 10, 0),
ZoneId.of("Asia/Tokyo")
);
ZoneId viewerZone = ZoneId.of("Europe/Berlin");
Locale viewerLocale = Locale.ENGLISH;
System.out.println("=== جميع الأحداث (بتوقيت برلين) ===");
scheduler.printAll(viewerZone, viewerLocale);
System.out.println();
System.out.println("=== الأحداث خلال اليومين القادمين ===");
ZonedDateTime now = ZonedDateTime.of(
LocalDateTime.of(2025, 11, 15, 8, 0),
viewerZone
);
List<Event> upcoming = scheduler.upcomingEvents(now, 2);
upcoming.forEach(e -> System.out.println(e.displayIn(viewerZone, viewerLocale)));
System.out.println();
System.out.println("=== مخرجات ISO-8601 الآلية ===");
upcoming.forEach(e -> System.out.println(e.toIso()));
System.out.println();
System.out.println("=== العدّ التنازلي ===");
upcoming.forEach(e -> System.out.println(scheduler.timeUntil(e, now)));
}
}
مثال على المخرجات (الأوقات معروضة بتوقيت أوروبا/برلين الذي هو UTC+1 في نوفمبر):
=== جميع الأحداث (بتوقيت برلين) ===
Saturday, 15 November 2025 at 15:00 CET — Java Summit
Saturday, 15 November 2025 at 15:00 CET — Cloud Architecture Webinar
Sunday, 16 November 2025 at 02:00 CET — Asia-Pacific Team Sync
=== الأحداث خلال اليومين القادمين ===
Java Summit — Saturday, 15 November 2025 at 15:00 CET
Cloud Architecture Webinar — Saturday, 15 November 2025 at 15:00 CET
Asia-Pacific Team Sync — Sunday, 16 November 2025 at 02:00 CET
=== مخرجات ISO-8601 الآلية ===
2025-11-15T14:00:00Z
2025-11-15T14:00:00Z
2025-11-16T01:00:00Z
=== العدّ التنازلي ===
Java Summit يبدأ خلال 7س 0د
Cloud Architecture Webinar يبدأ خلال 7س 0د
Asia-Pacific Team Sync يبدأ خلال 18س 0د
لاحظ التطابق. قمة نيويورك الساعة 09:00 والندوة اللندنية الساعة 14:00 كلتاهما تُحوَّل إلى 14:00 بتوقيت UTC — فهما تبدآن في اللحظة المطلقة ذاتها. هذا ليس خطأً؛ بل كشفت الحسابات بتوقيت UTC عن تعارض في الجدولة كان سيظل خفيًا لو قارنّا التوقيتات المحلية بشكل ساذج.
مبادئ التصميم الرئيسية التي يُجسّدها هذا المشروع
- خزّن كـ Instant، واعرض كـ ZonedDateTime. احتفظ بطبقة الاستمرارية محايدة من حيث المنطقة الزمنية؛ وحوّل فقط في طبقة العرض.
- احتفظ بالمنطقة الزمنية الأصلية للتدقيق لا للحساب. تجري جميع العمليات الحسابية على
Instant أو بعد تحويل صريح إلى منطقة زمنية محددة.
- حلِّل بحذر. لفّ
ZoneId.of() وLocalDateTime.parse() في try-catch وأظهر الأخطاء مبكرًا.
- استخدم
Duration لحسابات "الوقت حتى". تُعيد Duration.between() على قيمتَي Instant مدةً دقيقة محصّنة ضد التوقيت الصيفي.
- نسِّق متأخرًا، نسِّق بشكل صريح. مرِّر
Locale إلى كل مُنسِّق حتى تظهر أسماء الأيام والأشهر بلغة المشاهد.
الخلاصة
لقد بنيت مجدوِل أحداث كاملًا ومدركًا للمناطق الزمنية باستخدام واجهة برمجة التطبيقات القياسية java.time فحسب. النمط — قبول المدخلات المحلية ← التحويل الفوري إلى Instant ← تخزين Instant ← التحويل إلى المنطقة الزمنية للمشاهد عند العرض — هو النمط ذاته المستخدم في كل نظام تقويم وحجز وجدولة جاد، من Google Calendar إلى منصات حجوزات شركات الطيران. بهذا الأساس أصبحت مؤهلًا للتعامل مع الوقت بشكل صحيح في أي تطبيق Java.