hyb
2025-10-24 6861b499efcd43195796ee314c96124b34d1a327
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# Copyright (c) 2010-2024 openpyxl
 
from copy import copy
from operator import attrgetter
 
from openpyxl.descriptors import Strict
from openpyxl.descriptors import MinMax
from openpyxl.descriptors.sequence import UniqueSequence
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)]
 
 
    @property
    def cells(self):
        from itertools import product
        return product(range(self.min_row, self.max_row+1), range(self.min_col, self.max_col+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 fmt.format(cls=self.__class__.__name__, title=self.title, coord=self.coord)
 
 
    def __hash__(self):
        return hash((self.min_row, self.min_col, self.max_row, self.max_col))
 
 
    def __str__(self):
        fmt = "{coord}"
        title = self.title
        if title:
            fmt = u"{title}!{coord}"
            title = quote_sheetname(title)
        return fmt.format(title=title, coord=self.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.__superset(self)
 
    __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 __superset(self, 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)
        )
 
 
    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.__superset(other)
 
    __ge__ = issuperset
 
 
    def __contains__(self, coord):
        """
        Check whether the range contains a particular cell coordinate
        """
        cr = self.__class__(coord)
        return self.__superset(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 bottom 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 = UniqueSequence(expected_type=CellRange)
 
 
    def __init__(self, ranges=set()):
        if isinstance(ranges, str):
            ranges = [CellRange(r) for r in ranges.split()]
        self.ranges = set(ranges)
 
 
    def __contains__(self, coord):
        if isinstance(coord, str):
            coord = CellRange(coord)
        for r in self.ranges:
            if coord <= r:
                return True
        return False
 
 
    def __repr__(self):
        ranges = " ".join([str(r) for r in self.sorted()])
        return f"<{self.__class__.__name__} [{ranges}]>"
 
 
    def __str__(self):
        ranges = u" ".join([str(r) for r in self.sorted()])
        return ranges
 
 
    def __hash__(self):
        return hash(str(self))
 
 
    def sorted(self):
        """
        Return a sorted list of items
        """
        return sorted(self.ranges, key=attrgetter('min_col', 'min_row', 'max_col', 'max_row'))
 
 
    def add(self, coord):
        """
        Add a cell coordinate or CellRange
        """
        cr = coord
        if isinstance(coord, str):
            cr = CellRange(coord)
        elif not isinstance(coord, CellRange):
            raise ValueError("You can only add CellRanges")
        if cr not in self:
            self.ranges.add(cr)
 
 
    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)
 
 
    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):
        ranges = {copy(r) for r in self.ranges}
        return MultiCellRange(ranges)