Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/openpyxl/worksheet/cell_range.py: 26%

193 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1# Copyright (c) 2010-2022 openpyxl 

2 

3from copy import copy 

4 

5from openpyxl.descriptors import Strict 

6from openpyxl.descriptors import MinMax, Sequence 

7from openpyxl.descriptors.serialisable import Serialisable 

8 

9from openpyxl.utils import ( 

10 range_boundaries, 

11 range_to_tuple, 

12 get_column_letter, 

13 quote_sheetname, 

14) 

15 

16 

17class CellRange(Serialisable): 

18 """ 

19 Represents a range in a sheet: title and coordinates. 

20 

21 This object is used to perform operations on ranges, like: 

22 

23 - shift, expand or shrink 

24 - union/intersection with another sheet range, 

25 

26 We can check whether a range is: 

27 

28 - equal or not equal to another, 

29 - disjoint of another, 

30 - contained in another. 

31 

32 We can get: 

33 

34 - the size of a range. 

35 - the range bounds (vertices) 

36 - the coordinates, 

37 - the string representation, 

38 

39 """ 

40 

41 min_col = MinMax(min=1, max=18278, expected_type=int) 

42 min_row = MinMax(min=1, max=1048576, expected_type=int) 

43 max_col = MinMax(min=1, max=18278, expected_type=int) 

44 max_row = MinMax(min=1, max=1048576, expected_type=int) 

45 

46 

47 def __init__(self, range_string=None, min_col=None, min_row=None, 

48 max_col=None, max_row=None, title=None): 

49 if range_string is not None: 

50 if "!" in range_string: 

51 title, (min_col, min_row, max_col, max_row) = range_to_tuple(range_string) 

52 else: 

53 min_col, min_row, max_col, max_row = range_boundaries(range_string) 

54 

55 self.min_col = min_col 

56 self.min_row = min_row 

57 self.max_col = max_col 

58 self.max_row = max_row 

59 self.title = title 

60 

61 if min_col > max_col: 

62 fmt = "{max_col} must be greater than {min_col}" 

63 raise ValueError(fmt.format(min_col=min_col, max_col=max_col)) 

64 if min_row > max_row: 

65 fmt = "{max_row} must be greater than {min_row}" 

66 raise ValueError(fmt.format(min_row=min_row, max_row=max_row)) 

67 

68 

69 @property 

70 def bounds(self): 

71 """ 

72 Vertices of the range as a tuple 

73 """ 

74 return self.min_col, self.min_row, self.max_col, self.max_row 

75 

76 

77 @property 

78 def coord(self): 

79 """ 

80 Excel-style representation of the range 

81 """ 

82 fmt = "{min_col}{min_row}:{max_col}{max_row}" 

83 if (self.min_col == self.max_col 

84 and self.min_row == self.max_row): 

85 fmt = "{min_col}{min_row}" 

86 

87 return fmt.format( 

88 min_col=get_column_letter(self.min_col), 

89 min_row=self.min_row, 

90 max_col=get_column_letter(self.max_col), 

91 max_row=self.max_row 

92 ) 

93 

94 @property 

95 def rows(self): 

96 """ 

97 Return cell coordinates as rows 

98 """ 

99 for row in range(self.min_row, self.max_row+1): 

100 yield [(row, col) for col in range(self.min_col, self.max_col+1)] 

101 

102 

103 @property 

104 def cols(self): 

105 """ 

106 Return cell coordinates as columns 

107 """ 

108 for col in range(self.min_col, self.max_col+1): 

109 yield [(row, col) for row in range(self.min_row, self.max_row+1)] 

110 

111 

112 @property 

113 def cells(self): 

114 from itertools import product 

115 return product(range(self.min_row, self.max_row+1), range(self.min_col, self.max_col+1)) 

116 

117 

118 def _check_title(self, other): 

119 """ 

120 Check whether comparisons between ranges are possible. 

121 Cannot compare ranges from different worksheets 

122 Skip if the range passed in has no title. 

123 """ 

