---
title: "Column types and coercion"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Column types and coercion}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
resource_files:
- figures/logo.svg
- figures/logo-dark.svg
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```
harbouR maps SeaTable's column types onto predictable R types, so the
tibble you get back is analysis-ready, not raw JSON. This vignette is
the reference.
## The mapping table
The table below is derived from `hb_column_types()` so it cannot drift
out of sync with the code:
```{r mapping}
library(harbouR)
knitr::kable(hb_column_types())
```
Three of its rows are not column types of their own. `percent`, `dollar`
and `euro` are formats of a number column: SeaTable reports such a
column as type `number`, with the format in its `data`. They are listed
so that a table carrying them as a type still reads as numbers.
## What the rows look like
`hb_example_rows()` is a small hand-written frame covering the common
types. The bundled `.dtable` fixture is the exhaustive one - it carries a
column of nearly every type - and the last section reads it.
```{r samples}
samples <- hb_example_rows("Samples")
samples
```
### Scalars
```{r scalars}
str(samples[, c("Name", "Concentration", "Status", "Collected")])
```
* `text` / `single-select` come back as `character`. A single-select
value is not checked against the column's options when you write it:
SeaTable adds an unknown name as a new option, so a typo becomes one.
* `number` becomes a `double` (caveat: SeaTable's numeric column is
64-bit; large integers may round - if precision matters store them
as text).
* `checkbox` becomes `logical`. A box that was never ticked reads as
`FALSE`, as SeaTable shows it. When you write, harbouR sends the JSON
`true` or `false` SeaTable needs: a number is ticked unless it is 0,
and a string is read as `as.logical()` reads it, so `"TRUE"`,
`"true"` and `"T"` tick the box and `"FALSE"`, `"false"` and `"F"`
clear it. Any other non-empty string, `NA` and `NaN` are sent as
empty, the string with a warning.
* `date` always becomes `POSIXct`, in UTC. In a date-only column - one
whose format has no time - the value is midnight UTC on the date
SeaTable shows, whatever the server's time zone, so `as.Date()` gives
that calendar date. A date-time column holds an instant: a server
sends it with its UTC offset and harbouR converts it to UTC, while a
`.dtable` stores it without one and harbouR reads it as UTC. A column
that reports no format is read as a date-time. Written to a date-only
column, a `Date` is sent as itself and a date-time as the date it
shows in its own time zone, except one at exactly midnight UTC, which
keeps its UTC date, so a cell read and written back keeps its day.
### List-columns
These types always come back as list-columns, even when every row has
one value or none. That is the type-stability rule: the same column has
the same R type for every row.
```{r list-types}
subset(hb_column_types(), is_list)$seatable
```
```{r tags}
samples$Tags
```
Flatten with `tidyr::unnest` when you need a long form:
```{r unnest, eval = requireNamespace("tidyr", quietly = TRUE)}
tidyr::unnest(samples[, c("Name", "Tags")], cols = "Tags")
```
### File and image columns
```{r files}
samples$Reports[[1]]
```
Each entry is a list of file objects with `name`, `size`, `type` and
`url` - ready to be downloaded via `hb_download_file()`.
### Read-only columns
harbouR leaves columns of these types out of every write, so you can
round-trip a tibble through harbouR without writing back computed
values by accident.
```{r read-only}
subset(hb_column_types(), read_only)$seatable
```
Most of them are computed or maintained by SeaTable. Links are kept
through SeaTable's link endpoints rather than by writing the cell, and a
button carries no data. A `digital-sign` cell is left out for another
reason: a signature should come from the signer, not from a script.
## Empty tables
A table with no rows still comes back as a tibble with every column in
its correct type:
```{r empty}
empty <- hb_example_rows("Samples")[0, ]
empty
str(empty, max.level = 1)
```
Every column keeps the type it would have had with rows present -
`Concentration` is still a double, `Collected` still a `POSIXct`, and
`Tags` still a list-column - so downstream code that expects a schema
does not have to special-case the empty result.
## Every type at once
`hb_example_rows()` is deliberately small. The bundled `.dtable` fixture
carries a column of nearly every type SeaTable defines, so it is the
better place to see the whole mapping at work:
```{r fixture}
base <- hb_read_dtable(
system.file("extdata", "example.dtable", package = "harbouR")
)
schema <- hb_list_columns(base, "Samples")
knitr::kable(schema[, c("name", "type", "editable")])
```
```{r fixture-types}
data <- hb_read_table(base, "Samples")
vapply(data, function(col) class(col)[[1L]], character(1))
```
## Next steps
* `vignette("harbouR")` - the five-minute end-to-end story.
* `vignette("explorer-app")` - exploring a base interactively.