---
title: "Write Sheets"
description: >
  Targeting different parts of a Sheet, writing formulas, addressing cell format
---

```{r setup, include = FALSE}
can_decrypt <- gargle:::secret_can_decrypt("googlesheets4")
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  error = TRUE,
  purl = can_decrypt,
  eval = can_decrypt
)
```

```{r eval = !can_decrypt, echo = FALSE, comment = NA}
message("No token available. Code chunks will not be evaluated.")
```

```{r}
library(googlesheets4)
```

Basic Sheet writing is shown in the [Get started](https://googlesheets4.tidyverse.org/articles/googlesheets4.html) article. Here we explore the different uses for the writing functions:

  * `gs4_create()` emphasizes the creation of a new (spread)Sheet.
    - But it can create (work)sheets and write data.
  * `sheet_write()` emphasizes writing a data frame into a (work)sheet.
    - But it can create a new (spread)Sheet or a new (work)sheet.
  * `sheet_append()` is about adding rows to an existing data table.
  * `gs4_create()`, `sheet_write()`, and `sheet_append()` implement
    holistic operations for representing a single R data frame as a table in
    a (work)sheet within a Google Sheet.
    - `gs4_create()` and `sheet_write()` both impose this mentality via
      specific formatting, such as special treatment of the column header row.
    - All 3 functions aim to shrink-wrap the data.
  * `range_write()` is the one function that helps you write arbitrary data
    into an arbitrary range, although it is still oriented around a data frame.
  * `range_flood()` writes common content into all of the cells in a range.
    - An important special case is *clearing* a range of cells of their
      existing values and, optionally, formatting.
  * `range_delete()` deletes a range of cells and shifts other cells into the
    deleted area.

## Auth

As a regular, interactive user, you can just let googlesheets4 prompt you for anything it needs re: auth.

Since this article is compiled noninteractively on a server, we have arranged for googlesheets4 to use a service account token (not shown).

```{r include = FALSE}
# happens in .onLoad() when IN_PKGDOWN, but need this for local dev/preview
googlesheets4:::gs4_auth_docs(drive = TRUE)

# attempt to reduce quota exhaustion problems
if (identical(Sys.getenv("IN_PKGDOWN"), "true")) Sys.sleep(30)
```

## `gs4_create()`

Create a brand new Sheet with `gs4_create()`. You can specify the new Sheet's `name` (or accept a randomly generated name).

```{r}
ss1 <- gs4_create("sheets-create-demo-1")
ss1
```

Every Sheet *must* have at least one (work)sheet, so Google Sheets automatically creates an empty "Sheet1".

You can control the names and content of the initial (work)sheets with the `sheets` argument.

### Send sheet names

Use a character vector to specify the names of one or more empty sheets.

```{r}
ss2 <- gs4_create(
  "sheets-create-demo-2",
  sheets = c("alpha", "beta")
)
ss2
```

These sheets have no values and get their dimensions from Sheets default behaviour.

### Send a data frame

If you provide a data frame, it is used to populate the cells of a sheet and to set sheet dimensions (number of rows and columns). The header row also gets special treatment. The sheet inherits the name of the data frame, where possible.

```{r}
my_data <- data.frame(x = 1:3, y = letters[1:3])

ss3 <- gs4_create(
  "sheets-create-demo-3",
  sheets = my_data
)
ss3
```

### Send multiple data frames

If you provide a list of data frames, each is used to populate the cells of one sheet and to set sheet dimensions (number of rows and columns). The header row also gets special treatment. The sheets inherit the names from the list, if it has names.

```{r}
my_data_frames <- list(iris = head(iris), chickwts = head(chickwts))

ss4 <- gs4_create(
  "sheets-create-demo-4",
  sheets = my_data_frames
)
ss4
```

### Write metadata

Most users won't need to do this, but `gs4_create()` can set additional Sheet-level metadata, such as locale or time zone. To really make use of this feature, you need to read up on the [`spreadsheets.create` endpoint](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create).

Notice how the default empty Sheet here is named "Feuille 1", since we request a French locale.

```{r}
ss5 <- gs4_create(
  "sheets-create-demo-5",
  locale = "fr_FR",
  timeZone = "Europe/Paris"
)
ss5
```

I would only do this if you have specific evidence that the default behaviour with respect to locale and time zone is problematic for your use case.

### Clean up

Trash all the Sheets created above. This actually requires googledrive, since it is not possible to trash or delete Sheets through the Sheets API. In our hidden auth process, described earlier, we put a shared token into force for both Sheets and Drive. You can read how to do that in your own work in the article [Using googlesheets4 with googledrive](https://googlesheets4.tidyverse.org/articles/articles/drive-and-sheets.html).

```{r}
gs4_find("sheets-create-demo") %>%
  googledrive::drive_trash()
```

## `write_sheet()`, a.k.a. `sheet_write()`

`write_sheet()` is aliased to `sheet_write()` and is meant to evoke `readr::write_csv()` or `writexl::write_xlsx()`. Whereas `gs4_create()` emphasizes the *target Sheet*, `sheet_write()` emphasizes the *data you want to write*.

The only required argument for `sheet_write()` is the data.

```{r}
df <- data.frame(x = 1:3, y = letters[1:3])

random_ss <- sheet_write(df)
random_ss
```

This creates a new (work)sheet inside a new (spread)Sheet and returns its ID. You'll notice the new Sheet has a randomly generated name. If that is a problem, use `gs4_create()` instead, which affords more control over various aspects of the new Sheet.

Let's start over: we delete that Sheet and call `gs4_create()`, so we can specify the new Sheet's name. Then we'll modify it with `sheet_write()`. We send one sheet name, "chickwts", to prevent the creation of "Sheet1", but we send no data.

```{r}
googledrive::drive_trash(random_ss)

ss1 <- gs4_create(
  "write-sheets-demo-1",
  sheets = "chickwts"
)
ss1
```

`sheet_write()` allows us to write the actual `chickwts` data into the sheet by that name. 
```{r}
sheet_write(chickwts, ss = ss1, sheet = "chickwts")
ss1
```

`sheet_write()` can also write data into a new sheet, if `sheet` implicitly or explicitly targets a non-existent sheet.

```{r}
# explicitly make a new sheet named "iris"
sheet_write(iris, ss = ss1, sheet = "iris")

# implicitly make a new sheet named "mtcars"
sheet_write(mtcars, ss = ss1)
```

If no sheet name is given and it can't be determined from `data`, a name of the form "SheetN" is automatically generated by Sheets.

```{r}
sheet_write(data.frame(x = 1:2, y = 3:4), ss = ss1)
ss1
```

### Clean up

```{r}
gs4_find("write-sheets-demo") %>%
  googledrive::drive_trash()
```

## `sheet_append()`

```{r include = FALSE}
# attempt to reduce quota exhaustion problems
if (identical(Sys.getenv("IN_PKGDOWN"), "true")) Sys.sleep(100)
```

`sheet_append()` can add one or more rows of data to an existing (work)sheet.

Let's recreate the table of "other" deaths from an example Sheet, but without the annoying text above and below the data. First we bring that data into a local data frame and chop it into pieces.

```{r}
(deaths <- gs4_example("deaths") %>%
   range_read(range = "other_data", col_types = "????DD"))

deaths_zero  <- deaths[integer(), ] # "scaffolding" data frame with 0 rows
deaths_one   <- deaths[1:5, ] 
deaths_two   <- deaths[6, ]
deaths_three <- deaths[7:10, ]
```

We use `gs4_create()` to create a new (spread)Sheet and initialize a new (work)sheet with the "deaths" column headers, but no data. A second empty row is created (it must be, in order for us to freeze the top row), but it will soon be filled when we append.

```{r}
ss <- gs4_create("sheets-append-demo", sheets = list(deaths = deaths_zero))
ss
```

If you're following along, I recommend you open this Sheet in a web browser with `gs4_browse()` and revisit as we go along, to see how the initial empty row gets consumed and how additional rows are added to the targetted sheet automatically.

```{r}
gs4_browse(ss)
```

Send the data, one or more rows at a time. Keep inspecting in the browser if you're doing this yourself.

```{r}
ss

# send several rows
ss %>% sheet_append(deaths_one)
ss

# send a single row
ss %>% sheet_append(deaths_two)
ss

# send remaining rows
ss %>% sheet_append(deaths_three)
ss
```

Now the big reveal: have we successfully rebuilt that data through incremental updates?

```{r}
deaths_replica <- range_read(ss, col_types = "????DD")
identical(deaths, deaths_replica)
```

Gosh I hope that's still `TRUE` as it was the last time I checked this article!

### Clean up

```{r}
gs4_find("sheets-append-demo") %>%
  googledrive::drive_trash()
```

## `range_write()`

`range_write()` is the least opinionated writing function. It writes data into a range. It does no explicit formatting, although it can effectively apply formatting by *clearing* existing formats via `reformat = TRUE` (the default).

We focus here on the geometry of `range_write()`, i.e. which cells are edited.

In a hidden chunk, we've created a demo Sheet `ss_edit` and we've filled the cells with "-". We've also created `read_this()`, a wrapper around `range_read()` that names columns by letter, like spreadsheets do.

```{r include = FALSE}
library(tidyverse)

n <-  7

clear <- function(n) {
  suppressMessages(
    as_tibble(matrix("-", nrow = n, ncol = n), .name_repair = "unique")
  )
}

populate <- function(ss, sheet = NULL, range = NULL, col_names = FALSE) {
  suppressMessages(
    range_write(clear(n), ss = ss, sheet = sheet, range = range, col_names = col_names)
  )
}

read_this <- function(ss) {
  range_read(
    ss = ss,
    col_names = FALSE,
    .name_repair = ~ LETTERS[seq_along(.x)]
  )
}

ss_edit <- gs4_create("sheets-edit-demo")
#googlesheets4:::gs4_share(ss_edit) %>% gs4_browse()
sheet_resize(ss_edit, nrow = n, ncol = n, exact = TRUE)
populate(ss_edit)
```

Here's the initial state of `ss_edit`:

```{r}
read_this(ss_edit)
```

`df` is a small data frame we'll send as the `data` argument of `range_write()`:

```{r}
(df <- tibble(V1 = head(LETTERS,3), V2 = tail(LETTERS, 3)))
```

If we do not specify the range, `df` is written into the upper left corner and only affects cells spanned by `df`. To see where we've written, focus on the cells are NOT "x".

```{r}
range_write(ss_edit, data = df) %>% read_this()
```

(Here, and between all subsequent chunks, we reset `ss_edit` to its initial state.)

```{r, include = FALSE}
populate(ss_edit)
```

If we target a **single cell** with `range`, it specifies the upper left corner of the target area. The cells written are determined by the extent of the data.

```{r}
range_write(ss_edit, data = df, range = "C2") %>% read_this()
```

```{r, include = FALSE}
populate(ss_edit)
```

If `range` specifies multiple cells (it can even be unbounded on some sides), it is taken literally and all covered cells are written. If `range` is larger than the data, this results in some cells being *cleared* of their values. In this example, the `range` is "too big" for the data, so the remaining cells are cleared of their existing "-" value.

```{r}
range_write(ss_edit, data = df, range = "D4:G7") %>% read_this()
```

```{r, include = FALSE}
populate(ss_edit)
```

Here's another case where the `range` is bigger than it needs to be and it's unbounded on the bottom and top:

```{r}
range_write(ss_edit, data = df, range = "B:E") %>% read_this()
```

```{r, include = FALSE}
populate(ss_edit)
```

Here's another `range` that's unbounded on the left and "too big":

```{r}
range_write(ss_edit, data = df, range = "B2:6") %>% read_this()
```

```{r, include = FALSE}
populate(ss_edit)
```

The target sheet will be expanded, if necessary, if and only if `range` is a single cell (i.e. it gives the upper left corner).

```{r}
range_write(ss_edit, data = df, range = "G6") %>% read_this()
```

Although the `data` argument of `range_write()` must be a data frame, note that this does not actually limit what you can write:

 * Use `col_names = FALSE` to suppress sending the column names.
 * By definition, each variable of a data frame can be of different type.
 * By using a list-column, each row of a data frame column can be of different
   type.
   
The examples for `range_write()` show writing data of disparate type to a 1-row or a 1-column region.

### Clean up

```{r}
gs4_find("sheets-edit-demo") %>%
  googledrive::drive_trash()
```

## Write formulas

All the writing functions can write *formulas* into cells, if you indicate this in the R object you are writing, i.e. in the data frame. The `gs4_formula()` function marks a character vector as containing Sheets formulas, as opposed to regular character strings.

Here's a demo that also shows off using the Google Translate API inside a Sheet.

```{r}
lang_dat <- tibble::tribble(
       ~ english, ~ to,
           "dog", "es",
   "hello world", "ko",
  "baby turtles", "th" 
)
lang_dat$translated <- gs4_formula(
  '=GoogleTranslate(INDIRECT("R[0]C[-2]", FALSE), "en", INDIRECT("R[0]C[-1]", FALSE))'
)

(ss <- gs4_create("sheets-formula-demo", sheets = lang_dat))
```

Now we can read the data back out, complete with translations!

```{r}
range_read(ss)
```

### Clean up

```{r}
gs4_find("sheets-formula-demo") %>%
  googledrive::drive_trash()
```

## `range_flood()`

`range_flood()` "floods" all cells in a range with common content. By default, it floods them with no content, i.e. it clears cells of their existing value and format. Note that `range_clear()` is a convenience wrapper for this special case.

First, we create a data frame and initialize a new Sheet with that data.

```{r}
df <- gs4_fodder(10)
(ss <- gs4_create("range-flood-demo", sheets = list(df)))
```

To begin, each cell holds its A1-coordinates as its value. If you viewed this in the browser, you'd see the header row has some special formatting, including a gray background.

```{r}
range_read(ss)
```

By default, `range_flood()` sends an empty cell (and clears existing formatting).

```{r}
range_flood(ss, range = "A1:B3")
range_read(ss)
```

We can't easily demonstrate this here, but if you are working with a sheet in the browser, you can experiment with `reformat = TRUE/FALSE` to clear the existing format (or not).

We can also send a new value into a range of cells.

```{r}
range_flood(ss, range = "4:5", cell = ";-)")
range_read(ss)
```

### Clean up

```{r}
gs4_find("range-flood-demo") %>%
  googledrive::drive_trash()
```

## `range_delete()`

`range_delete()` deletes a range of cells and shifts other cells into the deleted area. If you're trying to delete an entire (work)sheet or change its actual dimensions or extent, you should, instead, use `sheet_delete()` or
`sheet_resize()`. If you just want to clear values or formats, use `range_clear()`.

First, we create a data frame and initialize a new Sheet with that data.

```{r}
df <- gs4_fodder(4)
(ss <- gs4_create("sheets-delete-demo", sheets = list(df)))
```

To begin, each cell holds its A1-coordinates as its value. If you viewed this in the browser, you'd see the header row has some special formatting, including a gray background.

```{r}
range_read(ss)
```

Let's delete some rows.

```{r}
range_delete(ss, range = "2:3")
range_read(ss)
```

Let's delete a column.

```{r}
range_delete(ss, range = "C")
range_read(ss)
```

Let's delete a bounded cell range. Since it's unclear how to fill this space, we must specify `shift`. We'll fill the space by shifting remaining cells to the left. Remember that `range` continues to refer to actual rows and columns and, due to our deletions, no longer match with the values which reflect the original locations.

```{r}
range_delete(ss, range = "B2:B3", shift = "left")
range_read(ss)
```

### Clean up

```{r}
gs4_find("sheets-delete-demo") %>%
  googledrive::drive_trash()
```
