# frozen_string_literal: true
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}"
@data_sheet = nil
@rows = []
@columns = []
@data = []
@pages = []
@subtotal = nil
@no_subtotals_on_headers = []
@sort_on_headers = {}
@style_info = {}
parse_options options
yield self if block_given?
end
# Defines the headers in which subtotals are not to be included.
# @return [Array]
attr_accessor :no_subtotals_on_headers
# Defines the headers in which sort is applied.
# Can be an array of headers to sort ascending by default, or a hash for specific control
# (with headers as keys, `:ascending` or `:descending` as values).
#
# Examples: `["year", "month"]` or `{"year" => :descending, "month" => :descending}`
# @return [Hash]
attr_reader :sort_on_headers
# (see #sort_on_headers)
def sort_on_headers=(headers)
headers ||= {}
headers = Hash[*headers.map { |h| [h, :ascending] }.flatten] if headers.is_a?(Array)
@sort_on_headers = headers
end
# Style info for the pivot table
# @return [Hash]
attr_accessor :style_info
# 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 sheet used as data source for the pivot table
# @return [Worksheet]
attr_writer :data_sheet
# @see #data_sheet
def data_sheet
@data_sheet || @sheet
end
# The range where the data for this pivot table lives.
# @return [String]
attr_reader :range
# (see #range)
def range=(v)
DataTypeValidator.validate "#{self.class}.range", [String], v
if v.is_a?(String)
@range = v
end
end
# The rows
# @return [Array]
attr_reader :rows
# (see #rows)
def rows=(v)
DataTypeValidator.validate "#{self.class}.rows", [Array], v
v.each do |ref|
DataTypeValidator.validate "#{self.class}.rows[]", [String], ref
end
@rows = v
end
# The columns
# @return [Array]
attr_reader :columns
# (see #columns)
def columns=(v)
DataTypeValidator.validate "#{self.class}.columns", [Array], v
v.each do |ref|
DataTypeValidator.validate "#{self.class}.columns[]", [String], ref
end
@columns = v
end
# The data
# @return [Array]
attr_reader :data
# (see #data)
def data=(v)
DataTypeValidator.validate "#{self.class}.data", [Array], v
@data = []
v.each do |data_field|
if data_field.is_a? String
data_field = { ref: data_field }
end
data_field.each do |key, value|
if key == :num_fmt
DataTypeValidator.validate "#{self.class}.data[]", [Integer], value
else
DataTypeValidator.validate "#{self.class}.data[]", [String], value
end
end
@data << data_field
end
end
# The pages
# @return [String]
attr_reader :pages
# (see #pages)
def pages=(v)
DataTypeValidator.validate "#{self.class}.pages", [Array], v
v.each do |ref|
DataTypeValidator.validate "#{self.class}.pages[]", [String], ref
end
@pages = v
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
format(PIVOT_TABLE_PN, index + 1)
end
# The relationship part name of this pivot table
# @return [String]
def rels_pn
format(PIVOT_TABLE_RELS_PN, index + 1)
end
# The cache_definition for this pivot table
# @return [PivotTableCacheDefinition]
def cache_definition
@cache_definition ||= PivotTableCacheDefinition.new(self)
end
# The relationships for this pivot table.
# @return [Relationships]
def relationships
r = Relationships.new
r << Relationship.new(cache_definition, PIVOT_TABLE_CACHE_DEFINITION_R, "../#{cache_definition.pn}")
r
end
# Serializes the object
# @param [String] str
# @return [String]
def to_xml_string(str = +'')
str << ''
str << ''
str << ''
str << ''
header_cell_values.each do |cell_value|
subtotal = !no_subtotals_on_headers.include?(cell_value)
sorttype = sort_on_headers[cell_value]
str << pivot_field_for(cell_value, subtotal, sorttype)
end
str << ''
if rows.empty?
str << ''
str << ' '
else
str << ''
rows.each do |row_value|
str << ''
end
str << ''
str << ''
rows.size.times do
str << ''
end
str << ''
end
if columns.empty?
if data.size > 1
str << ''
str << ""
str << ''
(data.size - 1).times do |i|
str << ""
end
str << ''
else
str << ''
end
else
str << ''
columns.each do |column_value|
str << ''
end
str << ''
end
unless pages.empty?
str << ''
pages.each do |page_value|
str << ''
end
str << ''
end
unless data.empty?
str << ""
data.each do |datum_value|
# The correct name prefix in ["Sum","Average", etc...]
str << ""
end
str << ''
end
# custom pivot table style
unless style_info.empty?
str << ''
end
str << ''
end
# References for header cells
# @return [Array]
def header_cell_refs
Axlsx.range_to_a(header_range).first
end
# The header cells for the pivot table
# @return [Array]
def header_cells
data_sheet[header_range]
end
# The values in the header cells collection
# @return [Array]
def header_cell_values
header_cells.map(&:value)
end
# The number of cells in the header_cells collection
# @return [Integer]
def header_cells_count
header_cells.count
end
# The index of a given value in the header cells
# @return [Integer]
def header_index_of(value)
header_cell_values.index(value)
end
private
def pivot_field_for(cell_ref, subtotal, sorttype)
attributes = %w[compact="0" outline="0" subtotalTop="0" showAll="0" includeNewItemsInFilter="1"]
items_tag = ' '
include_items_tag = false
if rows.include? cell_ref
attributes << 'axis="axisRow"'
attributes << "sortType=\"#{sorttype == :descending ? 'descending' : 'ascending'}\"" if sorttype
if subtotal
include_items_tag = true
else
attributes << 'defaultSubtotal="0"'
end
elsif columns.include? cell_ref
attributes << 'axis="axisCol"'
attributes << "sortType=\"#{sorttype == :descending ? 'descending' : 'ascending'}\"" if sorttype
include_items_tag = true
elsif pages.include? cell_ref
attributes << 'axis="axisPage"'
include_items_tag = true
elsif data_refs.include? cell_ref
attributes << 'dataField="1"'
end
"#{include_items_tag ? items_tag : nil}"
end
def data_refs
data.map { |hash| hash[:ref] }
end
def header_range
range.gsub(/^(\w+?)(\d+)\:(\w+?)\d+$/, '\1\2:\3\2')
end
end
end