124 if not isinstance(other, CellRange): 

125 raise TypeError(repr(type(other))) 

126 

127 if other.title and self.title != other.title: 

128 raise ValueError("Cannot work with ranges from different worksheets") 

129 

130 

131 def __repr__(self): 

132 fmt = u"<{cls} {coord}>" 

133 if self.title: 

134 fmt = u"<{cls} {title!r}!{coord}>" 

135 return fmt.format(cls=self.__class__.__name__, title=self.title, coord=self.coord) 

136 

137 

138 def __str__(self): 

139 fmt = "{coord}" 

140 title = self.title 

141 if title: 

142 fmt = u"{title}!{coord}" 

143 title = quote_sheetname(title) 

144 return fmt.format(title=title, coord=self.coord) 

145 

146 

147 def __copy__(self): 

148 return self.__class__(min_col=self.min_col, min_row=self.min_row, 

149 max_col=self.max_col, max_row=self.max_row, 

150 title=self.title) 

151 

152 

153 def shift(self, col_shift=0, row_shift=0): 

154 """ 

155 Shift the focus of the range according to the shift values (*col_shift*, *row_shift*). 

156 

157 :type col_shift: int 

158 :param col_shift: number of columns to be moved by, can be negative 

159 :type row_shift: int 

160 :param row_shift: number of rows to be moved by, can be negative 

161 :raise: :class:`ValueError` if any row or column index < 1 

162 """ 

163 

164 if (self.min_col + col_shift <= 0 

165 or self.min_row + row_shift <= 0): 

166 raise ValueError("Invalid shift value: col_shift={0}, row_shift={1}".format(col_shift, row_shift)) 

167 self.min_col += col_shift 

168 self.min_row += row_shift 

169 self.max_col += col_shift 

170 self.max_row += row_shift 

171 

172 

173 def __ne__(self, other): 

174 """ 

175 Test whether the ranges are not equal. 

176 

177 :type other: openpyxl.worksheet.cell_range.CellRange 

178 :param other: Other sheet range 

179 :return: ``True`` if *range* != *other*. 

180 """ 

181 try: 

182 self._check_title(other) 

183 except ValueError: 

184 return True 

185 

186 return ( 

187 other.min_row != self.min_row 

188 or self.max_row != other.max_row 

189 or other.min_col != self.min_col 

190 or self.max_col != other.max_col 

191 ) 

192 

193 

194 def __eq__(self, other): 

195 """ 

196 Test whether the ranges are equal. 

197 

198 :type other: openpyxl.worksheet.cell_range.CellRange 

199 :param other: Other sheet range 

200 :return: ``True`` if *range* == *other*. 

201 """ 

202 return not self.__ne__(other) 

203 

204 

205 def issubset(self, other): 

206 """ 

207 Test whether every cell in this range is also in *other*. 

208 

209 :type other: openpyxl.worksheet.cell_range.CellRange 

210 :param other: Other sheet range 

211 :return: ``True`` if *range* <= *other*. 

212 """ 

213 self._check_title(other) 

214 

215 return other.__superset(self) 

216 

217 __le__ = issubset 

218 

219 

220 def __lt__(self, other): 

221 """ 

222 Test whether *other* contains every cell of this range, and more. 

223 

224 :type other: openpyxl.worksheet.cell_range.CellRange 

225 :param other: Other sheet range 

226 :return: ``True`` if *range* < *other*. 

227 """ 

228 return self.__le__(other) and self.__ne__(other) 

229 

230 

231 def __superset(self, other): 

232 return ( 

233 (self.min_row <= other.min_row <= other.max_row <= self.max_row) 

234 and 

235 (self.min_col <= other.min_col <= other.max_col <= self.max_col) 

236 ) 

237 

238 

239 def issuperset(self, other): 

240 """ 

241 Test whether every cell in *other* is in this range. 

242 

243 :type other: openpyxl.worksheet.cell_range.CellRange 

244 :param other: Other sheet range 

245 :return: ``True`` if *range* >= *other* (or *other* in *range*). 

246 """ 

