1# Copyright (c) 2010-2024 openpyxl
2
3import re
4from openpyxl.descriptors import (
5 Strict,
6 Integer,
7 String,
8 Typed,
9)
10from openpyxl.utils import quote_sheetname, absolute_coordinate
11from openpyxl.utils.cell import SHEET_TITLE, SHEETRANGE_RE, RANGE_EXPR
12
13from .cell_range import MultiCellRange
14
15COL_RANGE = r"""(?P<cols>[$]?(?P<min_col>[a-zA-Z]{1,3}):[$]?(?P<max_col>[a-zA-Z]{1,3}))"""
16COL_RANGE_RE = re.compile(COL_RANGE)
17ROW_RANGE = r"""(?P<rows>[$]?(?P<min_row>\d+):[$]?(?P<max_row>\d+))"""
18ROW_RANGE_RE = re.compile(ROW_RANGE)
19TITLES_REGEX = re.compile("""{0}{1}?,?{2}?,?""".format(SHEET_TITLE, ROW_RANGE, COL_RANGE),
20 re.VERBOSE)
21PRINT_AREA_RE = re.compile(f"({SHEET_TITLE})?(?P<cells>{RANGE_EXPR})", re.VERBOSE)
22
23class ColRange(Strict):
24 """
25 Represent a range of at least one column
26 """
27
28 min_col = String()
29 max_col = String()
30
31
32 def __init__(self, range_string=None, min_col=None, max_col=None):
33 if range_string is not None:
34 match = COL_RANGE_RE.match(range_string)
35 if not match:
36 raise ValueError(f"{range_string} is not a valid column range")
37 min_col, max_col = match.groups()[1:]
38 self.min_col = min_col
39 self.max_col = max_col
40
41
42 def __eq__(self, other):
43 if isinstance(other, self.__class__):
44 return (self.min_col == other.min_col
45 and
46 self.max_col == other.max_col)
47 elif isinstance(other, str):
48 return (str(self) == other
49 or
50 f"{self.min_col}:{self.max_col}")
51 return False
52
53
54 def __repr__(self):
55 return f"Range of columns from '{self.min_col}' to '{self.max_col}'"
56
57
58 def __str__(self):
59 return f"${self.min_col}:${self.max_col}"
60
61
62class RowRange(Strict):
63 """
64 Represent a range of at least one row
65 """
66
67 min_row = Integer()
68 max_row = Integer()
69
70 def __init__(self, range_string=None, min_row=None, max_row=None):
71 if range_string is not None:
72 match = ROW_RANGE_RE.match(range_string)
73 if not match:
74 raise ValueError(f"{range_string} is not a valid row range")
75 min_row, max_row = match.groups()[1:]
76 self.min_row = min_row
77 self.max_row = max_row
78
79
80 def __eq__(self, other):
81 if isinstance(other, self.__class__):
82 return (self.min_row == other.min_row
83 and
84 self.max_row == other.max_row)
85 elif isinstance(other, str):
86 return (str(self) == other
87 or
88 f"{self.min_row}:{self.max_row}")
89 return False
90
91 def __repr__(self):
92 return f"Range of rows from '{self.min_row}' to '{self.max_row}'"
93
94
95 def __str__(self):
96 return f"${self.min_row}:${self.max_row}"
97
98
99class PrintTitles(Strict):
100 """
101 Contains at least either a range of rows or columns
102 """
103
104 cols = Typed(expected_type=ColRange, allow_none=True)
105 rows = Typed(expected_type=RowRange, allow_none=True)
106 title = String()
107
108
109 def __init__(self, cols=None, rows=None, title=""):
110 self.cols = cols
111 self.rows = rows
112 self.title = title
113
114
115 @classmethod
116 def from_string(cls, value):
117 kw = dict((k, v) for match in TITLES_REGEX.finditer(value)
118 for k, v in match.groupdict().items() if v)
119
120 if not kw:
121 raise ValueError(f"{value} is not a valid print titles definition")
122
123 cols = rows = None
124
125 if "cols" in kw:
126 cols = ColRange(kw["cols"])
127 if "rows" in kw:
128 rows = RowRange(kw["rows"])
129
130 title = kw.get("quoted") or kw.get("notquoted")
131
132 return cls(cols=cols, rows=rows, title=title)
133
134
135 def __eq__(self, other):
136 if isinstance(other, self.__class__):
137 return (self.cols == other.cols
138 and
139 self.rows == other.rows
140 and
141 self.title == other.title)
142 elif isinstance(other, str):
143 return str(self) == other
144 return False
145
146 def __repr__(self):
147 return f"Print titles for sheet {self.title} cols {self.rows}, rows {self.cols}"
148
149
150 def __str__(self):
151 title = quote_sheetname(self.title)
152 titles = ",".join([f"{title}!{value}" for value in (self.rows, self.cols) if value])
153 return titles or ""
154
155
156class PrintArea(MultiCellRange):
157
158
159 @classmethod
160 def from_string(cls, value):
161 new = []
162 for m in PRINT_AREA_RE.finditer(value): # can be multiple
163 coord = m.group("cells")
164 if coord:
165 new.append(coord)
166 return cls(new)
167
168
169 def __init__(self, ranges=(), title=""):
170 self.title = ""
171 super().__init__(ranges)
172
173
174 def __str__(self):
175 if self.ranges:
176 return ",".join([f"{quote_sheetname(self.title)}!{absolute_coordinate(str(range))}"
177 for range in self.sorted()])
178 return ""
179
180
181 def __eq__(self, other):
182 super().__eq__(other)
183 if isinstance(other, str):
184 return str(self) == other