ميزات Dart المتقدمة

التاريخ والوقت والمدة

45 دقيقة الدرس 12 من 16

فئة DateTime

فئة DateTime في Dart هي الأساس للعمل مع التواريخ والأوقات. تمثل نقطة في الزمن، إما بتوقيت UTC أو المنطقة الزمنية المحلية. فهم DateTime ضروري للجدولة والتسجيل ومعالجة البيانات وأي تطبيق حساس للوقت.

إنشاء كائنات DateTime

void main() {
  // التاريخ والوقت الحالي
  final now = DateTime.now();
  print(now);  // 2024-03-15 14:30:00.000

  // تاريخ ووقت محدد
  final specific = DateTime(2024, 3, 15, 14, 30, 0);
  print(specific);  // 2024-03-15 14:30:00.000

  // وقت UTC
  final utc = DateTime.utc(2024, 3, 15, 14, 30);
  print(utc);       // 2024-03-15 14:30:00.000Z
  print(utc.isUtc); // true

  // تاريخ فقط (الوقت الافتراضي 00:00:00)
  final dateOnly = DateTime(2024, 3, 15);
  print(dateOnly);  // 2024-03-15 00:00:00.000

  // الوصول للمكونات
  print('السنة: ${now.year}');
  print('الشهر: ${now.month}');     // 1-12
  print('اليوم: ${now.day}');          // 1-31
  print('الساعة: ${now.hour}');        // 0-23
  print('الدقيقة: ${now.minute}');    // 0-59
  print('الثانية: ${now.second}');    // 0-59
  print('المللي ثانية: ${now.millisecond}');
  print('المايكرو ثانية: ${now.microsecond}');
  print('يوم الأسبوع: ${now.weekday}');  // 1=الإثنين ... 7=الأحد
}
ملاحظة: شهور DateTime في Dart تبدأ من 1 (يناير = 1، ديسمبر = 12) وأيام الأسبوع تتبع ISO 8601 (الإثنين = 1، الأحد = 7). هذا يختلف عن بعض اللغات الأخرى حيث الشهور تبدأ من 0 أو الأحد = 0.

تحليل التواريخ مع DateTime.parse

طرق DateTime.parse و DateTime.tryParse تحوّل النصوص إلى كائنات DateTime. تقبل مجموعة فرعية من تنسيقات ISO 8601.

تحليل نصوص التواريخ

void main() {
  // تنسيقات ISO 8601 القياسية
  final d1 = DateTime.parse('2024-03-15');
  print(d1);  // 2024-03-15 00:00:00.000

  final d2 = DateTime.parse('2024-03-15 14:30:00');
  print(d2);  // 2024-03-15 14:30:00.000

  final d3 = DateTime.parse('2024-03-15T14:30:00Z');  // UTC
  print(d3);       // 2024-03-15 14:30:00.000Z
  print(d3.isUtc); // true

  final d4 = DateTime.parse('2024-03-15T14:30:00+05:00');  // مع إزاحة
  print(d4.toUtc());  // 2024-03-15 09:30:00.000Z

  // التحليل الآمن مع tryParse
  final valid = DateTime.tryParse('2024-03-15');
  final invalid = DateTime.tryParse('ليس-تاريخاً');
  print(valid);    // 2024-03-15 00:00:00.000
  print(invalid);  // null

  // مهم: DateTime.parse لا يعالج تنسيقات مثل '03/15/2024'
  // للتنسيقات المخصصة، استخدم حزمة intl (موضحة لاحقاً)
}

UTC مقابل الوقت المحلي

فهم الفرق بين UTC والوقت المحلي أمر حاسم للتطبيقات التي تتعامل مع مستخدمين في مناطق زمنية مختلفة، أو سجلات الخوادم، أو اتصالات API.

تحويل UTC والوقت المحلي

