From ea14e01a196aec312fb856baf13f1c10cf5e306d Mon Sep 17 00:00:00 2001 From: Benjamin Schwendinger <52290390+ben-schwen@users.noreply.github.com> Date: Fri, 1 Oct 2021 21:57:27 +0200 Subject: [PATCH 01/19] fix type coercion --- NEWS.md | 2 ++ inst/tests/tests.Rraw | 22 ++++++++++++++++++++++ src/shift.c | 13 ++----------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/NEWS.md b/NEWS.md index b83869e50d..2a3dd0ff20 100644 --- a/NEWS.md +++ b/NEWS.md @@ -376,6 +376,8 @@ 44. In v1.13.2 a version of an old bug was reintroduced where during a grouping operation list columns could retain a pointer to the last group. This affected only attributes of list elements and only if those were updated during the grouping operation, [#4963](https://github.com/Rdatatable/data.table/issues/4963). Thanks to @fujiaxiang for reporting and @avimallu and Václav Tlapák for investigating and the PR. +45. `shift(x, fill)` could error if `x` had type `bit64::integer64` and a `fill` argument was also provided, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. + ## NOTES 1. New feature 29 in v1.12.4 (Oct 2019) introduced zero-copy coercion. Our thinking is that requiring you to get the type right in the case of `0` (type double) vs `0L` (type integer) is too inconvenient for you the user. So such coercions happen in `data.table` automatically without warning. Thanks to zero-copy coercion there is no speed penalty, even when calling `set()` many times in a loop, so there's no speed penalty to warn you about either. However, we believe that assigning a character value such as `"2"` into an integer column is more likely to be a user mistake that you would like to be warned about. The type difference (character vs integer) may be the only clue that you have selected the wrong column, or typed the wrong variable to be assigned to that column. For this reason we view character to numeric-like coercion differently and will warn about it. If it is correct, then the warning is intended to nudge you to wrap the RHS with `as.()` so that it is clear to readers of your code that a coercion from character to that type is intended. For example : diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index bfa901d315..04783eedd7 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -18215,3 +18215,25 @@ DT1 = data.table(id=1:3, grp=c('a', 'a', 'b'), value=4:6) DT2 = data.table(grp = c('a', 'b'), agg = list(c('1' = 4, '2' = 5), c('3' = 6))) test(2217, DT1[, by = grp, .(agg = list(setNames(as.numeric(value), id)))], DT2) +# 4865 shift coerceAs with data +funs = list(as.integer, as.double, as.complex, as.character) +testnum = 2218 +for (f in funs) { + testnum = testnum + 0.01 + DT = data.table(x=f(1:4)) + test(testnum, DT[, shift(x)], f(c(NA, 1:3))) + testnum = testnum + 0.01 + test(testnum, DT[, shift(x, fill=f(0))], f(0:3)) +} + +funs = list(as.integer, as.double) +if (test_bit64) funs = c(funs, as.integer64) +for (f1 in funs) { + DT = data.table(x=f1(1:4)) + for (f2 in funs) { + testnum = testnum + 0.01 + test(testnum, DT[, shift(x, fill=f2(NA))], f1(c(NA, 1:3))) + testnum = testnum + 0.01 + test(testnum, DT[, shift(x, fill=f2(0))], f1(0:3)) + } +} diff --git a/src/shift.c b/src/shift.c index 11346648d5..bdbcec18bd 100644 --- a/src/shift.c +++ b/src/shift.c @@ -40,7 +40,7 @@ SEXP shift(SEXP obj, SEXP k, SEXP fill, SEXP type) R_xlen_t xrows = xlength(elem); switch (TYPEOF(elem)) { case INTSXP : { - SEXP thisfill = PROTECT(coerceVector(fill, INTSXP)); + SEXP thisfill = PROTECT(coerceAs(fill, elem, ScalarLogical(0))); const int ifill = INTEGER(thisfill)[0]; UNPROTECT(1); for (int j=0; j Date: Mon, 4 Oct 2021 19:26:43 +0200 Subject: [PATCH 02/19] tweak NEWS --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 2a3dd0ff20..e98123f9b8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -376,7 +376,7 @@ 44. In v1.13.2 a version of an old bug was reintroduced where during a grouping operation list columns could retain a pointer to the last group. This affected only attributes of list elements and only if those were updated during the grouping operation, [#4963](https://github.com/Rdatatable/data.table/issues/4963). Thanks to @fujiaxiang for reporting and @avimallu and Václav Tlapák for investigating and the PR. -45. `shift(x, fill)` could error if `x` had type `bit64::integer64` and a `fill` argument was also provided, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. +45. `shift(x, fill)` could error if `x` had type `bit64::integer64` and a `fill` argument of type different than `integer32` was also provided, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. ## NOTES From 02e02d2bbc8fe05e05e5f95d59f1d7b5475a56c5 Mon Sep 17 00:00:00 2001 From: Benjamin Schwendinger <52290390+ben-schwen@users.noreply.github.com> Date: Mon, 4 Oct 2021 19:27:35 +0200 Subject: [PATCH 03/19] word order --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index e98123f9b8..7356c9ff6d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -376,7 +376,7 @@ 44. In v1.13.2 a version of an old bug was reintroduced where during a grouping operation list columns could retain a pointer to the last group. This affected only attributes of list elements and only if those were updated during the grouping operation, [#4963](https://github.com/Rdatatable/data.table/issues/4963). Thanks to @fujiaxiang for reporting and @avimallu and Václav Tlapák for investigating and the PR. -45. `shift(x, fill)` could error if `x` had type `bit64::integer64` and a `fill` argument of type different than `integer32` was also provided, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. +45. `shift(x, fill)` could error if `x` had type `bit64::integer64` and a `fill` argument of different type than `integer32` was also provided, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. ## NOTES From afc3b1fae81fa463260d31c7e8f68ffc4f0fd479 Mon Sep 17 00:00:00 2001 From: mattdowle Date: Wed, 6 Oct 2021 13:53:19 -0600 Subject: [PATCH 04/19] news item, merged two loops in test --- NEWS.md | 2 +- inst/tests/tests.Rraw | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/NEWS.md b/NEWS.md index 7356c9ff6d..9f579f5988 100644 --- a/NEWS.md +++ b/NEWS.md @@ -376,7 +376,7 @@ 44. In v1.13.2 a version of an old bug was reintroduced where during a grouping operation list columns could retain a pointer to the last group. This affected only attributes of list elements and only if those were updated during the grouping operation, [#4963](https://github.com/Rdatatable/data.table/issues/4963). Thanks to @fujiaxiang for reporting and @avimallu and Václav Tlapák for investigating and the PR. -45. `shift(x, fill)` could error if `x` had type `bit64::integer64` and a `fill` argument of different type than `integer32` was also provided, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. +45. `shift(xInt64, fill=0)` would error with `INTEGER() can only be applied to a 'integer', not a 'double'` where `xInt64` conveys `bit64::integer64` and `0` is type `double` in R, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. ## NOTES diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 04783eedd7..194ffeee49 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -18215,25 +18215,24 @@ DT1 = data.table(id=1:3, grp=c('a', 'a', 'b'), value=4:6) DT2 = data.table(grp = c('a', 'b'), agg = list(c('1' = 4, '2' = 5), c('3' = 6))) test(2217, DT1[, by = grp, .(agg = list(setNames(as.numeric(value), id)))], DT2) -# 4865 shift coerceAs with data -funs = list(as.integer, as.double, as.complex, as.character) +# shift integer64 when fill isn't integer32, #4865 +# when test_bit64==FALSE these all passed before testnum = 2218 -for (f in funs) { - testnum = testnum + 0.01 - DT = data.table(x=f(1:4)) - test(testnum, DT[, shift(x)], f(c(NA, 1:3))) - testnum = testnum + 0.01 - test(testnum, DT[, shift(x, fill=f(0))], f(0:3)) -} - -funs = list(as.integer, as.double) +funs = list(as.integer, as.double, as.complex, as.character) if (test_bit64) funs = c(funs, as.integer64) for (f1 in funs) { DT = data.table(x=f1(1:4)) for (f2 in funs) { testnum = testnum + 0.01 - test(testnum, DT[, shift(x, fill=f2(NA))], f1(c(NA, 1:3))) + test(testnum, DT[, shift(x)], f1(c(NA, 1:3))) + testnum = testnum + 0.01 + test(testnum, DT[, shift(x, fill=f2(NA))], f1(c(NA, 1:3))) testnum = testnum + 0.01 - test(testnum, DT[, shift(x, fill=f2(0))], f1(0:3)) + if (identical(f1,as.character) && identical(f2,as.complex)) + # one special case due to as.complex(0)=="0+0i"!="0" + test(testnum, DT[, shift(x, fill="0")], f1(0:3)) + else + test(testnum, DT[, shift(x, fill=f2(0))], f1(0:3)) } } + From 2fc94f0b246ccedc9debf9df8ffc2f573d68d480 Mon Sep 17 00:00:00 2001 From: mattdowle Date: Wed, 6 Oct 2021 14:19:19 -0600 Subject: [PATCH 05/19] tweak news item --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 9f579f5988..313b32a9aa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -376,7 +376,7 @@ 44. In v1.13.2 a version of an old bug was reintroduced where during a grouping operation list columns could retain a pointer to the last group. This affected only attributes of list elements and only if those were updated during the grouping operation, [#4963](https://github.com/Rdatatable/data.table/issues/4963). Thanks to @fujiaxiang for reporting and @avimallu and Václav Tlapák for investigating and the PR. -45. `shift(xInt64, fill=0)` would error with `INTEGER() can only be applied to a 'integer', not a 'double'` where `xInt64` conveys `bit64::integer64` and `0` is type `double` in R, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. +45. `shift(xInt64, fill=0)` and `shift(xInt64, fill=as.integer64(0))` (but not `shift(xInt64, fill=0L)`) would error with `INTEGER() can only be applied to a 'integer', not a 'double'` where `xInt64` conveys `bit64::integer64`, `0` is type `double` and `0L` is type integer, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. ## NOTES From 227c9815c159e4335ffdb4c9a95db1cd1c9434fc Mon Sep 17 00:00:00 2001 From: mattdowle Date: Wed, 6 Oct 2021 15:07:37 -0600 Subject: [PATCH 06/19] coerceAs can now coerce complex to integer with warning on any loss --- src/assign.c | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/assign.c b/src/assign.c index d0faf337c8..77314e8733 100644 --- a/src/assign.c +++ b/src/assign.c @@ -856,7 +856,7 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con // inside BODY that cater for 'where' or not. Maybe there's a way to merge the two macros in future. // The idea is to do these range checks without calling coerceVector() (which allocates) -#define CHECK_RANGE(STYPE, RFUN, COND, FMT, TO) {{ \ +#define CHECK_RANGE(STYPE, RFUN, COND, FMT, TO, FMTVAL) {{ \ const STYPE *sd = (const STYPE *)RFUN(source); \ for (int i=0; i255, "d", "taken as 0") + case INTSXP: CHECK_RANGE(int, INTEGER, val<0 || val>255, "d", "taken as 0", val) case REALSXP: if (sourceIsI64) - CHECK_RANGE(int64_t, REAL, val<0 || val>255, PRId64, "taken as 0") - else CHECK_RANGE(double, REAL, !R_FINITE(val) || val<0.0 || val>256.0 || (int)val!=val, "f", "either truncated (precision lost) or taken as 0") + CHECK_RANGE(int64_t, REAL, val<0 || val>255, PRId64, "taken as 0", val) + else CHECK_RANGE(double, REAL, !R_FINITE(val) || val<0.0 || val>256.0 || (int)val!=val, "f", "either truncated (precision lost) or taken as 0", val) } break; case INTSXP: - if (TYPEOF(source)==REALSXP) { - if (sourceIsI64) - CHECK_RANGE(int64_t, REAL, val!=NA_INTEGER64 && (val<=NA_INTEGER || val>INT_MAX), PRId64, "out-of-range (NA)") - else CHECK_RANGE(double, REAL, !ISNAN(val) && (!R_FINITE(val) || (int)val!=val), "f", "truncated (precision lost)") + switch (TYPEOF(source)) { + case REALSXP: if (sourceIsI64) + CHECK_RANGE(int64_t, REAL, val!=NA_INTEGER64 && (val<=NA_INTEGER || val>INT_MAX), PRId64, "out-of-range (NA)", val) + else CHECK_RANGE(double, REAL, !ISNAN(val) && (!R_FINITE(val) || (int)val!=val), "f", "truncated (precision lost)", val) + case CPLXSXP: CHECK_RANGE(Rcomplex, COMPLEX, !((ISNAN(val.i) || (R_FINITE(val.i) && val.i==0.0)) && + (ISNAN(val.r) || (R_FINITE(val.r) && (int)val.r==val.r))), "f", "either imaginary part discarded or real part truncated (precision lost)", val.r) } break; case REALSXP: if (targetIsI64 && isReal(source) && !sourceIsI64) { - CHECK_RANGE(double, REAL, !ISNAN(val) && (!R_FINITE(val) || (int)val!=val), "f", "truncated (precision lost)") + CHECK_RANGE(double, REAL, !ISNAN(val) && (!R_FINITE(val) || (int)val!=val), "f", "truncated (precision lost)", val) } } } @@ -992,6 +994,7 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con if (sourceIsI64) BODY(int64_t, REAL, int, (val==NA_INTEGER64||val>INT_MAX||val<=NA_INTEGER) ? NA_INTEGER : (int)val, td[i]=cval) else BODY(double, REAL, int, ISNAN(val) ? NA_INTEGER : (int)val, td[i]=cval) + case CPLXSXP: BODY(Rcomplex, COMPLEX, int, ISNAN(val.r) ? NA_INTEGER : (int)val.r, td[i]=cval) default: COERCE_ERROR("integer"); // test 2005.4 } } break; From 1f42d47ce12ea95d5af566bc54575e9b8abca842 Mon Sep 17 00:00:00 2001 From: mattdowle Date: Wed, 6 Oct 2021 15:17:48 -0600 Subject: [PATCH 07/19] coerceAs can now coerce complex to double with warning if any non-NA non-0 imaginary part --- src/assign.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/assign.c b/src/assign.c index 77314e8733..e2abd7488b 100644 --- a/src/assign.c +++ b/src/assign.c @@ -897,8 +897,11 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con (ISNAN(val.r) || (R_FINITE(val.r) && (int)val.r==val.r))), "f", "either imaginary part discarded or real part truncated (precision lost)", val.r) } break; case REALSXP: - if (targetIsI64 && isReal(source) && !sourceIsI64) { + switch (TYPEOF(source)) { + case REALSXP: if (targetIsI64 && !sourceIsI64) CHECK_RANGE(double, REAL, !ISNAN(val) && (!R_FINITE(val) || (int)val!=val), "f", "truncated (precision lost)", val) + case CPLXSXP: if (!targetIsI64) + CHECK_RANGE(Rcomplex, COMPLEX, !(ISNAN(val.i) || (R_FINITE(val.i) && val.i==0.0)), "f", "imaginary part discarded", val.i) } } } @@ -1025,6 +1028,7 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con memcpy(td, (double *)REAL(source), slen*sizeof(double)); break; } else BODY(double, REAL, double, val, td[i]=cval) } else BODY(int64_t, REAL, double, val==NA_INTEGER64 ? NA_REAL : val, td[i]=cval) + case CPLXSXP: BODY(Rcomplex, COMPLEX, double, val.r, td[i]=cval) default: COERCE_ERROR("double"); } } From 8348c943153db7e5243e7c7a947c751c01de84ba Mon Sep 17 00:00:00 2001 From: mattdowle Date: Wed, 6 Oct 2021 15:31:17 -0600 Subject: [PATCH 08/19] coerce from character warnings look correct, added to new test --- inst/tests/tests.Rraw | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 194ffeee49..5f77b2fc0a 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -18226,13 +18226,14 @@ for (f1 in funs) { testnum = testnum + 0.01 test(testnum, DT[, shift(x)], f1(c(NA, 1:3))) testnum = testnum + 0.01 - test(testnum, DT[, shift(x, fill=f2(NA))], f1(c(NA, 1:3))) + w = if (identical(f2,as.character) && (identical(f1,as.integer) || identical(f1,as.double))) "Coercing.*character.*to match the type of target vector" + test(testnum, DT[, shift(x, fill=f2(NA))], f1(c(NA, 1:3)), warning=w) testnum = testnum + 0.01 if (identical(f1,as.character) && identical(f2,as.complex)) # one special case due to as.complex(0)=="0+0i"!="0" test(testnum, DT[, shift(x, fill="0")], f1(0:3)) else - test(testnum, DT[, shift(x, fill=f2(0))], f1(0:3)) + test(testnum, DT[, shift(x, fill=f2(0))], f1(0:3), warning=w) } } From 23570f54608fa873a4bddac7b788c10573b1a9e3 Mon Sep 17 00:00:00 2001 From: mattdowle Date: Wed, 6 Oct 2021 15:51:12 -0600 Subject: [PATCH 09/19] add complex to integer64 to coerceAs --- src/assign.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/assign.c b/src/assign.c index e2abd7488b..b0ffb7e010 100644 --- a/src/assign.c +++ b/src/assign.c @@ -889,19 +889,21 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con else CHECK_RANGE(double, REAL, !R_FINITE(val) || val<0.0 || val>256.0 || (int)val!=val, "f", "either truncated (precision lost) or taken as 0", val) } break; case INTSXP: - switch (TYPEOF(source)) { + switch (TYPEOF(source)) { case REALSXP: if (sourceIsI64) CHECK_RANGE(int64_t, REAL, val!=NA_INTEGER64 && (val<=NA_INTEGER || val>INT_MAX), PRId64, "out-of-range (NA)", val) else CHECK_RANGE(double, REAL, !ISNAN(val) && (!R_FINITE(val) || (int)val!=val), "f", "truncated (precision lost)", val) - case CPLXSXP: CHECK_RANGE(Rcomplex, COMPLEX, !((ISNAN(val.i) || (R_FINITE(val.i) && val.i==0.0)) && + case CPLXSXP: CHECK_RANGE(Rcomplex, COMPLEX, !((ISNAN(val.i) || (R_FINITE(val.i) && val.i==0.0)) && (ISNAN(val.r) || (R_FINITE(val.r) && (int)val.r==val.r))), "f", "either imaginary part discarded or real part truncated (precision lost)", val.r) } break; case REALSXP: switch (TYPEOF(source)) { case REALSXP: if (targetIsI64 && !sourceIsI64) CHECK_RANGE(double, REAL, !ISNAN(val) && (!R_FINITE(val) || (int)val!=val), "f", "truncated (precision lost)", val) - case CPLXSXP: if (!targetIsI64) - CHECK_RANGE(Rcomplex, COMPLEX, !(ISNAN(val.i) || (R_FINITE(val.i) && val.i==0.0)), "f", "imaginary part discarded", val.i) + case CPLXSXP: if (targetIsI64) + CHECK_RANGE(Rcomplex, COMPLEX, !((ISNAN(val.i) || (R_FINITE(val.i) && val.i==0.0)) && + (ISNAN(val.r) || (R_FINITE(val.r) && (int64_t)val.r==val.r))), "f", "either imaginary part discarded or real part truncated (precision lost)", val.r) + else CHECK_RANGE(Rcomplex, COMPLEX, !(ISNAN(val.i) || (R_FINITE(val.i) && val.i==0.0)), "f", "imaginary part discarded", val.i) } } } @@ -1014,6 +1016,7 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con memcpy(td, (int64_t *)REAL(source), slen*sizeof(int64_t)); break; } else BODY(int64_t, REAL, int64_t, val, td[i]=cval) } else BODY(double, REAL, int64_t, R_FINITE(val) ? val : NA_INTEGER64, td[i]=cval) + case CPLXSXP: BODY(Rcomplex, COMPLEX, int64_t, ISNAN(val.r) ? NA_INTEGER64 : (int64_t)val.r, td[i]=cval) default: COERCE_ERROR("integer64"); } } else { From 3426f29f2aab913fd41f2c534c5b8b8ce1123fcf Mon Sep 17 00:00:00 2001 From: mattdowle Date: Thu, 7 Oct 2021 01:22:07 -0600 Subject: [PATCH 10/19] missed a break, another coerceVector->coerceAs in shift, almost passing new test now --- inst/tests/tests.Rraw | 7 ++++--- src/assign.c | 3 ++- src/shift.c | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 5f77b2fc0a..5d400d9796 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -18226,14 +18226,15 @@ for (f1 in funs) { testnum = testnum + 0.01 test(testnum, DT[, shift(x)], f1(c(NA, 1:3))) testnum = testnum + 0.01 - w = if (identical(f2,as.character) && (identical(f1,as.integer) || identical(f1,as.double))) "Coercing.*character.*to match the type of target vector" + w = if (identical(f2,as.character) && !identical(f1,as.character)) "Coercing.*character.*to match the type of target vector" test(testnum, DT[, shift(x, fill=f2(NA))], f1(c(NA, 1:3)), warning=w) testnum = testnum + 0.01 - if (identical(f1,as.character) && identical(f2,as.complex)) + if (identical(f1,as.character) && identical(f2,as.complex)) { # one special case due to as.complex(0)=="0+0i"!="0" test(testnum, DT[, shift(x, fill="0")], f1(0:3)) - else + } else { test(testnum, DT[, shift(x, fill=f2(0))], f1(0:3), warning=w) + } } } diff --git a/src/assign.c b/src/assign.c index b0ffb7e010..f8b4e01403 100644 --- a/src/assign.c +++ b/src/assign.c @@ -830,7 +830,7 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con } } } else if (isString(source) && !isString(target) && !isNewList(target)) { - warning(_("Coercing 'character' RHS to '%s' to match the type of %s."), type2char(TYPEOF(target)), targetDesc); + warning(_("Coercing 'character' RHS to '%s' to match the type of %s."), targetIsI64?"integer64":type2char(TYPEOF(target)), targetDesc); // this "Coercing ..." warning first to give context in case coerceVector warns 'NAs introduced by coercion' // and also because 'character' to integer/double coercion is often a user mistake (e.g. wrong target column, or wrong // variable on RHS) which they are more likely to appreciate than find inconvenient @@ -900,6 +900,7 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con switch (TYPEOF(source)) { case REALSXP: if (targetIsI64 && !sourceIsI64) CHECK_RANGE(double, REAL, !ISNAN(val) && (!R_FINITE(val) || (int)val!=val), "f", "truncated (precision lost)", val) + break; case CPLXSXP: if (targetIsI64) CHECK_RANGE(Rcomplex, COMPLEX, !((ISNAN(val.i) || (R_FINITE(val.i) && val.i==0.0)) && (ISNAN(val.r) || (R_FINITE(val.r) && (int64_t)val.r==val.r))), "f", "either imaginary part discarded or real part truncated (precision lost)", val.r) diff --git a/src/shift.c b/src/shift.c index bdbcec18bd..10a5da93c5 100644 --- a/src/shift.c +++ b/src/shift.c @@ -95,7 +95,7 @@ SEXP shift(SEXP obj, SEXP k, SEXP fill, SEXP type) } break; case CPLXSXP : { - SEXP thisfill = PROTECT(coerceVector(fill, CPLXSXP)); + SEXP thisfill = PROTECT(coerceAs(fill, elem, ScalarLogical(0))); // #4865 use coerceAs too const Rcomplex cfill = COMPLEX(thisfill)[0]; UNPROTECT(1); for (int j=0; j Date: Thu, 7 Oct 2021 01:41:57 -0600 Subject: [PATCH 11/19] single coerceAs in one place before type cases in shift --- src/shift.c | 20 +------------------- src/utils.c | 2 -- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/src/shift.c b/src/shift.c index 10a5da93c5..dba598fe50 100644 --- a/src/shift.c +++ b/src/shift.c @@ -38,11 +38,10 @@ SEXP shift(SEXP obj, SEXP k, SEXP fill, SEXP type) SEXP elem = VECTOR_ELT(x, i); size_t size = SIZEOF(elem); R_xlen_t xrows = xlength(elem); + SEXP thisfill = PROTECT(coerceAs(fill, elem, ScalarLogical(0))); nprotect++; // #4865 use coerceAs for type coercion switch (TYPEOF(elem)) { case INTSXP : { - SEXP thisfill = PROTECT(coerceAs(fill, elem, ScalarLogical(0))); const int ifill = INTEGER(thisfill)[0]; - UNPROTECT(1); for (int j=0; j Date: Thu, 7 Oct 2021 01:57:09 -0600 Subject: [PATCH 12/19] shorter construct for funs in test --- inst/tests/tests.Rraw | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 5d400d9796..4c9966ff26 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -18218,8 +18218,7 @@ test(2217, DT1[, by = grp, .(agg = list(setNames(as.numeric(value), id)))], DT2) # shift integer64 when fill isn't integer32, #4865 # when test_bit64==FALSE these all passed before testnum = 2218 -funs = list(as.integer, as.double, as.complex, as.character) -if (test_bit64) funs = c(funs, as.integer64) +funs = c(as.integer, as.double, as.complex, as.character, if (test_bit64) as.integer64) for (f1 in funs) { DT = data.table(x=f1(1:4)) for (f2 in funs) { From a0e827437728a3911719b94c05422429d2f6da84 Mon Sep 17 00:00:00 2001 From: mattdowle Date: Thu, 7 Oct 2021 12:57:15 -0600 Subject: [PATCH 13/19] added coerceI64toStr, not full coverage yet is known --- inst/tests/nafill.Rraw | 4 ++-- inst/tests/tests.Rraw | 2 +- src/assign.c | 6 +++--- src/data.table.h | 1 + src/utils.c | 29 +++++++++++++++++++++++++++++ 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/inst/tests/nafill.Rraw b/inst/tests/nafill.Rraw index e8ea3d7eec..e512044bbb 100644 --- a/inst/tests/nafill.Rraw +++ b/inst/tests/nafill.Rraw @@ -281,7 +281,7 @@ if (test_bit64) { x = as.integer64(1L) test(10.81, coerceAs(x, 1), 1, output="double[integer64] into double[numeric]") test(10.82, coerceAs(x, 1L), 1L, output="double[integer64] into integer[integer]") - test(10.83, coerceAs(x, "1"), error="please use as.character", output="double[integer64] into character[character]") # not yet implemented + test(10.83, coerceAs(x, "1"), "1", output="double[integer64] into character[character]") test(10.84, coerceAs(1, x), x, output="double[numeric] into double[integer64]") test(10.85, coerceAs(1L, x), x, output="integer[integer] into double[integer64]") test(10.86, coerceAs("1", x), x, output="character[character] into double[integer64]", warning="Coercing.*character") @@ -301,7 +301,7 @@ if (test_nanotime) { } options(datatable.verbose=FALSE) test(11.01, coerceAs(list(a=1), 1), error="is not atomic") -test(11.02, coerceAs(1, list(a=1)), error="is not atomic") +test(11.02, coerceAs(1, list(a=1)), list(1)) test(11.03, coerceAs(sum, 1), error="is not atomic") test(11.04, coerceAs(quote(1+1), 1), error="is not atomic") test(11.05, coerceAs(as.name("x"), 1), error="is not atomic") diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 4c9966ff26..3ac2d475c1 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -14611,7 +14611,7 @@ if (test_bit64) { warning="-1.*integer64.*position 1 taken as 0 when assigning.*raw.*column 3 named 'c'") test(2005.66, DT[2:3, f:=as.integer64(c(NA,"2147483648"))]$f, as.complex(c(-42,NA,2147483648))) DT[,h:=LETTERS[1:3]] - test(2005.67, DT[2:3, h:=as.integer64(1:2)], error="To assign integer64 to.*type character, please use as.character.") + test(2005.67, DT[2:3, h:=as.integer64(1:2)]$h, c("A","1","2")) # tests coerceI64toStr PR#5189 } # rbindlist raw type, #2819 diff --git a/src/assign.c b/src/assign.c index f8b4e01403..287908ce85 100644 --- a/src/assign.c +++ b/src/assign.c @@ -1071,9 +1071,9 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con } break; } - if (sourceIsI64) - error(_("To assign integer64 to a target of type character, please use as.character() for clarity.")); // TODO: handle that here as well - source = PROTECT(coerceVector(source, STRSXP)); protecti++; + if (sourceIsI64 && INHERITS(source, char_nanotime)) + error(_("To assign nanotime to a target of type character, please use as.character() for clarity.")); // TODO: eval as.character here, perhaps instead of coerceVector too + source = PROTECT(sourceIsI64 ? coerceI64toStr(source) : coerceVector(source, STRSXP)); protecti++; } BODY(SEXP, STRING_PTR, SEXP, val, SET_STRING_ELT(target, off+i, cval)) } diff --git a/src/data.table.h b/src/data.table.h index 6bafdc52af..710e6ab7a8 100644 --- a/src/data.table.h +++ b/src/data.table.h @@ -243,6 +243,7 @@ SEXP islockedR(SEXP x); bool need2utf8(SEXP x); SEXP coerceUtf8IfNeeded(SEXP x); SEXP coerceAs(SEXP x, SEXP as, SEXP copyArg); +SEXP coerceI64toStr(SEXP x); // types.c char *end(char *start); diff --git a/src/utils.c b/src/utils.c index 9d6f5d7592..f89ac2c115 100644 --- a/src/utils.c +++ b/src/utils.c @@ -406,3 +406,32 @@ SEXP startsWithAny(const SEXP x, const SEXP y, SEXP start) { return ScalarLogical(false); } +SEXP coerceI64toStr(SEXP x) { + // similar to fwrite.c:writeInt64 but given the need to call mkChar we can avoid its reverse() + if (!isReal(x) || !INHERITS(x, char_integer64)) + error(_("Internal error: coerceI64toStr not passed an integer64")); // # nocov + static char s[21]; // longest int -(2^63) == -9223372036854775807\0 + const int n = length(x); + const int64_t *xd = (const int64_t *)REAL(x); + SEXP ans = PROTECT(allocVector(STRSXP, n)); + const SEXP *ansd = STRING_PTR(ans); + for (int i=0; i0 && v==xd[i-1]) + SET_STRING_ELT(ans, i, ansd[i-1]); + else { + bool neg=false; + if (v<0) { neg=true; v=-v; } + char *ch = s+20; + *ch-- = '\0'; + do { *ch-- = '0'+v%10; v/=10; } while (v>0); + if (neg) *ch='-'; else ch++; + SET_STRING_ELT(ans, i, mkChar(ch)); + } + } + UNPROTECT(1); + return ans; +} + From 146163b63d97d5ae9d6f45b1d732650f1e37979f Mon Sep 17 00:00:00 2001 From: mattdowle Date: Thu, 7 Oct 2021 15:32:56 -0600 Subject: [PATCH 14/19] eval as.character --- inst/tests/nafill.Rraw | 2 +- src/assign.c | 10 +++++++--- src/data.table.h | 2 +- src/init.c | 2 ++ src/utils.c | 29 ----------------------------- 5 files changed, 11 insertions(+), 34 deletions(-) diff --git a/inst/tests/nafill.Rraw b/inst/tests/nafill.Rraw index e512044bbb..b2fa93a7f1 100644 --- a/inst/tests/nafill.Rraw +++ b/inst/tests/nafill.Rraw @@ -294,7 +294,7 @@ if (test_nanotime) { x = nanotime(1L) test(10.91, coerceAs(x, 1), 1, output="double[nanotime] into double[numeric]") test(10.92, coerceAs(x, 1L), 1L, output="double[nanotime] into integer[integer]") - test(10.93, coerceAs(x, "1"), error="please use as.character", output="double[nanotime] into character[character]") # not yet implemented + test(10.93, coerceAs(x, "1"), "1", output="double[nanotime] into character[character]") # nanotime needs an as.character method for "1" to be "1970-01-01T00:00:00.000000001+00:00" test(10.94, coerceAs(1, x), x, output="double[numeric] into double[nanotime]") test(10.95, coerceAs(1L, x), x, output="integer[integer] into double[nanotime]") test(10.96, coerceAs("1", x), x, output="character[character] into double[nanotime]", warning="Coercing.*character") diff --git a/src/assign.c b/src/assign.c index 287908ce85..f525f5ea75 100644 --- a/src/assign.c +++ b/src/assign.c @@ -1071,9 +1071,13 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con } break; } - if (sourceIsI64 && INHERITS(source, char_nanotime)) - error(_("To assign nanotime to a target of type character, please use as.character() for clarity.")); // TODO: eval as.character here, perhaps instead of coerceVector too - source = PROTECT(sourceIsI64 ? coerceI64toStr(source) : coerceVector(source, STRSXP)); protecti++; + if (OBJECT(source) && getAttrib(source, R_ClassSymbol)!=R_NilValue) { + // otherwise coerceVector doesn't call the as.character method for Date, integer64, nanotime, etc; PR#5189 + // this if() is to save the overhead of the R call eval() when we know there can be no method + source = PROTECT(eval(PROTECT(lang2(sym_as_character, source)), R_GlobalEnv)); protecti+=2; + } else { + source = PROTECT(coerceVector(source, STRSXP)); protecti++; + } } BODY(SEXP, STRING_PTR, SEXP, val, SET_STRING_ELT(target, off+i, cval)) } diff --git a/src/data.table.h b/src/data.table.h index 710e6ab7a8..a7f52b5e09 100644 --- a/src/data.table.h +++ b/src/data.table.h @@ -103,6 +103,7 @@ extern SEXP sym_datatable_locked; extern SEXP sym_tzone; extern SEXP sym_old_fread_datetime_character; extern SEXP sym_variable_table; +extern SEXP sym_as_character; extern double NA_INT64_D; extern long long NA_INT64_LL; extern Rcomplex NA_CPLX; // initialized in init.c; see there for comments @@ -243,7 +244,6 @@ SEXP islockedR(SEXP x); bool need2utf8(SEXP x); SEXP coerceUtf8IfNeeded(SEXP x); SEXP coerceAs(SEXP x, SEXP as, SEXP copyArg); -SEXP coerceI64toStr(SEXP x); // types.c char *end(char *start); diff --git a/src/init.c b/src/init.c index 0c1774508b..38b0de1e2f 100644 --- a/src/init.c +++ b/src/init.c @@ -36,6 +36,7 @@ SEXP sym_datatable_locked; SEXP sym_tzone; SEXP sym_old_fread_datetime_character; SEXP sym_variable_table; +SEXP sym_as_character; double NA_INT64_D; long long NA_INT64_LL; Rcomplex NA_CPLX; @@ -366,6 +367,7 @@ void attribute_visible R_init_data_table(DllInfo *info) sym_tzone = install("tzone"); sym_old_fread_datetime_character = install("datatable.old.fread.datetime.character"); sym_variable_table = install("variable_table"); + sym_as_character = install("as.character"); initDTthreads(); avoid_openmp_hang_within_fork(); diff --git a/src/utils.c b/src/utils.c index f89ac2c115..9d6f5d7592 100644 --- a/src/utils.c +++ b/src/utils.c @@ -406,32 +406,3 @@ SEXP startsWithAny(const SEXP x, const SEXP y, SEXP start) { return ScalarLogical(false); } -SEXP coerceI64toStr(SEXP x) { - // similar to fwrite.c:writeInt64 but given the need to call mkChar we can avoid its reverse() - if (!isReal(x) || !INHERITS(x, char_integer64)) - error(_("Internal error: coerceI64toStr not passed an integer64")); // # nocov - static char s[21]; // longest int -(2^63) == -9223372036854775807\0 - const int n = length(x); - const int64_t *xd = (const int64_t *)REAL(x); - SEXP ans = PROTECT(allocVector(STRSXP, n)); - const SEXP *ansd = STRING_PTR(ans); - for (int i=0; i0 && v==xd[i-1]) - SET_STRING_ELT(ans, i, ansd[i-1]); - else { - bool neg=false; - if (v<0) { neg=true; v=-v; } - char *ch = s+20; - *ch-- = '\0'; - do { *ch-- = '0'+v%10; v/=10; } while (v>0); - if (neg) *ch='-'; else ch++; - SET_STRING_ELT(ans, i, mkChar(ch)); - } - } - UNPROTECT(1); - return ans; -} - From f60101e0e2a705404ecbdcb3534a0b0c6278e3e5 Mon Sep 17 00:00:00 2001 From: mattdowle Date: Thu, 7 Oct 2021 16:42:17 -0600 Subject: [PATCH 15/19] add a news item for coerce class to character --- NEWS.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/NEWS.md b/NEWS.md index 354fe2008b..cd3b058d2b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -378,6 +378,25 @@ 45. `shift(xInt64, fill=0)` and `shift(xInt64, fill=as.integer64(0))` (but not `shift(xInt64, fill=0L)`) would error with `INTEGER() can only be applied to a 'integer', not a 'double'` where `xInt64` conveys `bit64::integer64`, `0` is type `double` and `0L` is type integer, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. +46. `DT[i, strCol:=classVal]` did not coerce using the `as.character` method for the class resulting in either an unexpected string value or an error like `To assign integer64 to a target of type character, please use as.character() for clarity`. Discovered during work on the previous issue, [#5189](https://github.com/Rdatatable/data.table/pull/5189). + + ```R + DT + # A + # + # 1: a + # 2: b + # 3: c + DT[2, A:=as.IDate("2021-02-03")] + DT[3, A:=bit64::as.integer64("4611686018427387906")] + DT + # A + # + # 1: a + # 2: 2021-02-03 # was 18661 + # 3: 4611686018427387906 # was error 'please use as.character' + ``` + ## NOTES 1. New feature 29 in v1.12.4 (Oct 2019) introduced zero-copy coercion. Our thinking is that requiring you to get the type right in the case of `0` (type double) vs `0L` (type integer) is too inconvenient for you the user. So such coercions happen in `data.table` automatically without warning. Thanks to zero-copy coercion there is no speed penalty, even when calling `set()` many times in a loop, so there's no speed penalty to warn you about either. However, we believe that assigning a character value such as `"2"` into an integer column is more likely to be a user mistake that you would like to be warned about. The type difference (character vs integer) may be the only clue that you have selected the wrong column, or typed the wrong variable to be assigned to that column. For this reason we view character to numeric-like coercion differently and will warn about it. If it is correct, then the warning is intended to nudge you to wrap the RHS with `as.()` so that it is clear to readers of your code that a coercion from character to that type is intended. For example : From 84b4c1bae37a067b3fcd0f147937f4a5f968284d Mon Sep 17 00:00:00 2001 From: mattdowle Date: Thu, 7 Oct 2021 21:41:00 -0600 Subject: [PATCH 16/19] add test for coerce class to character --- inst/tests/tests.Rraw | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 99eb9fcc54..a5973265a6 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -18244,3 +18244,8 @@ for (f1 in funs) { } } +# subassign coerce a class to character, part of PR#5189 +DT = data.table(A=letters[1:3]) +test(2219.1, DT[2, A:=as.IDate("2021-02-03")], data.table(A=c("a","2021-02-03","c"))) +if (test_bit64) test(2219.2, DT[3, A:=as.integer64("4611686018427387906")], data.table(A=c("a","2021-02-03","4611686018427387906"))) + From f6c1de26029540a41dfe57aab2457299b44002fb Mon Sep 17 00:00:00 2001 From: mattdowle Date: Thu, 7 Oct 2021 21:53:09 -0600 Subject: [PATCH 17/19] tweak comments in code and tests --- inst/tests/tests.Rraw | 4 ++-- src/assign.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index a5973265a6..c349b99daf 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -14611,7 +14611,7 @@ if (test_bit64) { warning="-1.*integer64.*position 1 taken as 0 when assigning.*raw.*column 3 named 'c'") test(2005.66, DT[2:3, f:=as.integer64(c(NA,"2147483648"))]$f, as.complex(c(-42,NA,2147483648))) DT[,h:=LETTERS[1:3]] - test(2005.67, DT[2:3, h:=as.integer64(1:2)]$h, c("A","1","2")) # tests coerceI64toStr PR#5189 + test(2005.67, DT[2:3, h:=as.integer64(1:2)]$h, c("A","1","2")) # PR#5189 } # rbindlist raw type, #2819 @@ -18223,9 +18223,9 @@ DT2 = data.table(grp = c('a', 'b'), agg = list(c('1' = 4, '2' = 5), c('3' = 6))) test(2217, DT1[, by = grp, .(agg = list(setNames(as.numeric(value), id)))], DT2) # shift integer64 when fill isn't integer32, #4865 -# when test_bit64==FALSE these all passed before testnum = 2218 funs = c(as.integer, as.double, as.complex, as.character, if (test_bit64) as.integer64) +# when test_bit64==FALSE these all passed before; now passes with test_bit64==TRUE too for (f1 in funs) { DT = data.table(x=f1(1:4)) for (f2 in funs) { diff --git a/src/assign.c b/src/assign.c index f525f5ea75..7fb09fa71e 100644 --- a/src/assign.c +++ b/src/assign.c @@ -1072,7 +1072,7 @@ const char *memrecycle(const SEXP target, const SEXP where, const int start, con break; } if (OBJECT(source) && getAttrib(source, R_ClassSymbol)!=R_NilValue) { - // otherwise coerceVector doesn't call the as.character method for Date, integer64, nanotime, etc; PR#5189 + // otherwise coerceVector doesn't call the as.character method for Date, IDate, integer64, nanotime, etc; PR#5189 // this if() is to save the overhead of the R call eval() when we know there can be no method source = PROTECT(eval(PROTECT(lang2(sym_as_character, source)), R_GlobalEnv)); protecti+=2; } else { From d061dbcde36090ffc0c39aed224bc5795c3d76c8 Mon Sep 17 00:00:00 2001 From: mattdowle Date: Thu, 7 Oct 2021 21:59:46 -0600 Subject: [PATCH 18/19] tweak news item --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index cd3b058d2b..d0a8e8f5f7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -378,7 +378,7 @@ 45. `shift(xInt64, fill=0)` and `shift(xInt64, fill=as.integer64(0))` (but not `shift(xInt64, fill=0L)`) would error with `INTEGER() can only be applied to a 'integer', not a 'double'` where `xInt64` conveys `bit64::integer64`, `0` is type `double` and `0L` is type integer, [#4865](https://github.com/Rdatatable/data.table/issues/4865). Thanks to @peterlittlejohn for reporting and Benjamin Schwendinger for the PR. -46. `DT[i, strCol:=classVal]` did not coerce using the `as.character` method for the class resulting in either an unexpected string value or an error like `To assign integer64 to a target of type character, please use as.character() for clarity`. Discovered during work on the previous issue, [#5189](https://github.com/Rdatatable/data.table/pull/5189). +46. `DT[i, strCol:=classVal]` did not coerce using the `as.character` method for the class, resulting in either an unexpected string value or an error such as `To assign integer64 to a target of type character, please use as.character() for clarity`. Discovered during work on the previous issue, [#5189](https://github.com/Rdatatable/data.table/pull/5189). ```R DT From fdc1dc6da16d60db667e22856438a976c3268bc2 Mon Sep 17 00:00:00 2001 From: mattdowle Date: Thu, 7 Oct 2021 22:59:56 -0600 Subject: [PATCH 19/19] filed nanotime issue, tweaked test, added link to test --- inst/tests/nafill.Rraw | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inst/tests/nafill.Rraw b/inst/tests/nafill.Rraw index b2fa93a7f1..d2ee592ccc 100644 --- a/inst/tests/nafill.Rraw +++ b/inst/tests/nafill.Rraw @@ -294,7 +294,8 @@ if (test_nanotime) { x = nanotime(1L) test(10.91, coerceAs(x, 1), 1, output="double[nanotime] into double[numeric]") test(10.92, coerceAs(x, 1L), 1L, output="double[nanotime] into integer[integer]") - test(10.93, coerceAs(x, "1"), "1", output="double[nanotime] into character[character]") # nanotime needs an as.character method for "1" to be "1970-01-01T00:00:00.000000001+00:00" + test(10.93, substring(coerceAs(x, "1"),1,11) %in% c("1","1970-01-01T"), output="double[nanotime] into character[character]") + # ^ https://github.com/eddelbuettel/nanotime/issues/92; %in% so as not to break if nanotime adds as.character method test(10.94, coerceAs(1, x), x, output="double[numeric] into double[nanotime]") test(10.95, coerceAs(1L, x), x, output="integer[integer] into double[nanotime]") test(10.96, coerceAs("1", x), x, output="character[character] into double[nanotime]", warning="Coercing.*character")