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.
Drops the <dimension> element from a sheet so Excel recomputes the
used range on next open. Useful after bulk mutations where the cached
dimension no longer reflects the live cell extent. No-op if the sheet
has no <dimension> element.
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.
Creates or replaces a defined name. reference can be either a static
range ("Sheet1!$A$1:$A$10") or a formula ("=OFFSET(Sheet1!$A$1, 0, 0, COUNTA(Sheet1!$A:$A), 1)"). Excel evaluates it at open time.
Lists every defined name in the workbook.
Deletes count columns starting at column at.
Deletes count rows starting at row at.
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).
Inserts count blank columns at column at on the given sheet. Every
cell at or right of column at shifts right.
Inserts count blank rows at row at on the given sheet. Every cell
at or below row at shifts down. Cell formulas, merged ranges,
defined names, and row dimensions are updated so they continue to
reference the same logical data.
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.
Merges the given style options into the cell's existing style.
Removes a defined name. Returns {:error, :unknown_name} if no matching
name exists in the given scope. Scope defaults to :global to match
define_name/4's default.
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
@type cell_ref() :: String.t() | {pos_integer(), pos_integer()}
@type name_scope() :: :global | {:sheet, String.t()}
@type path() :: Path.t()
@type range_ref() :: String.t()
@type sheet_name() :: String.t()
Functions
@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.
@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 clear_sheet_dimension(ExVEx.Workbook.t(), sheet_name()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
Drops the <dimension> element from a sheet so Excel recomputes the
used range on next open. Useful after bulk mutations where the cached
dimension no longer reflects the live cell extent. No-op if the sheet
has no <dimension> element.
@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.
@spec define_name(ExVEx.Workbook.t(), String.t(), String.t(), keyword()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
Creates or replaces a defined name. reference can be either a static
range ("Sheet1!$A$1:$A$10") or a formula ("=OFFSET(Sheet1!$A$1, 0, 0, COUNTA(Sheet1!$A:$A), 1)"). Excel evaluates it at open time.
Options
:scope—:global(default) for workbook-wide names, or{:sheet, sheet_name}for sheet-local names.:hidden—false(default). Hidden names don't appear in Excel's Name Manager but are still resolvable by formulas.
@spec defined_names(ExVEx.Workbook.t()) :: [ %{ name: String.t(), reference: String.t(), scope: name_scope(), hidden: boolean() } ]
Lists every defined name in the workbook.
@spec delete_column(ExVEx.Workbook.t(), sheet_name(), pos_integer(), pos_integer()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
Deletes count columns starting at column at.
@spec delete_row(ExVEx.Workbook.t(), sheet_name(), pos_integer(), pos_integer()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
Deletes count rows starting at row at.
@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 insert_column(ExVEx.Workbook.t(), sheet_name(), pos_integer(), pos_integer()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
Inserts count blank columns at column at on the given sheet. Every
cell at or right of column at shifts right.
@spec insert_row(ExVEx.Workbook.t(), sheet_name(), pos_integer(), pos_integer()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
Inserts count blank rows at row at on the given sheet. Every cell
at or below row at shifts down. Cell formulas, merged ranges,
defined names, and row dimensions are updated so they continue to
reference the same logical data.
@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 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.
@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 put_style(ExVEx.Workbook.t(), sheet_name(), cell_ref(), keyword()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
Merges the given style options into the cell's existing style.
Options include font (:bold, :italic, :strike, :underline,
:font_size, :font_name, :color), fill (:background,
:fill_pattern, :fill_fg, :fill_bg), border (:border,
:border_color, :border_top, :border_bottom, :border_left,
:border_right), alignment (:align, :valign, :wrap_text,
:indent, :text_rotation, :shrink_to_fit), and :number_format
("#,##0.00", "yyyy-mm-dd", etc.).
Colours accept "RRGGBB" (auto-prefixed to ARGB), "AARRGGBB",
{:theme, n}, {:indexed, n}, or :auto.
See ExVEx.Style.Builder for the full list.
@spec remove_defined_name(ExVEx.Workbook.t(), String.t(), keyword()) :: {:ok, ExVEx.Workbook.t()} | {:error, term()}
Removes a defined name. Returns {:error, :unknown_name} if no matching
name exists in the given scope. Scope defaults to :global to match
define_name/4's default.
@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).
@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.
@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.