unlist() converts Date objects back to numeric

I just came across this behaviour that seems to me a quirk. When you have a list of vectors of class Date and you use unlist(), the result is a vector of class numeric.

I tried using purrr::flatten() and purrr::simplify(.type = "Date") to see if they would perform the same function as unlist() while preserving the class, but they don't. purrr::pluck(), however, does the job!

dates <- lubridate::as_date(
  lubridate::ymd("2019-01-01"):lubridate::ymd("2019-01-03")
)
class(dates)
#> [1] "Date"
typeof(dates)
#> [1] "double"

dates_list <- list(dates)

unlist(dates_list)    # class "Date" is converted to numeric
#> [1] 17897 17898 17899
class(unlist(dates_list))
#> [1] "numeric"
typeof(unlist(dates_list))
#> [1] "double"

purrr::flatten(dates_list)        # can't create a vector of class "Date" ?
#> [[1]]
#> [1] 17897
#> 
#> [[2]]
#> [1] 17898
#> 
#> [[3]]
#> [1] 17899
purrr::flatten_dbl(dates_list)   # can make a vector of class "numeric"
#> [1] 17897 17898 17899
class(purrr::flatten_dbl(dates_list))
#> [1] "numeric"

purrr::as_vector(dates_list)    # equiv to purrr::simplify(dates_list)
#> [1] 17897 17898 17899
class(purrr::as_vector(dates_list))
#> [1] "numeric"

purrr::pluck(dates_list, 1)      # can create a vector of class "Date"
#> [1] "2019-01-01" "2019-01-02" "2019-01-03"
class(purrr::pluck(dates_list, 1))
#> [1] "Date"

Created on 2020-10-02 by the reprex package (v0.3.0)

It's all fine; I just thought it was unexpected that unlist() doesn't preserve class in the way I would have expected. Any explanations to help my mental model are welcome!

It's interesting (to me) that there's no _date variant of purrr::map or purrr::flatten.
edit: I can see that the list of possible types of a vector does not include "date". This obviously also applies to the types available to purrr::as_vector

I'm now learning about the difference between types, modes and classes in R :smiley:

PS I also discovered while making an initial reprex that lubridate surprisingly doesn't import reexport the %>% pipe as most other tidyverse packages do

Even ggplot2 doesn't reexport pipe, and that's core tidyverse, where lubridate is not. I think only dplyr, purrr and tidyr does that, because those are what I get from ?`%>%`, apart from magrittr.

In my opinion, it makes sense as probably these contribute mostly in a pipe chain.


Regarding your original question, I don't know the answer. Your PS just caught my eye. But just noticed this in ?unlist, which may have something to do with it (not sure).

The output type is determined from the highest type of the components in the hierarchy NULL < raw < logical < integer < double < complex < character < list < expression, after coercion of pairlists to lists.

1 Like

Yes, that's to do with type - as I've been learning. It is very relevant here.
Date is a class not a type - there's no type Date. I'm just trying to get my head round why some functions seem to preserve class and others don't.
[Links: 1 2 3 etc]

My pluck solution above only works with lists of length 1! Oops. So that isn't going to help me unlist lists of length > 1.

Here's an updated summary then:

Attempting to reduce a list of Date vectors to a single vector with class Date

# create list of length(2)
dates_list <- list(
  lubridate::as_date(
    lubridate::ymd("2019-01-01"):lubridate::ymd("2019-01-03")
  ),
  lubridate::as_date(
    lubridate::ymd("2020-01-01"):lubridate::ymd("2020-01-03")
  )
)

unlist() outputs a numeric vector:

unlist(dates_list)
#> [1] 17897 17898 17899 18262 18263 18264

This fails with length(list) > 1 because pluck needs a single index to pull:

purrr::pluck(dates_list, 1:length(dates_list))
#> Error: Index 1 must have length 1, not 2

Using map to feed pluck one index at a time just gives me the original list back again:

purrr::map(1:length(dates_list), ~ purrr::pluck(dates_list, .))
#> [[1]]
#> [1] "2019-01-01" "2019-01-02" "2019-01-03"
#> 
#> [[2]]
#> [1] "2020-01-01" "2020-01-02" "2020-01-03"

… and there’s no "unlisting/vectorising" map_* variant that preserves Date class.

This may be the only way to do it?

Explicitly restoring to `Date class:

lubridate::as_date(
  purrr::simplify(dates_list) # obv unlist() would be equivalent here also
)
#> [1] "2019-01-01" "2019-01-02" "2019-01-03" "2020-01-01" "2020-01-02"
#> [6] "2020-01-03"

Created on 2020-10-02 by the reprex package (v0.3.0)

:cry: Just discovered that my pluck solution doesn't work after all, if I use c() to combine outputs into a single vector like this:

dates_list <- list(
  lubridate::as_date(
    lubridate::ymd("2019-01-01"):lubridate::ymd("2019-01-03")
  ),
  lubridate::as_date(
    lubridate::ymd("2020-01-01"):lubridate::ymd("2020-01-03")
  )
)

out <- NULL
for (i in 1:length(dates_list)) {
  out <- c(out,
           purrr::pluck(dates_list, i)
  )
}
out
#> [1] 17897 17898 17899 18262 18263 18264

However just using c() to combine two Date vectors normally does preserve class:

dates_list <- c(
  lubridate::as_date(
    lubridate::ymd("2019-01-01"):lubridate::ymd("2019-01-03")
  ),
  lubridate::as_date(
    lubridate::ymd("2020-01-01"):lubridate::ymd("2020-01-03")
  )
)
dates_list
#> [1] "2019-01-01" "2019-01-02" "2019-01-03" "2020-01-01" "2020-01-02"
#> [6] "2020-01-03"
class(dates_list)
#> [1] "Date"

Glad you figured it out. See this on SO, https://stackoverflow.com/questions/15659783/why-does-unlist-kill-dates-in-r

Not obvious, but simple:

> do.call("c",dates_list)
[1] "2019-01-01" "2019-01-02" "2019-01-03" "2020-01-01" "2020-01-02" "2020-01-03"

Ah that SO link is really helpful, thank you! To be honest I didn't do much googling, as I should have, I just came straight here to ask about my confusion.

I've heard of do before, but never really used it. Never even heard of do.call I don't think. That is another good tool to add to the kit.

Another good learning experience!

In other related news, I just discovered that 1:10 is not the same as c(1:10) - I'd assumed it was basically an equivalent formulation:

1:10[1:2]
#> Warning in 1:10[1:2]: numerical expression has 2 elements: only the first used
#>  [1]  1  2  3  4  5  6  7  8  9 10
c(1:10)[1:2]
#> [1] 1 2

Hmm - weirdly, this works ok though (as I'd have expected):

vec <- 1:10
vec[1:2]
#> [1] 1 2

This is completely different from your original question. This is because of order of operations.

When you do 1:10[1:2], it is evaluated similar to 1:(10[1:2]), and 10[1:2] is c(10, NA). Since the left : one (all, but for now that's in our consideration) support only one element as second argument, you are getting the warning. So, it's basically calculating 1:10 and hence that result.

You should have tried (1:10)[1:2], which is same as your edit and hence ut works fine.

Hope this helps.

1 Like

Yes that makes sense - I hadn't thought about the order of operations as the relevant issue. It seems obvious now! Thank you for explaining.
And you're right, it's off-topic, I was just experimenting with c() ...

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.