lib/axlsx/workbook/worksheet/cell.rb in axlsx-2.0.1 vs lib/axlsx/workbook/worksheet/cell.rb in axlsx-2.1.0.pre
- old
+ new
@@ -28,21 +28,28 @@
# @option options [Symbol] vertAlign must be one of :baseline, :subscript, :superscript
# @option options [Integer] sz
# @option options [String] color an 8 letter rgb specification
# @option options [Number] formula_value The value to cache for a formula cell.
# @option options [Symbol] scheme must be one of :none, major, :minor
- def initialize(row, value="", options={})
- self.row=row
- @value = nil
- #@value = @font_name = @charset = @family = @b = @i = @strike = @outline = @shadow = nil
- #@formula_value = @condense = @u = @vertAlign = @sz = @color = @scheme = @extend = @ssti = nil
- @styles = row.worksheet.workbook.styles
- @row.cells << self
- parse_options options
- @style ||= 0
- @type ||= cell_type_from_value(value)
- @value = cast_value(value)
+ def initialize(row, value = nil, options = {})
+ @row = row
+ # Do not use instance vars if not needed to use less RAM
+ # And do not call parse_options on frequently used options
+ # to get less GC cycles
+ type = options.delete(:type) || cell_type_from_value(value)
+ self.type = type unless type == :string
+
+
+ val = options.delete(:style)
+ self.style = val unless val.nil? || val == 0
+ val = options.delete(:formula_value)
+ self.formula_value = val unless val.nil?
+
+ parse_options(options)
+
+ self.value = value
+ value.cell = self if contains_rich_text?
end
# this is the cached value for formula cells. If you want the values to render in iOS/Mac OSX preview
# you need to set this.
attr_accessor :formula_value
@@ -51,19 +58,24 @@
# TODO change this to a hash where each key defines attr name and validator (and any info the validator requires)
# then move it out to a module so we can re-use in in other classes.
# needs to define bla=(v) and bla methods on the class that hook into a
# set_attr method that kicks the suplied validator and updates the instance_variable
# for the key
- INLINE_STYLES = ['value', 'type', 'font_name', 'charset',
- 'family', 'b', 'i', 'strike','outline',
- 'shadow', 'condense', 'extend', 'u',
- 'vertAlign', 'sz', 'color', 'scheme']
+ INLINE_STYLES = [:value, :type, :font_name, :charset,
+ :family, :b, :i, :strike, :outline,
+ :shadow, :condense, :extend, :u,
+ :vertAlign, :sz, :color, :scheme].freeze
+
+ CELL_TYPES = [:date, :time, :float, :integer, :richtext,
+ :string, :boolean, :iso_8601].freeze
# The index of the cellXfs item to be applied to this cell.
# @return [Integer]
# @see Axlsx::Styles
- attr_reader :style
+ def style
+ defined?(@style) ? @style : 0
+ end
# The row this cell belongs to.
# @return [Row]
attr_reader :row
@@ -76,41 +88,48 @@
# @note
# If the value provided cannot be cast into the type specified, type is changed to :string and the following logic is applied.
# :string to :integer or :float, type conversions always return 0 or 0.0
# :string, :integer, or :float to :time conversions always return the original value as a string and set the cells type to :string.
# No support is currently implemented for parsing time strings.
- attr_reader :type
+ def type
+ defined?(@type) ? @type : :string
+ end
+
# @see type
def type=(v)
- RestrictionValidator.validate "Cell.type", [:date, :time, :float, :integer, :string, :boolean, :iso_8601], v
- @type=v
- self.value = @value unless @value.nil?
+ RestrictionValidator.validate :cell_type, CELL_TYPES, v
+ @type = v
+ self.value = @value unless !defined?(@value) || @value.nil?
end
-
# The value of this cell.
# @return [String, Integer, Float, Time, Boolean] casted value based on cell's type attribute.
attr_reader :value
+
# @see value
def value=(v)
#TODO: consider doing value based type determination first?
@value = cast_value(v)
end
# Indicates that the cell has one or more of the custom cell styles applied.
# @return [Boolean]
def is_text_run?
- @is_text_run ||= false
+ defined?(@is_text_run) && @is_text_run && !contains_rich_text?
+ end
+
+ def contains_rich_text?
+ type == :richtext
end
-
+
# Indicates if the cell is good for shared string table
def plain_string?
- @type == :string && # String typed
+ type == :string && # String typed
!is_text_run? && # No inline styles
!@value.nil? && # Not nil
!@value.empty? && # Not empty
- !@value.start_with?('=') # Not a formula
+ !@value.start_with?(?=) # Not a formula
end
# The inline font_name property for the cell
# @return [String]
attr_reader :font_name
@@ -229,38 +248,38 @@
# this must be one of [:baseline, :subscript, :superscript]
# @return [Symbol]
attr_reader :vertAlign
# @see vertAlign
def vertAlign=(v)
- RestrictionValidator.validate "Cell.vertAlign", [:baseline, :subscript, :superscript], v
+ RestrictionValidator.validate :cell_vertAlign, [:baseline, :subscript, :superscript], v
set_run_style nil, :vertAlign, v
end
# The inline scheme property for the cell
# this must be one of [:none, major, minor]
# @return [Symbol]
attr_reader :scheme
# @see scheme
def scheme=(v)
- RestrictionValidator.validate "Cell.schema", [:none, :major, :minor], v
+ RestrictionValidator.validate :cell_scheme, [:none, :major, :minor], v
set_run_style nil, :scheme, v
end
# The Shared Strings Table index for this cell
# @return [Integer]
attr_reader :ssti
# @return [Integer] The index of the cell in the containing row.
def index
- @row.cells.index(self)
+ @row.index(self)
end
# @return [String] The alpha(column)numeric(row) reference for this sell.
# @example Relative Cell Reference
# ws.rows.first.cells.first.r #=> "A1"
def r
- Axlsx::cell_r index, @row.index
+ Axlsx::cell_r index, @row.row_index
end
# @return [String] The absolute alpha(column)numeric(row) reference for this sell.
# @example Absolute Cell Reference
# ws.rows.first.cells.first.r #=> "$A$1"
@@ -270,30 +289,30 @@
# @return [Integer] The cellXfs item index applied to this cell.
# @raise [ArgumentError] Invalid cellXfs id if the value provided is not within cellXfs items range.
def style=(v)
Axlsx::validate_unsigned_int(v)
- count = @styles.cellXfs.size
+ count = styles.cellXfs.size
raise ArgumentError, "Invalid cellXfs id" unless v < count
@style = v
end
- # @return [Array] of x/y coordinates in the cheet for this cell.
+ # @return [Array] of x/y coordinates in the sheet for this cell.
def pos
- [index, row.index]
+ [index, row.row_index]
end
# Merges all the cells in a range created between this cell and the cell or string name for a cell provided
# @see worksheet.merge_cells
# @param [Cell, String] target The last cell, or str ref for the cell in the merge range
def merge(target)
- range_end = if target.is_a?(String)
- target
- elsif(target.is_a?(Cell))
- target.r
- end
- self.row.worksheet.merge_cells "#{self.r}:#{range_end}" unless range_end.nil?
+ start, stop = if target.is_a?(String)
+ [self.r, target]
+ elsif(target.is_a?(Cell))
+ Axlsx.sort_cells([self, target]).map { |c| c.r }
+ end
+ self.row.worksheet.merge_cells "#{start}:#{stop}" unless stop.nil?
end
# Serializes the cell
# @param [Integer] r_index The row index for the cell
# @param [Integer] c_index The cell index in the row.
@@ -302,21 +321,15 @@
def to_xml_string(r_index, c_index, str = '')
CellSerializer.to_xml_string r_index, c_index, self, str
end
def is_formula?
- @type == :string && @value.to_s.start_with?('=')
+ type == :string && @value.to_s.start_with?(?=)
end
- # This is still not perfect...
- # - scaling is not linear as font sizes increst
- # - different fonts have different mdw and char widths
- def autowidth
- return if is_formula? || value == nil
- mdw = 1.78 #This is the widest width of 0..9 in arial@10px)
- font_scale = (font_size/10.0).to_f
- ((value.to_s.count(Worksheet.thin_chars) * mdw + 5) / mdw * 256) / 256.0 * font_scale
+ def is_array_formula?
+ type == :string && @value.to_s.start_with?('{=') && @value.to_s.end_with?('}')
end
# returns the absolute or relative string style reference for
# this cell.
# @param [Boolean] absolute -when false a relative reference will be
@@ -324,38 +337,83 @@
# @return [String]
def reference(absolute=true)
absolute ? r_abs : r
end
+ # Creates a defined name in the workbook for this cell.
+ def name=(label)
+ row.worksheet.workbook.add_defined_name "#{row.worksheet.name}!#{r_abs}", name: label
+ @name = label
+ end
+
+ # returns the name of the cell
+ attr_reader :name
+
+ def autowidth
+ return if is_formula? || value.nil?
+ if contains_rich_text?
+ string_width('', font_size) + value.autowidth
+ elsif styles.cellXfs[style].alignment && styles.cellXfs[style].alignment.wrap_text
+ max_width = 0
+ value.to_s.split(/\r?\n/).each do |line|
+ width = string_width(line, font_size)
+ max_width = width if width > max_width
+ end
+ max_width
+ else
+ string_width(value, font_size)
+ end
+ end
+
+ # Returns the sanatized value
+ # TODO find a better way to do this as it accounts for 30% of
+ # processing time in benchmarking...
+ def clean_value
+ if type == :string && !Axlsx::trust_input
+ Axlsx::sanitize(::CGI.escapeHTML(@value.to_s))
+ else
+ @value.to_s
+ end
+ end
+
private
+
+ def styles
+ row.worksheet.styles
+ end
+
+ # Returns the width of a string according to the current style
+ # This is still not perfect...
+ # - scaling is not linear as font sizes increase
+ def string_width(string, font_size)
+ font_scale = font_size / 10.0
+ (string.to_s.count(Worksheet::THIN_CHARS) + 3.0) * (font_size/10.0)
+ end
# we scale the font size if bold style is applied to either the style font or
# the cell itself. Yes, it is a bit of a hack, but it is much better than using
# imagemagick and loading metrics for every character.
def font_size
- font = @styles.fonts[@styles.cellXfs[style].fontId] || @styles.fonts[0]
- size_from_styles = (font.b || b) ? font.sz * 1.5 : font.sz
- sz || size_from_styles
+ return sz if sz
+ font = styles.fonts[styles.cellXfs[style].fontId] || styles.fonts[0]
+ (font.b || (defined?(@b) && @b)) ? (font.sz * 1.5) : font.sz
end
# Utility method for setting inline style attributes
- def set_run_style( validator, attr, value)
- return unless INLINE_STYLES.include?(attr.to_s)
- Axlsx.send(validator, value) unless validator == nil
+ def set_run_style(validator, attr, value)
+ return unless INLINE_STYLES.include?(attr.to_sym)
+ Axlsx.send(validator, value) unless validator.nil?
self.instance_variable_set :"@#{attr.to_s}", value
@is_text_run = true
end
# @see ssti
def ssti=(v)
Axlsx::validate_unsigned_int(v)
@ssti = v
end
- # assigns the owning row for this cell.
- def row=(v) @row=v end
-
# Determines the cell type based on the cell value.
# @note This is only used when a cell is created but no :type option is specified, the following rules apply:
# 1. If the value is an instance of Date, the type is set to :date
# 2. If the value is an instance of Time, the type is set to :time
# 3. If the value is an instance of TrueClass or FalseClass, the type is set to :boolean
@@ -367,49 +425,46 @@
:date
elsif v.is_a?(Time)
:time
elsif v.is_a?(TrueClass) || v.is_a?(FalseClass)
:boolean
- elsif v.to_s =~ /\A[+-]?\d+?\Z/ #numeric
+ elsif v.to_s =~ Axlsx::NUMERIC_REGEX
:integer
- elsif v.to_s =~ /\A[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\Z/ #float
+ elsif v.to_s =~ Axlsx::FLOAT_REGEX
:float
- # \A(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9])
- # T(2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?
- # (Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?\Z
- elsif v.to_s =~/\A(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9])T(2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?\Z/
+ elsif v.to_s =~ Axlsx::ISO_8601_REGEX
:iso_8601
+ elsif v.is_a? RichText
+ :richtext
else
:string
end
end
# Cast the value into this cells data type.
# @note
# About Time - Time in OOXML is *different* from what you might expect. The history as to why is interesting, but you can safely assume that if you are generating docs on a mac, you will want to specify Workbook.1904 as true when using time typed values.
# @see Axlsx#date1904
def cast_value(v)
- return nil if v.nil?
- if @type == :date
+ return v if v.is_a?(RichText) || v.nil?
+ case type
+ when :date
self.style = STYLE_DATE if self.style == 0
v
- elsif (@type == :time && v.is_a?(Time)) || (@type == :time && v.respond_to?(:to_time))
+ when :time
self.style = STYLE_DATE if self.style == 0
v.respond_to?(:to_time) ? v.to_time : v
- elsif @type == :float
+ when :float
v.to_f
- elsif @type == :integer
+ when :integer
v.to_i
- elsif @type == :boolean
+ when :boolean
v ? 1 : 0
- elsif @type == :iso_8601
+ when :iso_8601
#consumer is responsible for ensuring the iso_8601 format when specifying this type
v
else
- @type = :string
- # TODO find a better way to do this as it accounts for 30% of
- # processing time in benchmarking...
- Axlsx::trust_input ? v.to_s : ::CGI.escapeHTML(v.to_s)
+ v.to_s
end
end
end
end