# @markup markdown # @author Jakub Tesinsky # @title rspreadsheet Cell require 'andand' require 'rspreadsheet/xml_tied_item' require 'date' require 'time' # extended functions for time like Time.strptime require 'bigdecimal' require 'bigdecimal/util' # for to_d method require 'helpers/class_extensions' require 'rspreadsheet/cell_format' # CellFormat and Border classes module Rspreadsheet using ClassExtensions if RUBY_VERSION > '2.1' StartOfEpoch = Time.new(1899,12,30,0,0,0,0) ### # Represents a cell in spreadsheet which has coordinates, contains value, formula and can be formated. # You can get this object like this (suppose that @worksheet contains {Rspreadsheet::Worksheet} object) # # @worksheet.cells(5,2) # # Note that when using syntax like `@worksheet[5,2]` or `@worksheet.B5` you won't get this object, but rather the value of the cell. # More precisely it is equvalient to @worksheet.cells(5,2).value. Brief overview can be faound at [README] # class Cell < XMLTiedItem # RSpreadsheet::Worksheet in which the cell is contained. attr_accessor :worksheet # Row index of a cell. If you want to access the row object, see #row. attr_reader :rowi # @!group XMLTiedItem related methods and extensions def parent; row end #TODO: otazka zda je treba definovat zde, mozna uz je to v XMLTiedItem osefovano def coli; index end def set_rowi(arowi); @rowi = arowi end # this should ONLY be used by parent row def initialize(aworksheet,arowi,acoli) raise "First parameter should be Worksheet object not #{aworksheet.class}" unless aworksheet.kind_of?(Rspreadsheet::Worksheet) @worksheet = aworksheet @rowi = arowi initialize_xml_tied_item(row,acoli) end def row; @worksheet.rows(rowi) end def coordinates; [rowi,coli] end def address Tools.convert_cell_coordinates_to_address(coordinates) end def to_s; value.to_s end def valuexml; self.valuexmlnode.andand.inner_xml end def valuexmlnode; self.xmlnode.elements.first end # use this to find node in cell xml. ex. xmlfind('.//text:a') finds all link nodes def valuexmlfindall(path) valuexmlnode.nil? ? [] : valuexmlnode.find(path) end def valuexmlfindfirst(path) valuexmlfindall(path).first end def inspect "#" end def value gt = guess_cell_type amode = self.mode if (amode == :regular) or (amode == :repeated) case when gt == nil then nil when gt == Float then xmlnode.attributes['value'].to_f when gt == String then xmlnode.elements.first.andand.content.to_s when gt == :datetime then datetime_value when gt == :time then time_value when gt == :percentage then xmlnode.attributes['value'].to_f when gt == :currency then xmlnode.attributes['value'].to_d end elsif amode == :outbound nil else raise "Unknown cell mode #{amode}" end end ## according to http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1417674_253892949 ## the value od time-value is in a "duration" format defined here https://www.w3.org/TR/xmlschema-2/#duration ## this method converts the time-value to Time object. Note that it does not check if the cell is in time-value ## or not, this is the responibility of caller. However beware that specification does not specify how the time ## should be interpreted. By observing LibreOffice behaviour, I have found these options ## 1. "Time only cell" has time is stored as PT16H22M35S (16:22:35) where the duration is duration from midnight. ## Because ruby does NOT have TimeOfDay type we need to represent that as DateTime. I have chosen 1899-12-30 00:00:00 as ## StartOfEpoch time, because it plays well with case 2. ## 2. "DateTime converted to Time only cell" has time stored as PT923451H33M00.000000168S (15:33:00 with date part 2005-05-05 ## before conversion to time only). It is strange format which seems to have hours meaning number of hours after 1899-12-30 00:00:00 ## ## Returns time-value of the cell. It does not check if cell has or should have this value, it is responibility of caller to do so. def time_value Cell.parse_time_value(xmlnode.attributes['time-value'].to_s) end def self.parse_time_value(svalue) if (m = /^PT((?[0-9]+)H)?((?[0-9]+)M)?((?[0-9]+(\.[0-9]+)?)S)$/.match(svalue.delete(' '))) # time was parsed manually (StartOfEpoch + m[:hours].to_i*60*60 + m[:minutes].to_i*60 + m[:seconds].to_f.round(5)) #BASTL: Rounding is here because LibreOffice adds some fractions of seconds randomly else begin Time.strptime(svalue, InternalTimeFormat) rescue Time.parse(svalue) # maybe add defaults for year-mont-day end end end def datetime_value vs = xmlnode.attributes['date-value'].to_s begin DateTime.strptime(vs, InternalDateTimeFormat) rescue begin DateTime.strptime(vs, InternalDateFormat) rescue DateTime.parse(vs) end end end def value=(avalue) detach_if_needed if self.mode == :regular gt = guess_cell_type(avalue) # raise 'here'+gt.to_s if avalue == 666.66 case when gt == nil then raise 'This value type is not storable to cell' when gt == Float then remove_all_value_attributes_and_content set_type_attribute('float') sav=avalue.to_f.to_s # to_f handles case when avalue is decimal number Tools.set_ns_attribute(xmlnode,'office','value', sav) xmlnode << Tools.prepare_ns_node('text','p', sav) when gt == String then remove_all_value_attributes_and_content set_type_attribute('string') xmlnode << Tools.prepare_ns_node('text','p', avalue.to_s) when gt == :datetime then remove_all_value_attributes_and_content set_type_attribute('date') if avalue.kind_of?(DateTime) or avalue.kind_of?(Date) or avalue.kind_of?(Time) avalue = avalue.strftime(InternalDateTimeFormat) Tools.set_ns_attribute(xmlnode,'office','date-value', avalue) xmlnode << Tools.prepare_ns_node('text','p', avalue) end when gt == :time then remove_all_value_attributes_and_content set_type_attribute('time') if avalue.kind_of?(DateTime) or avalue.kind_of?(Date) or avalue.kind_of?(Time) Tools.set_ns_attribute(xmlnode,'office','time-value', avalue.strftime(InternalTimeFormat)) xmlnode << Tools.prepare_ns_node('text','p', avalue.strftime('%H:%M')) end when gt == :percentage then remove_all_value_attributes_and_content set_type_attribute('percentage') Tools.set_ns_attribute(xmlnode,'office','value', ('%0.2d' % avalue.to_f)+'%') xmlnode << Tools.prepare_ns_node('text','p', (avalue.to_f*100).round.to_s+'%') when gt == :currency then remove_all_value_attributes_and_content set_type_attribute('currency') unless avalue.nil? Tools.set_ns_attribute(xmlnode,'office','value', '%f' % avalue.to_d) xmlnode << Tools.prepare_ns_node('text','p', avalue.to_d.to_s+' '+self.format.currency) end end else raise "Unknown cell mode #{self.mode}" end end def set_type_attribute(typestring) Tools.set_ns_attribute(xmlnode,'office','value-type',typestring) Tools.set_ns_attribute(xmlnode,'calcext','value-type',typestring) end ## TODO: using this is NOT in line with the general intent of forward compatibility def remove_all_value_attributes_and_content(node=xmlnode) if att = Tools.get_ns_attribute(node, 'office','value') then att.remove! end if att = Tools.get_ns_attribute(node, 'office','date-value') then att.remove! end if att = Tools.get_ns_attribute(node, 'office','time-value') then att.remove! end if att = Tools.get_ns_attribute(node, 'table','formula') then att.remove! end node.content='' end def remove_all_type_attributes set_type_attribute(nil) end def relative(rowdiff,coldiff) @worksheet.cells(self.rowi+rowdiff, self.coli+coldiff) end def type gct = guess_cell_type case when gct == Float then :float when gct == String then :string when gct == :datetime then :datetime when gct == :time then :time when gct == :percentage then :percentage when gct == :unassigned then :unassigned when gct == :currency then :currency when gct == NilClass then :empty when gct == nil then :unknown else :unknown end end def guess_cell_type(avalue=nil) # try guessing by value valueguess = case avalue when Numeric then Float when Time then :time when Date, DateTime then :datetime when String,nil then nil else nil end result = valueguess if valueguess.nil? # valueguess is most important if not succesfull then try guessing by type from node xml typ = xmlnode.nil? ? 'N/A' : xmlnode.attributes['value-type'] typeguess = case typ when nil then nil when 'float' then Float when 'string' then String when 'time' then :time when 'date' then :datetime when 'percentage' then :percentage when 'N/A' then :unassigned when 'currency' then :currency else if xmlnode.elements.size == 0 nil else raise "Unknown type at #{coordinates.to_s} from #{xmlnode.to_s} / elements size=#{xmlnode.elements.size.to_s} / type=#{xmlnode.attributes['value-type'].to_s}" end end result = if !typeguess.nil? # if not certain by value, but have a typeguess if !avalue.nil? # with value we may try converting if (typeguess(avalue) rescue false) # if convertible then it is typeguess typeguess elsif (String(avalue) rescue false) # otherwise try string String else # if not convertible to anything concious then nil nil end else # without value we just beleive typeguess typeguess end else # it not have a typeguess if (avalue.nil?) # if nil then nil NilClass elsif (String(avalue) rescue false) # convertible to String String else # giving up nil end end elsif valueguess == Float case xmlnode.andand.attributes['value-type'] when 'percentage' then result = :percentage when 'currency' then result = :currency end end result end def format @format ||= CellFormat.new(self) end def formula rawformula = Tools.get_ns_attribute(xmlnode,'table','formula',nil).andand.value if rawformula.nil? nil elsif rawformula.match(/^of:(.*)$/) $1 else raise "Mischmatched value in table:formula attribute - does not start with of: (#{rawformula.to_s})" end end def formula=(formulastring) detach_if_needed raise 'Formula string must begin with "=" character' unless formulastring[0,1] == '=' remove_all_value_attributes_and_content(xmlnode) remove_all_type_attributes Tools.set_ns_attribute(xmlnode,'table','formula','of:'+formulastring.to_s) end def blank?; self.type==:empty or self.type==:unassigned end def border_top; format.border_top end def border_right; format.border_right end def border_bottom; format.border_bottom end def border_left; format.border_left end private InternalDateFormat = '%Y-%m-%d' InternalDateTimeFormat = '%FT%T' InternalTimeFormat = 'PT%HH%MM%SS' end ## Cell end # module