void main() {
  // الوقت المحلي
  final local = DateTime.now();
  print('محلي: $local');
  print('هل UTC: ${local.isUtc}');         // false
  print('المنطقة الزمنية: ${local.timeZoneName}');  // مثل AST, EST, PST
  print('الإزاحة: ${local.timeZoneOffset}');  // مثل 3:00:00.000000

  // تحويل المحلي إلى UTC
  final utc = local.toUtc();
  print('UTC: $utc');
  print('هل UTC: ${utc.isUtc}');  // true

  // تحويل UTC إلى المحلي
  final backToLocal = utc.toLocal();
  print('العودة للمحلي: $backToLocal');

  // إنشاء UTC مباشرة
  final utcDirect = DateTime.utc(2024, 3, 15, 12, 0);
  print(utcDirect);         // 2024-03-15 12:00:00.000Z
  print(utcDirect.toLocal()); // يعتمد على المنطقة الزمنية المحلية

  // millisecondsSinceEpoch — طابع زمني عالمي
  final epoch = local.millisecondsSinceEpoch;
  print('مللي ثانية العصر: $epoch');

  // إعادة البناء من العصر
  final fromEpoch = DateTime.fromMillisecondsSinceEpoch(epoch);
  print('من العصر: $fromEpoch');
  final fromEpochUtc = DateTime.fromMillisecondsSinceEpoch(epoch, isUtc: true);
  print('من العصر (UTC): $fromEpochUtc');
}
نصيحة: خزّن دائماً وأرسل التواريخ بتنسيق UTC. حوّل إلى الوقت المحلي فقط لأغراض العرض. هذا يمنع الأخطاء عندما يكون المستخدمون في مناطق زمنية مختلفة، أو عندما تستخدم الخوادم منطقة زمنية مختلفة عن العملاء، أو أثناء انتقالات التوقيت الصيفي.

فئة Duration

فئة Duration تمثل فترة من الزمن. تُستخدم لحساب التواريخ، والمهلات، والتأخيرات، وقياس الوقت المنقضي.

العمل مع Duration

void main() {
  // إنشاء كائنات Duration
  final fiveMinutes = Duration(minutes: 5);
  final oneHour = Duration(hours: 1);
  final complex = Duration(hours: 2, minutes: 30, seconds: 15);

  print(fiveMinutes);         // 0:05:00.000000
  print(oneHour);             // 1:00:00.000000
  print(complex);             // 2:30:15.000000

  // الوصول للمكونات
  print('بالدقائق: ${complex.inMinutes}');       // 150
  print('بالثواني: ${complex.inSeconds}');       // 9015
  print('بالمللي ثانية: ${complex.inMilliseconds}');  // 9015000

  // حساب المدة
  final total = fiveMinutes + oneHour;
  print('المجموع: $total');  // 1:05:00.000000

  final difference = oneHour - fiveMinutes;
  print('الفرق: $difference');  // 0:55:00.000000

  final doubled = fiveMinutes * 2;
  print('المضاعف: $doubled');  // 0:10:00.000000

  // المدد السالبة
  final negative = Duration(hours: -2);
  print(negative);                // -2:00:00.000000
  print(negative.isNegative);     // true
  print(negative.abs());          // 2:00:00.000000

  // المقارنة
  print(fiveMinutes < oneHour);   // true
  print(fiveMinutes > oneHour);   // false
  print(fiveMinutes == Duration(seconds: 300));  // true
}

حساب التواريخ

يمكنك إضافة أو طرح Duration من DateTime لحساب التواريخ المستقبلية أو الماضية. للعمليات المدركة للتقويم (مثل إضافة أشهر)، تحتاج حسابات يدوية.

إضافة وطرح الوقت

