Current section
Files
Jump to
Current section
Files
lib/elixlsx/sheet.ex
defmodule Elixlsx.Sheet do
alias __MODULE__
alias Elixlsx.Sheet
alias Elixlsx.Util
@moduledoc ~S"""
Describes a single sheet with a given name. The name can be up to 31
characters long.
The rows property is a list, each corresponding to a
row (from the top), of lists, each corresponding to
a column (from the left), of contents.
Content may be:
- a String.t (unicode),
- a number, or
- a list [String|number, property_list...]
The property list describes formatting options for that
cell. See `Font.from_props/1` for a list of options.
"""
defstruct name: "",
rows: [],
col_widths: %{},
row_heights: %{},
group_cols: [],
group_rows: [],
merge_cells: [],
pane_freeze: nil,
show_grid_lines: true,
data_validations: []
@type t :: %Sheet{
name: String.t(),
rows: list(list(any())),
col_widths: %{pos_integer => number},
row_heights: %{pos_integer => number},
group_cols: list(rowcol_group),
group_rows: list(rowcol_group),
merge_cells: [{String.t(), String.t()}],
pane_freeze: {number, number} | nil,
show_grid_lines: boolean(),
data_validations: list({String.t(), String.t(), list(String.t())})
}
@type rowcol_group :: Range.t() | {Range.t(), opts :: keyword}
@doc ~S"""
Create a sheet with a sheet name.
The name can be up to 31 characters long.
"""
@spec with_name(String.t()) :: Sheet.t()
def with_name(name) do
%Sheet{name: name}
end
defp split_cell_content_props(cell) do
cond do
is_list(cell) ->
{hd(cell), tl(cell)}
true ->
{cell, []}
end
end
@doc ~S"""
Returns a "CSV" representation of the Sheet.
This is mainly used for doctests and does not generate valid CSV (yet).
"""
def to_csv_string(sheet) do
Enum.map_join(sheet.rows, "\n", fn row ->
Enum.map_join(row, ",", fn cell ->
{content, _} = split_cell_content_props(cell)
case content do
nil -> ""
_ -> to_string(content)
end
end)
end)
end
@spec set_cell(Sheet.t(), String.t(), any(), Keyword.t()) :: Sheet.t()
@doc ~S"""
Set a cell indexed by excel coordinates.
## Examples
iex> %Elixlsx.Sheet{} |>
...> Elixlsx.Sheet.set_cell("C1", "Hello World",
...> bold: true, underline: true) |>
...> Elixlsx.Sheet.to_csv_string
",,Hello World"
"""
def set_cell(sheet, index, content, opts \\ []) when is_binary(index) do
{row, col} = Util.from_excel_coords0(index)
set_at(sheet, row, col, content, opts)
end
@spec set_at(Sheet.t(), non_neg_integer, non_neg_integer, any(), Keyword.t()) :: Sheet.t()
@doc ~S"""
Set a cell at a given row/column index.
Indizes start at 0.
## Examples
iex> %Elixlsx.Sheet{} |>
...> Elixlsx.Sheet.set_at(0, 2, "Hello World",
...> bold: true, underline: true) |>
...> Elixlsx.Sheet.to_csv_string
",,Hello World"
"""
def set_at(sheet, rowidx, colidx, content, opts \\ [])
when is_number(rowidx) and is_number(colidx) do
cond do
length(sheet.rows) <= rowidx ->
# append new rows, call self again with new sheet
n_new_rows = rowidx - length(sheet.rows)
new_rows = 0..n_new_rows |> Enum.map(fn _ -> [] end)
update_in(sheet.rows, &(&1 ++ new_rows))
|> set_at(rowidx, colidx, content, opts)
length(Enum.at(sheet.rows, rowidx)) <= colidx ->
n_new_cols = colidx - length(Enum.at(sheet.rows, rowidx))
new_cols = 0..n_new_cols |> Enum.map(fn _ -> nil end)
new_row = Enum.at(sheet.rows, rowidx) ++ new_cols
update_in(sheet.rows, &List.replace_at(&1, rowidx, new_row))
|> set_at(rowidx, colidx, content, opts)
true ->
update_in(sheet.rows, fn rows ->
List.update_at(rows, rowidx, fn cols ->
List.replace_at(cols, colidx, [content | opts])
end)
end)
end
end
@spec set_col_width(Sheet.t(), String.t(), number) :: Sheet.t()
@doc ~S"""
Set the column width for a given column.
Column is indexed by name ("A", ...)
"""
def set_col_width(sheet, column, width) do
update_in(
sheet.col_widths,
&Map.put(&1, Util.decode_col(column), width)
)
end
@spec set_row_height(Sheet.t(), number, number) :: Sheet.t()
@doc ~S"""
Set the row height for a given row.
Row is indexed starting from 1
"""
def set_row_height(sheet, row_idx, height) do
update_in(
sheet.row_heights,
&Map.put(&1, row_idx, height)
)
end
@spec group_cols(Sheet.t(), String.t(), String.t()) :: Sheet.t()
@doc ~S"""
Group given column range. (i.e. increase outline level by one)
Column is indexed by name ("A", ...)
## Options
- `collapsed`: if true, collapse this group.
"""
def group_cols(sheet, first_col, last_col, opts \\ []) do
col_range = Range.new(Util.decode_col(first_col), Util.decode_col(last_col))
new_group = if opts === [], do: col_range, else: {col_range, opts}
update_in(sheet.group_cols, fn groups -> groups ++ [new_group] end)
end
@spec group_rows(Sheet.t(), pos_integer, pos_integer) :: Sheet.t()
@doc ~S"""
Group given row range. (i.e. increase outline level by one)
Row is indexed starting from 1.
## Options
- `collapsed`: if true, collapse this group.
"""
def group_rows(sheet, first_row_idx, last_row_idx, opts \\ []) do
row_range = Range.new(first_row_idx, last_row_idx)
new_group = if opts === [], do: row_range, else: {row_range, opts}
update_in(sheet.group_rows, fn groups -> groups ++ [new_group] end)
end
@spec set_pane_freeze(Sheet.t(), number, number) :: Sheet.t()
@doc ~S"""
Set the pane freeze at the given row and column.
Row and column are indexed starting from 1.
Special value 0 means no freezing, e.g. {1, 0} will freeze first row and no columns.
"""
def set_pane_freeze(sheet, row_idx, col_idx) do
%{sheet | pane_freeze: {row_idx, col_idx}}
end
@spec remove_pane_freeze(Sheet.t()) :: Sheet.t()
@doc ~S"""
Removes any pane freezing that has been set.
"""
def remove_pane_freeze(sheet) do
%{sheet | pane_freeze: nil}
end
@spec add_data_validations(Sheet.t(), String.t(), String.t(), list(String.t())) :: Sheet.t()
def add_data_validations(sheet, start_cell, end_cell, values) do
%{sheet | data_validations: [{start_cell, end_cell, values} | sheet.data_validations]}
end
end