Skip to content

Commit bb28112

Browse files
committed
# Support for conditional formatting
Adds support for conditional formatting via two new classes, ConditionalFormatting and ConditionalFormattingRule. Conditional Formats apply to ranges of cells, and can include multiple rules, ranked by priority. A single worksheet has many @conditional_formattings applied to different ranges. There are still pieces of the spec missing from the implementation. The biggest glaring ommission are the child elements colorScale, dataBar, and iconSet (I only implemented formula). http://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.conditionalformattingrule.aspx
1 parent c8a6711 commit bb28112

File tree

6 files changed

+429
-2
lines changed

6 files changed

+429
-2
lines changed

lib/axlsx/util/validators.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,35 @@ def self.validate_pattern_type(v)
9696
:darkTrellis, :lightHorizontal, :lightVertical, :lightDown, :lightUp, :lightGrid, :lightTrellis, :gray125, :gray0625], v
9797
end
9898

99+
# Requires that the value is one of the ST_TimePeriod types
100+
# valid time period types are today, yesterday, tomorrow, last7Days,
101+
# thisMonth, lastMonth, nextMonth, thisWeek, lastWeek, nextWeek
102+
def self.validate_time_period_type(v)
103+
RestrictionValidator.validate :time_period_type, [:today, :yesterday, :tomorrow, :last7Days, :thisMonth, :lastMonth, :nextMonth, :thisWeek, :lastWeek, :nextWeek], v
104+
105+
106+
end
107+
108+
# Requires that the value is valid conditional formatting type.
109+
# valid types must be one of expression, cellIs, colorScale,
110+
# dataBar, iconSet, top10, uniqueValues, duplicateValues,
111+
# containsText, notContainsText, beginsWith, endsWith,
112+
# containsBlanks, notContainsBlanks, containsErrors,
113+
# notContainsErrors, timePeriod, aboveAverage
114+
# @param [Any] v The value validated
115+
def self.validate_conditional_formatting_type(v)
116+
RestrictionValidator.validate :conditional_formatting_type, [:expression, :cellIs, :colorScale, :dataBar, :iconSet, :top10, :uniqueValues, :duplicateValues, :containsText, :notContainsText, :beginsWith, :endsWith, :containsBlanks, :notContainsBlanks, :containsErrors, :notContainsErrors, :timePeriod, :aboveAverage], v
117+
end
118+
119+
# Requires that the value is valid conditional formatting operator.
120+
# valid operators must be one of lessThan, lessThanOrEqual, equal,
121+
# notEqual, greaterThanOrEqual, greaterThan, between, notBetween,
122+
# containsText, notContains, beginsWith, endsWith
123+
# @param [Any] v The value validated
124+
def self.validate_conditional_formatting_operator(v)
125+
RestrictionValidator.validate :conditional_formatting_type, [:lessThan, :lessThanOrEqual, :equal, :notEqual, :greaterThanOrEqual, :greaterThan, :between, :notBetween, :containsText, :notContains, :beginsWith, :endsWith], v
126+
end
127+
99128
# Requires that the value is a gradient_type.
100129
# valid types are :linear and :path
101130
# @param [Any] v The value validated

