Tracks
/
R
R
/
Exercises
/
Booking Up For Beauty
Booking Up For Beauty

Booking Up For Beauty

Learning Exercise

Introduction

"Dates and times are something we teach to young children. How hard can it be?"

Many programmers have made that mistake, and the subsequent experience tends to have a bad effect on their health and happiness.

Anyone doing non-trivial programming with dates and times should at least be prepared to understand and mitigate potential problems.

This document will attempt to give an overview of how R handles dates and times, but consulting the full documentation will be necessary for further details.

As a data science language, dates and times have a particular importance in R. Much of the data arrives in the form of time series: daily patient numbers during a disease outbreak, monthly employment figures reported by goverments, bird populations in a nesting colony, annual GDP for major economies, and endless more examples.

Does this mean that R makes data and time handling simple?

Sadly, no!

Date and time functionality in Base R has a poor reputation and most is now rarely used.

The lubridate package is a big improvement, but be prepared for a large and complicated library, designed to handle a large and complicated range of problems.

Originally independent, lubridate is now bundled with the tidyverse collection. Load it with either library(lubridate) or library(tidyverse).

Resources to help you work with lubridate include:

Dates and Datetimes

A date is just a day, with no time information.

Internally, dates are stored as the (signed) number of days since 1970-01-01.

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

A datetime combines date, time and timezone information.

Internally, it is stored as the number of seconds since the beginning of 1970 in the UTC timezone.

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

Perhaps surprisingly, R has no internal representation for time-only values.

Working with date and datetime values

Perhaps the most frequent date/time needs are:

  • Parse some appropriate input format to construct a date or datetime.
  • Get the required numerical or string format from a date or datetime.
  • Apply an offset to a date or datetime value.
  • Calculate the time span between two such values.
  • Get the current date (with today()) and/or datetime (with now()).

Parsing from strings

There are many ways to write dates and times, which tend to be culturally-specific. All-number dates such as "7/6/23" are ambiguous, confusing, and have led to many expensive mistakes in multinational organizations (the author says this from bitter personal experience).

The international standard (y-m-d) is defined in ISO 8601, with two main advantages:

  • Parsing is quick and unambiguous.
  • Sorting is easy, as the datetime can be treated as normal text and sorted alphabetically.

However, data science is about working with whatever data you can find, and it is often pointless to complain about how other people format it.

