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
Returns every populated cell on a sheet as a map of A1 refs to resolved values. Empty cells are omitted.
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.
Removes a merged range from a sheet.
Types
@type cell_ref() :: String.t() | {pos_integer(), pos_integer()}
@type path() :: Path.t()
@type range_ref() :: String.t()
@type sheet_name() :: String.t()
Functions
@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.
@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).
@spec get_cell(ExVEx.Workbook.t(), sheet_name(), cell_ref()) :: {:ok, cell_value() | Date.t() | NaiveDateTime.t()} | {:error, term()}
@spec get_formula(ExVEx.Workbook.t(), sheet_name(), cell_ref()) :: {:ok, String.t() | nil} | {:error, term()}
@spec get_style(ExVEx.Workbook.t(), sheet_name(), cell_ref()) :: {:ok, ExVEx.Style.t()} | {:error, term()}
@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_values—false(default) to clear every non-anchor cell in the range (Excel's convention);trueto leave underlying cells untouched. Excel will still only display the anchor cell's value, butget_cell/3on 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}};:replaceto remove the overlapping range(s) first;:allowto permit overlapping ranges (matches openpyxl's lenient behaviour).
@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.
@spec open(path()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
@spec put_cell(ExVEx.Workbook.t(), sheet_name(), cell_ref(), cell_value()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
@spec save(ExVEx.Workbook.t(), path()) :: :ok | {:error, term()}
@spec sheet_names(ExVEx.Workbook.t()) :: [sheet_name()]
@spec sheet_path(ExVEx.Workbook.t(), sheet_name()) :: {:ok, String.t()} | :error
@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;:ignoremakes the call a no-op in that case.