247 self._check_title(other) 

248 

249 return self.__superset(other) 

250 

251 __ge__ = issuperset 

252 

253 

254 def __contains__(self, coord): 

255 """ 

256 Check whether the range contains a particular cell coordinate 

257 """ 

258 cr = self.__class__(coord) 

259 return self.__superset(cr) 

260 

261 

262 def __gt__(self, other): 

263 """ 

264 Test whether this range contains every cell in *other*, and more. 

265 

266 :type other: openpyxl.worksheet.cell_range.CellRange 

267 :param other: Other sheet range 

268 :return: ``True`` if *range* > *other*. 

269 """ 

270 return self.__ge__(other) and self.__ne__(other) 

271 

272 

273 def isdisjoint(self, other): 

274 """ 

275 Return ``True`` if this range has no cell in common with *other*. 

276 Ranges are disjoint if and only if their intersection is the empty range. 

277 

278 :type other: openpyxl.worksheet.cell_range.CellRange 

279 :param other: Other sheet range. 

280 :return: ``True`` if the range has no cells in common with other. 

281 """ 

282 self._check_title(other) 

283 

284 # Sort by top-left vertex 

285 if self.bounds > other.bounds: 

286 self, other = other, self 

287 

288 return (self.max_col < other.min_col 

289 or self.max_row < other.min_row 

290 or other.max_row < self.min_row) 

291 

292 

293 def intersection(self, other): 

294 """ 

295 Return a new range with cells common to this range and *other* 

296 

297 :type other: openpyxl.worksheet.cell_range.CellRange 

298 :param other: Other sheet range. 

299 :return: the intersecting sheet range. 

300 :raise: :class:`ValueError` if the *other* range doesn't intersect 

301 with this range. 

302 """ 

303 if self.isdisjoint(other): 

304 raise ValueError("Range {0} doesn't intersect {0}".format(self, other)) 

305 

306 min_row = max(self.min_row, other.min_row) 

307 max_row = min(self.max_row, other.max_row) 

308 min_col = max(self.min_col, other.min_col) 

309 max_col = min(self.max_col, other.max_col) 

310 

311 return CellRange(min_col=min_col, min_row=min_row, max_col=max_col, 

312 max_row=max_row) 

313 

314 __and__ = intersection 

315 

316 

317 def union(self, other): 

318 """ 

319 Return the minimal superset of this range and *other*. This new range 

320 will contain all cells from this range, *other*, and any additional 

321 cells required to form a rectangular ``CellRange``. 

322 

323 :type other: openpyxl.worksheet.cell_range.CellRange 

324 :param other: Other sheet range. 

325 :return: a ``CellRange`` that is a superset of this and *other*. 

326 """ 

327 self._check_title(other) 

328 

329 min_row = min(self.min_row, other.min_row) 

330 max_row = max(self.max_row, other.max_row) 

331 min_col = min(self.min_col, other.min_col) 

332 max_col = max(self.max_col, other.max_col) 

333 return CellRange(min_col=min_col, min_row=min_row, max_col=max_col, 

334 max_row=max_row, title=self.title) 

335 

336 __or__ = union 

337 

338 

339 def __iter__(self): 

340 """ 

341 For use as a dictionary elsewhere in the library. 

342 """ 

343 for x in self.__attrs__: 

344 if x == "title": 

345 continue 

346 v = getattr(self, x) 

347 yield x, v 

348 

349 

350 def expand(self, right=0, down=0, left=0, up=0): 

351 """ 

352 Expand the range by the dimensions provided. 

353 

354 :type right: int 

355 :param right: expand range to the right by this number of cells 

356 :type down: int 

357 :param down: expand range down by this number of cells 

358 :type left: int 

359 :param left: expand range to the left by this number of cells 

360 :type up: int 

361 :param up: expand range up by this number of cells 

362 """ 

363 self.min_col -= left 

