class Xlsxrb::Ooxml::WorksheetWriter
Generates worksheet XML for a list of rows. Supports streaming: rows can be written one at a time.
Constants
- COLUMN_LETTERS
- DOC_REL_NS
- INTEGER_STRINGS
- SSML_NS
Public Class Methods
(untyped io, ?date1904: untyped) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 29 def initialize(io, date1904: false) @io = io @date1904 = date1904 ? true : false @builder = XmlBuilder.new(@io) @row_buffer = String.new(capacity: 65_536) @started = false @finished = false end
Public Instance Methods
(?drawing_rid: untyped, ?sheet_protection: untyped, ?auto_filter: untyped, ?filter_columns: untyped, ?sort_state: untyped, ?merge_cells: untyped, ?conditional_formats: untyped, ?data_validations: untyped, ?hyperlinks: untyped, ?print_options: untyped, ?page_margins: untyped, ?page_setup: untyped, ?header_footer: untyped, ?row_breaks: untyped, ?col_breaks: untyped, ?tables: untyped, ?table_start_rid: untyped, ?legacy_drawing_rid: untyped, ?sparkline_groups: untyped) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 665 def finish(drawing_rid: nil, sheet_protection: nil, auto_filter: nil, filter_columns: nil, sort_state: nil, merge_cells: nil, conditional_formats: nil, data_validations: nil, hyperlinks: nil, print_options: nil, page_margins: nil, page_setup: nil, header_footer: nil, row_breaks: nil, col_breaks: nil, tables: nil, table_start_rid: nil, legacy_drawing_rid: nil, sparkline_groups: nil) return if @finished start unless @started @finished = true if @row_buffer && !@row_buffer.empty? @io.write(@row_buffer) @row_buffer.clear end @builder.close_tag("sheetData") # Elements must appear in OOXML specification order after sheetData write_sheet_protection(sheet_protection) if sheet_protection write_auto_filter(auto_filter, filter_columns, sort_state) if auto_filter write_merge_cells(merge_cells) if merge_cells && !merge_cells.empty? write_conditional_formatting(conditional_formats) if conditional_formats && !conditional_formats.empty? write_data_validations(data_validations) if data_validations && !data_validations.empty? write_hyperlinks(hyperlinks) if hyperlinks && !hyperlinks.empty? write_print_options(print_options) if print_options && !print_options.empty? write_page_margins(page_margins) if page_margins write_page_setup(page_setup) if page_setup && !page_setup.empty? write_header_footer(header_footer) if header_footer && !header_footer.empty? write_row_breaks(row_breaks) if row_breaks && !row_breaks.empty? write_col_breaks(col_breaks) if col_breaks && !col_breaks.empty? @builder.empty_tag("drawing", { "r:id": drawing_rid }) if drawing_rid @builder.empty_tag("legacyDrawing", { "r:id": legacy_drawing_rid }) if legacy_drawing_rid write_table_parts(tables, table_start_rid) if tables && !tables.empty? write_sparklines(sparkline_groups) if sparkline_groups && !sparkline_groups.empty? @builder.close_tag("worksheet") end
Write the worksheet footer. Call once after all rows. Options for post-sheetData elements (in OOXML order):
(untyped col_index, untyped style_id) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 38 def set_column_style(col_index, style_id) @column_styles ||= {} @column_styles[col_index] = style_id end
(?columns: untyped, ?sheet_properties: untyped, ?freeze_pane: untyped, ?split_pane: untyped, ?selection: untyped, ?sheet_view: untyped) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 50 def start(columns: [], sheet_properties: nil, freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil) return if @started @started = true @builder.declaration @builder.open_tag("worksheet", { xmlns: SSML_NS, "xmlns:r": DOC_REL_NS }) @column_styles ||= {} if columns && !columns.empty? columns.each do |col| if col.is_a?(Elements::Column) s = col.style_index min = col.index max = col.index else s = col[:style_index] || col[:style] min = col[:index] || col[:min] || 0 max = col[:index] || col[:max] || col[:min] || 0 end next unless s (min..max).each { |i| @column_styles[i] = s } end end @column_styles = nil if @column_styles.empty? write_sheet_properties(sheet_properties) if sheet_properties && !sheet_properties.empty? write_sheet_views(freeze_pane: freeze_pane, split_pane: split_pane, selection: selection, sheet_view: sheet_view) if freeze_pane || split_pane || selection || (sheet_view && !sheet_view.empty?) write_columns(columns) unless columns.empty? @builder.open_tag("sheetData") end
Write the worksheet header. Call once before writing rows. Options for pre-sheetData elements:
sheet_properties: Hash of sheet-level properties (:tab_color, etc.)
freeze_pane: { row:, col:, state: :frozen }
split_pane: { x_split:, y_split:, top_left_cell: }
selection: { active_cell:, sqref:, pane: }
sheet_view: Hash of sheet view properties
(untyped row_index, untyped cells, ?attrs: untyped, ?unmapped: untyped, ?sst_index: untyped) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 84 def write_row(row_index, cells, attrs: {}, unmapped: [], sst_index: nil) start unless @started row_num = row_index + 1 row_num_str = row_num.to_s buf = @row_buffer ||= String.new(capacity: 65_536) buf << '<row r="' << row_num_str << '"' buf << ' s="' << attrs[:style_index].to_s << '" customFormat="1"' if attrs[:style_index] if attrs[:height] buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"' elsif attrs[:custom_height] buf << ' customHeight="1"' end buf << ' hidden="1"' if attrs[:hidden] buf << ' collapsed="1"' if attrs[:collapsed] buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level] buf << ">" cells.each do |cell| if cell.is_a?(Elements::Cell) value = cell.value style_id = cell.style_index col_ref = cell.ref || "#{column_letter(cell.column_index)}#{row_num_str}" col_idx = cell.column_index formula = cell.formula formula_ca = false cell_type_val = nil elsif cell.is_a?(Hash) value = cell[:value] style_id = cell[:style_index] col_idx = cell[:column_index] col_ref = cell[:ref] || (col_idx ? "#{column_letter(col_idx)}#{row_num_str}" : nil) formula = cell[:formula] formula_ca = cell[:formula_ca] cell_type_val = cell[:type] else value = cell style_id = nil col_idx = nil col_ref = nil formula = nil formula_ca = false cell_type_val = nil end col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil style_id ||= attrs[:style_index] || col_style # Fast path for common unstyled cells if !formula && !style_id && !cell_type_val case value when Integer, Float buf << '<c r="' << col_ref << '"><v>' << value.to_s << "</v></c>" next when String if !value.start_with?("=") && sst_index && (idx = sst_index[value]) buf << '<c r="' << col_ref << '" t="s"><v>' << idx.to_s << "</v></c>" next end when true buf << '<c r="' << col_ref << '" t="b"><v>1</v></c>' next when false buf << '<c r="' << col_ref << '" t="b"><v>0</v></c>' next when nil buf << '<c r="' << col_ref << '"/>' next when Date serial = Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904) buf << '<c r="' << col_ref << '"><v>' << serial.to_s << "</v></c>" next end end if value.nil? && formula.nil? buf << '<c r="' << col_ref << '"' buf << ' s="' << style_id.to_s << '"' if style_id buf << "/>" next end if value.is_a?(String) && value.start_with?("=") && !formula formula = value value = nil end xml_val = value type = cell_type_val formula_type = nil formula_ref = nil formula_si = nil if formula if formula.is_a?(Xlsxrb::Elements::Formula) formula_expr = formula.expression formula_ca = formula.calculate_always formula_type = formula.type formula_ref = formula.ref formula_si = formula.shared_index xml_val = formula.cached_value.nil? ? (value || nil) : formula.cached_value elsif formula.is_a?(Hash) formula_expr = formula[:formula] || formula[:expression] formula_ca = formula[:calculate_always] formula_type = formula[:type] formula_ref = formula[:ref] formula_si = formula[:si] || formula[:shared_index] xml_val = formula[:value] || formula[:cached_value] || value || nil else formula_expr = formula xml_val = value || nil end formula_expr = formula_expr[1..] if formula_expr.start_with?("=") end if !formula_expr && !type case value when String if sst_index && (idx = sst_index[value]) xml_val = idx type = "s" else type = "inlineStr" end when Xlsxrb::Elements::RichText if sst_index && (idx = sst_index[value]) xml_val = idx type = "s" else type = "inlineStr" xml_val = value end when true xml_val = "1" type = "b" when false xml_val = "0" type = "b" when Date xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904) when Time xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904) when BigDecimal xml_val = value.to_s("F") when Xlsxrb::Elements::CellError xml_val = value.code type = "e" end end buf << '<c r="' << col_ref << '"' buf << ' s="' << style_id.to_s << '"' if style_id buf << ' t="' << type << '"' if type if type == "inlineStr" if xml_val.is_a?(Xlsxrb::Elements::RichText) buf << "><is>" xml_val.runs.each do |run| font = run[:font] if font && !font.empty? buf << "<r><rPr>" buf << "<b/>" if font[:bold] buf << "<i/>" if font[:italic] buf << "<strike/>" if font[:strike] if font[:underline] if font[:underline] == true buf << "<u/>" else buf << '<u val="' << font[:underline].to_s << '"/>' end end buf << '<vertAlign val="' << font[:vert_align].to_s << '"/>' if font[:vert_align] buf << '<sz val="' << font[:sz].to_s << '"/>' if font[:sz] if font[:color] buf << '<color rgb="' << font[:color].to_s << '"/>' elsif font[:theme] tint_attr = font[:tint] ? " tint=\"#{font[:tint]}\"" : "" buf << '<color theme="' << font[:theme].to_s << '"' << tint_attr << "/>" end buf << '<rFont val="' << escape_xml(font[:name]) << '"/>' if font[:name] buf << '<family val="' << font[:family].to_s << '"/>' if font[:family] buf << '<scheme val="' << font[:scheme].to_s << '"/>' if font[:scheme] buf << "</rPr><t>" else buf << "<r><t>" end buf << escape_xml(run[:text]) << "</t></r>" end buf << "</is></c>" else buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>" end elsif formula_expr f_attrs = +"" if formula_type case formula_type.to_sym when :array f_attrs << ' t="array"' f_attrs << %( ref="#{formula_ref}") if formula_ref when :shared f_attrs << ' t="shared"' f_attrs << %( si="#{formula_si}") if formula_si f_attrs << %( ref="#{formula_ref}") if formula_ref end end f_attrs << ' ca="1"' if formula_ca buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>" if xml_val buf << "<v>" << xml_val.to_s << "</v></c>" else buf << "<v>0</v></c>" end else buf << "><v>" << xml_val.to_s << "</v></c>" end end buf << "</row>" if buf.bytesize >= 32_768 @io.write(buf) buf.clear end unmapped.each { |node| @builder.write_unmapped(node) } end
Write a single row. Automatically calls start if needed.
(untyped row_index, untyped values, ?styles: untyped, ?style_map: untyped, ?sst: untyped, ?sst_index: untyped, ?attrs: untyped) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 311 def write_row_values(row_index, values, styles: nil, style_map: nil, sst: nil, sst_index: nil, attrs: nil) start unless @started row_num = row_index + 1 row_num_str = row_num < 65_536 ? INTEGER_STRINGS[row_num] : row_num.to_s buf = @row_buffer ||= String.new(capacity: 65_536) if styles.nil? && attrs.nil? && @column_styles.nil? buf << "<row r=\"#{row_num_str}\">" col_index = 0 max_len = values.length while col_index < max_len value = values[col_index] col_ref = COLUMN_LETTERS[col_index] || column_letter(col_index) col_index += 1 next if value.nil? case value when Integer val_str = value >= 0 && value < 65_536 ? INTEGER_STRINGS[value] : value.to_s buf << "<c r=\"#{col_ref}#{row_num_str}\"><v>#{val_str}</v></c>" when Float buf << "<c r=\"#{col_ref}#{row_num_str}\"><v>#{value}</v></c>" when String unless value.start_with?("=") raise ArgumentError, "Cell text length #{value.length} exceeds Excel limit of 32,767 characters" if @strict_excel_mode && value.length > 32_767 idx = (sst_index[value] ||= begin sst << value sst.size - 1 end) idx_str = idx < 65_536 ? INTEGER_STRINGS[idx] : idx.to_s buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"s\"><v>#{idx_str}</v></c>" next end formula_expr = value[1..] buf << "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f><v>0</v></c>" when true buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"b\"><v>1</v></c>" when false buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"b\"><v>0</v></c>" when Date date_style_id = style_map ? style_map["__xlsxrb_date"] : nil buf << if date_style_id "<c r=\"#{col_ref}#{row_num_str}\" s=\"#{date_style_id}\"><v>#{Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904)}</v></c>" else "<c r=\"#{col_ref}#{row_num_str}\"><v>#{Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904)}</v></c>" end when Time time_style_id = style_map ? style_map["__xlsxrb_time"] : nil buf << if time_style_id "<c r=\"#{col_ref}#{row_num_str}\" s=\"#{time_style_id}\"><v>#{Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904)}</v></c>" else "<c r=\"#{col_ref}#{row_num_str}\"><v>#{Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904)}</v></c>" end when Xlsxrb::Elements::Formula formula_expr = value.expression formula_expr = formula_expr[1..] if formula_expr.start_with?("=") f_attrs = +"" if value.type case value.type.to_sym when :array f_attrs << ' t="array"' f_attrs << %( ref="#{value.ref}") if value.ref when :shared f_attrs << ' t="shared"' f_attrs << %( si="#{value.shared_index}") if value.shared_index f_attrs << %( ref="#{value.ref}") if value.ref end end f_attrs << ' ca="1"' if value.calculate_always v_val = value.cached_value.nil? ? 0 : value.cached_value buf << "<c r=\"#{col_ref}#{row_num_str}\"><f#{f_attrs}>#{escape_xml(formula_expr)}</f><v>#{v_val}</v></c>" when Hash if value.key?(:formula) formula_expr = value[:formula] || value[:expression] formula_expr = formula_expr[1..] if formula_expr.start_with?("=") f_attrs = +"" if value[:type] case value[:type].to_sym when :array f_attrs << ' t="array"' f_attrs << %( ref="#{value[:ref]}") if value[:ref] when :shared f_attrs << ' t="shared"' f_attrs << %( si="#{value[:si] || value[:shared_index]}") if value[:si] || value[:shared_index] f_attrs << %( ref="#{value[:ref]}") if value[:ref] end end f_attrs << ' ca="1"' if value[:calculate_always] xml_val = value[:value] || value[:cached_value] || 0 buf << "<c r=\"#{col_ref}#{row_num_str}\"><f#{f_attrs}>#{escape_xml(formula_expr)}</f><v>#{xml_val}</v></c>" end when Xlsxrb::Elements::CellError buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"e\"><v>#{value.code}</v></c>" when BigDecimal buf << "<c r=\"#{col_ref}#{row_num_str}\"><v>#{value.to_s("F")}</v></c>" else idx = (sst_index[value] ||= begin sst << value sst.size - 1 end) idx_str = idx < 65_536 ? INTEGER_STRINGS[idx] : idx.to_s buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"s\"><v>#{idx_str}</v></c>" end end buf << "</row>" return unless buf.bytesize >= 32_768 @io.write(buf) buf.clear return end is_styles_collection = styles && (styles.is_a?(Array) || styles.is_a?(Hash)) single_style_id = nil single_style_id = style_map[styles] if styles && style_map && !is_styles_collection buf << '<row r="' << row_num_str << '"' if attrs buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"' if attrs[:height] buf << ' hidden="1"' if attrs[:hidden] buf << ' collapsed="1"' if attrs[:collapsed] buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level] end buf << ">" max_len = values.length if is_styles_collection styles_len = styles.is_a?(Array) ? styles.length : (styles.keys.max || -1) + 1 max_len = [max_len, styles_len].max end row_style = attrs ? attrs[:style_index] : nil row_style_id = row_style && style_map ? (style_map[row_style] || style_map[row_style.to_s] || row_style) : row_style col_index = 0 while col_index < max_len value = col_index < values.length ? values[col_index] : nil style_id = single_style_id if is_styles_collection style_name = if styles.is_a?(Array) col_index < styles.length ? styles[col_index] : nil else styles[col_index] end style_id = if style_map && style_name style_map[style_name] || style_map[style_name.to_s] || style_name else style_name end end if style_id.nil? col_style = @column_styles ? @column_styles[col_index] : nil col_style_id = if col_style && style_map style_map[col_style] || style_map[col_style.to_s] || col_style else col_style end style_id = row_style_id || col_style_id end col_ref = COLUMN_LETTERS[col_index] || column_letter(col_index) if value.nil? buf << '<c r="' << col_ref << row_num_str << '" s="' << style_id.to_s << '"/>' if style_id col_index += 1 next end # Fast path: unstyled numbers, booleans, and simple strings (majority of cells) if style_id.nil? && !value.is_a?(Xlsxrb::Elements::Formula) && !value.is_a?(Hash) case value when Integer val_str = INTEGER_STRINGS[value] || value.to_s buf << '<c r="' << col_ref << row_num_str << '"><v>' << val_str << "</v></c>" col_index += 1 next when Float buf << '<c r="' << col_ref << row_num_str << '"><v>' << value.to_s << "</v></c>" col_index += 1 next when String unless value.start_with?("=") idx = sst_index[value] unless idx sst << value idx = sst.size - 1 sst_index[value] = idx end idx_str = INTEGER_STRINGS[idx] || idx.to_s buf << '<c r="' << col_ref << row_num_str << '" t="s"><v>' << idx_str << "</v></c>" col_index += 1 next end when true buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>1</v></c>' col_index += 1 next when false buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>0</v></c>' col_index += 1 next when Date buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904).to_s << "</v></c>" col_index += 1 next when Time buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904).to_s << "</v></c>" col_index += 1 next end end # General path: styled, formula, rich text, or complex cell formula_expr = nil formula_ca = false formula_type = nil formula_ref = nil formula_si = nil xml_val = value type = nil case value when Xlsxrb::Elements::Formula formula_expr = value.expression formula_ca = value.calculate_always formula_type = value.type formula_ref = value.ref formula_si = value.shared_index xml_val = value.cached_value case value.cached_value when String type = "str" when true type = "b" xml_val = "1" when false type = "b" xml_val = "0" end when Hash if value.key?(:formula) formula_expr = value[:formula] || value[:expression] formula_ca = value[:calculate_always] || false formula_type = value[:type] formula_ref = value[:ref] formula_si = value[:si] || value[:shared_index] xml_val = value[:value] || value[:cached_value] case xml_val when String type = "str" when true type = "b" xml_val = "1" when false type = "b" xml_val = "0" end end when String if value.start_with?("=") && value.length > 1 formula_expr = value xml_val = nil elsif sst_index idx = sst_index[value] unless idx sst << value idx = sst.size - 1 sst_index[value] = idx end xml_val = idx type = "s" else xml_val = value type = "inlineStr" end when Xlsxrb::Elements::RichText if sst_index idx = sst_index[value] unless idx sst << value idx = sst.size - 1 sst_index[value] = idx end xml_val = idx type = "s" else xml_val = value type = "inlineStr" end when true xml_val = "1" type = "b" when false xml_val = "0" type = "b" when Date xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904) when Time xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904) when BigDecimal xml_val = value.to_s("F") when Xlsxrb::Elements::CellError xml_val = value.code type = "e" end formula_expr = formula_expr[1..] if formula_expr&.start_with?("=") buf << '<c r="' << col_ref << row_num_str << '"' buf << ' s="' << style_id.to_s << '"' if style_id buf << ' t="' << type << '"' if type if formula_expr f_attrs = +"" if formula_type case formula_type.to_sym when :array f_attrs << ' t="array"' f_attrs << %( ref="#{formula_ref}") if formula_ref when :shared f_attrs << ' t="shared"' f_attrs << %( si="#{formula_si}") if formula_si f_attrs << %( ref="#{formula_ref}") if formula_ref end end f_attrs << ' ca="1"' if formula_ca buf << "<f#{f_attrs}>" << escape_xml(formula_expr) << "</f>" if xml_val buf << "<v>" << xml_val.to_s << "</v></c>" else buf << "<v>0</v></c>" end elsif type == "inlineStr" buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>" else buf << "><v>" << xml_val.to_s << "</v></c>" end col_index += 1 end buf << "</row>" return unless buf.bytesize >= 32_768 @io.write(buf) buf.clear end
Highly optimized row writing for StreamWriter that avoids allocating intermediate Hashes.