المسارات
/
R
R
/
التمارين
/
احجز موعدك للتجميل
احجز موعدك للتجميل

احجز موعدك للتجميل

تمرين تعلّمي

مقدمة

"التواريخ والأوقات مما نعلّمه للأطفال الصغار. فما مدى صعوبتها؟"

لقد ارتكب كثير من المبرمجين هذا الخطأ، وغالبًا ما يكون للخبرة اللاحقة أثر سيئ على صحتهم وسعادتهم.

وعلى أي شخص يمارس برمجة غير بسيطة بالتواريخ والأوقات أن يكون مستعدًا على الأقل لفهم المشكلات المحتملة والتخفيف من آثارها.

سيحاول هذا المستند تقديم نظرة عامة على كيفية تعامل R مع التواريخ والأوقات، لكن الرجوع إلى الوثائق الكاملة سيكون ضروريًا لمعرفة مزيد من التفاصيل.

وبوصفها لغة لعلوم البيانات، تحظى التواريخ والأوقات بأهمية خاصة في R. يصل الكثير من البيانات في صورة سلاسل زمنية: أعداد المرضى اليومية أثناء تفشي مرض، وأرقام التوظيف الشهرية التي تعلنها الحكومات، وأعداد الطيور في مستعمرة تعشيش، والناتج المحلي الإجمالي السنوي للاقتصادات الكبرى، وأمثلة أخرى لا تنتهي.

فهل يعني هذا أن R تجعل التعامل مع التواريخ والأوقات بسيطًا؟

للأسف، لا!

فلوظائف التواريخ والأوقات في R الأساسية سمعة سيئة، وقد أصبح معظمها الآن نادر الاستخدام.

تمثّل حزمة lubridate تحسينًا كبيرًا، لكن كن مستعدًا لمكتبة كبيرة ومعقدة، مصممة للتعامل مع مجموعة كبيرة ومعقدة من المشكلات.

كانت lubridate مستقلة في الأصل، وهي الآن مضمّنة ضمن مجموعة tidyverse. حمّلها إما بـ library(lubridate) أو library(tidyverse).

تشمل الموارد التي تساعدك في العمل مع lubridate:

التواريخ والأوقات

قيمة date هي مجرد يوم، دون أي معلومات عن الوقت.

تُخزَّن التواريخ داخليًا على هيئة عدد الأيام (الموجب أو السالب) منذ 1970-01-01.

today()
#> [1] "2026-04-22"

تجمع قيمة datetime بين التاريخ والوقت ومعلومات المنطقة الزمنية.

وتُخزَّن داخليًا على هيئة عدد الثواني منذ بداية عام 1970 في المنطقة الزمنية UTC.

now()
#> [1] "2026-04-22 15:37:11 MST"  # mid-afternoon in Arizona

وربما يكون مفاجئًا أن R ليس لديها تمثيل داخلي للقيم التي تقتصر على الوقت فقط.

العمل مع قيم date و datetime

ربما تكون احتياجات التاريخ والوقت الأكثر شيوعًا هي:

  • تحليل صيغة إدخال مناسبة لبناء تاريخ أو قيمة تاريخ ووقت.
  • استخراج الصيغة العددية أو النصية المطلوبة من تاريخ أو قيمة تاريخ ووقت.
  • تطبيق إزاحة على قيمة تاريخ أو قيمة تاريخ ووقت.
  • حساب المدة الزمنية بين قيمتين من هذا النوع.
  • الحصول على التاريخ الحالي (باستخدام today()) و/أو قيمة التاريخ والوقت الحالية (باستخدام now()).

التحليل من السلاسل النصية

هناك طرق عديدة لكتابة التواريخ والأوقات، وهذه الطرق غالبًا ما تكون مرتبطة بثقافة معينة. التواريخ المكتوبة بأرقام فقط مثل "7/6/23" غامضة ومربكة، وقد أدت إلى أخطاء مكلفة كثيرة في المنظمات متعددة الجنسيات (يقول المؤلف هذا من تجربة شخصية مريرة).

