cell_range.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. # Copyright (c) 2010-2024 openpyxl
  2. from copy import copy
  3. from operator import attrgetter
  4. from openpyxl.descriptors import Strict
  5. from openpyxl.descriptors import MinMax
  6. from openpyxl.descriptors.sequence import UniqueSequence
  7. from openpyxl.descriptors.serialisable import Serialisable
  8. from openpyxl.utils import (
  9. range_boundaries,
  10. range_to_tuple,
  11. get_column_letter,
  12. quote_sheetname,
  13. )
  14. class CellRange(Serialisable):
  15. """
  16. Represents a range in a sheet: title and coordinates.
  17. This object is used to perform operations on ranges, like:
  18. - shift, expand or shrink
  19. - union/intersection with another sheet range,
  20. We can check whether a range is:
  21. - equal or not equal to another,
  22. - disjoint of another,
  23. - contained in another.
  24. We can get:
  25. - the size of a range.
  26. - the range bounds (vertices)
  27. - the coordinates,
  28. - the string representation,
  29. """
  30. min_col = MinMax(min=1, max=18278, expected_type=int)
  31. min_row = MinMax(min=1, max=1048576, expected_type=int)
  32. max_col = MinMax(min=1, max=18278, expected_type=int)
  33. max_row = MinMax(min=1, max=1048576, expected_type=int)
  34. def __init__(self, range_string=None, min_col=None, min_row=None,
  35. max_col=None, max_row=None, title=None):
  36. if range_string is not None:
  37. if "!" in range_string:
  38. title, (min_col, min_row, max_col, max_row) = range_to_tuple(range_string)
  39. else:
  40. min_col, min_row, max_col, max_row = range_boundaries(range_string)
  41. self.min_col = min_col
  42. self.min_row = min_row
  43. self.max_col = max_col
  44. self.max_row = max_row
  45. self.title = title
  46. if min_col > max_col:
  47. fmt = "{max_col} must be greater than {min_col}"
  48. raise ValueError(fmt.format(min_col=min_col, max_col=max_col))
  49. if min_row > max_row:
  50. fmt = "{max_row} must be greater than {min_row}"
  51. raise ValueError(fmt.format(min_row=min_row, max_row=max_row))
  52. @property
  53. def bounds(self):
  54. """
  55. Vertices of the range as a tuple
  56. """
  57. return self.min_col, self.min_row, self.max_col, self.max_row
  58. @property
  59. def coord(self):
  60. """
  61. Excel-style representation of the range
  62. """
  63. fmt = "{min_col}{min_row}:{max_col}{max_row}"
  64. if (self.min_col == self.max_col
  65. and self.min_row == self.max_row):
  66. fmt = "{min_col}{min_row}"
  67. return fmt.format(
  68. min_col=get_column_letter(self.min_col),
  69. min_row=self.min_row,
  70. max_col=get_column_letter(self.max_col),
  71. max_row=self.max_row
  72. )
  73. @property
  74. def rows(self):
  75. """
  76. Return cell coordinates as rows
  77. """
  78. for row in range(self.min_row, self.max_row+1):
  79. yield [(row, col) for col in range(self.min_col, self.max_col+1)]
  80. @property
  81. def cols(self):
  82. """
  83. Return cell coordinates as columns
  84. """
  85. for col in range(self.min_col, self.max_col+1):
  86. yield [(row, col) for row in range(self.min_row, self.max_row+1)]
  87. @property
  88. def cells(self):
  89. from itertools import product
  90. return product(range(self.min_row, self.max_row+1), range(self.min_col, self.max_col+1))
  91. def _check_title(self, other):
  92. """
  93. Check whether comparisons between ranges are possible.
  94. Cannot compare ranges from different worksheets
  95. Skip if the range passed in has no title.
  96. """
  97. if not isinstance(other, CellRange):
  98. raise TypeError(repr(type(other)))
  99. if other.title and self.title != other.title:
  100. raise ValueError("Cannot work with ranges from different worksheets")
  101. def __repr__(self):
  102. fmt = u"<{cls} {coord}>"
  103. if self.title:
  104. fmt = u"<{cls} {title!r}!{coord}>"
  105. return fmt.format(cls=self.__class__.__name__, title=self.title, coord=self.coord)
  106. def __hash__(self):
  107. return hash((self.min_row, self.min_col, self.max_row, self.max_col))
  108. def __str__(self):
  109. fmt = "{coord}"
  110. title = self.title
  111. if title:
  112. fmt = u"{title}!{coord}"
  113. title = quote_sheetname(title)
  114. return fmt.format(title=title, coord=self.coord)
  115. def __copy__(self):
  116. return self.__class__(min_col=self.min_col, min_row=self.min_row,
  117. max_col=self.max_col, max_row=self.max_row,
  118. title=self.title)
  119. def shift(self, col_shift=0, row_shift=0):
  120. """
  121. Shift the focus of the range according to the shift values (*col_shift*, *row_shift*).
  122. :type col_shift: int
  123. :param col_shift: number of columns to be moved by, can be negative
  124. :type row_shift: int
  125. :param row_shift: number of rows to be moved by, can be negative
  126. :raise: :class:`ValueError` if any row or column index < 1
  127. """
  128. if (self.min_col + col_shift <= 0
  129. or self.min_row + row_shift <= 0):
  130. raise ValueError("Invalid shift value: col_shift={0}, row_shift={1}".format(col_shift, row_shift))
  131. self.min_col += col_shift
  132. self.min_row += row_shift
  133. self.max_col += col_shift
  134. self.max_row += row_shift
  135. def __ne__(self, other):
  136. """
  137. Test whether the ranges are not equal.
  138. :type other: openpyxl.worksheet.cell_range.CellRange
  139. :param other: Other sheet range
  140. :return: ``True`` if *range* != *other*.
  141. """
  142. try:
  143. self._check_title(other)
  144. except ValueError:
  145. return True
  146. return (
  147. other.min_row != self.min_row
  148. or self.max_row != other.max_row
  149. or other.min_col != self.min_col
  150. or self.max_col != other.max_col
  151. )
  152. def __eq__(self, other):
  153. """
  154. Test whether the ranges are equal.
  155. :type other: openpyxl.worksheet.cell_range.CellRange
  156. :param other: Other sheet range
  157. :return: ``True`` if *range* == *other*.
  158. """
  159. return not self.__ne__(other)
  160. def issubset(self, other):
  161. """
  162. Test whether every cell in this range is also in *other*.
  163. :type other: openpyxl.worksheet.cell_range.CellRange
  164. :param other: Other sheet range
  165. :return: ``True`` if *range* <= *other*.
  166. """
  167. self._check_title(other)
  168. return other.__superset(self)
  169. __le__ = issubset
  170. def __lt__(self, other):
  171. """
  172. Test whether *other* contains every cell of this range, and more.
  173. :type other: openpyxl.worksheet.cell_range.CellRange
  174. :param other: Other sheet range
  175. :return: ``True`` if *range* < *other*.
  176. """
  177. return self.__le__(other) and self.__ne__(other)
  178. def __superset(self, other):
  179. return (
  180. (self.min_row <= other.min_row <= other.max_row <= self.max_row)
  181. and
  182. (self.min_col <= other.min_col <= other.max_col <= self.max_col)
  183. )
  184. def issuperset(self, other):
  185. """
  186. Test whether every cell in *other* is in this range.
  187. :type other: openpyxl.worksheet.cell_range.CellRange
  188. :param other: Other sheet range
  189. :return: ``True`` if *range* >= *other* (or *other* in *range*).
  190. """
  191. self._check_title(other)
  192. return self.__superset(other)
  193. __ge__ = issuperset
  194. def __contains__(self, coord):
  195. """
  196. Check whether the range contains a particular cell coordinate
  197. """
  198. cr = self.__class__(coord)
  199. return self.__superset(cr)
  200. def __gt__(self, other):
  201. """
  202. Test whether this range contains every cell in *other*, and more.
  203. :type other: openpyxl.worksheet.cell_range.CellRange
  204. :param other: Other sheet range
  205. :return: ``True`` if *range* > *other*.
  206. """
  207. return self.__ge__(other) and self.__ne__(other)
  208. def isdisjoint(self, other):
  209. """
  210. Return ``True`` if this range has no cell in common with *other*.
  211. Ranges are disjoint if and only if their intersection is the empty range.
  212. :type other: openpyxl.worksheet.cell_range.CellRange
  213. :param other: Other sheet range.
  214. :return: ``True`` if the range has no cells in common with other.
  215. """
  216. self._check_title(other)
  217. # Sort by top-left vertex
  218. if self.bounds > other.bounds:
  219. self, other = other, self
  220. return (self.max_col < other.min_col
  221. or self.max_row < other.min_row
  222. or other.max_row < self.min_row)
  223. def intersection(self, other):
  224. """
  225. Return a new range with cells common to this range and *other*
  226. :type other: openpyxl.worksheet.cell_range.CellRange
  227. :param other: Other sheet range.
  228. :return: the intersecting sheet range.
  229. :raise: :class:`ValueError` if the *other* range doesn't intersect
  230. with this range.
  231. """
  232. if self.isdisjoint(other):
  233. raise ValueError("Range {0} doesn't intersect {0}".format(self, other))
  234. min_row = max(self.min_row, other.min_row)
  235. max_row = min(self.max_row, other.max_row)
  236. min_col = max(self.min_col, other.min_col)
  237. max_col = min(self.max_col, other.max_col)
  238. return CellRange(min_col=min_col, min_row=min_row, max_col=max_col,
  239. max_row=max_row)
  240. __and__ = intersection
  241. def union(self, other):
  242. """
  243. Return the minimal superset of this range and *other*. This new range
  244. will contain all cells from this range, *other*, and any additional
  245. cells required to form a rectangular ``CellRange``.
  246. :type other: openpyxl.worksheet.cell_range.CellRange
  247. :param other: Other sheet range.
  248. :return: a ``CellRange`` that is a superset of this and *other*.
  249. """
  250. self._check_title(other)
  251. min_row = min(self.min_row, other.min_row)
  252. max_row = max(self.max_row, other.max_row)
  253. min_col = min(self.min_col, other.min_col)
  254. max_col = max(self.max_col, other.max_col)
  255. return CellRange(min_col=min_col, min_row=min_row, max_col=max_col,
  256. max_row=max_row, title=self.title)
  257. __or__ = union
  258. def __iter__(self):
  259. """
  260. For use as a dictionary elsewhere in the library.
  261. """
  262. for x in self.__attrs__:
  263. if x == "title":
  264. continue
  265. v = getattr(self, x)
  266. yield x, v
  267. def expand(self, right=0, down=0, left=0, up=0):
  268. """
  269. Expand the range by the dimensions provided.
  270. :type right: int
  271. :param right: expand range to the right by this number of cells
  272. :type down: int
  273. :param down: expand range down by this number of cells
  274. :type left: int
  275. :param left: expand range to the left by this number of cells
  276. :type up: int
  277. :param up: expand range up by this number of cells
  278. """
  279. self.min_col -= left
  280. self.min_row -= up
  281. self.max_col += right
  282. self.max_row += down
  283. def shrink(self, right=0, bottom=0, left=0, top=0):
  284. """
  285. Shrink the range by the dimensions provided.
  286. :type right: int
  287. :param right: shrink range from the right by this number of cells
  288. :type down: int
  289. :param down: shrink range from the top by this number of cells
  290. :type left: int
  291. :param left: shrink range from the left by this number of cells
  292. :type up: int
  293. :param up: shrink range from the bottom by this number of cells
  294. """
  295. self.min_col += left
  296. self.min_row += top
  297. self.max_col -= right
  298. self.max_row -= bottom
  299. @property
  300. def size(self):
  301. """ Return the size of the range as a dictionary of rows and columns. """
  302. cols = self.max_col + 1 - self.min_col
  303. rows = self.max_row + 1 - self.min_row
  304. return {'columns':cols, 'rows':rows}
  305. @property
  306. def top(self):
  307. """A list of cell coordinates that comprise the top of the range"""
  308. return [(self.min_row, col) for col in range(self.min_col, self.max_col+1)]
  309. @property
  310. def bottom(self):
  311. """A list of cell coordinates that comprise the bottom of the range"""
  312. return [(self.max_row, col) for col in range(self.min_col, self.max_col+1)]
  313. @property
  314. def left(self):
  315. """A list of cell coordinates that comprise the left-side of the range"""
  316. return [(row, self.min_col) for row in range(self.min_row, self.max_row+1)]
  317. @property
  318. def right(self):
  319. """A list of cell coordinates that comprise the right-side of the range"""
  320. return [(row, self.max_col) for row in range(self.min_row, self.max_row+1)]
  321. class MultiCellRange(Strict):
  322. ranges = UniqueSequence(expected_type=CellRange)
  323. def __init__(self, ranges=set()):
  324. if isinstance(ranges, str):
  325. ranges = [CellRange(r) for r in ranges.split()]
  326. self.ranges = set(ranges)
  327. def __contains__(self, coord):
  328. if isinstance(coord, str):
  329. coord = CellRange(coord)
  330. for r in self.ranges:
  331. if coord <= r:
  332. return True
  333. return False
  334. def __repr__(self):
  335. ranges = " ".join([str(r) for r in self.sorted()])
  336. return f"<{self.__class__.__name__} [{ranges}]>"
  337. def __str__(self):
  338. ranges = u" ".join([str(r) for r in self.sorted()])
  339. return ranges
  340. def __hash__(self):
  341. return hash(str(self))
  342. def sorted(self):
  343. """
  344. Return a sorted list of items
  345. """
  346. return sorted(self.ranges, key=attrgetter('min_col', 'min_row', 'max_col', 'max_row'))
  347. def add(self, coord):
  348. """
  349. Add a cell coordinate or CellRange
  350. """
  351. cr = coord
  352. if isinstance(coord, str):
  353. cr = CellRange(coord)
  354. elif not isinstance(coord, CellRange):
  355. raise ValueError("You can only add CellRanges")
  356. if cr not in self:
  357. self.ranges.add(cr)
  358. def __iadd__(self, coord):
  359. self.add(coord)
  360. return self
  361. def __eq__(self, other):
  362. if isinstance(other, str):
  363. other = self.__class__(other)
  364. return self.ranges == other.ranges
  365. def __ne__(self, other):
  366. return not self == other
  367. def __bool__(self):
  368. return bool(self.ranges)
  369. def remove(self, coord):
  370. if not isinstance(coord, CellRange):
  371. coord = CellRange(coord)
  372. self.ranges.remove(coord)
  373. def __iter__(self):
  374. for cr in self.ranges:
  375. yield cr
  376. def __copy__(self):
  377. ranges = {copy(r) for r in self.ranges}
  378. return MultiCellRange(ranges)