cell_range.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. from __future__ import absolute_import, unicode_literals
  2. # Copyright (c) 2010-2019 openpyxl
  3. from copy import copy
  4. from openpyxl.compat.strings import safe_repr
  5. from openpyxl.descriptors import Strict
  6. from openpyxl.descriptors import MinMax, Sequence
  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. def _check_title(self, other):
  88. """
  89. Check whether comparisons between ranges are possible.
  90. Cannot compare ranges from different worksheets
  91. Skip if the range passed in has no title.
  92. """
  93. if not isinstance(other, CellRange):
  94. raise TypeError(repr(type(other)))
  95. if other.title and self.title != other.title:
  96. raise ValueError("Cannot work with ranges from different worksheets")
  97. def __repr__(self):
  98. fmt = u"<{cls} {coord}>"
  99. if self.title:
  100. fmt = u"<{cls} {title!r}!{coord}>"
  101. return safe_repr(fmt.format(cls=self.__class__.__name__, title=self.title, coord=self.coord))
  102. def _get_range_string(self):
  103. fmt = "{coord}"
  104. title = self.title
  105. if title:
  106. fmt = u"{title}!{coord}"
  107. title = quote_sheetname(title)
  108. return fmt.format(title=title, coord=self.coord)
  109. __unicode__ = _get_range_string
  110. def __str__(self):
  111. coord = self._get_range_string()
  112. return safe_repr(coord)
  113. def __copy__(self):
  114. return self.__class__(min_col=self.min_col, min_row=self.min_row,
  115. max_col=self.max_col, max_row=self.max_row,
  116. title=self.title)
  117. def shift(self, col_shift=0, row_shift=0):
  118. """
  119. Shift the focus of the range according to the shift values (*col_shift*, *row_shift*).
  120. :type col_shift: int
  121. :param col_shift: number of columns to be moved by, can be negative
  122. :type row_shift: int
  123. :param row_shift: number of rows to be moved by, can be negative
  124. :raise: :class:`ValueError` if any row or column index < 1
  125. """
  126. if (self.min_col + col_shift <= 0
  127. or self.min_row + row_shift <= 0):
  128. raise ValueError("Invalid shift value: col_shift={0}, row_shift={1}".format(col_shift, row_shift))
  129. self.min_col += col_shift
  130. self.min_row += row_shift
  131. self.max_col += col_shift
  132. self.max_row += row_shift
  133. def __ne__(self, other):
  134. """
  135. Test whether the ranges are not equal.
  136. :type other: openpyxl.worksheet.cell_range.CellRange
  137. :param other: Other sheet range
  138. :return: ``True`` if *range* != *other*.
  139. """
  140. try:
  141. self._check_title(other)
  142. except ValueError:
  143. return True
  144. return (
  145. other.min_row != self.min_row
  146. or self.max_row != other.max_row
  147. or other.min_col != self.min_col
  148. or self.max_col != other.max_col
  149. )
  150. def __eq__(self, other):
  151. """
  152. Test whether the ranges are equal.
  153. :type other: openpyxl.worksheet.cell_range.CellRange
  154. :param other: Other sheet range
  155. :return: ``True`` if *range* == *other*.
  156. """
  157. return not self.__ne__(other)
  158. def issubset(self, other):
  159. """
  160. Test whether every cell in this range is also in *other*.
  161. :type other: openpyxl.worksheet.cell_range.CellRange
  162. :param other: Other sheet range
  163. :return: ``True`` if *range* <= *other*.
  164. """
  165. self._check_title(other)
  166. return (
  167. (other.min_row <= self.min_row <= self.max_row <= other.max_row)
  168. and
  169. (other.min_col <= self.min_col <= self.max_col <= other.max_col)
  170. )
  171. __le__ = issubset
  172. def __lt__(self, other):
  173. """
  174. Test whether *other* contains every cell of this range, and more.
  175. :type other: openpyxl.worksheet.cell_range.CellRange
  176. :param other: Other sheet range
  177. :return: ``True`` if *range* < *other*.
  178. """
  179. return self.__le__(other) and self.__ne__(other)
  180. def issuperset(self, other):
  181. """
  182. Test whether every cell in *other* is in this range.
  183. :type other: openpyxl.worksheet.cell_range.CellRange
  184. :param other: Other sheet range
  185. :return: ``True`` if *range* >= *other* (or *other* in *range*).
  186. """
  187. self._check_title(other)
  188. return (
  189. (self.min_row <= other.min_row <= other.max_row <= self.max_row)
  190. and
  191. (self.min_col <= other.min_col <= other.max_col <= self.max_col)
  192. )
  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. if cr.title is None:
  200. cr.title = self.title
  201. return self.issuperset(cr)
  202. def __gt__(self, other):
  203. """
  204. Test whether this range contains every cell in *other*, and more.
  205. :type other: openpyxl.worksheet.cell_range.CellRange
  206. :param other: Other sheet range
  207. :return: ``True`` if *range* > *other*.
  208. """
  209. return self.__ge__(other) and self.__ne__(other)
  210. def isdisjoint(self, other):
  211. """
  212. Return ``True`` if this range has no cell in common with *other*.
  213. Ranges are disjoint if and only if their intersection is the empty range.
  214. :type other: openpyxl.worksheet.cell_range.CellRange
  215. :param other: Other sheet range.
  216. :return: ``True`` if the range has no cells in common with other.
  217. """
  218. self._check_title(other)
  219. # Sort by top-left vertex
  220. if self.bounds > other.bounds:
  221. self, other = other, self
  222. return (self.max_col < other.min_col
  223. or self.max_row < other.min_row
  224. or other.max_row < self.min_row)
  225. def intersection(self, other):
  226. """
  227. Return a new range with cells common to this range and *other*
  228. :type other: openpyxl.worksheet.cell_range.CellRange
  229. :param other: Other sheet range.
  230. :return: the intersecting sheet range.
  231. :raise: :class:`ValueError` if the *other* range doesn't intersect
  232. with this range.
  233. """
  234. if self.isdisjoint(other):
  235. raise ValueError("Range {0} doesn't intersect {0}".format(self, other))
  236. min_row = max(self.min_row, other.min_row)
  237. max_row = min(self.max_row, other.max_row)
  238. min_col = max(self.min_col, other.min_col)
  239. max_col = min(self.max_col, other.max_col)
  240. return CellRange(min_col=min_col, min_row=min_row, max_col=max_col,
  241. max_row=max_row)
  242. __and__ = intersection
  243. def union(self, other):
  244. """
  245. Return the minimal superset of this range and *other*. This new range
  246. will contain all cells from this range, *other*, and any additional
  247. cells required to form a rectangular ``CellRange``.
  248. :type other: openpyxl.worksheet.cell_range.CellRange
  249. :param other: Other sheet range.
  250. :return: a ``CellRange`` that is a superset of this and *other*.
  251. """
  252. self._check_title(other)
  253. min_row = min(self.min_row, other.min_row)
  254. max_row = max(self.max_row, other.max_row)
  255. min_col = min(self.min_col, other.min_col)
  256. max_col = max(self.max_col, other.max_col)
  257. return CellRange(min_col=min_col, min_row=min_row, max_col=max_col,
  258. max_row=max_row, title=self.title)
  259. __or__ = union
  260. def __iter__(self):
  261. """
  262. For use as a dictionary elsewhere in the library.
  263. """
  264. for x in self.__attrs__:
  265. if x == "title":
  266. continue
  267. v = getattr(self, x)
  268. yield x, v
  269. def expand(self, right=0, down=0, left=0, up=0):
  270. """
  271. Expand the range by the dimensions provided.
  272. :type right: int
  273. :param right: expand range to the right by this number of cells
  274. :type down: int
  275. :param down: expand range down by this number of cells
  276. :type left: int
  277. :param left: expand range to the left by this number of cells
  278. :type up: int
  279. :param up: expand range up by this number of cells
  280. """
  281. self.min_col -= left
  282. self.min_row -= up
  283. self.max_col += right
  284. self.max_row += down
  285. def shrink(self, right=0, bottom=0, left=0, top=0):
  286. """
  287. Shrink the range by the dimensions provided.
  288. :type right: int
  289. :param right: shrink range from the right by this number of cells
  290. :type down: int
  291. :param down: shrink range from the top by this number of cells
  292. :type left: int
  293. :param left: shrink range from the left by this number of cells
  294. :type up: int
  295. :param up: shrink range from the bottown by this number of cells
  296. """
  297. self.min_col += left
  298. self.min_row += top
  299. self.max_col -= right
  300. self.max_row -= bottom
  301. @property
  302. def size(self):
  303. """ Return the size of the range as a dictionary of rows and columns. """
  304. cols = self.max_col + 1 - self.min_col
  305. rows = self.max_row + 1 - self.min_row
  306. return {'columns':cols, 'rows':rows}
  307. @property
  308. def top(self):
  309. """A list of cell coordinates that comprise the top of the range"""
  310. return [(self.min_row, col) for col in range(self.min_col, self.max_col+1)]
  311. @property
  312. def bottom(self):
  313. """A list of cell coordinates that comprise the bottom of the range"""
  314. return [(self.max_row, col) for col in range(self.min_col, self.max_col+1)]
  315. @property
  316. def left(self):
  317. """A list of cell coordinates that comprise the left-side of the range"""
  318. return [(row, self.min_col) for row in range(self.min_row, self.max_row+1)]
  319. @property
  320. def right(self):
  321. """A list of cell coordinates that comprise the right-side of the range"""
  322. return [(row, self.max_col) for row in range(self.min_row, self.max_row+1)]
  323. class MultiCellRange(Strict):
  324. ranges = Sequence(expected_type=CellRange)
  325. def __init__(self, ranges=()):
  326. if isinstance(ranges, str):
  327. ranges = [CellRange(r) for r in ranges.split()]
  328. self.ranges = ranges
  329. def __contains__(self, coord):
  330. for r in self.ranges:
  331. if coord in r:
  332. return True
  333. return False
  334. def __repr__(self):
  335. ranges = " ".join([str(r) for r in self.ranges])
  336. return "<{0} [{1}]>".format(self.__class__.__name__, ranges)
  337. def __str__(self):
  338. ranges = u" ".join([str(r) for r in self.ranges])
  339. return ranges
  340. __unicode__ = __str__
  341. def add(self, coord):
  342. """
  343. Add a cell coordinate or CellRange
  344. """
  345. cr = None
  346. if isinstance(coord, CellRange):
  347. cr = coord
  348. coord = cr.coord
  349. if coord not in self:
  350. if cr is None:
  351. cr = CellRange(coord)
  352. ranges = self.ranges
  353. ranges.append(cr)
  354. self.ranges = ranges
  355. def __iadd__(self, coord):
  356. self.add(coord)
  357. return self
  358. def __eq__(self, other):
  359. if isinstance(other, str):
  360. other = self.__class__(other)
  361. return self.ranges == other.ranges
  362. def __ne__(self, other):
  363. return not self == other
  364. def __bool__(self):
  365. return bool(self.ranges)
  366. __nonzero__ = __bool__
  367. def remove(self, coord):
  368. if not isinstance(coord, CellRange):
  369. coord = CellRange(coord)
  370. self.ranges.remove(coord)
  371. def __iter__(self):
  372. for cr in self.ranges:
  373. yield cr
  374. def __copy__(self):
  375. n = MultiCellRange()
  376. for r in self.ranges:
  377. n.ranges.append(copy(r))
  378. return n