المعيار الدولي (y-m-d) معرّف في ISO 8601، وله ميزتان رئيسيتان:

  • التحليل سريع ولا لبس فيه.
  • الفرز سهل، إذ يمكن التعامل مع قيمة التاريخ والوقت كنص عادي وفرزها أبجديًا.

ومع ذلك، يقوم علم البيانات على العمل بالبيانات التي تجدها أينما كانت، وغالبًا لا جدوى من الشكوى من طريقة تنسيق الآخرين لها.

تحتوي R الأساسية على دوال تحويل تأخذ سلسلة نصية للصيغة (وصيغتها الافتراضية هي ISO 8601).

as.Date("2026-04-22")
#> [1] "2026-04-22"
as.Date("4/22/26", format = "%m/%d/%y")
#> [1] "2026-04-22"
as.POSIXct("2026-04-22 15:30:09 MST")
#> [1] "2026-04-22 15:30:09 MST"

تشبه سلاسل الصيغة النصية دالة strptime() التي يعود تاريخها إلى عقود، والمضمّنة في كثير من اللغات. ويمكن للمبرمجين المطلعين على هذه الصيغة استخدام دالة fast_strptime() في lubridate اختياريًا.

والأكثر فائدة أن lubridate توفر مجموعة واسعة من الدوال المساعدة، بأسماء تمثّل ترتيب الحقول. وبمجرد أن تعرف الدوال القيم التي تبحث عنها وترتيبها، يصبح التحليل مرنًا بشكل مثير للإعجاب.

mdy("4/22/26")
#> [1] "2026-04-22"
mdy("April 22nd 26")
#> [1] "2026-04-22"
mdy("Wednesday, April 22nd 2026")
#> [1] "2026-04-22"
mdy_hm("Wednesday, April 22nd 2026, 4:14 pm")
#> [1] "2026-04-22 16:14:00 UTC"  # defaults to your OS timezone

البناء من قيم عددية

في الحالات التي يكون فيها لبرنامجك مكوّنات تاريخ أو تاريخ ووقت بأعداد منفصلة، يمكن تجميعها باستخدام make_date() أو make_datetime().

هذه هي المكوّنات التي يمكن تمريرها، مع بيان ترتيبها وقيمها الافتراضية:

make_datetime(
  year = 1970L,
  month = 1L,
  day = 1L,
  hour = 0L,
  min = 0L,
  sec = 0,
  tz = "UTC"
)
#> [1] "1970-01-01 UTC"


make_datetime(2026, 4, 22, 17)
#> [1] "2026-04-22 17:00:00 UTC"

وبطبيعة الحال، تأخذ make_date() وسائط السنة والشهر واليوم فقط.

الإخراج في صورة يقرأها الإنسان

تعرض وحدة تحكم R التواريخ وقيم التاريخ والوقت تلقائيًا على هيئة تمثيل نصي. وهذا ملائم عادةً، لكنه مضلّل أحيانًا.

فالتواريخ وقيم التاريخ والوقت أعداد داخليًا (بالإضافة إلى بعض الخصائص)، وليست سلاسل نصية. وإرجاع تاريخ (أو تاريخ ووقت) في موضع يُتوقَّع فيه سلسلة نصية سيتسبب في مشكلات، لذا نحتاج إلى معرفة كيفية إجراء التحويل.

هناك طريقة بسيطة تتمثل في استخدام as.character() من R الأساسية، وهي تُخرج سلسلة نصية بصيغة ISO 8601.

وهناك أيضًا دالة format_ISO8601() لقيم التاريخ والوقت، ولها خيارات للتقريب إلى دقة محددة.

today() |> as.character()  # a date
#> [1] "2026-04-23"
now() |> as.character()  # a datetime
#> [1] "2026-04-23 09:07:09.779319"
now() |> format_ISO8601() # defaults to nearest second
#> [1] "2026-04-23T09:07:38"

قليل من غير المبرمجين يرغبون في رؤية مخرجات ISO 8601، لذا فإن التعامل مع صيغ التاريخ والوقت المرتبطة بثقافة معينة أمر ضروري.

وتوفر R الأساسية دالة format()، التي تستخدم سلاسل صيغة نصية مشابهة لدوال strftime() العريقة المضمّنة في كثير من اللغات الأخرى.