364 self.min_row -= up 

365 self.max_col += right 

366 self.max_row += down 

367 

368 

369 def shrink(self, right=0, bottom=0, left=0, top=0): 

370 """ 

371 Shrink the range by the dimensions provided. 

372 

373 :type right: int 

374 :param right: shrink range from the right by this number of cells 

375 :type down: int 

376 :param down: shrink range from the top by this number of cells 

377 :type left: int 

378 :param left: shrink range from the left by this number of cells 

379 :type up: int 

380 :param up: shrink range from the bottown by this number of cells 

381 """ 

382 self.min_col += left 

383 self.min_row += top 

384 self.max_col -= right 

385 self.max_row -= bottom 

386 

387 

388 @property 

389 def size(self): 

390 """ Return the size of the range as a dictionary of rows and columns. """ 

391 cols = self.max_col + 1 - self.min_col 

392 rows = self.max_row + 1 - self.min_row 

393 return {'columns':cols, 'rows':rows} 

394 

395 

396 @property 

397 def top(self): 

398 """A list of cell coordinates that comprise the top of the range""" 

399 return [(self.min_row, col) for col in range(self.min_col, self.max_col+1)] 

400 

401 

402 @property 

403 def bottom(self): 

404 """A list of cell coordinates that comprise the bottom of the range""" 

405 return [(self.max_row, col) for col in range(self.min_col, self.max_col+1)] 

406 

407 

408 @property 

409 def left(self): 

410 """A list of cell coordinates that comprise the left-side of the range""" 

411 return [(row, self.min_col) for row in range(self.min_row, self.max_row+1)] 

412 

413 

414 @property 

415 def right(self): 

416 """A list of cell coordinates that comprise the right-side of the range""" 

417 return [(row, self.max_col) for row in range(self.min_row, self.max_row+1)] 

418 

419 

420class MultiCellRange(Strict): 

421 

422 

423 ranges = Sequence(expected_type=CellRange) 

424 

425 

426 def __init__(self, ranges=()): 

427 if isinstance(ranges, str): 

428 ranges = [CellRange(r) for r in ranges.split()] 

429 self.ranges = ranges 

430 

431 

432 def __contains__(self, coord): 

433 if isinstance(coord, str): 

434 coord = CellRange(coord) 

435 for r in self.ranges: 

436 if coord <= r: 

437 return True 

438 return False 

439 

440 

441 def __repr__(self): 

442 ranges = " ".join([str(r) for r in self.ranges]) 

443 return "<{0} [{1}]>".format(self.__class__.__name__, ranges) 

444 

445 

446 def __str__(self): 

447 ranges = u" ".join([str(r) for r in self.ranges]) 

448 return ranges 

449 

450 __str__ = __str__ 

451 

452 

453 def add(self, coord): 

454 """ 

455 Add a cell coordinate or CellRange 

456 """ 

457 cr = coord 

458 if isinstance(coord, str): 

459 cr = CellRange(coord) 

460 elif not isinstance(coord, CellRange): 

461 raise ValueError("You can only add CellRanges") 

462 if cr not in self: 

463 self.ranges.append(cr) 

464 

465 

466 def __iadd__(self, coord): 

467 self.add(coord) 

468 return self 

469 

470 

471 def __eq__(self, other): 

472 if isinstance(other, str): 

473 other = self.__class__(other) 

474 return self.ranges == other.ranges 

475 

476 

477 def __ne__(self, other): 

478 return not self == other 

479 

480 

481 def __bool__(self): 

482 return bool(self.ranges) 

483 

484 

485 def remove(self, coord): 

486 if not isinstance(coord, CellRange): 

487 coord = CellRange(coord) 

488 self.ranges.remove(coord) 

489 

490 

491 def __iter__(self): 

492 for cr in self.ranges: 

493 yield cr 

494 

495 

496 def __copy__(self): 

497 n = MultiCellRange() 

498 

499 for r in self.ranges: 

500 n.ranges.append(copy(r)) 

501 return n