Module: Xlsxrb

Defined in:
lib/xlsxrb.rb,
lib/xlsxrb/ooxml.rb,
lib/xlsxrb/version.rb,
lib/xlsxrb/elements.rb,
lib/xlsxrb/ooxml/utils.rb,
lib/xlsxrb/elements/row.rb,
lib/xlsxrb/ooxml/reader.rb,
lib/xlsxrb/ooxml/writer.rb,
lib/xlsxrb/elements/cell.rb,
lib/xlsxrb/style_builder.rb,
lib/xlsxrb/elements/types.rb,
lib/xlsxrb/elements/column.rb,
lib/xlsxrb/ooxml/xml_parser.rb,
lib/xlsxrb/ooxml/zip_reader.rb,
lib/xlsxrb/ooxml/zip_writer.rb,
lib/xlsxrb/elements/workbook.rb,
lib/xlsxrb/ooxml/xml_builder.rb,
lib/xlsxrb/elements/worksheet.rb,
lib/xlsxrb/ooxml/styles_parser.rb,
lib/xlsxrb/ooxml/zip_generator.rb,
lib/xlsxrb/ooxml/workbook_parser.rb,
lib/xlsxrb/ooxml/workbook_writer.rb,
lib/xlsxrb/ooxml/worksheet_parser.rb,
lib/xlsxrb/ooxml/worksheet_writer.rb,
lib/xlsxrb/ooxml/shared_strings_parser.rb,
sig/xlsxrb.rbs

Overview

Ruby XLSX read/write library.

Defined Under Namespace

Modules: Elements, Ooxml Classes: ChartBuilder, Error, StreamWriter, StyleBuilder, WorkbookBuilder, WorksheetBuilder

Constant Summary collapse

TRACER =
OpenTelemetry.tracer_provider.tracer("xlsxrb", Xlsxrb::VERSION)
VERSION =

Returns:

  • (String)
"0.1.1"

Class Method Summary collapse

Class Method Details

.build(&block) ⇒ Object

Builds an Elements::Workbook in memory using a DSL.

Raises:



241
242
243
244
245
246
247
248
249
# File 'lib/xlsxrb.rb', line 241

def self.build(&block)
  raise Error, "block is required" unless block

  TRACER.in_span("Xlsxrb.build") do
    builder = WorkbookBuilder.new
    block.call(builder)
    builder.build
  end
end

.build_raw_cell_from_value(row_index, col_index, value, sst, sst_index) ⇒ Object

Builds a raw cell hash from a value for streaming writes.



1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
# File 'lib/xlsxrb.rb', line 1396

def self.build_raw_cell_from_value(row_index, col_index, value, sst, sst_index)
  ref = "#{Elements::Cell.column_letter(col_index)}#{row_index + 1}"
  result = { ref: ref }

  case value
  when Elements::Formula
    result[:formula] = value.expression
    result[:formula_ca] = true if value.calculate_always
    result[:value] = value.cached_value if value.cached_value
  when String
    idx = sst_index[value] ||= begin
      sst << value
      sst.size - 1
    end
    result[:value] = idx
    result[:type] = "s"
  when true, false
    result[:value] = value
    result[:type] = "b"
  when Integer, Float
    result[:value] = value
  when Date
    result[:value] = Xlsxrb::Ooxml::Utils.date_to_serial(value)
  when Time
    result[:value] = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
  when NilClass
    # empty cell
  end

  result
end

.foreach(source, sheet: 0, &block) ⇒ Object

Streaming read: yields Elements::Row one at a time. source: file path (String) or IO object. Options:

sheet: sheet index (0-based Integer) or name (String). Defaults to 0.


194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/xlsxrb.rb', line 194

def self.foreach(source, sheet: 0, &block)
  return enum_for(:foreach, source, sheet: sheet) unless block

  attributes = source.is_a?(String) ? { "filepath" => source } : {}
  TRACER.in_span("Xlsxrb.foreach", attributes: attributes) do
    entries = Ooxml::ZipReader.open(source, &:read_all)
    shared_strings = Ooxml::SharedStringsParser.parse(entries["xl/sharedStrings.xml"])
    workbook_sheets = Ooxml::WorkbookParser.parse(entries["xl/workbook.xml"])
    rels = Ooxml::RelationshipsParser.parse(entries["xl/_rels/workbook.xml.rels"])

    target_sheet = case sheet
                   when Integer
                     workbook_sheets[sheet]
                   when String
                     workbook_sheets.find { |s| s[:name] == sheet }
                   end
    next unless target_sheet

    target = rels[target_sheet[:r_id]]
    next unless target

    sheet_path = target.start_with?("/") ? target.delete_prefix("/") : "xl/#{target}"
    sheet_xml = entries[sheet_path]
    next if sheet_xml.nil? || sheet_xml.empty?

    Ooxml::WorksheetParser.each_row(sheet_xml, shared_strings: shared_strings) do |raw_row|
      row = build_row_from_raw(raw_row)
      block.call(row)
    end
  end
end

.formula(expression, cached_value: nil) ⇒ Object

Creates a Formula object for use in add_row values. expression: the formula text (e.g. "SUM(A1:A10)") cached_value: optional cached result. If nil, Excel will calculate on open.



80
81
82
83
84
85
86
# File 'lib/xlsxrb.rb', line 80

