ExVEx (ExVEx v0.1.2)

Copy Markdown View Source

Pure-Elixir reader and editor for .xlsx / .xlsm workbooks.

Quick start

{:ok, book} = ExVEx.open("path/to/file.xlsx")
ExVEx.sheet_names(book)            #=> ["Sheet1", "Sheet2"]
{:ok, "hello"} = ExVEx.get_cell(book, "Sheet1", "A1")
:ok = ExVEx.save(book, "path/to/output.xlsx")

Design

ExVEx opens a workbook lazily: raw ZIP part bytes are kept in memory and untouched parts are written back verbatim on save/2. This preserves unknown content (custom XML, VBA macros, extension schemas) on round-trip without the caller needing to opt in.

Summary

Functions

Appends a new empty sheet with the given name to the workbook.

Returns every populated cell on a sheet as a map of A1 refs to resolved values. Empty cells are omitted.

Releases the ETS tables backing a workbook's cell grid. Calling this is optional — tables are cleaned up when the owning process exits — but recommended in long-running processes that open many workbooks.

Streams every populated cell on a sheet as {a1_ref, value} pairs in row-major order (row 1 before row 2; column A before column B within a row).

Merges a rectangular range of cells on a sheet.

Returns the list of merged ranges on a sheet as A1-style range refs.

Returns a minimal blank workbook with a single empty sheet named "Sheet1". Compose with add_sheet/2, rename_sheet/3, remove_sheet/2, and put_cell/4 to build a template from scratch.

Removes a sheet and its worksheet part from the workbook.

Renames a sheet. Returns {:error, :unknown_sheet} if old is not found.

Removes a merged range from a sheet.

Types

cell_ref()

@type cell_ref() :: String.t() | {pos_integer(), pos_integer()}

cell_value()

@type cell_value() ::
  binary()
  | number()
  | boolean()
  | nil
  | {:formula, String.t()}
  | {:formula, String.t(), binary() | number() | boolean()}

path()

@type path() :: Path.t()

range_ref()

@type range_ref() :: String.t()

sheet_name()

@type sheet_name() :: String.t()

Functions

add_sheet(book, name)

@spec add_sheet(ExVEx.Workbook.t(), sheet_name()) ::
  {:ok, ExVEx.Workbook.t()} | {:error, term()}

Appends a new empty sheet with the given name to the workbook.

Returns {:error, :duplicate_sheet_name} if the name is already in use.

cells(book, sheet)

@spec cells(ExVEx.Workbook.t(), sheet_name()) ::
  {:ok, %{required(cell_ref()) => term()}} | {:error, term()}

Returns every populated cell on a sheet as a map of A1 refs to resolved values. Empty cells are omitted.

close(book)

@spec close(ExVEx.Workbook.t()) :: :ok

Releases the ETS tables backing a workbook's cell grid. Calling this is optional — tables are cleaned up when the owning process exits — but recommended in long-running processes that open many workbooks.

After close/1, the workbook is no longer usable; calls to get_cell, put_cell, etc. will fail.

each_cell(book, sheet)

@spec each_cell(ExVEx.Workbook.t(), sheet_name()) ::
  {:ok, Enumerable.t({cell_ref(), term()})} | {:error, term()}

Streams every populated cell on a sheet as {a1_ref, value} pairs in row-major order (row 1 before row 2; column A before column B within a row).

get_cell(book, sheet, ref)

@spec get_cell(ExVEx.Workbook.t(), sheet_name(), cell_ref()) ::
  {:ok, cell_value() | Date.t() | NaiveDateTime.t()} | {:error, term()}

get_formula(book, sheet, ref)

@spec get_formula(ExVEx.Workbook.t(), sheet_name(), cell_ref()) ::
  {:ok, String.t() | nil} | {:error, term()}

get_style(book, sheet, ref)

@spec get_style(ExVEx.Workbook.t(), sheet_name(), cell_ref()) ::
  {:ok, ExVEx.Style.t()} | {:error, term()}

merge_cells(book, sheet, ref, opts \\ [])

@spec merge_cells(ExVEx.Workbook.t(), sheet_name(), range_ref(), keyword()) ::
  {:ok, ExVEx.Workbook.t()} | {:error, term()}

Merges a rectangular range of cells on a sheet.

Options

  • :preserve_valuesfalse (default) to clear every non-anchor cell in the range (Excel's convention); true to leave underlying cells untouched. Excel will still only display the anchor cell's value, but get_cell/3 on a non-anchor cell will keep returning whatever was there.
  • :on_overlap:error (default) to refuse a range that overlaps an existing merge and return {:error, {:overlaps, existing_ref}}; :replace to remove the overlapping range(s) first; :allow to permit overlapping ranges (matches openpyxl's lenient behaviour).

merged_ranges(book, sheet)

@spec merged_ranges(ExVEx.Workbook.t(), sheet_name()) ::
  {:ok, [range_ref()]} | {:error, term()}

Returns the list of merged ranges on a sheet as A1-style range refs.

new()

@spec new() :: {:ok, ExVEx.Workbook.t()} | {:error, term()}

Returns a minimal blank workbook with a single empty sheet named "Sheet1". Compose with add_sheet/2, rename_sheet/3, remove_sheet/2, and put_cell/4 to build a template from scratch.

open(path)

@spec open(path()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}

put_cell(book, sheet, ref, value)

@spec put_cell(ExVEx.Workbook.t(), sheet_name(), cell_ref(), cell_value()) ::
  {:ok, ExVEx.Workbook.t()} | {:error, term()}

remove_sheet(book, name)

@spec remove_sheet(ExVEx.Workbook.t(), sheet_name()) ::
  {:ok, ExVEx.Workbook.t()} | {:error, term()}

Removes a sheet and its worksheet part from the workbook.

Returns {:error, :unknown_sheet} if the name is not found, or {:error, :last_sheet} if removing it would leave the workbook with zero sheets (invalid per the OOXML spec).

rename_sheet(book, old, new)

@spec rename_sheet(ExVEx.Workbook.t(), sheet_name(), sheet_name()) ::
  {:ok, ExVEx.Workbook.t()} | {:error, term()}

Renames a sheet. Returns {:error, :unknown_sheet} if old is not found.

save(book, path)

@spec save(ExVEx.Workbook.t(), path()) :: :ok | {:error, term()}

sheet_names(workbook)

@spec sheet_names(ExVEx.Workbook.t()) :: [sheet_name()]

sheet_path(book, name)

@spec sheet_path(ExVEx.Workbook.t(), sheet_name()) :: {:ok, String.t()} | :error

unmerge_cells(book, sheet, ref, opts \\ [])

@spec unmerge_cells(ExVEx.Workbook.t(), sheet_name(), range_ref(), keyword()) ::
  {:ok, ExVEx.Workbook.t()} | {:error, term()}

Removes a merged range from a sheet.

Options

  • :on_missing:error (default) returns {:error, :not_merged} when the exact range is not currently merged; :ignore makes the call a no-op in that case.