123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- from __future__ import absolute_import, unicode_literals
- # Copyright (c) 2010-2019 openpyxl
- from copy import copy
- from openpyxl.compat.strings import safe_repr
- from openpyxl.descriptors import Strict
- from openpyxl.descriptors import MinMax, Sequence
- from openpyxl.descriptors.serialisable import Serialisable
- from openpyxl.utils import (
- range_boundaries,
- range_to_tuple,
- get_column_letter,
- quote_sheetname,
- )
- class CellRange(Serialisable):
- """
- Represents a range in a sheet: title and coordinates.
- This object is used to perform operations on ranges, like:
- - shift, expand or shrink
- - union/intersection with another sheet range,
- We can check whether a range is:
- - equal or not equal to another,
- - disjoint of another,
- - contained in another.
- We can get:
- - the size of a range.
- - the range bounds (vertices)
- - the coordinates,
- - the string representation,
- """
- min_col = MinMax(min=1, max=18278, expected_type=int)
- min_row = MinMax(min=1, max=1048576, expected_type=int)
- max_col = MinMax(min=1, max=18278, expected_type=int)
- max_row = MinMax(min=1, max=1048576, expected_type=int)
- def __init__(self, range_string=None, min_col=None, min_row=None,
- max_col=None, max_row=None, title=None):
- if range_string is not None:
- if "!" in range_string:
- title, (min_col, min_row, max_col, max_row) = range_to_tuple(range_string)
- else:
- min_col, min_row, max_col, max_row = range_boundaries(range_string)
- self.min_col = min_col
- self.min_row = min_row
- self.max_col = max_col
- self.max_row = max_row
- self.title = title
- if min_col > max_col:
- fmt = "{max_col} must be greater than {min_col}"
- raise ValueError(fmt.format(min_col=min_col, max_col=max_col))
- if min_row > max_row:
- fmt = "{max_row} must be greater than {min_row}"
- raise ValueError(fmt.format(min_row=min_row, max_row=max_row))
- @property
- def bounds(self):
- """
- Vertices of the range as a tuple
- """
- return self.min_col, self.min_row, self.max_col, self.max_row
- @property
- def coord(self):
- """
- Excel-style representation of the range
- """
- fmt = "{min_col}{min_row}:{max_col}{max_row}"
- if (self.min_col == self.max_col
- and self.min_row == self.max_row):
- fmt = "{min_col}{min_row}"
- return fmt.format(
- min_col=get_column_letter(self.min_col),
- min_row=self.min_row,
- max_col=get_column_letter(self.max_col),
- max_row=self.max_row
- )
- @property
- def rows(self):
- """
- Return cell coordinates as rows
- """
- for row in range(self.min_row, self.max_row+1):
- yield [(row, col) for col in range(self.min_col, self.max_col+1)]
- @property
- def cols(self):
- """
- Return cell coordinates as columns
- """
- for col in range(self.min_col, self.max_col+1):
- yield [(row, col) for row in range(self.min_row, self.max_row+1)]
- def _check_title(self, other):
- """
- Check whether comparisons between ranges are possible.
- Cannot compare ranges from different worksheets
- Skip if the range passed in has no title.
- """
- if not isinstance(other, CellRange):
- raise TypeError(repr(type(other)))
- if other.title and self.title != other.title:
- raise ValueError("Cannot work with ranges from different worksheets")
- def __repr__(self):
- fmt = u"<{cls} {coord}>"
- if self.title:
- fmt = u"<{cls} {title!r}!{coord}>"
- return safe_repr(fmt.format(cls=self.__class__.__name__, title=self.title, coord=self.coord))
- def _get_range_string(self):
- fmt = "{coord}"
- title = self.title
- if title:
- fmt = u"{title}!{coord}"
- title = quote_sheetname(title)
- return fmt.format(title=title, coord=self.coord)
- __unicode__ = _get_range_string
- def __str__(self):
- coord = self._get_range_string()
- return safe_repr(coord)
- def __copy__(self):
- return self.__class__(min_col=self.min_col, min_row=self.min_row,
- max_col=self.max_col, max_row=self.max_row,
- title=self.title)
- def shift(self, col_shift=0, row_shift=0):
- """
- Shift the focus of the range according to the shift values (*col_shift*, *row_shift*).
- :type col_shift: int
- :param col_shift: number of columns to be moved by, can be negative
- :type row_shift: int
- :param row_shift: number of rows to be moved by, can be negative
- :raise: :class:`ValueError` if any row or column index < 1
- """
- if (self.min_col + col_shift <= 0
- or self.min_row + row_shift <= 0):
- raise ValueError("Invalid shift value: col_shift={0}, row_shift={1}".format(col_shift, row_shift))
- self.min_col += col_shift
- self.min_row += row_shift
- self.max_col += col_shift
- self.max_row += row_shift
- def __ne__(self, other):
- """
- Test whether the ranges are not equal.
- :type other: openpyxl.worksheet.cell_range.CellRange
- :param other: Other sheet range
- :return: ``True`` if *range* != *other*.
- """
- try:
- self._check_title(other)
- except ValueError:
- return True
- return (
- other.min_row != self.min_row
- or self.max_row != other.max_row
- or other.min_col != self.min_col
- or self.max_col != other.max_col
- )
- def __eq__(self, other):
- """
- Test whether the ranges are equal.
- :type other: openpyxl.worksheet.cell_range.CellRange
- :param other: Other sheet range
- :return: ``True`` if *range* == *other*.
- """
- return not self.__ne__(other)
- def issubset(self, other):
- """
- Test whether every cell in this range is also in *other*.
- :type other: openpyxl.worksheet.cell_range.CellRange
- :param other: Other sheet range
- :return: ``True`` if *range* <= *other*.
- """
- self._check_title(other)
- return (
- (other.min_row <= self.min_row <= self.max_row <= other.max_row)
- and
- (other.min_col <= self.min_col <= self.max_col <= other.max_col)
- )
- __le__ = issubset
- def __lt__(self, other):
- """
- Test whether *other* contains every cell of this range, and more.
- :type other: openpyxl.worksheet.cell_range.CellRange
- :param other: Other sheet range
- :return: ``True`` if *range* < *other*.
- """
- return self.__le__(other) and self.__ne__(other)
- def issuperset(self, other):
- """
- Test whether every cell in *other* is in this range.
- :type other: openpyxl.worksheet.cell_range.CellRange
- :param other: Other sheet range
- :return: ``True`` if *range* >= *other* (or *other* in *range*).
- """
- self._check_title(other)
- return (
- (self.min_row <= other.min_row <= other.max_row <= self.max_row)
- and
- (self.min_col <= other.min_col <= other.max_col <= self.max_col)
- )
- __ge__ = issuperset
- def __contains__(self, coord):
- """
- Check whether the range contains a particular cell coordinate
- """
- cr = self.__class__(coord)
- if cr.title is None:
- cr.title = self.title
- return self.issuperset(cr)
- def __gt__(self, other):
- """
- Test whether this range contains every cell in *other*, and more.
- :type other: openpyxl.worksheet.cell_range.CellRange
- :param other: Other sheet range
- :return: ``True`` if *range* > *other*.
- """
- return self.__ge__(other) and self.__ne__(other)
- def isdisjoint(self, other):
- """
- Return ``True`` if this range has no cell in common with *other*.
- Ranges are disjoint if and only if their intersection is the empty range.
- :type other: openpyxl.worksheet.cell_range.CellRange
- :param other: Other sheet range.
- :return: ``True`` if the range has no cells in common with other.
- """
- self._check_title(other)
- # Sort by top-left vertex
- if self.bounds > other.bounds:
- self, other = other, self
- return (self.max_col < other.min_col
- or self.max_row < other.min_row
- or other.max_row < self.min_row)
- def intersection(self, other):
- """
- Return a new range with cells common to this range and *other*
- :type other: openpyxl.worksheet.cell_range.CellRange
- :param other: Other sheet range.
- :return: the intersecting sheet range.
- :raise: :class:`ValueError` if the *other* range doesn't intersect
- with this range.
- """
- if self.isdisjoint(other):
- raise ValueError("Range {0} doesn't intersect {0}".format(self, other))
- min_row = max(self.min_row, other.min_row)
- max_row = min(self.max_row, other.max_row)
- min_col = max(self.min_col, other.min_col)
- max_col = min(self.max_col, other.max_col)
- return CellRange(min_col=min_col, min_row=min_row, max_col=max_col,
- max_row=max_row)
- __and__ = intersection
- def union(self, other):
- """
- Return the minimal superset of this range and *other*. This new range
- will contain all cells from this range, *other*, and any additional
- cells required to form a rectangular ``CellRange``.
- :type other: openpyxl.worksheet.cell_range.CellRange
- :param other: Other sheet range.
- :return: a ``CellRange`` that is a superset of this and *other*.
- """
- self._check_title(other)
- min_row = min(self.min_row, other.min_row)
- max_row = max(self.max_row, other.max_row)
- min_col = min(self.min_col, other.min_col)
- max_col = max(self.max_col, other.max_col)
- return CellRange(min_col=min_col, min_row=min_row, max_col=max_col,
- max_row=max_row, title=self.title)
- __or__ = union
- def __iter__(self):
- """
- For use as a dictionary elsewhere in the library.
- """
- for x in self.__attrs__:
- if x == "title":
- continue
- v = getattr(self, x)
- yield x, v
- def expand(self, right=0, down=0, left=0, up=0):
- """
- Expand the range by the dimensions provided.
- :type right: int
- :param right: expand range to the right by this number of cells
- :type down: int
- :param down: expand range down by this number of cells
- :type left: int
- :param left: expand range to the left by this number of cells
- :type up: int
- :param up: expand range up by this number of cells
- """
- self.min_col -= left
- self.min_row -= up
- self.max_col += right
- self.max_row += down
- def shrink(self, right=0, bottom=0, left=0, top=0):
- """
- Shrink the range by the dimensions provided.
- :type right: int
- :param right: shrink range from the right by this number of cells
- :type down: int
- :param down: shrink range from the top by this number of cells
- :type left: int
- :param left: shrink range from the left by this number of cells
- :type up: int
- :param up: shrink range from the bottown by this number of cells
- """
- self.min_col += left
- self.min_row += top
- self.max_col -= right
- self.max_row -= bottom
- @property
- def size(self):
- """ Return the size of the range as a dictionary of rows and columns. """
- cols = self.max_col + 1 - self.min_col
- rows = self.max_row + 1 - self.min_row
- return {'columns':cols, 'rows':rows}
- @property
- def top(self):
- """A list of cell coordinates that comprise the top of the range"""
- return [(self.min_row, col) for col in range(self.min_col, self.max_col+1)]
- @property
- def bottom(self):
- """A list of cell coordinates that comprise the bottom of the range"""
- return [(self.max_row, col) for col in range(self.min_col, self.max_col+1)]
- @property
- def left(self):
- """A list of cell coordinates that comprise the left-side of the range"""
- return [(row, self.min_col) for row in range(self.min_row, self.max_row+1)]
- @property
- def right(self):
- """A list of cell coordinates that comprise the right-side of the range"""
- return [(row, self.max_col) for row in range(self.min_row, self.max_row+1)]
- class MultiCellRange(Strict):
- ranges = Sequence(expected_type=CellRange)
- def __init__(self, ranges=()):
- if isinstance(ranges, str):
- ranges = [CellRange(r) for r in ranges.split()]
- self.ranges = ranges
- def __contains__(self, coord):
- for r in self.ranges:
- if coord in r:
- return True
- return False
- def __repr__(self):
- ranges = " ".join([str(r) for r in self.ranges])
- return "<{0} [{1}]>".format(self.__class__.__name__, ranges)
- def __str__(self):
- ranges = u" ".join([str(r) for r in self.ranges])
- return ranges
- __unicode__ = __str__
- def add(self, coord):
- """
- Add a cell coordinate or CellRange
- """
- cr = None
- if isinstance(coord, CellRange):
- cr = coord
- coord = cr.coord
- if coord not in self:
- if cr is None:
- cr = CellRange(coord)
- ranges = self.ranges
- ranges.append(cr)
- self.ranges = ranges
- def __iadd__(self, coord):
- self.add(coord)
- return self
- def __eq__(self, other):
- if isinstance(other, str):
- other = self.__class__(other)
- return self.ranges == other.ranges
- def __ne__(self, other):
- return not self == other
- def __bool__(self):
- return bool(self.ranges)
- __nonzero__ = __bool__
- def remove(self, coord):
- if not isinstance(coord, CellRange):
- coord = CellRange(coord)
- self.ranges.remove(coord)
- def __iter__(self):
- for cr in self.ranges:
- yield cr
- def __copy__(self):
- n = MultiCellRange()
- for r in self.ranges:
- n.ranges.append(copy(r))
- return n
|