format(today(), "%A, %d %B %Y")
#> [1] "Thursday, 23 April 2026"

هذه الطريقة في R الأساسية قبيحة لكنها موثوقة. وهذه بعض الرموز الأكثر شيوعًا:

الرمز النتيجة
%d رقم اليوم في الشهر (5، 17، 28، إلخ.)
%a اسم اليوم المختصر (Mon، Tue، Wed، إلخ.)
%A اسم اليوم الكامل (Monday، Tuesday، إلخ.)
%w رقم اليوم في الأسبوع (0-6، الأحد هو 0)
%u رقم اليوم في الأسبوع (1-7، الاثنين هو 1)
%b اسم الشهر المختصر (Jan، Feb، إلخ.)
%B اسم الشهر الكامل (January، February، إلخ.)
%y السنة برقمين (مثل 89)
%Y السنة بأربعة أرقام (مثل 1989)
%h الساعات (بنظام 24 ساعة)
%m الدقائق
%s الثواني
%z الإزاحة عن UTC
%Z المنطقة الزمنية (نص)

تسعى lubridate إلى تقديم بديل أكثر ملاءمة للإنسان عبر دالة stamp()، والدالتين الأكثر تخصصًا stamp_date() و stamp_time().

تأخذ هاتان الدالتان سلسلة نصية قالبية تمثّل مثالًا للمخرجات المطلوبة، وتنشئان دالة مخصصة لتوليد تلك الصيغة.

تحاول lubridate تخمين الصيغة، لكنها كثيرًا ما تصطدم بحالات غموض وتُنتج نتيجة خاطئة. كن مستعدًا لتقديم سلاسل صيغة نصية (مثل "dmy") لإزالة الغموض.

وفي Exercism، قد يكون من المفيد أيضًا تحديد locale = "C" لتجاوز الإعدادات المحلية في نظامك والعمل بالإنجليزية (كما تتوقع الاختبارات). الإعدادات المحلية معقّدة!

sf <- stamp("Saturday, June 27, 2026 at 14:45", locale = "C")
#> Multiple formats matched: "%A, %B %d, %Y at %H:%M"(1), "Saturday, %Om %d, %Y at %H:%M"(1), "Saturday, %B %d, %Y at %H:%M"(1), "%A, %Om %d, %Y at %H:%M"(0)
#> Using: "%A, %B %d, %Y at %H:%M"

sf(make_datetime(2023, 3, 29, 15, 0, 0))
#> [1] "Wednesday, March 29, 2023 at 15:00"

ضبط المكوّنات أو استخراجها

توفر lubridate دوالًا كثيرة للحصول على عناصر التاريخ أو قيم التاريخ والوقت أو لضبطها: منها الواضحة مثل year() و second()، وكذلك دوال أكثر تخصصًا مثل quarter() و epiweek().

يمكن استخدام كل هذه الدوال للحصول على قيمة، ويمكن استخدام معظمها أيضًا لضبط القيمة.

dt <- now()
dt
#> [1] "2026-04-23 15:47:11 MST"

# get
month(dt)
#> [1] 4

# set
month(dt) <- 11
dt
#> [1] "2026-11-23 15:47:11 MST"

بالنسبة إلى wday() (يوم الأسبوع) و month()، يكون الإخراج الافتراضي عددًا صحيحًا، لكن:

  • ضبط labels = TRUE يُرجع سلسلة نصية من الأحرف المختصرة توافق الإعدادات المحلية الحالية.
  • وضبط abbr = FALSE أيضًا يُرجع الاسم الكامل.
d <- today()
d
#> [1] "2026-04-23"
wday(d)
#> [1] 5
wday(d, label = TRUE)
#> [1] Thu
#> Levels: Sun < Mon < Tue < Wed < Thu < Fri < Sat
wday(d, label = TRUE, abbr = FALSE)
#> [1] Thursday
#> Levels: Sunday < Monday < Tuesday < Wednesday < Thursday < Friday < Saturday

التسميات هي categorical variables، التي ستكون موضوع مفهوم آخر. وتقتصر القيم على Levels المدرجة، والتي تُظهر أيضًا ترتيب الفرز.

