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

التنسيق والتحليل

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

التنسيق والتحليل

كل تطبيق يعرض تواريخ على المستخدم أو يقرأها من مدخلات خارجية يحتاج إلى التحويل بين كائنات java.time والنصوص. والإجابة في Java هي DateTimeFormatter، وهو صنف غير قابل للتغيير (immutable) وآمن للخيوط (thread-safe) يحلّ محل SimpleDateFormat القديم. في هذا الدرس ستتعلّم كيف تستخدم المنسّقات الجاهزة، وتبني أنماطك الخاصة، وتتعامل مع المخرجات الحساسة للإعدادات المحلية بشكل صحيح.

لماذا DateTimeFormatter أفضل من SimpleDateFormat؟

اشتُهر SimpleDateFormat القديم بمشكلتين: فهو غير آمن للخيوط (مما يتسبب في تلف البيانات عند مشاركة نفس النسخة بين خيوط متعددة)، وينتمي إلى عالم java.util.Date الفوضوي. يحل DateTimeFormatter كلتا المشكلتين — فهو غير قابل للتغيير ويتكامل بشكل مباشر مع java.time. لا ينبغي لجوؤك إلى SimpleDateFormat في الكود الجديد أبدًا.

أمان الخيوط أمر بالغ الأهمية. يمكن تخزين نسخة واحدة من DateTimeFormatter في حقل static final وإعادة استخدامها من عدة خيوط متزامنة دون الحاجة إلى أي تزامن. أما SimpleDateFormat المشترك فسيُفسد المخرجات بصمت عند الاستخدام المتزامن.

المنسّقات الجاهزة

يأتي الصنف DateTimeFormatter مزودًا بمجموعة من الثوابت الجاهزة لأكثر حالات الاستخدام شيوعًا. لا تحتاج هذه المنسّقات إلى أي إعداد وتُنتج صيغًا معروفة.

import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; LocalDate date = LocalDate.of(2025, 9, 15); LocalDateTime dt = LocalDateTime.of(2025, 9, 15, 14, 30, 45); // ISO 8601 — الخيار الافتراضي للواجهات البرمجية والتخزين System.out.println(date.format(DateTimeFormatter.ISO_LOCAL_DATE)); // 2025-09-15 System.out.println(dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); // 2025-09-15T14:30:45 // RFC 1123 — يُستخدم في ترويسات HTTP (يتطلب ZonedDateTime) import java.time.ZonedDateTime; import java.time.ZoneOffset; ZonedDateTime zdt = ZonedDateTime.of(dt, ZoneOffset.UTC); System.out.println(DateTimeFormatter.RFC_1123_DATE_TIME.format(zdt)); // Mon, 15 Sep 2025 14:30:45 GMT
فضّل ISO 8601 في التواصل بين الأنظمة. عند الكتابة في قاعدة بيانات أو طابور رسائل أو واجهة REST API، استخدم دائمًا ISO_LOCAL_DATE_TIME أو ISO_INSTANT. الصيغ الحساسة للإعدادات المحلية مثل "Sep 15, 2025" تنتمي إلى طبقة العرض فقط.

الأنماط المخصصة باستخدام ofPattern()

عندما لا يتطابق المنسّق الجاهز مع متطلباتك، استخدم DateTimeFormatter.ofPattern(String pattern). لغة الأنماط مشابهة للغة SimpleDateFormat القديمة لكنها أكثر صرامة واتساقًا.

أبرز حروف الأنماط (حالة الحرف مهمة):

  • y — السنة؛ yyyy لأربعة أرقام، yy لرقمين
  • M / MM / MMM / MMMM — الشهر رقمًا، برقمين، بالاسم المختصر، بالاسم الكامل
  • d / dd — يوم الشهر
  • H / HH — الساعة في اليوم (0–23)
  • h / hh — الساعة بنظام AM/PM (1–12)؛ يُقرن بـ a (محدد AM/PM)
  • m / mm — الدقيقة؛ s / ss — الثانية
  • E / EEEE — اسم يوم الأسبوع مختصرًا / كاملًا
  • z — اسم المنطقة الزمنية؛ Z — الفرق بالصيغة +0000؛ xxx — الفرق مع نقطتين +00:00
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"); LocalDateTime dt = LocalDateTime.of(2025, 9, 15, 14, 30); // التنسيق String s = dt.format(fmt); // "15/09/2025 14:30" System.out.println(s); // التحليل LocalDateTime parsed = LocalDateTime.parse("01/03/2026 09:00", fmt); System.out.println(parsed); // 2026-03-01T09:00
انتبه إلى حرف الشهر M وحرف الدقيقة m — استخدم دائمًا m الصغيرة للدقائق. كتابة MM:ss بدلًا من mm:ss سيُظهر رقم الشهر بدلًا من الدقيقة بصمت. يعدّ هذا من أكثر الأخطاء المطبعية شيوعًا.

التنسيق الحساس للإعدادات المحلية

المنسّقات القائمة على الأنماط محايدة للإعدادات المحلية — إذ يستخدم MMMM دائمًا الإعدادات المحلية الافتراضية لآلة JVM ما لم تحدد غير ذلك. استخدم DateTimeFormatter.ofPattern(pattern, locale) أو التابع withLocale() لإنتاج مخرجات تراعي الإعدادات المحلية.