Base R has conversion functions which take a format string (defaulting to 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"

The format strings resemble the decades-old strptime() function, included in many languages. Programmers familiar with this syntax can optionally use the fast_strptime() function in lubridate.

More helpfully, lubridate provides a wide range of helper functions, with names that represent the order of fields. Once the functions know which values to look for, and in which order, parsing is impressively flexible.

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

Constructing from numerical values

In situations where your program has components of a date/datetime as separate numbers, they can be assembled with make_date() or make_datetime().

These are the components that can be supplied, showing their ordering and default values:

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"

Naturally, make_date() takes only year, month and day arguments.

Outputting in human-readable form

The R console automatically displays dates/datetimes as a string representation. This is usually convenient, but sometimes misleading.

Internally, dates and datetimes are numbers (plus a few attributes), not strings. Returning a date(time) when a string is expected will cause problems, so we need to know how to do the conversion.

A simple approach is to use as.character() from Base R, which outputs a string in ISO 8601 format.

There is also a format_ISO8601() function for datetimes, which has options for rounding to a desired precision.

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"

Few non-progammers want to see ISO 8601 output, so handling culture-specific date/time formats is essential.

Base R provides the format() function, which uses format strings similar to the venerable strftime() functions included in many other languages.

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

That Base R approach is ugly but reliable. These are some of the more common codes:

Code Result
%d Day number of month (5, 17, 28, etc.)
%a Abbreviated weekday (Mon, Tue, Wed, etc.)
%A Full weekday (Monday, Tuesday, etc.)
%w Weekday number (0-6, Sunday is 0)
%u Weekday number (1-7, Monday is 1)
%b Abbreviated month (Jan, Feb, etc.)
%B Full month (January, February, etc.)
%y 2-digit year (e.g. 89)
%Y 4-digit year (e.g. 1989)
%h hours (24-hr clock)
%m minutes
%s seconds
%z offset from UTC
%Z Time zone (character)

Lubridate aims to provide a more human-friendly alternative with the stamp() function, and the narrower stamp_date() and stamp_time().

These take a template string which is an example of the desired output, and create a custom function to generate that format.

Lubridate attempts to guess the format, but often runs into ambiguities and produces the wrong result. Be prepared to provide format strings (such as "dmy") to disambiguate.

For Exercism, it can also be useful to specify locale = "C", to bypass your system locale and work in English (as expected by the tests). Locales are complicated!

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"

Setting or extracting components

Lubridate provides many functions to get or set elements of a date or datetime: obvious ones such as year() and second(), but also more specialist ones such as quarter() and epiweek().

All such functions can be used to get a value, and most can also be used to set the value.

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"

For wday() (day of week) and month(), the default is integer output but:

  • Setting labels = TRUE returns an abbreviated character string corresponding to the current locale.
  • Also setting abbr = FALSE returns the full name.
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

The labels are categorical variables, which will be the subject of another Concept. Values are restricted to the listed Levels, which also show sort order.

The week starts on Sunday by default, but this can be changed with the week_start argument (use 1 for Monday).

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

A few utility functions return TRUE/FALSE values, such as leap_year() and dst() (is Daylight Saving Time active?).

Months vary in length, so days_in_month() gives the integer value.

Time spans

Both date and datetime values represent an instant along the timeline.

How do we handle concepts like "this time yesterday" or "a week from now"?

Students familiar with other science-oriented languages may be aware of datetime.timedelta in Python or the Period type in Julia.

In contrast, R has no less than three ways to represent time spans, with different strengths and limitations. As always, choose the one best suited to your context.

The various spans can easily be interconverted, with as.period(), as_duration() and as.interval().

Span types can be interrogated with is.period(), etc.

Periods

A period, such as hours(3), can be used to increment a clock time by 3 hours.

Irregularities such as daylight saving time (DST) are largely ignored.

All periods are the pluralized name of a unit: years, weeks, etc.

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"

Durations

A duration, such as dhours(3), increments physical time by 3 hours, respecting irregularities such as DST that cause clock time to deviate from physical time.

All durations add a d prefix to the corresponding period: dyears, dweeks, etc.

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

Intervals

An interval is a combination of a start and end time, created either with the interval() function or the %--% infix operator.

# 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

Timezones

Dealing with timezones can be challanging. The quote below is from the Python documentation, but equally relevant to R:

"The rules for time adjustment across the world are more political than rational, change frequently, and there is no standard suitable for every application aside from UTC."

A few general points about timezones:

  • A datetime always includes a timezone (tz); a date is not timezone-aware.
  • Datetime constructors generally default to UTC.
  • The now() function is an exception: it gets your local time from the system clock, including tz. Local tz is used by default, but others can be specified such as 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

Timezone-related arithmetic with durations is more reliable then with periods.

Conclusion

Working with dates and datetimes is usually much easier with lubridate than with Base R.

This Concept has tried to give an overview, but inevitably misses out very many details.

At the risk of repetition: the documentation is your friend, so please use it.

Instructions

In this exercise you'll be working on an appointment scheduler for a beauty salon in New York that opened on September 15th in 2012.

You have five tasks, which will all involve appointment dates. Times are in 24-hour clock format (no AM/PM). To keep timezone issues as simple as possible, assume that everything uses UTC.

1. Parse all-number appointment date

Implement the schedule_appointment() function to parse a textual representation of an appointment date into the corresponding DateTime format.

Note that the input is in U.S.-style month/day/year order.

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

2. Check if an appointment has already passed

Implement the has_passed() function that takes an appointment date and checks if the appointment was somewhere in the past:

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

3. Check if appointment is in the afternoon

Implement the is_afternoon_appointment() function that takes an appointment date and checks if the appointment is in the afternoon (>= 12:00 and < 18:00):

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

4. Check the day of the week for an appointment

Implement the day_of_week() function that returns the numerical day of the week. The salon is closed on Sundays, so Monday is day 1 and Saturday is day 6.

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

5. Reschedule an appointment

When a customer needs to reschedule an appointment, this is automatically made for the next Friday at the same time of day (as this is the slowest day of the week).

Implement the reschedule() function that takes the original appointment (as a text string in month/day/year order) and returns a rescheduled appointment.

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

If the original appointment was for Monday to Thursday, reschedule to Friday of this week. For Friday or Saturday appointments, reschedule to Friday of next week.

Edit via GitHub The link opens in a new window or tab
R Exercism

Ready to start Booking Up For Beauty?

Sign up to Exercism to learn and master R with 21 concepts111 exercises, and real human mentoring, all for free.