Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for some of the conditional formatting spec #83

Merged
merged 9 commits into from Apr 20, 2012
31 changes: 31 additions & 0 deletions examples/example_conditional_formatting.rb
@@ -0,0 +1,31 @@
#!/usr/bin/env ruby -w -s
# -*- coding: utf-8 -*-
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require 'axlsx'

p = Axlsx::Package.new
book = p.workbook
ws = book.add_worksheet

# define your regular styles
percent = book.styles.add_style(:format_code => "0.00%", :border => Axlsx::STYLE_THIN_BORDER)
money = book.styles.add_style(:format_code => '0,000', :border => Axlsx::STYLE_THIN_BORDER)

# define the style for conditional formatting
profitable = book.styles.add_style( :fg_color=>"FF428751",
:type => :dxf)

# Generate 20 rows of data
ws.add_row ["Previous Year Quarterly Profits (JPY)"]
ws.add_row ["Quarter", "Profit", "% of Total"]
offset = 3
rows = 20
offset.upto(rows + offset) do |i|
ws.add_row ["Q#{i}", 10000*((rows/2-i) * (rows/2-i)), "=100*B#{i}/SUM(B3:B#{rows+offset})"], :style=>[nil, money, percent]
end

# Apply conditional formatting to range B4:B100 in the worksheet
ws.add_conditional_formatting("B4:B100", { :type => :cellIs, :operator => :greaterThan, :formula => "100000", :dxfId => profitable, :priority => 1 })

f = File.open('example_differential_styling.xlsx', 'w')
p.serialize(f)
77 changes: 77 additions & 0 deletions lib/axlsx/stylesheet/dxf.rb
@@ -0,0 +1,77 @@
# encoding: UTF-8
module Axlsx
# The Dxf class defines an incremental formatting record for use in Styles. The recommended way to manage styles for your workbook is with Styles#add_style
# @see Styles#add_style
class Dxf
# The order in which the child elements is put in the XML seems to
# be important for Excel
CHILD_ELEMENTS = [:font, :numFmt, :fill, :alignment, :border, :protection]
#does not support extList (ExtensionList)

# The cell alignment for this style
# @return [CellAlignment]
# @see CellAlignment
attr_reader :alignment

# The cell protection for this style
# @return [CellProtection]
# @see CellProtection
attr_reader :protection

# the child NumFmt to be used to this style
# @return [NumFmt]
attr_reader :numFmt

# the child font to be used for this style
# @return [Font]
attr_reader :font

# the child fill to be used in this style
# @return [Fill]
attr_reader :fill

# the border to be used in this style
# @return [Border]
attr_reader :border

# Creates a new Xf object
# @option options [Border] border
# @option options [NumFmt] numFmt
# @option options [Fill] fill
# @option options [Font] font
# @option options [CellAlignment] alignment
# @option options [CellProtection] protection
def initialize(options={})
options.each do |o|
self.send("#{o[0]}=", o[1]) if self.respond_to? "#{o[0]}="
end
end

# @see Dxf#alignment
def alignment=(v) DataTypeValidator.validate "Dxf.alignment", CellAlignment, v; @alignment = v end
# @see protection
def protection=(v) DataTypeValidator.validate "Dxf.protection", CellProtection, v; @protection = v end
# @see numFmt
def numFmt=(v) DataTypeValidator.validate "Dxf.numFmt", NumFmt, v; @numFmt = v end
# @see font
def font=(v) DataTypeValidator.validate "Dxf.font", Font, v; @font = v end
# @see border
def border=(v) DataTypeValidator.validate "Dxf.border", Border, v; @border = v end
# @see fill
def fill=(v) DataTypeValidator.validate "Dxf.fill", Fill, v; @fill = v end

# Serializes the object
# @param [String] str
# @return [String]
def to_xml_string(str = '')
str << '<dxf>'
# Dxf elements have no attributes. All of the instance variables
# are child elements.
CHILD_ELEMENTS.each do |element|
self.send(element).to_xml_string(str) if self.send(element)
end
str << '</dxf>'
end

end
end
117 changes: 82 additions & 35 deletions lib/axlsx/stylesheet/styles.rb
Expand Up @@ -14,6 +14,7 @@ module Axlsx
require 'axlsx/stylesheet/table_style.rb'
require 'axlsx/stylesheet/table_styles.rb'
require 'axlsx/stylesheet/table_style_element.rb'
require 'axlsx/stylesheet/dxf.rb'
require 'axlsx/stylesheet/xf.rb'
require 'axlsx/stylesheet/cell_protection.rb'