lib/axlsx/workbook/workbook.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ module Axlsx
44
require 'axlsx/workbook/worksheet/date_time_converter.rb'
55
require 'axlsx/workbook/worksheet/cell.rb'
66
require 'axlsx/workbook/worksheet/page_margins.rb'
7+
require 'axlsx/workbook/worksheet/conditional_formatting.rb'
8+
require 'axlsx/workbook/worksheet/conditional_formatting_rule.rb'
79
require 'axlsx/workbook/worksheet/row.rb'
810
require 'axlsx/workbook/worksheet/col.rb'
911
require 'axlsx/workbook/worksheet/worksheet.rb'
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
module Axlsx
2+
# Conditional formatting allows styling of ranges based on functions
3+
#
4+
# @note The recommended way to manage conditional formatting is via Worksheet#add_conditional_formatting
5+
# @see Worksheet#add_conditional_formatting
6+
class ConditionalFormatting
7+
8+
# Sqref
9+
# Range over which the formatting is applied
10+
# @return [String]
11+
attr_reader :sqref
12+
13+
# Rules
14+
# Rules to apply the formatting to. Can be either a hash of
15+
# options for one ConditionalFormattingRule, an array of hashes
16+
# for multiple ConditionalFormattingRules, or an array of already
17+
# created ConditionalFormattingRules.
18+
# @return [Array]
19+
attr_reader :rules
20+
21+
# Creates a new ConditionalFormatting object
22+
# @option options [Array] rules The rules to apply
23+
# @option options [String] sqref The range to apply the rules to
24+
def initialize(options={})
25+
@rules = []
26+
options.each do |o|
27+
self.send("#{o[0]}=", o[1]) if self.respond_to? "#{o[0]}="
28+
end
29+
end
30+
31+
# Add ConditionalFormattingRules to this object. Rules can either
32+
# be already created objects or hashes of options for automatic
33+
# creation.
34+
# @option rules [Array, Hash] the rules apply, can be just one in hash form
35+
# @see ConditionalFormattingRule#initialize
36+
def add_rules(rules)
37+
rules = [rules] if rules.is_a? Hash
38+
conditional_rules = rules.each do |rule|
39+
add_rule rule
40+
end
41+
end
42+
43+
# Add a ConditionalFormattingRule. If a hash of options is passed
44+
# in create a rule on the fly
45+
# @option rule [ConditionalFormattingRule, Hash] A rule to create
46+
# @see ConditionalFormattingRule#initialize
47+
def add_rule(rule)
48+
if rule.is_a? Axlsx::ConditionalFormattingRule
49+
@rules << rule
50+
elsif rule.is_a? Hash
51+
@rules << ConditionalFormattingRule.new(rule)
52+
end
53+
end
54+
55+
# @see rules
56+
def rules=(v); @rules = v end
57+
# @see sqref
58+
def sqref=(v); Axlsx::validate_string(v); @sqref = v end
59+
60+
# Serializes the conditional formatting element
61+
# @param [String] str
62+
# @return [String]
63+
def to_xml_string(str = '')
64+
str << '<conditionalFormatting sqref="' << sqref << '">'
65+
str << rules.collect{ |rule| rule.to_xml_string }.join(' ')
66+
str << '</conditionalFormatting>'
67+
end
68+
end
69+
end
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
module Axlsx
2+
# Conditional formatting rules specify formulas whose evaluations
3+
# format cells
4+
#
5+
# @note The recommended way to manage these rules is via Worksheet#add_conditional_formatting
6+
# @see Worksheet#add_conditional_formatting
7+
class ConditionalFormattingRule
8+
@@child_elements = [:formula]
9+
10+
# Formula
11+
# @return [String]
12+
attr_reader :formula
13+
14+
# Type (ST_CfType)
15+
# options are expression, cellIs, colorScale, dataBar, iconSet,
16+
# top10, uniqueValues, duplicateValues, containsText,
17+
# notContainsText, beginsWith, endsWith, containsBlanks,
18+
# notContainsBlanks, containsErrors, notContainsErrors,
19+
# timePeriod, aboveAverage
20+
# @return [Symbol]
21+
attr_reader :type
22+
23+
# Above average rule
24+
# Indicates whether the rule is an "above average" rule. True
25+
# indicates 'above average'. This attribute is ignored if type is
26+
# not equal to aboveAverage.
27+
# @return [Boolean]
28+
attr_reader :aboveAverage
29+
30+
# Bottom N rule
31+
# @return [Boolean]
32+
attr_reader :bottom
33+
34+
# Differential Formatting Id
35+
# @return [Integer]
36+
attr_reader :dxfId
37+
38+
# Equal Average
39+
# Flag indicating whether the 'aboveAverage' and 'belowAverage'
40+
# criteria is inclusive of the average itself, or exclusive of
41+
# that value.
42+
# @return [Boolean]
43+
attr_reader :equalAverage
44+
45+
# Operator
46+
# The operator in a "cell value is" conditional formatting
47+
# rule. This attribute is ignored if type is not equal to cellIs
48+
#
49+
# Operator must be one of lessThan, lessThanOrEqual, equal,
50+
# notEqual, greaterThanOrEqual, greaterThan, between, notBetween,
51+
# containsText, notContains, beginsWith, endsWith
52+
# @return [Symbol]
53+
attr_reader :operator
54+
55+
# Priority
56+
# The priority of this conditional formatting rule. This value is
57+
# used to determine which format should be evaluated and
58+
# rendered. Lower numeric values are higher priority than higher
59+
# numeric values, where '1' is the highest priority.
60+
# @return [Integer]
61+
attr_reader :priority
62+
63+
# Text
64+
# The text value in a "text contains" conditional formatting
65+
# rule.
66+
# @return [String]
67+
attr_reader :text
68+
69+
# percent (Top 10 Percent)
70+
# Indicates whether a "top/bottom n" rule is a "top/bottom n
71+
# percent" rule. This attribute is ignored if type is not equal to
72+
# top10.
73+
# @return [Boolean]
74+
attr_reader :percent
75+
76+
# rank (Rank)
77+
# The value of "n" in a "top/bottom n" conditional formatting
78+
# rule. This attribute is ignored if type is not equal to top10.
79+
# @return [Integer]
80+
attr_reader :rank
81+
82+
# stdDev (StdDev)
83+
# The number of standard deviations to include above or below the
84+
# average in the conditional formatting rule. This attribute is
85+
# ignored if type is not equal to aboveAverage. If a value is
86+
# present for stdDev and the rule type = aboveAverage, then this
87+
# rule is automatically an "above or below N standard deviations"
88+
# rule.
89+
# @return [Integer]
90+
attr_reader :stdDev
91+
92+
# stopIfTrue (Stop If True)
93+
# If this flag is '1', no rules with lower priority shall be
94+
# applied over this rule, when this rule evaluates to true.
95+
# @return [Boolean]
96+
attr_reader :stopIfTrue
97+
98+
# timePeriod (Time Period)
99+
# The applicable time period in a "date occurring…" conditional
100+
# formatting rule. This attribute is ignored if type is not equal
101+
# to timePeriod.
102+
# Valid types are today, yesterday, tomorrow, last7Days,
103+
# thisMonth, lastMonth, nextMonth, thisWeek, lastWeek, nextWeek
104+
attr_reader :timePeriod
105+
106+
# Creates a new Conditional Formatting Rule object
107+
# @option options [Symbol] type The type of this formatting rule
108+
# @option options [Boolean] aboveAverage This is an aboveAverage rule
109+
# @option options [Boolean] bottom This is a bottom N rule.
110+
# @option options [Integer] dxfId The formatting id to apply to matches
111+
# @option options [Boolean] equalAverage Is the aboveAverage or belowAverage rule inclusive
112+
# @option options [Integer] priority The priority of the rule, 1 is highest
113+
# @option options [Symbol] operator Which operator to apply
114+
# @option options [String] text The value to apply a text operator against
115+
# @option options [Boolean] percent If a top/bottom N rule, evaluate as N% rather than N
116+
# @option options [Integer] rank If a top/bottom N rule, the value of N
117+
# @option options [Integer] stdDev The number of standard deviations above or below the average to match
118+
# @option options [Boolean] stopIfTrue Stop evaluating rules after this rule matches
119+
# @option options [Symbol] timePeriod The time period in a date occuring... rule
120+
# @option options [String] formula The formula to match against in i.e. an equal rule
121+
def initialize(options={})
122+
options.each do |o|
123+
self.send("#{o[0]}=", o[1]) if self.respond_to? "#{o[0]}="
124+
end
125+
end
126+
127+
# @see type
128+
def type=(v); Axlsx::validate_conditional_formatting_type(v); @type = v end
129+
# @see aboveAverage
130+
def aboveAverage=(v); Axlsx::validate_boolean(v); @aboveAverage = v end
131+
# @see bottom
132+
def bottom=(v); Axlsx::validate_boolean(v); @bottom = v end
133+
# @see dxfId
134+
def dxfId=(v); Axlsx::validate_unsigned_numeric(v); @dxfId = v end
135+
# @see equalAverage
136+
def equalAverage=(v); Axlsx::validate_boolean(v); @equalAverage = v end
137+
# @see priority
138+
def priority=(v); Axlsx::validate_unsigned_numeric(v); @priority = v end
139+
# @see operator
140+
def operator=(v); Axlsx::validate_conditional_formatting_operator(v); @operator = v end
141+
# @see text
142+
def text=(v); Axlsx::validate_string(v); @text = v end
143+
# @see percent
144+
def percent=(v); Axlsx::validate_boolean(v); @percent = v end
145+
# @see rank
146+
def rank=(v); Axlsx::validate_unsigned_numeric(v); @rank = v end
147+
# @see stdDev
148+
def stdDev=(v); Axlsx::validate_unsigned_numeric(v); @stdDev = v end
149+
# @see stopIfTrue
150+
def stopIfTrue=(v); Axlsx::validate_boolean(v); @stopIfTrue = v end
151+
# @see timePeriod
152+
def timePeriod=(v); Axlsx::validate_time_period_type(v); @timePeriod = v end
153+
# @see formula
154+
def formula=(v); Axlsx::validate_string(v); @formula = v end
155+
156+
# Serializes the conditional formatting rule
157+
# @param [String] str
158+
# @return [String]
159+
def to_xml_string(str = '')
160+
str << '<cfRule '
161+
str << instance_values.map { |key, value| '' << key << '="' << value.to_s << '"' unless @@child_elements.include?(key.to_sym) }.join(' ')
162+
str << '>'
163+
@@child_elements.each do |el|
164+
str << "<#{el}>" << self.send(el) << "</#{el}>" if self.send(el)
165+
end
166+
str << '</cfRule>'
167+
end
168+
end
169+
end

