Почему as.character() возвращает целое число в списке дат?

Я с удивлением наблюдал следующее поведение в R:

as.character(c(Sys.Date()))
#> [1] "2018-02-05"

as.character(list(Sys.Date()))
#> [1] "17567"

Почему это происходит? То есть, "17567" является результатом as.integer(Sys.Date), но я не следую логике, почему as.character(list(Sys.Date())) должен завершать вызов as.integer().

(Обычно строки, обрабатываемые как целые числа, могут быть обвинены в отсутствии настроек options(stringsAsFactors=FALSE), но, похоже, это не так.)

EDIT. Как отмечает Джош, это связано с основным поведением as.vector, но я не считаю это более интуитивным:

as.vector(Sys.Date())
#> 17567
as.vector(Sys.Date(), "character")
#> "17567"

Почему? (Да, я считаю, что даты хранятся как целые числа в внутренних структурах нижнего уровня, но это принуждение к буквальному целому числу в этом случае без предупреждения кажется мне удивительным).

И это проявляется более утонченными способами:

tbl <- tibble:::as_data_frame(list(col1 = list(Sys.Date(), "stuff")))
df <- as.data.frame(tbl)
df
#>    col1
#> 1 17567
#> 2 stuff

df[1, 1]
#> [[1]]
#> [1] "2018-02-05"

Обратите внимание, что метод печати для data.frame показывает дату как целое число, когда на самом деле это столбец списка, а дата все еще является датой.

Не ясно, что происходит с методом печати в этом случае и почему он показывает такое вводящее в заблуждение представление данных.

ИЗМЕНИТЬ

Другие примеры, когда класс Date неожиданно падает, подвергая базовый тип числовой базы:

vapply(list(Sys.Date()), I, Sys.Date())
vapply(list(Sys.Date()), lubridate::as_date, Sys.Date())

и мой любимый до сих пор:

unlist(list(Sys.Date()))

Похоже, что векторные операции с Date (и объектами POSIX) являются хрупкими; следует сосредоточиться на mode/typeof, а не class, чтобы предвидеть, как будет вести себя вектор.

Ответ 1

Проблема в конечном счете связана с поведением функции as.vector().

Когда вы применяете as.character() к списку, он видит объект класса "list" (не один из класса "Date"). Поскольку для списков нет метода as.character(), отправляется метод по умолчанию as.character.default. Он выполняет следующие действия:

as.character.default
# function (x, ...) 
# .Internal(as.vector(x, "character"))
# <bytecode: 0x0000000006793e88>
# <environment: namespace:base>

Как вы можете видеть, он сначала подготавливает объект данных, принуждая его к вектору. Выполнение as.vector() непосредственно в списке объектов Date показывает, в свою очередь, что это то, что производит принуждение к целому, а затем к символу.

as.vector(list(Sys.Date()), "character")
# [1] "17567"

Как указывает Карл, объяснение выше, даже если оно точно, на самом деле не удовлетворяет. Более полный ответ требует просмотра того, что происходит под капотом, в коде C, выполняемом вызовом .Internal(as.vector(x, "character")). Весь соответствующий код C находится в исходном файле coerce.c.

Сначала do_asvector(), который вызывает ascommon() который вызывает coerceVector(), который вызывает coerceVectorList() и затем, наконец, coerceToString(). coerceToString() проверяет "typeof" обрабатываемый элемент, и в нашем случае, если это "REAL", переключается на этот блок кода:

case REALSXP:
PrintDefaults();
savedigits = R_print.digits; R_print.digits = DBL_DIG;/* MAX precision */
for (i = 0; i < n; i++) {
//  if ((i+1) % NINTERRUPT == 0) R_CheckUserInterrupt();
    SET_STRING_ELT(ans, i, StringFromReal(REAL(v)[i], &warn));
}
R_print.digits = savedigits;
break;

И зачем он использует блок для объектов с типом REALSXP? Потому что режим хранения объектов R Date (как видно из выполнения mode(Sys.Date()) или typeof(Sys.Date())).


Приёмник - это следующее: в цепочке событий, описанных выше, элементы списка не так или иначе пойманы и рассматриваются как объекты "Date", в то время как в области вызовов функций R и отправки метода. Вместо этого они передаются как "list" (aka VECSXP) в ряд функций C. И в этот момент это слишком поздно, поскольку функции C, которые обрабатывают этот список, ничего не знают о классе "Date" его элементов. В частности, функция, которая в конечном счете выполняет преобразование в символ, coerceToCharacter() видит только режим хранения элементов, который является REAL/numeric/double, и обрабатывает их, как если бы это было all, что они были,

Ответ 2

Вы можете добиться того, чего хотите с помощью функции format, как в

format(Sys.Date(), "%a %b %d")

Sys.Date дает объект Date, и есть много способов его преобразования в строку, поэтому требуется специальная функция. format предоставит вам символическое представление вашего Date.

BTW: Если вы просто нажмете Sys.Date() на консоли, это вызовет print.Date, который внутренне использует format, как вы можете видеть, набрав print.Date без скобок () на консоли.