يبدأ الأسبوع يوم الأحد افتراضيًا، لكن يمكن تغيير ذلك باستخدام الوسيط week_start (استخدم 1 للاثنين).

dt <- make_datetime(2026, 4, 23)

# default behavior
wday(dt)
#> [1] 5
wday(dt, label = T)
#> [1] Thu

# set week_start to Monday
wday(dt, week_start = 1)
#> [1] 4

تُرجع بعض الدوال المساعدة قيم TRUE/FALSE، مثل leap_year() و dst() (هل التوقيت الصيفي مفعّل؟).

وتتفاوت الأشهر في الطول، لذا تُعطي days_in_month() القيمة الصحيحة.

المدد الزمنية

تمثّل كل من قيمتي date و datetime لحظة على الخط الزمني.

فكيف نتعامل مع مفاهيم مثل "هذا الوقت من الأمس" أو "بعد أسبوع من الآن"؟

قد يكون الطلاب المطلعون على لغات أخرى موجهة نحو العلوم على دراية بـ datetime.timedelta في Python أو النوع Period في Julia.

وفي المقابل، تمتلك R ما لا يقل عن ثلاثة أساليب لتمثيل المدد الزمنية، لكل منها نقاط قوة وقيود مختلفة. وكما هو الحال دائمًا، اختر الأسلوب الأنسب لسياقك.

ويمكن التحويل بين المدد المختلفة بسهولة باستخدام as.period() و as_duration() و as.interval().

ويمكن فحص أنواع المدد باستخدام is.period() وغيره.

الفترات

يمكن استخدام period، مثل hours(3)، لزيادة وقت الساعة بمقدار 3 ساعات.

ويُتجاهل إلى حد كبير أي شذوذ مثل التوقيت الصيفي (DST).

وجميع الفترات هي اسم الوحدة في صيغة الجمع: years، weeks، إلخ.

d <- today()
d
#> [1] "2026-04-25"

d + weeks(2)
#> [1] "2026-05-09"

# dates are coerced to datetimes at midnight if necessary
d - hours(20)
#> [1] "2026-04-24 04:00:00 UTC"

hours(3)
#> [1] "3H 0M 0S"

المدد

يزيد duration، مثل dhours(3)، الوقت الفيزيائي بمقدار 3 ساعات، مع مراعاة حالات الشذوذ مثل التوقيت الصيفي التي تجعل وقت الساعة ينحرف عن الوقت الفيزيائي.

وتُضيف كل المدد السابقة d إلى الفترة المقابلة لها: dyears، dweeks، إلخ.

dhours(3)
#> [1] "10800s (~3 hours)"

الفواصل الزمنية

interval هو تركيب يجمع بين وقت بداية ووقت نهاية، ويُنشأ إما بدالة interval() أو باستخدام العامل %--% الذي يُكتب بين قيمتين.

# the next 2 lines were run about 33s apart, so now() is different
dt1 <- now()
dt2 <- now() + hours(1)

# interval(0 function)
ivl <- interval(dt1, dt2)
ivl
#> [1] 2026-04-24 16:24:54 MST--2026-04-24 17:25:27 MST

# infix operator
iv2 <- dt1 %--% dt2
iv2 == iv1
#> [1] TRUE

المناطق الزمنية

قد يكون التعامل مع المناطق الزمنية أمرًا صعبًا. والاقتباس أدناه من وثائق Python، لكنه ينطبق بالقدر نفسه على R:

"قواعد تعديل الوقت في أنحاء العالم سياسية أكثر منها عقلانية، وهي تتغير بوتيرة متكررة، ولا يوجد معيار مناسب لكل التطبيقات سوى UTC."

وهذه بعض النقاط العامة عن المناطق الزمنية:

  • تحتوي قيمة datetime دائمًا على منطقة زمنية (tz)؛ أما قيمة date فلا تراعي المنطقة الزمنية.
  • تعتمد دوال بناء قيم التاريخ والوقت افتراضيًا على UTC.
  • ودالة now() استثناء: فهي تحصل على وقتك المحلي من ساعة النظام، بما في ذلك tz. وتُستخدم المنطقة الزمنية المحلية tz افتراضيًا، لكن يمكن تحديد مناطق أخرى مثل now("UTC").
