Google Sheets from Elixir, as values.
A spreadsheet is a list of Sheetshow.Cell structs in no particular order.
The functions here build such lists from rows, columns, tables and records
and lay them out next to and below each other, and plan/2 turns the result
into the ops a backend carries out.
iex> import Sheetshow
iex> costs = [%{item: "Rent", cost: 1000}, %{item: "Food", cost: 400}]
iex> cells =
...> stack([
...> row(["Item", "Cost"], style: %{bold: true}),
...> records(costs, [:item, :cost])
...> ])
...> |> put_sheet("Costs")
iex> to_rows(cells)
[["Item", "Cost"], ["Rent", 1000], ["Food", 400]]
iex> Sheetshow.Range.bounding(cells) |> Sheetshow.Range.to_a1()
"Costs!A1:B3"Layout works on row and column numbers only. A group keeps its own internal offsets when concatenated, so a group whose first cell sits at row 2 lands two rows below the group above it.
Sheetshow defines no GenServer, supervisor or application callback module, and no macros. Credentials, tokens and caches are values you pass in, and so are retries. A plan is one request however many ops it holds, which is what keeps you inside Google's quota; see What Sheetshow Can Promise for that, and for what a write can and cannot guarantee.
Summary
Functions
Trades the workbook's credentials for an access token. When and whether to do
this again is yours to decide: Sheetshow.Client.ready?/2 answers it.
Places groups of cells side by side, each starting on the column after the previous group's last one. Empty groups take no room.
Same as beside([left, right]), for pipelines.
One column of cells from a list of values, top to bottom. Same options as
row/2.
Makes a workbook ready to use and learns the sheets it has. For Google that is a token and one metadata request, and it authenticates only when there is no good token already, so calling it again is cheap in everything but the request. For a backend with nothing to reach it is free.
Asks the spreadsheet which sheets it has, and puts them on the workbook.
The highest column any cell sits on, or nil for no cells.
The highest row any cell sits on, or nil for no cells.
Adds n empty rows below the cells, so the next stack/1 leaves a
gap. The room is held by one empty cell in the first column of the last
padded row.
Adds n empty columns to the right of the cells; see pad_below/2.
Turns cells into a plan: the ops that write them, in the order they are to be carried out.
Same as plan/2, raising on failure.
Puts every cell on a sheet.
Merges a style into every cell's; see Sheetshow.Cell.put_style/2.
The cells in a range, in reading order, with empty cells left out.
Same as read_cells/2, raising on failure.
The values in a range, as rows: what the cells work out to, rather than what they hold.
Same as read_rows/2, raising on failure.
One row of cells per record (a map or struct), one column per key, in key order; a missing key is an empty cell.
One row of cells from a list of values, left to right.
Cells from rows of values. Rows may be ragged. Same options as
row/2.
Carries out a plan and gives back the workbook. At Google it is one
spreadsheets.batchUpdate, applied whole or not at all, so there is no half
written spreadsheet to clean up; Sheetshow.Backend.supports?/2 says whether
a given backend promises that much.
Moves every cell by rows down and cols right; see Sheetshow.Cell.shift/3.
Stacks groups of cells, each starting on the row after the previous group's last one. Empty groups take no room.
Same as stack([above, below]), for pipelines.
Values back from cells, as rows relative to their bounding range; gaps are
nil. When two cells share a coordinate the later one wins. Cells must share
a sheet. No cells is no rows, which is what a read of an empty range gives.
Types
@type cells() :: [Sheetshow.Cell.t()]
@type layout_opts() :: [ row: non_neg_integer(), col: non_neg_integer(), sheet: String.t(), style: Sheetshow.Style.t() ]
Functions
@spec authenticate(Sheetshow.Workbook.t()) :: {:ok, Sheetshow.Workbook.t()} | {:error, Sheetshow.Error.t()}
Trades the workbook's credentials for an access token. When and whether to do
this again is yours to decide: Sheetshow.Client.ready?/2 answers it.
A backend with no credentials to trade, which means the in-memory one and
every file backend, answers {:error, %Sheetshow.Error{reason: :unsupported}}.
Places groups of cells side by side, each starting on the column after the previous group's last one. Empty groups take no room.
iex> Sheetshow.beside([Sheetshow.col([1, 2]), Sheetshow.col([3, 4])])
...> |> Sheetshow.to_rows()
[[1, 3], [2, 4]]
Same as beside([left, right]), for pipelines.
@spec col([Sheetshow.Value.t()], layout_opts()) :: cells()
One column of cells from a list of values, top to bottom. Same options as
row/2.
iex> Sheetshow.col([1, 2, 3]) |> Sheetshow.max_row()
2
@spec connect(Sheetshow.Workbook.t()) :: {:ok, Sheetshow.Workbook.t()} | {:error, Sheetshow.Error.t()}
Makes a workbook ready to use and learns the sheets it has. For Google that is a token and one metadata request, and it authenticates only when there is no good token already, so calling it again is cheap in everything but the request. For a backend with nothing to reach it is free.
workbook = Sheetshow.Workbook.google(id, credentials: Sheetshow.ServiceAccount.from_file!(path))
{:ok, workbook} = Sheetshow.connect(workbook)
workbook.sheets
%{"Sheet1" => 0}
iex> {:ok, workbook} = Sheetshow.Workbook.memory(["Costs"]) |> Sheetshow.connect()
iex> Sheetshow.Workbook.titles(workbook)
["Costs"]
@spec fetch_sheets(Sheetshow.Workbook.t()) :: {:ok, Sheetshow.Workbook.t()} | {:error, Sheetshow.Error.t()}
Asks the spreadsheet which sheets it has, and puts them on the workbook.
@spec max_col(cells()) :: non_neg_integer() | nil
The highest column any cell sits on, or nil for no cells.
iex> Sheetshow.max_col(Sheetshow.row([1, 2, 3]))
2
@spec max_row(cells()) :: non_neg_integer() | nil
The highest row any cell sits on, or nil for no cells.
iex> Sheetshow.max_row([])
nil
@spec pad_below(cells(), pos_integer()) :: cells()
Adds n empty rows below the cells, so the next stack/1 leaves a
gap. The room is held by one empty cell in the first column of the last
padded row.
iex> Sheetshow.row([1]) |> Sheetshow.pad_below(2) |> Sheetshow.max_row()
2
@spec pad_right(cells(), pos_integer()) :: cells()
Adds n empty columns to the right of the cells; see pad_below/2.
iex> Sheetshow.col([1]) |> Sheetshow.pad_right() |> Sheetshow.max_col()
1
@spec plan(cells(), plan_opts()) :: {:ok, Sheetshow.Op.plan()} | {:error, Sheetshow.Error.t()}
Turns cells into a plan: the ops that write them, in the order they are to be carried out.
Options:
:sheet, the sheet for cells that name none. Without it, a cell that names no sheet is an error, because an op has to say where it writes.:existing_sheets, the sheets the spreadsheet already has. Every other sheet the cells name gets aSheetshow.Op.AddSheetat the front of the plan. Leave it out and the plan assumes the sheets are all there.
Cells are checked before anything else, so a plan is only ever made of cells
that can be written. Scattered cells become one Sheetshow.Op.PutCells per
run, so a write never clears a neighbour; a date, time or datetime with no
:number_format of its own gets the format that makes it readable; and the
:col_width and :row_height styles are lifted out into
Sheetshow.Op.SetDimensions. Two cells at one coordinate are a late edit of
the same cell: the last one wins.
iex> cells = [Sheetshow.Cell.new("A1", 1), Sheetshow.Cell.new("C1", 3)]
iex> {:ok, plan} = Sheetshow.plan(cells, sheet: "Costs")
iex> Enum.map(plan, &Sheetshow.Range.to_a1(Sheetshow.Op.PutCells.range(&1)))
["Costs!A1", "Costs!C1"]
iex> {:error, %Sheetshow.Error{reason: :missing_sheet}} =
...> Sheetshow.plan(Sheetshow.row([1]))
@spec plan!(cells(), plan_opts()) :: Sheetshow.Op.plan()
Same as plan/2, raising on failure.
iex> Sheetshow.row([1, 2], sheet: "Costs") |> Sheetshow.plan!() |> length()
1
Puts every cell on a sheet.
iex> Sheetshow.row([1]) |> Sheetshow.put_sheet("Costs") |> hd() |> Map.fetch!(:coord)
%Sheetshow.Coord{row: 0, col: 0, sheet: "Costs"}
@spec put_style(cells(), Sheetshow.Style.t()) :: cells()
Merges a style into every cell's; see Sheetshow.Cell.put_style/2.
iex> Sheetshow.row([1], style: %{bold: true}) |> Sheetshow.put_style(%{italic: true}) |> hd() |> Map.fetch!(:style)
%{bold: true, italic: true}
@spec read_cells(Sheetshow.Range.t() | String.t(), Sheetshow.Workbook.t()) :: {:ok, cells()} | {:error, Sheetshow.Error.t()}
The cells in a range, in reading order, with empty cells left out.
A cell's value is what was entered, so a formula reads back as a formula and
the cells you read are cells you can write again. What Sheets worked it out
to is in meta, under :formatted and :effective. The range needs a
sheet, because a spreadsheet has more than one.
{:ok, cells} = Sheetshow.read_cells("Costs!A1:C10", workbook)
Sheetshow.to_rows(cells)
[["Item", "Cost"], ["Rent", 1000]]
@spec read_cells!(Sheetshow.Range.t() | String.t(), Sheetshow.Workbook.t()) :: cells()
Same as read_cells/2, raising on failure.
@spec read_rows( Sheetshow.Range.t() | String.t() | [Sheetshow.Range.t() | String.t()], Sheetshow.Workbook.t() ) :: {:ok, [[term()]] | [[[term()]]]} | {:error, Sheetshow.Error.t()}
The values in a range, as rows: what the cells work out to, rather than what they hold.
This is the other read, and the one a stored table wants. read_cells/2 asks for
cells and gets back everything about them: what was entered, what Sheets
made of it, and the format, which is the only thing that says a number is a
date. read_rows/2 asks the values endpoint for the computed value alone,
which is about a twelfth of the bytes, and leaves the question of what a
number means to a Sheetshow.Schema that already knows. A formula arrives as
its result here, and a formula's error as the text of it, so cells you read
this way are not cells you can write back.
Rows are padded to the width of the widest and an empty cell is nil, as in
to_rows/1. The range needs a sheet.
{:ok, rows} = Sheetshow.read_rows("Costs!A1:C10", workbook)
[["Item", "Cost"], ["Rent", 1000]]Given a list of ranges it asks for all of them at once, and answers with one table per range, in the order you gave them. Two reads that would otherwise be two round trips, a header and the rows below a cursor, say, become one.
{:ok, [header, rows]} = Sheetshow.read_rows(["log!A1:C1", "log!A900:C"], workbook)
@spec read_rows!( Sheetshow.Range.t() | String.t() | [Sheetshow.Range.t() | String.t()], Sheetshow.Workbook.t() ) :: [[term()]] | [[[term()]]]
Same as read_rows/2, raising on failure.
One row of cells per record (a map or struct), one column per key, in key order; a missing key is an empty cell.
Same options as row/2, plus :header: true puts the keys' names
in a first row, a list of strings puts those labels there.
iex> [%{item: "Rent", cost: 1000}, %{item: "Food"}]
...> |> Sheetshow.records([:item, :cost], header: true)
...> |> Sheetshow.to_rows()
[["item", "cost"], ["Rent", 1000], ["Food", nil]]
@spec row([Sheetshow.Value.t()], layout_opts()) :: cells()
One row of cells from a list of values, left to right.
Options: :row and :col place the first cell (default 0, 0); :sheet
and :style apply to every cell.
iex> Sheetshow.row(["a", "b"], row: 1, style: %{bold: true}) |> Sheetshow.to_rows()
[["a", "b"]]
@spec rows([[Sheetshow.Value.t()]], layout_opts()) :: cells()
Cells from rows of values. Rows may be ragged. Same options as
row/2.
iex> Sheetshow.rows([[1, 2], [3]]) |> Enum.map(&Sheetshow.Coord.to_a1(&1.coord))
["A1", "B1", "A2"]
@spec run(Sheetshow.Op.plan(), Sheetshow.Workbook.t()) :: {:ok, Sheetshow.Workbook.t()} | {:error, Sheetshow.Error.t()}
Carries out a plan and gives back the workbook. At Google it is one
spreadsheets.batchUpdate, applied whole or not at all, so there is no half
written spreadsheet to clean up; Sheetshow.Backend.supports?/2 says whether
a given backend promises that much.
The workbook comes back knowing about any sheets the plan created, so a second plan against it needs no fresh metadata. An empty plan asks the backend nothing.
{:ok, workbook} = Sheetshow.connect(workbook)
cells = Sheetshow.row(["Rent", 1000], sheet: "Costs")
{:ok, workbook} =
cells
|> Sheetshow.plan!(existing_sheets: Map.keys(workbook.sheets))
|> Sheetshow.run(workbook)
iex> workbook = Sheetshow.Workbook.memory()
iex> cells = Sheetshow.row(["Rent", 1000], sheet: "Costs")
iex> {:ok, workbook} = cells |> Sheetshow.plan!(existing_sheets: []) |> Sheetshow.run(workbook)
iex> Sheetshow.read_cells!("Costs!A1:B1", workbook) |> Sheetshow.to_rows()
[["Rent", 1000]]
Moves every cell by rows down and cols right; see Sheetshow.Cell.shift/3.
iex> Sheetshow.row([1]) |> Sheetshow.shift(2, 3) |> hd() |> Map.fetch!(:coord) |> Sheetshow.Coord.to_a1()
"D3"
Stacks groups of cells, each starting on the row after the previous group's last one. Empty groups take no room.
iex> Sheetshow.stack([Sheetshow.row([1, 2]), Sheetshow.row([3, 4])])
...> |> Sheetshow.to_rows()
[[1, 2], [3, 4]]
Same as stack([above, below]), for pipelines.
@spec to_rows(cells()) :: [[Sheetshow.Value.t()]]
Values back from cells, as rows relative to their bounding range; gaps are
nil. When two cells share a coordinate the later one wins. Cells must share
a sheet. No cells is no rows, which is what a read of an empty range gives.
iex> Sheetshow.to_rows([Sheetshow.Cell.new("B2", 1), Sheetshow.Cell.new("C3", 2)])
[[1, nil], [nil, 2]]
iex> Sheetshow.to_rows([])
[]