From 6d0dd3531b8ad61d88b2048c74dac3ae9e2da923 Mon Sep 17 00:00:00 2001 From: LunaticSage218 Date: Thu, 23 Apr 2026 12:04:29 -0700 Subject: [PATCH] yearmon() now always ends with two integers --- NEWS.md | 2 ++ R/IDateTime.R | 26 ++++++++++++++++++++++++-- inst/tests/tests.Rraw | 14 ++++++++++++++ man/IDateTime.Rd | 16 +++++++++++++--- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index 5d9e163ad7..8b0a733470 100644 --- a/NEWS.md +++ b/NEWS.md @@ -388,6 +388,8 @@ See [#2611](https://github.com/Rdatatable/data.table/issues/2611) for details. T 20. `fread()` now supports the `comment.char` argument to skip trailing comments or comment-only lines, consistent with `read.table()`, [#856](https://github.com/Rdatatable/data.table/issues/856). The default remains `comment.char = ""` (no comment parsing) for backward compatibility and performance, in contrast to `read.table(comment.char = "#")`. Thanks to @arunsrinivasan and many others for the suggestion and @ben-schwen for the implementation. +21. `yearqtr()` and `yearmon()` now gain an optional format specifier [#7694](https://github.com/Rdatatable/data.table/issues/7694). 'numeric' is the deafult, which preserves the original behavior, but 'character' formats `yearqtr()` as YYYYQ# (e.g. 2025Q2) and `yearmon()` as YYYYM## (e.g. 2025M02, 2025M10). Thanks to @jan-swissre for the report and @LunaticSage218 for the implementation. + ### BUG FIXES 1. `fread()` no longer warns on certain systems on R 4.5.0+ where the file owner can't be resolved, [#6918](https://github.com/Rdatatable/data.table/issues/6918). Thanks @ProfFancyPants for the report and PR. diff --git a/R/IDateTime.R b/R/IDateTime.R index 49fa5abda2..c30be3999e 100644 --- a/R/IDateTime.R +++ b/R/IDateTime.R @@ -365,8 +365,30 @@ isoyear = function(x) as.integer(format(as.IDate(x), "%G")) month = function(x) convertDate(as.IDate(x), "month") quarter = function(x) convertDate(as.IDate(x), "quarter") year = function(x) convertDate(as.IDate(x), "year") -yearmon = function(x) convertDate(as.IDate(x), "yearmon") -yearqtr = function(x) convertDate(as.IDate(x), "yearqtr") +yearmon = function(x, format=c("numeric", "character")) { + format = match.arg(format) + x_as_idate = as.IDate(x) + ymon = convertDate(x_as_idate, "yearmon") + if (format == "numeric") return(ymon) + ans = rep(NA_character_, length(x_as_idate)) + ok = !is.na(x_as_idate) + yr = floor(ymon[ok]) + mon = round((ymon[ok] - yr) * 12) + 1L + ans[ok] = sprintf("%dM%02d", as.integer(yr), as.integer(mon)) + ans +} +yearqtr = function(x, format=c("numeric", "character")) { + format = match.arg(format) + x_as_idate = as.IDate(x) + yqtr = convertDate(x_as_idate, "yearqtr") + if (format == "numeric") return(yqtr) + ans = rep(NA_character_, length(x_as_idate)) + ok = !is.na(x_as_idate) + yr = floor(yqtr[ok]) + qtr = round((yqtr[ok] - yr) * 4) + 1L + ans[ok] = paste0(yr, "Q", qtr) + ans +} convertDate = function(x, type) { type = match.arg(type, c("yday", "wday", "mday", "week", "month", "quarter", "year", "yearmon", "yearqtr")) diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 1b5ea162ae..9f924f773a 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -21591,3 +21591,17 @@ x = fread("x\n-1-01-01")$x test(2368.1, year(x), -1L) test(2368.2, month(x), 1L) test(2368.3, mday(x), 1L) + +# yearqtr() and yearmon() could optionally output 2025Q4 format #7694 +x = c("1111-11-11", "2019-01-01", "2019-02-28", "2019-03-01", "2019-12-31", "2020-02-29", "2020-03-01", "2020-12-31", "2040-01-01", "2040-12-31", "2100-03-01", NA) +test(2369.1, yearqtr(x, format="numeric"), c(1111.75, 2019, 2019, 2019, 2019.75, 2020, 2020, 2020.75, 2040, 2040.75, 2100, NA)) +test(2369.2, yearqtr(x, format="numeric"), yearqtr(x)) # numeric is the default, preserves backwards compatibility +test(2369.3, yearqtr(x, format="character"), c("1111Q4", "2019Q1", "2019Q1", "2019Q1", "2019Q4", "2020Q1", "2020Q1", "2020Q4", "2040Q1", "2040Q4", "2100Q1", NA_character_)) +test(2369.4, yearqtr("2016-08-03 01:02:03.45", format="character"), "2016Q3") +test(2369.5, yearqtr(NA, format="character"), NA_character_) + +test(2370.1, yearmon(x, format="numeric"), c(1111+10/12, 2019, 2019+1/12, 2019+2/12, 2019+11/12, 2020+1/12, 2020+2/12, 2020+11/12, 2040, 2040+11/12, 2100+2/12, NA)) +test(2370.2, yearmon(x, format="numeric"), yearmon(x)) # numeric is the default, preserves backwards compatibility +test(2370.3, yearmon(x, format="character"), c("1111M11", "2019M01", "2019M02", "2019M03", "2019M12", "2020M02", "2020M03", "2020M12", "2040M01", "2040M12", "2100M03", NA_character_)) +test(2370.4, yearmon("2016-08-03 01:02:03.45", format="character"), "2016M08") +test(2370.5, yearmon(NA, format="character"), NA_character_) diff --git a/man/IDateTime.Rd b/man/IDateTime.Rd index cf762337e9..7b52207840 100644 --- a/man/IDateTime.Rd +++ b/man/IDateTime.Rd @@ -97,8 +97,8 @@ isoyear(x) month(x) quarter(x) year(x) -yearmon(x) -yearqtr(x) +yearmon(x, format = c("numeric", "character")) +yearqtr(x, format = c("numeric", "character")) } @@ -115,6 +115,7 @@ yearqtr(x) the S3 generic.} \item{units}{one of the units listed for truncating. May be abbreviated.} \item{ms}{ For \code{as.ITime} methods, what should be done with sub-second fractions of input? Valid values are \code{'truncate'} (floor), \code{'nearest'} (round), and \code{'ceil'} (ceiling). See Details. } + \item{format}{For \code{yearmon} and \code{yearqtr}, either \code{"numeric"} (default) or \code{"character"}. \code{"character"} formats the result as \code{"2025M04"} for \code{yearmon} and \code{"2025Q2"} for \code{yearqtr}.} } \details{ \code{IDate} is a date class derived from \code{Date}. It has the same @@ -209,7 +210,11 @@ Similarly, \code{isoyear()} returns the ISO 8601 year corresponding to the ISO w for second, minute, hour, day of year, day of week, day of month, week, month, quarter, and year, respectively. \code{yearmon} and \code{yearqtr} return double values representing - respectively \code{year + (month-1) / 12} and \code{year + (quarter-1) / 4}. + respectively \code{year + (month-1) / 12} and \code{year + (quarter-1) / 4} + when \code{format = "numeric"} (the default). When \code{format = "character"}, + they return character vectors of the form \code{"YYYYMMM"} (e.g. \code{"2025M04"}, + zero-padded for sortability) and \code{"YYYYQN"} (e.g. \code{"2025Q2"}) respectively, + with \code{NA} input returned as \code{NA_character_}. \code{second}, \code{minute}, \code{hour} are taken directly from the \code{POSIXlt} representation. @@ -296,6 +301,11 @@ year(d2) isoweek(d2) isoyear(d2) +# Character format for yearmon() and yearqtr() +d3 = as.IDate(c("2019-01-01", "2019-12-31")) +yearmon(d3, format = "character") # "2019M01" "2019M12" +yearqtr(d3, format = "character") # "2019Q1" "2019Q4" + } \keyword{utilities}