rule.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. # Copyright (c) 2010-2024 openpyxl
  2. from openpyxl.descriptors.serialisable import Serialisable
  3. from openpyxl.descriptors import (
  4. Typed,
  5. String,
  6. Sequence,
  7. Bool,
  8. NoneSet,
  9. Set,
  10. Integer,
  11. Float,
  12. )
  13. from openpyxl.descriptors.excel import ExtensionList
  14. from openpyxl.styles.colors import Color, ColorDescriptor
  15. from openpyxl.styles.differential import DifferentialStyle
  16. from openpyxl.utils.cell import COORD_RE
  17. class ValueDescriptor(Float):
  18. """
  19. Expected type depends upon type attribute of parent :-(
  20. Most values should be numeric BUT they can also be cell references
  21. """
  22. def __set__(self, instance, value):
  23. ref = None
  24. if value is not None and isinstance(value, str):
  25. ref = COORD_RE.match(value)
  26. if instance.type == "formula" or ref:
  27. self.expected_type = str
  28. else:
  29. self.expected_type = float
  30. super().__set__(instance, value)
  31. class FormatObject(Serialisable):
  32. tagname = "cfvo"
  33. type = Set(values=(['num', 'percent', 'max', 'min', 'formula', 'percentile']))
  34. val = ValueDescriptor(allow_none=True)
  35. gte = Bool(allow_none=True)
  36. extLst = Typed(expected_type=ExtensionList, allow_none=True)
  37. __elements__ = ()
  38. def __init__(self,
  39. type,
  40. val=None,
  41. gte=None,
  42. extLst=None,
  43. ):
  44. self.type = type
  45. self.val = val
  46. self.gte = gte
  47. class RuleType(Serialisable):
  48. cfvo = Sequence(expected_type=FormatObject)
  49. class IconSet(RuleType):
  50. tagname = "iconSet"
  51. iconSet = NoneSet(values=(['3Arrows', '3ArrowsGray', '3Flags',
  52. '3TrafficLights1', '3TrafficLights2', '3Signs', '3Symbols', '3Symbols2',
  53. '4Arrows', '4ArrowsGray', '4RedToBlack', '4Rating', '4TrafficLights',
  54. '5Arrows', '5ArrowsGray', '5Rating', '5Quarters']))
  55. showValue = Bool(allow_none=True)
  56. percent = Bool(allow_none=True)
  57. reverse = Bool(allow_none=True)
  58. __elements__ = ("cfvo",)
  59. def __init__(self,
  60. iconSet=None,
  61. showValue=None,
  62. percent=None,
  63. reverse=None,
  64. cfvo=None,
  65. ):
  66. self.iconSet = iconSet
  67. self.showValue = showValue
  68. self.percent = percent
  69. self.reverse = reverse
  70. self.cfvo = cfvo
  71. class DataBar(RuleType):
  72. tagname = "dataBar"
  73. minLength = Integer(allow_none=True)
  74. maxLength = Integer(allow_none=True)
  75. showValue = Bool(allow_none=True)
  76. color = ColorDescriptor()
  77. __elements__ = ('cfvo', 'color')
  78. def __init__(self,
  79. minLength=None,
  80. maxLength=None,
  81. showValue=None,
  82. cfvo=None,
  83. color=None,
  84. ):
  85. self.minLength = minLength
  86. self.maxLength = maxLength
  87. self.showValue = showValue
  88. self.cfvo = cfvo
  89. self.color = color
  90. class ColorScale(RuleType):
  91. tagname = "colorScale"
  92. color = Sequence(expected_type=Color)
  93. __elements__ = ('cfvo', 'color')
  94. def __init__(self,
  95. cfvo=None,
  96. color=None,
  97. ):
  98. self.cfvo = cfvo
  99. self.color = color
  100. class Rule(Serialisable):
  101. tagname = "cfRule"
  102. type = Set(values=(['expression', 'cellIs', 'colorScale', 'dataBar',
  103. 'iconSet', 'top10', 'uniqueValues', 'duplicateValues', 'containsText',
  104. 'notContainsText', 'beginsWith', 'endsWith', 'containsBlanks',
  105. 'notContainsBlanks', 'containsErrors', 'notContainsErrors', 'timePeriod',
  106. 'aboveAverage']))
  107. dxfId = Integer(allow_none=True)
  108. priority = Integer()
  109. stopIfTrue = Bool(allow_none=True)
  110. aboveAverage = Bool(allow_none=True)
  111. percent = Bool(allow_none=True)
  112. bottom = Bool(allow_none=True)
  113. operator = NoneSet(values=(['lessThan', 'lessThanOrEqual', 'equal',
  114. 'notEqual', 'greaterThanOrEqual', 'greaterThan', 'between', 'notBetween',
  115. 'containsText', 'notContains', 'beginsWith', 'endsWith']))
  116. text = String(allow_none=True)
  117. timePeriod = NoneSet(values=(['today', 'yesterday', 'tomorrow', 'last7Days',
  118. 'thisMonth', 'lastMonth', 'nextMonth', 'thisWeek', 'lastWeek',
  119. 'nextWeek']))
  120. rank = Integer(allow_none=True)
  121. stdDev = Integer(allow_none=True)
  122. equalAverage = Bool(allow_none=True)
  123. formula = Sequence(expected_type=str)
  124. colorScale = Typed(expected_type=ColorScale, allow_none=True)
  125. dataBar = Typed(expected_type=DataBar, allow_none=True)
  126. iconSet = Typed(expected_type=IconSet, allow_none=True)
  127. extLst = Typed(expected_type=ExtensionList, allow_none=True)
  128. dxf = Typed(expected_type=DifferentialStyle, allow_none=True)
  129. __elements__ = ('colorScale', 'dataBar', 'iconSet', 'formula')
  130. __attrs__ = ('type', 'rank', 'priority', 'equalAverage', 'operator',
  131. 'aboveAverage', 'dxfId', 'stdDev', 'stopIfTrue', 'timePeriod', 'text',
  132. 'percent', 'bottom')
  133. def __init__(self,
  134. type,
  135. dxfId=None,
  136. priority=0,
  137. stopIfTrue=None,
  138. aboveAverage=None,
  139. percent=None,
  140. bottom=None,
  141. operator=None,
  142. text=None,
  143. timePeriod=None,
  144. rank=None,
  145. stdDev=None,
  146. equalAverage=None,
  147. formula=(),
  148. colorScale=None,
  149. dataBar=None,
  150. iconSet=None,
  151. extLst=None,
  152. dxf=None,
  153. ):
  154. self.type = type
  155. self.dxfId = dxfId
  156. self.priority = priority
  157. self.stopIfTrue = stopIfTrue
  158. self.aboveAverage = aboveAverage
  159. self.percent = percent
  160. self.bottom = bottom
  161. self.operator = operator
  162. self.text = text
  163. self.timePeriod = timePeriod
  164. self.rank = rank
  165. self.stdDev = stdDev
  166. self.equalAverage = equalAverage
  167. self.formula = formula
  168. self.colorScale = colorScale
  169. self.dataBar = dataBar
  170. self.iconSet = iconSet
  171. self.dxf = dxf
  172. def ColorScaleRule(start_type=None,
  173. start_value=None,
  174. start_color=None,
  175. mid_type=None,
  176. mid_value=None,
  177. mid_color=None,
  178. end_type=None,
  179. end_value=None,
  180. end_color=None):
  181. """Backwards compatibility"""
  182. formats = []
  183. if start_type is not None:
  184. formats.append(FormatObject(type=start_type, val=start_value))
  185. if mid_type is not None:
  186. formats.append(FormatObject(type=mid_type, val=mid_value))
  187. if end_type is not None:
  188. formats.append(FormatObject(type=end_type, val=end_value))
  189. colors = []
  190. for v in (start_color, mid_color, end_color):
  191. if v is not None:
  192. if not isinstance(v, Color):
  193. v = Color(v)
  194. colors.append(v)
  195. cs = ColorScale(cfvo=formats, color=colors)
  196. rule = Rule(type="colorScale", colorScale=cs)
  197. return rule
  198. def FormulaRule(formula=None, stopIfTrue=None, font=None, border=None,
  199. fill=None):
  200. """
  201. Conditional formatting with custom differential style
  202. """
  203. rule = Rule(type="expression", formula=formula, stopIfTrue=stopIfTrue)
  204. rule.dxf = DifferentialStyle(font=font, border=border, fill=fill)
  205. return rule
  206. def CellIsRule(operator=None, formula=None, stopIfTrue=None, font=None, border=None, fill=None):
  207. """
  208. Conditional formatting rule based on cell contents.
  209. """
  210. # Excel doesn't use >, >=, etc, but allow for ease of python development
  211. expand = {">": "greaterThan", ">=": "greaterThanOrEqual", "<": "lessThan", "<=": "lessThanOrEqual",
  212. "=": "equal", "==": "equal", "!=": "notEqual"}
  213. operator = expand.get(operator, operator)
  214. rule = Rule(type='cellIs', operator=operator, formula=formula, stopIfTrue=stopIfTrue)
  215. rule.dxf = DifferentialStyle(font=font, border=border, fill=fill)
  216. return rule
  217. def IconSetRule(icon_style=None, type=None, values=None, showValue=None, percent=None, reverse=None):
  218. """
  219. Convenience function for creating icon set rules
  220. """
  221. cfvo = []
  222. for val in values:
  223. cfvo.append(FormatObject(type, val))
  224. icon_set = IconSet(iconSet=icon_style, cfvo=cfvo, showValue=showValue,
  225. percent=percent, reverse=reverse)
  226. rule = Rule(type='iconSet', iconSet=icon_set)
  227. return rule
  228. def DataBarRule(start_type=None, start_value=None, end_type=None,
  229. end_value=None, color=None, showValue=None, minLength=None, maxLength=None):
  230. start = FormatObject(start_type, start_value)
  231. end = FormatObject(end_type, end_value)
  232. data_bar = DataBar(cfvo=[start, end], color=color, showValue=showValue,
  233. minLength=minLength, maxLength=maxLength)
  234. rule = Rule(type='dataBar', dataBar=data_bar)
  235. return rule