Expand Down Expand Up @@ -137,6 +138,7 @@ def initialize()
# @option options [String] bg_color The background color to apply to the cell
# @option options [Boolean] hidden Indicates if the cell should be hidden
# @option options [Boolean] locked Indicates if the cell should be locked
# @option options [Symbol] type What type of style is this. Options are [:dxf, :xf]. :xf is default
# @option options [Hash] alignment A hash defining any of the attributes used in CellAlignment
# @see CellAlignment
#
Expand Down Expand Up @@ -189,67 +191,112 @@ def initialize()
# ws.add_row :values => ["Q4", 2000, 20], :style=>[title, currency, percent]
# f = File.open('example_you_got_style.xlsx', 'w')
# p.serialize(f)
#
# @example Differential styling
# # Differential styles apply on top of cell styles. Used in Conditional Formatting. Must specify :type => :dxf, and you can't use :num_fmt.
# require "rubygems" # if that is your preferred way to manage gems!
# require "axlsx"
#
# p = Axlsx::Package.new
# wb = p.workbook
# ws = wb.add_worksheet
#
# # define your styles
# profitable = wb.styles.add_style(:bg_color => "FFFF0000",
# :fg_color=>"#FF000000",
# :type => :dxf)
#
# ws.add_row :values => ["Genreated At:", Time.now], :styles=>[nil, date_time]
# ws.add_row :values => ["Previous Year Quarterly Profits (JPY)"], :style=>title
# ws.add_row :values => ["Quarter", "Profit", "% of Total"], :style=>title
# ws.add_row :values => ["Q1", 4000, 40], :style=>[title, currency, percent]
# ws.add_row :values => ["Q2", 3000, 30], :style=>[title, currency, percent]
# ws.add_row :values => ["Q3", 1000, 10], :style=>[title, currency, percent]
# ws.add_row :values => ["Q4", 2000, 20], :style=>[title, currency, percent]
#
# ws.add_conditional_formatting("A1:A7", { :type => :cellIs, :operator => :greaterThan, :formula => "2000", :dxfId => profitable, :priority => 1 })
# f = File.open('example_differential_styling', 'w')
# p.serialize(f)
#
def add_style(options={})
# Default to :xf
options[:type] ||= :xf
raise ArgumentError, "Type must be one of [:xf, :dxf]" unless [:xf, :dxf].include?(options[:type] )

numFmt = if options[:format_code]
n = @numFmts.map{ |f| f.numFmtId }.max + 1
NumFmt.new(:numFmtId => n, :formatCode=> options[:format_code])
elsif options[:type] == :xf
options[:num_fmt] || 0
elsif options[:type] == :dxf and options[:num_fmt]
raise ArgumentError, "Can't use :num_fmt with :dxf"
end

border = options[:border]

numFmtId = if options[:format_code]
n = @numFmts.map{ |f| f.numFmtId }.max + 1
numFmts << NumFmt.new(:numFmtId => n, :formatCode=> options[:format_code])
n
else
options[:num_fmt] || 0
end

borderId = options[:border] || 0

if borderId.is_a?(Hash)
raise ArgumentError, "border hash definitions must include both style and color" unless borderId.keys.include?(:style) && borderId.keys.include?(:color)
if border.is_a?(Hash)
raise ArgumentError, "border hash definitions must include both style and color" unless border.keys.include?(:style) && border.keys.include?(:color)

s = borderId[:style]
c = borderId[:color]
edges = borderId[:edges] || [:left, :right, :top, :bottom]
s = border[:style]
c = border[:color]
edges = border[:edges] || [:left, :right, :top, :bottom]

border = Border.new
edges.each {|pr| border.prs << BorderPr.new(:name => pr, :style=>s, :color => Color.new(:rgb => c))}
borderId = self.borders << border
elsif border.is_a? Integer
raise ArgumentError, "Must pass border options directly if specifying dxf" if options[:type] == :dxf
raise ArgumentError, "Invalid borderId" unless border < borders.size
end

raise ArgumentError, "Invalid borderId" unless borderId < borders.size

fill = if options[:bg_color]
color = Color.new(:rgb=>options[:bg_color])
pattern = PatternFill.new(:patternType =>:solid, :fgColor=>color)
fills << Fill.new(pattern)
else
0
Fill.new(pattern)
end

fontId = if (options.values_at(:fg_color, :sz, :b, :i, :u, :strike, :outline, :shadow, :charset, :family, :font_name).length)
font = if (options.values_at(:fg_color, :sz, :b, :i, :u, :strike, :outline, :shadow, :charset, :family, :font_name).length)
font = Font.new()
[:b, :i, :u, :strike, :outline, :shadow, :charset, :family, :sz].each { |k| font.send("#{k}=", options[k]) unless options[k].nil? }
font.color = Color.new(:rgb => options[:fg_color]) unless options[:fg_color].nil?
font.name = options[:font_name] unless options[:font_name].nil?
fonts << font
else
0
font
end

applyProtection = (options[:hidden] || options[:locked]) ? 1 : 0

xf = Xf.new(:fillId => fill, :fontId=>fontId, :applyFill=>1, :applyFont=>1, :numFmtId=>numFmtId, :borderId=>borderId, :applyProtection=>applyProtection)

xf.applyNumberFormat = true if xf.numFmtId > 0
xf.applyBorder = true if borderId > 0

