diff --git a/NEWS.md b/NEWS.md index ef2148e099..63d80ca509 100644 --- a/NEWS.md +++ b/NEWS.md @@ -30,6 +30,8 @@ 2. `set()` now automatically pre-allocates new column slots if needed, similar to what `:=` already does, [#1831](https://github.com/Rdatatable/data.table/issues/1831) [#4100](https://github.com/Rdatatable/data.table/issues/4100). Thanks to @zachokeeffe and @tyner for the report and @ben-schwen for the fix. +3. `fcoalesce()` and `setcoalesce()` now allow mixing of `Date` and `IDate` classes, and mixing of `integer` and `double` types (coercing to `double` if needed), [#7463](https://github.com/Rdatatable/data.table/issues/7463). Thanks @akshkaushik for the report and the fix. + ## data.table [v1.18.0](https://github.com/Rdatatable/data.table/milestone/37?closed=1) 23 December 2025 ### BREAKING CHANGE diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 8cad916e3b..cc026816d7 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -15628,8 +15628,8 @@ test(2060.202, fcoalesce(fkt), fkt) test(2060.203, fcoalesce(bool, 1L), error='Item 2 is type integer but the first item is type logical. Please coerce before coalescing') test(2060.204, fcoalesce(bool, NA_integer_), error='Item 2 is type integer but the first item is type logical.') test(2060.205, fcoalesce(fkt, 1L), error='Item 1 is a factor but item 2 is not a factor. When factors are involved, all items must be factor') -test(2060.206, fcoalesce(num, 3L), error='Item 2 is type integer but the first item is type double') -test(2060.207, fcoalesce(int, 3), error='Item 2 is type double but the first item is type integer') +test(2060.206, fcoalesce(num, 3L), num_val) +test(2060.207, fcoalesce(int, 3), num_val) test(2060.208, fcoalesce(fkt, 'b'), error='Item 1 is a factor but item 2 is not a factor. When factors are involved, all items must be factor.') test(2060.209, fcoalesce(str, factor('b')), error='Item 2 is a factor but item 1 is not a factor. When factors are involved, all items must be factor') test(2060.212, fcoalesce(list(1), list(2)), error="The first argument is a list, data.table or data.frame. In this case there should be no other arguments provided.") @@ -15653,7 +15653,7 @@ if (test_bit64) { test(2060.302, as.character(fcoalesce(int64, as.integer64(NA), as.integer64(3))), as.character(int64_val)) test(2060.303, as.character(fcoalesce(int64, as.integer64(rep(3, 4L)))), as.character(int64_val)) test(2060.304, fcoalesce(int64, 1), error='Item 2 has a different class than item 1') - test(2060.305, fcoalesce(int64, 1L), error = 'Item 2 is type integer but the first item is type double') + test(2060.305, fcoalesce(int64, 1L), error = 'Item 2 has a different class than item 1.') } # 2060.401-405 tested nanotime moved to other.Rraw 23, #5516 # setcoalesce @@ -20607,6 +20607,7 @@ test(2294.59, VDate = fifelse(VDate == as.Date("3000-01-01"), as.Date(NA), VDate), VIDate = fifelse(VIDate == as.IDate("3000-01-01"), as.IDate(NA), VIDate), VNumA, VNumB, Count, Y_Sum)]) + # 60. label is a scalar with class not matching any variable in 'by'. test(2294.60, groupingsets(DT1, .(Count = .N, Y_Sum = sum(Y)), @@ -21968,3 +21969,21 @@ test(2356.2, options=c(datatable.alloccol=1L), set(DT, j="c", value=3), data.tab # ensure := and set are consistent if they need to overallocate DT = data.table(); DT2 = data.table() test(2356.3, options=c(datatable.alloccol=1L), {for (i in seq(10L)) set(DT, j = sprintf("V%d",i), value = i); DT}, {for (i in seq(10)) DT2[, sprintf("V%d",i) := i]; DT2}) + +# fcoalesce Date and IDate mixing, #7463 +test(2357.1, fcoalesce(as.Date("2026-01-01"), as.IDate("2026-01-02")), as.Date("2026-01-01")) +test(2357.2, { + res = fcoalesce(as.IDate("2026-01-02"), as.Date("2026-01-01")) + list(res, class(res)) +}, list(as.double(as.IDate("2026-01-02")), c("IDate", "Date"))) # promoted to double +test(2357.3, fcoalesce(1L, 2.0), 1.0) +test(2357.4, fcoalesce(2.0, 1L), 2.0) +test(2357.5, { + r = c(NA_real_, 2.0) + setcoalesce(r, 1L) + r +}, c(1.0, 2.0)) +test(2357.6, { + i = c(NA_integer_, 2L) + setcoalesce(i, 1.0) +}, error="In-place coalesce cannot promote integer to double") diff --git a/src/coalesce.c b/src/coalesce.c index 10b7b77576..af944dca2d 100644 --- a/src/coalesce.c +++ b/src/coalesce.c @@ -7,190 +7,304 @@ - The conditional checks within parallelized loops */ SEXP coalesce(SEXP x, SEXP inplaceArg, SEXP nan_is_na_arg) { - if (TYPEOF(x)!=VECSXP) internal_error(__func__, "input is list(...) at R level"); // # nocov - if (!IS_TRUE_OR_FALSE(inplaceArg)) internal_error(__func__, "argument 'inplaceArg' must be TRUE or FALSE"); // # nocov - if (!IS_TRUE_OR_FALSE(nan_is_na_arg)) internal_error(__func__, "argument 'nan_is_na_arg' must be TRUE or FALSE"); // # nocov + if (TYPEOF(x) != VECSXP) + internal_error(__func__, "input is list(...) at R level"); // # nocov + if (!IS_TRUE_OR_FALSE(inplaceArg)) + internal_error(__func__, + "argument 'inplaceArg' must be TRUE or FALSE"); // # nocov + if (!IS_TRUE_OR_FALSE(nan_is_na_arg)) + internal_error(__func__, + "argument 'nan_is_na_arg' must be TRUE or FALSE"); // # nocov const bool inplace = LOGICAL(inplaceArg)[0]; const bool nan_is_na = LOGICAL(nan_is_na_arg)[0]; const bool verbose = GetVerbose(); int nprotect = 0; - if (length(x)==0 || isNull(VECTOR_ELT(x,0))) return R_NilValue; // coalesce(NULL, "foo") return NULL even though character type mismatches type NULL - SEXP first; // the first vector (it might be the first argument, or it might be the first column of a data.table|frame - int off = 1; // when x has been pointed to the list of replacement candidates, is the first candidate in position 0 or 1 in the list - if (TYPEOF(VECTOR_ELT(x,0)) == VECSXP) { - if (length(x)!=1) - error(_("The first argument is a list, data.table or data.frame. In this case there should be no other arguments provided.")); - x = VECTOR_ELT(x,0); - if (length(x)==0) return R_NilValue; - first = VECTOR_ELT(x,0); + if (length(x) == 0 || isNull(VECTOR_ELT(x, 0))) + return R_NilValue; // coalesce(NULL, "foo") return NULL even though + // character type mismatches type NULL + SEXP first; // the first vector (it might be the first argument, or it might + // be the first column of a data.table|frame + int off = 1; // when x has been pointed to the list of replacement candidates, + // is the first candidate in position 0 or 1 in the list + if (TYPEOF(VECTOR_ELT(x, 0)) == VECSXP) { + if (length(x) != 1) + error(_("The first argument is a list, data.table or data.frame. In this " + "case there should be no other arguments provided.")); + x = VECTOR_ELT(x, 0); + if (length(x) == 0) + return R_NilValue; + first = VECTOR_ELT(x, 0); } else { - first = VECTOR_ELT(x,0); - if (length(x)>1 && TYPEOF(VECTOR_ELT(x,1))==VECSXP) { x=VECTOR_ELT(x,1); off=0; } // coalesce(x, list(y,z)) + first = VECTOR_ELT(x, 0); + if (length(x) > 1 && TYPEOF(VECTOR_ELT(x, 1)) == VECSXP) { + x = VECTOR_ELT(x, 1); + off = 0; + } // coalesce(x, list(y,z)) } - const int nval = length(x)-off; - if (nval==0) return first; + const int nval = length(x) - off; + if (nval == 0) + return first; const bool factor = isFactor(first); const int nrow = length(first); - for (int i=0; i