From 8adf4eb548ef6e612b8440e9844e096f061fd22c Mon Sep 17 00:00:00 2001 From: LunaticSage218 Date: Mon, 20 Apr 2026 12:55:27 -0700 Subject: [PATCH 1/5] Added format specifier for issue #7694 --- NEWS.md | 2 ++ R/IDateTime.R | 10 +++++++++- inst/tests/tests.Rraw | 9 +++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 4e64f3faa0..9e6d959dc5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -48,6 +48,8 @@ 8. `frollapply()` no longer produces output longer than the input when the window length is also longer than the input [#7646](https://github.com/Rdatatable/data.table/issues/7646). Thanks to @hadley-johnson for reporting and @jangorecki for the fix. +9. `yearqtr()` now accepts 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 the date like so: YYYYQ# (e.g. 2025Q2). Thanks to @jan-swissre for the report and @LunaticSage218 for the implementation. + ### Notes 1. {data.table} now depends on R 3.5.0 (2018). diff --git a/R/IDateTime.R b/R/IDateTime.R index 49fa5abda2..772934e70d 100644 --- a/R/IDateTime.R +++ b/R/IDateTime.R @@ -366,7 +366,15 @@ 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") +yearqtr = function(x, format=c("numeric", "character")) { + format = match.arg(format) + if (format == "numeric") return(convertDate(as.IDate(x), "yearqtr")) + yr = convertDate(as.IDate(x), "year") + qtr = convertDate(as.IDate(x), "quarter") + ans = paste0(yr, "Q", qtr) + ans[is.na(as.IDate(x))] = NA_character_ + 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 b73b2767a8..9924b5f5cd 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -21577,3 +21577,12 @@ close(con) file.create(f <- tempfile()) test(2367.6, fread(file(f)), data.table(), warning="Connection has size 0.") unlink(f) + +# yearqtr() 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(2368.1, yearqtr(x, format="numeric"), c(1111.75, 2019, 2019, 2019, 2019.75, 2020, 2020, 2020.75, 2040, 2040.75, 2100, NA)) +test(2368.2, yearqtr(x, format="numeric"), yearqtr(x)) # numeric is the default, preserves backwards compatibility +test(2368.3, yearqtr(x, format="character"), c("1111Q4", "2019Q1", "2019Q1", "2019Q1", "2019Q4", "2020Q1", "2020Q1", "2020Q4", "2040Q1", "2040Q4", "2100Q1", NA_character_)) +test(2368.4, yearqtr("2016-08-03 01:02:03.45", format="character"), "2016Q3") +test(2368.5, yearqtr(NA, format="character"), NA_character_) +test(2368.6, yearqtr(x, format="invalid"), error="should be one of") From 9e531a356a25be57f4e8cb0dcea6dd7b18a1791e Mon Sep 17 00:00:00 2001 From: LunaticSage218 Date: Mon, 20 Apr 2026 17:14:09 -0700 Subject: [PATCH 2/5] Added documentation for yearqtr changes --- man/IDateTime.Rd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/man/IDateTime.Rd b/man/IDateTime.Rd index cf762337e9..376812dbae 100644 --- a/man/IDateTime.Rd +++ b/man/IDateTime.Rd @@ -98,8 +98,7 @@ month(x) quarter(x) year(x) yearmon(x) -yearqtr(x) - +yearqtr(x, format = c("numeric", "character")) } \arguments{ @@ -115,6 +114,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}{format is either "numeric (default) or "character". "character" formats like '2025Q4'"} } \details{ \code{IDate} is a date class derived from \code{Date}. It has the same From 8c045f7ca564b18743c51df2220bbfed98f45fc2 Mon Sep 17 00:00:00 2001 From: LunaticSage218 <113202587+LunaticSage218@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:56:39 -0700 Subject: [PATCH 3/5] Update man/IDateTime.Rd Co-authored-by: Benjamin Schwendinger <52290390+ben-schwen@users.noreply.github.com> --- man/IDateTime.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/IDateTime.Rd b/man/IDateTime.Rd index 376812dbae..41e36d267e 100644 --- a/man/IDateTime.Rd +++ b/man/IDateTime.Rd @@ -114,7 +114,7 @@ yearqtr(x, format = c("numeric", "character")) 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}{format is either "numeric (default) or "character". "character" formats like '2025Q4'"} + \item{format}{format is either \code{"numeric"} (default) or \code{"character"}. \code{"character"} formats the result as \code{"2025Q4"}.} } \details{ \code{IDate} is a date class derived from \code{Date}. It has the same From c325bee514eede7c98e62dcce1d7ea9e103f6298 Mon Sep 17 00:00:00 2001 From: LunaticSage218 Date: Tue, 21 Apr 2026 13:46:21 -0700 Subject: [PATCH 4/5] Optimized yearqtr formatting and fixed doc typos --- NEWS.md | 2 +- R/IDateTime.R | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index 7e8273d289..04f49960c1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -50,7 +50,7 @@ 9. `fread()` no longer replaces a literal header column name `"NA"` with an auto-generated `Vn` name when `na.strings` includes `"NA"`, [#5124](https://github.com/Rdatatable/data.table/issues/5124). Data rows still continue to parse `"NA"` as missing. Thanks @Mashin6 for the report and @shrektan for the fix. -10. `yearqtr()` now accepts 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 the date like so: YYYYQ# (e.g. 2025Q2). Thanks to @jan-swissre for the report and @LunaticSage218 for the implementation. +10. `yearqtr()` gains an optional `format` argument [#7694](https://github.com/Rdatatable/data.table/issues/7694). 'numeric' is the default and preserves the original behavior, while 'character' formats the date as YYYYQ# (e.g., 2025Q2). Thanks to @jan-swissre for the report and @LunaticSage218 for the implementation. ### Notes diff --git a/R/IDateTime.R b/R/IDateTime.R index 772934e70d..45248611df 100644 --- a/R/IDateTime.R +++ b/R/IDateTime.R @@ -368,11 +368,13 @@ year = function(x) convertDate(as.IDate(x), "year") yearmon = function(x) convertDate(as.IDate(x), "yearmon") yearqtr = function(x, format=c("numeric", "character")) { format = match.arg(format) - if (format == "numeric") return(convertDate(as.IDate(x), "yearqtr")) - yr = convertDate(as.IDate(x), "year") - qtr = convertDate(as.IDate(x), "quarter") + x_as_idate = as.IDate(x) + yqtr = convertDate(x_as_idate, "yearqtr") + if (format == "numeric") return(yqtr) + yr = floor(yqtr) + qtr = round((yqtr - yr) * 4) + 1L ans = paste0(yr, "Q", qtr) - ans[is.na(as.IDate(x))] = NA_character_ + ans[is.na(x_as_idate)] = NA_character_ ans } From 9e75f468101cf3f5dffea9208bb6672e5a9c331e Mon Sep 17 00:00:00 2001 From: LunaticSage218 Date: Wed, 22 Apr 2026 13:09:29 -0700 Subject: [PATCH 5/5] Added yearmon format --- NEWS.md | 2 +- R/IDateTime.R | 22 +++++++++++++++++----- inst/tests/tests.Rraw | 8 +++++++- man/IDateTime.Rd | 10 +++++++--- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/NEWS.md b/NEWS.md index 04f49960c1..f62bfe6a3f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -50,7 +50,7 @@ 9. `fread()` no longer replaces a literal header column name `"NA"` with an auto-generated `Vn` name when `na.strings` includes `"NA"`, [#5124](https://github.com/Rdatatable/data.table/issues/5124). Data rows still continue to parse `"NA"` as missing. Thanks @Mashin6 for the report and @shrektan for the fix. -10. `yearqtr()` gains an optional `format` argument [#7694](https://github.com/Rdatatable/data.table/issues/7694). 'numeric' is the default and preserves the original behavior, while 'character' formats the date as YYYYQ# (e.g., 2025Q2). Thanks to @jan-swissre for the report and @LunaticSage218 for the implementation. +10. `yearqtr()` and `yearmon()` gain an optional `format` argument [#7694](https://github.com/Rdatatable/data.table/issues/7694). 'numeric' is the default and preserves the original behavior, while 'character' formats the date as YYYYQ# (e.g., 2025Q2) for `yearqtr()` and YYYYM# (e.g., 2025M4) for `yearmon()`. Thanks to @jan-swissre for the report and @LunaticSage218 for the implementation. ### Notes diff --git a/R/IDateTime.R b/R/IDateTime.R index 45248611df..0bb2568c81 100644 --- a/R/IDateTime.R +++ b/R/IDateTime.R @@ -365,16 +365,28 @@ 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") +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] = paste0(yr, "M", 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) - yr = floor(yqtr) - qtr = round((yqtr - yr) * 4) + 1L - ans = paste0(yr, "Q", qtr) - ans[is.na(x_as_idate)] = NA_character_ + 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 } diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index af18b0d2b1..c1dc6561b7 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -21593,4 +21593,10 @@ test(2368.2, yearqtr(x, format="numeric"), yearqtr(x)) # numeric is the default test(2368.3, yearqtr(x, format="character"), c("1111Q4", "2019Q1", "2019Q1", "2019Q1", "2019Q4", "2020Q1", "2020Q1", "2020Q4", "2040Q1", "2040Q4", "2100Q1", NA_character_)) test(2368.4, yearqtr("2016-08-03 01:02:03.45", format="character"), "2016Q3") test(2368.5, yearqtr(NA, format="character"), NA_character_) -test(2368.6, yearqtr(x, format="invalid"), error="should be one of") + +# yearmon() could optionally output 2025M4 format #7694 +test(2369.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(2369.2, yearmon(x, format="numeric"), yearmon(x)) # numeric is the default, preserves backwards compatibility +test(2369.3, yearmon(x, format="character"), c("1111M11", "2019M1", "2019M2", "2019M3", "2019M12", "2020M2", "2020M3", "2020M12", "2040M1", "2040M12", "2100M3", NA_character_)) +test(2369.4, yearmon("2016-08-03 01:02:03.45", format="character"), "2016M8") +test(2369.5, yearmon(NA, format="character"), NA_character_) diff --git a/man/IDateTime.Rd b/man/IDateTime.Rd index 41e36d267e..a66ccd8886 100644 --- a/man/IDateTime.Rd +++ b/man/IDateTime.Rd @@ -97,7 +97,7 @@ isoyear(x) month(x) quarter(x) year(x) -yearmon(x) +yearmon(x, format = c("numeric", "character")) yearqtr(x, format = c("numeric", "character")) } @@ -114,7 +114,7 @@ yearqtr(x, format = c("numeric", "character")) 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}{format is either \code{"numeric"} (default) or \code{"character"}. \code{"character"} formats the result as \code{"2025Q4"}.} + \item{format}{format is either \code{"numeric"} (default) or \code{"character"}. \code{"character"} formats the result as \code{"2025M4"} for \code{yearmon} and \code{"2025Q4"} for \code{yearqtr}.} } \details{ \code{IDate} is a date class derived from \code{Date}. It has the same @@ -209,7 +209,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{"YYYYMM"} (e.g. \code{"2025M4"}) + and \code{"YYYYQN"} (e.g. \code{"2025Q4"}) respectively, with \code{NA} input + returned as \code{NA_character_}. \code{second}, \code{minute}, \code{hour} are taken directly from the \code{POSIXlt} representation.