diff options
| author | Alex Rothenberg <[email protected]> | 2012-11-27 09:53:30 -0500 |
|---|---|---|
| committer | Alex Rothenberg <[email protected]> | 2012-11-27 09:54:23 -0500 |
| commit | 4560bd0a1b8b46bf4d8c0783f9fa12e8ceee714f (patch) | |
| tree | f5e6d539c7b4962442509ab6a09b2b4c42e8eaa5 /lib | |
| parent | 2feca4f74f21e6a3e63bd3badd02267be4062047 (diff) | |
| download | caxlsx-4560bd0a1b8b46bf4d8c0783f9fa12e8ceee714f.tar.gz caxlsx-4560bd0a1b8b46bf4d8c0783f9fa12e8ceee714f.zip | |
Create a simple Pivot Table
* an example can be run with `ruby examples/pivot_table.rb`
* right now you cannot set options on the pivot table to make it useful (coming soon...)
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/axlsx/package.rb | 13 | ||||
| -rw-r--r-- | lib/axlsx/rels/relationship.rb | 1 | ||||
| -rw-r--r-- | lib/axlsx/util/constants.rb | 21 | ||||
| -rw-r--r-- | lib/axlsx/util/validators.rb | 5 | ||||
| -rw-r--r-- | lib/axlsx/workbook/workbook.rb | 28 | ||||
| -rw-r--r-- | lib/axlsx/workbook/worksheet/pivot_table.rb | 121 | ||||
| -rw-r--r-- | lib/axlsx/workbook/worksheet/pivot_table_cache_definition.rb | 63 | ||||
| -rw-r--r-- | lib/axlsx/workbook/worksheet/pivot_tables.rb | 24 | ||||
| -rw-r--r-- | lib/axlsx/workbook/worksheet/worksheet.rb | 33 |
9 files changed, 292 insertions, 17 deletions
diff --git a/lib/axlsx/package.rb b/lib/axlsx/package.rb index e3981d62..df87ed12 100644 --- a/lib/axlsx/package.rb +++ b/lib/axlsx/package.rb @@ -192,6 +192,12 @@ module Axlsx workbook.tables.each do |table| parts << {:entry => "xl/#{table.pn}", :doc => table.to_xml_string, :schema => SML_XSD} end + workbook.pivot_tables.each do |pivot_table| + cache_definition = pivot_table.cache_definition + parts << {:entry => "xl/#{pivot_table.rels_pn}", :doc => pivot_table.relationships.to_xml_string, :schema => RELS_XSD} + parts << {:entry => "xl/#{pivot_table.pn}", :doc => pivot_table.to_xml_string} #, :schema => SML_XSD} + parts << {:entry => "xl/#{cache_definition.pn}", :doc => cache_definition.to_xml_string} #, :schema => SML_XSD} + end workbook.comments.each do|comment| if comment.size > 0 @@ -255,6 +261,13 @@ module Axlsx :ContentType => TABLE_CT) end + workbook.pivot_tables.each do |pivot_table| + c_types << Axlsx::Override.new(:PartName => "/xl/#{pivot_table.pn}", + :ContentType => PIVOT_TABLE_CT) + c_types << Axlsx::Override.new(:PartName => "/xl/#{pivot_table.cache_definition.pn}", + :ContentType => PIVOT_TABLE_CACHE_DEFINITION_CT) + end + workbook.comments.each do |comment| if comment.size > 0 c_types << Axlsx::Override.new(:PartName => "/xl/#{comment.pn}", diff --git a/lib/axlsx/rels/relationship.rb b/lib/axlsx/rels/relationship.rb index 99c8e112..b19a18f7 100644 --- a/lib/axlsx/rels/relationship.rb +++ b/lib/axlsx/rels/relationship.rb @@ -12,6 +12,7 @@ module Axlsx # @note Supported types are defined as constants in Axlsx: # @see XML_NS_R # @see TABLE_R + # @see PIVOT_TABLE_R # @see WORKBOOK_R # @see WORKSHEET_R # @see APP_R diff --git a/lib/axlsx/util/constants.rb b/lib/axlsx/util/constants.rb index 66434b87..437de6db 100644 --- a/lib/axlsx/util/constants.rb +++ b/lib/axlsx/util/constants.rb @@ -51,6 +51,10 @@ module Axlsx # table rels namespace TABLE_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" + # pivot table rels namespace + PIVOT_TABLE_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + PIVOT_TABLE_CACHE_DEFINITION_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" + # workbook rels namespace WORKBOOK_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" @@ -99,6 +103,12 @@ module Axlsx # table content type TABLE_CT = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" + # pivot table content type + PIVOT_TABLE_CT = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + + # pivot table cache definition content type + PIVOT_TABLE_CACHE_DEFINITION_CT = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" + # workbook content type WORKBOOK_CT = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" @@ -208,6 +218,13 @@ module Axlsx # drawing part TABLE_PN = "tables/table%d.xml" + # pivot table parts + PIVOT_TABLE_PN = "pivotTables/pivotTable%d.xml" + PIVOT_TABLE_CACHE_DEFINITION_PN = "pivotCache/pivotCacheDefinition%d.xml" + + # pivot table rels parts + PIVOT_TABLE_RELS_PN = "pivotTables/_rels/pivotTable%d.xml.rels" + # chart part CHART_PN = "charts/chart%d.xml" @@ -320,7 +337,7 @@ module Axlsx # x1E Information Separator Two # x1F Information Separator One # - # The following are not dealt with. + # The following are not dealt with. # If you have this in your data, expect excel to blow up! # # x7F Delete @@ -365,7 +382,7 @@ module Axlsx # @see http://www.codetable.net/asciikeycodes pattern = "[\x0-\x08\x0B\x0C\x0E-\x1F]" pattern= pattern.respond_to?(:encode) ? pattern.encode('UTF-8') : pattern - + # The regular expression used to remove control characters from worksheets CONTROL_CHAR_REGEX = Regexp.new(pattern, 'n') diff --git a/lib/axlsx/util/validators.rb b/lib/axlsx/util/validators.rb index 9c1ec741..03f0c89a 100644 --- a/lib/axlsx/util/validators.rb +++ b/lib/axlsx/util/validators.rb @@ -41,6 +41,7 @@ module Axlsx raise ArgumentError, (ERR_REGEX % [v.inspect, regex.to_s]) unless (v.respond_to?(:to_s) && v.to_s.match(regex)) end end + # Validate that the class of the value provided is either an instance or the class of the allowed types and that any specified additional validation returns true. class DataTypeValidator # Perform validation @@ -221,14 +222,14 @@ module Axlsx # TABLE_CT, WORKBOOK_CT, APP_CT, RELS_CT, STYLES_CT, XML_CT, WORKSHEET_CT, SHARED_STRINGS_CT, CORE_CT, CHART_CT, DRAWING_CT, COMMENT_CT are allowed # @param [Any] v The value validated def self.validate_content_type(v) - RestrictionValidator.validate :content_type, [TABLE_CT, WORKBOOK_CT, APP_CT, RELS_CT, STYLES_CT, XML_CT, WORKSHEET_CT, SHARED_STRINGS_CT, CORE_CT, CHART_CT, JPEG_CT, GIF_CT, PNG_CT, DRAWING_CT, COMMENT_CT, VML_DRAWING_CT], v + RestrictionValidator.validate :content_type, [TABLE_CT, WORKBOOK_CT, APP_CT, RELS_CT, STYLES_CT, XML_CT, WORKSHEET_CT, SHARED_STRINGS_CT, CORE_CT, CHART_CT, JPEG_CT, GIF_CT, PNG_CT, DRAWING_CT, COMMENT_CT, VML_DRAWING_CT, PIVOT_TABLE_CT, PIVOT_TABLE_CACHE_DEFINITION_CT], v end # Requires that the value is a valid relationship_type # XML_NS_R, TABLE_R, WORKBOOK_R, WORKSHEET_R, APP_R, RELS_R, CORE_R, STYLES_R, CHART_R, DRAWING_R, IMAGE_R, HYPERLINK_R, SHARED_STRINGS_R are allowed # @param [Any] v The value validated def self.validate_relationship_type(v) - RestrictionValidator.validate :relationship_type, [XML_NS_R, TABLE_R, WORKBOOK_R, WORKSHEET_R, APP_R, RELS_R, CORE_R, STYLES_R, CHART_R, DRAWING_R, IMAGE_R, HYPERLINK_R, SHARED_STRINGS_R, COMMENT_R, VML_DRAWING_R, COMMENT_R_NULL], v + RestrictionValidator.validate :relationship_type, [XML_NS_R, TABLE_R, WORKBOOK_R, WORKSHEET_R, APP_R, RELS_R, CORE_R, STYLES_R, CHART_R, DRAWING_R, IMAGE_R, HYPERLINK_R, SHARED_STRINGS_R, COMMENT_R, VML_DRAWING_R, COMMENT_R_NULL, PIVOT_TABLE_R, PIVOT_TABLE_CACHE_DEFINITION_R], v end # Requires that the value is a valid table element type diff --git a/lib/axlsx/workbook/workbook.rb b/lib/axlsx/workbook/workbook.rb index c9556c4d..a3c42743 100644 --- a/lib/axlsx/workbook/workbook.rb +++ b/lib/axlsx/workbook/workbook.rb @@ -39,6 +39,9 @@ require 'axlsx/workbook/defined_names.rb' require 'axlsx/workbook/worksheet/table_style_info.rb' require 'axlsx/workbook/worksheet/table.rb' require 'axlsx/workbook/worksheet/tables.rb' +require 'axlsx/workbook/worksheet/pivot_table_cache_definition.rb' +require 'axlsx/workbook/worksheet/pivot_table.rb' +require 'axlsx/workbook/worksheet/pivot_tables.rb' require 'axlsx/workbook/worksheet/data_validation.rb' require 'axlsx/workbook/worksheet/data_validations.rb' require 'axlsx/workbook/worksheet/sheet_view.rb' @@ -120,10 +123,17 @@ require 'axlsx/workbook/worksheet/selection.rb' # @return [SimpleTypedList] attr_reader :tables + # A colllection of pivot tables associated with this workbook + # @note The recommended way to manage drawings is Worksheet#add_table + # @see Worksheet#add_table + # @see Table + # @return [SimpleTypedList] + attr_reader :pivot_tables + # A collection of defined names for this workbook # @note The recommended way to manage defined names is Workbook#add_defined_name - # @see DefinedName + # @see DefinedName # @return [DefinedNames] def defined_names @defined_names ||= DefinedNames.new @@ -169,7 +179,7 @@ require 'axlsx/workbook/worksheet/selection.rb' # w.parse_string :date1904, "//xmlns:workbookPr/@date1904" # w #end - + # Creates a new Workbook # The recomended way to work with workbooks is via Package#workbook # @option options [Boolean] date1904. If this is not specified, date1904 is set to false. Office 2011 for Mac defaults to false. @@ -181,6 +191,7 @@ require 'axlsx/workbook/worksheet/selection.rb' @images = SimpleTypedList.new Pic # Are these even used????? Check package serialization parts @tables = SimpleTypedList.new Table + @pivot_tables = SimpleTypedList.new PivotTable @comments = SimpleTypedList.new Comments @@ -216,7 +227,7 @@ require 'axlsx/workbook/worksheet/selection.rb' def use_autowidth=(v=true) Axlsx::validate_boolean v; @use_autowidth = v; end # inserts a worksheet into this workbook at the position specified. - # It the index specified is out of range, the worksheet will be added to the end of the + # It the index specified is out of range, the worksheet will be added to the end of the # worksheets collection # @return [Worksheet] # @param index The zero based position to insert the newly created worksheet @@ -258,6 +269,9 @@ require 'axlsx/workbook/worksheet/selection.rb' @worksheets.each do |sheet| r << Relationship.new(WORKSHEET_R, WORKSHEET_PN % (r.size+1)) end + pivot_tables.each_with_index do |pivot_table, index| + r << Relationship.new(PIVOT_TABLE_CACHE_DEFINITION_R, PIVOT_TABLE_CACHE_DEFINITION_PN % (index+1)) + end r << Relationship.new(STYLES_R, STYLES_PN) if use_shared_strings r << Relationship.new(SHARED_STRINGS_R, SHARED_STRINGS_PN) @@ -298,6 +312,14 @@ require 'axlsx/workbook/worksheet/selection.rb' end end str << '</sheets>' + unless pivot_tables.empty? + str << '<pivotCaches>' + pivot_tables.each_with_index do |pivot_table, index| + rId = "rId#{@worksheets.size + index + 1 }" + str << '<pivotCache cacheId="' << pivot_table.cache_definition.cache_id.to_s << '" r:id="' << rId << '"/>' + end + str << '</pivotCaches>' + end defined_names.to_xml_string(str) str << '</workbook>' end diff --git a/lib/axlsx/workbook/worksheet/pivot_table.rb b/lib/axlsx/workbook/worksheet/pivot_table.rb new file mode 100644 index 00000000..c4eff8be --- /dev/null +++ b/lib/axlsx/workbook/worksheet/pivot_table.rb @@ -0,0 +1,121 @@ +# encoding: UTF-8 +module Axlsx + # Table + # @note Worksheet#add_pivot_table is the recommended way to create tables for your worksheets. + # @see README for examples + class PivotTable + + include Axlsx::OptionsParser + + # Creates a new PivotTable object + # @param [String] ref The reference to where the pivot table lives like 'G4:L17'. + # @param [String] range The reference to the pivot table data like 'A1:D31'. + # @param [Worksheet] sheet The sheet containing the table data. + # @option options [Cell, String] name + # @option options [TableStyle] style + def initialize(ref, range, sheet, options={}) + @ref = ref + self.range = range + @sheet = sheet + @sheet.workbook.pivot_tables << self + @name = "PivotTable#{index+1}" + parse_options options + yield self if block_given? + end + + # The reference to the table data + # @return [String] + attr_reader :ref + + # The name of the table. + # @return [String] + attr_reader :name + + # The name of the sheet. + # @return [String] + attr_reader :sheet + + # The range where the data for this pivot table lives. + # @return [String] + attr_reader :range + + def range=(v) + DataTypeValidator.validate "#{self.class}.range", [String], v + if v.is_a?(String) + @range = v + end + end + + # The index of this chart in the workbooks charts collection + # @return [Integer] + def index + @sheet.workbook.pivot_tables.index(self) + end + + # The part name for this table + # @return [String] + def pn + "#{PIVOT_TABLE_PN % (index+1)}" + end + + # The relationship part name of this pivot table + # @return [String] + def rels_pn + "#{PIVOT_TABLE_RELS_PN % (index+1)}" + end + + def header_cells_count + header_cells.count + end + + def cache_definition + @cache_definition ||= PivotTableCacheDefinition.new(self) + end + + # The worksheet relationships. This is managed automatically by the worksheet + # @return [Relationships] + def relationships + r = Relationships.new + r << Relationship.new(PIVOT_TABLE_CACHE_DEFINITION_R, "../#{cache_definition.pn}") + r + end + + # identifies the index of an object withing the collections used in generating relationships for the worksheet + # @param [Any] object the object to search for + # @return [Integer] The index of the object + def relationships_index_of(object) + objects = [cache_definition] + objects.index(object) + end + + # The relation reference id for this table + # @return [String] + def rId + "rId#{index+1}" + end + + # Serializes the object + # @param [String] str + # @return [String] + def to_xml_string(str = '') + str << '<?xml version="1.0" encoding="UTF-8"?>' + str << '<pivotTableDefinition xmlns="' << XML_NS << '" name="' << name << '" cacheId="' << cache_definition.cache_id.to_s << '" dataOnRows="1" applyNumberFormats="0" applyBorderFormats="0" applyFontFormats="0" applyPatternFormats="0" applyAlignmentFormats="0" applyWidthHeightFormats="1" dataCaption="Data" showMultipleLabel="0" showMemberPropertyTips="0" useAutoFormatting="1" indent="0" compact="0" compactData="0" gridDropZones="1" multipleFieldFilters="0">' + str << '<location firstDataCol="1" firstDataRow="1" firstHeaderRow="1" ref="' << ref << '"/>' + str << '<pivotFields count="' << header_cells_count.to_s << '">' + header_cells_count.times do + str << '<pivotField compact="0" outline="0" subtotalTop="0" showAll="0" includeNewItemsInFilter="1"/>' + end + str << '</pivotFields>' + str << '</pivotTableDefinition>' + end + + private + + # get the header cells (hackish) + def header_cells + header = range.gsub(/^(\w+?)(\d+)\:(\w+?)\d+$/, '\1\2:\3\2') + @sheet[header] + end + + end +end diff --git a/lib/axlsx/workbook/worksheet/pivot_table_cache_definition.rb b/lib/axlsx/workbook/worksheet/pivot_table_cache_definition.rb new file mode 100644 index 00000000..e70bcc6a --- /dev/null +++ b/lib/axlsx/workbook/worksheet/pivot_table_cache_definition.rb @@ -0,0 +1,63 @@ +# encoding: UTF-8 +module Axlsx + # Table + # @note Worksheet#add_pivot_table is the recommended way to create tables for your worksheets. + # @see README for examples + class PivotTableCacheDefinition + + include Axlsx::OptionsParser + + # Creates a new PivotTable object + # @param [String] pivot_table The pivot table this cache definition is in + def initialize(pivot_table) + @pivot_table = pivot_table + end + + # # The reference to the pivot table data + # # @return [PivotTable] + attr_reader :pivot_table + + # The index of this chart in the workbooks charts collection + # @return [Integer] + def index + pivot_table.sheet.workbook.pivot_tables.index(pivot_table) + end + + # The part name for this table + # @return [String] + def pn + "#{PIVOT_TABLE_CACHE_DEFINITION_PN % (index+1)}" + end + + def cache_id + index + end + + # The relation reference id for this table + # @return [String] + def rId + "rId#{index + 1}" + end + + # Serializes the object + # @param [String] str + # @return [String] + def to_xml_string(str = '') + str << '<?xml version="1.0" encoding="UTF-8"?>' + str << '<pivotCacheDefinition xmlns="' << XML_NS << '" xmlns:r="' << XML_NS_R << '" invalid="1" refreshOnLoad="1" recordCount="0">' + str << '<cacheSource type="worksheet">' + str << '<worksheetSource ref="' << pivot_table.range << '" sheet="Data Sheet"/>' + str << '</cacheSource>' + str << '<cacheFields count="' << pivot_table.header_cells_count.to_s << '">' + pivot_table.header_cells_count.times do |i| + str << '<cacheField name="placeholder_' << i.to_s << '" numFmtId="0">' + str << '<sharedItems count="0">' + str << '</sharedItems>' + str << '</cacheField>' + end + str << '</cacheFields>' + str << '</pivotCacheDefinition>' + end + + end +end diff --git a/lib/axlsx/workbook/worksheet/pivot_tables.rb b/lib/axlsx/workbook/worksheet/pivot_tables.rb new file mode 100644 index 00000000..f5625fc0 --- /dev/null +++ b/lib/axlsx/workbook/worksheet/pivot_tables.rb @@ -0,0 +1,24 @@ +module Axlsx + + # A simple, self serializing class for storing pivot tables + class PivotTables < SimpleTypedList + + # creates a new Tables object + def initialize(worksheet) + raise ArgumentError, "you must provide a worksheet" unless worksheet.is_a?(Worksheet) + super PivotTable + @worksheet = worksheet + end + + # The worksheet that owns this collection of pivot tables + # @return [Worksheet] + attr_reader :worksheet + + # returns the relationships required by this collection + def relationships + return [] if empty? + map{ |pivot_table| Relationship.new(PIVOT_TABLE_R, "../#{pivot_table.pn}") } + end + end + +end diff --git a/lib/axlsx/workbook/worksheet/worksheet.rb b/lib/axlsx/workbook/worksheet/worksheet.rb index 6a32e881..ff3e022e 100644 --- a/lib/axlsx/workbook/worksheet/worksheet.rb +++ b/lib/axlsx/workbook/worksheet/worksheet.rb @@ -41,7 +41,7 @@ module Axlsx def name @name ||= "Sheet" + (index+1).to_s end - + # The sheet calculation properties # @return [SheetCalcPr] def sheet_calc_pr @@ -76,6 +76,12 @@ module Axlsx @tables ||= Tables.new self end + # The pivot tables in this worksheet + # @return [Array] of Table + def pivot_tables + @pivot_tables ||= PivotTables.new self + end + # A typed collection of hyperlinks associated with this worksheet # @return [WorksheetHyperlinks] def hyperlinks @@ -104,7 +110,7 @@ module Axlsx # An range that excel will apply an autfilter to "A1:B3" # This will turn filtering on for the cells in the range. # The first row is considered the header, while subsequent rows are considerd to be data. - # @return String + # @return String def auto_filter @auto_filter ||= AutoFilter.new self end @@ -280,7 +286,7 @@ module Axlsx # The name of the worksheet # The name of a worksheet must be unique in the workbook, and must not exceed 31 characters - # @param [String] name + # @param [String] name def name=(name) validate_sheet_name name @name=Axlsx::coder.encode(name) @@ -388,7 +394,7 @@ module Axlsx cf = ConditionalFormatting.new( :sqref => cells ) cf.add_rules rules conditional_formattings << cf - conditional_formattings + conditional_formattings end # Add data validation to this worksheet. @@ -436,6 +442,12 @@ module Axlsx tables.last end + def add_pivot_table(ref, range, options={}) + pivot_tables << PivotTable.new(ref, range, self, options) + yield pivot_tables.last if block_given? + pivot_tables.last + end + # Shortcut to worsksheet_comments#add_comment def add_comment(options={}) worksheet_comments.add_comment(options) @@ -513,7 +525,7 @@ module Axlsx def sanitize(str) str.gsub(CONTROL_CHAR_REGEX, '') end - + # The worksheet relationships. This is managed automatically by the worksheet # @return [Relationships] def relationships @@ -521,7 +533,8 @@ module Axlsx r + [tables.relationships, worksheet_comments.relationships, hyperlinks.relationships, - worksheet_drawing.relationship].flatten.compact || [] + worksheet_drawing.relationship, + pivot_tables.relationships].flatten.compact || [] r end @@ -570,12 +583,12 @@ module Axlsx private - + def validate_sheet_name(name) DataTypeValidator.validate "Worksheet.name", String, name raise ArgumentError, (ERR_SHEET_NAME_TOO_LONG % name) if name.size > 31 raise ArgumentError, (ERR_SHEET_NAME_COLON_FORBIDDEN % name) if name.include? ':' - name = Axlsx::coder.encode(name) + name = Axlsx::coder.encode(name) sheet_names = @workbook.worksheets.map { |s| s.name } raise ArgumentError, (ERR_DUPLICATE_SHEET_NAME % name) if sheet_names.include?(name) end @@ -606,7 +619,7 @@ module Axlsx # @see Worksheet#protect_range # @return [SimpleTypedList] The protected ranges for this worksheet def protected_ranges - @protected_ranges ||= ProtectedRanges.new self + @protected_ranges ||= ProtectedRanges.new self # SimpleTypedList.new ProtectedRange end @@ -619,7 +632,7 @@ module Axlsx # data validations array # @return [Array] def data_validations - @data_validations ||= DataValidations.new self + @data_validations ||= DataValidations.new self end # merged cells array |