# timezone defaults to UTC
local <- ymd_hm("2026-04-25 10:32")
local
#> [1] "2026-04-25 10:32:00 UTC"

# timezone can be specified
AZ <- ymd_hm("2026-04-25 10:32", tz = "America/Phoenix")
AZ
#> [1] "2026-04-25 10:32:00 MST"

# same clocktime, different tz
local - AZ
#> Time difference of -7 hours

الحسابات المتعلقة بالمناطق الزمنية باستخدام المدد أكثر موثوقية من الحسابات باستخدام الفترات.

الخاتمة

عادةً ما يكون العمل مع التواريخ وقيم التاريخ والوقت أسهل بكثير باستخدام lubridate مقارنة بـ R الأساسية.

حاول هذا المفهوم تقديم نظرة عامة، لكنه يغفل حتمًا تفاصيل كثيرة جدًا.

وعلى خطر التكرار: الوثائق صديقتك، فاستخدمها من فضلك.

التعليمات

في هذا التمرين ستعمل على مُجدول مواعيد لصالون تجميل في نيويورك افتُتح في 15 سبتمبر 2012.

لديك خمس مهام، وكلها تتعلق بتواريخ المواعيد. الأوقات بصيغة 24 ساعة (بدون AM/PM). ولتبسيط مسائل المنطقة الزمنية قدر الإمكان، افترض أن كل شيء يستخدم التوقيت العالمي المنسق (UTC).

1. تحليل تاريخ موعد مكوّن من أرقام فقط

نفّذ دالة schedule_appointment() لتحليل تمثيل نصي لتاريخ موعد إلى صيغة DateTime المقابلة.

لاحظ أن المُدخل بترتيب الشهر/اليوم/السنة على الطريقة الأمريكية.

julia> schedule_appointment("07/25/2023 13:45:00")
2023-07-25T13:45:00

2. التحقق مما إذا كان الموعد قد مضى

نفّذ دالة has_passed() التي تأخذ تاريخ موعد وتتحقق مما إذا كان الموعد قد وقع في وقت ما في الماضي:

julia> has_passed(DateTime(1999, 12, 31, 9, 0, 0))
true

3. التحقق مما إذا كان الموعد في فترة بعد الظهر

نفّذ دالة is_afternoon_appointment() التي تأخذ تاريخ موعد وتتحقق مما إذا كان الموعد في فترة بعد الظهر (>= 12:00 و < 18:00):

julia> is_afternoon_appointment(DateTime(2023, 3, 29, 15, 0, 0))
true

4. التحقق من يوم الأسبوع للموعد

نفّذ دالة day_of_week() التي تُرجع اليوم الرقمي من أيام الأسبوع. الصالون مغلق أيام الأحد، لذا فإن الاثنين هو اليوم 1 والسبت هو اليوم 6.

# a Friday
day_of_week(make_datetime(2026, 06, 05, 12, 30, 00))
#> [1] 5

5. إعادة جدولة موعد

عندما يحتاج أحد العملاء إلى إعادة جدولة موعد، يُعاد جدولته تلقائيًا إلى يوم الجمعة التالي في الوقت نفسه من اليوم (فهو أهدأ أيام الأسبوع).

نفّذ دالة reschedule() التي تأخذ الموعد الأصلي (كسلسلة نصية بترتيب الشهر/اليوم/السنة) وتُرجع موعدًا مُعاد جدولته.

# a Saturday
reschedule("06/13/2026 12:30:00")
#> [1] "2026-06-19 12:30:00 UTC"

إذا كان الموعد الأصلي بين الاثنين والخميس، فأعِد جدولته إلى يوم الجمعة من هذا الأسبوع. أما مواعيد الجمعة أو السبت، فأعِد جدولتها إلى يوم الجمعة من الأسبوع التالي.

تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
R Exercism

مستعد لبدء احجز موعدك للتجميل؟

سجّل في Exercism لتتعلّم وتتقن R عبر 21 مفهومًا111 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.