void main() {
  final now = DateTime(2024, 3, 15, 10, 0);

  // إضافة مدة
  final inTwoHours = now.add(Duration(hours: 2));
  print(inTwoHours);  // 2024-03-15 12:00:00.000

  final tomorrow = now.add(Duration(days: 1));
  print(tomorrow);  // 2024-03-16 10:00:00.000

  final nextWeek = now.add(Duration(days: 7));
  print(nextWeek);  // 2024-03-22 10:00:00.000

  // طرح مدة
  final yesterday = now.subtract(Duration(days: 1));
  print(yesterday);  // 2024-03-14 10:00:00.000

  // الفرق بين تاريخين
  final start = DateTime(2024, 1, 1);
  final end = DateTime(2024, 12, 31);
  final diff = end.difference(start);
  print('الأيام بينهما: ${diff.inDays}');  // 365

  // إضافة أشهر (مدرك للتقويم — يجب أن يتم يدوياً)
  DateTime addMonths(DateTime date, int months) {
    var year = date.year;
    var month = date.month + months;
    while (month > 12) {
      month -= 12;
      year++;
    }
    while (month < 1) {
      month += 12;
      year--;
    }
    // تقييد اليوم للنطاق الصالح للشهر المستهدف
    final maxDay = DateTime(year, month + 1, 0).day;
    final day = date.day > maxDay ? maxDay : date.day;
    return DateTime(year, month, day, date.hour, date.minute, date.second);
  }

  final jan31 = DateTime(2024, 1, 31);
  print(addMonths(jan31, 1));  // 2024-02-29 (سنة كبيسة، مقيّد!)
  print(addMonths(jan31, 2));  // 2024-03-31
}
تحذير: إضافة Duration(days: 30) لا تضيف بشكل صحيح “شهراً واحداً” لأن الأشهر لها أطوال مختلفة (28-31 يوماً). استخدم دائماً منطقاً مدركاً للتقويم عند العمل مع الأشهر أو السنوات. وبالمثل، إضافة Duration(days: 365) ليست دائماً “سنة واحدة” بسبب السنوات الكبيسة.

مقارنة التواريخ

يوفر Dart عدة طرق لمقارنة كائنات DateTime. فهم دلالات المقارنة مهم للترتيب والتصفية والجدولة.

طرق مقارنة التواريخ

void main() {
  final date1 = DateTime(2024, 3, 15);
  final date2 = DateTime(2024, 6, 20);
  final date3 = DateTime(2024, 3, 15);

  // عوامل المقارنة
  print(date1.isBefore(date2));  // true
  print(date1.isAfter(date2));   // false
  print(date1.isAtSameMomentAs(date3));  // true

  // compareTo للترتيب
  print(date1.compareTo(date2));  // سالب (date1 قبل date2)
  print(date2.compareTo(date1));  // موجب
  print(date1.compareTo(date3));  // 0 (متساويان)

  // ترتيب قائمة من التواريخ
  final dates = [
    DateTime(2024, 12, 25),
    DateTime(2024, 1, 1),
    DateTime(2024, 7, 4),
    DateTime(2024, 3, 15),
  ];
  dates.sort((a, b) => a.compareTo(b));
  print(dates.map((d) => '${d.month}/${d.day}').toList());
  // [1/1, 3/15, 7/4, 12/25]

  // التحقق مما إذا كان التاريخ ضمن نطاق
  bool isInRange(DateTime date, DateTime start, DateTime end) {
    return !date.isBefore(start) && !date.isAfter(end);
  }

  final checkDate = DateTime(2024, 5, 10);
  print(isInRange(checkDate, date1, date2));  // true
}

تنسيق التواريخ مع حزمة intl

طريقة DateTime.toString() المدمجة في Dart تنتج تنسيق ISO، الذي ليس صديقاً للمستخدم. حزمة intl توفر DateFormat للتنسيق القابل للتخصيص والمدرك للغة.

تنسيق التواريخ مع DateFormat

import 'package:intl/intl.dart';