import java.util.Locale; LocalDate date = LocalDate.of(2025, 9, 15); DateTimeFormatter enFmt = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy", Locale.ENGLISH); DateTimeFormatter arFmt = DateTimeFormatter.ofPattern("EEEE، d MMMM yyyy", Locale.forLanguageTag("ar")); DateTimeFormatter deFmt = DateTimeFormatter.ofPattern("EEEE, d. MMMM yyyy", Locale.GERMAN); System.out.println(date.format(enFmt)); // Monday, September 15, 2025 System.out.println(date.format(arFmt)); // الاثنين، 15 سبتمبر 2025 System.out.println(date.format(deFmt)); // Montag, 15. September 2025

FormatStyle — صيغ مُترجَمة رفيعة المستوى

عندما تريد مخرجات مراعية للإعدادات المحلية دون تحديد نمط، استخدم DateTimeFormatter.ofLocalizedDate(FormatStyle). تقوم آلة JVM بحل النمط الصحيح للإعدادات المحلية وقت التشغيل.

import java.time.format.FormatStyle; LocalDate date = LocalDate.of(2025, 9, 15); DateTimeFormatter shortEn = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.ENGLISH); DateTimeFormatter mediumEn = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.ENGLISH); DateTimeFormatter longEn = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(Locale.ENGLISH); DateTimeFormatter fullEn = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(Locale.ENGLISH); System.out.println(date.format(shortEn)); // 9/15/25 System.out.println(date.format(mediumEn)); // Sep 15, 2025 System.out.println(date.format(longEn)); // September 15, 2025 System.out.println(date.format(fullEn)); // Monday, September 15, 2025
فضّل FormatStyle في كود واجهة المستخدم. ترميز نمط صريح مثل "MM/dd/yyyy" سيكون خاطئًا بصمت للمستخدمين في إعدادات محلية مختلفة. أما FormatStyle.SHORT.withLocale(userLocale) فيتكيف تلقائيًا دون تغيير في الكود.

التحليل ومعالجة الأخطاء

كل من LocalDate.parse(text, formatter) وformatter.parse(text) يطرح DateTimeParseException (وهو RuntimeException) عندما لا تتطابق المدخلات مع النمط. تعامل مع هذا الاستثناء دائمًا عند الحدود التي يدخل منها النص غير الموثوق إلى تطبيقك.

import java.time.format.DateTimeParseException; DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String input = "not-a-date"; try { LocalDate d = LocalDate.parse(input, fmt); } catch (DateTimeParseException e) { System.out.println("Invalid date: " + e.getMessage()); // "Invalid date: Text 'not-a-date' could not be parsed at index 0" }

DateTimeFormatterBuilder للصيغ المعقدة

أحيانًا تحتاج إلى صيغ لا يمكن التعبير عنها كسلسلة نمط بسيطة — أقسام اختيارية، أو نصوص حرفية، أو أنماط احتياطية متعددة. يتيح لك DateTimeFormatterBuilder تركيب منسّق برمجيًا.

import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; // منسّق يقبل "yyyy-MM-dd" و "yyyy-MM-dd HH:mm" كلاهما DateTimeFormatter flexible = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM-dd") .optionalStart() .appendLiteral(' ') .appendPattern("HH:mm") .optionalEnd() .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0) .toFormatter(); LocalDateTime a = LocalDateTime.parse("2025-09-15 14:30", flexible); // مع الوقت LocalDateTime b = LocalDateTime.parse("2025-09-15", flexible); // يأخذ القيمة الافتراضية 00:00 System.out.println(a); // 2025-09-15T14:30 System.out.println(b); // 2025-09-15T00:00
parseDefaulting ضرورة لا غنى عنها عندما لا يحتوي النمط على مكوّن الوقت لكنك تُحلّل إلى LocalDateTime. بدونه سيفشل التحليل لأن حقول الوقت غائبة.

إعادة استخدام المنسّقات — نمط الثوابت الساكنة

بما أن DateTimeFormatter غير قابل للتغيير وآمن للخيوط، احرص على تعريف النسخ المشتركة كثوابت static final. إنشاء منسّق جديد داخل كل استدعاء تابع يضيف تخصيصًا غير ضروري ويجعل قاعدة الكود أصعب في التغيير المركزي.

public final class DateFormats { public static final DateTimeFormatter ISO_DATE = DateTimeFormatter.ISO_LOCAL_DATE; public static final DateTimeFormatter DISPLAY = DateTimeFormatter.ofPattern("d MMM yyyy", Locale.ENGLISH); public static final DateTimeFormatter API_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT); private DateFormats() {} // صنف أدوات مساعدة — لا نسخ منه }

الخلاصة

يعدّ DateTimeFormatter الأداة الوحيدة الآمنة للخيوط لتحويل التاريخ والوقت إلى نص والعكس في Java الحديثة. استخدم الثوابت الجاهزة للصيغ الآلية (ISO 8601، RFC 1123)، وofPattern للتخطيطات المخصصة الثابتة، وofLocalizedDate مع FormatStyle عندما يجب أن يتكيف المخرج مع الإعدادات المحلية للمستخدم. خزّن المنسّقات القابلة لإعادة الاستخدام في ثوابت static final، والتقط دائمًا DateTimeParseException عند الحدود بين النص الخارجي ونموذج مجالك.