lib/axlsx/workbook/worksheet/worksheet.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# encoding: UTF-8
21
module Axlsx
32

43
# The Worksheet class represents a worksheet in the workbook.
@@ -101,7 +100,8 @@ def initialize(wb, options={})
101100
@drawing = @page_margins = @auto_filter = nil
102101
@merged_cells = []
103102
@auto_fit_data = []
104-
103+
@conditional_formattings = []
104+
105105
@selected = false
106106
@show_gridlines = true
107107
self.name = "Sheet" + (index+1).to_s
@@ -123,6 +123,20 @@ def cells
123123
rows.flatten
124124
end
125125

126+
# Add conditinoal formatting to this worksheet.
127+
#
128+
# @option cells [String] The range to apply the formatting to
129+
# @option rules [Array, Hash] An array of hashes (or just one) to create Conditional formatting rules from
130+
# @example This would color column A whenever it is FALSE
131+
# worksheet.add_conditional_formatting( "A1:A1048576", { :type => :cellIs, :operator => :equal, :formula => "FALSE", :dxfId => 0, priority => 1 }
132+
#
133+
# @see ConditionalFormattingRule.#initialize
134+
def add_conditional_formatting(cells, rules)
135+
cf = ConditionalFormatting.new( :sqref => cells )
136+
cf.add_rules rules
137+
@conditional_formattings << cf
138+
end
139+
126140
# Creates merge information for this worksheet.
127141
# Cells can be merged by calling the merge_cells method on a worksheet.
128142
# @example This would merge the three cells C1..E1 #
@@ -411,6 +425,9 @@ def to_xml_string
411425
unless @tables.empty?
412426
str.concat "<tableParts count='%s'>%s</tableParts>" % [@tables.size, @tables.reduce('') { |memo, obj| memo += "<tablePart r:id='%s'/>" % obj.rId }]
413427
end
428+
@conditional_formattings.each do |cf|
429+
str.concat cf.to_xml_string
430+
end
414431
str + '</worksheet>'
415432
end
416433

0 commit comments

Comments
 (0)