void main() {
  final now = DateTime(2024, 3, 15, 14, 30, 0);

  // تنسيقات محددة مسبقاً
  print(DateFormat.yMMMd().format(now));       // Mar 15, 2024
  print(DateFormat.yMMMMEEEEd().format(now));  // Friday, March 15, 2024
  print(DateFormat.Hm().format(now));          // 14:30
  print(DateFormat.jm().format(now));          // 2:30 PM

  // أنماط مخصصة
  print(DateFormat('yyyy-MM-dd').format(now));           // 2024-03-15
  print(DateFormat('dd/MM/yyyy').format(now));           // 15/03/2024
  print(DateFormat('EEEE, MMMM d, y').format(now));     // Friday, March 15, 2024
  print(DateFormat('h:mm a').format(now));                // 2:30 PM
  print(DateFormat('MMM d, y ''at'' h:mm a').format(now)); // Mar 15, 2024 at 2:30 PM

  // تنسيق خاص بالمنطقة
  print(DateFormat.yMMMd('ar').format(now));  // ١٥ مارس ٢٠٢٤
  print(DateFormat.yMMMd('fr').format(now));  // 15 mars 2024
  print(DateFormat.yMMMd('ja').format(now));  // 2024/03/15

  // التحليل مع DateFormat
  final parsed = DateFormat('MM/dd/yyyy').parse('03/15/2024');
  print(parsed);  // 2024-03-15 00:00:00.000
}
ملاحظة: لاستخدام حزمة intl، أضفها إلى pubspec.yaml: dependencies: intl: ^0.19.0. الحزمة توفر أيضاً تنسيق الأرقام وقواعد الجمع وقدرات ترجمة الرسائل بخلاف التواريخ فقط.

Stopwatch لقياس الوقت المنقضي

فئة Stopwatch توفر قياس وقت دقيق لتحليل الأداء والمقارنة المرجعية.

استخدام Stopwatch

void main() {
  final stopwatch = Stopwatch();

  // بدء القياس
  stopwatch.start();

  // محاكاة عمل
  int sum = 0;
  for (var i = 0; i < 1000000; i++) {
    sum += i;
  }

  // التوقف والقراءة
  stopwatch.stop();
  print('المنقضي: ${stopwatch.elapsed}');
  print('مللي ثانية: ${stopwatch.elapsedMilliseconds}');
  print('مايكرو ثانية: ${stopwatch.elapsedMicroseconds}');

  // إعادة التعيين وإعادة الاستخدام
  stopwatch.reset();
  print('بعد إعادة التعيين: ${stopwatch.elapsedMilliseconds}');  // 0

  // توقيت اللفات
  stopwatch.start();
  // ... المرحلة 1 ...
  final lap1 = stopwatch.elapsedMilliseconds;
  // ... المرحلة 2 ...
  final lap2 = stopwatch.elapsedMilliseconds;
  stopwatch.stop();

  print('المرحلة 1: ${lap1}ms');
  print('المرحلة 2: ${lap2 - lap1}ms');
  print('الإجمالي: ${stopwatch.elapsedMilliseconds}ms');
}

مثال عملي: حاسبة العمر

حساب عمر الشخص يتطلب معالجة دقيقة للأشهر والأيام، وليس فقط طرح السنوات.

حاسبة عمر دقيقة

class Age {
  final int years;
  final int months;
  final int days;

  Age(this.years, this.months, this.days);

  @override
  String toString() => '$years سنة، $months شهر، $days يوم';
}

Age calculateAge(DateTime birthDate, [DateTime? referenceDate]) {
  final now = referenceDate ?? DateTime.now();

  int years = now.year - birthDate.year;
  int months = now.month - birthDate.month;
  int days = now.day - birthDate.day;

  if (days < 0) {
    months--;
    // أيام الشهر السابق
    final prevMonth = DateTime(now.year, now.month, 0);
    days += prevMonth.day;
  }

  if (months < 0) {
    years--;
    months += 12;
  }

  return Age(years, months, days);
}

