مسیرها
/
R
R
/
تمرین‌ها
/
نوبت‌گیری برای زیبایی
نوبت‌گیری برای زیبایی

نوبت‌گیری برای زیبایی

تمرین یادگیری

مقدمه

«تاریخ‌ها و زمان‌ها چیزی است که به بچه‌های کوچک یاد می‌دهیم. چقدر می‌تواند سخت باشد؟»

برنامه‌نویسان زیادی این اشتباه را مرتکب شده‌اند و تجربه‌ی پس از آن معمولاً تأثیر بدی بر سلامت و شادی آنان می‌گذارد.

هر کسی که برنامه‌نویسی غیرساده با تاریخ و زمان انجام می‌دهد، دست‌کم باید آماده باشد مشکلات بالقوه را درک و برطرف کند.

این سند تلاش می‌کند نمای کلی‌ای از نحوه‌ی برخورد R با تاریخ‌ها و زمان‌ها ارائه دهد، اما برای جزئیات بیشتر مراجعه به مستندات کامل ضروری خواهد بود.

به‌عنوان یک زبان علم داده، تاریخ و زمان اهمیت ویژه‌ای در R دارد. بخش بزرگی از داده‌ها در قالب سری‌های زمانی می‌رسد: شمار روزانه‌ی بیماران در طول شیوع یک بیماری، آمار ماهانه‌ی اشتغال که دولت‌ها گزارش می‌کنند، جمعیت پرندگان در یک کلونی لانه‌سازی، تولید ناخالص داخلی سالانه‌ی اقتصادهای بزرگ و نمونه‌های بی‌شمار دیگری.

آیا این بدان معناست که R کار با داده و زمان را ساده می‌کند؟

متأسفانه نه!

قابلیت تاریخ و زمان در R پایه شهرت خوبی ندارد و بیشتر آن‌ها اکنون به‌ندرت استفاده می‌شوند.

بسته‌ی lubridate پیشرفت بزرگی است، اما آماده باشید برای کتابخانه‌ای بزرگ و پیچیده که برای رسیدگی به طیف وسیع و پیچیده‌ای از مسائل طراحی شده است.

lubridate در ابتدا مستقل بود، اما اکنون همراه مجموعه‌ی tidyverse ارائه می‌شود. آن را با library(lubridate) یا library(tidyverse) بارگذاری کنید.

منابعی که به شما در کار با lubridate کمک می‌کند عبارت‌اند از:

تاریخ‌ها و زمان‌ها

یک date فقط یک روز است، بدون اطلاعات زمان.

در داخل، تاریخ‌ها به‌صورت تعداد روز (با علامت) از 1970-01-01 ذخیره می‌شوند.

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

یک datetime تاریخ، زمان و اطلاعات منطقه‌ی زمانی را با هم ترکیب می‌کند.

در داخل، به‌صورت تعداد ثانیه از آغاز سال ۱۹۷۰ در منطقه‌ی زمانی 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() فقط آرگومان‌های year، month و day را می‌گیرد.

خروجی به شکل خوانا برای انسان

کنسول 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 شماره‌ی روز در ماه (۵، ۱۷، ۲۸ و غیره)
%a نام خلاصه‌ی روز هفته (Mon، Tue، Wed و غیره)
%A نام کامل روز هفته (Monday، Tuesday و غیره)
%w شماره‌ی روز هفته (۰-۶، یکشنبه ۰ است)
%u شماره‌ی روز هفته (۱-۷، دوشنبه ۱ است)
%b نام خلاصه‌ی ماه (Jan، Feb و غیره)
%B نام کامل ماه (January، February و غیره)
%y سال دورقمی (مثلاً ۸۹)
%Y سال چهاررقمی (مثلاً ۱۹۸۹)
%h ساعت (قالب ۲۴ساعته)
%m دقیقه
%s ثانیه
%z اختلاف با UTC
%Z منطقه‌ی زمانی (کاراکتری)

lubridate هدفش ارائه‌ی جایگزینی دوستدارتر برای انسان با تابع stamp() و توابع محدودترِ stamp_date() و stamp_time() است.

این‌ها یک رشته‌ی الگو می‌گیرند که نمونه‌ای از خروجی موردنظر است و یک تابع سفارشی برای تولید آن قالب می‌سازند.

lubridate تلاش می‌کند قالب را حدس بزند، اما اغلب با ابهام روبه‌رو می‌شود و نتیجه‌ی نادرست تولید می‌کند. آماده باشید رشته‌های قالب (مانند "dmy") را برای رفع ابهام ارائه دهید.

برای Exercism، تعیین locale = "C" نیز می‌تواند مفید باشد تا از locale سیستم خود عبور کنید و به انگلیسی کار کنید (همان‌طور که تست‌ها انتظار دارند). locale‌ها پیچیده‌اند!

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 یک رشته‌ی کاراکتری خلاصه مربوط به locale جاری برمی‌گرداند.
  • تنظیم 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‌ها

یک period، مانند hours(3)، می‌تواند برای افزودن ۳ ساعت به زمان ساعت به کار رود.

بی‌نظمی‌هایی مانند ساعت تابستانی (DST) تا حد زیادی نادیده گرفته می‌شوند.

همه‌ی period‌ها نام جمع‌شده‌ی یک واحد هستند: 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‌ها

یک duration، مانند dhours(3)، زمان فیزیکی را ۳ ساعت افزایش می‌دهد و بی‌نظمی‌هایی مانند DST را که باعث انحراف زمان ساعت از زمان فیزیکی می‌شوند در نظر می‌گیرد.

همه‌ی duration‌ها پیشوند d را به period متناظر اضافه می‌کنند: dyears، dweeks و غیره.

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

Interval‌ها

یک 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 از منطقه‌ی زمانی آگاه نیست.
  • سازنده‌های datetime عموماً به‌طور پیش‌فرض 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

حساب‌های مربوط به منطقه‌ی زمانی با duration‌ها قابل‌اعتمادتر از period‌هاست.

نتیجه‌گیری

کار با تاریخ‌ها و زمان‌ها معمولاً با lubridate بسیار ساده‌تر از R پایه است.

این مفهوم تلاش کرده نمای کلی‌ای ارائه دهد، اما به‌ناچار جزئیات بسیار زیادی را جا انداخته است.

با خطر تکرار: مستندات دوست شماست، پس لطفاً از آن استفاده کنید.

دستورالعمل‌ها

در این تمرین روی یک برنامه‌ریز نوبت‌دهی برای یک سالن زیبایی در نیویورک کار می‌کنید که در ۱۵ سپتامبر ۲۰۱۲ افتتاح شد.

پنج کار دارید که همه‌ی آن‌ها به تاریخ‌های نوبت مربوط می‌شوند. زمان‌ها در قالب ۲۴ ساعته هستند (بدون 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() را پیاده‌سازی کنید که یک تاریخ نوبت می‌گیرد و بررسی می‌کند آیا آن نوبت در بعدازظهر است (از ساعت ۱۲:۰۰ به بعد و پیش از ۱۸:۰۰):

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

4. بررسی روز هفته برای یک نوبت

تابع day_of_week() را پیاده‌سازی کنید که روز هفته را به‌صورت عددی برمی‌گرداند. سالن یکشنبه‌ها تعطیل است، پس دوشنبه روز ۱ است و شنبه روز ۶.

# 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 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.