# -*- coding: utf-8 -*- module Axlsx # The Worksheet class represents a worksheet in the workbook. class Worksheet # The name of the worksheet # @return [String] attr_accessor :name # The workbook that owns this worksheet # @return [Workbook] attr_reader :workbook # The worksheet relationships. This is managed automatically by the worksheet # @return [Relationships] attr_reader :relationships # The rows in this worksheet # @note The recommended way to manage rows is Worksheet#add_row # @return [SimpleTypedList] # @see Worksheet#add_row attr_reader :rows # The drawing associated with this worksheet. # @note the recommended way to work with drawings and charts is Worksheet#add_chart # @return [Drawing] # @see Worksheet#add_chart attr_reader :drawing # An array of content based calculated column widths. # @note a single auto fit data item is a hash with :longest => [String] and :sz=> [Integer] members. # @return [Array] of Hash attr_reader :auto_fit_data # The part name of this worksheet # @return [String] attr_reader :pn # The relationship part name of this worksheet # @return [String] attr_reader :rels_pn # The relationship Id of thiw worksheet # @return [String] attr_reader :rId # The index of this worksheet in the owning Workbook's worksheets list. # @return [Integer] attr_reader :index # TODO Merge Cells # attr_reader :merge_cells # Creates a new worksheet. # @note the recommended way to manage worksheets is Workbook#add_worksheet # @see Workbook#add_worksheet # @option options [String] name The name of this sheet. def initialize(wb, options={}) @rows = SimpleTypedList.new Row self.workbook = wb @workbook.worksheets << self @auto_fit_data = [] self.name = options[:name] || "Sheet" + (index+1).to_s @magick_draw = Magick::Draw.new end def name=(v) DataTypeValidator.validate "Worksheet.name", String, v; @name=v end def pn "#{WORKSHEET_PN % (index+1)}" end def rels_pn "#{WORKSHEET_RELS_PN % (index+1)}" end def rId "rId#{index+1}" end def index @workbook.worksheets.index(self) end def drawing @drawing || @drawing = Axlsx::Drawing.new(self) end # Adds a row to the worksheet and updates auto fit data # @return [Row] # @option options [Array] values # @option options [Array, Symbol] types # @option options [Array, Integer] style def add_row(values=[], options={}) Row.new(self, values, options) update_auto_fit_data @rows.last.cells yield @rows.last if block_given? @rows.last end # Adds a chart to this worksheets drawing. # @param [Class] chart_type # @option options [Array] start_at # @option options [Array] end_at # @option options [Cell, String] title def add_chart(chart_type, options={}) chart = drawing.add_chart(chart_type, options) yield chart if block_given? chart end # Serializes the worksheet document # @return [String] def to_xml builder = Nokogiri::XML::Builder.new(:encoding => ENCODING) do |xml| xml.worksheet(:xmlns => XML_NS, :'xmlns:r' => XML_NS_R) { if @auto_fit_data.size > 0 xml.cols { @auto_fit_data.each_with_index do |col, index| min_max = index+1 xml.col(:min=>min_max, :max=>min_max, :width => auto_width(col), :customWidth=>"true") end } end xml.sheetData { @rows.each do |row| row.to_xml(xml) end } xml.drawing :"r:id"=>"rId1" if @drawing } end builder.to_xml(:indent=>0, :save_with=>0) end # The worksheet's relationships. def relationships r = Relationships.new r << Relationship.new(DRAWING_R, "../#{@drawing.pn}") if @drawing r end private # assigns the owner workbook for this worksheet def workbook=(v) DataTypeValidator.validate "Worksheet.workbook", Workbook, v; @workbook = v; end # Updates auto fit data. # Autofit data attempts to determine the cell in a column that has the greatest width by comparing the length of the text multiplied by the size of the font. # @return [Array] of Cell objects # @param [Array] cells an array of cells def update_auto_fit_data(cells) styles = self.workbook.styles cellXfs, fonts = styles.cellXfs, styles.fonts sz = fonts[0].sz cells.each_with_index do |item, index| col = @auto_fit_data[index] || {:longest=>"", :sz=>sz} cell_xf = cellXfs[item.style] font = fonts[cell_xf.fontId || 0] sz = font.sz || sz if (col[:longest].scan(/./mu).size * col[:sz]) < (item.value.to_s.scan(/./mu).size * sz) col[:sz] = sz col[:longest] = item.value.to_s end @auto_fit_data[index] = col end cells end # Determines the proper width for a column based on content. # @note # From ECMA docs # Column width measured as the number of characters of the maximum digit width of the numbers 0 .. 9 as # rendered in the normal style's font. There are 4 pixels of margin padding (two on each side), plus 1 pixel padding for the gridlines. # width = Truncate([{Number of Characters} * {Maximum Digit Width} + {5 pixel padding}]/{Maximum Digit Width}*256)/256 # @return [Float] # @param [Hash] A hash of auto_fit_data def auto_width(col) mdw = 6.0 # maximum digit with is always 6.0 in testable fonts so instead of beating RMagick every time, I am hardcoding it here. mdw_count = 0 best_guess = 1.5 #direct testing shows the results of the documented formula to be a bit too small. This is a best guess scaling font_scale = col[:sz].to_f / (self.workbook.styles.fonts[0].sz.to_f || 11.0) col[:longest].scan(/./mu).each do |i| mdw_count +=1 if @magick_draw.get_type_metrics(i).width >= mdw end ((mdw_count * mdw + 5) / mdw * 256) / 256.0 * best_guess * font_scale end end end