A recorded year and a full date have different precision. We should preserve that distinction when converting date text, selecting a time window, or interpreting a measurement. This chapter uses the real monthly economics series supplied with ggplot2; it needs no external data file.
By the end of this chapter, you will be able to:
Extract a year from date text and parse a complete date with an explicit format.
Diagnose failed conversions and compare results with a known date column.
Parse documented month-name text with an explicit language and century check.
Select a time window without changing the observation’s recorded precision.
We use column creation and conversion from Chapter 12, text matching from Chapter 15, and row selection from Chapter 6. Load tidyverse and lubridate explicitly; the latter supplies year():
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔dplyr 1.2.1 ✔readr 2.2.0
✔forcats 1.0.1 ✔stringr 1.6.0
✔ggplot2 4.0.3 ✔tibble 3.3.1
✔lubridate 1.9.5 ✔tidyr 1.3.2
✔purrr 1.2.2
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖dplyr::filter() masks stats::filter()
✖dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
16.1 Read date text
Annual life expectancy does not need an invented month or day. Keep the Year in the earlier Our World in Data (OWID) file as a year. To learn about full date strings, we turn to a real monthly series already supplied with ggplot2. economics contains US economic indicators, including unemploy, the number of unemployed people in thousands. Its date column is already a Date. The dataset documentation describes monthly observations; the first day shown is a representation of the month, not evidence that the entire measurement occurred on that day.
A calendar date identifies a day without a clock time. A timestamp also records a time of day, whose interpretation can depend on its time zone. This chapter works with monthly observations represented by Date values. Its parsing and comparison examples preserve that monthly precision; they do not teach timestamp conversion or time-zone handling.
We will convert those existing dates to character values and back. as.character() returns the printed text representation. Adding date_text gives us an inspectable source for parsing while keeping the original date and economic measurement.
There are 574 monthly records. The displayed dates and strings look similar, but their column types differ. A Date supports date comparisons with known calendar meaning. A character value is a sequence of text characters until we interpret it.
For the first extraction examples, retain two existing source months so both centuries fit in a short preview. The familiar membership condition selects their exact text; it does not change the complete monthly_text table:
# A tibble: 2 × 3
date unemploy date_text
<date><dbl><chr>1 1967-07-01 2944 1967-07-01
2 2000-07-01 5747 2000-07-01
Sometimes we want only a component of the text. str_extract() takes character values and a matching pattern. It returns the first matching part of each string, or NA when no part matches. The call returns extracted text without changing the date strings. In a pattern, ^ means the start of a string. Start by extracting the leading 2 where it occurs:
str_extract(date_preview$date_text, "^2")
[1] NA "2"
The July 2000 string returns "2"; the July 1967 string returns NA. A missing match here does not mean the source date is missing. Our pattern simply did not ask for its first digit.
The character class [0-9] matches any one digit. Keeping ^ restricts it to the first position:
str_extract(date_preview$date_text, "^[0-9]")
[1] "1" "2"
Each ISO date now yields its first year digit. Add {4} to require four consecutive digits. The result is still character text, even though it consists of digits:
The new values identify the first four digits of the stored date strings. Extraction alone does not validate the rest of a date. A string could begin with four digits and still contain an invalid month later. For calendar calculations, parse the complete value and inspect the result.
16.2 Parse and check dates
as.Date() converts character values into Date values. With our familiar ISO text, the default conversion recognizes the year-month-day arrangement:
as.Date(date_preview$date_text)
[1] "1967-07-01" "2000-07-01"
The result is a vector of dates. It has not replaced either column in monthly_text. Make the rule explicit before storing the result. The format argument tells R how the source string is arranged: %Y is a four-digit year, %m a numeric month, and %d a day. The hyphens in "%Y-%m-%d" match the literal separators.
monthly_dates <- monthly_text |>mutate(parsed_date =as.Date(date_text, format ="%Y-%m-%d"))monthly_dates
The new column has date type, while date_text remains character. We have retained the original date as a reference. Use the familiar identical() check to compare the complete columns:
identical() compares the complete objects, including their class and missing values, and returns one logical value. Here TRUE confirms a reversible conversion for the entire column, not just the printed rows. It verifies our representation; it does not independently validate the economic source or make its monthly measurements daily observations.
An incorrect format can produce missing dates without stopping the command. To see why checking matters, deliberately ask for slashes where the source contains hyphens. This demonstration keeps the failed result in a separate column and does not replace our successfully parsed dates.
parse_diagnosis <- monthly_text |>mutate(wrong_date =as.Date(date_text, format ="%Y/%m/%d"))parse_diagnosis
# A tibble: 574 × 5
date unemploy date_text year_text wrong_date
<date><dbl><chr><chr><date> 1 1967-07-01 2944 1967-07-01 1967 NA 2 1967-08-01 2945 1967-08-01 1967 NA 3 1967-09-01 2958 1967-09-01 1967 NA 4 1967-10-01 3143 1967-10-01 1967 NA 5 1967-11-01 3066 1967-11-01 1967 NA 6 1967-12-01 3018 1967-12-01 1967 NA 7 1968-01-01 2878 1968-01-01 1968 NA 8 1968-02-01 3001 1968-02-01 1968 NA 9 1968-03-01 2877 1968-03-01 1968 NA10 1968-04-01 2709 1968-04-01 1968 NA# ℹ 564 more rows
# A tibble: 574 × 5
date unemploy date_text year_text wrong_date
<date><dbl><chr><chr><date> 1 1967-07-01 2944 1967-07-01 1967 NA 2 1967-08-01 2945 1967-08-01 1967 NA 3 1967-09-01 2958 1967-09-01 1967 NA 4 1967-10-01 3143 1967-10-01 1967 NA 5 1967-11-01 3066 1967-11-01 1967 NA 6 1967-12-01 3018 1967-12-01 1967 NA 7 1968-01-01 2878 1968-01-01 1968 NA 8 1968-02-01 3001 1968-02-01 1968 NA 9 1968-03-01 2877 1968-03-01 1968 NA10 1968-04-01 2709 1968-04-01 1968 NA# ℹ 564 more rows
The last output isolates non-missing source strings whose conversions failed. All 574 monthly records fail this deliberately mismatched format. Read a source value beside the supplied format, correct the format, and repeat the check. Do not fill failed parses with a convenient date: that would turn a conversion problem into invented information.
Some wrong formats can also return plausible but incorrect dates, particularly when the order of month and day is ambiguous. Therefore, absence of NA is only one check. Retain the source strings, inspect the known date convention, and compare complete values when a reference date column is available, as it is here.
16.3 Read month names
A source may write a month name instead of a month number. We need a parser that knows both the arrangement of the text and the language of its names. Readr’s parse_date() returns Date values from character input. Unlike as.Date(), it reports parsing problems in a warning when a non-missing input fails. We first use it on text whose interpretation we have already checked:
parse_date(date_preview$date_text)
[1] "1967-07-01" "2000-07-01"
Its default format is ISO year-month-day. The printed dates should agree with the earlier result. As with as.Date(), the returned vector does not alter the input. The format option makes the expected arrangement explicit:
readr_dates <-parse_date(monthly_text$date_text, format ="%Y-%m-%d")identical(monthly_dates$date, readr_dates)
[1] TRUE
The comparison checks the complete column again. We have not gained a different time series by using a second parser.
For month names, locale() constructs a set of reading conventions. It does not read data by itself. Inspect its default specification first:
locale()
<locale>
Numbers: 123,456.78
Formats: %AD / %AT
Timezone: UTC
Encoding: UTF-8
<date_names>
Days: Sunday (Sun), Monday (Mon), Tuesday (Tue), Wednesday (Wed), Thursday
(Thu), Friday (Fri), Saturday (Sat)
Months: January (Jan), February (Feb), March (Mar), April (Apr), May (May),
June (Jun), July (Jul), August (Aug), September (Sep), October
(Oct), November (Nov), December (Dec)
AM/PM: AM/PM
The output describes conventions for dates, numbers, and text encoding, rather than economic observations. The default uses English date names. State that choice explicitly with the date_names option set to "en", storing the result for reuse:
<locale>
Numbers: 123,456.78
Formats: %AD / %AT
Timezone: UTC
Encoding: UTF-8
<date_names>
Days: Sunday (Sun), Monday (Mon), Tuesday (Tue), Wednesday (Wed), Thursday
(Thu), Friday (Fri), Saturday (Sat)
Months: January (Jan), February (Feb), March (Mar), April (Apr), May (May),
June (Jun), July (Jul), August (Aug), September (Sep), October
(Oct), November (Nov), December (Dec)
AM/PM: AM/PM
The printed specification includes English month names and abbreviations. It also lists other reading conventions that we do not change here. A locale determines how existing text is interpreted; it does not translate or modify the source text.
Use two real observations from economics to check an alternative representation: July 1993 and June 1999. The strings below transcribe their months using English abbreviations and two-digit years. The original date and unemploy remain alongside them. The condition in if_else() chooses the July spelling for the first date and the June spelling for the other selected date:
# A tibble: 2 × 3
date unemploy month_text
<date><dbl><chr>1 1993-07-01 8930 01 Jul.93
2 1999-06-01 5951 01 Jun.99
The table has 2 source months. We have changed their text representation, not their unemployment measurements. The source dates establish the intended years as 1993 and 1999; two digits alone would not document the century.
Pass the stored locale through parse_date()’s locale option. The pattern %d %b.%y expects a day, a space, an abbreviated month, a literal period, and a two-digit year. Punctuation is part of the format, just as the hyphens were in the ISO lesson:
TRUE checks both transcribed strings against the real reference dates, including the century. If a documented month name does not parse, inspect both its spelling and language and the supplied pattern. An English locale cannot identify arbitrary names from another language. Do not treat an absence of missing results as proof that the year or month is right: compare against the source convention too. The day 01 continues to represent a monthly observation, not an event known to have occurred on that day.
16.4 Select a time window
Once parsing is checked, year() from lubridate returns the numeric calendar year of each date. Use a new calendar_year column to make the filtering rule readable. We want the monthly observations from 2000 through 2014, including both endpoint years. The comma inside filter() requires both conditions to hold.
The result retains 180 monthly observations. calendar_year repeats within each year because extraction does not aggregate observations. The unemployment measure still refers to a month and is still recorded in thousands of people. Computing one annual statistic would require a separate, explicitly chosen summary.
This distinction also protects the annual OWID data. A year can locate an annual observation on a graph without conversion into an exact date. Do not append a midyear month or day merely to satisfy a preferred storage class. Preserve what the source records and choose a representation appropriate to the comparison.
16.5 Review
str_extract() returns a matching substring; extracting a year does not validate the complete date text.
as.character() and as.Date() change representation; an explicit format and a complete comparison help check conversion.
parse_date() reads dates with an explicit format; locale() specifies the language of recorded month names. Check the century against the source.
year() extracts a calendar component without changing the observation’s recorded time precision.
Retain source strings, diagnose failed conversions, and avoid inventing days for annual or monthly measurements.
Chapter 17 next develops list columns by splitting the entity names used in Section 15.7. Later, Chapter 19 uses ordered monthly records in an applied reshaping workflow.
16.6 Exercises
Recreate date text from economics. Diagnose the slash-separated format, then parse the correct format in a new column. Check exact agreement with the original dates and explain why replacing failed dates would be inappropriate.
Before running code, predict how the number of retained months changes if the 2000–2014 window is extended through 2015. Extract the text year and calendar year independently from economics, then check the prediction. Does the first day in a displayed date establish an exact event day? Should OWID’s Year receive an added day?
Recreate the July 1993 and June 1999 source rows and their alternative month-name strings. Parse them with an English locale and verify complete equality with the source dates. Explain why "%Y-%m-%d" is unsuitable for these strings and why a format change alone would not resolve a month-name language mismatch.
Worked answers
NoteRepair the format
This answer uses only packaged data. The incorrect parse is retained for diagnosis beside the corrected result.
library(tidyverse)answer_dates <- economics |>select(date, unemploy) |>mutate(date_text =as.character(date),wrong_date =as.Date(date_text, format ="%Y/%m/%d"),parsed_date =as.Date(date_text, format ="%Y-%m-%d") )answer_failed <- answer_dates |>filter(!is.na(date_text), is.na(wrong_date))answer_failed
# A tibble: 574 × 5
date unemploy date_text wrong_date parsed_date
<date><dbl><chr><date><date> 1 1967-07-01 2944 1967-07-01 NA 1967-07-01
2 1967-08-01 2945 1967-08-01 NA 1967-08-01
3 1967-09-01 2958 1967-09-01 NA 1967-09-01
4 1967-10-01 3143 1967-10-01 NA 1967-10-01
5 1967-11-01 3066 1967-11-01 NA 1967-11-01
6 1967-12-01 3018 1967-12-01 NA 1967-12-01
7 1968-01-01 2878 1968-01-01 NA 1968-01-01
8 1968-02-01 3001 1968-02-01 NA 1968-02-01
9 1968-03-01 2877 1968-03-01 NA 1968-03-01
10 1968-04-01 2709 1968-04-01 NA 1968-04-01
# ℹ 564 more rows
All 574 failures come from a format that expects the wrong separators. The corrected column agrees exactly with the original Date column. These are conversion failures, not evidence of unknown source months. Replacing them with arbitrary dates would conceal the cause and alter the time series.
NoteRetain monthly precision
Load the packages explicitly in a fresh session. Extract the text and calendar components after retaining the source date.
The original window has 180 monthly records. Extending the endpoint can add only available source months in 2015, not an assumed twelve. It adds 4 records, as the source series ends during that year. year_text is character; calendar_year is numeric. Neither reduces twelve monthly measurements to one annual measurement. The first day represents the month in this dataset; it is not an exact event day. OWID’s annual Year should remain a year, without an assumed month or day.
# A tibble: 0 × 4# ℹ 4 variables: date <date>, unemploy <dbl>, month_text <chr>,# parsed_month <date>
The equality is TRUE, and the failure check has zero rows. The known dates verify July 1993 and June 1999, including the century. ISO format expects a four-digit year first and numeric month with hyphens, whereas these strings put the day first and use English month abbreviations. A language mismatch requires the documented language in the locale as well as the correct format. A plausible but incorrect century could pass a missingness check, which is why we retained reference dates. No source measurement has been replaced.