void main() {
  final birthday = DateTime(1995, 8, 15);
  final today = DateTime(2024, 3, 15);

  final age = calculateAge(birthday, today);
  print('العمر: $age');  // العمر: 28 سنة، 7 شهر، 0 يوم

  // الأيام حتى عيد الميلاد التالي
  var nextBirthday = DateTime(today.year, birthday.month, birthday.day);
  if (nextBirthday.isBefore(today) || nextBirthday.isAtSameMomentAs(today)) {
    nextBirthday = DateTime(today.year + 1, birthday.month, birthday.day);
  }
  final daysUntil = nextBirthday.difference(today).inDays;
  print('الأيام حتى عيد الميلاد التالي: $daysUntil');  // 153
}

مثال عملي: مؤقت العد التنازلي

مؤقت العد التنازلي يحسب الوقت المتبقي حتى تاريخ مستهدف ويعرضه بتنسيق قابل للقراءة.

مؤقت العد التنازلي

class Countdown {
  final DateTime target;

  Countdown(this.target);

  Duration get remaining {
    final now = DateTime.now();
    if (now.isAfter(target)) return Duration.zero;
    return target.difference(now);
  }

  bool get isExpired => DateTime.now().isAfter(target);

  String get formatted {
    final r = remaining;
    if (r == Duration.zero) return 'انتهى!';

    final days = r.inDays;
    final hours = r.inHours % 24;
    final minutes = r.inMinutes % 60;
    final seconds = r.inSeconds % 60;

    final parts = <String>[];
    if (days > 0) parts.add('$days يوم');
    if (hours > 0) parts.add('$hours ساعة');
    if (minutes > 0) parts.add('$minutes دقيقة');
    if (seconds > 0) parts.add('$seconds ثانية');

    return parts.join('، ');
  }
}

void main() {
  // العد التنازلي لرأس السنة 2025
  final newYear = DateTime(2025, 1, 1);
  final countdown = Countdown(newYear);

  print('الوقت حتى رأس السنة: ${countdown.formatted}');
  print('هل انتهى: ${countdown.isExpired}');
}

مثال عملي: أدوات نطاق التاريخ

عمليات نطاق التاريخ شائعة في الجدولة والتقارير وتطبيقات التقويم.

فئة نطاق التاريخ

class DateRange {
  final DateTime start;
  final DateTime end;

  DateRange(this.start, this.end) : assert(!end.isBefore(start));

  Duration get duration => end.difference(start);
  int get dayCount => duration.inDays + 1;  // شامل

  bool contains(DateTime date) {
    return !date.isBefore(start) && !date.isAfter(end);
  }

  bool overlaps(DateRange other) {
    return !end.isBefore(other.start) && !start.isAfter(other.end);
  }

  DateRange? intersection(DateRange other) {
    if (!overlaps(other)) return null;
    final s = start.isAfter(other.start) ? start : other.start;
    final e = end.isBefore(other.end) ? end : other.end;
    return DateRange(s, e);
  }

  /// يولّد جميع التواريخ في النطاق (شامل).
  Iterable<DateTime> get days sync* {
    var current = DateTime(start.year, start.month, start.day);
    final endDate = DateTime(end.year, end.month, end.day);
    while (!current.isAfter(endDate)) {
      yield current;
      current = current.add(Duration(days: 1));
    }
  }

  /// يُرجع أيام العمل فقط (الإثنين-الجمعة).
  Iterable<DateTime> get weekdays =>
      days.where((d) => d.weekday <= 5);

  @override
  String toString() => '$start - $end ($dayCount يوم)';
}

void main() {
  final q1 = DateRange(DateTime(2024, 1, 1), DateTime(2024, 3, 31));
  final march = DateRange(DateTime(2024, 3, 1), DateTime(2024, 3, 31));

  print('الربع الأول: ${q1.dayCount} يوم');  // الربع الأول: 91 يوم
  print('الربع الأول يحتوي 15 مارس: ${q1.contains(DateTime(2024, 3, 15))}');  // true
  print('الربع الأول يتداخل مع مارس: ${q1.overlaps(march)}');  // true

  final overlap = q1.intersection(march);
  print('التداخل: $overlap');  // 1-31 مارس

  // أيام العمل في مارس 2024
  final businessDays = march.weekdays.length;
  print('أيام العمل في مارس: $businessDays');  // 21
}

