From b0a9b0f8b5c73540be8de0ef61b989441ca52441 Mon Sep 17 00:00:00 2001 From: manmita Date: Wed, 28 Jan 2026 02:10:50 +0530 Subject: [PATCH 01/10] feat(7614): added s3 method at dcast and melt for data.frame --- NEWS.md | 2 ++ R/fcast.R | 6 ++++++ R/fmelt.R | 5 +++++ inst/tests/tests.Rraw | 10 ++++++++++ 4 files changed, 23 insertions(+) diff --git a/NEWS.md b/NEWS.md index 24a078836..50fe85fcc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -26,6 +26,8 @@ - Type conversion support in GForce expressions (e.g., `sum(as.numeric(x))` will use GForce, saving the need to coerce `x` in a setup step) [#2934](https://github.com/Rdatatable/data.table/issues/2934) - Arithmetic operation support in GForce (e.g., `max(x) - min(x)` will use GForce on both `max(x)` and `min(x)`, saving the need to do the subtraction in a follow-up step) [#3815](https://github.com/Rdatatable/data.table/issues/3815) +4. Added support for dcast/melt of data.frames, Thanks @MichaelChirico for the suggestion and @manmita for the PR. + ### BUG FIXES 1. `fread()` with `skip=0` and `(header=TRUE|FALSE)` no longer skips the first row when it has fewer fields than subsequent rows, [#7463](https://github.com/Rdatatable/data.table/issues/7463). Thanks @emayerhofer for the report and @ben-schwen for the fix. diff --git a/R/fcast.R b/R/fcast.R index c11eb76d5..d062bafc0 100644 --- a/R/fcast.R +++ b/R/fcast.R @@ -247,3 +247,9 @@ dcast.data.table = function(data, formula, fun.aggregate = NULL, sep = "_", ..., setattr(ans, 'sorted', lhsnames) ans } + +dcast = function(data, ...) { + if (!is.data.frame(data)) stopf("'%s' must be a data.frame", "data") + data <- as.data.table(data) + dcast.data.table(data, ...) +} \ No newline at end of file diff --git a/R/fmelt.R b/R/fmelt.R index 85a8a641e..af082884f 100644 --- a/R/fmelt.R +++ b/R/fmelt.R @@ -219,3 +219,8 @@ melt.data.table = function(data, id.vars, measure.vars, variable.name = "variabl setattr(ans, 'sorted', NULL) ans } +melt.data.frame = function(data, ...) { + if (!is.data.frame(data)) stopf("'%s' must be a data.frame", "data") + data <- as.data.table(data) + melt.data.table(data, ...) +} diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 1c7ab6837..25554bac4 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -21509,3 +21509,13 @@ setdroplevels(x) setdroplevels(y) test(2364.2, levels(x$a), levels(y$a)) rm(x, y) + +# test for data.frame reshape for melt +df_melt = data.frame(a = 1:2, b = 3:4) +dt_melt = data.table(a = 1:2, b = 3:4) +test(2365.1, melt(df_melt), melt(dt_melt)) + +# test for data.frame reshape for dcast +df_dcast = data.frame(a = c("x", "y"), b = 1:2, v = 3:4) +dt_dcast = data.table(a = c("x", "y"), b = 1:2, v = 3:4) +test(2365.2, dcast(df_dcast, a ~ b, value.var = "v"), dcast(dt_dcast, a ~ b, value.var = "v")) \ No newline at end of file From 00fa0213dd7153190e9f699162589677788d8593 Mon Sep 17 00:00:00 2001 From: manmita Date: Wed, 28 Jan 2026 02:14:59 +0530 Subject: [PATCH 02/10] feat(7614): added to news --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 50fe85fcc..4dff14591 100644 --- a/NEWS.md +++ b/NEWS.md @@ -26,7 +26,7 @@ - Type conversion support in GForce expressions (e.g., `sum(as.numeric(x))` will use GForce, saving the need to coerce `x` in a setup step) [#2934](https://github.com/Rdatatable/data.table/issues/2934) - Arithmetic operation support in GForce (e.g., `max(x) - min(x)` will use GForce on both `max(x)` and `min(x)`, saving the need to do the subtraction in a follow-up step) [#3815](https://github.com/Rdatatable/data.table/issues/3815) -4. Added support for dcast/melt of data.frames, Thanks @MichaelChirico for the suggestion and @manmita for the PR. +4. Added support for dcast/melt of data.frames, Thanks @MichaelChirico for the suggestion and @manmita for the PR. [#7614](https://github.com/Rdatatable/data.table/issues/7614) ### BUG FIXES From c50d1dc32c0020c5362c343d73b235e28ccbd88f Mon Sep 17 00:00:00 2001 From: manmita Date: Wed, 28 Jan 2026 02:16:39 +0530 Subject: [PATCH 03/10] feat(7614): code styling --- R/fcast.R | 2 +- R/fmelt.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/fcast.R b/R/fcast.R index d062bafc0..01c30f9e7 100644 --- a/R/fcast.R +++ b/R/fcast.R @@ -250,6 +250,6 @@ dcast.data.table = function(data, formula, fun.aggregate = NULL, sep = "_", ..., dcast = function(data, ...) { if (!is.data.frame(data)) stopf("'%s' must be a data.frame", "data") - data <- as.data.table(data) + data = as.data.table(data) dcast.data.table(data, ...) } \ No newline at end of file diff --git a/R/fmelt.R b/R/fmelt.R index af082884f..8b9b43b0d 100644 --- a/R/fmelt.R +++ b/R/fmelt.R @@ -221,6 +221,6 @@ melt.data.table = function(data, id.vars, measure.vars, variable.name = "variabl } melt.data.frame = function(data, ...) { if (!is.data.frame(data)) stopf("'%s' must be a data.frame", "data") - data <- as.data.table(data) + data = as.data.table(data) melt.data.table(data, ...) } From fa909a642f8b291d84dfede517480152b5f6748c Mon Sep 17 00:00:00 2001 From: manmita Date: Wed, 28 Jan 2026 02:21:00 +0530 Subject: [PATCH 04/10] feat(7614): bug fix on dcast --- R/fcast.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/fcast.R b/R/fcast.R index 01c30f9e7..506990844 100644 --- a/R/fcast.R +++ b/R/fcast.R @@ -248,7 +248,7 @@ dcast.data.table = function(data, formula, fun.aggregate = NULL, sep = "_", ..., ans } -dcast = function(data, ...) { +dcast.data.frame = function(data, ...) { if (!is.data.frame(data)) stopf("'%s' must be a data.frame", "data") data = as.data.table(data) dcast.data.table(data, ...) From 9cc619b32adf715226a7a736b17c04336c9d5585 Mon Sep 17 00:00:00 2001 From: manmita Date: Wed, 28 Jan 2026 02:41:45 +0530 Subject: [PATCH 05/10] feat(7614): added s3method to namespace --- NAMESPACE | 3 +++ R/fmelt.R | 1 + inst/tests/tests.Rraw | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index 8381a14a7..dee6f9e22 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -139,11 +139,14 @@ S3method(split, data.table) export(dcast, melt) S3method(dcast, data.table) S3method(melt, data.table) +S3method(dcast, data.frame) +S3method(melt, data.frame) # exported for historical reasons -- if reshape2 is higher on search path, # dcast(DT) will not dispatch since reshape2::dcast is not generic. So users # and many packages on CRAN call dcast.data.table() and/or melt.data.table() directly. See #3082. export(melt.data.table, dcast.data.table) +export(melt.data.frame, dcast.data.frame) importFrom(utils, capture.output, contrib.url, download.file, flush.console, getS3method, head, packageVersion, tail, untar, unzip) export(update_dev_pkg) diff --git a/R/fmelt.R b/R/fmelt.R index 8b9b43b0d..621e624ca 100644 --- a/R/fmelt.R +++ b/R/fmelt.R @@ -219,6 +219,7 @@ melt.data.table = function(data, id.vars, measure.vars, variable.name = "variabl setattr(ans, 'sorted', NULL) ans } + melt.data.frame = function(data, ...) { if (!is.data.frame(data)) stopf("'%s' must be a data.frame", "data") data = as.data.table(data) diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 25554bac4..b7139c96d 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -21513,7 +21513,7 @@ rm(x, y) # test for data.frame reshape for melt df_melt = data.frame(a = 1:2, b = 3:4) dt_melt = data.table(a = 1:2, b = 3:4) -test(2365.1, melt(df_melt), melt(dt_melt)) +test(2365.1, melt(df_melt, id.vars=1:2, measure.vars=3:4), melt(dt_melt, id.vars=1:2, measure.vars=3:4)) # test for data.frame reshape for dcast df_dcast = data.frame(a = c("x", "y"), b = 1:2, v = 3:4) From 6502e866f0b6df4ac444d5b8310e5295fed1d67a Mon Sep 17 00:00:00 2001 From: manmita Date: Wed, 28 Jan 2026 02:46:06 +0530 Subject: [PATCH 06/10] feat(7614): fix test 2365.1 as its calling undefined columns --- inst/tests/tests.Rraw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index b7139c96d..d39bcc0af 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -21513,7 +21513,7 @@ rm(x, y) # test for data.frame reshape for melt df_melt = data.frame(a = 1:2, b = 3:4) dt_melt = data.table(a = 1:2, b = 3:4) -test(2365.1, melt(df_melt, id.vars=1:2, measure.vars=3:4), melt(dt_melt, id.vars=1:2, measure.vars=3:4)) +test(2365.1, melt(df_melt, id.vars=1:2), melt(dt_melt, id.vars=1:2)) # test for data.frame reshape for dcast df_dcast = data.frame(a = c("x", "y"), b = 1:2, v = 3:4) From b4cd9ed32ba3d9887846cf5784bd972088d2207d Mon Sep 17 00:00:00 2001 From: manmita Date: Wed, 28 Jan 2026 03:53:34 +0530 Subject: [PATCH 07/10] feat(7614): removed s3method and used redirection incase of data.frame for dcast/melt --- NAMESPACE | 3 --- R/fcast.R | 13 ++++++------- R/fmelt.R | 9 ++------- inst/tests/tests.Rraw | 7 ++++--- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index dee6f9e22..8381a14a7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -139,14 +139,11 @@ S3method(split, data.table) export(dcast, melt) S3method(dcast, data.table) S3method(melt, data.table) -S3method(dcast, data.frame) -S3method(melt, data.frame) # exported for historical reasons -- if reshape2 is higher on search path, # dcast(DT) will not dispatch since reshape2::dcast is not generic. So users # and many packages on CRAN call dcast.data.table() and/or melt.data.table() directly. See #3082. export(melt.data.table, dcast.data.table) -export(melt.data.frame, dcast.data.frame) importFrom(utils, capture.output, contrib.url, download.file, flush.console, getS3method, head, packageVersion, tail, untar, unzip) export(update_dev_pkg) diff --git a/R/fcast.R b/R/fcast.R index 506990844..30c9358db 100644 --- a/R/fcast.R +++ b/R/fcast.R @@ -12,6 +12,12 @@ dcast = function( data, formula, fun.aggregate = NULL, ..., margins = NULL, subset = NULL, fill = NULL, value.var = guess(data) ) { + if (!is.data.table(data) && is.data.frame(data)) + return( + dcast.data.table(data, formula, fun.aggregate = fun.aggregate, ..., + margins = margins, subset = subset, fill = fill, + value.var = value.var) + ) UseMethod("dcast", data) } @@ -119,7 +125,6 @@ aggregate_funs = function(funs, vals, sep="_", ...) { } dcast.data.table = function(data, formula, fun.aggregate = NULL, sep = "_", ..., margins = NULL, subset = NULL, fill = NULL, drop = TRUE, value.var = guess(data), verbose = getOption("datatable.verbose"), value.var.in.dots = FALSE, value.var.in.LHSdots = value.var.in.dots, value.var.in.RHSdots = value.var.in.dots) { - if (!is.data.table(data)) stopf("'%s' must be a data.table", "data") drop = as.logical(rep_len(drop, 2L)) if (anyNA(drop)) stopf("'drop' must be logical vector with no missing entries") if (!isTRUEorFALSE(value.var.in.dots)) @@ -247,9 +252,3 @@ dcast.data.table = function(data, formula, fun.aggregate = NULL, sep = "_", ..., setattr(ans, 'sorted', lhsnames) ans } - -dcast.data.frame = function(data, ...) { - if (!is.data.frame(data)) stopf("'%s' must be a data.frame", "data") - data = as.data.table(data) - dcast.data.table(data, ...) -} \ No newline at end of file diff --git a/R/fmelt.R b/R/fmelt.R index 621e624ca..89b07723e 100644 --- a/R/fmelt.R +++ b/R/fmelt.R @@ -4,6 +4,8 @@ # redirection as well melt = function(data, ..., na.rm = FALSE, value.name = "value") { + if (!is.data.table(data) && is.data.frame(data)) + return(melt.data.table(data, ..., na.rm = na.rm, value.name = value.name)) UseMethod("melt", data) } @@ -176,7 +178,6 @@ measurev = function(fun.list, sep="_", pattern, cols, multiple.keyword="value.na melt.data.table = function(data, id.vars, measure.vars, variable.name = "variable", value.name = "value", ..., na.rm = FALSE, variable.factor = TRUE, value.factor = FALSE, verbose = getOption("datatable.verbose")) { - if (!is.data.table(data)) stopf("'%s' must be a data.table", "data") for(type.vars in c("id.vars","measure.vars")){ sub.lang <- substitute({ if (missing(VAR)) VAR=NULL @@ -219,9 +220,3 @@ melt.data.table = function(data, id.vars, measure.vars, variable.name = "variabl setattr(ans, 'sorted', NULL) ans } - -melt.data.frame = function(data, ...) { - if (!is.data.frame(data)) stopf("'%s' must be a data.frame", "data") - data = as.data.table(data) - melt.data.table(data, ...) -} diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index d39bcc0af..34df83ddb 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -13043,8 +13043,8 @@ test(1953.2, melt(DT, id.vars = 'id', measure.vars = patterns(a = 'a', b = 'b', test(1953.3, melt(DT, id.vars = 'id', measure.vars = patterns(1L)), error = 'Input patterns must be of type character') setDF(DT) -test(1953.4, melt.data.table(DT, id.vars = 'id', measure.vars = 'a'), - error = "must be a data.table") +expected = data.table(id = rep(DT$id, 2), variable = factor(rep(c("a1", "a2"), each = 3)), value = c(DT$a1, DT$a2)) +test(1953.4, melt.data.table(DT, id.vars = "id", measure.vars = c("a1", "a2")), expected) # appearance order of two low-cardinality columns that were squashed in pr#3124 DT = data.table(A=INT(1,3,2,3,2), B=1:5) # respect groups in 1st column (3's and 2's) @@ -13390,7 +13390,8 @@ setnames(DT, 'V') test(1962.084, guess(DT), 'V', message = 'Using.*value column.*override') setDF(DT) -test(1962.085, dcast.data.table(DT), error = 'must be a data.table') +test(1962.085, guess(DT), 'V', + message = 'Using.*value column.*override') setDT(DT) test(1962.086, dcast(DT, a ~ a, drop = NA), error = "'drop' must be logical vector with no missing entries") From a05b0691f753f260f1a3e394d81f94fcfc03557b Mon Sep 17 00:00:00 2001 From: manmita Date: Wed, 28 Jan 2026 03:58:13 +0530 Subject: [PATCH 08/10] feat(7614): code linting --- R/fcast.R | 6 +++--- R/fmelt.R | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/fcast.R b/R/fcast.R index 30c9358db..4d5ea99e9 100644 --- a/R/fcast.R +++ b/R/fcast.R @@ -12,10 +12,10 @@ dcast = function( data, formula, fun.aggregate = NULL, ..., margins = NULL, subset = NULL, fill = NULL, value.var = guess(data) ) { - if (!is.data.table(data) && is.data.frame(data)) + if (!is.data.table(data) && is.data.frame(data)) return( - dcast.data.table(data, formula, fun.aggregate = fun.aggregate, ..., - margins = margins, subset = subset, fill = fill, + dcast.data.table(data, formula, fun.aggregate = fun.aggregate, ..., + margins = margins, subset = subset, fill = fill, value.var = value.var) ) UseMethod("dcast", data) diff --git a/R/fmelt.R b/R/fmelt.R index 89b07723e..2e42be0c9 100644 --- a/R/fmelt.R +++ b/R/fmelt.R @@ -4,7 +4,7 @@ # redirection as well melt = function(data, ..., na.rm = FALSE, value.name = "value") { - if (!is.data.table(data) && is.data.frame(data)) + if (!is.data.table(data) && is.data.frame(data)) return(melt.data.table(data, ..., na.rm = na.rm, value.name = value.name)) UseMethod("melt", data) } From a3e5465b12b41bdff9c447e665f78aecd788fb24 Mon Sep 17 00:00:00 2001 From: manmita Date: Thu, 29 Jan 2026 03:48:07 +0530 Subject: [PATCH 09/10] feat(7614): use call to capture args and redirect to dcast.data.table and melt.data.table --- R/fcast.R | 15 ++++++++------- R/fmelt.R | 11 ++++++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/R/fcast.R b/R/fcast.R index 4d5ea99e9..8beb94c24 100644 --- a/R/fcast.R +++ b/R/fcast.R @@ -12,13 +12,14 @@ dcast = function( data, formula, fun.aggregate = NULL, ..., margins = NULL, subset = NULL, fill = NULL, value.var = guess(data) ) { - if (!is.data.table(data) && is.data.frame(data)) - return( - dcast.data.table(data, formula, fun.aggregate = fun.aggregate, ..., - margins = margins, subset = subset, fill = fill, - value.var = value.var) - ) - UseMethod("dcast", data) + if (!is.data.table(data) && is.data.frame(data)){ + mc <- match.call() + mc[[1L]] <- as.name("dcast.data.table") + eval(mc, parent.frame()) + } + else { + UseMethod("dcast", data) + } } check_formula = function(formula, varnames, valnames, value.var.in.LHSdots, value.var.in.RHSdots) { diff --git a/R/fmelt.R b/R/fmelt.R index 2e42be0c9..44cae2e6c 100644 --- a/R/fmelt.R +++ b/R/fmelt.R @@ -4,9 +4,14 @@ # redirection as well melt = function(data, ..., na.rm = FALSE, value.name = "value") { - if (!is.data.table(data) && is.data.frame(data)) - return(melt.data.table(data, ..., na.rm = na.rm, value.name = value.name)) - UseMethod("melt", data) + if (!is.data.table(data) && is.data.frame(data)){ + mc <- match.call() + mc[[1L]] <- as.name("melt.data.table") + eval(mc, parent.frame()) + } + else { + UseMethod("melt", data) + } } patterns = function(..., cols=character(0L), ignore.case=FALSE, perl=FALSE, fixed=FALSE, useBytes=FALSE) { From 3cfe6a01dc442a52f88bba6daaa4593bf9a99ccb Mon Sep 17 00:00:00 2001 From: manmita Date: Thu, 29 Jan 2026 09:00:21 +0530 Subject: [PATCH 10/10] feat(7614): updated tests and dcast, melt --- R/fcast.R | 6 ++---- R/fmelt.R | 6 ++---- inst/tests/tests.Rraw | 8 +++----- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/R/fcast.R b/R/fcast.R index 8beb94c24..3de3fcb8f 100644 --- a/R/fcast.R +++ b/R/fcast.R @@ -15,11 +15,9 @@ dcast = function( if (!is.data.table(data) && is.data.frame(data)){ mc <- match.call() mc[[1L]] <- as.name("dcast.data.table") - eval(mc, parent.frame()) - } - else { - UseMethod("dcast", data) + return(eval(mc, parent.frame())) } + UseMethod("dcast", data) } check_formula = function(formula, varnames, valnames, value.var.in.LHSdots, value.var.in.RHSdots) { diff --git a/R/fmelt.R b/R/fmelt.R index 44cae2e6c..6d26d6b10 100644 --- a/R/fmelt.R +++ b/R/fmelt.R @@ -7,11 +7,9 @@ melt = function(data, ..., na.rm = FALSE, value.name = "value") { if (!is.data.table(data) && is.data.frame(data)){ mc <- match.call() mc[[1L]] <- as.name("melt.data.table") - eval(mc, parent.frame()) - } - else { - UseMethod("melt", data) + return(eval(mc, parent.frame())) } + UseMethod("melt", data) } patterns = function(..., cols=character(0L), ignore.case=FALSE, perl=FALSE, fixed=FALSE, useBytes=FALSE) { diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 34df83ddb..06018be21 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -13044,7 +13044,7 @@ test(1953.3, melt(DT, id.vars = 'id', measure.vars = patterns(1L)), error = 'Input patterns must be of type character') setDF(DT) expected = data.table(id = rep(DT$id, 2), variable = factor(rep(c("a1", "a2"), each = 3)), value = c(DT$a1, DT$a2)) -test(1953.4, melt.data.table(DT, id.vars = "id", measure.vars = c("a1", "a2")), expected) +test(1953.4, melt(DT, id.vars = "id", measure.vars = c("a1", "a2")), expected) # appearance order of two low-cardinality columns that were squashed in pr#3124 DT = data.table(A=INT(1,3,2,3,2), B=1:5) # respect groups in 1st column (3's and 2's) @@ -13389,9 +13389,7 @@ test(1962.083, guess(DT), '(all)') setnames(DT, 'V') test(1962.084, guess(DT), 'V', message = 'Using.*value column.*override') -setDF(DT) -test(1962.085, guess(DT), 'V', - message = 'Using.*value column.*override') +## removed test of error case 1962.085 for dcast() passed a data.frame for #7614 setDT(DT) test(1962.086, dcast(DT, a ~ a, drop = NA), error = "'drop' must be logical vector with no missing entries") @@ -21519,4 +21517,4 @@ test(2365.1, melt(df_melt, id.vars=1:2), melt(dt_melt, id.vars=1:2)) # test for data.frame reshape for dcast df_dcast = data.frame(a = c("x", "y"), b = 1:2, v = 3:4) dt_dcast = data.table(a = c("x", "y"), b = 1:2, v = 3:4) -test(2365.2, dcast(df_dcast, a ~ b, value.var = "v"), dcast(dt_dcast, a ~ b, value.var = "v")) \ No newline at end of file +test(2365.2, dcast(df_dcast, a ~ b, value.var = "v"), dcast(dt_dcast, a ~ b, value.var = "v"))