Zaxcel is a Ruby library built on top of caxlsx that adds an abstraction layer to building Excel documents. It provides simple methods for building formulas and references to other cells, even across many worksheets, using a clean Ruby DSL without having to think about the underlying Excel implementation.
- Goal: Enable building Excel sheets in a Ruby-idiomatic DSL without having to think about the underlying Excel. We want to interact with purely Ruby objects in a purely Ruby way while building Excel sheets.
- Anti-goal: Reimplementing Excel in Ruby.
Zaxcel is available on RubyGems.
Add this line to your application's Gemfile:
gem 'zaxcel'And then execute:
$ bundle installOr install it yourself as:
$ gem install zaxcel- Ruby >= 3.0 (tested up to 3.4)
- Runtime dependencies are managed automatically via Rubygems, but you should have Bundler set up in your project. Zaxcel depends on:
caxlsx(~> 4.0)activesupport(>= 6.0)money(~> 6.0)sorbet-runtime(~> 0.5)
For running examples directly from the repo, prefer using Bundler:
bundle install
bundle exec ruby examples/basic_spreadsheet.rbrequire 'zaxcel'
# Create a new document
document = Zaxcel::Document.new
sheet = document.add_sheet!('my_sheet')
# Add columns
sheet.add_column!(:column_1)
sheet.add_column!(:column_2)
# Add rows with data
row_1 = sheet.add_row!(:row_1)
.add!(:column_1, value: 'Row 1')
.add!(:column_2, value: 99)
row_2 = sheet.add_row!(:row_2)
.add!(:column_1, value: 'Row 2')
.add!(:column_2, value: 1)
# Add a total row with a formula that references other cells
sheet.add_row!(:total)
.add!(:column_1, value: 'Total')
.add!(:column_2, row_1.ref(:column_2) + row_2.ref(:column_2))
# Finalize the sheet (position and generate after all content is added)
sheet.position_rows!
sheet.generate_sheet!
# Write to file (binary-safe)
data = document.file_contents
raise 'Document had no contents' if data.nil?
File.write('output.xlsx', data, mode: 'wb')Zaxcel makes it easy to build Excel formulas using Ruby operators:
# Arithmetic operations
cell_sum = row_1.ref(:amount) + row_2.ref(:amount)
cell_diff = row_1.ref(:amount) - row_2.ref(:amount)
cell_product = row_1.ref(:amount) * row_2.ref(:amount)
cell_quotient = row_1.ref(:amount) / row_2.ref(:amount)
# Excel functions
sum_formula = Zaxcel::Functions.sum(
Zaxcel::Lang.range(row_1.ref(:amount), row_10.ref(:amount))
)
average_formula = Zaxcel::Functions::Average.new(
Zaxcel::Lang.range(row_1.ref(:amount), row_10.ref(:amount))
)
max_formula = Zaxcel::Functions.max(
Zaxcel::Lang.range(row_1.ref(:amount), row_10.ref(:amount))
)
min_formula = Zaxcel::Functions.min(
Zaxcel::Lang.range(row_1.ref(:amount), row_10.ref(:amount))
)
# Conditional formulas
if_formula = Zaxcel::Lang
.if(row_1.ref(:amount) > 100)
.then('High')
.else('Low')
# Rounding
rounded = Zaxcel::Functions.round(row_1.ref(:amount), precision: 2)One of Zaxcel's strengths is making cross-sheet references easy:
document = Zaxcel::Document.new
# Create first sheet with data
data_sheet = document.add_sheet!('Data')
data_sheet.add_column!(:category)
data_sheet.add_column!(:value)
row_1 = data_sheet.add_row!(:row_1)
.add!(:category, value: 'Sales')
.add!(:value, value: 1000)
# Create summary sheet that references the data sheet
summary_sheet = document.add_sheet!('Summary')
summary_sheet.add_column!(:description)
summary_sheet.add_column!(:amount)
summary_sheet.add_row!(:sales_summary)
.add!(:description, value: 'Total Sales')
.add!(:amount, data_sheet.cell_ref(:value, :row_1))
# Position and generate all sheets at once (after all content is added)
# This allows bidirectional references between sheets
document.sheet_by_name.values.each do |sheet|
sheet.position_rows!
sheet.generate_sheet!
endApply styles to cells and columns:
# Define styles
document.add_style!(:header, {
bg_color: '0066CC',
fg_color: 'FFFFFF',
b: true,
alignment: { horizontal: :center }
})
document.add_style!(:currency, {
format_code: '$#,##0.00'
})
document.add_style!(:percentage, {
format_code: '0.00%'
})
# Apply to cells
sheet.add_row!(:header)
.add!(:name, value: 'Product', style: :header)
.add!(:price, value: 'Price', style: :header)
sheet.add_row!(:product_1)
.add!(:name, value: 'Widget')
.add!(:price, value: 19.99, style: :currency)Configure column widths and other properties:
sheet.add_column!(:narrow, width: 10)
sheet.add_column!(:wide, width: 30)
sheet.add_column!(:auto, width: nil) # Let caxlsx auto-calc width from contents
# Or use computed widths
sheet.add_column!(
:fit_header,
width: Zaxcel::Column::ComputedColumnWidth::Header,
)
sheet.add_column!(
:fit_content,
width: Zaxcel::Column::ComputedColumnWidth::MaxContent,
)# Sum values where a condition is met
sum_if = Zaxcel::Functions.sum_if(
column_to_check: category_column,
value_to_check: 'Sales',
column_to_sum: amount_column,
)
# Sum with multiple conditions
sum_ifs = Zaxcel::Functions.sum_ifs(
ranges_to_check: [
Zaxcel::Lang.range(category_column),
Zaxcel::Lang.range(region_column),
],
values_to_check: ['Sales', 'West'],
range_to_sum: Zaxcel::Lang.range(amount_column),
)# Modern XLOOKUP
lookup = Zaxcel::Functions.x_lookup(
'Product A',
idx_range: Zaxcel::Lang.range(product_column),
value_range: Zaxcel::Lang.range(price_column),
)
# Traditional INDEX/MATCH
match = Zaxcel::Functions.match(
value: 'Product A',
range: Zaxcel::Lang.range(product_column),
match_type: Zaxcel::Functions::Match::MatchType::EXACT,
)
index = Zaxcel::Functions.index(
index_value: match,
range: price_column,
)# Build conditional logic
status = Zaxcel::Lang
.if(row.ref(:amount) > 1000)
.then('High')
.else('Low')Control whether sheets are visible:
# Create a hidden sheet
hidden_sheet = document.add_sheet!(
'Calculations',
sheet_visibility: Zaxcel::Sheet::SheetVisibility::Hidden
)
# Create a visible sheet (default)
visible_sheet = document.add_sheet!(
'Report',
sheet_visibility: Zaxcel::Sheet::SheetVisibility::Visible
)The main container for your Excel workbook.
new(width_units_by_default_character: Float)- Create a new documentadd_sheet!(name, sheet_visibility: SheetVisibility)- Add a new worksheetadd_style!(name, **kwargs)- Define a named stylesheet(name)- Get a sheet by namefile_contents- Get the binary Excel file content
Represents a worksheet within the document.
add_column!(name, width: nil)- Add a column (usenilfor auto width; orZaxcel::Column::ComputedColumnWidth)add_row!(name, style_group: nil)- Add a rowcell_ref(row_name, col_name, sheet_name: nil)- Get a reference to a cellposition_rows!thengenerate_sheet!- Finalize the sheet (call before writing)
Represents a row within a sheet.
add!(column_name, value:, style: nil, to_extract: false)- Add a cell valueadd_many!(hash)- Add multiple cells at onceref(column_name)- Get a reference to a cell in this row
Represents a column within a sheet.
All Excel functions are available under Zaxcel::Functions:
abs(value)- Absolute valuesum(*values_or_ranges)- Sum values and/or rangessum_range([first_ref, last_ref])- Sum a contiguous range (convenience)sum_if(column_to_check:, value_to_check:, column_to_sum:)sum_ifs(ranges_to_check:, values_to_check:, range_to_sum:)average(range)- Average of a contiguous rangemax(*values_or_ranges)- Maximum valuemin(*values_or_ranges)- Minimum valueround(value, precision:)- Round to decimal placesand(lhs, rhs)/or(lhs, rhs)- Logical operationsconcatenate(*values)- Join stringstext(value, format_string:)- Format value as textlen(value)- String lengthx_lookup(condition, idx_range:, value_range:)index(index_value:, range:)match(value:, range:, match_type:)withMatchType::EXACT,LESS_THAN_OR_EQUAL,GREATER_THAN_OR_EQUAL
After checking out the repo, run:
bundle installRun the test suite:
bundle exec rspecRun type checking:
bundle exec srb tcBug reports and pull requests are welcome on GitHub at https://github.com/angellist/zaxcel.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
The gem is available as open source under the terms of the MIT License.
Created by the engineering team at AngelList.
Built on top of the excellent caxlsx library.