مثال عملي: نظام جدولة بسيط

الجمع بين DateTime و Duration ونطاقات التواريخ لبناء نظام جدولة أساسي.

مُجدول الأحداث

class ScheduleEvent {
  final String title;
  final DateTime start;
  final Duration duration;

  ScheduleEvent(this.title, this.start, this.duration);

  DateTime get end => start.add(duration);

  bool conflictsWith(ScheduleEvent other) {
    return start.isBefore(other.end) && end.isAfter(other.start);
  }

  @override
  String toString() {
    final endTime = end;
    return '$title: ${_formatTime(start)} - ${_formatTime(endTime)}';
  }

  String _formatTime(DateTime dt) =>
      '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}

class Scheduler {
  final List<ScheduleEvent> _events = [];

  List<ScheduleEvent> get events => List.unmodifiable(_events);

  bool addEvent(ScheduleEvent event) {
    // التحقق من التعارضات
    for (final existing in _events) {
      if (existing.conflictsWith(event)) {
        print('تعارض: "${event.title}" يتداخل مع "${existing.title}"');
        return false;
      }
    }
    _events.add(event);
    _events.sort((a, b) => a.start.compareTo(b.start));
    return true;
  }

  List<ScheduleEvent> eventsOn(DateTime date) {
    return _events.where((e) =>
        e.start.year == date.year &&
        e.start.month == date.month &&
        e.start.day == date.day).toList();
  }
}

void main() {
  final scheduler = Scheduler();
  final today = DateTime(2024, 3, 15);

  scheduler.addEvent(ScheduleEvent(
    'اجتماع الفريق',
    DateTime(2024, 3, 15, 9, 0),
    Duration(hours: 1),
  ));

  scheduler.addEvent(ScheduleEvent(
    'الغداء',
    DateTime(2024, 3, 15, 12, 0),
    Duration(minutes: 45),
  ));

  // هذا سيُظهر تعارض
  scheduler.addEvent(ScheduleEvent(
    'مكالمة متعارضة',
    DateTime(2024, 3, 15, 9, 30),
    Duration(minutes: 30),
  ));

  print('\nالجدول لليوم:');
  for (final event in scheduler.eventsOn(today)) {
    print('  $event');
  }
  // اجتماع الفريق: 09:00 - 10:00
  // الغداء: 12:00 - 12:45
}
نصيحة: لأنظمة الجدولة الإنتاجية، فكّر في استخدام حزمة timezone لدعم مناطق IANA الزمنية المناسبة. DateTime المدمج في Dart يدعم فقط UTC والمنطقة الزمنية المحلية للنظام، وهو غير كافٍ للتطبيقات التي تجدول أحداثاً عبر مناطق زمنية متعددة.

الملخص

يوفر Dart أدوات متينة لعمليات التاريخ والوقت والمدة. النقاط الرئيسية:

  • DateTime يمثل نقطة في الزمن؛ أنشئ بالمنشئات أو .now() أو .parse().
  • خزّن/أرسل دائماً بـ UTC؛ حوّل للمحلي فقط للعرض.
  • Duration يمثل فترة من الزمن؛ استخدم add() و subtract() لحساب التواريخ.
  • إضافة الأشهر/السنوات تتطلب منطقاً مدركاً للتقويم — Duration وحدها غير كافية.
  • استخدم حزمة intl (DateFormat) للتنسيق المدرك للغة وتحليل نصوص التواريخ المخصصة.
  • Stopwatch مثالي لقياس الوقت المنقضي في الكود الحساس للأداء.
  • ابنِ أدوات مثل DateRange لاكتشاف التداخل والتكرار وعدّ أيام العمل.