def self.formula(expression, cached_value: nil)
  Elements::Formula.new(
    expression: expression,
    cached_value: cached_value,
    calculate_always: cached_value.nil? || nil
  )
end

.generate(target, &block) ⇒ Object

Streaming write: yields a StreamWriter context for building XLSX on-the-fly. target: file path (String) or IO object.

Raises:



228
229
230
231
232
233
234
235
236
237
238
# File 'lib/xlsxrb.rb', line 228

def self.generate(target, &block)
  raise Error, "target is required" if target.nil?
  raise Error, "block is required" unless block

  attributes = target.is_a?(String) ? { "filepath" => target } : {}
  TRACER.in_span("Xlsxrb.generate", attributes: attributes) do
    stream_writer = StreamWriter.new(target)
    block.call(stream_writer)
    stream_writer.close
  end
end

.modify(source, target = nil, &block) ⇒ Object

Modifies an existing XLSX file. Reads the workbook, passes it to the block, and writes the result. The block receives an Elements::Workbook and must return a modified one (e.g. via with). If no target is given, the source is overwritten.

Example:

Xlsxrb.modify("template.xlsx", "output.xlsx") do |wb|
sheet = wb.sheet(0)
row0 = sheet.row_at(0)
new_cell = Xlsxrb::Elements::Cell.new(row_index: 0, column_index: 1, value: "Updated")
new_row = row0.with(cells: row0.cells.map { |c| c.column_index == 1 ? new_cell : c })
new_sheet = sheet.with(rows: sheet.rows.map { |r| r.index == 0 ? new_row : r })
wb.with(sheets: wb.sheets.map.with_index { |s, i| i == 0 ? new_sheet : s })
end

Raises:



178
179
180
181
182
183
184
185
186
187
188
# File 'lib/xlsxrb.rb', line 178

def self.modify(source, target = nil, &block)
  raise Error, "source is required" if source.nil?
  raise Error, "block is required" unless block

  workbook = read(source)
  result_workbook = block.call(workbook)
  result_workbook = workbook unless result_workbook.is_a?(Elements::Workbook)

  write_target = target || source
  write(write_target, result_workbook)
end

.read(source) ⇒ Object

Reads an XLSX file into an Elements::Workbook. source: file path (String) or IO object.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/xlsxrb.rb', line 90

def self.read(source)
  attributes = source.is_a?(String) ? { "filepath" => source } : {}
  TRACER.in_span("Xlsxrb.read", attributes: attributes) do
    entries = Ooxml::ZipReader.open(source, &:read_all)
    shared_strings = Ooxml::SharedStringsParser.parse(entries["xl/sharedStrings.xml"])
    styles = Ooxml::StylesParser.parse(entries["xl/styles.xml"])
    workbook_sheets = Ooxml::WorkbookParser.parse(entries["xl/workbook.xml"])
    rels = Ooxml::RelationshipsParser.parse(entries["xl/_rels/workbook.xml.rels"])

    sheets = workbook_sheets.map do |sheet_info|
      target = rels[sheet_info[:r_id]]
      next nil unless target

      sheet_path = target.start_with?("/") ? target.delete_prefix("/") : "xl/#{target}"
      sheet_xml = entries[sheet_path]
      build_worksheet(sheet_info[:name], sheet_xml, shared_strings, styles)
    end.compact

    Elements::Workbook.new(sheets: sheets, shared_strings: shared_strings, styles: styles)
  end
end

.write(target, workbook) ⇒ Object

Writes an Elements::Workbook to an XLSX file. target: file path (String) or IO object.

Raises:



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/xlsxrb.rb', line 114

def self.write(target, workbook)
  raise Error, "target is required" if target.nil?
  raise Error, "workbook must be an Elements::Workbook" unless workbook.is_a?(Elements::Workbook)

  attributes = target.is_a?(String) ? { "filepath" => target } : {}
  TRACER.in_span("Xlsxrb.write", attributes: attributes) do
    sst = []
    sst_index = {}

    # Collect shared strings and build index without allocating new Hashes
    sheet_data = workbook.sheets.map do |ws|
      ws.rows.each do |row|
        row.cells.each do |cell|
          val = cell.value
          if (val.is_a?(String) || val.is_a?(Elements::RichText)) && !sst_index.key?(val)
            sst << val
            sst_index[val] = sst.size - 1
          end
        end
      end
      columns = ws.columns.map do |col|
        { index: col.index, width: col.width, hidden: col.hidden, custom_width: col.custom_width, outline_level: col.outline_level }
      end
      sd = { name: ws.name, rows: ws.rows, columns: columns }
      sd[:charts] = ws.charts unless ws.charts.empty?

      # Extract facade metadata from unmapped_data
      facade = ws.unmapped_data[:facade]
      facade&.each { |key, val| sd[key] = val }

      sd
    end

    # Extract workbook-level facade metadata
    wb_facade = workbook.unmapped_data[:facade] || {}
    Ooxml::WorkbookWriter.write(
      target,
      sheets: sheet_data,
      shared_strings: sst,
      shared_strings_index: sst_index,
      styles: workbook.styles,
      defined_names: wb_facade[:defined_names],
      core_properties: wb_facade[:core_properties],
      app_properties: wb_facade[:app_properties],
      custom_properties: wb_facade[:custom_properties],
      workbook_protection: wb_facade[:workbook_protection]
    )
  end
end