if options[:type] == :dxf
style = Dxf.new
style.fill = fill if fill
style.font = font if font
style.numFmt = numFmt if numFmt
style.border = border if border
else
# Only add styles if we're adding to Xf. They're embedded inside the Dxf pieces directly
fontId = font ? fonts << font : 0
fillId = fill ? fills << fill : 0
# Default to borderId = 0 rather than no border
border ||= 0
borderId = border.is_a?(Border) ? borders << border : border
numFmtId = numFmt.is_a?(NumFmt) ? numFmts << numFmt : numFmt
style = Xf.new(:fillId=>fillId, :fontId=>fontId, :applyFill=>1, :applyFont=>1, :numFmtId=>numFmtId, :borderId=>borderId, :applyProtection=>applyProtection)
style.applyNumberFormat = true if style.numFmtId > 0
style.applyBorder = true if borderId > 0
end

if options[:alignment]
xf.alignment = CellAlignment.new(options[:alignment])
xf.applyAlignment = true
style.alignment = CellAlignment.new(options[:alignment])
style.applyAlignment = true if style.is_a? Xf
end

if applyProtection
xf.protection = CellProtection.new(options)
style.protection = CellProtection.new(options)
end

cellXfs << xf
if style.is_a? Dxf
dxfs << style
else
cellXfs << style if style.is_a? Xf
end
end

# Serializes the object
Expand Down Expand Up @@ -306,7 +353,7 @@ def load_default_styles
@cellXfs << Xf.new(:borderId=>0, :xfId=>0, :numFmtId=>14, :fontId=>0, :fillId=>0, :applyNumberFormat=>1)
@cellXfs.lock

@dxfs = SimpleTypedList.new(Xf, "dxfs"); @dxfs.lock
@dxfs = SimpleTypedList.new(Dxf, "dxfs"); @dxfs.lock
@tableStyles = TableStyles.new(:defaultTableStyle => "TableStyleMedium9", :defaultPivotStyle => "PivotStyleLight16"); @tableStyles.lock
end
end
Expand Down
29 changes: 29 additions & 0 deletions lib/axlsx/util/validators.rb
Expand Up @@ -96,6 +96,35 @@ def self.validate_pattern_type(v)
:darkTrellis, :lightHorizontal, :lightVertical, :lightDown, :lightUp, :lightGrid, :lightTrellis, :gray125, :gray0625], v
end

# Requires that the value is one of the ST_TimePeriod types
# valid time period types are today, yesterday, tomorrow, last7Days,
# thisMonth, lastMonth, nextMonth, thisWeek, lastWeek, nextWeek
def self.validate_time_period_type(v)
RestrictionValidator.validate :time_period_type, [:today, :yesterday, :tomorrow, :last7Days, :thisMonth, :lastMonth, :nextMonth, :thisWeek, :lastWeek, :nextWeek], v


end

# Requires that the value is valid conditional formatting type.
# valid types must be one of expression, cellIs, colorScale,
# dataBar, iconSet, top10, uniqueValues, duplicateValues,
# containsText, notContainsText, beginsWith, endsWith,
# containsBlanks, notContainsBlanks, containsErrors,
# notContainsErrors, timePeriod, aboveAverage
# @param [Any] v The value validated
def self.validate_conditional_formatting_type(v)
RestrictionValidator.validate :conditional_formatting_type, [:expression, :cellIs, :colorScale, :dataBar, :iconSet, :top10, :uniqueValues, :duplicateValues, :containsText, :notContainsText, :beginsWith, :endsWith, :containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :timePeriod, :aboveAverage], v
end

# Requires that the value is valid conditional formatting operator.
# valid operators must be one of lessThan, lessThanOrEqual, equal,
# notEqual, greaterThanOrEqual, greaterThan, between, notBetween,
# containsText, notContains, beginsWith, endsWith
# @param [Any] v The value validated
def self.validate_conditional_formatting_operator(v)
RestrictionValidator.validate :conditional_formatting_type, [:lessThan, :lessThanOrEqual, :equal, :notEqual, :greaterThanOrEqual, :greaterThan, :between, :notBetween, :containsText, :notContains, :beginsWith, :endsWith], v
end

# Requires that the value is a gradient_type.
# valid types are :linear and :path
# @param [Any] v The value validated
Expand Down
2 changes: 2 additions & 0 deletions lib/axlsx/workbook/workbook.rb
Expand Up @@ -4,6 +4,8 @@ module Axlsx
require 'axlsx/workbook/worksheet/date_time_converter.rb'
require 'axlsx/workbook/worksheet/cell.rb'
require 'axlsx/workbook/worksheet/page_margins.rb'
require 'axlsx/workbook/worksheet/conditional_formatting.rb'
require 'axlsx/workbook/worksheet/conditional_formatting_rule.rb'
require 'axlsx/workbook/worksheet/row.rb'
require 'axlsx/workbook/worksheet/col.rb'
require 'axlsx/workbook/worksheet/worksheet.rb'
Expand Down