"التواريخ والأوقات مما نعلّمه للأطفال الصغار. فما مدى صعوبتها؟"
لقد ارتكب كثير من المبرمجين هذا الخطأ، وغالبًا ما يكون للخبرة اللاحقة أثر سيئ على صحتهم وسعادتهم.
وعلى أي شخص يمارس برمجة غير بسيطة بالتواريخ والأوقات أن يكون مستعدًا على الأقل لفهم المشكلات المحتملة والتخفيف من آثارها.
سيحاول هذا المستند تقديم نظرة عامة على كيفية تعامل 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 فلا تراعي المنطقة الزمنية.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).
نفّذ دالة schedule_appointment() لتحليل تمثيل نصي لتاريخ موعد إلى صيغة DateTime المقابلة.
لاحظ أن المُدخل بترتيب الشهر/اليوم/السنة على الطريقة الأمريكية.
julia> schedule_appointment("07/25/2023 13:45:00")
2023-07-25T13:45:00
نفّذ دالة has_passed() التي تأخذ تاريخ موعد وتتحقق مما إذا كان الموعد قد وقع في وقت ما في الماضي:
julia> has_passed(DateTime(1999, 12, 31, 9, 0, 0))
true
نفّذ دالة is_afternoon_appointment() التي تأخذ تاريخ موعد وتتحقق مما إذا كان الموعد في فترة بعد الظهر (>= 12:00 و < 18:00):
julia> is_afternoon_appointment(DateTime(2023, 3, 29, 15, 0, 0))
true
نفّذ دالة day_of_week() التي تُرجع اليوم الرقمي من أيام الأسبوع.
الصالون مغلق أيام الأحد، لذا فإن الاثنين هو اليوم 1 والسبت هو اليوم 6.
# a Friday
day_of_week(make_datetime(2026, 06, 05, 12, 30, 00))
#> [1] 5
عندما يحتاج أحد العملاء إلى إعادة جدولة موعد، يُعاد جدولته تلقائيًا إلى يوم الجمعة التالي في الوقت نفسه من اليوم (فهو أهدأ أيام الأسبوع).
نفّذ دالة reschedule() التي تأخذ الموعد الأصلي (كسلسلة نصية بترتيب الشهر/اليوم/السنة) وتُرجع موعدًا مُعاد جدولته.
# a Saturday
reschedule("06/13/2026 12:30:00")
#> [1] "2026-06-19 12:30:00 UTC"
إذا كان الموعد الأصلي بين الاثنين والخميس، فأعِد جدولته إلى يوم الجمعة من هذا الأسبوع. أما مواعيد الجمعة أو السبت، فأعِد جدولتها إلى يوم الجمعة من الأسبوع التالي.
سجّل في Exercism لتتعلّم وتتقن R عبر 21 مفهومًا111 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.