Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xlsxwriter/worksheet.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

3765 statements  

1############################################################################### 

2# 

3# Worksheet - A class for writing the Excel XLSX Worksheet file. 

4# 

5# SPDX-License-Identifier: BSD-2-Clause 

6# 

7# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org 

8# 

9 

10# pylint: disable=too-many-return-statements 

11 

12# Standard packages. 

13import datetime 

14import math 

15import os 

16import re 

17import tempfile 

18from collections import defaultdict, namedtuple 

19from decimal import Decimal 

20from fractions import Fraction 

21from functools import wraps 

22from io import BytesIO, StringIO 

23from math import isinf, isnan 

24from typing import Any, Dict, List, Literal, Optional, Union 

25from warnings import warn 

26 

27from xlsxwriter.chart import Chart 

28from xlsxwriter.color import Color 

29from xlsxwriter.comments import CommentType 

30from xlsxwriter.image import Image 

31from xlsxwriter.url import Url, UrlTypes 

32from xlsxwriter.vml import ButtonType 

33 

34# Package imports. 

35from . import xmlwriter 

36from .drawing import Drawing, DrawingInfo, DrawingTypes 

37from .exceptions import DuplicateTableName, OverlappingRange 

38from .format import Format 

39from .shape import Shape 

40from .utility import ( 

41 _datetime_to_excel_datetime, 

42 _get_sparkline_style, 

43 _preserve_whitespace, 

44 _supported_datetime, 

45 quote_sheetname, 

46 xl_cell_to_rowcol, 

47 xl_col_to_name, 

48 xl_pixel_width, 

49 xl_range, 

50 xl_rowcol_to_cell, 

51 xl_rowcol_to_cell_fast, 

52) 

53from .xmlwriter import XMLwriter 

54 

55re_dynamic_function = re.compile( 

56 r""" 

57 \bANCHORARRAY\( | 

58 \bBYCOL\( | 

59 \bBYROW\( | 

60 \bCHOOSECOLS\( | 

61 \bCHOOSEROWS\( | 

62 \bDROP\( | 

63 \bEXPAND\( | 

64 \bFILTER\( | 

65 \bHSTACK\( | 

66 \bLAMBDA\( | 

67 \bMAKEARRAY\( | 

68 \bMAP\( | 

69 \bRANDARRAY\( | 

70 \bREDUCE\( | 

71 \bSCAN\( | 

72 \bSEQUENCE\( | 

73 \bSINGLE\( | 

74 \bSORT\( | 

75 \bSORTBY\( | 

76 \bSWITCH\( | 

77 \bTAKE\( | 

78 \bTEXTSPLIT\( | 

79 \bTOCOL\( | 

80 \bTOROW\( | 

81 \bUNIQUE\( | 

82 \bVSTACK\( | 

83 \bWRAPCOLS\( | 

84 \bWRAPROWS\( | 

85 \bXLOOKUP\(""", 

86 re.VERBOSE, 

87) 

88 

89 

90############################################################################### 

91# 

92# Decorator functions. 

93# 

94############################################################################### 

95def convert_cell_args(method): 

96 """ 

97 Decorator function to convert A1 notation in cell method calls 

98 to the default row/col notation. 

99 

100 """ 

101 

102 @wraps(method) 

103 def cell_wrapper(self, *args, **kwargs): 

104 try: 

105 # First arg is an int, default to row/col notation. 

106 if args: 

107 first_arg = args[0] 

108 int(first_arg) 

109 except ValueError: 

110 # First arg isn't an int, convert to A1 notation. 

111 new_args = xl_cell_to_rowcol(first_arg) 

112 args = new_args + args[1:] 

113 

114 return method(self, *args, **kwargs) 

115 

116 return cell_wrapper 

117 

118 

119def convert_range_args(method): 

120 """ 

121 Decorator function to convert A1 notation in range method calls 

122 to the default row/col notation. 

123 

124 """ 

125 

126 @wraps(method) 

127 def cell_wrapper(self, *args, **kwargs): 

128 try: 

129 # First arg is an int, default to row/col notation. 

130 if args: 

131 int(args[0]) 

132 except ValueError: 

133 # First arg isn't an int, convert to A1 notation. 

134 if ":" in args[0]: 

135 cell_1, cell_2 = args[0].split(":") 

136 row_1, col_1 = xl_cell_to_rowcol(cell_1) 

137 row_2, col_2 = xl_cell_to_rowcol(cell_2) 

138 else: 

139 row_1, col_1 = xl_cell_to_rowcol(args[0]) 

140 row_2, col_2 = row_1, col_1 

141 

142 new_args = [row_1, col_1, row_2, col_2] 

143 new_args.extend(args[1:]) 

144 args = new_args 

145 

146 return method(self, *args, **kwargs) 

147 

148 return cell_wrapper 

149 

150 

151def convert_column_args(method): 

152 """ 

153 Decorator function to convert A1 notation in columns method calls 

154 to the default row/col notation. 

155 

156 """ 

157 

158 @wraps(method) 

159 def column_wrapper(self, *args, **kwargs): 

160 try: 

161 # First arg is an int, default to row/col notation. 

162 if args: 

163 int(args[0]) 

164 except ValueError: 

165 # First arg isn't an int, convert to A1 notation. 

166 cell_1, cell_2 = [col + "1" for col in args[0].split(":")] 

167 _, col_1 = xl_cell_to_rowcol(cell_1) 

168 _, col_2 = xl_cell_to_rowcol(cell_2) 

169 new_args = [col_1, col_2] 

170 new_args.extend(args[1:]) 

171 args = new_args 

172 

173 return method(self, *args, **kwargs) 

174 

175 return column_wrapper 

176 

177 

178############################################################################### 

179# 

180# Named tuples used for cell types. 

181# 

182############################################################################### 

183CellBlankTuple = namedtuple("Blank", "format") 

184CellErrorTuple = namedtuple("Error", "error, format, value") 

185CellNumberTuple = namedtuple("Number", "number, format") 

186CellStringTuple = namedtuple("String", "string, format") 

187CellBooleanTuple = namedtuple("Boolean", "boolean, format") 

188CellFormulaTuple = namedtuple("Formula", "formula, format, value") 

189CellDatetimeTuple = namedtuple("Datetime", "number, format") 

190CellRichStringTuple = namedtuple("RichString", "string, format, raw_string") 

191CellArrayFormulaTuple = namedtuple( 

192 "ArrayFormula", "formula, format, value, range, atype" 

193) 

194 

195 

196############################################################################### 

197# 

198# Worksheet Class definition. 

199# 

200############################################################################### 

201class Worksheet(xmlwriter.XMLwriter): 

202 """ 

203 A class for writing the Excel XLSX Worksheet file. 

204 

205 """ 

206 

207 ########################################################################### 

208 # 

209 # Public API. 

210 # 

211 ########################################################################### 

212 

213 def __init__(self) -> None: 

214 """ 

215 Constructor. 

216 

217 """ 

218 

219 super().__init__() 

220 

221 self.name = None 

222 self.index = None 

223 self.str_table = None 

224 self.palette = None 

225 self.constant_memory = 0 

226 self.tmpdir = None 

227 self.is_chartsheet = False 

228 

229 self.ext_sheets = [] 

230 self.fileclosed = 0 

231 self.excel_version = 2007 

232 self.excel2003_style = False 

233 

234 self.xls_rowmax = 1048576 

235 self.xls_colmax = 16384 

236 self.xls_strmax = 32767 

237 self.dim_rowmin = None 

238 self.dim_rowmax = None 

239 self.dim_colmin = None 

240 self.dim_colmax = None 

241 

242 self.col_info = {} 

243 self.selections = [] 

244 self.hidden = 0 

245 self.active = 0 

246 self.tab_color = 0 

247 self.top_left_cell = "" 

248 

249 self.panes = [] 

250 self.active_pane = 3 

251 self.selected = 0 

252 

253 self.page_setup_changed = False 

254 self.paper_size = 0 

255 self.orientation = 1 

256 

257 self.print_options_changed = False 

258 self.hcenter = False 

259 self.vcenter = False 

260 self.print_gridlines = False 

261 self.screen_gridlines = True 

262 self.print_headers = False 

263 self.row_col_headers = False 

264 

265 self.header_footer_changed = False 

266 self.header = "" 

267 self.footer = "" 

268 self.header_footer_aligns = True 

269 self.header_footer_scales = True 

270 self.header_images = [] 

271 self.footer_images = [] 

272 self.header_images_list = [] 

273 

274 self.margin_left = 0.7 

275 self.margin_right = 0.7 

276 self.margin_top = 0.75 

277 self.margin_bottom = 0.75 

278 self.margin_header = 0.3 

279 self.margin_footer = 0.3 

280 

281 self.repeat_row_range = "" 

282 self.repeat_col_range = "" 

283 self.print_area_range = "" 

284 

285 self.page_order = 0 

286 self.black_white = 0 

287 self.draft_quality = 0 

288 self.print_comments = 0 

289 self.page_start = 0 

290 

291 self.fit_page = 0 

292 self.fit_width = 0 

293 self.fit_height = 0 

294 

295 self.hbreaks = [] 

296 self.vbreaks = [] 

297 

298 self.protect_options = {} 

299 self.protected_ranges = [] 

300 self.num_protected_ranges = 0 

301 self.set_cols = {} 

302 self.set_rows = defaultdict(dict) 

303 

304 self.zoom = 100 

305 self.zoom_scale_normal = True 

306 self.zoom_to_fit = False 

307 self.print_scale = 100 

308 self.is_right_to_left = False 

309 self.show_zeros = 1 

310 self.leading_zeros = 0 

311 

312 self.outline_row_level = 0 

313 self.outline_col_level = 0 

314 self.outline_style = 0 

315 self.outline_below = 1 

316 self.outline_right = 1 

317 self.outline_on = 1 

318 self.outline_changed = False 

319 

320 self.original_row_height = 15 

321 self.default_row_height = 15 

322 self.default_row_pixels = 20 

323 self.default_col_width = 8.43 

324 self.default_col_pixels = 64 

325 self.default_date_pixels = 68 

326 self.default_row_zeroed = 0 

327 

328 self.names = {} 

329 self.write_match = [] 

330 self.table = defaultdict(dict) 

331 self.merge = [] 

332 self.merged_cells = {} 

333 self.table_cells = {} 

334 self.row_spans = {} 

335 

336 self.has_vml = False 

337 self.has_header_vml = False 

338 self.has_comments = False 

339 self.comments = defaultdict(dict) 

340 self.comments_list = [] 

341 self.comments_author = "" 

342 self.comments_visible = False 

343 self.vml_shape_id = 1024 

344 self.buttons_list = [] 

345 self.vml_header_id = 0 

346 

347 self.autofilter_area = "" 

348 self.autofilter_ref = None 

349 self.filter_range = [0, 9] 

350 self.filter_on = 0 

351 self.filter_cols = {} 

352 self.filter_type = {} 

353 self.filter_cells = {} 

354 

355 self.row_sizes = {} 

356 self.col_size_changed = False 

357 self.row_size_changed = False 

358 

359 self.last_shape_id = 1 

360 self.rel_count = 0 

361 self.hlink_count = 0 

362 self.hlink_refs = [] 

363 self.external_hyper_links = [] 

364 self.external_drawing_links = [] 

365 self.external_comment_links = [] 

366 self.external_vml_links = [] 

367 self.external_table_links = [] 

368 self.external_background_links = [] 

369 self.drawing_links = [] 

370 self.vml_drawing_links = [] 

371 self.charts = [] 

372 self.images = [] 

373 self.tables = [] 

374 self.sparklines = [] 

375 self.shapes = [] 

376 self.shape_hash = {} 

377 self.drawing = 0 

378 self.drawing_rels = {} 

379 self.drawing_rels_id = 0 

380 self.vml_drawing_rels = {} 

381 self.vml_drawing_rels_id = 0 

382 self.background_image = None 

383 

384 self.rstring = "" 

385 self.previous_row = 0 

386 

387 self.validations = [] 

388 self.cond_formats = {} 

389 self.data_bars_2010 = [] 

390 self.use_data_bars_2010 = False 

391 self.dxf_priority = 1 

392 self.page_view = 0 

393 

394 self.vba_codename = None 

395 

396 self.date_1904 = False 

397 self.hyperlinks = defaultdict(dict) 

398 

399 self.strings_to_numbers = False 

400 self.strings_to_urls = True 

401 self.nan_inf_to_errors = False 

402 self.strings_to_formulas = True 

403 

404 self.default_date_format = None 

405 self.default_url_format = None 

406 self.default_checkbox_format = None 

407 self.workbook_add_format = None 

408 self.remove_timezone = False 

409 self.max_url_length = 2079 

410 

411 self.row_data_filename = None 

412 self.row_data_fh = None 

413 self.worksheet_meta = None 

414 self.vml_data_id = None 

415 self.vml_shape_id = None 

416 

417 self.row_data_filename = None 

418 self.row_data_fh = None 

419 self.row_data_fh_closed = False 

420 

421 self.vertical_dpi = 0 

422 self.horizontal_dpi = 0 

423 

424 self.write_handlers = {} 

425 

426 self.ignored_errors = None 

427 

428 self.has_dynamic_arrays = False 

429 self.use_future_functions = False 

430 self.ignore_write_string = False 

431 self.embedded_images = None 

432 

433 # Utility function for writing different types of strings. 

434 def _write_token_as_string(self, token, row: int, col: int, *args): 

435 # Map the data to the appropriate write_*() method. 

436 if token == "": 

437 return self._write_blank(row, col, *args) 

438 

439 if self.strings_to_formulas and token.startswith("="): 

440 return self._write_formula(row, col, *args) 

441 

442 if token.startswith("{=") and token.endswith("}"): 

443 return self._write_formula(row, col, *args) 

444 

445 # pylint: disable=too-many-boolean-expressions 

446 if ( 

447 ":" in token 

448 and self.strings_to_urls 

449 and ( 

450 re.match("(ftp|http)s?://", token) 

451 or re.match("mailto:", token) 

452 or re.match("(in|ex)ternal:", token) 

453 or re.match("file://", token) 

454 ) 

455 ): 

456 return self._write_url(row, col, *args) 

457 

458 if self.strings_to_numbers: 

459 try: 

460 f = float(token) 

461 if self.nan_inf_to_errors or (not isnan(f) and not isinf(f)): 

462 return self._write_number(row, col, f, *args[1:]) 

463 except ValueError: 

464 # Not a number, write as a string. 

465 pass 

466 

467 return self._write_string(row, col, *args) 

468 

469 # We have a plain string. 

470 return self._write_string(row, col, *args) 

471 

472 @convert_cell_args 

473 def write(self, row: int, col: int, *args) -> Union[Literal[0, -1], Any]: 

474 """ 

475 Write data to a worksheet cell by calling the appropriate write_*() 

476 method based on the type of data being passed. 

477 

478 Args: 

479 row: The cell row (zero indexed). 

480 col: The cell column (zero indexed). 

481 *args: Args to pass to sub functions. 

482 

483 Returns: 

484 0: Success. 

485 -1: Row or column is out of worksheet bounds. 

486 other: Return value of called method. 

487 

488 """ 

489 return self._write(row, col, *args) 

490 

491 # Undecorated version of write(). 

492 def _write(self, row: int, col: int, *args): 

493 # pylint: disable=raise-missing-from 

494 # Check the number of args passed. 

495 if not args: 

496 raise TypeError("write() takes at least 4 arguments (3 given)") 

497 

498 # The first arg should be the token for all write calls. 

499 token = args[0] 

500 

501 # Avoid isinstance() for better performance. 

502 token_type = token.__class__ 

503 

504 # Check for any user defined type handlers with callback functions. 

505 if token_type in self.write_handlers: 

506 write_handler = self.write_handlers[token_type] 

507 function_return = write_handler(self, row, col, *args) 

508 

509 # If the return value is None then the callback has returned 

510 # control to this function and we should continue as 

511 # normal. Otherwise we return the value to the caller and exit. 

512 if function_return is None: 

513 pass 

514 else: 

515 return function_return 

516 

517 # Write None as a blank cell. 

518 if token is None: 

519 return self._write_blank(row, col, *args) 

520 

521 # Check for standard Python types. 

522 if token_type is bool: 

523 return self._write_boolean(row, col, *args) 

524 

525 if token_type in (float, int, Decimal, Fraction): 

526 return self._write_number(row, col, *args) 

527 

528 if token_type is str: 

529 return self._write_token_as_string(token, row, col, *args) 

530 

531 if token_type in ( 

532 datetime.datetime, 

533 datetime.date, 

534 datetime.time, 

535 datetime.timedelta, 

536 ): 

537 return self._write_datetime(row, col, *args) 

538 

539 # Resort to isinstance() for subclassed primitives. 

540 

541 # Write number types. 

542 if isinstance(token, (float, int, Decimal, Fraction)): 

543 return self._write_number(row, col, *args) 

544 

545 # Write string types. 

546 if isinstance(token, str): 

547 return self._write_token_as_string(token, row, col, *args) 

548 

549 # Write boolean types. 

550 if isinstance(token, bool): 

551 return self._write_boolean(row, col, *args) 

552 

553 # Write datetime objects. 

554 if _supported_datetime(token): 

555 return self._write_datetime(row, col, *args) 

556 

557 # Write Url type. 

558 if isinstance(token, Url): 

559 return self._write_url(row, col, *args) 

560 

561 # We haven't matched a supported type. Try float. 

562 try: 

563 f = float(token) 

564 return self._write_number(row, col, f, *args[1:]) 

565 except ValueError: 

566 pass 

567 except TypeError: 

568 raise TypeError(f"Unsupported type {type(token)} in write()") 

569 

570 # Finally try string. 

571 try: 

572 str(token) 

573 return self._write_string(row, col, *args) 

574 except ValueError: 

575 raise TypeError(f"Unsupported type {type(token)} in write()") 

576 

577 @convert_cell_args 

578 def write_string( 

579 self, row: int, col: int, string: str, cell_format: Optional[Format] = None 

580 ) -> Literal[0, -1, -2]: 

581 """ 

582 Write a string to a worksheet cell. 

583 

584 Args: 

585 row: The cell row (zero indexed). 

586 col: The cell column (zero indexed). 

587 string: Cell data. Str. 

588 format: An optional cell Format object. 

589 

590 Returns: 

591 0: Success. 

592 -1: Row or column is out of worksheet bounds. 

593 -2: String truncated to 32k characters. 

594 

595 """ 

596 return self._write_string(row, col, string, cell_format) 

597 

598 # Undecorated version of write_string(). 

599 def _write_string( 

600 self, row: int, col: int, string: str, cell_format: Optional[Format] = None 

601 ) -> Literal[0, -1, -2]: 

602 str_error = 0 

603 

604 # Check that row and col are valid and store max and min values. 

605 if self._check_dimensions(row, col): 

606 return -1 

607 

608 # Check that the string is < 32767 chars. 

609 if len(string) > self.xls_strmax: 

610 string = string[: self.xls_strmax] 

611 str_error = -2 

612 

613 # Write a shared string or an in-line string in constant_memory mode. 

614 if not self.constant_memory: 

615 string_index = self.str_table._get_shared_string_index(string) 

616 else: 

617 string_index = string 

618 

619 # Write previous row if in in-line string constant_memory mode. 

620 if self.constant_memory and row > self.previous_row: 

621 self._write_single_row(row) 

622 

623 # Store the cell data in the worksheet data table. 

624 self.table[row][col] = CellStringTuple(string_index, cell_format) 

625 

626 return str_error 

627 

628 @convert_cell_args 

629 def write_number( 

630 self, 

631 row: int, 

632 col: int, 

633 number: Union[int, float, Fraction], 

634 cell_format: Optional[Format] = None, 

635 ) -> Literal[0, -1]: 

636 """ 

637 Write a number to a worksheet cell. 

638 

639 Args: 

640 row: The cell row (zero indexed). 

641 col: The cell column (zero indexed). 

642 number: Cell data. Int or float. 

643 cell_format: An optional cell Format object. 

644 

645 Returns: 

646 0: Success. 

647 -1: Row or column is out of worksheet bounds. 

648 

649 """ 

650 return self._write_number(row, col, number, cell_format) 

651 

652 # Undecorated version of write_number(). 

653 def _write_number( 

654 self, 

655 row: int, 

656 col: int, 

657 number: Union[int, float, Fraction], 

658 cell_format: Optional[Format] = None, 

659 ) -> Literal[0, -1]: 

660 if isnan(number) or isinf(number): 

661 if self.nan_inf_to_errors: 

662 if isnan(number): 

663 return self._write_formula(row, col, "#NUM!", cell_format, "#NUM!") 

664 

665 if number == math.inf: 

666 return self._write_formula(row, col, "1/0", cell_format, "#DIV/0!") 

667 

668 if number == -math.inf: 

669 return self._write_formula(row, col, "-1/0", cell_format, "#DIV/0!") 

670 else: 

671 raise TypeError( 

672 "NAN/INF not supported in write_number() " 

673 "without 'nan_inf_to_errors' Workbook() option" 

674 ) 

675 

676 if number.__class__ is Fraction: 

677 number = float(number) 

678 

679 # Check that row and col are valid and store max and min values. 

680 if self._check_dimensions(row, col): 

681 return -1 

682 

683 # Write previous row if in in-line string constant_memory mode. 

684 if self.constant_memory and row > self.previous_row: 

685 self._write_single_row(row) 

686 

687 # Store the cell data in the worksheet data table. 

688 self.table[row][col] = CellNumberTuple(number, cell_format) 

689 

690 return 0 

691 

692 @convert_cell_args 

693 def write_blank( 

694 self, row: int, col: int, blank: Any, cell_format: Optional[Format] = None 

695 ): 

696 """ 

697 Write a blank cell with formatting to a worksheet cell. The blank 

698 token is ignored and the format only is written to the cell. 

699 

700 Args: 

701 row: The cell row (zero indexed). 

702 col: The cell column (zero indexed). 

703 blank: Any value. It is ignored. 

704 cell_format: An optional cell Format object. 

705 

706 Returns: 

707 0: Success. 

708 -1: Row or column is out of worksheet bounds. 

709 

710 """ 

711 return self._write_blank(row, col, blank, cell_format) 

712 

713 # Undecorated version of write_blank(). 

714 def _write_blank( 

715 self, row: int, col: int, _, cell_format: Optional[Format] = None 

716 ) -> Literal[0, -1]: 

717 # Don't write a blank cell unless it has a format. 

718 if cell_format is None: 

719 return 0 

720 

721 # Check that row and col are valid and store max and min values. 

722 if self._check_dimensions(row, col): 

723 return -1 

724 

725 # Write previous row if in in-line string constant_memory mode. 

726 if self.constant_memory and row > self.previous_row: 

727 self._write_single_row(row) 

728 

729 # Store the cell data in the worksheet data table. 

730 self.table[row][col] = CellBlankTuple(cell_format) 

731 

732 return 0 

733 

734 @convert_cell_args 

735 def write_formula( 

736 self, 

737 row: int, 

738 col: int, 

739 formula: str, 

740 cell_format: Optional[Format] = None, 

741 value=0, 

742 ) -> Literal[0, -1, -2]: 

743 """ 

744 Write a formula to a worksheet cell. 

745 

746 Args: 

747 row: The cell row (zero indexed). 

748 col: The cell column (zero indexed). 

749 formula: Cell formula. 

750 cell_format: An optional cell Format object. 

751 value: An optional value for the formula. Default is 0. 

752 

753 Returns: 

754 0: Success. 

755 -1: Row or column is out of worksheet bounds. 

756 -2: Formula can't be None or empty. 

757 

758 """ 

759 # Check that row and col are valid and store max and min values. 

760 return self._write_formula(row, col, formula, cell_format, value) 

761 

762 # Undecorated version of write_formula(). 

763 def _write_formula( 

764 self, 

765 row: int, 

766 col: int, 

767 formula: str, 

768 cell_format: Optional[Format] = None, 

769 value=0, 

770 ) -> Literal[0, -1, -2]: 

771 if self._check_dimensions(row, col): 

772 return -1 

773 

774 if formula is None or formula == "": 

775 warn("Formula can't be None or empty") 

776 return -1 

777 

778 # Check for dynamic array functions. 

779 if re_dynamic_function.search(formula): 

780 return self.write_dynamic_array_formula( 

781 row, col, row, col, formula, cell_format, value 

782 ) 

783 

784 # Hand off array formulas. 

785 if formula.startswith("{") and formula.endswith("}"): 

786 return self._write_array_formula( 

787 row, col, row, col, formula, cell_format, value 

788 ) 

789 

790 # Modify the formula string, as needed. 

791 formula = self._prepare_formula(formula) 

792 

793 # Write previous row if in in-line string constant_memory mode. 

794 if self.constant_memory and row > self.previous_row: 

795 self._write_single_row(row) 

796 

797 # Store the cell data in the worksheet data table. 

798 self.table[row][col] = CellFormulaTuple(formula, cell_format, value) 

799 

800 return 0 

801 

802 @convert_range_args 

803 def write_array_formula( 

804 self, 

805 first_row: int, 

806 first_col: int, 

807 last_row: int, 

808 last_col: int, 

809 formula: str, 

810 cell_format: Optional[Format] = None, 

811 value=0, 

812 ) -> Literal[0, -1]: 

813 """ 

814 Write a formula to a worksheet cell/range. 

815 

816 Args: 

817 first_row: The first row of the cell range. (zero indexed). 

818 first_col: The first column of the cell range. 

819 last_row: The last row of the cell range. (zero indexed). 

820 last_col: The last column of the cell range. 

821 formula: Cell formula. 

822 cell_format: An optional cell Format object. 

823 value: An optional value for the formula. Default is 0. 

824 

825 Returns: 

826 0: Success. 

827 -1: Row or column is out of worksheet bounds. 

828 

829 """ 

830 # Check for dynamic array functions. 

831 if re_dynamic_function.search(formula): 

832 return self.write_dynamic_array_formula( 

833 first_row, first_col, last_row, last_col, formula, cell_format, value 

834 ) 

835 

836 return self._write_array_formula( 

837 first_row, 

838 first_col, 

839 last_row, 

840 last_col, 

841 formula, 

842 cell_format, 

843 value, 

844 "static", 

845 ) 

846 

847 @convert_range_args 

848 def write_dynamic_array_formula( 

849 self, 

850 first_row: int, 

851 first_col: int, 

852 last_row: int, 

853 last_col: int, 

854 formula: str, 

855 cell_format: Optional[Format] = None, 

856 value=0, 

857 ) -> Literal[0, -1]: 

858 """ 

859 Write a dynamic array formula to a worksheet cell/range. 

860 

861 Args: 

862 first_row: The first row of the cell range. (zero indexed). 

863 first_col: The first column of the cell range. 

864 last_row: The last row of the cell range. (zero indexed). 

865 last_col: The last column of the cell range. 

866 formula: Cell formula. 

867 cell_format: An optional cell Format object. 

868 value: An optional value for the formula. Default is 0. 

869 

870 Returns: 

871 0: Success. 

872 -1: Row or column is out of worksheet bounds. 

873 

874 """ 

875 error = self._write_array_formula( 

876 first_row, 

877 first_col, 

878 last_row, 

879 last_col, 

880 formula, 

881 cell_format, 

882 value, 

883 "dynamic", 

884 ) 

885 

886 if error == 0: 

887 self.has_dynamic_arrays = True 

888 

889 return error 

890 

891 # Utility method to strip equal sign and array braces from a formula and 

892 # also expand out future and dynamic array formulas. 

893 def _prepare_formula(self, formula, expand_future_functions=False): 

894 # Remove array formula braces and the leading =. 

895 if formula.startswith("{"): 

896 formula = formula[1:] 

897 if formula.startswith("="): 

898 formula = formula[1:] 

899 if formula.endswith("}"): 

900 formula = formula[:-1] 

901 

902 # Check if formula is already expanded by the user. 

903 if "_xlfn." in formula: 

904 return formula 

905 

906 # Expand dynamic formulas. 

907 formula = re.sub(r"\bANCHORARRAY\(", "_xlfn.ANCHORARRAY(", formula) 

908 formula = re.sub(r"\bBYCOL\(", "_xlfn.BYCOL(", formula) 

909 formula = re.sub(r"\bBYROW\(", "_xlfn.BYROW(", formula) 

910 formula = re.sub(r"\bCHOOSECOLS\(", "_xlfn.CHOOSECOLS(", formula) 

911 formula = re.sub(r"\bCHOOSEROWS\(", "_xlfn.CHOOSEROWS(", formula) 

912 formula = re.sub(r"\bDROP\(", "_xlfn.DROP(", formula) 

913 formula = re.sub(r"\bEXPAND\(", "_xlfn.EXPAND(", formula) 

914 formula = re.sub(r"\bFILTER\(", "_xlfn._xlws.FILTER(", formula) 

915 formula = re.sub(r"\bHSTACK\(", "_xlfn.HSTACK(", formula) 

916 formula = re.sub(r"\bLAMBDA\(", "_xlfn.LAMBDA(", formula) 

917 formula = re.sub(r"\bMAKEARRAY\(", "_xlfn.MAKEARRAY(", formula) 

918 formula = re.sub(r"\bMAP\(", "_xlfn.MAP(", formula) 

919 formula = re.sub(r"\bRANDARRAY\(", "_xlfn.RANDARRAY(", formula) 

920 formula = re.sub(r"\bREDUCE\(", "_xlfn.REDUCE(", formula) 

921 formula = re.sub(r"\bSCAN\(", "_xlfn.SCAN(", formula) 

922 formula = re.sub(r"\SINGLE\(", "_xlfn.SINGLE(", formula) 

923 formula = re.sub(r"\bSEQUENCE\(", "_xlfn.SEQUENCE(", formula) 

924 formula = re.sub(r"\bSORT\(", "_xlfn._xlws.SORT(", formula) 

925 formula = re.sub(r"\bSORTBY\(", "_xlfn.SORTBY(", formula) 

926 formula = re.sub(r"\bSWITCH\(", "_xlfn.SWITCH(", formula) 

927 formula = re.sub(r"\bTAKE\(", "_xlfn.TAKE(", formula) 

928 formula = re.sub(r"\bTEXTSPLIT\(", "_xlfn.TEXTSPLIT(", formula) 

929 formula = re.sub(r"\bTOCOL\(", "_xlfn.TOCOL(", formula) 

930 formula = re.sub(r"\bTOROW\(", "_xlfn.TOROW(", formula) 

931 formula = re.sub(r"\bUNIQUE\(", "_xlfn.UNIQUE(", formula) 

932 formula = re.sub(r"\bVSTACK\(", "_xlfn.VSTACK(", formula) 

933 formula = re.sub(r"\bWRAPCOLS\(", "_xlfn.WRAPCOLS(", formula) 

934 formula = re.sub(r"\bWRAPROWS\(", "_xlfn.WRAPROWS(", formula) 

935 formula = re.sub(r"\bXLOOKUP\(", "_xlfn.XLOOKUP(", formula) 

936 

937 if not self.use_future_functions and not expand_future_functions: 

938 return formula 

939 

940 formula = re.sub(r"\bACOTH\(", "_xlfn.ACOTH(", formula) 

941 formula = re.sub(r"\bACOT\(", "_xlfn.ACOT(", formula) 

942 formula = re.sub(r"\bAGGREGATE\(", "_xlfn.AGGREGATE(", formula) 

943 formula = re.sub(r"\bARABIC\(", "_xlfn.ARABIC(", formula) 

944 formula = re.sub(r"\bARRAYTOTEXT\(", "_xlfn.ARRAYTOTEXT(", formula) 

945 formula = re.sub(r"\bBASE\(", "_xlfn.BASE(", formula) 

946 formula = re.sub(r"\bBETA.DIST\(", "_xlfn.BETA.DIST(", formula) 

947 formula = re.sub(r"\bBETA.INV\(", "_xlfn.BETA.INV(", formula) 

948 formula = re.sub(r"\bBINOM.DIST.RANGE\(", "_xlfn.BINOM.DIST.RANGE(", formula) 

949 formula = re.sub(r"\bBINOM.DIST\(", "_xlfn.BINOM.DIST(", formula) 

950 formula = re.sub(r"\bBINOM.INV\(", "_xlfn.BINOM.INV(", formula) 

951 formula = re.sub(r"\bBITAND\(", "_xlfn.BITAND(", formula) 

952 formula = re.sub(r"\bBITLSHIFT\(", "_xlfn.BITLSHIFT(", formula) 

953 formula = re.sub(r"\bBITOR\(", "_xlfn.BITOR(", formula) 

954 formula = re.sub(r"\bBITRSHIFT\(", "_xlfn.BITRSHIFT(", formula) 

955 formula = re.sub(r"\bBITXOR\(", "_xlfn.BITXOR(", formula) 

956 formula = re.sub(r"\bCEILING.MATH\(", "_xlfn.CEILING.MATH(", formula) 

957 formula = re.sub(r"\bCEILING.PRECISE\(", "_xlfn.CEILING.PRECISE(", formula) 

958 formula = re.sub(r"\bCHISQ.DIST.RT\(", "_xlfn.CHISQ.DIST.RT(", formula) 

959 formula = re.sub(r"\bCHISQ.DIST\(", "_xlfn.CHISQ.DIST(", formula) 

960 formula = re.sub(r"\bCHISQ.INV.RT\(", "_xlfn.CHISQ.INV.RT(", formula) 

961 formula = re.sub(r"\bCHISQ.INV\(", "_xlfn.CHISQ.INV(", formula) 

962 formula = re.sub(r"\bCHISQ.TEST\(", "_xlfn.CHISQ.TEST(", formula) 

963 formula = re.sub(r"\bCOMBINA\(", "_xlfn.COMBINA(", formula) 

964 formula = re.sub(r"\bCONCAT\(", "_xlfn.CONCAT(", formula) 

965 formula = re.sub(r"\bCONFIDENCE.NORM\(", "_xlfn.CONFIDENCE.NORM(", formula) 

966 formula = re.sub(r"\bCONFIDENCE.T\(", "_xlfn.CONFIDENCE.T(", formula) 

967 formula = re.sub(r"\bCOTH\(", "_xlfn.COTH(", formula) 

968 formula = re.sub(r"\bCOT\(", "_xlfn.COT(", formula) 

969 formula = re.sub(r"\bCOVARIANCE.P\(", "_xlfn.COVARIANCE.P(", formula) 

970 formula = re.sub(r"\bCOVARIANCE.S\(", "_xlfn.COVARIANCE.S(", formula) 

971 formula = re.sub(r"\bCSCH\(", "_xlfn.CSCH(", formula) 

972 formula = re.sub(r"\bCSC\(", "_xlfn.CSC(", formula) 

973 formula = re.sub(r"\bDAYS\(", "_xlfn.DAYS(", formula) 

974 formula = re.sub(r"\bDECIMAL\(", "_xlfn.DECIMAL(", formula) 

975 formula = re.sub(r"\bERF.PRECISE\(", "_xlfn.ERF.PRECISE(", formula) 

976 formula = re.sub(r"\bERFC.PRECISE\(", "_xlfn.ERFC.PRECISE(", formula) 

977 formula = re.sub(r"\bEXPON.DIST\(", "_xlfn.EXPON.DIST(", formula) 

978 formula = re.sub(r"\bF.DIST.RT\(", "_xlfn.F.DIST.RT(", formula) 

979 formula = re.sub(r"\bF.DIST\(", "_xlfn.F.DIST(", formula) 

980 formula = re.sub(r"\bF.INV.RT\(", "_xlfn.F.INV.RT(", formula) 

981 formula = re.sub(r"\bF.INV\(", "_xlfn.F.INV(", formula) 

982 formula = re.sub(r"\bF.TEST\(", "_xlfn.F.TEST(", formula) 

983 formula = re.sub(r"\bFILTERXML\(", "_xlfn.FILTERXML(", formula) 

984 formula = re.sub(r"\bFLOOR.MATH\(", "_xlfn.FLOOR.MATH(", formula) 

985 formula = re.sub(r"\bFLOOR.PRECISE\(", "_xlfn.FLOOR.PRECISE(", formula) 

986 formula = re.sub( 

987 r"\bFORECAST.ETS.CONFINT\(", "_xlfn.FORECAST.ETS.CONFINT(", formula 

988 ) 

989 formula = re.sub( 

990 r"\bFORECAST.ETS.SEASONALITY\(", "_xlfn.FORECAST.ETS.SEASONALITY(", formula 

991 ) 

992 formula = re.sub(r"\bFORECAST.ETS.STAT\(", "_xlfn.FORECAST.ETS.STAT(", formula) 

993 formula = re.sub(r"\bFORECAST.ETS\(", "_xlfn.FORECAST.ETS(", formula) 

994 formula = re.sub(r"\bFORECAST.LINEAR\(", "_xlfn.FORECAST.LINEAR(", formula) 

995 formula = re.sub(r"\bFORMULATEXT\(", "_xlfn.FORMULATEXT(", formula) 

996 formula = re.sub(r"\bGAMMA.DIST\(", "_xlfn.GAMMA.DIST(", formula) 

997 formula = re.sub(r"\bGAMMA.INV\(", "_xlfn.GAMMA.INV(", formula) 

998 formula = re.sub(r"\bGAMMALN.PRECISE\(", "_xlfn.GAMMALN.PRECISE(", formula) 

999 formula = re.sub(r"\bGAMMA\(", "_xlfn.GAMMA(", formula) 

1000 formula = re.sub(r"\bGAUSS\(", "_xlfn.GAUSS(", formula) 

1001 formula = re.sub(r"\bHYPGEOM.DIST\(", "_xlfn.HYPGEOM.DIST(", formula) 

1002 formula = re.sub(r"\bIFNA\(", "_xlfn.IFNA(", formula) 

1003 formula = re.sub(r"\bIFS\(", "_xlfn.IFS(", formula) 

1004 formula = re.sub(r"\bIMAGE\(", "_xlfn.IMAGE(", formula) 

1005 formula = re.sub(r"\bIMCOSH\(", "_xlfn.IMCOSH(", formula) 

1006 formula = re.sub(r"\bIMCOT\(", "_xlfn.IMCOT(", formula) 

1007 formula = re.sub(r"\bIMCSCH\(", "_xlfn.IMCSCH(", formula) 

1008 formula = re.sub(r"\bIMCSC\(", "_xlfn.IMCSC(", formula) 

1009 formula = re.sub(r"\bIMSECH\(", "_xlfn.IMSECH(", formula) 

1010 formula = re.sub(r"\bIMSEC\(", "_xlfn.IMSEC(", formula) 

1011 formula = re.sub(r"\bIMSINH\(", "_xlfn.IMSINH(", formula) 

1012 formula = re.sub(r"\bIMTAN\(", "_xlfn.IMTAN(", formula) 

1013 formula = re.sub(r"\bISFORMULA\(", "_xlfn.ISFORMULA(", formula) 

1014 formula = re.sub(r"\bISOMITTED\(", "_xlfn.ISOMITTED(", formula) 

1015 formula = re.sub(r"\bISOWEEKNUM\(", "_xlfn.ISOWEEKNUM(", formula) 

1016 formula = re.sub(r"\bLET\(", "_xlfn.LET(", formula) 

1017 formula = re.sub(r"\bLOGNORM.DIST\(", "_xlfn.LOGNORM.DIST(", formula) 

1018 formula = re.sub(r"\bLOGNORM.INV\(", "_xlfn.LOGNORM.INV(", formula) 

1019 formula = re.sub(r"\bMAXIFS\(", "_xlfn.MAXIFS(", formula) 

1020 formula = re.sub(r"\bMINIFS\(", "_xlfn.MINIFS(", formula) 

1021 formula = re.sub(r"\bMODE.MULT\(", "_xlfn.MODE.MULT(", formula) 

1022 formula = re.sub(r"\bMODE.SNGL\(", "_xlfn.MODE.SNGL(", formula) 

1023 formula = re.sub(r"\bMUNIT\(", "_xlfn.MUNIT(", formula) 

1024 formula = re.sub(r"\bNEGBINOM.DIST\(", "_xlfn.NEGBINOM.DIST(", formula) 

1025 formula = re.sub(r"\bNORM.DIST\(", "_xlfn.NORM.DIST(", formula) 

1026 formula = re.sub(r"\bNORM.INV\(", "_xlfn.NORM.INV(", formula) 

1027 formula = re.sub(r"\bNORM.S.DIST\(", "_xlfn.NORM.S.DIST(", formula) 

1028 formula = re.sub(r"\bNORM.S.INV\(", "_xlfn.NORM.S.INV(", formula) 

1029 formula = re.sub(r"\bNUMBERVALUE\(", "_xlfn.NUMBERVALUE(", formula) 

1030 formula = re.sub(r"\bPDURATION\(", "_xlfn.PDURATION(", formula) 

1031 formula = re.sub(r"\bPERCENTILE.EXC\(", "_xlfn.PERCENTILE.EXC(", formula) 

1032 formula = re.sub(r"\bPERCENTILE.INC\(", "_xlfn.PERCENTILE.INC(", formula) 

1033 formula = re.sub(r"\bPERCENTRANK.EXC\(", "_xlfn.PERCENTRANK.EXC(", formula) 

1034 formula = re.sub(r"\bPERCENTRANK.INC\(", "_xlfn.PERCENTRANK.INC(", formula) 

1035 formula = re.sub(r"\bPERMUTATIONA\(", "_xlfn.PERMUTATIONA(", formula) 

1036 formula = re.sub(r"\bPHI\(", "_xlfn.PHI(", formula) 

1037 formula = re.sub(r"\bPOISSON.DIST\(", "_xlfn.POISSON.DIST(", formula) 

1038 formula = re.sub(r"\bQUARTILE.EXC\(", "_xlfn.QUARTILE.EXC(", formula) 

1039 formula = re.sub(r"\bQUARTILE.INC\(", "_xlfn.QUARTILE.INC(", formula) 

1040 formula = re.sub(r"\bQUERYSTRING\(", "_xlfn.QUERYSTRING(", formula) 

1041 formula = re.sub(r"\bRANK.AVG\(", "_xlfn.RANK.AVG(", formula) 

1042 formula = re.sub(r"\bRANK.EQ\(", "_xlfn.RANK.EQ(", formula) 

1043 formula = re.sub(r"\bRRI\(", "_xlfn.RRI(", formula) 

1044 formula = re.sub(r"\bSECH\(", "_xlfn.SECH(", formula) 

1045 formula = re.sub(r"\bSEC\(", "_xlfn.SEC(", formula) 

1046 formula = re.sub(r"\bSHEETS\(", "_xlfn.SHEETS(", formula) 

1047 formula = re.sub(r"\bSHEET\(", "_xlfn.SHEET(", formula) 

1048 formula = re.sub(r"\bSKEW.P\(", "_xlfn.SKEW.P(", formula) 

1049 formula = re.sub(r"\bSTDEV.P\(", "_xlfn.STDEV.P(", formula) 

1050 formula = re.sub(r"\bSTDEV.S\(", "_xlfn.STDEV.S(", formula) 

1051 formula = re.sub(r"\bT.DIST.2T\(", "_xlfn.T.DIST.2T(", formula) 

1052 formula = re.sub(r"\bT.DIST.RT\(", "_xlfn.T.DIST.RT(", formula) 

1053 formula = re.sub(r"\bT.DIST\(", "_xlfn.T.DIST(", formula) 

1054 formula = re.sub(r"\bT.INV.2T\(", "_xlfn.T.INV.2T(", formula) 

1055 formula = re.sub(r"\bT.INV\(", "_xlfn.T.INV(", formula) 

1056 formula = re.sub(r"\bT.TEST\(", "_xlfn.T.TEST(", formula) 

1057 formula = re.sub(r"\bTEXTAFTER\(", "_xlfn.TEXTAFTER(", formula) 

1058 formula = re.sub(r"\bTEXTBEFORE\(", "_xlfn.TEXTBEFORE(", formula) 

1059 formula = re.sub(r"\bTEXTJOIN\(", "_xlfn.TEXTJOIN(", formula) 

1060 formula = re.sub(r"\bUNICHAR\(", "_xlfn.UNICHAR(", formula) 

1061 formula = re.sub(r"\bUNICODE\(", "_xlfn.UNICODE(", formula) 

1062 formula = re.sub(r"\bVALUETOTEXT\(", "_xlfn.VALUETOTEXT(", formula) 

1063 formula = re.sub(r"\bVAR.P\(", "_xlfn.VAR.P(", formula) 

1064 formula = re.sub(r"\bVAR.S\(", "_xlfn.VAR.S(", formula) 

1065 formula = re.sub(r"\bWEBSERVICE\(", "_xlfn.WEBSERVICE(", formula) 

1066 formula = re.sub(r"\bWEIBULL.DIST\(", "_xlfn.WEIBULL.DIST(", formula) 

1067 formula = re.sub(r"\bXMATCH\(", "_xlfn.XMATCH(", formula) 

1068 formula = re.sub(r"\bXOR\(", "_xlfn.XOR(", formula) 

1069 formula = re.sub(r"\bZ.TEST\(", "_xlfn.Z.TEST(", formula) 

1070 

1071 return formula 

1072 

1073 # Escape/expand table functions. This mainly involves converting Excel 2010 

1074 # "@" table ref to 2007 "[#This Row],". We parse the string to avoid 

1075 # replacements in string literals within the formula. 

1076 @staticmethod 

1077 def _prepare_table_formula(formula): 

1078 if "@" not in formula: 

1079 # No escaping required. 

1080 return formula 

1081 

1082 escaped_formula = [] 

1083 in_string_literal = False 

1084 

1085 for char in formula: 

1086 # Match the start/end of string literals to avoid escaping 

1087 # references in strings. 

1088 if char == '"': 

1089 in_string_literal = not in_string_literal 

1090 

1091 # Copy the string literal. 

1092 if in_string_literal: 

1093 escaped_formula.append(char) 

1094 continue 

1095 

1096 # Replace table reference. 

1097 if char == "@": 

1098 escaped_formula.append("[#This Row],") 

1099 else: 

1100 escaped_formula.append(char) 

1101 

1102 return ("").join(escaped_formula) 

1103 

1104 # Undecorated version of write_array_formula() and 

1105 # write_dynamic_array_formula(). 

1106 def _write_array_formula( 

1107 self, 

1108 first_row, 

1109 first_col, 

1110 last_row, 

1111 last_col, 

1112 formula, 

1113 cell_format=None, 

1114 value=0, 

1115 atype="static", 

1116 ) -> Literal[0, -1]: 

1117 # Swap last row/col with first row/col as necessary. 

1118 if first_row > last_row: 

1119 first_row, last_row = last_row, first_row 

1120 if first_col > last_col: 

1121 first_col, last_col = last_col, first_col 

1122 

1123 # Check that row and col are valid and store max and min values. 

1124 if self._check_dimensions(first_row, first_col): 

1125 return -1 

1126 if self._check_dimensions(last_row, last_col): 

1127 return -1 

1128 

1129 # Define array range 

1130 if first_row == last_row and first_col == last_col: 

1131 cell_range = xl_rowcol_to_cell(first_row, first_col) 

1132 else: 

1133 cell_range = ( 

1134 xl_rowcol_to_cell(first_row, first_col) 

1135 + ":" 

1136 + xl_rowcol_to_cell(last_row, last_col) 

1137 ) 

1138 

1139 # Modify the formula string, as needed. 

1140 formula = self._prepare_formula(formula) 

1141 

1142 # Write previous row if in in-line string constant_memory mode. 

1143 if self.constant_memory and first_row > self.previous_row: 

1144 self._write_single_row(first_row) 

1145 

1146 # Store the cell data in the worksheet data table. 

1147 self.table[first_row][first_col] = CellArrayFormulaTuple( 

1148 formula, cell_format, value, cell_range, atype 

1149 ) 

1150 

1151 # Pad out the rest of the area with formatted zeroes. 

1152 if not self.constant_memory: 

1153 for row in range(first_row, last_row + 1): 

1154 for col in range(first_col, last_col + 1): 

1155 if row != first_row or col != first_col: 

1156 self._write_number(row, col, 0, cell_format) 

1157 

1158 return 0 

1159 

1160 @convert_cell_args 

1161 def write_datetime( 

1162 self, 

1163 row: int, 

1164 col: int, 

1165 date: datetime.datetime, 

1166 cell_format: Optional[Format] = None, 

1167 ) -> Literal[0, -1]: 

1168 """ 

1169 Write a date or time to a worksheet cell. 

1170 

1171 Args: 

1172 row: The cell row (zero indexed). 

1173 col: The cell column (zero indexed). 

1174 date: Date and/or time as a datetime object. 

1175 cell_format: A cell Format object. 

1176 

1177 Returns: 

1178 0: Success. 

1179 -1: Row or column is out of worksheet bounds. 

1180 

1181 """ 

1182 return self._write_datetime(row, col, date, cell_format) 

1183 

1184 # Undecorated version of write_datetime(). 

1185 def _write_datetime(self, row: int, col: int, date, cell_format=None) -> int: 

1186 # Check that row and col are valid and store max and min values. 

1187 if self._check_dimensions(row, col): 

1188 return -1 

1189 

1190 # Write previous row if in in-line string constant_memory mode. 

1191 if self.constant_memory and row > self.previous_row: 

1192 self._write_single_row(row) 

1193 

1194 # Convert datetime to an Excel date. 

1195 number = self._convert_date_time(date) 

1196 

1197 # Add the default date format. 

1198 if cell_format is None: 

1199 cell_format = self.default_date_format 

1200 

1201 # Store the cell data in the worksheet data table. 

1202 self.table[row][col] = CellDatetimeTuple(number, cell_format) 

1203 

1204 return 0 

1205 

1206 @convert_cell_args 

1207 def write_boolean( 

1208 self, row: int, col: int, boolean: bool, cell_format: Optional[Format] = None 

1209 ): 

1210 """ 

1211 Write a boolean value to a worksheet cell. 

1212 

1213 Args: 

1214 row: The cell row (zero indexed). 

1215 col: The cell column (zero indexed). 

1216 boolean: Cell data. bool type. 

1217 cell_format: An optional cell Format object. 

1218 

1219 Returns: 

1220 0: Success. 

1221 -1: Row or column is out of worksheet bounds. 

1222 

1223 """ 

1224 return self._write_boolean(row, col, boolean, cell_format) 

1225 

1226 # Undecorated version of write_boolean(). 

1227 def _write_boolean(self, row: int, col: int, boolean, cell_format=None) -> int: 

1228 # Check that row and col are valid and store max and min values. 

1229 if self._check_dimensions(row, col): 

1230 return -1 

1231 

1232 # Write previous row if in in-line string constant_memory mode. 

1233 if self.constant_memory and row > self.previous_row: 

1234 self._write_single_row(row) 

1235 

1236 if boolean: 

1237 value = 1 

1238 else: 

1239 value = 0 

1240 

1241 # Store the cell data in the worksheet data table. 

1242 self.table[row][col] = CellBooleanTuple(value, cell_format) 

1243 

1244 return 0 

1245 

1246 # Write a hyperlink. This is comprised of two elements: the displayed 

1247 # string and the non-displayed link. The displayed string is the same as 

1248 # the link unless an alternative string is specified. The display string 

1249 # is written using the write_string() method. Therefore the max characters 

1250 # string limit applies. 

1251 # 

1252 # The hyperlink can be to a http, ftp, mail, internal sheet, or external 

1253 # directory urls. 

1254 @convert_cell_args 

1255 def write_url( 

1256 self, 

1257 row: int, 

1258 col: int, 

1259 url: str, 

1260 cell_format: Optional[Format] = None, 

1261 string: Optional[str] = None, 

1262 tip: Optional[str] = None, 

1263 ): 

1264 """ 

1265 Write a hyperlink to a worksheet cell. 

1266 

1267 Args: 

1268 row: The cell row (zero indexed). 

1269 col: The cell column (zero indexed). 

1270 url: Hyperlink url. 

1271 format: An optional cell Format object. 

1272 string: An optional display string for the hyperlink. 

1273 tip: An optional tooltip. 

1274 Returns: 

1275 0: Success. 

1276 -1: Row or column is out of worksheet bounds. 

1277 -2: String longer than 32767 characters. 

1278 -3: URL longer than Excel limit of 255 characters. 

1279 -4: Exceeds Excel limit of 65,530 urls per worksheet. 

1280 """ 

1281 return self._write_url(row, col, url, cell_format, string, tip) 

1282 

1283 # Undecorated version of write_url(). 

1284 def _write_url( 

1285 self, row: int, col: int, url, cell_format=None, string=None, tip=None 

1286 ) -> int: 

1287 # Check that row and col are valid and store max and min values 

1288 if self._check_dimensions(row, col): 

1289 return -1 

1290 

1291 # If the URL is a string convert it to a Url object. 

1292 if not isinstance(url, Url): 

1293 

1294 # For backwards compatibility check if the string URL exceeds the 

1295 # Excel character limit for URLs and ignore it with a warning. 

1296 max_url = self.max_url_length 

1297 if "#" in url: 

1298 url_str, anchor_str = url.split("#", 1) 

1299 else: 

1300 url_str = url 

1301 anchor_str = "" 

1302 

1303 if len(url_str) > max_url or len(anchor_str) > max_url: 

1304 warn( 

1305 f"Ignoring URL '{url}' with link or location/anchor > {max_url} " 

1306 f"characters since it exceeds Excel's limit for URLs." 

1307 ) 

1308 return -3 

1309 

1310 url = Url(url) 

1311 

1312 if string is not None: 

1313 url._text = string 

1314 

1315 if tip is not None: 

1316 url._tip = tip 

1317 

1318 # Check the limit of URLs per worksheet. 

1319 self.hlink_count += 1 

1320 

1321 if self.hlink_count > 65530: 

1322 warn( 

1323 f"Ignoring URL '{url._original_url}' since it exceeds Excel's limit of " 

1324 f"65,530 URLs per worksheet." 

1325 ) 

1326 return -4 

1327 

1328 # Add the default URL format. 

1329 if cell_format is None: 

1330 cell_format = self.default_url_format 

1331 

1332 if not self.ignore_write_string: 

1333 # Write previous row if in in-line string constant_memory mode. 

1334 if self.constant_memory and row > self.previous_row: 

1335 self._write_single_row(row) 

1336 

1337 # Write the hyperlink string. 

1338 self._write_string(row, col, url.text, cell_format) 

1339 

1340 # Store the hyperlink data in a separate structure. 

1341 self.hyperlinks[row][col] = url 

1342 

1343 return 0 

1344 

1345 @convert_cell_args 

1346 def write_rich_string( 

1347 self, row: int, col: int, *args: Union[str, Format] 

1348 ) -> Literal[0, -1, -2, -3, -4, -5]: 

1349 """ 

1350 Write a "rich" string with multiple formats to a worksheet cell. 

1351 

1352 Args: 

1353 row: The cell row (zero indexed). 

1354 col: The cell column (zero indexed). 

1355 string_parts: String and format pairs. 

1356 cell_format: Optional Format object. 

1357 

1358 Returns: 

1359 0: Success. 

1360 -1: Row or column is out of worksheet bounds. 

1361 -2: String truncated to 32k characters. 

1362 -3: 2 consecutive formats used. 

1363 -4: Empty string used. 

1364 -5: Insufficient parameters. 

1365 

1366 """ 

1367 

1368 return self._write_rich_string(row, col, *args) 

1369 

1370 # Undecorated version of write_rich_string(). 

1371 def _write_rich_string(self, row: int, col: int, *args) -> int: 

1372 tokens = list(args) 

1373 cell_format = None 

1374 string_index = 0 

1375 raw_string = "" 

1376 

1377 # Check that row and col are valid and store max and min values 

1378 if self._check_dimensions(row, col): 

1379 return -1 

1380 

1381 # If the last arg is a format we use it as the cell format. 

1382 if isinstance(tokens[-1], Format): 

1383 cell_format = tokens.pop() 

1384 

1385 # Create a temp XMLWriter object and use it to write the rich string 

1386 # XML to a string. 

1387 fh = StringIO() 

1388 self.rstring = XMLwriter() 

1389 self.rstring._set_filehandle(fh) 

1390 

1391 # Create a temp format with the default font for unformatted fragments. 

1392 default = Format() 

1393 

1394 # Convert list of format, string tokens to pairs of (format, string) 

1395 # except for the first string fragment which doesn't require a default 

1396 # formatting run. Use the default for strings without a leading format. 

1397 fragments = [] 

1398 previous = "format" 

1399 pos = 0 

1400 

1401 if len(tokens) <= 2: 

1402 warn( 

1403 "You must specify more than 2 format/fragments for rich " 

1404 "strings. Ignoring input in write_rich_string()." 

1405 ) 

1406 return -5 

1407 

1408 for token in tokens: 

1409 if not isinstance(token, Format): 

1410 # Token is a string. 

1411 if previous != "format": 

1412 # If previous token wasn't a format add one before string. 

1413 fragments.append(default) 

1414 fragments.append(token) 

1415 else: 

1416 # If previous token was a format just add the string. 

1417 fragments.append(token) 

1418 

1419 if token == "": 

1420 warn( 

1421 "Excel doesn't allow empty strings in rich strings. " 

1422 "Ignoring input in write_rich_string()." 

1423 ) 

1424 return -4 

1425 

1426 # Keep track of unformatted string. 

1427 raw_string += token 

1428 previous = "string" 

1429 else: 

1430 # Can't allow 2 formats in a row. 

1431 if previous == "format" and pos > 0: 

1432 warn( 

1433 "Excel doesn't allow 2 consecutive formats in rich " 

1434 "strings. Ignoring input in write_rich_string()." 

1435 ) 

1436 return -3 

1437 

1438 # Token is a format object. Add it to the fragment list. 

1439 fragments.append(token) 

1440 previous = "format" 

1441 

1442 pos += 1 

1443 

1444 # If the first token is a string start the <r> element. 

1445 if not isinstance(fragments[0], Format): 

1446 self.rstring._xml_start_tag("r") 

1447 

1448 # Write the XML elements for the $format $string fragments. 

1449 for token in fragments: 

1450 if isinstance(token, Format): 

1451 # Write the font run. 

1452 self.rstring._xml_start_tag("r") 

1453 self._write_font(token) 

1454 else: 

1455 # Write the string fragment part, with whitespace handling. 

1456 attributes = [] 

1457 

1458 if _preserve_whitespace(token): 

1459 attributes.append(("xml:space", "preserve")) 

1460 

1461 self.rstring._xml_data_element("t", token, attributes) 

1462 self.rstring._xml_end_tag("r") 

1463 

1464 # Read the in-memory string. 

1465 string = self.rstring.fh.getvalue() 

1466 

1467 # Check that the string is < 32767 chars. 

1468 if len(raw_string) > self.xls_strmax: 

1469 warn( 

1470 "String length must be less than or equal to Excel's limit " 

1471 "of 32,767 characters in write_rich_string()." 

1472 ) 

1473 return -2 

1474 

1475 # Write a shared string or an in-line string in constant_memory mode. 

1476 if not self.constant_memory: 

1477 string_index = self.str_table._get_shared_string_index(string) 

1478 else: 

1479 string_index = string 

1480 

1481 # Write previous row if in in-line string constant_memory mode. 

1482 if self.constant_memory and row > self.previous_row: 

1483 self._write_single_row(row) 

1484 

1485 # Store the cell data in the worksheet data table. 

1486 self.table[row][col] = CellRichStringTuple( 

1487 string_index, cell_format, raw_string 

1488 ) 

1489 

1490 return 0 

1491 

1492 def add_write_handler(self, user_type, user_function) -> None: 

1493 """ 

1494 Add a callback function to the write() method to handle user defined 

1495 types. 

1496 

1497 Args: 

1498 user_type: The user type() to match on. 

1499 user_function: The user defined function to write the type data. 

1500 Returns: 

1501 Nothing. 

1502 

1503 """ 

1504 

1505 self.write_handlers[user_type] = user_function 

1506 

1507 @convert_cell_args 

1508 def write_row( 

1509 self, row: int, col: int, data, cell_format: Optional[Format] = None 

1510 ) -> Union[Literal[0], Any]: 

1511 """ 

1512 Write a row of data starting from (row, col). 

1513 

1514 Args: 

1515 row: The cell row (zero indexed). 

1516 col: The cell column (zero indexed). 

1517 data: A list of tokens to be written with write(). 

1518 format: An optional cell Format object. 

1519 Returns: 

1520 0: Success. 

1521 other: Return value of write() method. 

1522 

1523 """ 

1524 for token in data: 

1525 error = self._write(row, col, token, cell_format) 

1526 if error: 

1527 return error 

1528 col += 1 

1529 

1530 return 0 

1531 

1532 @convert_cell_args 

1533 def write_column( 

1534 self, row: int, col: int, data, cell_format: Optional[Format] = None 

1535 ) -> Union[Literal[0], Any]: 

1536 """ 

1537 Write a column of data starting from (row, col). 

1538 

1539 Args: 

1540 row: The cell row (zero indexed). 

1541 col: The cell column (zero indexed). 

1542 data: A list of tokens to be written with write(). 

1543 format: An optional cell Format object. 

1544 Returns: 

1545 0: Success. 

1546 other: Return value of write() method. 

1547 

1548 """ 

1549 for token in data: 

1550 error = self._write(row, col, token, cell_format) 

1551 if error: 

1552 return error 

1553 row += 1 

1554 

1555 return 0 

1556 

1557 @convert_cell_args 

1558 def insert_image( 

1559 self, 

1560 row: int, 

1561 col: int, 

1562 source: Union[str, BytesIO, Image], 

1563 options: Optional[Dict[str, Any]] = None, 

1564 ) -> Literal[0, -1]: 

1565 """ 

1566 Insert an image with its top-left corner in a worksheet cell. 

1567 

1568 Args: 

1569 row: The cell row (zero indexed). 

1570 col: The cell column (zero indexed). 

1571 source: Filename, BytesIO, or Image object. 

1572 options: Position, scale, url and data stream of the image. 

1573 

1574 Returns: 

1575 0: Success. 

1576 -1: Row or column is out of worksheet bounds. 

1577 

1578 """ 

1579 # Check insert (row, col) without storing. 

1580 if self._check_dimensions(row, col, True, True): 

1581 warn(f"Cannot insert image at ({row}, {col}).") 

1582 return -1 

1583 

1584 # Convert the source to an Image object. 

1585 image = self._image_from_source(source, options) 

1586 

1587 image._row = row 

1588 image._col = col 

1589 image._set_user_options(options) 

1590 

1591 self.images.append(image) 

1592 

1593 return 0 

1594 

1595 @convert_cell_args 

1596 def embed_image( 

1597 self, 

1598 row: int, 

1599 col: int, 

1600 source: Union[str, BytesIO, Image], 

1601 options: Optional[Dict[str, Any]] = None, 

1602 ) -> Literal[0, -1]: 

1603 """ 

1604 Embed an image in a worksheet cell. 

1605 

1606 Args: 

1607 row: The cell row (zero indexed). 

1608 col: The cell column (zero indexed). 

1609 source: Filename, BytesIO, or Image object. 

1610 options: Url and data stream of the image. 

1611 

1612 Returns: 

1613 0: Success. 

1614 -1: Row or column is out of worksheet bounds. 

1615 

1616 """ 

1617 # Check insert (row, col) without storing. 

1618 if self._check_dimensions(row, col): 

1619 warn(f"Cannot embed image at ({row}, {col}).") 

1620 return -1 

1621 

1622 if options is None: 

1623 options = {} 

1624 

1625 # Convert the source to an Image object. 

1626 image = self._image_from_source(source, options) 

1627 image._set_user_options(options) 

1628 

1629 cell_format = options.get("cell_format", None) 

1630 

1631 if image.url: 

1632 if cell_format is None: 

1633 cell_format = self.default_url_format 

1634 

1635 self.ignore_write_string = True 

1636 self.write_url(row, col, image.url, cell_format) 

1637 self.ignore_write_string = False 

1638 

1639 image_index = self.embedded_images.get_image_index(image) 

1640 

1641 # Store the cell error and image index in the worksheet data table. 

1642 self.table[row][col] = CellErrorTuple("#VALUE!", cell_format, image_index) 

1643 

1644 return 0 

1645 

1646 @convert_cell_args 

1647 def insert_textbox( 

1648 self, row: int, col: int, text: str, options: Optional[Dict[str, Any]] = None 

1649 ) -> Literal[0, -1]: 

1650 """ 

1651 Insert an textbox with its top-left corner in a worksheet cell. 

1652 

1653 Args: 

1654 row: The cell row (zero indexed). 

1655 col: The cell column (zero indexed). 

1656 text: The text for the textbox. 

1657 options: Textbox options. 

1658 

1659 Returns: 

1660 0: Success. 

1661 -1: Row or column is out of worksheet bounds. 

1662 

1663 """ 

1664 # Check insert (row, col) without storing. 

1665 if self._check_dimensions(row, col, True, True): 

1666 warn(f"Cannot insert textbox at ({row}, {col}).") 

1667 return -1 

1668 

1669 if text is None: 

1670 text = "" 

1671 

1672 if options is None: 

1673 options = {} 

1674 

1675 x_offset = options.get("x_offset", 0) 

1676 y_offset = options.get("y_offset", 0) 

1677 x_scale = options.get("x_scale", 1) 

1678 y_scale = options.get("y_scale", 1) 

1679 anchor = options.get("object_position", 1) 

1680 description = options.get("description", None) 

1681 decorative = options.get("decorative", False) 

1682 

1683 self.shapes.append( 

1684 [ 

1685 row, 

1686 col, 

1687 x_offset, 

1688 y_offset, 

1689 x_scale, 

1690 y_scale, 

1691 text, 

1692 anchor, 

1693 options, 

1694 description, 

1695 decorative, 

1696 ] 

1697 ) 

1698 return 0 

1699 

1700 @convert_cell_args 

1701 def insert_chart( 

1702 self, row: int, col: int, chart: Chart, options: Optional[Dict[str, Any]] = None 

1703 ) -> Literal[0, -1, -2]: 

1704 """ 

1705 Insert an chart with its top-left corner in a worksheet cell. 

1706 

1707 Args: 

1708 row: The cell row (zero indexed). 

1709 col: The cell column (zero indexed). 

1710 chart: Chart object. 

1711 options: Position and scale of the chart. 

1712 

1713 Returns: 

1714 0: Success. 

1715 -1: Row or column is out of worksheet bounds. 

1716 

1717 """ 

1718 # Check insert (row, col) without storing. 

1719 if self._check_dimensions(row, col, True, True): 

1720 warn(f"Cannot insert chart at ({row}, {col}).") 

1721 return -1 

1722 

1723 if options is None: 

1724 options = {} 

1725 

1726 # Ensure a chart isn't inserted more than once. 

1727 if chart.already_inserted or chart.combined and chart.combined.already_inserted: 

1728 warn("Chart cannot be inserted in a worksheet more than once.") 

1729 return -2 

1730 

1731 chart.already_inserted = True 

1732 

1733 if chart.combined: 

1734 chart.combined.already_inserted = True 

1735 

1736 x_offset = options.get("x_offset", 0) 

1737 y_offset = options.get("y_offset", 0) 

1738 x_scale = options.get("x_scale", 1) 

1739 y_scale = options.get("y_scale", 1) 

1740 anchor = options.get("object_position", 1) 

1741 description = options.get("description", None) 

1742 decorative = options.get("decorative", False) 

1743 

1744 # Allow Chart to override the scale and offset. 

1745 if chart.x_scale != 1: 

1746 x_scale = chart.x_scale 

1747 

1748 if chart.y_scale != 1: 

1749 y_scale = chart.y_scale 

1750 

1751 if chart.x_offset: 

1752 x_offset = chart.x_offset 

1753 

1754 if chart.y_offset: 

1755 y_offset = chart.y_offset 

1756 

1757 self.charts.append( 

1758 [ 

1759 row, 

1760 col, 

1761 chart, 

1762 x_offset, 

1763 y_offset, 

1764 x_scale, 

1765 y_scale, 

1766 anchor, 

1767 description, 

1768 decorative, 

1769 ] 

1770 ) 

1771 return 0 

1772 

1773 @convert_cell_args 

1774 def write_comment( 

1775 self, row: int, col: int, comment: str, options: Optional[Dict[str, Any]] = None 

1776 ) -> Literal[0, -1, -2]: 

1777 """ 

1778 Write a comment to a worksheet cell. 

1779 

1780 Args: 

1781 row: The cell row (zero indexed). 

1782 col: The cell column (zero indexed). 

1783 comment: Cell comment. Str. 

1784 options: Comment formatting options. 

1785 

1786 Returns: 

1787 0: Success. 

1788 -1: Row or column is out of worksheet bounds. 

1789 -2: String longer than 32k characters. 

1790 

1791 """ 

1792 # Check that row and col are valid and store max and min values 

1793 if self._check_dimensions(row, col): 

1794 return -1 

1795 

1796 # Check that the comment string is < 32767 chars. 

1797 if len(comment) > self.xls_strmax: 

1798 return -2 

1799 

1800 self.has_vml = True 

1801 self.has_comments = True 

1802 

1803 # Store the options of the cell comment, to process on file close. 

1804 comment = CommentType(row, col, comment, options) 

1805 self.comments[row][col] = comment 

1806 

1807 return 0 

1808 

1809 def show_comments(self) -> None: 

1810 """ 

1811 Make any comments in the worksheet visible. 

1812 

1813 Args: 

1814 None. 

1815 

1816 Returns: 

1817 Nothing. 

1818 

1819 """ 

1820 self.comments_visible = True 

1821 

1822 def set_background( 

1823 self, source: Union[str, BytesIO, Image], is_byte_stream: bool = False 

1824 ) -> Literal[0]: 

1825 """ 

1826 Set a background image for a worksheet. 

1827 

1828 Args: 

1829 source: Filename, BytesIO, or Image object. 

1830 is_byte_stream: Deprecated. Use a BytesIO object instead. 

1831 

1832 Returns: 

1833 0: Success. 

1834 

1835 """ 

1836 # Convert the source to an Image object. 

1837 image = self._image_from_source(source) 

1838 

1839 self.background_image = image 

1840 

1841 if is_byte_stream: 

1842 warn( 

1843 "The `is_byte_stream` parameter in `set_background()` is deprecated. " 

1844 "This argument can be omitted if you are using a BytesIO object." 

1845 ) 

1846 

1847 return 0 

1848 

1849 def set_comments_author(self, author) -> None: 

1850 """ 

1851 Set the default author of the cell comments. 

1852 

1853 Args: 

1854 author: Comment author name. String. 

1855 

1856 Returns: 

1857 Nothing. 

1858 

1859 """ 

1860 self.comments_author = author 

1861 

1862 def get_name(self): 

1863 """ 

1864 Retrieve the worksheet name. 

1865 

1866 Args: 

1867 None. 

1868 

1869 Returns: 

1870 Nothing. 

1871 

1872 """ 

1873 # There is no set_name() method. Name must be set in add_worksheet(). 

1874 return self.name 

1875 

1876 def activate(self) -> None: 

1877 """ 

1878 Set this worksheet as the active worksheet, i.e. the worksheet that is 

1879 displayed when the workbook is opened. Also set it as selected. 

1880 

1881 Note: An active worksheet cannot be hidden. 

1882 

1883 Args: 

1884 None. 

1885 

1886 Returns: 

1887 Nothing. 

1888 

1889 """ 

1890 self.hidden = 0 

1891 self.selected = 1 

1892 self.worksheet_meta.activesheet = self.index 

1893 

1894 def select(self) -> None: 

1895 """ 

1896 Set current worksheet as a selected worksheet, i.e. the worksheet 

1897 has its tab highlighted. 

1898 

1899 Note: A selected worksheet cannot be hidden. 

1900 

1901 Args: 

1902 None. 

1903 

1904 Returns: 

1905 Nothing. 

1906 

1907 """ 

1908 self.selected = 1 

1909 self.hidden = 0 

1910 

1911 def hide(self) -> None: 

1912 """ 

1913 Hide the current worksheet. 

1914 

1915 Args: 

1916 None. 

1917 

1918 Returns: 

1919 Nothing. 

1920 

1921 """ 

1922 self.hidden = 1 

1923 

1924 # A hidden worksheet shouldn't be active or selected. 

1925 self.selected = 0 

1926 

1927 def very_hidden(self) -> None: 

1928 """ 

1929 Hide the current worksheet. This can only be unhidden by VBA. 

1930 

1931 Args: 

1932 None. 

1933 

1934 Returns: 

1935 Nothing. 

1936 

1937 """ 

1938 self.hidden = 2 

1939 

1940 # A hidden worksheet shouldn't be active or selected. 

1941 self.selected = 0 

1942 

1943 def set_first_sheet(self) -> None: 

1944 """ 

1945 Set current worksheet as the first visible sheet. This is necessary 

1946 when there are a large number of worksheets and the activated 

1947 worksheet is not visible on the screen. 

1948 

1949 Note: A selected worksheet cannot be hidden. 

1950 

1951 Args: 

1952 None. 

1953 

1954 Returns: 

1955 Nothing. 

1956 

1957 """ 

1958 self.hidden = 0 # Active worksheet can't be hidden. 

1959 self.worksheet_meta.firstsheet = self.index 

1960 

1961 @convert_column_args 

1962 def set_column( 

1963 self, 

1964 first_col: int, 

1965 last_col: int, 

1966 width: Optional[float] = None, 

1967 cell_format: Optional[Format] = None, 

1968 options: Optional[Dict[str, Any]] = None, 

1969 ) -> Literal[0, -1]: 

1970 """ 

1971 Set the width, and other properties of a single column or a 

1972 range of columns. 

1973 

1974 Args: 

1975 first_col: First column (zero-indexed). 

1976 last_col: Last column (zero-indexed). Can be same as first_col. 

1977 width: Column width. (optional). 

1978 cell_format: Column cell_format. (optional). 

1979 options: Dict of options such as hidden and level. 

1980 

1981 Returns: 

1982 0: Success. 

1983 -1: Column number is out of worksheet bounds. 

1984 

1985 """ 

1986 if options is None: 

1987 options = {} 

1988 

1989 # Ensure 2nd col is larger than first. 

1990 if first_col > last_col: 

1991 (first_col, last_col) = (last_col, first_col) 

1992 

1993 # Don't modify the row dimensions when checking the columns. 

1994 ignore_row = True 

1995 

1996 # Set optional column values. 

1997 hidden = options.get("hidden", False) 

1998 collapsed = options.get("collapsed", False) 

1999 level = options.get("level", 0) 

2000 

2001 # Store the column dimension only in some conditions. 

2002 if cell_format or (width and hidden): 

2003 ignore_col = False 

2004 else: 

2005 ignore_col = True 

2006 

2007 # Check that each column is valid and store the max and min values. 

2008 if self._check_dimensions(0, last_col, ignore_row, ignore_col): 

2009 return -1 

2010 if self._check_dimensions(0, first_col, ignore_row, ignore_col): 

2011 return -1 

2012 

2013 # Set the limits for the outline levels (0 <= x <= 7). 

2014 level = max(level, 0) 

2015 level = min(level, 7) 

2016 

2017 self.outline_col_level = max(self.outline_col_level, level) 

2018 

2019 # Store the column data. 

2020 for col in range(first_col, last_col + 1): 

2021 self.col_info[col] = [width, cell_format, hidden, level, collapsed, False] 

2022 

2023 # Store the column change to allow optimizations. 

2024 self.col_size_changed = True 

2025 

2026 return 0 

2027 

2028 @convert_column_args 

2029 def set_column_pixels( 

2030 self, 

2031 first_col: int, 

2032 last_col: int, 

2033 width: Optional[float] = None, 

2034 cell_format: Optional[Format] = None, 

2035 options: Optional[Dict[str, Any]] = None, 

2036 ) -> Literal[0, -1]: 

2037 """ 

2038 Set the width, and other properties of a single column or a 

2039 range of columns, where column width is in pixels. 

2040 

2041 Args: 

2042 first_col: First column (zero-indexed). 

2043 last_col: Last column (zero-indexed). Can be same as first_col. 

2044 width: Column width in pixels. (optional). 

2045 cell_format: Column cell_format. (optional). 

2046 options: Dict of options such as hidden and level. 

2047 

2048 Returns: 

2049 0: Success. 

2050 -1: Column number is out of worksheet bounds. 

2051 

2052 """ 

2053 if width is not None: 

2054 width = self._pixels_to_width(width) 

2055 

2056 return self.set_column(first_col, last_col, width, cell_format, options) 

2057 

2058 def autofit(self, max_width: int = 1790) -> None: 

2059 """ 

2060 Simulate autofit based on the data, and datatypes in each column. 

2061 

2062 Args: 

2063 max_width (optional): max column width to autofit, in pixels. 

2064 

2065 Returns: 

2066 Nothing. 

2067 

2068 """ 

2069 # pylint: disable=too-many-nested-blocks 

2070 if self.constant_memory: 

2071 warn("Autofit is not supported in constant_memory mode.") 

2072 return 

2073 

2074 # No data written to the target sheet; nothing to autofit 

2075 if self.dim_rowmax is None: 

2076 return 

2077 

2078 # Store the max pixel width for each column. 

2079 col_width_max = {} 

2080 

2081 # Convert the autofit maximum pixel width to a column/character width, 

2082 # but limit it to the Excel max limit. 

2083 max_width = min(self._pixels_to_width(max_width), 255.0) 

2084 

2085 # Create a reverse lookup for the share strings table so we can convert 

2086 # the string id back to the original string. 

2087 strings = sorted( 

2088 self.str_table.string_table, key=self.str_table.string_table.__getitem__ 

2089 ) 

2090 

2091 for row_num in range(self.dim_rowmin, self.dim_rowmax + 1): 

2092 if not self.table.get(row_num): 

2093 continue 

2094 

2095 for col_num in range(self.dim_colmin, self.dim_colmax + 1): 

2096 if col_num in self.table[row_num]: 

2097 cell = self.table[row_num][col_num] 

2098 cell_type = cell.__class__.__name__ 

2099 length = 0 

2100 

2101 if cell_type in ("String", "RichString"): 

2102 # Handle strings and rich strings. 

2103 # 

2104 # For standard shared strings we do a reverse lookup 

2105 # from the shared string id to the actual string. For 

2106 # rich strings we use the unformatted string. We also 

2107 # split multi-line strings and handle each part 

2108 # separately. 

2109 if cell_type == "String": 

2110 string_id = cell.string 

2111 string = strings[string_id] 

2112 else: 

2113 string = cell.raw_string 

2114 

2115 if "\n" not in string: 

2116 # Single line string. 

2117 length = xl_pixel_width(string) 

2118 else: 

2119 # Handle multi-line strings. 

2120 for string in string.split("\n"): 

2121 seg_length = xl_pixel_width(string) 

2122 length = max(length, seg_length) 

2123 

2124 elif cell_type == "Number": 

2125 # Handle numbers. 

2126 # 

2127 # We use a workaround/optimization for numbers since 

2128 # digits all have a pixel width of 7. This gives a 

2129 # slightly greater width for the decimal place and 

2130 # minus sign but only by a few pixels and 

2131 # over-estimation is okay. 

2132 length = 7 * len(str(cell.number)) 

2133 

2134 elif cell_type == "Datetime": 

2135 # Handle dates. 

2136 # 

2137 # The following uses the default width for mm/dd/yyyy 

2138 # dates. It isn't feasible to parse the number format 

2139 # to get the actual string width for all format types. 

2140 length = self.default_date_pixels 

2141 

2142 elif cell_type == "Boolean": 

2143 # Handle boolean values. 

2144 # 

2145 # Use the Excel standard widths for TRUE and FALSE. 

2146 if cell.boolean: 

2147 length = 31 

2148 else: 

2149 length = 36 

2150 

2151 elif cell_type in ("Formula", "ArrayFormula"): 

2152 # Handle formulas. 

2153 # 

2154 # We only try to autofit a formula if it has a 

2155 # non-zero value. 

2156 if isinstance(cell.value, (float, int)): 

2157 if cell.value > 0: 

2158 length = 7 * len(str(cell.value)) 

2159 

2160 elif isinstance(cell.value, str): 

2161 length = xl_pixel_width(cell.value) 

2162 

2163 elif isinstance(cell.value, bool): 

2164 if cell.value: 

2165 length = 31 

2166 else: 

2167 length = 36 

2168 

2169 # If the cell is in an autofilter header we add an 

2170 # additional 16 pixels for the dropdown arrow. 

2171 if self.filter_cells.get((row_num, col_num)) and length > 0: 

2172 length += 16 

2173 

2174 # Add the string length to the lookup table. 

2175 width_max = col_width_max.get(col_num, 0) 

2176 if length > width_max: 

2177 col_width_max[col_num] = length 

2178 

2179 # Apply the width to the column. 

2180 for col_num, pixel_width in col_width_max.items(): 

2181 # Convert the string pixel width to a character width using an 

2182 # additional padding of 7 pixels, like Excel. 

2183 width = self._pixels_to_width(pixel_width + 7) 

2184 

2185 # Limit the width to the maximum user or Excel value. 

2186 width = min(width, max_width) 

2187 

2188 # Add the width to an existing col info structure or add a new one. 

2189 if self.col_info.get(col_num): 

2190 # We only update the width for an existing column if it is 

2191 # greater than the user defined value. This allows the user 

2192 # to pre-load a minimum col width. 

2193 col_info = self.col_info.get(col_num) 

2194 user_width = col_info[0] 

2195 hidden = col_info[5] 

2196 if user_width is not None and not hidden: 

2197 # Col info is user defined. 

2198 if width > user_width: 

2199 self.col_info[col_num][0] = width 

2200 self.col_info[col_num][5] = True 

2201 else: 

2202 self.col_info[col_num][0] = width 

2203 self.col_info[col_num][5] = True 

2204 else: 

2205 self.col_info[col_num] = [width, None, False, 0, False, True] 

2206 

2207 def set_row( 

2208 self, 

2209 row: int, 

2210 height: Optional[float] = None, 

2211 cell_format: Optional[Format] = None, 

2212 options: Optional[Dict[str, Any]] = None, 

2213 ) -> Literal[0, -1]: 

2214 """ 

2215 Set the width, and other properties of a row. 

2216 

2217 Args: 

2218 row: Row number (zero-indexed). 

2219 height: Row height. (optional). 

2220 cell_format: Row cell_format. (optional). 

2221 options: Dict of options such as hidden, level and collapsed. 

2222 

2223 Returns: 

2224 0: Success. 

2225 -1: Row number is out of worksheet bounds. 

2226 

2227 """ 

2228 if options is None: 

2229 options = {} 

2230 

2231 # Use minimum col in _check_dimensions(). 

2232 if self.dim_colmin is not None: 

2233 min_col = self.dim_colmin 

2234 else: 

2235 min_col = 0 

2236 

2237 # Check that row is valid. 

2238 if self._check_dimensions(row, min_col): 

2239 return -1 

2240 

2241 if height is None: 

2242 height = self.default_row_height 

2243 

2244 # Set optional row values. 

2245 hidden = options.get("hidden", False) 

2246 collapsed = options.get("collapsed", False) 

2247 level = options.get("level", 0) 

2248 

2249 # If the height is 0 the row is hidden and the height is the default. 

2250 if height == 0: 

2251 hidden = 1 

2252 height = self.default_row_height 

2253 

2254 # Set the limits for the outline levels (0 <= x <= 7). 

2255 level = max(level, 0) 

2256 level = min(level, 7) 

2257 

2258 self.outline_row_level = max(self.outline_row_level, level) 

2259 

2260 # Store the row properties. 

2261 self.set_rows[row] = [height, cell_format, hidden, level, collapsed] 

2262 

2263 # Store the row change to allow optimizations. 

2264 self.row_size_changed = True 

2265 

2266 # Store the row sizes for use when calculating image vertices. 

2267 self.row_sizes[row] = [height, hidden] 

2268 

2269 return 0 

2270 

2271 def set_row_pixels( 

2272 self, 

2273 row: int, 

2274 height: Optional[float] = None, 

2275 cell_format: Optional[Format] = None, 

2276 options: Optional[Dict[str, Any]] = None, 

2277 ) -> Literal[0, -1]: 

2278 """ 

2279 Set the width (in pixels), and other properties of a row. 

2280 

2281 Args: 

2282 row: Row number (zero-indexed). 

2283 height: Row height in pixels. (optional). 

2284 cell_format: Row cell_format. (optional). 

2285 options: Dict of options such as hidden, level and collapsed. 

2286 

2287 Returns: 

2288 0: Success. 

2289 -1: Row number is out of worksheet bounds. 

2290 

2291 """ 

2292 if height is not None: 

2293 height = self._pixels_to_height(height) 

2294 

2295 return self.set_row(row, height, cell_format, options) 

2296 

2297 def set_default_row( 

2298 self, height: Optional[float] = None, hide_unused_rows: bool = False 

2299 ) -> None: 

2300 """ 

2301 Set the default row properties. 

2302 

2303 Args: 

2304 height: Default height. Optional, defaults to 15. 

2305 hide_unused_rows: Hide unused rows. Optional, defaults to False. 

2306 

2307 Returns: 

2308 Nothing. 

2309 

2310 """ 

2311 if height is None: 

2312 height = self.default_row_height 

2313 

2314 if height != self.original_row_height: 

2315 # Store the row change to allow optimizations. 

2316 self.row_size_changed = True 

2317 self.default_row_height = height 

2318 

2319 if hide_unused_rows: 

2320 self.default_row_zeroed = 1 

2321 

2322 @convert_range_args 

2323 def merge_range( 

2324 self, 

2325 first_row: int, 

2326 first_col: int, 

2327 last_row: int, 

2328 last_col: int, 

2329 data: Any, 

2330 cell_format: Optional[Format] = None, 

2331 ) -> int: 

2332 """ 

2333 Merge a range of cells. 

2334 

2335 Args: 

2336 first_row: The first row of the cell range. (zero indexed). 

2337 first_col: The first column of the cell range. 

2338 last_row: The last row of the cell range. (zero indexed). 

2339 last_col: The last column of the cell range. 

2340 data: Cell data. 

2341 cell_format: Cell Format object. 

2342 

2343 Returns: 

2344 0: Success. 

2345 -1: Row or column is out of worksheet bounds. 

2346 other: Return value of write(). 

2347 

2348 """ 

2349 # Merge a range of cells. The first cell should contain the data and 

2350 # the others should be blank. All cells should have the same format. 

2351 

2352 # Excel doesn't allow a single cell to be merged 

2353 if first_row == last_row and first_col == last_col: 

2354 warn("Can't merge single cell") 

2355 return -1 

2356 

2357 # Swap last row/col with first row/col as necessary 

2358 if first_row > last_row: 

2359 (first_row, last_row) = (last_row, first_row) 

2360 if first_col > last_col: 

2361 (first_col, last_col) = (last_col, first_col) 

2362 

2363 # Check that row and col are valid and store max and min values. 

2364 if self._check_dimensions(first_row, first_col): 

2365 return -1 

2366 if self._check_dimensions(last_row, last_col): 

2367 return -1 

2368 

2369 # Check if the merge range overlaps a previous merged or table range. 

2370 # This is a critical file corruption error in Excel. 

2371 cell_range = xl_range(first_row, first_col, last_row, last_col) 

2372 for row in range(first_row, last_row + 1): 

2373 for col in range(first_col, last_col + 1): 

2374 if self.merged_cells.get((row, col)): 

2375 previous_range = self.merged_cells.get((row, col)) 

2376 raise OverlappingRange( 

2377 f"Merge range '{cell_range}' overlaps previous merge " 

2378 f"range '{previous_range}'." 

2379 ) 

2380 

2381 if self.table_cells.get((row, col)): 

2382 previous_range = self.table_cells.get((row, col)) 

2383 raise OverlappingRange( 

2384 f"Merge range '{cell_range}' overlaps previous table " 

2385 f"range '{previous_range}'." 

2386 ) 

2387 

2388 self.merged_cells[(row, col)] = cell_range 

2389 

2390 # Store the merge range. 

2391 self.merge.append([first_row, first_col, last_row, last_col]) 

2392 

2393 # Write the first cell 

2394 self._write(first_row, first_col, data, cell_format) 

2395 

2396 # Pad out the rest of the area with formatted blank cells. 

2397 for row in range(first_row, last_row + 1): 

2398 for col in range(first_col, last_col + 1): 

2399 if row == first_row and col == first_col: 

2400 continue 

2401 self._write_blank(row, col, "", cell_format) 

2402 

2403 return 0 

2404 

2405 @convert_range_args 

2406 def autofilter( 

2407 self, first_row: int, first_col: int, last_row: int, last_col: int 

2408 ) -> None: 

2409 """ 

2410 Set the autofilter area in the worksheet. 

2411 

2412 Args: 

2413 first_row: The first row of the cell range. (zero indexed). 

2414 first_col: The first column of the cell range. 

2415 last_row: The last row of the cell range. (zero indexed). 

2416 last_col: The last column of the cell range. 

2417 

2418 Returns: 

2419 Nothing. 

2420 

2421 """ 

2422 # Reverse max and min values if necessary. 

2423 if last_row < first_row: 

2424 (first_row, last_row) = (last_row, first_row) 

2425 if last_col < first_col: 

2426 (first_col, last_col) = (last_col, first_col) 

2427 

2428 # Build up the autofilter area range "Sheet1!$A$1:$C$13". 

2429 area = self._convert_name_area(first_row, first_col, last_row, last_col) 

2430 ref = xl_range(first_row, first_col, last_row, last_col) 

2431 

2432 self.autofilter_area = area 

2433 self.autofilter_ref = ref 

2434 self.filter_range = [first_col, last_col] 

2435 

2436 # Store the filter cell positions for use in the autofit calculation. 

2437 for col in range(first_col, last_col + 1): 

2438 # Check that the autofilter doesn't overlap a table filter. 

2439 if self.filter_cells.get((first_row, col)): 

2440 filter_type, filter_range = self.filter_cells.get((first_row, col)) 

2441 if filter_type == "table": 

2442 raise OverlappingRange( 

2443 f"Worksheet autofilter range '{ref}' overlaps previous " 

2444 f"Table autofilter range '{filter_range}'." 

2445 ) 

2446 

2447 self.filter_cells[(first_row, col)] = ("worksheet", ref) 

2448 

2449 def filter_column(self, col: int, criteria: str) -> None: 

2450 """ 

2451 Set the column filter criteria. 

2452 

2453 Args: 

2454 col: Filter column (zero-indexed). 

2455 criteria: Filter criteria. 

2456 

2457 Returns: 

2458 Nothing. 

2459 

2460 """ 

2461 if not self.autofilter_area: 

2462 warn("Must call autofilter() before filter_column()") 

2463 return 

2464 

2465 # Check for a column reference in A1 notation and substitute. 

2466 try: 

2467 int(col) 

2468 except ValueError: 

2469 # Convert col ref to a cell ref and then to a col number. 

2470 col_letter = col 

2471 (_, col) = xl_cell_to_rowcol(col + "1") 

2472 

2473 if col >= self.xls_colmax: 

2474 warn(f"Invalid column '{col_letter}'") 

2475 return 

2476 

2477 (col_first, col_last) = self.filter_range 

2478 

2479 # Reject column if it is outside filter range. 

2480 if col < col_first or col > col_last: 

2481 warn( 

2482 f"Column '{col}' outside autofilter() column " 

2483 f"range ({col_first}, {col_last})" 

2484 ) 

2485 return 

2486 

2487 tokens = self._extract_filter_tokens(criteria) 

2488 

2489 if len(tokens) not in (3, 7): 

2490 warn(f"Incorrect number of tokens in criteria '{criteria}'") 

2491 

2492 tokens = self._parse_filter_expression(criteria, tokens) 

2493 

2494 # Excel handles single or double custom filters as default filters. 

2495 # We need to check for them and handle them accordingly. 

2496 if len(tokens) == 2 and tokens[0] == 2: 

2497 # Single equality. 

2498 self.filter_column_list(col, [tokens[1]]) 

2499 elif len(tokens) == 5 and tokens[0] == 2 and tokens[2] == 1 and tokens[3] == 2: 

2500 # Double equality with "or" operator. 

2501 self.filter_column_list(col, [tokens[1], tokens[4]]) 

2502 else: 

2503 # Non default custom filter. 

2504 self.filter_cols[col] = tokens 

2505 self.filter_type[col] = 0 

2506 

2507 self.filter_on = 1 

2508 

2509 def filter_column_list(self, col: int, filters: List[str]) -> None: 

2510 """ 

2511 Set the column filter criteria in Excel 2007 list style. 

2512 

2513 Args: 

2514 col: Filter column (zero-indexed). 

2515 filters: List of filter criteria to match. 

2516 

2517 Returns: 

2518 Nothing. 

2519 

2520 """ 

2521 if not self.autofilter_area: 

2522 warn("Must call autofilter() before filter_column()") 

2523 return 

2524 

2525 # Check for a column reference in A1 notation and substitute. 

2526 try: 

2527 int(col) 

2528 except ValueError: 

2529 # Convert col ref to a cell ref and then to a col number. 

2530 col_letter = col 

2531 (_, col) = xl_cell_to_rowcol(col + "1") 

2532 

2533 if col >= self.xls_colmax: 

2534 warn(f"Invalid column '{col_letter}'") 

2535 return 

2536 

2537 (col_first, col_last) = self.filter_range 

2538 

2539 # Reject column if it is outside filter range. 

2540 if col < col_first or col > col_last: 

2541 warn( 

2542 f"Column '{col}' outside autofilter() column range " 

2543 f"({col_first},{col_last})" 

2544 ) 

2545 return 

2546 

2547 self.filter_cols[col] = filters 

2548 self.filter_type[col] = 1 

2549 self.filter_on = 1 

2550 

2551 @convert_range_args 

2552 def data_validation( 

2553 self, 

2554 first_row: int, 

2555 first_col: int, 

2556 last_row: int, 

2557 last_col: int, 

2558 options: Optional[Dict[str, Any]] = None, 

2559 ) -> Literal[0, -1, -2]: 

2560 """ 

2561 Add a data validation to a worksheet. 

2562 

2563 Args: 

2564 first_row: The first row of the cell range. (zero indexed). 

2565 first_col: The first column of the cell range. 

2566 last_row: The last row of the cell range. (zero indexed). 

2567 last_col: The last column of the cell range. 

2568 options: Data validation options. 

2569 

2570 Returns: 

2571 0: Success. 

2572 -1: Row or column is out of worksheet bounds. 

2573 -2: Incorrect parameter or option. 

2574 """ 

2575 # Check that row and col are valid without storing the values. 

2576 if self._check_dimensions(first_row, first_col, True, True): 

2577 return -1 

2578 if self._check_dimensions(last_row, last_col, True, True): 

2579 return -1 

2580 

2581 if options is None: 

2582 options = {} 

2583 else: 

2584 # Copy the user defined options so they aren't modified. 

2585 options = options.copy() 

2586 

2587 # Valid input parameters. 

2588 valid_parameters = { 

2589 "validate", 

2590 "criteria", 

2591 "value", 

2592 "source", 

2593 "minimum", 

2594 "maximum", 

2595 "ignore_blank", 

2596 "dropdown", 

2597 "show_input", 

2598 "input_title", 

2599 "input_message", 

2600 "show_error", 

2601 "error_title", 

2602 "error_message", 

2603 "error_type", 

2604 "other_cells", 

2605 "multi_range", 

2606 } 

2607 

2608 # Check for valid input parameters. 

2609 for param_key in options.keys(): 

2610 if param_key not in valid_parameters: 

2611 warn(f"Unknown parameter '{param_key}' in data_validation()") 

2612 return -2 

2613 

2614 # Map alternative parameter names 'source' or 'minimum' to 'value'. 

2615 if "source" in options: 

2616 options["value"] = options["source"] 

2617 if "minimum" in options: 

2618 options["value"] = options["minimum"] 

2619 

2620 # 'validate' is a required parameter. 

2621 if "validate" not in options: 

2622 warn("Parameter 'validate' is required in data_validation()") 

2623 return -2 

2624 

2625 # List of valid validation types. 

2626 valid_types = { 

2627 "any": "none", 

2628 "any value": "none", 

2629 "whole number": "whole", 

2630 "whole": "whole", 

2631 "integer": "whole", 

2632 "decimal": "decimal", 

2633 "list": "list", 

2634 "date": "date", 

2635 "time": "time", 

2636 "text length": "textLength", 

2637 "length": "textLength", 

2638 "custom": "custom", 

2639 } 

2640 

2641 # Check for valid validation types. 

2642 if options["validate"] not in valid_types: 

2643 warn( 

2644 f"Unknown validation type '{options['validate']}' for parameter " 

2645 f"'validate' in data_validation()" 

2646 ) 

2647 return -2 

2648 

2649 options["validate"] = valid_types[options["validate"]] 

2650 

2651 # No action is required for validation type 'any' if there are no 

2652 # input messages to display. 

2653 if ( 

2654 options["validate"] == "none" 

2655 and options.get("input_title") is None 

2656 and options.get("input_message") is None 

2657 ): 

2658 return -2 

2659 

2660 # The any, list and custom validations don't have a criteria so we use 

2661 # a default of 'between'. 

2662 if ( 

2663 options["validate"] == "none" 

2664 or options["validate"] == "list" 

2665 or options["validate"] == "custom" 

2666 ): 

2667 options["criteria"] = "between" 

2668 options["maximum"] = None 

2669 

2670 # 'criteria' is a required parameter. 

2671 if "criteria" not in options: 

2672 warn("Parameter 'criteria' is required in data_validation()") 

2673 return -2 

2674 

2675 # Valid criteria types. 

2676 criteria_types = { 

2677 "between": "between", 

2678 "not between": "notBetween", 

2679 "equal to": "equal", 

2680 "=": "equal", 

2681 "==": "equal", 

2682 "not equal to": "notEqual", 

2683 "!=": "notEqual", 

2684 "<>": "notEqual", 

2685 "greater than": "greaterThan", 

2686 ">": "greaterThan", 

2687 "less than": "lessThan", 

2688 "<": "lessThan", 

2689 "greater than or equal to": "greaterThanOrEqual", 

2690 ">=": "greaterThanOrEqual", 

2691 "less than or equal to": "lessThanOrEqual", 

2692 "<=": "lessThanOrEqual", 

2693 } 

2694 

2695 # Check for valid criteria types. 

2696 if options["criteria"] not in criteria_types: 

2697 warn( 

2698 f"Unknown criteria type '{options['criteria']}' for parameter " 

2699 f"'criteria' in data_validation()" 

2700 ) 

2701 return -2 

2702 

2703 options["criteria"] = criteria_types[options["criteria"]] 

2704 

2705 # 'Between' and 'Not between' criteria require 2 values. 

2706 if options["criteria"] == "between" or options["criteria"] == "notBetween": 

2707 if "maximum" not in options: 

2708 warn( 

2709 "Parameter 'maximum' is required in data_validation() " 

2710 "when using 'between' or 'not between' criteria" 

2711 ) 

2712 return -2 

2713 else: 

2714 options["maximum"] = None 

2715 

2716 # Valid error dialog types. 

2717 error_types = { 

2718 "stop": 0, 

2719 "warning": 1, 

2720 "information": 2, 

2721 } 

2722 

2723 # Check for valid error dialog types. 

2724 if "error_type" not in options: 

2725 options["error_type"] = 0 

2726 elif options["error_type"] not in error_types: 

2727 warn( 

2728 f"Unknown criteria type '{options['error_type']}' " 

2729 f"for parameter 'error_type'." 

2730 ) 

2731 return -2 

2732 else: 

2733 options["error_type"] = error_types[options["error_type"]] 

2734 

2735 # Convert date/times value if required. 

2736 if ( 

2737 options["validate"] in ("date", "time") 

2738 and options["value"] 

2739 and _supported_datetime(options["value"]) 

2740 ): 

2741 date_time = self._convert_date_time(options["value"]) 

2742 # Format date number to the same precision as Excel. 

2743 options["value"] = f"{date_time:.16g}" 

2744 

2745 if options["maximum"] and _supported_datetime(options["maximum"]): 

2746 date_time = self._convert_date_time(options["maximum"]) 

2747 options["maximum"] = f"{date_time:.16g}" 

2748 

2749 # Check that the input title doesn't exceed the maximum length. 

2750 if options.get("input_title") and len(options["input_title"]) > 32: 

2751 warn( 

2752 f"Length of input title '{options['input_title']}' " 

2753 f"exceeds Excel's limit of 32" 

2754 ) 

2755 return -2 

2756 

2757 # Check that the error title doesn't exceed the maximum length. 

2758 if options.get("error_title") and len(options["error_title"]) > 32: 

2759 warn( 

2760 f"Length of error title '{options['error_title']}' " 

2761 f"exceeds Excel's limit of 32" 

2762 ) 

2763 return -2 

2764 

2765 # Check that the input message doesn't exceed the maximum length. 

2766 if options.get("input_message") and len(options["input_message"]) > 255: 

2767 warn( 

2768 f"Length of input message '{options['input_message']}' " 

2769 f"exceeds Excel's limit of 255" 

2770 ) 

2771 return -2 

2772 

2773 # Check that the error message doesn't exceed the maximum length. 

2774 if options.get("error_message") and len(options["error_message"]) > 255: 

2775 warn( 

2776 f"Length of error message '{options['error_message']}' " 

2777 f"exceeds Excel's limit of 255" 

2778 ) 

2779 return -2 

2780 

2781 # Check that the input list doesn't exceed the maximum length. 

2782 if options["validate"] == "list" and isinstance(options["value"], list): 

2783 formula = self._csv_join(*options["value"]) 

2784 if len(formula) > 255: 

2785 warn( 

2786 f"Length of list items '{formula}' exceeds Excel's limit of " 

2787 f"255, use a formula range instead" 

2788 ) 

2789 return -2 

2790 

2791 # Set some defaults if they haven't been defined by the user. 

2792 if "ignore_blank" not in options: 

2793 options["ignore_blank"] = 1 

2794 if "dropdown" not in options: 

2795 options["dropdown"] = 1 

2796 if "show_input" not in options: 

2797 options["show_input"] = 1 

2798 if "show_error" not in options: 

2799 options["show_error"] = 1 

2800 

2801 # These are the cells to which the validation is applied. 

2802 options["cells"] = [[first_row, first_col, last_row, last_col]] 

2803 

2804 # A (for now) undocumented parameter to pass additional cell ranges. 

2805 if "other_cells" in options: 

2806 options["cells"].extend(options["other_cells"]) 

2807 

2808 # Override with user defined multiple range if provided. 

2809 if "multi_range" in options: 

2810 options["multi_range"] = options["multi_range"].replace("$", "") 

2811 

2812 # Store the validation information until we close the worksheet. 

2813 self.validations.append(options) 

2814 

2815 return 0 

2816 

2817 @convert_range_args 

2818 def conditional_format( 

2819 self, 

2820 first_row: int, 

2821 first_col: int, 

2822 last_row: int, 

2823 last_col: int, 

2824 options: Optional[Dict[str, Any]] = None, 

2825 ) -> Literal[0, -1, -2]: 

2826 """ 

2827 Add a conditional format to a worksheet. 

2828 

2829 Args: 

2830 first_row: The first row of the cell range. (zero indexed). 

2831 first_col: The first column of the cell range. 

2832 last_row: The last row of the cell range. (zero indexed). 

2833 last_col: The last column of the cell range. 

2834 options: Conditional format options. 

2835 

2836 Returns: 

2837 0: Success. 

2838 -1: Row or column is out of worksheet bounds. 

2839 -2: Incorrect parameter or option. 

2840 """ 

2841 # Check that row and col are valid without storing the values. 

2842 if self._check_dimensions(first_row, first_col, True, True): 

2843 return -1 

2844 if self._check_dimensions(last_row, last_col, True, True): 

2845 return -1 

2846 

2847 if options is None: 

2848 options = {} 

2849 else: 

2850 # Copy the user defined options so they aren't modified. 

2851 options = options.copy() 

2852 

2853 # Valid input parameters. 

2854 valid_parameter = { 

2855 "type", 

2856 "format", 

2857 "criteria", 

2858 "value", 

2859 "minimum", 

2860 "maximum", 

2861 "stop_if_true", 

2862 "min_type", 

2863 "mid_type", 

2864 "max_type", 

2865 "min_value", 

2866 "mid_value", 

2867 "max_value", 

2868 "min_color", 

2869 "mid_color", 

2870 "max_color", 

2871 "min_length", 

2872 "max_length", 

2873 "multi_range", 

2874 "bar_color", 

2875 "bar_negative_color", 

2876 "bar_negative_color_same", 

2877 "bar_solid", 

2878 "bar_border_color", 

2879 "bar_negative_border_color", 

2880 "bar_negative_border_color_same", 

2881 "bar_no_border", 

2882 "bar_direction", 

2883 "bar_axis_position", 

2884 "bar_axis_color", 

2885 "bar_only", 

2886 "data_bar_2010", 

2887 "icon_style", 

2888 "reverse_icons", 

2889 "icons_only", 

2890 "icons", 

2891 } 

2892 

2893 # Check for valid input parameters. 

2894 for param_key in options.keys(): 

2895 if param_key not in valid_parameter: 

2896 warn(f"Unknown parameter '{param_key}' in conditional_format()") 

2897 return -2 

2898 

2899 # 'type' is a required parameter. 

2900 if "type" not in options: 

2901 warn("Parameter 'type' is required in conditional_format()") 

2902 return -2 

2903 

2904 # Valid types. 

2905 valid_type = { 

2906 "cell": "cellIs", 

2907 "date": "date", 

2908 "time": "time", 

2909 "average": "aboveAverage", 

2910 "duplicate": "duplicateValues", 

2911 "unique": "uniqueValues", 

2912 "top": "top10", 

2913 "bottom": "top10", 

2914 "text": "text", 

2915 "time_period": "timePeriod", 

2916 "blanks": "containsBlanks", 

2917 "no_blanks": "notContainsBlanks", 

2918 "errors": "containsErrors", 

2919 "no_errors": "notContainsErrors", 

2920 "2_color_scale": "2_color_scale", 

2921 "3_color_scale": "3_color_scale", 

2922 "data_bar": "dataBar", 

2923 "formula": "expression", 

2924 "icon_set": "iconSet", 

2925 } 

2926 

2927 # Check for valid types. 

2928 if options["type"] not in valid_type: 

2929 warn( 

2930 f"Unknown value '{options['type']}' for parameter 'type' " 

2931 f"in conditional_format()" 

2932 ) 

2933 return -2 

2934 

2935 if options["type"] == "bottom": 

2936 options["direction"] = "bottom" 

2937 options["type"] = valid_type[options["type"]] 

2938 

2939 # Valid criteria types. 

2940 criteria_type = { 

2941 "between": "between", 

2942 "not between": "notBetween", 

2943 "equal to": "equal", 

2944 "=": "equal", 

2945 "==": "equal", 

2946 "not equal to": "notEqual", 

2947 "!=": "notEqual", 

2948 "<>": "notEqual", 

2949 "greater than": "greaterThan", 

2950 ">": "greaterThan", 

2951 "less than": "lessThan", 

2952 "<": "lessThan", 

2953 "greater than or equal to": "greaterThanOrEqual", 

2954 ">=": "greaterThanOrEqual", 

2955 "less than or equal to": "lessThanOrEqual", 

2956 "<=": "lessThanOrEqual", 

2957 "containing": "containsText", 

2958 "not containing": "notContains", 

2959 "begins with": "beginsWith", 

2960 "ends with": "endsWith", 

2961 "yesterday": "yesterday", 

2962 "today": "today", 

2963 "last 7 days": "last7Days", 

2964 "last week": "lastWeek", 

2965 "this week": "thisWeek", 

2966 "next week": "nextWeek", 

2967 "last month": "lastMonth", 

2968 "this month": "thisMonth", 

2969 "next month": "nextMonth", 

2970 # For legacy, but incorrect, support. 

2971 "continue week": "nextWeek", 

2972 "continue month": "nextMonth", 

2973 } 

2974 

2975 # Check for valid criteria types. 

2976 if "criteria" in options and options["criteria"] in criteria_type: 

2977 options["criteria"] = criteria_type[options["criteria"]] 

2978 

2979 # Convert boolean values if required. 

2980 if "value" in options and isinstance(options["value"], bool): 

2981 options["value"] = str(options["value"]).upper() 

2982 

2983 # Convert date/times value if required. 

2984 if options["type"] in ("date", "time"): 

2985 options["type"] = "cellIs" 

2986 

2987 if "value" in options: 

2988 if not _supported_datetime(options["value"]): 

2989 warn("Conditional format 'value' must be a datetime object.") 

2990 return -2 

2991 

2992 date_time = self._convert_date_time(options["value"]) 

2993 # Format date number to the same precision as Excel. 

2994 options["value"] = f"{date_time:.16g}" 

2995 

2996 if "minimum" in options: 

2997 if not _supported_datetime(options["minimum"]): 

2998 warn("Conditional format 'minimum' must be a datetime object.") 

2999 return -2 

3000 

3001 date_time = self._convert_date_time(options["minimum"]) 

3002 options["minimum"] = f"{date_time:.16g}" 

3003 

3004 if "maximum" in options: 

3005 if not _supported_datetime(options["maximum"]): 

3006 warn("Conditional format 'maximum' must be a datetime object.") 

3007 return -2 

3008 

3009 date_time = self._convert_date_time(options["maximum"]) 

3010 options["maximum"] = f"{date_time:.16g}" 

3011 

3012 # Valid icon styles. 

3013 valid_icons = { 

3014 "3_arrows": "3Arrows", # 1 

3015 "3_flags": "3Flags", # 2 

3016 "3_traffic_lights_rimmed": "3TrafficLights2", # 3 

3017 "3_symbols_circled": "3Symbols", # 4 

3018 "4_arrows": "4Arrows", # 5 

3019 "4_red_to_black": "4RedToBlack", # 6 

3020 "4_traffic_lights": "4TrafficLights", # 7 

3021 "5_arrows_gray": "5ArrowsGray", # 8 

3022 "5_quarters": "5Quarters", # 9 

3023 "3_arrows_gray": "3ArrowsGray", # 10 

3024 "3_traffic_lights": "3TrafficLights", # 11 

3025 "3_signs": "3Signs", # 12 

3026 "3_symbols": "3Symbols2", # 13 

3027 "4_arrows_gray": "4ArrowsGray", # 14 

3028 "4_ratings": "4Rating", # 15 

3029 "5_arrows": "5Arrows", # 16 

3030 "5_ratings": "5Rating", 

3031 } # 17 

3032 

3033 # Set the icon set properties. 

3034 if options["type"] == "iconSet": 

3035 # An icon_set must have an icon style. 

3036 if not options.get("icon_style"): 

3037 warn( 

3038 "The 'icon_style' parameter must be specified when " 

3039 "'type' == 'icon_set' in conditional_format()." 

3040 ) 

3041 return -3 

3042 

3043 # Check for valid icon styles. 

3044 if options["icon_style"] not in valid_icons: 

3045 warn( 

3046 f"Unknown icon_style '{options['icon_style']}' " 

3047 f"in conditional_format()." 

3048 ) 

3049 return -2 

3050 

3051 options["icon_style"] = valid_icons[options["icon_style"]] 

3052 

3053 # Set the number of icons for the icon style. 

3054 options["total_icons"] = 3 

3055 if options["icon_style"].startswith("4"): 

3056 options["total_icons"] = 4 

3057 elif options["icon_style"].startswith("5"): 

3058 options["total_icons"] = 5 

3059 

3060 options["icons"] = self._set_icon_props( 

3061 options.get("total_icons"), options.get("icons") 

3062 ) 

3063 

3064 # Swap last row/col for first row/col as necessary 

3065 if first_row > last_row: 

3066 first_row, last_row = last_row, first_row 

3067 

3068 if first_col > last_col: 

3069 first_col, last_col = last_col, first_col 

3070 

3071 # Set the formatting range. 

3072 cell_range = xl_range(first_row, first_col, last_row, last_col) 

3073 start_cell = xl_rowcol_to_cell(first_row, first_col) 

3074 

3075 # Override with user defined multiple range if provided. 

3076 if "multi_range" in options: 

3077 cell_range = options["multi_range"] 

3078 cell_range = cell_range.replace("$", "") 

3079 

3080 # Get the dxf format index. 

3081 if "format" in options and options["format"]: 

3082 options["format"] = options["format"]._get_dxf_index() 

3083 

3084 # Set the priority based on the order of adding. 

3085 options["priority"] = self.dxf_priority 

3086 self.dxf_priority += 1 

3087 

3088 # Check for 2010 style data_bar parameters. 

3089 # pylint: disable=too-many-boolean-expressions 

3090 if ( 

3091 self.use_data_bars_2010 

3092 or options.get("data_bar_2010") 

3093 or options.get("bar_solid") 

3094 or options.get("bar_border_color") 

3095 or options.get("bar_negative_color") 

3096 or options.get("bar_negative_color_same") 

3097 or options.get("bar_negative_border_color") 

3098 or options.get("bar_negative_border_color_same") 

3099 or options.get("bar_no_border") 

3100 or options.get("bar_axis_position") 

3101 or options.get("bar_axis_color") 

3102 or options.get("bar_direction") 

3103 ): 

3104 options["is_data_bar_2010"] = True 

3105 

3106 # Special handling of text criteria. 

3107 if options["type"] == "text": 

3108 value = options["value"] 

3109 length = len(value) 

3110 criteria = options["criteria"] 

3111 

3112 if options["criteria"] == "containsText": 

3113 options["type"] = "containsText" 

3114 options["formula"] = f'NOT(ISERROR(SEARCH("{value}",{start_cell})))' 

3115 elif options["criteria"] == "notContains": 

3116 options["type"] = "notContainsText" 

3117 options["formula"] = f'ISERROR(SEARCH("{value}",{start_cell}))' 

3118 elif options["criteria"] == "beginsWith": 

3119 options["type"] = "beginsWith" 

3120 options["formula"] = f'LEFT({start_cell},{length})="{value}"' 

3121 elif options["criteria"] == "endsWith": 

3122 options["type"] = "endsWith" 

3123 options["formula"] = f'RIGHT({start_cell},{length})="{value}"' 

3124 else: 

3125 warn(f"Invalid text criteria '{criteria}' in conditional_format()") 

3126 

3127 # Special handling of time time_period criteria. 

3128 if options["type"] == "timePeriod": 

3129 if options["criteria"] == "yesterday": 

3130 options["formula"] = f"FLOOR({start_cell},1)=TODAY()-1" 

3131 

3132 elif options["criteria"] == "today": 

3133 options["formula"] = f"FLOOR({start_cell},1)=TODAY()" 

3134 

3135 elif options["criteria"] == "tomorrow": 

3136 options["formula"] = f"FLOOR({start_cell},1)=TODAY()+1" 

3137 

3138 # fmt: off 

3139 elif options["criteria"] == "last7Days": 

3140 options["formula"] = ( 

3141 f"AND(TODAY()-FLOOR({start_cell},1)<=6," 

3142 f"FLOOR({start_cell},1)<=TODAY())" 

3143 ) 

3144 # fmt: on 

3145 

3146 elif options["criteria"] == "lastWeek": 

3147 options["formula"] = ( 

3148 f"AND(TODAY()-ROUNDDOWN({start_cell},0)>=(WEEKDAY(TODAY()))," 

3149 f"TODAY()-ROUNDDOWN({start_cell},0)<(WEEKDAY(TODAY())+7))" 

3150 ) 

3151 

3152 elif options["criteria"] == "thisWeek": 

3153 options["formula"] = ( 

3154 f"AND(TODAY()-ROUNDDOWN({start_cell},0)<=WEEKDAY(TODAY())-1," 

3155 f"ROUNDDOWN({start_cell},0)-TODAY()<=7-WEEKDAY(TODAY()))" 

3156 ) 

3157 

3158 elif options["criteria"] == "nextWeek": 

3159 options["formula"] = ( 

3160 f"AND(ROUNDDOWN({start_cell},0)-TODAY()>(7-WEEKDAY(TODAY()))," 

3161 f"ROUNDDOWN({start_cell},0)-TODAY()<(15-WEEKDAY(TODAY())))" 

3162 ) 

3163 

3164 elif options["criteria"] == "lastMonth": 

3165 options["formula"] = ( 

3166 f"AND(MONTH({start_cell})=MONTH(TODAY())-1," 

3167 f"OR(YEAR({start_cell})=YEAR(" 

3168 f"TODAY()),AND(MONTH({start_cell})=1,YEAR(A1)=YEAR(TODAY())-1)))" 

3169 ) 

3170 

3171 # fmt: off 

3172 elif options["criteria"] == "thisMonth": 

3173 options["formula"] = ( 

3174 f"AND(MONTH({start_cell})=MONTH(TODAY())," 

3175 f"YEAR({start_cell})=YEAR(TODAY()))" 

3176 ) 

3177 # fmt: on 

3178 

3179 elif options["criteria"] == "nextMonth": 

3180 options["formula"] = ( 

3181 f"AND(MONTH({start_cell})=MONTH(TODAY())+1," 

3182 f"OR(YEAR({start_cell})=YEAR(" 

3183 f"TODAY()),AND(MONTH({start_cell})=12," 

3184 f"YEAR({start_cell})=YEAR(TODAY())+1)))" 

3185 ) 

3186 

3187 else: 

3188 warn( 

3189 f"Invalid time_period criteria '{options['criteria']}' " 

3190 f"in conditional_format()" 

3191 ) 

3192 

3193 # Special handling of blanks/error types. 

3194 if options["type"] == "containsBlanks": 

3195 options["formula"] = f"LEN(TRIM({start_cell}))=0" 

3196 

3197 if options["type"] == "notContainsBlanks": 

3198 options["formula"] = f"LEN(TRIM({start_cell}))>0" 

3199 

3200 if options["type"] == "containsErrors": 

3201 options["formula"] = f"ISERROR({start_cell})" 

3202 

3203 if options["type"] == "notContainsErrors": 

3204 options["formula"] = f"NOT(ISERROR({start_cell}))" 

3205 

3206 # Special handling for 2 color scale. 

3207 if options["type"] == "2_color_scale": 

3208 options["type"] = "colorScale" 

3209 

3210 # Color scales don't use any additional formatting. 

3211 options["format"] = None 

3212 

3213 # Turn off 3 color parameters. 

3214 options["mid_type"] = None 

3215 options["mid_color"] = None 

3216 

3217 options.setdefault("min_type", "min") 

3218 options.setdefault("max_type", "max") 

3219 options.setdefault("min_value", 0) 

3220 options.setdefault("max_value", 0) 

3221 options.setdefault("min_color", Color("#FF7128")) 

3222 options.setdefault("max_color", Color("#FFEF9C")) 

3223 

3224 options["min_color"] = Color._from_value(options["min_color"]) 

3225 options["max_color"] = Color._from_value(options["max_color"]) 

3226 

3227 # Special handling for 3 color scale. 

3228 if options["type"] == "3_color_scale": 

3229 options["type"] = "colorScale" 

3230 

3231 # Color scales don't use any additional formatting. 

3232 options["format"] = None 

3233 

3234 options.setdefault("min_type", "min") 

3235 options.setdefault("mid_type", "percentile") 

3236 options.setdefault("max_type", "max") 

3237 options.setdefault("min_value", 0) 

3238 options.setdefault("max_value", 0) 

3239 options.setdefault("min_color", Color("#F8696B")) 

3240 options.setdefault("mid_color", Color("#FFEB84")) 

3241 options.setdefault("max_color", Color("#63BE7B")) 

3242 

3243 options["min_color"] = Color._from_value(options["min_color"]) 

3244 options["mid_color"] = Color._from_value(options["mid_color"]) 

3245 options["max_color"] = Color._from_value(options["max_color"]) 

3246 

3247 # Set a default mid value. 

3248 if "mid_value" not in options: 

3249 options["mid_value"] = 50 

3250 

3251 # Special handling for data bar. 

3252 if options["type"] == "dataBar": 

3253 # Color scales don't use any additional formatting. 

3254 options["format"] = None 

3255 

3256 if not options.get("min_type"): 

3257 options["min_type"] = "min" 

3258 options["x14_min_type"] = "autoMin" 

3259 else: 

3260 options["x14_min_type"] = options["min_type"] 

3261 

3262 if not options.get("max_type"): 

3263 options["max_type"] = "max" 

3264 options["x14_max_type"] = "autoMax" 

3265 else: 

3266 options["x14_max_type"] = options["max_type"] 

3267 

3268 options.setdefault("min_value", 0) 

3269 options.setdefault("max_value", 0) 

3270 options.setdefault("bar_color", Color("#638EC6")) 

3271 options.setdefault("bar_border_color", options["bar_color"]) 

3272 options.setdefault("bar_only", False) 

3273 options.setdefault("bar_no_border", False) 

3274 options.setdefault("bar_solid", False) 

3275 options.setdefault("bar_direction", "") 

3276 options.setdefault("bar_negative_color", Color("#FF0000")) 

3277 options.setdefault("bar_negative_border_color", Color("#FF0000")) 

3278 options.setdefault("bar_negative_color_same", False) 

3279 options.setdefault("bar_negative_border_color_same", False) 

3280 options.setdefault("bar_axis_position", "") 

3281 options.setdefault("bar_axis_color", Color("#000000")) 

3282 

3283 options["bar_color"] = Color._from_value(options["bar_color"]) 

3284 options["bar_border_color"] = Color._from_value(options["bar_border_color"]) 

3285 options["bar_axis_color"] = Color._from_value(options["bar_axis_color"]) 

3286 options["bar_negative_color"] = Color._from_value( 

3287 options["bar_negative_color"] 

3288 ) 

3289 options["bar_negative_border_color"] = Color._from_value( 

3290 options["bar_negative_border_color"] 

3291 ) 

3292 

3293 # Adjust for 2010 style data_bar parameters. 

3294 if options.get("is_data_bar_2010"): 

3295 self.excel_version = 2010 

3296 

3297 if options["min_type"] == "min" and options["min_value"] == 0: 

3298 options["min_value"] = None 

3299 

3300 if options["max_type"] == "max" and options["max_value"] == 0: 

3301 options["max_value"] = None 

3302 

3303 options["range"] = cell_range 

3304 

3305 # Strip the leading = from formulas. 

3306 try: 

3307 options["min_value"] = options["min_value"].lstrip("=") 

3308 except (KeyError, AttributeError): 

3309 pass 

3310 try: 

3311 options["mid_value"] = options["mid_value"].lstrip("=") 

3312 except (KeyError, AttributeError): 

3313 pass 

3314 try: 

3315 options["max_value"] = options["max_value"].lstrip("=") 

3316 except (KeyError, AttributeError): 

3317 pass 

3318 

3319 # Store the conditional format until we close the worksheet. 

3320 if cell_range in self.cond_formats: 

3321 self.cond_formats[cell_range].append(options) 

3322 else: 

3323 self.cond_formats[cell_range] = [options] 

3324 

3325 return 0 

3326 

3327 @convert_range_args 

3328 def add_table( 

3329 self, 

3330 first_row: int, 

3331 first_col: int, 

3332 last_row: int, 

3333 last_col: int, 

3334 options: Optional[Dict[str, Any]] = None, 

3335 ) -> Literal[0, -1, -2, -3]: 

3336 """ 

3337 Add an Excel table to a worksheet. 

3338 

3339 Args: 

3340 first_row: The first row of the cell range. (zero indexed). 

3341 first_col: The first column of the cell range. 

3342 last_row: The last row of the cell range. (zero indexed). 

3343 last_col: The last column of the cell range. 

3344 options: Table format options. (Optional) 

3345 

3346 Returns: 

3347 0: Success. 

3348 -1: Row or column is out of worksheet bounds. 

3349 -2: Incorrect parameter or option. 

3350 -3: Not supported in constant_memory mode. 

3351 """ 

3352 table = {} 

3353 col_formats = {} 

3354 

3355 if options is None: 

3356 options = {} 

3357 else: 

3358 # Copy the user defined options so they aren't modified. 

3359 options = options.copy() 

3360 

3361 if self.constant_memory: 

3362 warn("add_table() isn't supported in 'constant_memory' mode") 

3363 return -3 

3364 

3365 # Check that row and col are valid without storing the values. 

3366 if self._check_dimensions(first_row, first_col, True, True): 

3367 return -1 

3368 if self._check_dimensions(last_row, last_col, True, True): 

3369 return -1 

3370 

3371 # Swap last row/col for first row/col as necessary. 

3372 if first_row > last_row: 

3373 (first_row, last_row) = (last_row, first_row) 

3374 if first_col > last_col: 

3375 (first_col, last_col) = (last_col, first_col) 

3376 

3377 # Check if the table range overlaps a previous merged or table range. 

3378 # This is a critical file corruption error in Excel. 

3379 cell_range = xl_range(first_row, first_col, last_row, last_col) 

3380 for row in range(first_row, last_row + 1): 

3381 for col in range(first_col, last_col + 1): 

3382 if self.table_cells.get((row, col)): 

3383 previous_range = self.table_cells.get((row, col)) 

3384 raise OverlappingRange( 

3385 f"Table range '{cell_range}' overlaps previous " 

3386 f"table range '{previous_range}'." 

3387 ) 

3388 

3389 if self.merged_cells.get((row, col)): 

3390 previous_range = self.merged_cells.get((row, col)) 

3391 raise OverlappingRange( 

3392 f"Table range '{cell_range}' overlaps previous " 

3393 f"merge range '{previous_range}'." 

3394 ) 

3395 

3396 self.table_cells[(row, col)] = cell_range 

3397 

3398 # Valid input parameters. 

3399 valid_parameter = { 

3400 "autofilter", 

3401 "banded_columns", 

3402 "banded_rows", 

3403 "columns", 

3404 "data", 

3405 "first_column", 

3406 "header_row", 

3407 "last_column", 

3408 "name", 

3409 "style", 

3410 "total_row", 

3411 } 

3412 

3413 # Check for valid input parameters. 

3414 for param_key in options.keys(): 

3415 if param_key not in valid_parameter: 

3416 warn(f"Unknown parameter '{param_key}' in add_table()") 

3417 return -2 

3418 

3419 # Turn on Excel's defaults. 

3420 options["banded_rows"] = options.get("banded_rows", True) 

3421 options["header_row"] = options.get("header_row", True) 

3422 options["autofilter"] = options.get("autofilter", True) 

3423 

3424 # Check that there are enough rows. 

3425 num_rows = last_row - first_row 

3426 if options["header_row"]: 

3427 num_rows -= 1 

3428 

3429 if num_rows < 0: 

3430 warn("Must have at least one data row in in add_table()") 

3431 return -2 

3432 

3433 # Set the table options. 

3434 table["show_first_col"] = options.get("first_column", False) 

3435 table["show_last_col"] = options.get("last_column", False) 

3436 table["show_row_stripes"] = options.get("banded_rows", False) 

3437 table["show_col_stripes"] = options.get("banded_columns", False) 

3438 table["header_row_count"] = options.get("header_row", 0) 

3439 table["totals_row_shown"] = options.get("total_row", False) 

3440 

3441 # Set the table name. 

3442 if "name" in options: 

3443 name = options["name"] 

3444 table["name"] = name 

3445 

3446 if " " in name: 

3447 warn(f"Name '{name}' in add_table() cannot contain spaces") 

3448 return -2 

3449 

3450 # Warn if the name contains invalid chars as defined by Excel. 

3451 if not re.match(r"^[\w\\][\w\\.]*$", name, re.UNICODE) or re.match( 

3452 r"^\d", name 

3453 ): 

3454 warn(f"Invalid Excel characters in add_table(): '{name}'") 

3455 return -2 

3456 

3457 # Warn if the name looks like a cell name. 

3458 if re.match(r"^[a-zA-Z][a-zA-Z]?[a-dA-D]?\d+$", name): 

3459 warn(f"Name looks like a cell name in add_table(): '{name}'") 

3460 return -2 

3461 

3462 # Warn if the name looks like a R1C1 cell reference. 

3463 if re.match(r"^[rcRC]$", name) or re.match(r"^[rcRC]\d+[rcRC]\d+$", name): 

3464 warn(f"Invalid name '{name}' like a RC cell ref in add_table()") 

3465 return -2 

3466 

3467 # Set the table style. 

3468 if "style" in options: 

3469 table["style"] = options["style"] 

3470 

3471 if table["style"] is None: 

3472 table["style"] = "" 

3473 

3474 # Remove whitespace from style name. 

3475 table["style"] = table["style"].replace(" ", "") 

3476 else: 

3477 table["style"] = "TableStyleMedium9" 

3478 

3479 # Set the data range rows (without the header and footer). 

3480 first_data_row = first_row 

3481 last_data_row = last_row 

3482 

3483 if options.get("header_row"): 

3484 first_data_row += 1 

3485 

3486 if options.get("total_row"): 

3487 last_data_row -= 1 

3488 

3489 # Set the table and autofilter ranges. 

3490 table["range"] = xl_range(first_row, first_col, last_row, last_col) 

3491 

3492 table["a_range"] = xl_range(first_row, first_col, last_data_row, last_col) 

3493 

3494 # If the header row if off the default is to turn autofilter off. 

3495 if not options["header_row"]: 

3496 options["autofilter"] = 0 

3497 

3498 # Set the autofilter range. 

3499 if options["autofilter"]: 

3500 table["autofilter"] = table["a_range"] 

3501 

3502 # Add the table columns. 

3503 col_id = 1 

3504 table["columns"] = [] 

3505 seen_names = {} 

3506 

3507 for col_num in range(first_col, last_col + 1): 

3508 # Set up the default column data. 

3509 col_data = { 

3510 "id": col_id, 

3511 "name": "Column" + str(col_id), 

3512 "total_string": "", 

3513 "total_function": "", 

3514 "custom_total": "", 

3515 "total_value": 0, 

3516 "formula": "", 

3517 "format": None, 

3518 "name_format": None, 

3519 } 

3520 

3521 # Overwrite the defaults with any user defined values. 

3522 if "columns" in options: 

3523 # Check if there are user defined values for this column. 

3524 if col_id <= len(options["columns"]): 

3525 user_data = options["columns"][col_id - 1] 

3526 else: 

3527 user_data = None 

3528 

3529 if user_data: 

3530 # Get the column format. 

3531 xformat = user_data.get("format", None) 

3532 

3533 # Map user defined values to internal values. 

3534 if user_data.get("header"): 

3535 col_data["name"] = user_data["header"] 

3536 

3537 # Excel requires unique case insensitive header names. 

3538 header_name = col_data["name"] 

3539 name = header_name.lower() 

3540 if name in seen_names: 

3541 warn(f"Duplicate header name in add_table(): '{name}'") 

3542 return -2 

3543 

3544 seen_names[name] = True 

3545 

3546 col_data["name_format"] = user_data.get("header_format") 

3547 

3548 # Handle the column formula. 

3549 if "formula" in user_data and user_data["formula"]: 

3550 formula = user_data["formula"] 

3551 

3552 # Remove the formula '=' sign if it exists. 

3553 if formula.startswith("="): 

3554 formula = formula.lstrip("=") 

3555 

3556 # Convert Excel 2010 "@" ref to 2007 "#This Row". 

3557 formula = self._prepare_table_formula(formula) 

3558 

3559 # Escape any future functions. 

3560 formula = self._prepare_formula(formula, True) 

3561 

3562 col_data["formula"] = formula 

3563 # We write the formulas below after the table data. 

3564 

3565 # Handle the function for the total row. 

3566 if user_data.get("total_function"): 

3567 function = user_data["total_function"] 

3568 if function == "count_nums": 

3569 function = "countNums" 

3570 if function == "std_dev": 

3571 function = "stdDev" 

3572 

3573 subtotals = set( 

3574 [ 

3575 "average", 

3576 "countNums", 

3577 "count", 

3578 "max", 

3579 "min", 

3580 "stdDev", 

3581 "sum", 

3582 "var", 

3583 ] 

3584 ) 

3585 

3586 if function in subtotals: 

3587 formula = self._table_function_to_formula( 

3588 function, col_data["name"] 

3589 ) 

3590 else: 

3591 formula = self._prepare_formula(function, True) 

3592 col_data["custom_total"] = formula 

3593 function = "custom" 

3594 

3595 col_data["total_function"] = function 

3596 

3597 value = user_data.get("total_value", 0) 

3598 

3599 self._write_formula(last_row, col_num, formula, xformat, value) 

3600 

3601 elif user_data.get("total_string"): 

3602 # Total label only (not a function). 

3603 total_string = user_data["total_string"] 

3604 col_data["total_string"] = total_string 

3605 

3606 self._write_string( 

3607 last_row, col_num, total_string, user_data.get("format") 

3608 ) 

3609 

3610 # Get the dxf format index. 

3611 if xformat is not None: 

3612 col_data["format"] = xformat._get_dxf_index() 

3613 

3614 # Store the column format for writing the cell data. 

3615 # It doesn't matter if it is undefined. 

3616 col_formats[col_id - 1] = xformat 

3617 

3618 # Store the column data. 

3619 table["columns"].append(col_data) 

3620 

3621 # Write the column headers to the worksheet. 

3622 if options["header_row"]: 

3623 self._write_string( 

3624 first_row, col_num, col_data["name"], col_data["name_format"] 

3625 ) 

3626 

3627 col_id += 1 

3628 

3629 # Write the cell data if supplied. 

3630 if "data" in options: 

3631 data = options["data"] 

3632 

3633 i = 0 # For indexing the row data. 

3634 for row in range(first_data_row, last_data_row + 1): 

3635 j = 0 # For indexing the col data. 

3636 for col in range(first_col, last_col + 1): 

3637 if i < len(data) and j < len(data[i]): 

3638 token = data[i][j] 

3639 if j in col_formats: 

3640 self._write(row, col, token, col_formats[j]) 

3641 else: 

3642 self._write(row, col, token, None) 

3643 j += 1 

3644 i += 1 

3645 

3646 # Write any columns formulas after the user supplied table data to 

3647 # overwrite it if required. 

3648 for col_id, col_num in enumerate(range(first_col, last_col + 1)): 

3649 column_data = table["columns"][col_id] 

3650 if column_data and column_data["formula"]: 

3651 formula_format = col_formats.get(col_id) 

3652 formula = column_data["formula"] 

3653 

3654 for row in range(first_data_row, last_data_row + 1): 

3655 self._write_formula(row, col_num, formula, formula_format) 

3656 

3657 # Store the table data. 

3658 self.tables.append(table) 

3659 

3660 # Store the filter cell positions for use in the autofit calculation. 

3661 if options["autofilter"]: 

3662 for col in range(first_col, last_col + 1): 

3663 # Check that the table autofilter doesn't overlap a worksheet filter. 

3664 if self.filter_cells.get((first_row, col)): 

3665 filter_type, filter_range = self.filter_cells.get((first_row, col)) 

3666 if filter_type == "worksheet": 

3667 raise OverlappingRange( 

3668 f"Table autofilter range '{cell_range}' overlaps previous " 

3669 f"Worksheet autofilter range '{filter_range}'." 

3670 ) 

3671 

3672 self.filter_cells[(first_row, col)] = ("table", cell_range) 

3673 

3674 return 0 

3675 

3676 @convert_cell_args 

3677 def add_sparkline( 

3678 self, row: int, col: int, options: Optional[Dict[str, Any]] = None 

3679 ) -> Literal[0, -1, -2]: 

3680 """ 

3681 Add sparklines to the worksheet. 

3682 

3683 Args: 

3684 row: The cell row (zero indexed). 

3685 col: The cell column (zero indexed). 

3686 options: Sparkline formatting options. 

3687 

3688 Returns: 

3689 0: Success. 

3690 -1: Row or column is out of worksheet bounds. 

3691 -2: Incorrect parameter or option. 

3692 

3693 """ 

3694 

3695 # Check that row and col are valid without storing the values. 

3696 if self._check_dimensions(row, col, True, True): 

3697 return -1 

3698 

3699 sparkline = {"locations": [xl_rowcol_to_cell(row, col)]} 

3700 

3701 if options is None: 

3702 options = {} 

3703 

3704 # Valid input parameters. 

3705 valid_parameters = { 

3706 "location", 

3707 "range", 

3708 "type", 

3709 "high_point", 

3710 "low_point", 

3711 "negative_points", 

3712 "first_point", 

3713 "last_point", 

3714 "markers", 

3715 "style", 

3716 "series_color", 

3717 "negative_color", 

3718 "markers_color", 

3719 "first_color", 

3720 "last_color", 

3721 "high_color", 

3722 "low_color", 

3723 "max", 

3724 "min", 

3725 "axis", 

3726 "reverse", 

3727 "empty_cells", 

3728 "show_hidden", 

3729 "plot_hidden", 

3730 "date_axis", 

3731 "weight", 

3732 } 

3733 

3734 # Check for valid input parameters. 

3735 for param_key in options.keys(): 

3736 if param_key not in valid_parameters: 

3737 warn(f"Unknown parameter '{param_key}' in add_sparkline()") 

3738 return -1 

3739 

3740 # 'range' is a required parameter. 

3741 if "range" not in options: 

3742 warn("Parameter 'range' is required in add_sparkline()") 

3743 return -2 

3744 

3745 # Handle the sparkline type. 

3746 spark_type = options.get("type", "line") 

3747 

3748 if spark_type not in ("line", "column", "win_loss"): 

3749 warn( 

3750 "Parameter 'type' must be 'line', 'column' " 

3751 "or 'win_loss' in add_sparkline()" 

3752 ) 

3753 return -2 

3754 

3755 if spark_type == "win_loss": 

3756 spark_type = "stacked" 

3757 sparkline["type"] = spark_type 

3758 

3759 # We handle single location/range values or list of values. 

3760 if "location" in options: 

3761 if isinstance(options["location"], list): 

3762 sparkline["locations"] = options["location"] 

3763 else: 

3764 sparkline["locations"] = [options["location"]] 

3765 

3766 if isinstance(options["range"], list): 

3767 sparkline["ranges"] = options["range"] 

3768 else: 

3769 sparkline["ranges"] = [options["range"]] 

3770 

3771 range_count = len(sparkline["ranges"]) 

3772 location_count = len(sparkline["locations"]) 

3773 

3774 # The ranges and locations must match. 

3775 if range_count != location_count: 

3776 warn( 

3777 "Must have the same number of location and range " 

3778 "parameters in add_sparkline()" 

3779 ) 

3780 return -2 

3781 

3782 # Store the count. 

3783 sparkline["count"] = len(sparkline["locations"]) 

3784 

3785 # Get the worksheet name for the range conversion below. 

3786 sheetname = quote_sheetname(self.name) 

3787 

3788 # Cleanup the input ranges. 

3789 new_ranges = [] 

3790 for spark_range in sparkline["ranges"]: 

3791 # Remove the absolute reference $ symbols. 

3792 spark_range = spark_range.replace("$", "") 

3793 

3794 # Remove the = from formula. 

3795 spark_range = spark_range.lstrip("=") 

3796 

3797 # Convert a simple range into a full Sheet1!A1:D1 range. 

3798 if "!" not in spark_range: 

3799 spark_range = sheetname + "!" + spark_range 

3800 

3801 new_ranges.append(spark_range) 

3802 

3803 sparkline["ranges"] = new_ranges 

3804 

3805 # Cleanup the input locations. 

3806 new_locations = [] 

3807 for location in sparkline["locations"]: 

3808 location = location.replace("$", "") 

3809 new_locations.append(location) 

3810 

3811 sparkline["locations"] = new_locations 

3812 

3813 # Map options. 

3814 sparkline["high"] = options.get("high_point") 

3815 sparkline["low"] = options.get("low_point") 

3816 sparkline["negative"] = options.get("negative_points") 

3817 sparkline["first"] = options.get("first_point") 

3818 sparkline["last"] = options.get("last_point") 

3819 sparkline["markers"] = options.get("markers") 

3820 sparkline["min"] = options.get("min") 

3821 sparkline["max"] = options.get("max") 

3822 sparkline["axis"] = options.get("axis") 

3823 sparkline["reverse"] = options.get("reverse") 

3824 sparkline["hidden"] = options.get("show_hidden") 

3825 sparkline["weight"] = options.get("weight") 

3826 

3827 # Map empty cells options. 

3828 empty = options.get("empty_cells", "") 

3829 

3830 if empty == "zero": 

3831 sparkline["empty"] = 0 

3832 elif empty == "connect": 

3833 sparkline["empty"] = "span" 

3834 else: 

3835 sparkline["empty"] = "gap" 

3836 

3837 # Map the date axis range. 

3838 date_range = options.get("date_axis") 

3839 

3840 if date_range and "!" not in date_range: 

3841 date_range = sheetname + "!" + date_range 

3842 

3843 sparkline["date_axis"] = date_range 

3844 

3845 # Set the sparkline styles. 

3846 style_id = options.get("style", 0) 

3847 style = _get_sparkline_style(style_id) 

3848 

3849 sparkline["series_color"] = style["series"] 

3850 sparkline["negative_color"] = style["negative"] 

3851 sparkline["markers_color"] = style["markers"] 

3852 sparkline["first_color"] = style["first"] 

3853 sparkline["last_color"] = style["last"] 

3854 sparkline["high_color"] = style["high"] 

3855 sparkline["low_color"] = style["low"] 

3856 

3857 # Override the style colors with user defined colors. 

3858 self._set_spark_color(sparkline, options, "series_color") 

3859 self._set_spark_color(sparkline, options, "negative_color") 

3860 self._set_spark_color(sparkline, options, "markers_color") 

3861 self._set_spark_color(sparkline, options, "first_color") 

3862 self._set_spark_color(sparkline, options, "last_color") 

3863 self._set_spark_color(sparkline, options, "high_color") 

3864 self._set_spark_color(sparkline, options, "low_color") 

3865 

3866 self.sparklines.append(sparkline) 

3867 

3868 return 0 

3869 

3870 @convert_range_args 

3871 def set_selection( 

3872 self, first_row: int, first_col: int, last_row: int, last_col: int 

3873 ) -> None: 

3874 """ 

3875 Set the selected cell or cells in a worksheet 

3876 

3877 Args: 

3878 first_row: The first row of the cell range. (zero indexed). 

3879 first_col: The first column of the cell range. 

3880 last_row: The last row of the cell range. (zero indexed). 

3881 last_col: The last column of the cell range. 

3882 

3883 Returns: 

3884 0: Nothing. 

3885 """ 

3886 pane = None 

3887 

3888 # Range selection. Do this before swapping max/min to allow the 

3889 # selection direction to be reversed. 

3890 active_cell = xl_rowcol_to_cell(first_row, first_col) 

3891 

3892 # Swap last row/col for first row/col if necessary 

3893 if first_row > last_row: 

3894 (first_row, last_row) = (last_row, first_row) 

3895 

3896 if first_col > last_col: 

3897 (first_col, last_col) = (last_col, first_col) 

3898 

3899 sqref = xl_range(first_row, first_col, last_row, last_col) 

3900 

3901 # Selection isn't set for cell A1. 

3902 if sqref == "A1": 

3903 return 

3904 

3905 self.selections = [[pane, active_cell, sqref]] 

3906 

3907 @convert_cell_args 

3908 def set_top_left_cell(self, row: int = 0, col: int = 0) -> None: 

3909 """ 

3910 Set the first visible cell at the top left of a worksheet. 

3911 

3912 Args: 

3913 row: The cell row (zero indexed). 

3914 col: The cell column (zero indexed). 

3915 

3916 Returns: 

3917 0: Nothing. 

3918 """ 

3919 

3920 if row == 0 and col == 0: 

3921 return 

3922 

3923 self.top_left_cell = xl_rowcol_to_cell(row, col) 

3924 

3925 def outline_settings( 

3926 self, 

3927 visible: bool = 1, 

3928 symbols_below: bool = 1, 

3929 symbols_right: bool = 1, 

3930 auto_style: bool = 0, 

3931 ) -> None: 

3932 """ 

3933 Control outline settings. 

3934 

3935 Args: 

3936 visible: Outlines are visible. Optional, defaults to True. 

3937 symbols_below: Show row outline symbols below the outline bar. 

3938 Optional, defaults to True. 

3939 symbols_right: Show column outline symbols to the right of the 

3940 outline bar. Optional, defaults to True. 

3941 auto_style: Use Automatic style. Optional, defaults to False. 

3942 

3943 Returns: 

3944 0: Nothing. 

3945 """ 

3946 self.outline_on = visible 

3947 self.outline_below = symbols_below 

3948 self.outline_right = symbols_right 

3949 self.outline_style = auto_style 

3950 

3951 self.outline_changed = True 

3952 

3953 @convert_cell_args 

3954 def freeze_panes( 

3955 self, 

3956 row: int, 

3957 col: int, 

3958 top_row: Optional[int] = None, 

3959 left_col: Optional[int] = None, 

3960 pane_type: int = 0, 

3961 ) -> None: 

3962 """ 

3963 Create worksheet panes and mark them as frozen. 

3964 

3965 Args: 

3966 row: The cell row (zero indexed). 

3967 col: The cell column (zero indexed). 

3968 top_row: Topmost visible row in scrolling region of pane. 

3969 left_col: Leftmost visible row in scrolling region of pane. 

3970 

3971 Returns: 

3972 0: Nothing. 

3973 

3974 """ 

3975 if top_row is None: 

3976 top_row = row 

3977 

3978 if left_col is None: 

3979 left_col = col 

3980 

3981 self.panes = [row, col, top_row, left_col, pane_type] 

3982 

3983 @convert_cell_args 

3984 def split_panes( 

3985 self, 

3986 x: float, 

3987 y: float, 

3988 top_row: Optional[int] = None, 

3989 left_col: Optional[int] = None, 

3990 ) -> None: 

3991 """ 

3992 Create worksheet panes and mark them as split. 

3993 

3994 Args: 

3995 x: The position for the vertical split. 

3996 y: The position for the horizontal split. 

3997 top_row: Topmost visible row in scrolling region of pane. 

3998 left_col: Leftmost visible row in scrolling region of pane. 

3999 

4000 Returns: 

4001 0: Nothing. 

4002 

4003 """ 

4004 # Same as freeze panes with a different pane type. 

4005 self.freeze_panes(x, y, top_row, left_col, 2) 

4006 

4007 def set_zoom(self, zoom: int = 100) -> None: 

4008 """ 

4009 Set the worksheet zoom factor. 

4010 

4011 Args: 

4012 zoom: Scale factor: 10 <= zoom <= 400. 

4013 

4014 Returns: 

4015 Nothing. 

4016 

4017 """ 

4018 # Ensure the zoom scale is in Excel's range. 

4019 if zoom < 10 or zoom > 400: 

4020 warn(f"Zoom factor '{zoom}' outside range: 10 <= zoom <= 400") 

4021 zoom = 100 

4022 

4023 self.zoom = int(zoom) 

4024 

4025 def set_zoom_to_fit(self) -> None: 

4026 """ 

4027 Set the worksheet zoom to selection/fit. Only works for chartsheets. 

4028 

4029 Args: 

4030 None. 

4031 

4032 Returns: 

4033 Nothing. 

4034 

4035 """ 

4036 self.zoom_to_fit = True 

4037 

4038 def right_to_left(self) -> None: 

4039 """ 

4040 Display the worksheet right to left for some versions of Excel. 

4041 

4042 Args: 

4043 None. 

4044 

4045 Returns: 

4046 Nothing. 

4047 

4048 """ 

4049 self.is_right_to_left = True 

4050 

4051 def hide_zero(self) -> None: 

4052 """ 

4053 Hide zero values in worksheet cells. 

4054 

4055 Args: 

4056 None. 

4057 

4058 Returns: 

4059 Nothing. 

4060 

4061 """ 

4062 self.show_zeros = 0 

4063 

4064 def set_tab_color(self, color: Union[str, Color]) -> None: 

4065 """ 

4066 Set the color of the worksheet tab. 

4067 

4068 Args: 

4069 color: A #RGB color index. 

4070 

4071 Returns: 

4072 Nothing. 

4073 

4074 """ 

4075 self.tab_color = Color._from_value(color) 

4076 

4077 def protect( 

4078 self, password: str = "", options: Optional[Dict[str, Any]] = None 

4079 ) -> None: 

4080 """ 

4081 Set the password and protection options of the worksheet. 

4082 

4083 Args: 

4084 password: An optional password string. 

4085 options: A dictionary of worksheet objects to protect. 

4086 

4087 Returns: 

4088 Nothing. 

4089 

4090 """ 

4091 if password != "": 

4092 password = self._encode_password(password) 

4093 

4094 if not options: 

4095 options = {} 

4096 

4097 # Default values for objects that can be protected. 

4098 defaults = { 

4099 "sheet": True, 

4100 "content": False, 

4101 "objects": False, 

4102 "scenarios": False, 

4103 "format_cells": False, 

4104 "format_columns": False, 

4105 "format_rows": False, 

4106 "insert_columns": False, 

4107 "insert_rows": False, 

4108 "insert_hyperlinks": False, 

4109 "delete_columns": False, 

4110 "delete_rows": False, 

4111 "select_locked_cells": True, 

4112 "sort": False, 

4113 "autofilter": False, 

4114 "pivot_tables": False, 

4115 "select_unlocked_cells": True, 

4116 } 

4117 

4118 # Overwrite the defaults with user specified values. 

4119 for key in options.keys(): 

4120 if key in defaults: 

4121 defaults[key] = options[key] 

4122 else: 

4123 warn(f"Unknown protection object: '{key}'") 

4124 

4125 # Set the password after the user defined values. 

4126 defaults["password"] = password 

4127 

4128 self.protect_options = defaults 

4129 

4130 def unprotect_range( 

4131 self, 

4132 cell_range: str, 

4133 range_name: Optional[str] = None, 

4134 password: Optional[str] = None, 

4135 ) -> int: 

4136 """ 

4137 Unprotect ranges within a protected worksheet. 

4138 

4139 Args: 

4140 cell_range: The cell or cell range to unprotect. 

4141 range_name: An optional name for the range. 

4142 password: An optional password string. (undocumented) 

4143 

4144 Returns: 

4145 0: Success. 

4146 -1: Parameter error. 

4147 

4148 """ 

4149 if cell_range is None: 

4150 warn("Cell range must be specified in unprotect_range()") 

4151 return -1 

4152 

4153 # Sanitize the cell range. 

4154 cell_range = cell_range.lstrip("=") 

4155 cell_range = cell_range.replace("$", "") 

4156 

4157 self.num_protected_ranges += 1 

4158 

4159 if range_name is None: 

4160 range_name = "Range" + str(self.num_protected_ranges) 

4161 

4162 if password: 

4163 password = self._encode_password(password) 

4164 

4165 self.protected_ranges.append((cell_range, range_name, password)) 

4166 

4167 return 0 

4168 

4169 @convert_cell_args 

4170 def insert_button( 

4171 self, row: int, col: int, options: Optional[Dict[str, Any]] = None 

4172 ) -> Literal[0, -1]: 

4173 """ 

4174 Insert a button form object into the worksheet. 

4175 

4176 Args: 

4177 row: The cell row (zero indexed). 

4178 col: The cell column (zero indexed). 

4179 options: Button formatting options. 

4180 

4181 Returns: 

4182 0: Success. 

4183 -1: Row or column is out of worksheet bounds. 

4184 

4185 """ 

4186 # Check insert (row, col) without storing. 

4187 if self._check_dimensions(row, col, True, True): 

4188 warn(f"Cannot insert button at ({row}, {col}).") 

4189 return -1 

4190 

4191 if options is None: 

4192 options = {} 

4193 

4194 # Create a new button object. 

4195 height = self.default_row_pixels 

4196 width = self.default_col_pixels 

4197 button_number = 1 + len(self.buttons_list) 

4198 

4199 button = ButtonType(row, col, height, width, button_number, options) 

4200 

4201 self.buttons_list.append(button) 

4202 

4203 self.has_vml = True 

4204 

4205 return 0 

4206 

4207 @convert_cell_args 

4208 def insert_checkbox( 

4209 self, row: int, col: int, boolean: bool, cell_format: Optional[Format] = None 

4210 ): 

4211 """ 

4212 Insert a boolean checkbox in a worksheet cell. 

4213 

4214 Args: 

4215 row: The cell row (zero indexed). 

4216 col: The cell column (zero indexed). 

4217 boolean: The boolean value to display as a checkbox. 

4218 cell_format: Cell Format object. (optional) 

4219 

4220 Returns: 

4221 0: Success. 

4222 -1: Row or column is out of worksheet bounds. 

4223 

4224 """ 

4225 # Ensure that the checkbox property is set in the user defined format. 

4226 if cell_format and not cell_format.checkbox: 

4227 # This needs to be fixed with a clone. 

4228 cell_format.set_checkbox() 

4229 

4230 # If no format is supplied create and/or use the default checkbox format. 

4231 if not cell_format: 

4232 if not self.default_checkbox_format: 

4233 self.default_checkbox_format = self.workbook_add_format() 

4234 self.default_checkbox_format.set_checkbox() 

4235 

4236 cell_format = self.default_checkbox_format 

4237 

4238 return self._write_boolean(row, col, boolean, cell_format) 

4239 

4240 ########################################################################### 

4241 # 

4242 # Public API. Page Setup methods. 

4243 # 

4244 ########################################################################### 

4245 def set_landscape(self) -> None: 

4246 """ 

4247 Set the page orientation as landscape. 

4248 

4249 Args: 

4250 None. 

4251 

4252 Returns: 

4253 Nothing. 

4254 

4255 """ 

4256 self.orientation = 0 

4257 self.page_setup_changed = True 

4258 

4259 def set_portrait(self) -> None: 

4260 """ 

4261 Set the page orientation as portrait. 

4262 

4263 Args: 

4264 None. 

4265 

4266 Returns: 

4267 Nothing. 

4268 

4269 """ 

4270 self.orientation = 1 

4271 self.page_setup_changed = True 

4272 

4273 def set_page_view(self, view: Literal[0, 1, 2] = 1) -> None: 

4274 """ 

4275 Set the page view mode. 

4276 

4277 Args: 

4278 0: Normal view mode 

4279 1: Page view mode (the default) 

4280 2: Page break view mode 

4281 

4282 Returns: 

4283 Nothing. 

4284 

4285 """ 

4286 self.page_view = view 

4287 

4288 def set_pagebreak_view(self) -> None: 

4289 """ 

4290 Set the page view mode. 

4291 

4292 Args: 

4293 None. 

4294 

4295 Returns: 

4296 Nothing. 

4297 

4298 """ 

4299 self.page_view = 2 

4300 

4301 def set_paper(self, paper_size: Union[Literal[1, 9], int]) -> None: 

4302 """ 

4303 Set the paper type. US Letter = 1, A4 = 9. 

4304 

4305 Args: 

4306 paper_size: Paper index. 

4307 

4308 Returns: 

4309 Nothing. 

4310 

4311 """ 

4312 if paper_size: 

4313 self.paper_size = paper_size 

4314 self.page_setup_changed = True 

4315 

4316 def center_horizontally(self) -> None: 

4317 """ 

4318 Center the page horizontally. 

4319 

4320 Args: 

4321 None. 

4322 

4323 Returns: 

4324 Nothing. 

4325 

4326 """ 

4327 self.print_options_changed = True 

4328 self.hcenter = 1 

4329 

4330 def center_vertically(self) -> None: 

4331 """ 

4332 Center the page vertically. 

4333 

4334 Args: 

4335 None. 

4336 

4337 Returns: 

4338 Nothing. 

4339 

4340 """ 

4341 self.print_options_changed = True 

4342 self.vcenter = 1 

4343 

4344 def set_margins( 

4345 self, 

4346 left: float = 0.7, 

4347 right: float = 0.7, 

4348 top: float = 0.75, 

4349 bottom: float = 0.75, 

4350 ) -> None: 

4351 """ 

4352 Set all the page margins in inches. 

4353 

4354 Args: 

4355 left: Left margin. 

4356 right: Right margin. 

4357 top: Top margin. 

4358 bottom: Bottom margin. 

4359 

4360 Returns: 

4361 Nothing. 

4362 

4363 """ 

4364 self.margin_left = left 

4365 self.margin_right = right 

4366 self.margin_top = top 

4367 self.margin_bottom = bottom 

4368 

4369 def set_header( 

4370 self, header: str = "", options: Optional[Dict[str, Any]] = None, margin=None 

4371 ) -> None: 

4372 """ 

4373 Set the page header caption and optional margin. 

4374 

4375 Args: 

4376 header: Header string. 

4377 margin: Header margin. 

4378 options: Header options, mainly for images. 

4379 

4380 Returns: 

4381 Nothing. 

4382 

4383 """ 

4384 header_orig = header 

4385 header = header.replace("&[Picture]", "&G") 

4386 

4387 if len(header) > 255: 

4388 warn("Header string cannot be longer than Excel's limit of 255 characters") 

4389 return 

4390 

4391 if options is not None: 

4392 # For backward compatibility allow options to be the margin. 

4393 if not isinstance(options, dict): 

4394 options = {"margin": options} 

4395 else: 

4396 options = {} 

4397 

4398 # Copy the user defined options so they aren't modified. 

4399 options = options.copy() 

4400 

4401 # For backward compatibility. 

4402 if margin is not None: 

4403 options["margin"] = margin 

4404 

4405 # Reset the list in case the function is called more than once. 

4406 self.header_images = [] 

4407 

4408 if options.get("image_left"): 

4409 options["image_data"] = options.get("image_data_left") 

4410 image = self._image_from_source(options.get("image_left"), options) 

4411 image._header_position = "LH" 

4412 self.header_images.append(image) 

4413 

4414 if options.get("image_center"): 

4415 options["image_data"] = options.get("image_data_center") 

4416 image = self._image_from_source(options.get("image_center"), options) 

4417 image._header_position = "CH" 

4418 self.header_images.append(image) 

4419 

4420 if options.get("image_right"): 

4421 options["image_data"] = options.get("image_data_right") 

4422 image = self._image_from_source(options.get("image_right"), options) 

4423 image._header_position = "RH" 

4424 self.header_images.append(image) 

4425 

4426 placeholder_count = header.count("&G") 

4427 image_count = len(self.header_images) 

4428 

4429 if placeholder_count != image_count: 

4430 warn( 

4431 f"Number of footer images '{image_count}' doesn't match placeholder " 

4432 f"count '{placeholder_count}' in string: {header_orig}" 

4433 ) 

4434 self.header_images = [] 

4435 return 

4436 

4437 if "align_with_margins" in options: 

4438 self.header_footer_aligns = options["align_with_margins"] 

4439 

4440 if "scale_with_doc" in options: 

4441 self.header_footer_scales = options["scale_with_doc"] 

4442 

4443 self.header = header 

4444 self.margin_header = options.get("margin", 0.3) 

4445 self.header_footer_changed = True 

4446 

4447 if image_count: 

4448 self.has_header_vml = True 

4449 

4450 def set_footer( 

4451 self, footer: str = "", options: Optional[Dict[str, Any]] = None, margin=None 

4452 ) -> None: 

4453 """ 

4454 Set the page footer caption and optional margin. 

4455 

4456 Args: 

4457 footer: Footer string. 

4458 margin: Footer margin. 

4459 options: Footer options, mainly for images. 

4460 

4461 Returns: 

4462 Nothing. 

4463 

4464 """ 

4465 footer_orig = footer 

4466 footer = footer.replace("&[Picture]", "&G") 

4467 

4468 if len(footer) > 255: 

4469 warn("Footer string cannot be longer than Excel's limit of 255 characters") 

4470 return 

4471 

4472 if options is not None: 

4473 # For backward compatibility allow options to be the margin. 

4474 if not isinstance(options, dict): 

4475 options = {"margin": options} 

4476 else: 

4477 options = {} 

4478 

4479 # Copy the user defined options so they aren't modified. 

4480 options = options.copy() 

4481 

4482 # For backward compatibility. 

4483 if margin is not None: 

4484 options["margin"] = margin 

4485 

4486 # Reset the list in case the function is called more than once. 

4487 self.footer_images = [] 

4488 

4489 if options.get("image_left"): 

4490 options["image_data"] = options.get("image_data_left") 

4491 image = self._image_from_source(options.get("image_left"), options) 

4492 image._header_position = "LF" 

4493 self.footer_images.append(image) 

4494 

4495 if options.get("image_center"): 

4496 options["image_data"] = options.get("image_data_center") 

4497 image = self._image_from_source(options.get("image_center"), options) 

4498 image._header_position = "CF" 

4499 self.footer_images.append(image) 

4500 

4501 if options.get("image_right"): 

4502 options["image_data"] = options.get("image_data_right") 

4503 image = self._image_from_source(options.get("image_right"), options) 

4504 image._header_position = "RF" 

4505 self.footer_images.append(image) 

4506 

4507 placeholder_count = footer.count("&G") 

4508 image_count = len(self.footer_images) 

4509 

4510 if placeholder_count != image_count: 

4511 warn( 

4512 f"Number of footer images '{image_count}' doesn't match placeholder " 

4513 f"count '{placeholder_count}' in string: {footer_orig}" 

4514 ) 

4515 self.footer_images = [] 

4516 return 

4517 

4518 if "align_with_margins" in options: 

4519 self.header_footer_aligns = options["align_with_margins"] 

4520 

4521 if "scale_with_doc" in options: 

4522 self.header_footer_scales = options["scale_with_doc"] 

4523 

4524 self.footer = footer 

4525 self.margin_footer = options.get("margin", 0.3) 

4526 self.header_footer_changed = True 

4527 

4528 if image_count: 

4529 self.has_header_vml = True 

4530 

4531 def repeat_rows(self, first_row: int, last_row: Optional[int] = None) -> None: 

4532 """ 

4533 Set the rows to repeat at the top of each printed page. 

4534 

4535 Args: 

4536 first_row: Start row for range. 

4537 last_row: End row for range. 

4538 

4539 Returns: 

4540 Nothing. 

4541 

4542 """ 

4543 if last_row is None: 

4544 last_row = first_row 

4545 

4546 # Convert rows to 1 based. 

4547 first_row += 1 

4548 last_row += 1 

4549 

4550 # Create the row range area like: $1:$2. 

4551 area = f"${first_row}:${last_row}" 

4552 

4553 # Build up the print titles area "Sheet1!$1:$2" 

4554 sheetname = quote_sheetname(self.name) 

4555 self.repeat_row_range = sheetname + "!" + area 

4556 

4557 @convert_column_args 

4558 def repeat_columns(self, first_col: int, last_col: Optional[int] = None) -> None: 

4559 """ 

4560 Set the columns to repeat at the left hand side of each printed page. 

4561 

4562 Args: 

4563 first_col: Start column for range. 

4564 last_col: End column for range. 

4565 

4566 Returns: 

4567 Nothing. 

4568 

4569 """ 

4570 if last_col is None: 

4571 last_col = first_col 

4572 

4573 # Convert to A notation. 

4574 first_col = xl_col_to_name(first_col, 1) 

4575 last_col = xl_col_to_name(last_col, 1) 

4576 

4577 # Create a column range like $C:$D. 

4578 area = first_col + ":" + last_col 

4579 

4580 # Build up the print area range "=Sheet2!$C:$D" 

4581 sheetname = quote_sheetname(self.name) 

4582 self.repeat_col_range = sheetname + "!" + area 

4583 

4584 def hide_gridlines(self, option: Literal[0, 1, 2] = 1) -> None: 

4585 """ 

4586 Set the option to hide gridlines on the screen and the printed page. 

4587 

4588 Args: 

4589 option: 0 : Don't hide gridlines 

4590 1 : Hide printed gridlines only 

4591 2 : Hide screen and printed gridlines 

4592 

4593 Returns: 

4594 Nothing. 

4595 

4596 """ 

4597 if option == 0: 

4598 self.print_gridlines = 1 

4599 self.screen_gridlines = 1 

4600 self.print_options_changed = True 

4601 elif option == 1: 

4602 self.print_gridlines = 0 

4603 self.screen_gridlines = 1 

4604 else: 

4605 self.print_gridlines = 0 

4606 self.screen_gridlines = 0 

4607 

4608 def print_row_col_headers(self) -> None: 

4609 """ 

4610 Set the option to print the row and column headers on the printed page. 

4611 

4612 Args: 

4613 None. 

4614 

4615 Returns: 

4616 Nothing. 

4617 

4618 """ 

4619 self.print_headers = True 

4620 self.print_options_changed = True 

4621 

4622 def hide_row_col_headers(self) -> None: 

4623 """ 

4624 Set the option to hide the row and column headers on the worksheet. 

4625 

4626 Args: 

4627 None. 

4628 

4629 Returns: 

4630 Nothing. 

4631 

4632 """ 

4633 self.row_col_headers = True 

4634 

4635 @convert_range_args 

4636 def print_area( 

4637 self, first_row: int, first_col: int, last_row: int, last_col: int 

4638 ) -> Literal[0, -1]: 

4639 """ 

4640 Set the print area in the current worksheet. 

4641 

4642 Args: 

4643 first_row: The first row of the cell range. (zero indexed). 

4644 first_col: The first column of the cell range. 

4645 last_row: The last row of the cell range. (zero indexed). 

4646 last_col: The last column of the cell range. 

4647 

4648 Returns: 

4649 0: Success. 

4650 -1: Row or column is out of worksheet bounds. 

4651 

4652 """ 

4653 # Set the print area in the current worksheet. 

4654 

4655 # Ignore max print area since it is the same as no area for Excel. 

4656 if ( 

4657 first_row == 0 

4658 and first_col == 0 

4659 and last_row == self.xls_rowmax - 1 

4660 and last_col == self.xls_colmax - 1 

4661 ): 

4662 return -1 

4663 

4664 # Build up the print area range "Sheet1!$A$1:$C$13". 

4665 area = self._convert_name_area(first_row, first_col, last_row, last_col) 

4666 self.print_area_range = area 

4667 

4668 return 0 

4669 

4670 def print_across(self) -> None: 

4671 """ 

4672 Set the order in which pages are printed. 

4673 

4674 Args: 

4675 None. 

4676 

4677 Returns: 

4678 Nothing. 

4679 

4680 """ 

4681 self.page_order = 1 

4682 self.page_setup_changed = True 

4683 

4684 def fit_to_pages(self, width: int, height: int) -> None: 

4685 """ 

4686 Fit the printed area to a specific number of pages both vertically and 

4687 horizontally. 

4688 

4689 Args: 

4690 width: Number of pages horizontally. 

4691 height: Number of pages vertically. 

4692 

4693 Returns: 

4694 Nothing. 

4695 

4696 """ 

4697 self.fit_page = 1 

4698 self.fit_width = width 

4699 self.fit_height = height 

4700 self.page_setup_changed = True 

4701 

4702 def set_start_page(self, start_page: int) -> None: 

4703 """ 

4704 Set the start page number when printing. 

4705 

4706 Args: 

4707 start_page: Start page number. 

4708 

4709 Returns: 

4710 Nothing. 

4711 

4712 """ 

4713 self.page_start = start_page 

4714 

4715 def set_print_scale(self, scale: int) -> None: 

4716 """ 

4717 Set the scale factor for the printed page. 

4718 

4719 Args: 

4720 scale: Print scale. 10 <= scale <= 400. 

4721 

4722 Returns: 

4723 Nothing. 

4724 

4725 """ 

4726 # Confine the scale to Excel's range. 

4727 if scale < 10 or scale > 400: 

4728 warn(f"Print scale '{scale}' outside range: 10 <= scale <= 400") 

4729 return 

4730 

4731 # Turn off "fit to page" option when print scale is on. 

4732 self.fit_page = 0 

4733 

4734 self.print_scale = int(scale) 

4735 self.page_setup_changed = True 

4736 

4737 def print_black_and_white(self) -> None: 

4738 """ 

4739 Set the option to print the worksheet in black and white. 

4740 

4741 Args: 

4742 None. 

4743 

4744 Returns: 

4745 Nothing. 

4746 

4747 """ 

4748 self.black_white = True 

4749 self.page_setup_changed = True 

4750 

4751 def set_h_pagebreaks(self, breaks: List[int]) -> None: 

4752 """ 

4753 Set the horizontal page breaks on a worksheet. 

4754 

4755 Args: 

4756 breaks: List of rows where the page breaks should be added. 

4757 

4758 Returns: 

4759 Nothing. 

4760 

4761 """ 

4762 self.hbreaks = breaks 

4763 

4764 def set_v_pagebreaks(self, breaks: List[int]) -> None: 

4765 """ 

4766 Set the horizontal page breaks on a worksheet. 

4767 

4768 Args: 

4769 breaks: List of columns where the page breaks should be added. 

4770 

4771 Returns: 

4772 Nothing. 

4773 

4774 """ 

4775 self.vbreaks = breaks 

4776 

4777 def set_vba_name(self, name: Optional[str] = None) -> None: 

4778 """ 

4779 Set the VBA name for the worksheet. By default this is the 

4780 same as the sheet name: i.e., Sheet1 etc. 

4781 

4782 Args: 

4783 name: The VBA name for the worksheet. 

4784 

4785 Returns: 

4786 Nothing. 

4787 

4788 """ 

4789 if name is not None: 

4790 self.vba_codename = name 

4791 else: 

4792 self.vba_codename = "Sheet" + str(self.index + 1) 

4793 

4794 def ignore_errors(self, options: Optional[Dict[str, Any]] = None) -> Literal[0, -1]: 

4795 """ 

4796 Ignore various Excel errors/warnings in a worksheet for user defined 

4797 ranges. 

4798 

4799 Args: 

4800 options: A dict of ignore errors keys with cell range values. 

4801 

4802 Returns: 

4803 0: Success. 

4804 -1: Incorrect parameter or option. 

4805 

4806 """ 

4807 if options is None: 

4808 return -1 

4809 

4810 # Copy the user defined options so they aren't modified. 

4811 options = options.copy() 

4812 

4813 # Valid input parameters. 

4814 valid_parameters = { 

4815 "number_stored_as_text", 

4816 "eval_error", 

4817 "formula_differs", 

4818 "formula_range", 

4819 "formula_unlocked", 

4820 "empty_cell_reference", 

4821 "list_data_validation", 

4822 "calculated_column", 

4823 "two_digit_text_year", 

4824 } 

4825 

4826 # Check for valid input parameters. 

4827 for param_key in options.keys(): 

4828 if param_key not in valid_parameters: 

4829 warn(f"Unknown parameter '{param_key}' in ignore_errors()") 

4830 return -1 

4831 

4832 self.ignored_errors = options 

4833 

4834 return 0 

4835 

4836 ########################################################################### 

4837 # 

4838 # Private API. 

4839 # 

4840 ########################################################################### 

4841 def _initialize(self, init_data) -> None: 

4842 self.name = init_data["name"] 

4843 self.index = init_data["index"] 

4844 self.str_table = init_data["str_table"] 

4845 self.worksheet_meta = init_data["worksheet_meta"] 

4846 self.constant_memory = init_data["constant_memory"] 

4847 self.tmpdir = init_data["tmpdir"] 

4848 self.date_1904 = init_data["date_1904"] 

4849 self.strings_to_numbers = init_data["strings_to_numbers"] 

4850 self.strings_to_formulas = init_data["strings_to_formulas"] 

4851 self.strings_to_urls = init_data["strings_to_urls"] 

4852 self.nan_inf_to_errors = init_data["nan_inf_to_errors"] 

4853 self.default_date_format = init_data["default_date_format"] 

4854 self.default_url_format = init_data["default_url_format"] 

4855 self.workbook_add_format = init_data["workbook_add_format"] 

4856 self.excel2003_style = init_data["excel2003_style"] 

4857 self.remove_timezone = init_data["remove_timezone"] 

4858 self.max_url_length = init_data["max_url_length"] 

4859 self.use_future_functions = init_data["use_future_functions"] 

4860 self.embedded_images = init_data["embedded_images"] 

4861 

4862 if self.excel2003_style: 

4863 self.original_row_height = 12.75 

4864 self.default_row_height = 12.75 

4865 self.default_row_pixels = 17 

4866 self.margin_left = 0.75 

4867 self.margin_right = 0.75 

4868 self.margin_top = 1 

4869 self.margin_bottom = 1 

4870 self.margin_header = 0.5 

4871 self.margin_footer = 0.5 

4872 self.header_footer_aligns = False 

4873 

4874 # Open a temp filehandle to store row data in constant_memory mode. 

4875 if self.constant_memory: 

4876 # This is sub-optimal but we need to create a temp file 

4877 # with utf8 encoding in Python < 3. 

4878 (fd, filename) = tempfile.mkstemp(dir=self.tmpdir) 

4879 os.close(fd) 

4880 self.row_data_filename = filename 

4881 # pylint: disable=consider-using-with 

4882 self.row_data_fh = open(filename, mode="w+", encoding="utf-8") 

4883 

4884 # Set as the worksheet filehandle until the file is assembled. 

4885 self.fh = self.row_data_fh 

4886 

4887 def _assemble_xml_file(self) -> None: 

4888 # Assemble and write the XML file. 

4889 

4890 # Write the XML declaration. 

4891 self._xml_declaration() 

4892 

4893 # Write the root worksheet element. 

4894 self._write_worksheet() 

4895 

4896 # Write the worksheet properties. 

4897 self._write_sheet_pr() 

4898 

4899 # Write the worksheet dimensions. 

4900 self._write_dimension() 

4901 

4902 # Write the sheet view properties. 

4903 self._write_sheet_views() 

4904 

4905 # Write the sheet format properties. 

4906 self._write_sheet_format_pr() 

4907 

4908 # Write the sheet column info. 

4909 self._write_cols() 

4910 

4911 # Write the worksheet data such as rows columns and cells. 

4912 if not self.constant_memory: 

4913 self._write_sheet_data() 

4914 else: 

4915 self._write_optimized_sheet_data() 

4916 

4917 # Write the sheetProtection element. 

4918 self._write_sheet_protection() 

4919 

4920 # Write the protectedRanges element. 

4921 self._write_protected_ranges() 

4922 

4923 # Write the phoneticPr element. 

4924 if self.excel2003_style: 

4925 self._write_phonetic_pr() 

4926 

4927 # Write the autoFilter element. 

4928 self._write_auto_filter() 

4929 

4930 # Write the mergeCells element. 

4931 self._write_merge_cells() 

4932 

4933 # Write the conditional formats. 

4934 self._write_conditional_formats() 

4935 

4936 # Write the dataValidations element. 

4937 self._write_data_validations() 

4938 

4939 # Write the hyperlink element. 

4940 self._write_hyperlinks() 

4941 

4942 # Write the printOptions element. 

4943 self._write_print_options() 

4944 

4945 # Write the worksheet page_margins. 

4946 self._write_page_margins() 

4947 

4948 # Write the worksheet page setup. 

4949 self._write_page_setup() 

4950 

4951 # Write the headerFooter element. 

4952 self._write_header_footer() 

4953 

4954 # Write the rowBreaks element. 

4955 self._write_row_breaks() 

4956 

4957 # Write the colBreaks element. 

4958 self._write_col_breaks() 

4959 

4960 # Write the ignoredErrors element. 

4961 self._write_ignored_errors() 

4962 

4963 # Write the drawing element. 

4964 self._write_drawings() 

4965 

4966 # Write the legacyDrawing element. 

4967 self._write_legacy_drawing() 

4968 

4969 # Write the legacyDrawingHF element. 

4970 self._write_legacy_drawing_hf() 

4971 

4972 # Write the picture element, for the background. 

4973 self._write_picture() 

4974 

4975 # Write the tableParts element. 

4976 self._write_table_parts() 

4977 

4978 # Write the extLst elements. 

4979 self._write_ext_list() 

4980 

4981 # Close the worksheet tag. 

4982 self._xml_end_tag("worksheet") 

4983 

4984 # Close the file. 

4985 self._xml_close() 

4986 

4987 def _check_dimensions( 

4988 self, row: int, col: int, ignore_row=False, ignore_col=False 

4989 ) -> int: 

4990 # Check that row and col are valid and store the max and min 

4991 # values for use in other methods/elements. The ignore_row / 

4992 # ignore_col flags is used to indicate that we wish to perform 

4993 # the dimension check without storing the value. The ignore 

4994 # flags are use by set_row() and data_validate. 

4995 

4996 # Check that the row/col are within the worksheet bounds. 

4997 if row < 0 or col < 0: 

4998 return -1 

4999 if row >= self.xls_rowmax or col >= self.xls_colmax: 

5000 return -1 

5001 

5002 # In constant_memory mode we don't change dimensions for rows 

5003 # that are already written. 

5004 if not ignore_row and not ignore_col and self.constant_memory: 

5005 if row < self.previous_row: 

5006 return -2 

5007 

5008 if not ignore_row: 

5009 if self.dim_rowmin is None or row < self.dim_rowmin: 

5010 self.dim_rowmin = row 

5011 if self.dim_rowmax is None or row > self.dim_rowmax: 

5012 self.dim_rowmax = row 

5013 

5014 if not ignore_col: 

5015 if self.dim_colmin is None or col < self.dim_colmin: 

5016 self.dim_colmin = col 

5017 if self.dim_colmax is None or col > self.dim_colmax: 

5018 self.dim_colmax = col 

5019 

5020 return 0 

5021 

5022 def _convert_date_time(self, dt_obj): 

5023 # Convert a datetime object to an Excel serial date and time. 

5024 return _datetime_to_excel_datetime(dt_obj, self.date_1904, self.remove_timezone) 

5025 

5026 def _convert_name_area(self, row_num_1, col_num_1, row_num_2, col_num_2): 

5027 # Convert zero indexed rows and columns to the format required by 

5028 # worksheet named ranges, eg, "Sheet1!$A$1:$C$13". 

5029 

5030 range1 = "" 

5031 range2 = "" 

5032 area = "" 

5033 row_col_only = 0 

5034 

5035 # Convert to A1 notation. 

5036 col_char_1 = xl_col_to_name(col_num_1, 1) 

5037 col_char_2 = xl_col_to_name(col_num_2, 1) 

5038 row_char_1 = "$" + str(row_num_1 + 1) 

5039 row_char_2 = "$" + str(row_num_2 + 1) 

5040 

5041 # We need to handle special cases that refer to rows or columns only. 

5042 if row_num_1 == 0 and row_num_2 == self.xls_rowmax - 1: 

5043 range1 = col_char_1 

5044 range2 = col_char_2 

5045 row_col_only = 1 

5046 elif col_num_1 == 0 and col_num_2 == self.xls_colmax - 1: 

5047 range1 = row_char_1 

5048 range2 = row_char_2 

5049 row_col_only = 1 

5050 else: 

5051 range1 = col_char_1 + row_char_1 

5052 range2 = col_char_2 + row_char_2 

5053 

5054 # A repeated range is only written once (if it isn't a special case). 

5055 if range1 == range2 and not row_col_only: 

5056 area = range1 

5057 else: 

5058 area = range1 + ":" + range2 

5059 

5060 # Build up the print area range "Sheet1!$A$1:$C$13". 

5061 sheetname = quote_sheetname(self.name) 

5062 area = sheetname + "!" + area 

5063 

5064 return area 

5065 

5066 def _sort_pagebreaks(self, breaks): 

5067 # This is an internal method used to filter elements of a list of 

5068 # pagebreaks used in the _store_hbreak() and _store_vbreak() methods. 

5069 # It: 

5070 # 1. Removes duplicate entries from the list. 

5071 # 2. Sorts the list. 

5072 # 3. Removes 0 from the list if present. 

5073 if not breaks: 

5074 return [] 

5075 

5076 breaks_set = set(breaks) 

5077 

5078 if 0 in breaks_set: 

5079 breaks_set.remove(0) 

5080 

5081 breaks_list = list(breaks_set) 

5082 breaks_list.sort() 

5083 

5084 # The Excel 2007 specification says that the maximum number of page 

5085 # breaks is 1026. However, in practice it is actually 1023. 

5086 max_num_breaks = 1023 

5087 if len(breaks_list) > max_num_breaks: 

5088 breaks_list = breaks_list[:max_num_breaks] 

5089 

5090 return breaks_list 

5091 

5092 def _extract_filter_tokens(self, expression): 

5093 # Extract the tokens from the filter expression. The tokens are mainly 

5094 # non-whitespace groups. The only tricky part is to extract string 

5095 # tokens that contain whitespace and/or quoted double quotes (Excel's 

5096 # escaped quotes). 

5097 # 

5098 # Examples: 'x < 2000' 

5099 # 'x > 2000 and x < 5000' 

5100 # 'x = "foo"' 

5101 # 'x = "foo bar"' 

5102 # 'x = "foo "" bar"' 

5103 # 

5104 if not expression: 

5105 return [] 

5106 

5107 token_re = re.compile(r'"(?:[^"]|"")*"|\S+') 

5108 tokens = token_re.findall(expression) 

5109 

5110 new_tokens = [] 

5111 # Remove single leading and trailing quotes and un-escape other quotes. 

5112 for token in tokens: 

5113 if token.startswith('"'): 

5114 token = token[1:] 

5115 

5116 if token.endswith('"'): 

5117 token = token[:-1] 

5118 

5119 token = token.replace('""', '"') 

5120 

5121 new_tokens.append(token) 

5122 

5123 return new_tokens 

5124 

5125 def _parse_filter_expression(self, expression, tokens): 

5126 # Converts the tokens of a possibly conditional expression into 1 or 2 

5127 # sub expressions for further parsing. 

5128 # 

5129 # Examples: 

5130 # ('x', '==', 2000) -> exp1 

5131 # ('x', '>', 2000, 'and', 'x', '<', 5000) -> exp1 and exp2 

5132 

5133 if len(tokens) == 7: 

5134 # The number of tokens will be either 3 (for 1 expression) 

5135 # or 7 (for 2 expressions). 

5136 conditional = tokens[3] 

5137 

5138 if re.match("(and|&&)", conditional): 

5139 conditional = 0 

5140 elif re.match(r"(or|\|\|)", conditional): 

5141 conditional = 1 

5142 else: 

5143 warn( 

5144 f"Token '{conditional}' is not a valid conditional " 

5145 f"in filter expression '{expression}'" 

5146 ) 

5147 

5148 expression_1 = self._parse_filter_tokens(expression, tokens[0:3]) 

5149 expression_2 = self._parse_filter_tokens(expression, tokens[4:7]) 

5150 return expression_1 + [conditional] + expression_2 

5151 

5152 return self._parse_filter_tokens(expression, tokens) 

5153 

5154 def _parse_filter_tokens(self, expression, tokens): 

5155 # Parse the 3 tokens of a filter expression and return the operator 

5156 # and token. The use of numbers instead of operators is a legacy of 

5157 # Spreadsheet::WriteExcel. 

5158 operators = { 

5159 "==": 2, 

5160 "=": 2, 

5161 "=~": 2, 

5162 "eq": 2, 

5163 "!=": 5, 

5164 "!~": 5, 

5165 "ne": 5, 

5166 "<>": 5, 

5167 "<": 1, 

5168 "<=": 3, 

5169 ">": 4, 

5170 ">=": 6, 

5171 } 

5172 

5173 operator = operators.get(tokens[1], None) 

5174 token = tokens[2] 

5175 

5176 # Special handling of "Top" filter expressions. 

5177 if re.match("top|bottom", tokens[0].lower()): 

5178 value = int(tokens[1]) 

5179 

5180 if value < 1 or value > 500: 

5181 warn( 

5182 f"The value '{token}' in expression '{expression}' " 

5183 f"must be in the range 1 to 500" 

5184 ) 

5185 

5186 token = token.lower() 

5187 

5188 if token not in ("items", "%"): 

5189 warn( 

5190 f"The type '{token}' in expression '{expression}' " 

5191 f"must be either 'items' or '%%'" 

5192 ) 

5193 

5194 if tokens[0].lower() == "top": 

5195 operator = 30 

5196 else: 

5197 operator = 32 

5198 

5199 if tokens[2] == "%": 

5200 operator += 1 

5201 

5202 token = str(value) 

5203 

5204 if not operator and tokens[0]: 

5205 warn( 

5206 f"Token '{token[0]}' is not a valid operator " 

5207 f"in filter expression '{expression}'." 

5208 ) 

5209 

5210 # Special handling for Blanks/NonBlanks. 

5211 if re.match("blanks|nonblanks", token.lower()): 

5212 # Only allow Equals or NotEqual in this context. 

5213 if operator not in (2, 5): 

5214 warn( 

5215 f"The operator '{tokens[1]}' in expression '{expression}' " 

5216 f"is not valid in relation to Blanks/NonBlanks'." 

5217 ) 

5218 

5219 token = token.lower() 

5220 

5221 # The operator should always be 2 (=) to flag a "simple" equality 

5222 # in the binary record. Therefore we convert <> to =. 

5223 if token == "blanks": 

5224 if operator == 5: 

5225 token = " " 

5226 else: 

5227 if operator == 5: 

5228 operator = 2 

5229 token = "blanks" 

5230 else: 

5231 operator = 5 

5232 token = " " 

5233 

5234 # if the string token contains an Excel match character then change the 

5235 # operator type to indicate a non "simple" equality. 

5236 if operator == 2 and re.search("[*?]", token): 

5237 operator = 22 

5238 

5239 return [operator, token] 

5240 

5241 def _encode_password(self, password) -> str: 

5242 # Hash a worksheet password. Based on the algorithm in 

5243 # ECMA-376-4:2016, Office Open XML File Formats — Transitional 

5244 # Migration Features, Additional attributes for workbookProtection 

5245 # element (Part 1, §18.2.29). 

5246 digest = 0x0000 

5247 

5248 for char in password[::-1]: 

5249 digest = ((digest >> 14) & 0x01) | ((digest << 1) & 0x7FFF) 

5250 digest ^= ord(char) 

5251 

5252 digest = ((digest >> 14) & 0x01) | ((digest << 1) & 0x7FFF) 

5253 digest ^= len(password) 

5254 digest ^= 0xCE4B 

5255 

5256 return f"{digest:X}" 

5257 

5258 def _image_from_source(self, source, options: Optional[Dict[str, Any]] = None): 

5259 # Backward compatibility utility method to convert an input argument to 

5260 # an Image object. The source can be a filename, BytesIO stream or 

5261 # an existing Image object. 

5262 if isinstance(source, Image): 

5263 image = source 

5264 elif options is not None and options.get("image_data"): 

5265 image = Image(options["image_data"]) 

5266 image.image_name = source 

5267 else: 

5268 image = Image(source) 

5269 

5270 return image 

5271 

5272 def _prepare_image( 

5273 self, 

5274 image: Image, 

5275 image_id: int, 

5276 drawing_id: int, 

5277 ) -> None: 

5278 # Set up images/drawings. 

5279 

5280 # Get the effective image width and height in pixels. 

5281 width = image._width * image._x_scale 

5282 height = image._height * image._y_scale 

5283 

5284 # Scale by non 96dpi resolutions. 

5285 width *= 96.0 / image._x_dpi 

5286 height *= 96.0 / image._y_dpi 

5287 

5288 dimensions = self._position_object_emus( 

5289 image._col, 

5290 image._row, 

5291 image._x_offset, 

5292 image._y_offset, 

5293 width, 

5294 height, 

5295 image._anchor, 

5296 ) 

5297 

5298 # Convert from pixels to emus. 

5299 width = int(0.5 + (width * 9525)) 

5300 height = int(0.5 + (height * 9525)) 

5301 

5302 # Create a Drawing obj to use with worksheet unless one already exists. 

5303 if not self.drawing: 

5304 drawing = Drawing() 

5305 drawing.embedded = 1 

5306 self.drawing = drawing 

5307 

5308 self.external_drawing_links.append( 

5309 ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml", None] 

5310 ) 

5311 else: 

5312 drawing = self.drawing 

5313 

5314 drawing_object = DrawingInfo() 

5315 drawing_object._drawing_type = DrawingTypes.IMAGE 

5316 drawing_object._dimensions = dimensions 

5317 drawing_object._description = image.image_name 

5318 drawing_object._width = width 

5319 drawing_object._height = height 

5320 drawing_object._shape = None 

5321 drawing_object._anchor = image._anchor 

5322 drawing_object._rel_index = 0 

5323 drawing_object._decorative = image._decorative 

5324 

5325 if image.description is not None: 

5326 drawing_object._description = image.description 

5327 

5328 if image._url: 

5329 url = image._url 

5330 target = url._target() 

5331 target_mode = url._target_mode() 

5332 

5333 if not self.drawing_rels.get(url._link): 

5334 self.drawing_links.append(["/hyperlink", target, target_mode]) 

5335 

5336 url._rel_index = self._get_drawing_rel_index(url._link) 

5337 drawing_object._url = url 

5338 

5339 if not self.drawing_rels.get(image._digest): 

5340 self.drawing_links.append( 

5341 [ 

5342 "/image", 

5343 "../media/image" + str(image_id) + "." + image._image_extension, 

5344 ] 

5345 ) 

5346 

5347 drawing_object._rel_index = self._get_drawing_rel_index(image._digest) 

5348 drawing._add_drawing_object(drawing_object) 

5349 

5350 def _prepare_shape(self, index, drawing_id) -> None: 

5351 # Set up shapes/drawings. 

5352 ( 

5353 row, 

5354 col, 

5355 x_offset, 

5356 y_offset, 

5357 x_scale, 

5358 y_scale, 

5359 text, 

5360 anchor, 

5361 options, 

5362 description, 

5363 decorative, 

5364 ) = self.shapes[index] 

5365 

5366 width = options.get("width", self.default_col_pixels * 3) 

5367 height = options.get("height", self.default_row_pixels * 6) 

5368 

5369 width *= x_scale 

5370 height *= y_scale 

5371 

5372 dimensions = self._position_object_emus( 

5373 col, row, x_offset, y_offset, width, height, anchor 

5374 ) 

5375 

5376 # Convert from pixels to emus. 

5377 width = int(0.5 + (width * 9525)) 

5378 height = int(0.5 + (height * 9525)) 

5379 

5380 # Create a Drawing obj to use with worksheet unless one already exists. 

5381 if not self.drawing: 

5382 drawing = Drawing() 

5383 drawing.embedded = 1 

5384 self.drawing = drawing 

5385 

5386 self.external_drawing_links.append( 

5387 ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml", None] 

5388 ) 

5389 else: 

5390 drawing = self.drawing 

5391 

5392 shape = Shape("rect", "TextBox", options) 

5393 shape.text = text 

5394 

5395 drawing_object = DrawingInfo() 

5396 drawing_object._drawing_type = DrawingTypes.SHAPE 

5397 drawing_object._dimensions = dimensions 

5398 drawing_object._width = width 

5399 drawing_object._height = height 

5400 drawing_object._description = description 

5401 drawing_object._shape = shape 

5402 drawing_object._anchor = anchor 

5403 drawing_object._rel_index = 0 

5404 drawing_object._decorative = decorative 

5405 

5406 url = Url.from_options(options) 

5407 if url: 

5408 target = url._target() 

5409 target_mode = url._target_mode() 

5410 

5411 if not self.drawing_rels.get(url._link): 

5412 self.drawing_links.append(["/hyperlink", target, target_mode]) 

5413 

5414 url._rel_index = self._get_drawing_rel_index(url._link) 

5415 drawing_object._url = url 

5416 

5417 drawing._add_drawing_object(drawing_object) 

5418 

5419 def _prepare_header_image(self, image_id, image) -> None: 

5420 # Set up an image without a drawing object for header/footer images. 

5421 

5422 # Strip the extension from the filename. 

5423 image.image_name = re.sub(r"\..*$", "", image.image_name) 

5424 

5425 if not self.vml_drawing_rels.get(image._digest): 

5426 self.vml_drawing_links.append( 

5427 [ 

5428 "/image", 

5429 "../media/image" + str(image_id) + "." + image._image_extension, 

5430 ] 

5431 ) 

5432 

5433 image._ref_id = self._get_vml_drawing_rel_index(image._digest) 

5434 

5435 self.header_images_list.append(image) 

5436 

5437 def _prepare_background(self, image_id, image_extension) -> None: 

5438 # Set up an image without a drawing object for backgrounds. 

5439 self.external_background_links.append( 

5440 ["/image", "../media/image" + str(image_id) + "." + image_extension] 

5441 ) 

5442 

5443 def _prepare_chart(self, index, chart_id, drawing_id) -> None: 

5444 # Set up chart/drawings. 

5445 ( 

5446 row, 

5447 col, 

5448 chart, 

5449 x_offset, 

5450 y_offset, 

5451 x_scale, 

5452 y_scale, 

5453 anchor, 

5454 description, 

5455 decorative, 

5456 ) = self.charts[index] 

5457 

5458 chart.id = chart_id - 1 

5459 

5460 # Use user specified dimensions, if any. 

5461 width = int(0.5 + (chart.width * x_scale)) 

5462 height = int(0.5 + (chart.height * y_scale)) 

5463 

5464 dimensions = self._position_object_emus( 

5465 col, row, x_offset, y_offset, width, height, anchor 

5466 ) 

5467 

5468 # Set the chart name for the embedded object if it has been specified. 

5469 name = chart.chart_name 

5470 

5471 # Create a Drawing obj to use with worksheet unless one already exists. 

5472 if not self.drawing: 

5473 drawing = Drawing() 

5474 drawing.embedded = 1 

5475 self.drawing = drawing 

5476 

5477 self.external_drawing_links.append( 

5478 ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml"] 

5479 ) 

5480 else: 

5481 drawing = self.drawing 

5482 

5483 drawing_object = DrawingInfo() 

5484 drawing_object._drawing_type = DrawingTypes.CHART 

5485 drawing_object._dimensions = dimensions 

5486 drawing_object._width = width 

5487 drawing_object._height = height 

5488 drawing_object._name = name 

5489 drawing_object._shape = None 

5490 drawing_object._anchor = anchor 

5491 drawing_object._rel_index = self._get_drawing_rel_index() 

5492 drawing_object._description = description 

5493 drawing_object._decorative = decorative 

5494 

5495 drawing._add_drawing_object(drawing_object) 

5496 

5497 self.drawing_links.append( 

5498 ["/chart", "../charts/chart" + str(chart_id) + ".xml"] 

5499 ) 

5500 

5501 def _position_object_emus( 

5502 self, col_start, row_start, x1, y1, width, height, anchor 

5503 ): 

5504 # Calculate the vertices that define the position of a graphical 

5505 # object within the worksheet in EMUs. 

5506 # 

5507 # The vertices are expressed as English Metric Units (EMUs). There are 

5508 # 12,700 EMUs per point. Therefore, 12,700 * 3 /4 = 9,525 EMUs per 

5509 # pixel 

5510 ( 

5511 col_start, 

5512 row_start, 

5513 x1, 

5514 y1, 

5515 col_end, 

5516 row_end, 

5517 x2, 

5518 y2, 

5519 x_abs, 

5520 y_abs, 

5521 ) = self._position_object_pixels( 

5522 col_start, row_start, x1, y1, width, height, anchor 

5523 ) 

5524 

5525 # Convert the pixel values to EMUs. See above. 

5526 x1 = int(0.5 + 9525 * x1) 

5527 y1 = int(0.5 + 9525 * y1) 

5528 x2 = int(0.5 + 9525 * x2) 

5529 y2 = int(0.5 + 9525 * y2) 

5530 x_abs = int(0.5 + 9525 * x_abs) 

5531 y_abs = int(0.5 + 9525 * y_abs) 

5532 

5533 return (col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs) 

5534 

5535 # Calculate the vertices that define the position of a graphical object 

5536 # within the worksheet in pixels. 

5537 # 

5538 # +------------+------------+ 

5539 # | A | B | 

5540 # +-----+------------+------------+ 

5541 # | |(x1,y1) | | 

5542 # | 1 |(A1)._______|______ | 

5543 # | | | | | 

5544 # | | | | | 

5545 # +-----+----| OBJECT |-----+ 

5546 # | | | | | 

5547 # | 2 | |______________. | 

5548 # | | | (B2)| 

5549 # | | | (x2,y2)| 

5550 # +---- +------------+------------+ 

5551 # 

5552 # Example of an object that covers some of the area from cell A1 to B2. 

5553 # 

5554 # Based on the width and height of the object we need to calculate 8 vars: 

5555 # 

5556 # col_start, row_start, col_end, row_end, x1, y1, x2, y2. 

5557 # 

5558 # We also calculate the absolute x and y position of the top left vertex of 

5559 # the object. This is required for images. 

5560 # 

5561 # The width and height of the cells that the object occupies can be 

5562 # variable and have to be taken into account. 

5563 # 

5564 # The values of col_start and row_start are passed in from the calling 

5565 # function. The values of col_end and row_end are calculated by 

5566 # subtracting the width and height of the object from the width and 

5567 # height of the underlying cells. 

5568 # 

5569 def _position_object_pixels( 

5570 self, col_start, row_start, x1, y1, width, height, anchor 

5571 ): 

5572 # col_start # Col containing upper left corner of object. 

5573 # x1 # Distance to left side of object. 

5574 # 

5575 # row_start # Row containing top left corner of object. 

5576 # y1 # Distance to top of object. 

5577 # 

5578 # col_end # Col containing lower right corner of object. 

5579 # x2 # Distance to right side of object. 

5580 # 

5581 # row_end # Row containing bottom right corner of object. 

5582 # y2 # Distance to bottom of object. 

5583 # 

5584 # width # Width of object frame. 

5585 # height # Height of object frame. 

5586 # 

5587 # x_abs # Absolute distance to left side of object. 

5588 # y_abs # Absolute distance to top side of object. 

5589 x_abs = 0 

5590 y_abs = 0 

5591 

5592 # Adjust start column for negative offsets. 

5593 # pylint: disable=chained-comparison 

5594 while x1 < 0 and col_start > 0: 

5595 x1 += self._size_col(col_start - 1) 

5596 col_start -= 1 

5597 

5598 # Adjust start row for negative offsets. 

5599 while y1 < 0 and row_start > 0: 

5600 y1 += self._size_row(row_start - 1) 

5601 row_start -= 1 

5602 

5603 # Ensure that the image isn't shifted off the page at top left. 

5604 x1 = max(0, x1) 

5605 y1 = max(0, y1) 

5606 

5607 # Calculate the absolute x offset of the top-left vertex. 

5608 if self.col_size_changed: 

5609 for col_id in range(col_start): 

5610 x_abs += self._size_col(col_id) 

5611 else: 

5612 # Optimization for when the column widths haven't changed. 

5613 x_abs += self.default_col_pixels * col_start 

5614 

5615 x_abs += x1 

5616 

5617 # Calculate the absolute y offset of the top-left vertex. 

5618 if self.row_size_changed: 

5619 for row_id in range(row_start): 

5620 y_abs += self._size_row(row_id) 

5621 else: 

5622 # Optimization for when the row heights haven't changed. 

5623 y_abs += self.default_row_pixels * row_start 

5624 

5625 y_abs += y1 

5626 

5627 # Adjust start column for offsets that are greater than the col width. 

5628 while x1 >= self._size_col(col_start, anchor): 

5629 x1 -= self._size_col(col_start) 

5630 col_start += 1 

5631 

5632 # Adjust start row for offsets that are greater than the row height. 

5633 while y1 >= self._size_row(row_start, anchor): 

5634 y1 -= self._size_row(row_start) 

5635 row_start += 1 

5636 

5637 # Initialize end cell to the same as the start cell. 

5638 col_end = col_start 

5639 row_end = row_start 

5640 

5641 # Don't offset the image in the cell if the row/col is hidden. 

5642 if self._size_col(col_start, anchor) > 0: 

5643 width = width + x1 

5644 if self._size_row(row_start, anchor) > 0: 

5645 height = height + y1 

5646 

5647 # Subtract the underlying cell widths to find end cell of the object. 

5648 while width >= self._size_col(col_end, anchor): 

5649 width -= self._size_col(col_end, anchor) 

5650 col_end += 1 

5651 

5652 # Subtract the underlying cell heights to find end cell of the object. 

5653 while height >= self._size_row(row_end, anchor): 

5654 height -= self._size_row(row_end, anchor) 

5655 row_end += 1 

5656 

5657 # The end vertices are whatever is left from the width and height. 

5658 x2 = width 

5659 y2 = height 

5660 

5661 return [col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs] 

5662 

5663 def _size_col(self, col: int, anchor=0): 

5664 # Convert the width of a cell from character units to pixels. Excel 

5665 # rounds the column width to the nearest pixel. If the width hasn't 

5666 # been set by the user we use the default value. A hidden column is 

5667 # treated as having a width of zero unless it has the special 

5668 # "object_position" of 4 (size with cells). 

5669 max_digit_width = 7 # For Calibri 11. 

5670 padding = 5 

5671 pixels = 0 

5672 

5673 # Look up the cell value to see if it has been changed. 

5674 if col in self.col_info: 

5675 width = self.col_info[col][0] 

5676 hidden = self.col_info[col][2] 

5677 

5678 if width is None: 

5679 width = self.default_col_width 

5680 

5681 # Convert to pixels. 

5682 if hidden and anchor != 4: 

5683 pixels = 0 

5684 elif width < 1: 

5685 pixels = int(width * (max_digit_width + padding) + 0.5) 

5686 else: 

5687 pixels = int(width * max_digit_width + 0.5) + padding 

5688 else: 

5689 pixels = self.default_col_pixels 

5690 

5691 return pixels 

5692 

5693 def _size_row(self, row: int, anchor=0): 

5694 # Convert the height of a cell from character units to pixels. If the 

5695 # height hasn't been set by the user we use the default value. A 

5696 # hidden row is treated as having a height of zero unless it has the 

5697 # special "object_position" of 4 (size with cells). 

5698 pixels = 0 

5699 

5700 # Look up the cell value to see if it has been changed 

5701 if row in self.row_sizes: 

5702 height = self.row_sizes[row][0] 

5703 hidden = self.row_sizes[row][1] 

5704 

5705 if hidden and anchor != 4: 

5706 pixels = 0 

5707 else: 

5708 pixels = int(4.0 / 3.0 * height) 

5709 else: 

5710 pixels = int(4.0 / 3.0 * self.default_row_height) 

5711 

5712 return pixels 

5713 

5714 def _pixels_to_width(self, pixels): 

5715 # Convert the width of a cell from pixels to character units. 

5716 max_digit_width = 7.0 # For Calabri 11. 

5717 padding = 5.0 

5718 

5719 if pixels <= 12: 

5720 width = pixels / (max_digit_width + padding) 

5721 else: 

5722 width = (pixels - padding) / max_digit_width 

5723 

5724 return width 

5725 

5726 def _pixels_to_height(self, pixels): 

5727 # Convert the height of a cell from pixels to character units. 

5728 return 0.75 * pixels 

5729 

5730 def _comment_vertices(self, comment: CommentType): 

5731 # Calculate the positions of the comment object. 

5732 anchor = 0 

5733 vertices = self._position_object_pixels( 

5734 comment.start_col, 

5735 comment.start_row, 

5736 comment.x_offset, 

5737 comment.y_offset, 

5738 comment.width, 

5739 comment.height, 

5740 anchor, 

5741 ) 

5742 

5743 # Add the width and height for VML. 

5744 vertices.append(comment.width) 

5745 vertices.append(comment.height) 

5746 

5747 return vertices 

5748 

5749 def _button_vertices(self, button: ButtonType): 

5750 # Calculate the positions of the button object. 

5751 anchor = 0 

5752 vertices = self._position_object_pixels( 

5753 button.col, 

5754 button.row, 

5755 button.x_offset, 

5756 button.y_offset, 

5757 button.width, 

5758 button.height, 

5759 anchor, 

5760 ) 

5761 

5762 # Add the width and height for VML. 

5763 vertices.append(button.width) 

5764 vertices.append(button.height) 

5765 

5766 return vertices 

5767 

5768 def _prepare_vml_objects( 

5769 self, vml_data_id, vml_shape_id, vml_drawing_id, comment_id 

5770 ): 

5771 comments = [] 

5772 # Sort the comments into row/column order for easier comparison 

5773 # testing and set the external links for comments and buttons. 

5774 row_nums = sorted(self.comments.keys()) 

5775 

5776 for row in row_nums: 

5777 col_nums = sorted(self.comments[row].keys()) 

5778 

5779 for col in col_nums: 

5780 comment = self.comments[row][col] 

5781 comment.vertices = self._comment_vertices(comment) 

5782 

5783 # Set comment visibility if required and not user defined. 

5784 if comment.is_visible is None: 

5785 comment.is_visible = self.comments_visible 

5786 

5787 # Set comment author if not already user defined. 

5788 if comment.author is None: 

5789 comment.author = self.comments_author 

5790 

5791 comments.append(comment) 

5792 

5793 for button in self.buttons_list: 

5794 button.vertices = self._button_vertices(button) 

5795 

5796 self.external_vml_links.append( 

5797 ["/vmlDrawing", "../drawings/vmlDrawing" + str(vml_drawing_id) + ".vml"] 

5798 ) 

5799 

5800 if self.has_comments: 

5801 self.comments_list = comments 

5802 

5803 self.external_comment_links.append( 

5804 ["/comments", "../comments" + str(comment_id) + ".xml"] 

5805 ) 

5806 

5807 count = len(comments) 

5808 start_data_id = vml_data_id 

5809 

5810 # The VML o:idmap data id contains a comma separated range when there 

5811 # is more than one 1024 block of comments, like this: data="1,2". 

5812 for i in range(int(count / 1024)): 

5813 data_id = start_data_id + i + 1 

5814 vml_data_id = f"{vml_data_id},{data_id}" 

5815 

5816 self.vml_data_id = vml_data_id 

5817 self.vml_shape_id = vml_shape_id 

5818 

5819 return count 

5820 

5821 def _prepare_header_vml_objects(self, vml_header_id, vml_drawing_id) -> None: 

5822 # Set up external linkage for VML header/footer images. 

5823 

5824 self.vml_header_id = vml_header_id 

5825 

5826 self.external_vml_links.append( 

5827 ["/vmlDrawing", "../drawings/vmlDrawing" + str(vml_drawing_id) + ".vml"] 

5828 ) 

5829 

5830 def _prepare_tables(self, table_id, seen) -> None: 

5831 # Set the table ids for the worksheet tables. 

5832 for table in self.tables: 

5833 table["id"] = table_id 

5834 

5835 if table.get("name") is None: 

5836 # Set a default name. 

5837 table["name"] = "Table" + str(table_id) 

5838 

5839 # Check for duplicate table names. 

5840 name = table["name"].lower() 

5841 

5842 if name in seen: 

5843 raise DuplicateTableName( 

5844 f"Duplicate name '{table['name']}' used in worksheet.add_table()." 

5845 ) 

5846 

5847 seen[name] = True 

5848 

5849 # Store the link used for the rels file. 

5850 self.external_table_links.append( 

5851 ["/table", "../tables/table" + str(table_id) + ".xml"] 

5852 ) 

5853 table_id += 1 

5854 

5855 def _table_function_to_formula(self, function, col_name): 

5856 # Convert a table total function to a worksheet formula. 

5857 formula = "" 

5858 

5859 # Escape special characters, as required by Excel. 

5860 col_name = col_name.replace("'", "''") 

5861 col_name = col_name.replace("#", "'#") 

5862 col_name = col_name.replace("]", "']") 

5863 col_name = col_name.replace("[", "'[") 

5864 

5865 subtotals = { 

5866 "average": 101, 

5867 "countNums": 102, 

5868 "count": 103, 

5869 "max": 104, 

5870 "min": 105, 

5871 "stdDev": 107, 

5872 "sum": 109, 

5873 "var": 110, 

5874 } 

5875 

5876 if function in subtotals: 

5877 func_num = subtotals[function] 

5878 formula = f"SUBTOTAL({func_num},[{col_name}])" 

5879 else: 

5880 warn(f"Unsupported function '{function}' in add_table()") 

5881 

5882 return formula 

5883 

5884 def _set_spark_color(self, sparkline, options, user_color) -> None: 

5885 # Set the sparkline color. 

5886 if user_color not in options: 

5887 return 

5888 

5889 sparkline[user_color] = Color._from_value(options[user_color]) 

5890 

5891 def _get_range_data(self, row_start, col_start, row_end, col_end): 

5892 # Returns a range of data from the worksheet _table to be used in 

5893 # chart cached data. Strings are returned as SST ids and decoded 

5894 # in the workbook. Return None for data that doesn't exist since 

5895 # Excel can chart series with data missing. 

5896 

5897 if self.constant_memory: 

5898 return () 

5899 

5900 data = [] 

5901 

5902 # Iterate through the table data. 

5903 for row_num in range(row_start, row_end + 1): 

5904 # Store None if row doesn't exist. 

5905 if row_num not in self.table: 

5906 data.append(None) 

5907 continue 

5908 

5909 for col_num in range(col_start, col_end + 1): 

5910 if col_num in self.table[row_num]: 

5911 cell = self.table[row_num][col_num] 

5912 

5913 cell_type = cell.__class__.__name__ 

5914 

5915 if cell_type in ("Number", "Datetime"): 

5916 # Return a number with Excel's precision. 

5917 data.append(f"{cell.number:.16g}") 

5918 

5919 elif cell_type == "String": 

5920 # Return a string from it's shared string index. 

5921 index = cell.string 

5922 string = self.str_table._get_shared_string(index) 

5923 

5924 data.append(string) 

5925 

5926 elif cell_type in ("Formula", "ArrayFormula"): 

5927 # Return the formula value. 

5928 value = cell.value 

5929 

5930 if value is None: 

5931 value = 0 

5932 

5933 data.append(value) 

5934 

5935 elif cell_type == "Blank": 

5936 # Return a empty cell. 

5937 data.append("") 

5938 else: 

5939 # Store None if column doesn't exist. 

5940 data.append(None) 

5941 

5942 return data 

5943 

5944 def _csv_join(self, *items): 

5945 # Create a csv string for use with data validation formulas and lists. 

5946 

5947 # Convert non string types to string. 

5948 items = [str(item) if not isinstance(item, str) else item for item in items] 

5949 

5950 return ",".join(items) 

5951 

5952 def _escape_url(self, url): 

5953 # Don't escape URL if it looks already escaped. 

5954 if re.search("%[0-9a-fA-F]{2}", url): 

5955 return url 

5956 

5957 # Can't use url.quote() here because it doesn't match Excel. 

5958 url = url.replace("%", "%25") 

5959 url = url.replace('"', "%22") 

5960 url = url.replace(" ", "%20") 

5961 url = url.replace("<", "%3c") 

5962 url = url.replace(">", "%3e") 

5963 url = url.replace("[", "%5b") 

5964 url = url.replace("]", "%5d") 

5965 url = url.replace("^", "%5e") 

5966 url = url.replace("`", "%60") 

5967 url = url.replace("{", "%7b") 

5968 url = url.replace("}", "%7d") 

5969 

5970 return url 

5971 

5972 def _get_drawing_rel_index(self, target=None): 

5973 # Get the index used to address a drawing rel link. 

5974 if target is None: 

5975 self.drawing_rels_id += 1 

5976 return self.drawing_rels_id 

5977 

5978 if self.drawing_rels.get(target): 

5979 return self.drawing_rels[target] 

5980 

5981 self.drawing_rels_id += 1 

5982 self.drawing_rels[target] = self.drawing_rels_id 

5983 return self.drawing_rels_id 

5984 

5985 def _get_vml_drawing_rel_index(self, target=None): 

5986 # Get the index used to address a vml drawing rel link. 

5987 if self.vml_drawing_rels.get(target): 

5988 return self.vml_drawing_rels[target] 

5989 

5990 self.vml_drawing_rels_id += 1 

5991 self.vml_drawing_rels[target] = self.vml_drawing_rels_id 

5992 return self.vml_drawing_rels_id 

5993 

5994 ########################################################################### 

5995 # 

5996 # The following font methods are mainly duplicated from the Styles class 

5997 # with appropriate changes for rich string styles. 

5998 # 

5999 ########################################################################### 

6000 def _write_font(self, xf_format) -> None: 

6001 # Write the <font> element. 

6002 xml_writer = self.rstring 

6003 

6004 xml_writer._xml_start_tag("rPr") 

6005 

6006 # Handle the main font properties. 

6007 if xf_format.bold: 

6008 xml_writer._xml_empty_tag("b") 

6009 if xf_format.italic: 

6010 xml_writer._xml_empty_tag("i") 

6011 if xf_format.font_strikeout: 

6012 xml_writer._xml_empty_tag("strike") 

6013 if xf_format.font_outline: 

6014 xml_writer._xml_empty_tag("outline") 

6015 if xf_format.font_shadow: 

6016 xml_writer._xml_empty_tag("shadow") 

6017 

6018 # Handle the underline variants. 

6019 if xf_format.underline: 

6020 self._write_underline(xf_format.underline) 

6021 

6022 # Handle super/subscript. 

6023 if xf_format.font_script == 1: 

6024 self._write_vert_align("superscript") 

6025 if xf_format.font_script == 2: 

6026 self._write_vert_align("subscript") 

6027 

6028 # Write the font size 

6029 xml_writer._xml_empty_tag("sz", [("val", xf_format.font_size)]) 

6030 

6031 # Handle colors. 

6032 if xf_format.theme == -1: 

6033 # Ignore for excel2003_style. 

6034 pass 

6035 elif xf_format.theme: 

6036 self._write_rstring_color("color", [("theme", xf_format.theme)]) 

6037 elif xf_format.color_indexed: 

6038 self._write_rstring_color("color", [("indexed", xf_format.color_indexed)]) 

6039 elif xf_format.font_color: 

6040 color = xf_format.font_color 

6041 if not color._is_automatic: 

6042 self._write_rstring_color("color", color._attributes()) 

6043 else: 

6044 self._write_rstring_color("color", [("theme", 1)]) 

6045 

6046 # Write some other font properties related to font families. 

6047 xml_writer._xml_empty_tag("rFont", [("val", xf_format.font_name)]) 

6048 xml_writer._xml_empty_tag("family", [("val", xf_format.font_family)]) 

6049 

6050 if xf_format.font_name == "Calibri" and not xf_format.hyperlink: 

6051 xml_writer._xml_empty_tag("scheme", [("val", xf_format.font_scheme)]) 

6052 

6053 xml_writer._xml_end_tag("rPr") 

6054 

6055 def _write_underline(self, underline) -> None: 

6056 # Write the underline font element. 

6057 attributes = [] 

6058 

6059 # Handle the underline variants. 

6060 if underline == 2: 

6061 attributes = [("val", "double")] 

6062 elif underline == 33: 

6063 attributes = [("val", "singleAccounting")] 

6064 elif underline == 34: 

6065 attributes = [("val", "doubleAccounting")] 

6066 

6067 self.rstring._xml_empty_tag("u", attributes) 

6068 

6069 def _write_vert_align(self, val) -> None: 

6070 # Write the <vertAlign> font sub-element. 

6071 attributes = [("val", val)] 

6072 

6073 self.rstring._xml_empty_tag("vertAlign", attributes) 

6074 

6075 def _write_rstring_color(self, name, attributes) -> None: 

6076 # Write the <color> element. 

6077 self.rstring._xml_empty_tag(name, attributes) 

6078 

6079 def _opt_close(self) -> None: 

6080 # Close the row data filehandle in constant_memory mode. 

6081 if not self.row_data_fh_closed: 

6082 self.row_data_fh.close() 

6083 self.row_data_fh_closed = True 

6084 

6085 def _opt_reopen(self) -> None: 

6086 # Reopen the row data filehandle in constant_memory mode. 

6087 if self.row_data_fh_closed: 

6088 filename = self.row_data_filename 

6089 # pylint: disable=consider-using-with 

6090 self.row_data_fh = open(filename, mode="a+", encoding="utf-8") 

6091 self.row_data_fh_closed = False 

6092 self.fh = self.row_data_fh 

6093 

6094 def _set_icon_props(self, total_icons, user_props=None): 

6095 # Set the sub-properties for icons. 

6096 props = [] 

6097 

6098 # Set the defaults. 

6099 for _ in range(total_icons): 

6100 props.append({"criteria": False, "value": 0, "type": "percent"}) 

6101 

6102 # Set the default icon values based on the number of icons. 

6103 if total_icons == 3: 

6104 props[0]["value"] = 67 

6105 props[1]["value"] = 33 

6106 

6107 if total_icons == 4: 

6108 props[0]["value"] = 75 

6109 props[1]["value"] = 50 

6110 props[2]["value"] = 25 

6111 

6112 if total_icons == 5: 

6113 props[0]["value"] = 80 

6114 props[1]["value"] = 60 

6115 props[2]["value"] = 40 

6116 props[3]["value"] = 20 

6117 

6118 # Overwrite default properties with user defined properties. 

6119 if user_props: 

6120 # Ensure we don't set user properties for lowest icon. 

6121 max_data = len(user_props) 

6122 if max_data >= total_icons: 

6123 max_data = total_icons - 1 

6124 

6125 for i in range(max_data): 

6126 # Set the user defined 'value' property. 

6127 if user_props[i].get("value") is not None: 

6128 props[i]["value"] = user_props[i]["value"] 

6129 

6130 # Remove the formula '=' sign if it exists. 

6131 tmp = props[i]["value"] 

6132 if isinstance(tmp, str) and tmp.startswith("="): 

6133 props[i]["value"] = tmp.lstrip("=") 

6134 

6135 # Set the user defined 'type' property. 

6136 if user_props[i].get("type"): 

6137 valid_types = ("percent", "percentile", "number", "formula") 

6138 

6139 if user_props[i]["type"] not in valid_types: 

6140 warn( 

6141 f"Unknown icon property type '{user_props[i]['type']}' " 

6142 f"for sub-property 'type' in conditional_format()." 

6143 ) 

6144 else: 

6145 props[i]["type"] = user_props[i]["type"] 

6146 

6147 if props[i]["type"] == "number": 

6148 props[i]["type"] = "num" 

6149 

6150 # Set the user defined 'criteria' property. 

6151 criteria = user_props[i].get("criteria") 

6152 if criteria and criteria == ">": 

6153 props[i]["criteria"] = True 

6154 

6155 return props 

6156 

6157 ########################################################################### 

6158 # 

6159 # XML methods. 

6160 # 

6161 ########################################################################### 

6162 

6163 def _write_worksheet(self) -> None: 

6164 # Write the <worksheet> element. This is the root element. 

6165 

6166 schema = "http://schemas.openxmlformats.org/" 

6167 xmlns = schema + "spreadsheetml/2006/main" 

6168 xmlns_r = schema + "officeDocument/2006/relationships" 

6169 xmlns_mc = schema + "markup-compatibility/2006" 

6170 ms_schema = "http://schemas.microsoft.com/" 

6171 xmlns_x14ac = ms_schema + "office/spreadsheetml/2009/9/ac" 

6172 

6173 attributes = [("xmlns", xmlns), ("xmlns:r", xmlns_r)] 

6174 

6175 # Add some extra attributes for Excel 2010. Mainly for sparklines. 

6176 if self.excel_version == 2010: 

6177 attributes.append(("xmlns:mc", xmlns_mc)) 

6178 attributes.append(("xmlns:x14ac", xmlns_x14ac)) 

6179 attributes.append(("mc:Ignorable", "x14ac")) 

6180 

6181 self._xml_start_tag("worksheet", attributes) 

6182 

6183 def _write_dimension(self) -> None: 

6184 # Write the <dimension> element. This specifies the range of 

6185 # cells in the worksheet. As a special case, empty 

6186 # spreadsheets use 'A1' as a range. 

6187 

6188 if self.dim_rowmin is None and self.dim_colmin is None: 

6189 # If the min dimensions are not defined then no dimensions 

6190 # have been set and we use the default 'A1'. 

6191 ref = "A1" 

6192 

6193 elif self.dim_rowmin is None and self.dim_colmin is not None: 

6194 # If the row dimensions aren't set but the column 

6195 # dimensions are set then they have been changed via 

6196 # set_column(). 

6197 

6198 if self.dim_colmin == self.dim_colmax: 

6199 # The dimensions are a single cell and not a range. 

6200 ref = xl_rowcol_to_cell(0, self.dim_colmin) 

6201 else: 

6202 # The dimensions are a cell range. 

6203 cell_1 = xl_rowcol_to_cell(0, self.dim_colmin) 

6204 cell_2 = xl_rowcol_to_cell(0, self.dim_colmax) 

6205 ref = cell_1 + ":" + cell_2 

6206 

6207 elif self.dim_rowmin == self.dim_rowmax and self.dim_colmin == self.dim_colmax: 

6208 # The dimensions are a single cell and not a range. 

6209 ref = xl_rowcol_to_cell(self.dim_rowmin, self.dim_colmin) 

6210 else: 

6211 # The dimensions are a cell range. 

6212 cell_1 = xl_rowcol_to_cell(self.dim_rowmin, self.dim_colmin) 

6213 cell_2 = xl_rowcol_to_cell(self.dim_rowmax, self.dim_colmax) 

6214 ref = cell_1 + ":" + cell_2 

6215 

6216 self._xml_empty_tag("dimension", [("ref", ref)]) 

6217 

6218 def _write_sheet_views(self) -> None: 

6219 # Write the <sheetViews> element. 

6220 self._xml_start_tag("sheetViews") 

6221 

6222 # Write the sheetView element. 

6223 self._write_sheet_view() 

6224 

6225 self._xml_end_tag("sheetViews") 

6226 

6227 def _write_sheet_view(self) -> None: 

6228 # Write the <sheetViews> element. 

6229 attributes = [] 

6230 

6231 # Hide screen gridlines if required. 

6232 if not self.screen_gridlines: 

6233 attributes.append(("showGridLines", 0)) 

6234 

6235 # Hide screen row/column headers. 

6236 if self.row_col_headers: 

6237 attributes.append(("showRowColHeaders", 0)) 

6238 

6239 # Hide zeroes in cells. 

6240 if not self.show_zeros: 

6241 attributes.append(("showZeros", 0)) 

6242 

6243 # Display worksheet right to left for Hebrew, Arabic and others. 

6244 if self.is_right_to_left: 

6245 attributes.append(("rightToLeft", 1)) 

6246 

6247 # Show that the sheet tab is selected. 

6248 if self.selected: 

6249 attributes.append(("tabSelected", 1)) 

6250 

6251 # Turn outlines off. Also required in the outlinePr element. 

6252 if not self.outline_on: 

6253 attributes.append(("showOutlineSymbols", 0)) 

6254 

6255 # Set the page view/layout mode if required. 

6256 if self.page_view == 1: 

6257 attributes.append(("view", "pageLayout")) 

6258 elif self.page_view == 2: 

6259 attributes.append(("view", "pageBreakPreview")) 

6260 

6261 # Set the first visible cell. 

6262 if self.top_left_cell != "": 

6263 attributes.append(("topLeftCell", self.top_left_cell)) 

6264 

6265 # Set the zoom level. 

6266 if self.zoom != 100: 

6267 attributes.append(("zoomScale", self.zoom)) 

6268 

6269 if self.page_view == 0 and self.zoom_scale_normal: 

6270 attributes.append(("zoomScaleNormal", self.zoom)) 

6271 if self.page_view == 1: 

6272 attributes.append(("zoomScalePageLayoutView", self.zoom)) 

6273 if self.page_view == 2: 

6274 attributes.append(("zoomScaleSheetLayoutView", self.zoom)) 

6275 

6276 attributes.append(("workbookViewId", 0)) 

6277 

6278 if self.is_chartsheet and self.zoom_to_fit: 

6279 attributes.append(("zoomToFit", 1)) 

6280 

6281 if self.panes or self.selections: 

6282 self._xml_start_tag("sheetView", attributes) 

6283 self._write_panes() 

6284 self._write_selections() 

6285 self._xml_end_tag("sheetView") 

6286 else: 

6287 self._xml_empty_tag("sheetView", attributes) 

6288 

6289 def _write_sheet_format_pr(self) -> None: 

6290 # Write the <sheetFormatPr> element. 

6291 default_row_height = self.default_row_height 

6292 row_level = self.outline_row_level 

6293 col_level = self.outline_col_level 

6294 

6295 attributes = [("defaultRowHeight", default_row_height)] 

6296 

6297 if self.default_row_height != self.original_row_height: 

6298 attributes.append(("customHeight", 1)) 

6299 

6300 if self.default_row_zeroed: 

6301 attributes.append(("zeroHeight", 1)) 

6302 

6303 if row_level: 

6304 attributes.append(("outlineLevelRow", row_level)) 

6305 if col_level: 

6306 attributes.append(("outlineLevelCol", col_level)) 

6307 

6308 if self.excel_version == 2010: 

6309 attributes.append(("x14ac:dyDescent", "0.25")) 

6310 

6311 self._xml_empty_tag("sheetFormatPr", attributes) 

6312 

6313 def _write_cols(self) -> None: 

6314 # Write the <cols> element and <col> sub elements. 

6315 

6316 # Exit unless some column have been formatted. 

6317 if not self.col_info: 

6318 return 

6319 

6320 self._xml_start_tag("cols") 

6321 

6322 # Use the first element of the column information structures to set 

6323 # the initial/previous properties. 

6324 first_col = (sorted(self.col_info.keys()))[0] 

6325 last_col = first_col 

6326 prev_col_options = self.col_info[first_col] 

6327 del self.col_info[first_col] 

6328 deleted_col = first_col 

6329 deleted_col_options = prev_col_options 

6330 

6331 for col in sorted(self.col_info.keys()): 

6332 col_options = self.col_info[col] 

6333 # Check if the column number is contiguous with the previous 

6334 # column and if the properties are the same. 

6335 if col == last_col + 1 and col_options == prev_col_options: 

6336 last_col = col 

6337 else: 

6338 # If not contiguous/equal then we write out the current range 

6339 # of columns and start again. 

6340 self._write_col_info(first_col, last_col, prev_col_options) 

6341 first_col = col 

6342 last_col = first_col 

6343 prev_col_options = col_options 

6344 

6345 # We will exit the previous loop with one unhandled column range. 

6346 self._write_col_info(first_col, last_col, prev_col_options) 

6347 

6348 # Put back the deleted first column information structure. 

6349 self.col_info[deleted_col] = deleted_col_options 

6350 

6351 self._xml_end_tag("cols") 

6352 

6353 def _write_col_info(self, col_min, col_max, col_info) -> None: 

6354 # Write the <col> element. 

6355 (width, cell_format, hidden, level, collapsed, autofit) = col_info 

6356 

6357 custom_width = 1 

6358 xf_index = 0 

6359 

6360 # Get the cell_format index. 

6361 if cell_format: 

6362 xf_index = cell_format._get_xf_index() 

6363 

6364 # Set the Excel default column width. 

6365 if width is None: 

6366 if not hidden: 

6367 width = 8.43 

6368 custom_width = 0 

6369 else: 

6370 width = 0 

6371 elif width == 8.43: 

6372 # Width is defined but same as default. 

6373 custom_width = 0 

6374 

6375 # Convert column width from user units to character width. 

6376 if width > 0: 

6377 # For Calabri 11. 

6378 max_digit_width = 7 

6379 padding = 5 

6380 

6381 if width < 1: 

6382 width = ( 

6383 int( 

6384 (int(width * (max_digit_width + padding) + 0.5)) 

6385 / float(max_digit_width) 

6386 * 256.0 

6387 ) 

6388 / 256.0 

6389 ) 

6390 else: 

6391 width = ( 

6392 int( 

6393 (int(width * max_digit_width + 0.5) + padding) 

6394 / float(max_digit_width) 

6395 * 256.0 

6396 ) 

6397 / 256.0 

6398 ) 

6399 

6400 attributes = [ 

6401 ("min", col_min + 1), 

6402 ("max", col_max + 1), 

6403 ("width", f"{width:.16g}"), 

6404 ] 

6405 

6406 if xf_index: 

6407 attributes.append(("style", xf_index)) 

6408 if hidden: 

6409 attributes.append(("hidden", "1")) 

6410 if autofit: 

6411 attributes.append(("bestFit", "1")) 

6412 if custom_width: 

6413 attributes.append(("customWidth", "1")) 

6414 if level: 

6415 attributes.append(("outlineLevel", level)) 

6416 if collapsed: 

6417 attributes.append(("collapsed", "1")) 

6418 

6419 self._xml_empty_tag("col", attributes) 

6420 

6421 def _write_sheet_data(self) -> None: 

6422 # Write the <sheetData> element. 

6423 if self.dim_rowmin is None: 

6424 # If the dimensions aren't defined there is no data to write. 

6425 self._xml_empty_tag("sheetData") 

6426 else: 

6427 self._xml_start_tag("sheetData") 

6428 self._write_rows() 

6429 self._xml_end_tag("sheetData") 

6430 

6431 def _write_optimized_sheet_data(self) -> None: 

6432 # Write the <sheetData> element when constant_memory is on. In this 

6433 # case we read the data stored in the temp file and rewrite it to the 

6434 # XML sheet file. 

6435 if self.dim_rowmin is None: 

6436 # If the dimensions aren't defined then there is no data to write. 

6437 self._xml_empty_tag("sheetData") 

6438 else: 

6439 self._xml_start_tag("sheetData") 

6440 

6441 # Rewind the filehandle that was used for temp row data. 

6442 buff_size = 65536 

6443 self.row_data_fh.seek(0) 

6444 data = self.row_data_fh.read(buff_size) 

6445 

6446 while data: 

6447 self.fh.write(data) 

6448 data = self.row_data_fh.read(buff_size) 

6449 

6450 self.row_data_fh.close() 

6451 os.unlink(self.row_data_filename) 

6452 

6453 self._xml_end_tag("sheetData") 

6454 

6455 def _write_page_margins(self) -> None: 

6456 # Write the <pageMargins> element. 

6457 attributes = [ 

6458 ("left", self.margin_left), 

6459 ("right", self.margin_right), 

6460 ("top", self.margin_top), 

6461 ("bottom", self.margin_bottom), 

6462 ("header", self.margin_header), 

6463 ("footer", self.margin_footer), 

6464 ] 

6465 

6466 self._xml_empty_tag("pageMargins", attributes) 

6467 

6468 def _write_page_setup(self) -> None: 

6469 # Write the <pageSetup> element. 

6470 # 

6471 # The following is an example taken from Excel. 

6472 # 

6473 # <pageSetup 

6474 # paperSize="9" 

6475 # scale="110" 

6476 # fitToWidth="2" 

6477 # fitToHeight="2" 

6478 # pageOrder="overThenDown" 

6479 # orientation="portrait" 

6480 # blackAndWhite="1" 

6481 # draft="1" 

6482 # horizontalDpi="200" 

6483 # verticalDpi="200" 

6484 # r:id="rId1" 

6485 # /> 

6486 # 

6487 attributes = [] 

6488 

6489 # Skip this element if no page setup has changed. 

6490 if not self.page_setup_changed: 

6491 return 

6492 

6493 # Set paper size. 

6494 if self.paper_size: 

6495 attributes.append(("paperSize", self.paper_size)) 

6496 

6497 # Set the print_scale. 

6498 if self.print_scale != 100: 

6499 attributes.append(("scale", self.print_scale)) 

6500 

6501 # Set the "Fit to page" properties. 

6502 if self.fit_page and self.fit_width != 1: 

6503 attributes.append(("fitToWidth", self.fit_width)) 

6504 

6505 if self.fit_page and self.fit_height != 1: 

6506 attributes.append(("fitToHeight", self.fit_height)) 

6507 

6508 # Set the page print direction. 

6509 if self.page_order: 

6510 attributes.append(("pageOrder", "overThenDown")) 

6511 

6512 # Set start page for printing. 

6513 if self.page_start > 1: 

6514 attributes.append(("firstPageNumber", self.page_start)) 

6515 

6516 # Set page orientation. 

6517 if self.orientation: 

6518 attributes.append(("orientation", "portrait")) 

6519 else: 

6520 attributes.append(("orientation", "landscape")) 

6521 

6522 # Set the print in black and white option. 

6523 if self.black_white: 

6524 attributes.append(("blackAndWhite", "1")) 

6525 

6526 # Set start page for printing. 

6527 if self.page_start != 0: 

6528 attributes.append(("useFirstPageNumber", "1")) 

6529 

6530 # Set the DPI. Mainly only for testing. 

6531 if self.is_chartsheet: 

6532 if self.horizontal_dpi: 

6533 attributes.append(("horizontalDpi", self.horizontal_dpi)) 

6534 

6535 if self.vertical_dpi: 

6536 attributes.append(("verticalDpi", self.vertical_dpi)) 

6537 else: 

6538 if self.vertical_dpi: 

6539 attributes.append(("verticalDpi", self.vertical_dpi)) 

6540 

6541 if self.horizontal_dpi: 

6542 attributes.append(("horizontalDpi", self.horizontal_dpi)) 

6543 

6544 self._xml_empty_tag("pageSetup", attributes) 

6545 

6546 def _write_print_options(self) -> None: 

6547 # Write the <printOptions> element. 

6548 attributes = [] 

6549 

6550 if not self.print_options_changed: 

6551 return 

6552 

6553 # Set horizontal centering. 

6554 if self.hcenter: 

6555 attributes.append(("horizontalCentered", 1)) 

6556 

6557 # Set vertical centering. 

6558 if self.vcenter: 

6559 attributes.append(("verticalCentered", 1)) 

6560 

6561 # Enable row and column headers. 

6562 if self.print_headers: 

6563 attributes.append(("headings", 1)) 

6564 

6565 # Set printed gridlines. 

6566 if self.print_gridlines: 

6567 attributes.append(("gridLines", 1)) 

6568 

6569 self._xml_empty_tag("printOptions", attributes) 

6570 

6571 def _write_header_footer(self) -> None: 

6572 # Write the <headerFooter> element. 

6573 attributes = [] 

6574 

6575 if not self.header_footer_scales: 

6576 attributes.append(("scaleWithDoc", 0)) 

6577 

6578 if not self.header_footer_aligns: 

6579 attributes.append(("alignWithMargins", 0)) 

6580 

6581 if self.header_footer_changed: 

6582 self._xml_start_tag("headerFooter", attributes) 

6583 if self.header: 

6584 self._write_odd_header() 

6585 if self.footer: 

6586 self._write_odd_footer() 

6587 self._xml_end_tag("headerFooter") 

6588 elif self.excel2003_style: 

6589 self._xml_empty_tag("headerFooter", attributes) 

6590 

6591 def _write_odd_header(self) -> None: 

6592 # Write the <headerFooter> element. 

6593 self._xml_data_element("oddHeader", self.header) 

6594 

6595 def _write_odd_footer(self) -> None: 

6596 # Write the <headerFooter> element. 

6597 self._xml_data_element("oddFooter", self.footer) 

6598 

6599 def _write_rows(self) -> None: 

6600 # Write out the worksheet data as a series of rows and cells. 

6601 self._calculate_spans() 

6602 

6603 for row_num in range(self.dim_rowmin, self.dim_rowmax + 1): 

6604 if ( 

6605 row_num in self.set_rows 

6606 or row_num in self.comments 

6607 or self.table[row_num] 

6608 ): 

6609 # Only process rows with formatting, cell data and/or comments. 

6610 

6611 span_index = int(row_num / 16) 

6612 

6613 if span_index in self.row_spans: 

6614 span = self.row_spans[span_index] 

6615 else: 

6616 span = None 

6617 

6618 if self.table[row_num]: 

6619 # Write the cells if the row contains data. 

6620 if row_num not in self.set_rows: 

6621 self._write_row(row_num, span) 

6622 else: 

6623 self._write_row(row_num, span, self.set_rows[row_num]) 

6624 

6625 for col_num in range(self.dim_colmin, self.dim_colmax + 1): 

6626 if col_num in self.table[row_num]: 

6627 col_ref = self.table[row_num][col_num] 

6628 self._write_cell(row_num, col_num, col_ref) 

6629 

6630 self._xml_end_tag("row") 

6631 

6632 elif row_num in self.comments: 

6633 # Row with comments in cells. 

6634 self._write_empty_row(row_num, span, self.set_rows[row_num]) 

6635 else: 

6636 # Blank row with attributes only. 

6637 self._write_empty_row(row_num, span, self.set_rows[row_num]) 

6638 

6639 def _write_single_row(self, current_row_num=0) -> None: 

6640 # Write out the worksheet data as a single row with cells. 

6641 # This method is used when constant_memory is on. A single 

6642 # row is written and the data table is reset. That way only 

6643 # one row of data is kept in memory at any one time. We don't 

6644 # write span data in the optimized case since it is optional. 

6645 

6646 # Set the new previous row as the current row. 

6647 row_num = self.previous_row 

6648 self.previous_row = current_row_num 

6649 

6650 if row_num in self.set_rows or row_num in self.comments or self.table[row_num]: 

6651 # Only process rows with formatting, cell data and/or comments. 

6652 

6653 # No span data in optimized mode. 

6654 span = None 

6655 

6656 if self.table[row_num]: 

6657 # Write the cells if the row contains data. 

6658 if row_num not in self.set_rows: 

6659 self._write_row(row_num, span) 

6660 else: 

6661 self._write_row(row_num, span, self.set_rows[row_num]) 

6662 

6663 for col_num in range(self.dim_colmin, self.dim_colmax + 1): 

6664 if col_num in self.table[row_num]: 

6665 col_ref = self.table[row_num][col_num] 

6666 self._write_cell(row_num, col_num, col_ref) 

6667 

6668 self._xml_end_tag("row") 

6669 else: 

6670 # Row attributes or comments only. 

6671 self._write_empty_row(row_num, span, self.set_rows[row_num]) 

6672 

6673 # Reset table. 

6674 self.table.clear() 

6675 

6676 def _calculate_spans(self) -> None: 

6677 # Calculate the "spans" attribute of the <row> tag. This is an 

6678 # XLSX optimization and isn't strictly required. However, it 

6679 # makes comparing files easier. The span is the same for each 

6680 # block of 16 rows. 

6681 spans = {} 

6682 span_min = None 

6683 span_max = None 

6684 

6685 for row_num in range(self.dim_rowmin, self.dim_rowmax + 1): 

6686 if row_num in self.table: 

6687 # Calculate spans for cell data. 

6688 for col_num in range(self.dim_colmin, self.dim_colmax + 1): 

6689 if col_num in self.table[row_num]: 

6690 if span_min is None: 

6691 span_min = col_num 

6692 span_max = col_num 

6693 else: 

6694 span_min = min(span_min, col_num) 

6695 span_max = max(span_max, col_num) 

6696 

6697 if row_num in self.comments: 

6698 # Calculate spans for comments. 

6699 for col_num in range(self.dim_colmin, self.dim_colmax + 1): 

6700 if row_num in self.comments and col_num in self.comments[row_num]: 

6701 if span_min is None: 

6702 span_min = col_num 

6703 span_max = col_num 

6704 else: 

6705 span_min = min(span_min, col_num) 

6706 span_max = max(span_max, col_num) 

6707 

6708 if ((row_num + 1) % 16 == 0) or row_num == self.dim_rowmax: 

6709 span_index = int(row_num / 16) 

6710 

6711 if span_min is not None: 

6712 span_min += 1 

6713 span_max += 1 

6714 spans[span_index] = f"{span_min}:{span_max}" 

6715 span_min = None 

6716 

6717 self.row_spans = spans 

6718 

6719 def _write_row(self, row: int, spans, properties=None, empty_row=False) -> None: 

6720 # Write the <row> element. 

6721 xf_index = 0 

6722 

6723 if properties: 

6724 height, cell_format, hidden, level, collapsed = properties 

6725 else: 

6726 height, cell_format, hidden, level, collapsed = None, None, 0, 0, 0 

6727 

6728 if height is None: 

6729 height = self.default_row_height 

6730 

6731 attributes = [("r", row + 1)] 

6732 

6733 # Get the cell_format index. 

6734 if cell_format: 

6735 xf_index = cell_format._get_xf_index() 

6736 

6737 # Add row attributes where applicable. 

6738 if spans: 

6739 attributes.append(("spans", spans)) 

6740 

6741 if xf_index: 

6742 attributes.append(("s", xf_index)) 

6743 

6744 if cell_format: 

6745 attributes.append(("customFormat", 1)) 

6746 

6747 if height != self.original_row_height or ( 

6748 height == self.original_row_height and height != self.default_row_height 

6749 ): 

6750 attributes.append(("ht", f"{height:g}")) 

6751 

6752 if hidden: 

6753 attributes.append(("hidden", 1)) 

6754 

6755 if height != self.original_row_height or ( 

6756 height == self.original_row_height and height != self.default_row_height 

6757 ): 

6758 attributes.append(("customHeight", 1)) 

6759 

6760 if level: 

6761 attributes.append(("outlineLevel", level)) 

6762 

6763 if collapsed: 

6764 attributes.append(("collapsed", 1)) 

6765 

6766 if self.excel_version == 2010: 

6767 attributes.append(("x14ac:dyDescent", "0.25")) 

6768 

6769 if empty_row: 

6770 self._xml_empty_tag_unencoded("row", attributes) 

6771 else: 

6772 self._xml_start_tag_unencoded("row", attributes) 

6773 

6774 def _write_empty_row(self, row: int, spans, properties=None) -> None: 

6775 # Write and empty <row> element. 

6776 self._write_row(row, spans, properties, empty_row=True) 

6777 

6778 def _write_cell(self, row: int, col: int, cell) -> None: 

6779 # Write the <cell> element. 

6780 # Note. This is the innermost loop so efficiency is important. 

6781 

6782 cell_range = xl_rowcol_to_cell_fast(row, col) 

6783 attributes = [("r", cell_range)] 

6784 

6785 if cell.format: 

6786 # Add the cell format index. 

6787 xf_index = cell.format._get_xf_index() 

6788 attributes.append(("s", xf_index)) 

6789 elif row in self.set_rows and self.set_rows[row][1]: 

6790 # Add the row format. 

6791 row_xf = self.set_rows[row][1] 

6792 attributes.append(("s", row_xf._get_xf_index())) 

6793 elif col in self.col_info: 

6794 # Add the column format. 

6795 col_xf = self.col_info[col][1] 

6796 if col_xf is not None: 

6797 attributes.append(("s", col_xf._get_xf_index())) 

6798 

6799 type_cell_name = cell.__class__.__name__ 

6800 

6801 # Write the various cell types. 

6802 if type_cell_name in ("Number", "Datetime"): 

6803 # Write a number. 

6804 self._xml_number_element(cell.number, attributes) 

6805 

6806 elif type_cell_name in ("String", "RichString"): 

6807 # Write a string. 

6808 string = cell.string 

6809 

6810 if not self.constant_memory: 

6811 # Write a shared string. 

6812 self._xml_string_element(string, attributes) 

6813 else: 

6814 # Write an optimized in-line string. 

6815 

6816 # Convert control character to a _xHHHH_ escape. 

6817 string = self._escape_control_characters(string) 

6818 

6819 # Write any rich strings without further tags. 

6820 if string.startswith("<r>") and string.endswith("</r>"): 

6821 self._xml_rich_inline_string(string, attributes) 

6822 else: 

6823 # Add attribute to preserve leading or trailing whitespace. 

6824 preserve = _preserve_whitespace(string) 

6825 self._xml_inline_string(string, preserve, attributes) 

6826 

6827 elif type_cell_name == "Formula": 

6828 # Write a formula. First check the formula value type. 

6829 value = cell.value 

6830 if isinstance(cell.value, bool): 

6831 attributes.append(("t", "b")) 

6832 if cell.value: 

6833 value = 1 

6834 else: 

6835 value = 0 

6836 

6837 elif isinstance(cell.value, str): 

6838 error_codes = ( 

6839 "#DIV/0!", 

6840 "#N/A", 

6841 "#NAME?", 

6842 "#NULL!", 

6843 "#NUM!", 

6844 "#REF!", 

6845 "#VALUE!", 

6846 ) 

6847 

6848 if cell.value == "": 

6849 # Allow blank to force recalc in some third party apps. 

6850 pass 

6851 elif cell.value in error_codes: 

6852 attributes.append(("t", "e")) 

6853 else: 

6854 attributes.append(("t", "str")) 

6855 

6856 self._xml_formula_element(cell.formula, value, attributes) 

6857 

6858 elif type_cell_name == "ArrayFormula": 

6859 # Write a array formula. 

6860 

6861 if cell.atype == "dynamic": 

6862 attributes.append(("cm", 1)) 

6863 

6864 # First check if the formula value is a string. 

6865 try: 

6866 float(cell.value) 

6867 except ValueError: 

6868 attributes.append(("t", "str")) 

6869 

6870 # Write an array formula. 

6871 self._xml_start_tag("c", attributes) 

6872 

6873 self._write_cell_array_formula(cell.formula, cell.range) 

6874 self._write_cell_value(cell.value) 

6875 self._xml_end_tag("c") 

6876 

6877 elif type_cell_name == "Blank": 

6878 # Write a empty cell. 

6879 self._xml_empty_tag("c", attributes) 

6880 

6881 elif type_cell_name == "Boolean": 

6882 # Write a boolean cell. 

6883 attributes.append(("t", "b")) 

6884 self._xml_start_tag("c", attributes) 

6885 self._write_cell_value(cell.boolean) 

6886 self._xml_end_tag("c") 

6887 

6888 elif type_cell_name == "Error": 

6889 # Write a boolean cell. 

6890 attributes.append(("t", "e")) 

6891 attributes.append(("vm", cell.value)) 

6892 self._xml_start_tag("c", attributes) 

6893 self._write_cell_value(cell.error) 

6894 self._xml_end_tag("c") 

6895 

6896 def _write_cell_value(self, value) -> None: 

6897 # Write the cell value <v> element. 

6898 if value is None: 

6899 value = "" 

6900 

6901 self._xml_data_element("v", value) 

6902 

6903 def _write_cell_array_formula(self, formula, cell_range) -> None: 

6904 # Write the cell array formula <f> element. 

6905 attributes = [("t", "array"), ("ref", cell_range)] 

6906 

6907 self._xml_data_element("f", formula, attributes) 

6908 

6909 def _write_sheet_pr(self) -> None: 

6910 # Write the <sheetPr> element for Sheet level properties. 

6911 attributes = [] 

6912 

6913 if ( 

6914 not self.fit_page 

6915 and not self.filter_on 

6916 and not self.tab_color 

6917 and not self.outline_changed 

6918 and not self.vba_codename 

6919 ): 

6920 return 

6921 

6922 if self.vba_codename: 

6923 attributes.append(("codeName", self.vba_codename)) 

6924 

6925 if self.filter_on: 

6926 attributes.append(("filterMode", 1)) 

6927 

6928 if self.fit_page or self.tab_color or self.outline_changed: 

6929 self._xml_start_tag("sheetPr", attributes) 

6930 self._write_tab_color() 

6931 self._write_outline_pr() 

6932 self._write_page_set_up_pr() 

6933 self._xml_end_tag("sheetPr") 

6934 else: 

6935 self._xml_empty_tag("sheetPr", attributes) 

6936 

6937 def _write_page_set_up_pr(self) -> None: 

6938 # Write the <pageSetUpPr> element. 

6939 if not self.fit_page: 

6940 return 

6941 

6942 attributes = [("fitToPage", 1)] 

6943 self._xml_empty_tag("pageSetUpPr", attributes) 

6944 

6945 def _write_tab_color(self) -> None: 

6946 # Write the <tabColor> element. 

6947 color = self.tab_color 

6948 

6949 if not color: 

6950 return 

6951 

6952 self._write_color("tabColor", color._attributes()) 

6953 

6954 def _write_outline_pr(self) -> None: 

6955 # Write the <outlinePr> element. 

6956 attributes = [] 

6957 

6958 if not self.outline_changed: 

6959 return 

6960 

6961 if self.outline_style: 

6962 attributes.append(("applyStyles", 1)) 

6963 if not self.outline_below: 

6964 attributes.append(("summaryBelow", 0)) 

6965 if not self.outline_right: 

6966 attributes.append(("summaryRight", 0)) 

6967 if not self.outline_on: 

6968 attributes.append(("showOutlineSymbols", 0)) 

6969 

6970 self._xml_empty_tag("outlinePr", attributes) 

6971 

6972 def _write_row_breaks(self) -> None: 

6973 # Write the <rowBreaks> element. 

6974 page_breaks = self._sort_pagebreaks(self.hbreaks) 

6975 

6976 if not page_breaks: 

6977 return 

6978 

6979 count = len(page_breaks) 

6980 

6981 attributes = [ 

6982 ("count", count), 

6983 ("manualBreakCount", count), 

6984 ] 

6985 

6986 self._xml_start_tag("rowBreaks", attributes) 

6987 

6988 for row_num in page_breaks: 

6989 self._write_brk(row_num, 16383) 

6990 

6991 self._xml_end_tag("rowBreaks") 

6992 

6993 def _write_col_breaks(self) -> None: 

6994 # Write the <colBreaks> element. 

6995 page_breaks = self._sort_pagebreaks(self.vbreaks) 

6996 

6997 if not page_breaks: 

6998 return 

6999 

7000 count = len(page_breaks) 

7001 

7002 attributes = [ 

7003 ("count", count), 

7004 ("manualBreakCount", count), 

7005 ] 

7006 

7007 self._xml_start_tag("colBreaks", attributes) 

7008 

7009 for col_num in page_breaks: 

7010 self._write_brk(col_num, 1048575) 

7011 

7012 self._xml_end_tag("colBreaks") 

7013 

7014 def _write_brk(self, brk_id, brk_max) -> None: 

7015 # Write the <brk> element. 

7016 attributes = [("id", brk_id), ("max", brk_max), ("man", 1)] 

7017 

7018 self._xml_empty_tag("brk", attributes) 

7019 

7020 def _write_merge_cells(self) -> None: 

7021 # Write the <mergeCells> element. 

7022 merged_cells = self.merge 

7023 count = len(merged_cells) 

7024 

7025 if not count: 

7026 return 

7027 

7028 attributes = [("count", count)] 

7029 

7030 self._xml_start_tag("mergeCells", attributes) 

7031 

7032 for merged_range in merged_cells: 

7033 # Write the mergeCell element. 

7034 self._write_merge_cell(merged_range) 

7035 

7036 self._xml_end_tag("mergeCells") 

7037 

7038 def _write_merge_cell(self, merged_range) -> None: 

7039 # Write the <mergeCell> element. 

7040 (row_min, col_min, row_max, col_max) = merged_range 

7041 

7042 # Convert the merge dimensions to a cell range. 

7043 cell_1 = xl_rowcol_to_cell(row_min, col_min) 

7044 cell_2 = xl_rowcol_to_cell(row_max, col_max) 

7045 ref = cell_1 + ":" + cell_2 

7046 

7047 attributes = [("ref", ref)] 

7048 

7049 self._xml_empty_tag("mergeCell", attributes) 

7050 

7051 def _write_hyperlinks(self) -> None: 

7052 # Process any stored hyperlinks in row/col order and write the 

7053 # <hyperlinks> element. The attributes are different for internal 

7054 # and external links. 

7055 

7056 # Sort the hyperlinks into row order. 

7057 row_nums = sorted(self.hyperlinks.keys()) 

7058 

7059 # Exit if there are no hyperlinks to process. 

7060 if not row_nums: 

7061 return 

7062 

7063 # Write the hyperlink elements. 

7064 self._xml_start_tag("hyperlinks") 

7065 

7066 # Iterate over the rows. 

7067 for row_num in row_nums: 

7068 # Sort the hyperlinks into column order. 

7069 col_nums = sorted(self.hyperlinks[row_num].keys()) 

7070 

7071 # Iterate over the columns. 

7072 for col_num in col_nums: 

7073 # Get the link data for this cell. 

7074 url = self.hyperlinks[row_num][col_num] 

7075 

7076 # If the cell was overwritten by the user and isn't a string 

7077 # then we have to add the url as the string to display. 

7078 if self.table and self.table[row_num] and self.table[row_num][col_num]: 

7079 cell = self.table[row_num][col_num] 

7080 if cell.__class__.__name__ != "String": 

7081 url._is_object_link = True 

7082 

7083 if url._link_type in (UrlTypes.URL, UrlTypes.EXTERNAL): 

7084 # External link with rel file relationship. 

7085 self.rel_count += 1 

7086 

7087 self._write_hyperlink_external( 

7088 row_num, col_num, self.rel_count, url 

7089 ) 

7090 

7091 # Links for use by the packager. 

7092 self.external_hyper_links.append( 

7093 ["/hyperlink", url._target(), "External"] 

7094 ) 

7095 else: 

7096 # Internal link with rel file relationship. 

7097 self._write_hyperlink_internal(row_num, col_num, url) 

7098 

7099 self._xml_end_tag("hyperlinks") 

7100 

7101 def _write_hyperlink_external( 

7102 self, row: int, col: int, id_num: int, url: Url 

7103 ) -> None: 

7104 # Write the <hyperlink> element for external links. 

7105 ref = xl_rowcol_to_cell(row, col) 

7106 r_id = "rId" + str(id_num) 

7107 

7108 attributes = [("ref", ref), ("r:id", r_id)] 

7109 

7110 if url._anchor: 

7111 attributes.append(("location", url._anchor)) 

7112 

7113 if url._is_object_link: 

7114 attributes.append(("display", url._text)) 

7115 

7116 if url._tip: 

7117 attributes.append(("tooltip", url._tip)) 

7118 

7119 self._xml_empty_tag("hyperlink", attributes) 

7120 

7121 def _write_hyperlink_internal(self, row: int, col: int, url: Url) -> None: 

7122 # Write the <hyperlink> element for internal links. 

7123 ref = xl_rowcol_to_cell(row, col) 

7124 

7125 attributes = [("ref", ref), ("location", url._link)] 

7126 

7127 if url._tip: 

7128 attributes.append(("tooltip", url._tip)) 

7129 

7130 attributes.append(("display", url._text)) 

7131 

7132 self._xml_empty_tag("hyperlink", attributes) 

7133 

7134 def _write_auto_filter(self) -> None: 

7135 # Write the <autoFilter> element. 

7136 if not self.autofilter_ref: 

7137 return 

7138 

7139 attributes = [("ref", self.autofilter_ref)] 

7140 

7141 if self.filter_on: 

7142 # Autofilter defined active filters. 

7143 self._xml_start_tag("autoFilter", attributes) 

7144 self._write_autofilters() 

7145 self._xml_end_tag("autoFilter") 

7146 

7147 else: 

7148 # Autofilter defined without active filters. 

7149 self._xml_empty_tag("autoFilter", attributes) 

7150 

7151 def _write_autofilters(self) -> None: 

7152 # Function to iterate through the columns that form part of an 

7153 # autofilter range and write the appropriate filters. 

7154 (col1, col2) = self.filter_range 

7155 

7156 for col in range(col1, col2 + 1): 

7157 # Skip if column doesn't have an active filter. 

7158 if col not in self.filter_cols: 

7159 continue 

7160 

7161 # Retrieve the filter tokens and write the autofilter records. 

7162 tokens = self.filter_cols[col] 

7163 filter_type = self.filter_type[col] 

7164 

7165 # Filters are relative to first column in the autofilter. 

7166 self._write_filter_column(col - col1, filter_type, tokens) 

7167 

7168 def _write_filter_column(self, col_id, filter_type, filters) -> None: 

7169 # Write the <filterColumn> element. 

7170 attributes = [("colId", col_id)] 

7171 

7172 self._xml_start_tag("filterColumn", attributes) 

7173 

7174 if filter_type == 1: 

7175 # Type == 1 is the new XLSX style filter. 

7176 self._write_filters(filters) 

7177 else: 

7178 # Type == 0 is the classic "custom" filter. 

7179 self._write_custom_filters(filters) 

7180 

7181 self._xml_end_tag("filterColumn") 

7182 

7183 def _write_filters(self, filters) -> None: 

7184 # Write the <filters> element. 

7185 non_blanks = [filter for filter in filters if str(filter).lower() != "blanks"] 

7186 attributes = [] 

7187 

7188 if len(filters) != len(non_blanks): 

7189 attributes = [("blank", 1)] 

7190 

7191 if len(filters) == 1 and len(non_blanks) == 0: 

7192 # Special case for blank cells only. 

7193 self._xml_empty_tag("filters", attributes) 

7194 else: 

7195 # General case. 

7196 self._xml_start_tag("filters", attributes) 

7197 

7198 for autofilter in sorted(non_blanks): 

7199 self._write_filter(autofilter) 

7200 

7201 self._xml_end_tag("filters") 

7202 

7203 def _write_filter(self, val) -> None: 

7204 # Write the <filter> element. 

7205 attributes = [("val", val)] 

7206 

7207 self._xml_empty_tag("filter", attributes) 

7208 

7209 def _write_custom_filters(self, tokens) -> None: 

7210 # Write the <customFilters> element. 

7211 if len(tokens) == 2: 

7212 # One filter expression only. 

7213 self._xml_start_tag("customFilters") 

7214 self._write_custom_filter(*tokens) 

7215 self._xml_end_tag("customFilters") 

7216 else: 

7217 # Two filter expressions. 

7218 attributes = [] 

7219 

7220 # Check if the "join" operand is "and" or "or". 

7221 if tokens[2] == 0: 

7222 attributes = [("and", 1)] 

7223 else: 

7224 attributes = [("and", 0)] 

7225 

7226 # Write the two custom filters. 

7227 self._xml_start_tag("customFilters", attributes) 

7228 self._write_custom_filter(tokens[0], tokens[1]) 

7229 self._write_custom_filter(tokens[3], tokens[4]) 

7230 self._xml_end_tag("customFilters") 

7231 

7232 def _write_custom_filter(self, operator, val) -> None: 

7233 # Write the <customFilter> element. 

7234 attributes = [] 

7235 

7236 operators = { 

7237 1: "lessThan", 

7238 2: "equal", 

7239 3: "lessThanOrEqual", 

7240 4: "greaterThan", 

7241 5: "notEqual", 

7242 6: "greaterThanOrEqual", 

7243 22: "equal", 

7244 } 

7245 

7246 # Convert the operator from a number to a descriptive string. 

7247 if operators[operator] is not None: 

7248 operator = operators[operator] 

7249 else: 

7250 warn(f"Unknown operator = {operator}") 

7251 

7252 # The 'equal' operator is the default attribute and isn't stored. 

7253 if operator != "equal": 

7254 attributes.append(("operator", operator)) 

7255 attributes.append(("val", val)) 

7256 

7257 self._xml_empty_tag("customFilter", attributes) 

7258 

7259 def _write_sheet_protection(self) -> None: 

7260 # Write the <sheetProtection> element. 

7261 attributes = [] 

7262 

7263 if not self.protect_options: 

7264 return 

7265 

7266 options = self.protect_options 

7267 

7268 if options["password"]: 

7269 attributes.append(("password", options["password"])) 

7270 if options["sheet"]: 

7271 attributes.append(("sheet", 1)) 

7272 if options["content"]: 

7273 attributes.append(("content", 1)) 

7274 if not options["objects"]: 

7275 attributes.append(("objects", 1)) 

7276 if not options["scenarios"]: 

7277 attributes.append(("scenarios", 1)) 

7278 if options["format_cells"]: 

7279 attributes.append(("formatCells", 0)) 

7280 if options["format_columns"]: 

7281 attributes.append(("formatColumns", 0)) 

7282 if options["format_rows"]: 

7283 attributes.append(("formatRows", 0)) 

7284 if options["insert_columns"]: 

7285 attributes.append(("insertColumns", 0)) 

7286 if options["insert_rows"]: 

7287 attributes.append(("insertRows", 0)) 

7288 if options["insert_hyperlinks"]: 

7289 attributes.append(("insertHyperlinks", 0)) 

7290 if options["delete_columns"]: 

7291 attributes.append(("deleteColumns", 0)) 

7292 if options["delete_rows"]: 

7293 attributes.append(("deleteRows", 0)) 

7294 if not options["select_locked_cells"]: 

7295 attributes.append(("selectLockedCells", 1)) 

7296 if options["sort"]: 

7297 attributes.append(("sort", 0)) 

7298 if options["autofilter"]: 

7299 attributes.append(("autoFilter", 0)) 

7300 if options["pivot_tables"]: 

7301 attributes.append(("pivotTables", 0)) 

7302 if not options["select_unlocked_cells"]: 

7303 attributes.append(("selectUnlockedCells", 1)) 

7304 

7305 self._xml_empty_tag("sheetProtection", attributes) 

7306 

7307 def _write_protected_ranges(self) -> None: 

7308 # Write the <protectedRanges> element. 

7309 if self.num_protected_ranges == 0: 

7310 return 

7311 

7312 self._xml_start_tag("protectedRanges") 

7313 

7314 for cell_range, range_name, password in self.protected_ranges: 

7315 self._write_protected_range(cell_range, range_name, password) 

7316 

7317 self._xml_end_tag("protectedRanges") 

7318 

7319 def _write_protected_range(self, cell_range, range_name, password) -> None: 

7320 # Write the <protectedRange> element. 

7321 attributes = [] 

7322 

7323 if password: 

7324 attributes.append(("password", password)) 

7325 

7326 attributes.append(("sqref", cell_range)) 

7327 attributes.append(("name", range_name)) 

7328 

7329 self._xml_empty_tag("protectedRange", attributes) 

7330 

7331 def _write_drawings(self) -> None: 

7332 # Write the <drawing> elements. 

7333 if not self.drawing: 

7334 return 

7335 

7336 self.rel_count += 1 

7337 self._write_drawing(self.rel_count) 

7338 

7339 def _write_drawing(self, drawing_id) -> None: 

7340 # Write the <drawing> element. 

7341 r_id = "rId" + str(drawing_id) 

7342 

7343 attributes = [("r:id", r_id)] 

7344 

7345 self._xml_empty_tag("drawing", attributes) 

7346 

7347 def _write_legacy_drawing(self) -> None: 

7348 # Write the <legacyDrawing> element. 

7349 if not self.has_vml: 

7350 return 

7351 

7352 # Increment the relationship id for any drawings or comments. 

7353 self.rel_count += 1 

7354 r_id = "rId" + str(self.rel_count) 

7355 

7356 attributes = [("r:id", r_id)] 

7357 

7358 self._xml_empty_tag("legacyDrawing", attributes) 

7359 

7360 def _write_legacy_drawing_hf(self) -> None: 

7361 # Write the <legacyDrawingHF> element. 

7362 if not self.has_header_vml: 

7363 return 

7364 

7365 # Increment the relationship id for any drawings or comments. 

7366 self.rel_count += 1 

7367 r_id = "rId" + str(self.rel_count) 

7368 

7369 attributes = [("r:id", r_id)] 

7370 

7371 self._xml_empty_tag("legacyDrawingHF", attributes) 

7372 

7373 def _write_picture(self) -> None: 

7374 # Write the <picture> element. 

7375 if not self.background_image: 

7376 return 

7377 

7378 # Increment the relationship id. 

7379 self.rel_count += 1 

7380 r_id = "rId" + str(self.rel_count) 

7381 

7382 attributes = [("r:id", r_id)] 

7383 

7384 self._xml_empty_tag("picture", attributes) 

7385 

7386 def _write_data_validations(self) -> None: 

7387 # Write the <dataValidations> element. 

7388 validations = self.validations 

7389 count = len(validations) 

7390 

7391 if not count: 

7392 return 

7393 

7394 attributes = [("count", count)] 

7395 

7396 self._xml_start_tag("dataValidations", attributes) 

7397 

7398 for validation in validations: 

7399 # Write the dataValidation element. 

7400 self._write_data_validation(validation) 

7401 

7402 self._xml_end_tag("dataValidations") 

7403 

7404 def _write_data_validation(self, options) -> None: 

7405 # Write the <dataValidation> element. 

7406 sqref = "" 

7407 attributes = [] 

7408 

7409 # Set the cell range(s) for the data validation. 

7410 for cells in options["cells"]: 

7411 # Add a space between multiple cell ranges. 

7412 if sqref != "": 

7413 sqref += " " 

7414 

7415 (row_first, col_first, row_last, col_last) = cells 

7416 

7417 # Swap last row/col for first row/col as necessary 

7418 if row_first > row_last: 

7419 (row_first, row_last) = (row_last, row_first) 

7420 

7421 if col_first > col_last: 

7422 (col_first, col_last) = (col_last, col_first) 

7423 

7424 sqref += xl_range(row_first, col_first, row_last, col_last) 

7425 

7426 if options.get("multi_range"): 

7427 sqref = options["multi_range"] 

7428 

7429 if options["validate"] != "none": 

7430 attributes.append(("type", options["validate"])) 

7431 

7432 if options["criteria"] != "between": 

7433 attributes.append(("operator", options["criteria"])) 

7434 

7435 if "error_type" in options: 

7436 if options["error_type"] == 1: 

7437 attributes.append(("errorStyle", "warning")) 

7438 if options["error_type"] == 2: 

7439 attributes.append(("errorStyle", "information")) 

7440 

7441 if options["ignore_blank"]: 

7442 attributes.append(("allowBlank", 1)) 

7443 

7444 if not options["dropdown"]: 

7445 attributes.append(("showDropDown", 1)) 

7446 

7447 if options["show_input"]: 

7448 attributes.append(("showInputMessage", 1)) 

7449 

7450 if options["show_error"]: 

7451 attributes.append(("showErrorMessage", 1)) 

7452 

7453 if "error_title" in options: 

7454 attributes.append(("errorTitle", options["error_title"])) 

7455 

7456 if "error_message" in options: 

7457 attributes.append(("error", options["error_message"])) 

7458 

7459 if "input_title" in options: 

7460 attributes.append(("promptTitle", options["input_title"])) 

7461 

7462 if "input_message" in options: 

7463 attributes.append(("prompt", options["input_message"])) 

7464 

7465 attributes.append(("sqref", sqref)) 

7466 

7467 if options["validate"] == "none": 

7468 self._xml_empty_tag("dataValidation", attributes) 

7469 else: 

7470 self._xml_start_tag("dataValidation", attributes) 

7471 

7472 # Write the formula1 element. 

7473 self._write_formula_1(options["value"]) 

7474 

7475 # Write the formula2 element. 

7476 if options["maximum"] is not None: 

7477 self._write_formula_2(options["maximum"]) 

7478 

7479 self._xml_end_tag("dataValidation") 

7480 

7481 def _write_formula_1(self, formula) -> None: 

7482 # Write the <formula1> element. 

7483 

7484 if isinstance(formula, list): 

7485 formula = self._csv_join(*formula) 

7486 formula = f'"{formula}"' 

7487 else: 

7488 # Check if the formula is a number. 

7489 try: 

7490 float(formula) 

7491 except ValueError: 

7492 # Not a number. Remove the formula '=' sign if it exists. 

7493 if formula.startswith("="): 

7494 formula = formula.lstrip("=") 

7495 

7496 self._xml_data_element("formula1", formula) 

7497 

7498 def _write_formula_2(self, formula) -> None: 

7499 # Write the <formula2> element. 

7500 

7501 # Check if the formula is a number. 

7502 try: 

7503 float(formula) 

7504 except ValueError: 

7505 # Not a number. Remove the formula '=' sign if it exists. 

7506 if formula.startswith("="): 

7507 formula = formula.lstrip("=") 

7508 

7509 self._xml_data_element("formula2", formula) 

7510 

7511 def _write_conditional_formats(self) -> None: 

7512 # Write the Worksheet conditional formats. 

7513 ranges = sorted(self.cond_formats.keys()) 

7514 

7515 if not ranges: 

7516 return 

7517 

7518 for cond_range in ranges: 

7519 self._write_conditional_formatting( 

7520 cond_range, self.cond_formats[cond_range] 

7521 ) 

7522 

7523 def _write_conditional_formatting(self, cond_range, params) -> None: 

7524 # Write the <conditionalFormatting> element. 

7525 attributes = [("sqref", cond_range)] 

7526 self._xml_start_tag("conditionalFormatting", attributes) 

7527 for param in params: 

7528 # Write the cfRule element. 

7529 self._write_cf_rule(param) 

7530 self._xml_end_tag("conditionalFormatting") 

7531 

7532 def _write_cf_rule(self, params) -> None: 

7533 # Write the <cfRule> element. 

7534 attributes = [("type", params["type"])] 

7535 

7536 if "format" in params and params["format"] is not None: 

7537 attributes.append(("dxfId", params["format"])) 

7538 

7539 attributes.append(("priority", params["priority"])) 

7540 

7541 if params.get("stop_if_true"): 

7542 attributes.append(("stopIfTrue", 1)) 

7543 

7544 if params["type"] == "cellIs": 

7545 attributes.append(("operator", params["criteria"])) 

7546 

7547 self._xml_start_tag("cfRule", attributes) 

7548 

7549 if "minimum" in params and "maximum" in params: 

7550 self._write_formula_element(params["minimum"]) 

7551 self._write_formula_element(params["maximum"]) 

7552 else: 

7553 self._write_formula_element(params["value"]) 

7554 

7555 self._xml_end_tag("cfRule") 

7556 

7557 elif params["type"] == "aboveAverage": 

7558 if re.search("below", params["criteria"]): 

7559 attributes.append(("aboveAverage", 0)) 

7560 

7561 if re.search("equal", params["criteria"]): 

7562 attributes.append(("equalAverage", 1)) 

7563 

7564 if re.search("[123] std dev", params["criteria"]): 

7565 match = re.search("([123]) std dev", params["criteria"]) 

7566 attributes.append(("stdDev", match.group(1))) 

7567 

7568 self._xml_empty_tag("cfRule", attributes) 

7569 

7570 elif params["type"] == "top10": 

7571 if "criteria" in params and params["criteria"] == "%": 

7572 attributes.append(("percent", 1)) 

7573 

7574 if "direction" in params: 

7575 attributes.append(("bottom", 1)) 

7576 

7577 rank = params["value"] or 10 

7578 attributes.append(("rank", rank)) 

7579 

7580 self._xml_empty_tag("cfRule", attributes) 

7581 

7582 elif params["type"] == "duplicateValues": 

7583 self._xml_empty_tag("cfRule", attributes) 

7584 

7585 elif params["type"] == "uniqueValues": 

7586 self._xml_empty_tag("cfRule", attributes) 

7587 

7588 elif ( 

7589 params["type"] == "containsText" 

7590 or params["type"] == "notContainsText" 

7591 or params["type"] == "beginsWith" 

7592 or params["type"] == "endsWith" 

7593 ): 

7594 attributes.append(("operator", params["criteria"])) 

7595 attributes.append(("text", params["value"])) 

7596 self._xml_start_tag("cfRule", attributes) 

7597 self._write_formula_element(params["formula"]) 

7598 self._xml_end_tag("cfRule") 

7599 

7600 elif params["type"] == "timePeriod": 

7601 attributes.append(("timePeriod", params["criteria"])) 

7602 self._xml_start_tag("cfRule", attributes) 

7603 self._write_formula_element(params["formula"]) 

7604 self._xml_end_tag("cfRule") 

7605 

7606 elif ( 

7607 params["type"] == "containsBlanks" 

7608 or params["type"] == "notContainsBlanks" 

7609 or params["type"] == "containsErrors" 

7610 or params["type"] == "notContainsErrors" 

7611 ): 

7612 self._xml_start_tag("cfRule", attributes) 

7613 self._write_formula_element(params["formula"]) 

7614 self._xml_end_tag("cfRule") 

7615 

7616 elif params["type"] == "colorScale": 

7617 self._xml_start_tag("cfRule", attributes) 

7618 self._write_color_scale(params) 

7619 self._xml_end_tag("cfRule") 

7620 

7621 elif params["type"] == "dataBar": 

7622 self._xml_start_tag("cfRule", attributes) 

7623 self._write_data_bar(params) 

7624 

7625 if params.get("is_data_bar_2010"): 

7626 self._write_data_bar_ext(params) 

7627 

7628 self._xml_end_tag("cfRule") 

7629 

7630 elif params["type"] == "expression": 

7631 self._xml_start_tag("cfRule", attributes) 

7632 self._write_formula_element(params["criteria"]) 

7633 self._xml_end_tag("cfRule") 

7634 

7635 elif params["type"] == "iconSet": 

7636 self._xml_start_tag("cfRule", attributes) 

7637 self._write_icon_set(params) 

7638 self._xml_end_tag("cfRule") 

7639 

7640 def _write_formula_element(self, formula) -> None: 

7641 # Write the <formula> element. 

7642 

7643 # Check if the formula is a number. 

7644 try: 

7645 float(formula) 

7646 except ValueError: 

7647 # Not a number. Remove the formula '=' sign if it exists. 

7648 if formula.startswith("="): 

7649 formula = formula.lstrip("=") 

7650 

7651 self._xml_data_element("formula", formula) 

7652 

7653 def _write_color_scale(self, param) -> None: 

7654 # Write the <colorScale> element. 

7655 

7656 self._xml_start_tag("colorScale") 

7657 

7658 self._write_cfvo(param["min_type"], param["min_value"]) 

7659 

7660 if param["mid_type"] is not None: 

7661 self._write_cfvo(param["mid_type"], param["mid_value"]) 

7662 

7663 self._write_cfvo(param["max_type"], param["max_value"]) 

7664 

7665 self._write_color("color", param["min_color"]._attributes()) 

7666 

7667 if param["mid_color"] is not None: 

7668 self._write_color("color", param["mid_color"]._attributes()) 

7669 

7670 self._write_color("color", param["max_color"]._attributes()) 

7671 

7672 self._xml_end_tag("colorScale") 

7673 

7674 def _write_data_bar(self, param) -> None: 

7675 # Write the <dataBar> element. 

7676 attributes = [] 

7677 

7678 # Min and max bar lengths in in the spec but not supported directly by 

7679 # Excel. 

7680 if "min_length" in param: 

7681 attributes.append(("minLength", param["min_length"])) 

7682 

7683 if "max_length" in param: 

7684 attributes.append(("maxLength", param["max_length"])) 

7685 

7686 if param.get("bar_only"): 

7687 attributes.append(("showValue", 0)) 

7688 

7689 self._xml_start_tag("dataBar", attributes) 

7690 

7691 self._write_cfvo(param["min_type"], param["min_value"]) 

7692 self._write_cfvo(param["max_type"], param["max_value"]) 

7693 self._write_color("color", param["bar_color"]._attributes()) 

7694 

7695 self._xml_end_tag("dataBar") 

7696 

7697 def _write_data_bar_ext(self, param) -> None: 

7698 # Write the <extLst> dataBar extension element. 

7699 

7700 # Create a pseudo GUID for each unique Excel 2010 data bar. 

7701 worksheet_count = self.index + 1 

7702 data_bar_count = len(self.data_bars_2010) + 1 

7703 guid = "{DA7ABA51-AAAA-BBBB-%04X-%012X}" % (worksheet_count, data_bar_count) 

7704 

7705 # Store the 2010 data bar parameters to write the extLst elements. 

7706 param["guid"] = guid 

7707 self.data_bars_2010.append(param) 

7708 

7709 self._xml_start_tag("extLst") 

7710 self._write_ext("{B025F937-C7B1-47D3-B67F-A62EFF666E3E}") 

7711 self._xml_data_element("x14:id", guid) 

7712 self._xml_end_tag("ext") 

7713 self._xml_end_tag("extLst") 

7714 

7715 def _write_icon_set(self, param) -> None: 

7716 # Write the <iconSet> element. 

7717 attributes = [] 

7718 

7719 # Don't set attribute for default style. 

7720 if param["icon_style"] != "3TrafficLights": 

7721 attributes = [("iconSet", param["icon_style"])] 

7722 

7723 if param.get("icons_only"): 

7724 attributes.append(("showValue", 0)) 

7725 

7726 if param.get("reverse_icons"): 

7727 attributes.append(("reverse", 1)) 

7728 

7729 self._xml_start_tag("iconSet", attributes) 

7730 

7731 # Write the properties for different icon styles. 

7732 for icon in reversed(param["icons"]): 

7733 self._write_cfvo(icon["type"], icon["value"], icon["criteria"]) 

7734 

7735 self._xml_end_tag("iconSet") 

7736 

7737 def _write_cfvo(self, cf_type, val, criteria=None) -> None: 

7738 # Write the <cfvo> element. 

7739 attributes = [("type", cf_type)] 

7740 

7741 if val is not None: 

7742 attributes.append(("val", val)) 

7743 

7744 if criteria: 

7745 attributes.append(("gte", 0)) 

7746 

7747 self._xml_empty_tag("cfvo", attributes) 

7748 

7749 def _write_color(self, name, attributes) -> None: 

7750 # Write the <color> element. 

7751 self._xml_empty_tag(name, attributes) 

7752 

7753 def _write_selections(self) -> None: 

7754 # Write the <selection> elements. 

7755 for selection in self.selections: 

7756 self._write_selection(*selection) 

7757 

7758 def _write_selection(self, pane, active_cell, sqref) -> None: 

7759 # Write the <selection> element. 

7760 attributes = [] 

7761 

7762 if pane: 

7763 attributes.append(("pane", pane)) 

7764 

7765 if active_cell: 

7766 attributes.append(("activeCell", active_cell)) 

7767 

7768 if sqref: 

7769 attributes.append(("sqref", sqref)) 

7770 

7771 self._xml_empty_tag("selection", attributes) 

7772 

7773 def _write_panes(self) -> None: 

7774 # Write the frozen or split <pane> elements. 

7775 panes = self.panes 

7776 

7777 if not panes: 

7778 return 

7779 

7780 if panes[4] == 2: 

7781 self._write_split_panes(*panes) 

7782 else: 

7783 self._write_freeze_panes(*panes) 

7784 

7785 def _write_freeze_panes( 

7786 self, row: int, col: int, top_row, left_col, pane_type 

7787 ) -> None: 

7788 # Write the <pane> element for freeze panes. 

7789 attributes = [] 

7790 

7791 y_split = row 

7792 x_split = col 

7793 top_left_cell = xl_rowcol_to_cell(top_row, left_col) 

7794 active_pane = "" 

7795 state = "" 

7796 active_cell = "" 

7797 sqref = "" 

7798 

7799 # Move user cell selection to the panes. 

7800 if self.selections: 

7801 (_, active_cell, sqref) = self.selections[0] 

7802 self.selections = [] 

7803 

7804 # Set the active pane. 

7805 if row and col: 

7806 active_pane = "bottomRight" 

7807 

7808 row_cell = xl_rowcol_to_cell(row, 0) 

7809 col_cell = xl_rowcol_to_cell(0, col) 

7810 

7811 self.selections.append(["topRight", col_cell, col_cell]) 

7812 self.selections.append(["bottomLeft", row_cell, row_cell]) 

7813 self.selections.append(["bottomRight", active_cell, sqref]) 

7814 

7815 elif col: 

7816 active_pane = "topRight" 

7817 self.selections.append(["topRight", active_cell, sqref]) 

7818 

7819 else: 

7820 active_pane = "bottomLeft" 

7821 self.selections.append(["bottomLeft", active_cell, sqref]) 

7822 

7823 # Set the pane type. 

7824 if pane_type == 0: 

7825 state = "frozen" 

7826 elif pane_type == 1: 

7827 state = "frozenSplit" 

7828 else: 

7829 state = "split" 

7830 

7831 if x_split: 

7832 attributes.append(("xSplit", x_split)) 

7833 

7834 if y_split: 

7835 attributes.append(("ySplit", y_split)) 

7836 

7837 attributes.append(("topLeftCell", top_left_cell)) 

7838 attributes.append(("activePane", active_pane)) 

7839 attributes.append(("state", state)) 

7840 

7841 self._xml_empty_tag("pane", attributes) 

7842 

7843 def _write_split_panes(self, row: int, col: int, top_row, left_col, _) -> None: 

7844 # Write the <pane> element for split panes. 

7845 attributes = [] 

7846 has_selection = False 

7847 active_pane = "" 

7848 active_cell = "" 

7849 sqref = "" 

7850 

7851 y_split = row 

7852 x_split = col 

7853 

7854 # Move user cell selection to the panes. 

7855 if self.selections: 

7856 (_, active_cell, sqref) = self.selections[0] 

7857 self.selections = [] 

7858 has_selection = True 

7859 

7860 # Convert the row and col to 1/20 twip units with padding. 

7861 if y_split: 

7862 y_split = int(20 * y_split + 300) 

7863 

7864 if x_split: 

7865 x_split = self._calculate_x_split_width(x_split) 

7866 

7867 # For non-explicit topLeft definitions, estimate the cell offset based 

7868 # on the pixels dimensions. This is only a workaround and doesn't take 

7869 # adjusted cell dimensions into account. 

7870 if top_row == row and left_col == col: 

7871 top_row = int(0.5 + (y_split - 300) / 20 / 15) 

7872 left_col = int(0.5 + (x_split - 390) / 20 / 3 * 4 / 64) 

7873 

7874 top_left_cell = xl_rowcol_to_cell(top_row, left_col) 

7875 

7876 # If there is no selection set the active cell to the top left cell. 

7877 if not has_selection: 

7878 active_cell = top_left_cell 

7879 sqref = top_left_cell 

7880 

7881 # Set the Cell selections. 

7882 if row and col: 

7883 active_pane = "bottomRight" 

7884 

7885 row_cell = xl_rowcol_to_cell(top_row, 0) 

7886 col_cell = xl_rowcol_to_cell(0, left_col) 

7887 

7888 self.selections.append(["topRight", col_cell, col_cell]) 

7889 self.selections.append(["bottomLeft", row_cell, row_cell]) 

7890 self.selections.append(["bottomRight", active_cell, sqref]) 

7891 

7892 elif col: 

7893 active_pane = "topRight" 

7894 self.selections.append(["topRight", active_cell, sqref]) 

7895 

7896 else: 

7897 active_pane = "bottomLeft" 

7898 self.selections.append(["bottomLeft", active_cell, sqref]) 

7899 

7900 # Format splits to the same precision as Excel. 

7901 if x_split: 

7902 attributes.append(("xSplit", f"{x_split:.16g}")) 

7903 

7904 if y_split: 

7905 attributes.append(("ySplit", f"{y_split:.16g}")) 

7906 

7907 attributes.append(("topLeftCell", top_left_cell)) 

7908 

7909 if has_selection: 

7910 attributes.append(("activePane", active_pane)) 

7911 

7912 self._xml_empty_tag("pane", attributes) 

7913 

7914 def _calculate_x_split_width(self, width): 

7915 # Convert column width from user units to pane split width. 

7916 

7917 max_digit_width = 7 # For Calabri 11. 

7918 padding = 5 

7919 

7920 # Convert to pixels. 

7921 if width < 1: 

7922 pixels = int(width * (max_digit_width + padding) + 0.5) 

7923 else: 

7924 pixels = int(width * max_digit_width + 0.5) + padding 

7925 

7926 # Convert to points. 

7927 points = pixels * 3 / 4 

7928 

7929 # Convert to twips (twentieths of a point). 

7930 twips = points * 20 

7931 

7932 # Add offset/padding. 

7933 width = twips + 390 

7934 

7935 return width 

7936 

7937 def _write_table_parts(self) -> None: 

7938 # Write the <tableParts> element. 

7939 tables = self.tables 

7940 count = len(tables) 

7941 

7942 # Return if worksheet doesn't contain any tables. 

7943 if not count: 

7944 return 

7945 

7946 attributes = [ 

7947 ( 

7948 "count", 

7949 count, 

7950 ) 

7951 ] 

7952 

7953 self._xml_start_tag("tableParts", attributes) 

7954 

7955 for _ in tables: 

7956 # Write the tablePart element. 

7957 self.rel_count += 1 

7958 self._write_table_part(self.rel_count) 

7959 

7960 self._xml_end_tag("tableParts") 

7961 

7962 def _write_table_part(self, r_id) -> None: 

7963 # Write the <tablePart> element. 

7964 

7965 r_id = "rId" + str(r_id) 

7966 

7967 attributes = [ 

7968 ( 

7969 "r:id", 

7970 r_id, 

7971 ) 

7972 ] 

7973 

7974 self._xml_empty_tag("tablePart", attributes) 

7975 

7976 def _write_ext_list(self) -> None: 

7977 # Write the <extLst> element for data bars and sparklines. 

7978 has_data_bars = len(self.data_bars_2010) 

7979 has_sparklines = len(self.sparklines) 

7980 

7981 if not has_data_bars and not has_sparklines: 

7982 return 

7983 

7984 # Write the extLst element. 

7985 self._xml_start_tag("extLst") 

7986 

7987 if has_data_bars: 

7988 self._write_ext_list_data_bars() 

7989 

7990 if has_sparklines: 

7991 self._write_ext_list_sparklines() 

7992 

7993 self._xml_end_tag("extLst") 

7994 

7995 def _write_ext_list_data_bars(self) -> None: 

7996 # Write the Excel 2010 data_bar subelements. 

7997 self._write_ext("{78C0D931-6437-407d-A8EE-F0AAD7539E65}") 

7998 

7999 self._xml_start_tag("x14:conditionalFormattings") 

8000 

8001 # Write the Excel 2010 conditional formatting data bar elements. 

8002 for data_bar in self.data_bars_2010: 

8003 # Write the x14:conditionalFormatting element. 

8004 self._write_conditional_formatting_2010(data_bar) 

8005 

8006 self._xml_end_tag("x14:conditionalFormattings") 

8007 self._xml_end_tag("ext") 

8008 

8009 def _write_conditional_formatting_2010(self, data_bar) -> None: 

8010 # Write the <x14:conditionalFormatting> element. 

8011 xmlns_xm = "http://schemas.microsoft.com/office/excel/2006/main" 

8012 

8013 attributes = [("xmlns:xm", xmlns_xm)] 

8014 

8015 self._xml_start_tag("x14:conditionalFormatting", attributes) 

8016 

8017 # Write the x14:cfRule element. 

8018 self._write_x14_cf_rule(data_bar) 

8019 

8020 # Write the x14:dataBar element. 

8021 self._write_x14_data_bar(data_bar) 

8022 

8023 # Write the x14 max and min data bars. 

8024 self._write_x14_cfvo(data_bar["x14_min_type"], data_bar["min_value"]) 

8025 self._write_x14_cfvo(data_bar["x14_max_type"], data_bar["max_value"]) 

8026 

8027 if not data_bar["bar_no_border"]: 

8028 # Write the x14:borderColor element. 

8029 self._write_x14_border_color(data_bar["bar_border_color"]) 

8030 

8031 # Write the x14:negativeFillColor element. 

8032 if not data_bar["bar_negative_color_same"]: 

8033 self._write_x14_negative_fill_color(data_bar["bar_negative_color"]) 

8034 

8035 # Write the x14:negativeBorderColor element. 

8036 if ( 

8037 not data_bar["bar_no_border"] 

8038 and not data_bar["bar_negative_border_color_same"] 

8039 ): 

8040 self._write_x14_negative_border_color(data_bar["bar_negative_border_color"]) 

8041 

8042 # Write the x14:axisColor element. 

8043 if data_bar["bar_axis_position"] != "none": 

8044 self._write_x14_axis_color(data_bar["bar_axis_color"]) 

8045 

8046 self._xml_end_tag("x14:dataBar") 

8047 self._xml_end_tag("x14:cfRule") 

8048 

8049 # Write the xm:sqref element. 

8050 self._xml_data_element("xm:sqref", data_bar["range"]) 

8051 

8052 self._xml_end_tag("x14:conditionalFormatting") 

8053 

8054 def _write_x14_cf_rule(self, data_bar) -> None: 

8055 # Write the <x14:cfRule> element. 

8056 rule_type = "dataBar" 

8057 guid = data_bar["guid"] 

8058 attributes = [("type", rule_type), ("id", guid)] 

8059 

8060 self._xml_start_tag("x14:cfRule", attributes) 

8061 

8062 def _write_x14_data_bar(self, data_bar) -> None: 

8063 # Write the <x14:dataBar> element. 

8064 min_length = 0 

8065 max_length = 100 

8066 

8067 attributes = [ 

8068 ("minLength", min_length), 

8069 ("maxLength", max_length), 

8070 ] 

8071 

8072 if not data_bar["bar_no_border"]: 

8073 attributes.append(("border", 1)) 

8074 

8075 if data_bar["bar_solid"]: 

8076 attributes.append(("gradient", 0)) 

8077 

8078 if data_bar["bar_direction"] == "left": 

8079 attributes.append(("direction", "leftToRight")) 

8080 

8081 if data_bar["bar_direction"] == "right": 

8082 attributes.append(("direction", "rightToLeft")) 

8083 

8084 if data_bar["bar_negative_color_same"]: 

8085 attributes.append(("negativeBarColorSameAsPositive", 1)) 

8086 

8087 if ( 

8088 not data_bar["bar_no_border"] 

8089 and not data_bar["bar_negative_border_color_same"] 

8090 ): 

8091 attributes.append(("negativeBarBorderColorSameAsPositive", 0)) 

8092 

8093 if data_bar["bar_axis_position"] == "middle": 

8094 attributes.append(("axisPosition", "middle")) 

8095 

8096 if data_bar["bar_axis_position"] == "none": 

8097 attributes.append(("axisPosition", "none")) 

8098 

8099 self._xml_start_tag("x14:dataBar", attributes) 

8100 

8101 def _write_x14_cfvo(self, rule_type, value) -> None: 

8102 # Write the <x14:cfvo> element. 

8103 attributes = [("type", rule_type)] 

8104 

8105 if rule_type in ("min", "max", "autoMin", "autoMax"): 

8106 self._xml_empty_tag("x14:cfvo", attributes) 

8107 else: 

8108 self._xml_start_tag("x14:cfvo", attributes) 

8109 self._xml_data_element("xm:f", value) 

8110 self._xml_end_tag("x14:cfvo") 

8111 

8112 def _write_x14_border_color(self, color) -> None: 

8113 # Write the <x14:borderColor> element. 

8114 self._write_color("x14:borderColor", color._attributes()) 

8115 

8116 def _write_x14_negative_fill_color(self, color) -> None: 

8117 # Write the <x14:negativeFillColor> element. 

8118 self._xml_empty_tag("x14:negativeFillColor", color._attributes()) 

8119 

8120 def _write_x14_negative_border_color(self, color) -> None: 

8121 # Write the <x14:negativeBorderColor> element. 

8122 self._xml_empty_tag("x14:negativeBorderColor", color._attributes()) 

8123 

8124 def _write_x14_axis_color(self, color) -> None: 

8125 # Write the <x14:axisColor> element. 

8126 self._xml_empty_tag("x14:axisColor", color._attributes()) 

8127 

8128 def _write_ext_list_sparklines(self) -> None: 

8129 # Write the sparkline extension sub-elements. 

8130 self._write_ext("{05C60535-1F16-4fd2-B633-F4F36F0B64E0}") 

8131 

8132 # Write the x14:sparklineGroups element. 

8133 self._write_sparkline_groups() 

8134 

8135 # Write the sparkline elements. 

8136 for sparkline in reversed(self.sparklines): 

8137 # Write the x14:sparklineGroup element. 

8138 self._write_sparkline_group(sparkline) 

8139 

8140 # Write the x14:colorSeries element. 

8141 self._write_color_series(sparkline["series_color"]) 

8142 

8143 # Write the x14:colorNegative element. 

8144 self._write_color_negative(sparkline["negative_color"]) 

8145 

8146 # Write the x14:colorAxis element. 

8147 self._write_color_axis() 

8148 

8149 # Write the x14:colorMarkers element. 

8150 self._write_color_markers(sparkline["markers_color"]) 

8151 

8152 # Write the x14:colorFirst element. 

8153 self._write_color_first(sparkline["first_color"]) 

8154 

8155 # Write the x14:colorLast element. 

8156 self._write_color_last(sparkline["last_color"]) 

8157 

8158 # Write the x14:colorHigh element. 

8159 self._write_color_high(sparkline["high_color"]) 

8160 

8161 # Write the x14:colorLow element. 

8162 self._write_color_low(sparkline["low_color"]) 

8163 

8164 if sparkline["date_axis"]: 

8165 self._xml_data_element("xm:f", sparkline["date_axis"]) 

8166 

8167 self._write_sparklines(sparkline) 

8168 

8169 self._xml_end_tag("x14:sparklineGroup") 

8170 

8171 self._xml_end_tag("x14:sparklineGroups") 

8172 self._xml_end_tag("ext") 

8173 

8174 def _write_sparklines(self, sparkline) -> None: 

8175 # Write the <x14:sparklines> element and <x14:sparkline> sub-elements. 

8176 

8177 # Write the sparkline elements. 

8178 self._xml_start_tag("x14:sparklines") 

8179 

8180 for i in range(sparkline["count"]): 

8181 spark_range = sparkline["ranges"][i] 

8182 location = sparkline["locations"][i] 

8183 

8184 self._xml_start_tag("x14:sparkline") 

8185 self._xml_data_element("xm:f", spark_range) 

8186 self._xml_data_element("xm:sqref", location) 

8187 self._xml_end_tag("x14:sparkline") 

8188 

8189 self._xml_end_tag("x14:sparklines") 

8190 

8191 def _write_ext(self, uri) -> None: 

8192 # Write the <ext> element. 

8193 schema = "http://schemas.microsoft.com/office/" 

8194 xmlns_x14 = schema + "spreadsheetml/2009/9/main" 

8195 

8196 attributes = [ 

8197 ("xmlns:x14", xmlns_x14), 

8198 ("uri", uri), 

8199 ] 

8200 

8201 self._xml_start_tag("ext", attributes) 

8202 

8203 def _write_sparkline_groups(self) -> None: 

8204 # Write the <x14:sparklineGroups> element. 

8205 xmlns_xm = "http://schemas.microsoft.com/office/excel/2006/main" 

8206 

8207 attributes = [("xmlns:xm", xmlns_xm)] 

8208 

8209 self._xml_start_tag("x14:sparklineGroups", attributes) 

8210 

8211 def _write_sparkline_group(self, options) -> None: 

8212 # Write the <x14:sparklineGroup> element. 

8213 # 

8214 # Example for order. 

8215 # 

8216 # <x14:sparklineGroup 

8217 # manualMax="0" 

8218 # manualMin="0" 

8219 # lineWeight="2.25" 

8220 # type="column" 

8221 # dateAxis="1" 

8222 # displayEmptyCellsAs="span" 

8223 # markers="1" 

8224 # high="1" 

8225 # low="1" 

8226 # first="1" 

8227 # last="1" 

8228 # negative="1" 

8229 # displayXAxis="1" 

8230 # displayHidden="1" 

8231 # minAxisType="custom" 

8232 # maxAxisType="custom" 

8233 # rightToLeft="1"> 

8234 # 

8235 empty = options.get("empty") 

8236 attributes = [] 

8237 

8238 if options.get("max") is not None: 

8239 if options["max"] == "group": 

8240 options["cust_max"] = "group" 

8241 else: 

8242 attributes.append(("manualMax", options["max"])) 

8243 options["cust_max"] = "custom" 

8244 

8245 if options.get("min") is not None: 

8246 if options["min"] == "group": 

8247 options["cust_min"] = "group" 

8248 else: 

8249 attributes.append(("manualMin", options["min"])) 

8250 options["cust_min"] = "custom" 

8251 

8252 # Ignore the default type attribute (line). 

8253 if options["type"] != "line": 

8254 attributes.append(("type", options["type"])) 

8255 

8256 if options.get("weight"): 

8257 attributes.append(("lineWeight", options["weight"])) 

8258 

8259 if options.get("date_axis"): 

8260 attributes.append(("dateAxis", 1)) 

8261 

8262 if empty: 

8263 attributes.append(("displayEmptyCellsAs", empty)) 

8264 

8265 if options.get("markers"): 

8266 attributes.append(("markers", 1)) 

8267 

8268 if options.get("high"): 

8269 attributes.append(("high", 1)) 

8270 

8271 if options.get("low"): 

8272 attributes.append(("low", 1)) 

8273 

8274 if options.get("first"): 

8275 attributes.append(("first", 1)) 

8276 

8277 if options.get("last"): 

8278 attributes.append(("last", 1)) 

8279 

8280 if options.get("negative"): 

8281 attributes.append(("negative", 1)) 

8282 

8283 if options.get("axis"): 

8284 attributes.append(("displayXAxis", 1)) 

8285 

8286 if options.get("hidden"): 

8287 attributes.append(("displayHidden", 1)) 

8288 

8289 if options.get("cust_min"): 

8290 attributes.append(("minAxisType", options["cust_min"])) 

8291 

8292 if options.get("cust_max"): 

8293 attributes.append(("maxAxisType", options["cust_max"])) 

8294 

8295 if options.get("reverse"): 

8296 attributes.append(("rightToLeft", 1)) 

8297 

8298 self._xml_start_tag("x14:sparklineGroup", attributes) 

8299 

8300 def _write_spark_color(self, tag, color) -> None: 

8301 # Helper function for the sparkline color functions below. 

8302 if color: 

8303 self._write_color(tag, color._attributes()) 

8304 

8305 def _write_color_series(self, color) -> None: 

8306 # Write the <x14:colorSeries> element. 

8307 self._write_spark_color("x14:colorSeries", color) 

8308 

8309 def _write_color_negative(self, color) -> None: 

8310 # Write the <x14:colorNegative> element. 

8311 self._write_spark_color("x14:colorNegative", color) 

8312 

8313 def _write_color_axis(self) -> None: 

8314 # Write the <x14:colorAxis> element. 

8315 self._write_spark_color("x14:colorAxis", Color("#000000")) 

8316 

8317 def _write_color_markers(self, color) -> None: 

8318 # Write the <x14:colorMarkers> element. 

8319 self._write_spark_color("x14:colorMarkers", color) 

8320 

8321 def _write_color_first(self, color) -> None: 

8322 # Write the <x14:colorFirst> element. 

8323 self._write_spark_color("x14:colorFirst", color) 

8324 

8325 def _write_color_last(self, color) -> None: 

8326 # Write the <x14:colorLast> element. 

8327 self._write_spark_color("x14:colorLast", color) 

8328 

8329 def _write_color_high(self, color) -> None: 

8330 # Write the <x14:colorHigh> element. 

8331 self._write_spark_color("x14:colorHigh", color) 

8332 

8333 def _write_color_low(self, color) -> None: 

8334 # Write the <x14:colorLow> element. 

8335 self._write_spark_color("x14:colorLow", color) 

8336 

8337 def _write_phonetic_pr(self) -> None: 

8338 # Write the <phoneticPr> element. 

8339 attributes = [ 

8340 ("fontId", "0"), 

8341 ("type", "noConversion"), 

8342 ] 

8343 

8344 self._xml_empty_tag("phoneticPr", attributes) 

8345 

8346 def _write_ignored_errors(self) -> None: 

8347 # Write the <ignoredErrors> element. 

8348 if not self.ignored_errors: 

8349 return 

8350 

8351 self._xml_start_tag("ignoredErrors") 

8352 

8353 if self.ignored_errors.get("number_stored_as_text"): 

8354 ignored_range = self.ignored_errors["number_stored_as_text"] 

8355 self._write_ignored_error("numberStoredAsText", ignored_range) 

8356 

8357 if self.ignored_errors.get("eval_error"): 

8358 ignored_range = self.ignored_errors["eval_error"] 

8359 self._write_ignored_error("evalError", ignored_range) 

8360 

8361 if self.ignored_errors.get("formula_differs"): 

8362 ignored_range = self.ignored_errors["formula_differs"] 

8363 self._write_ignored_error("formula", ignored_range) 

8364 

8365 if self.ignored_errors.get("formula_range"): 

8366 ignored_range = self.ignored_errors["formula_range"] 

8367 self._write_ignored_error("formulaRange", ignored_range) 

8368 

8369 if self.ignored_errors.get("formula_unlocked"): 

8370 ignored_range = self.ignored_errors["formula_unlocked"] 

8371 self._write_ignored_error("unlockedFormula", ignored_range) 

8372 

8373 if self.ignored_errors.get("empty_cell_reference"): 

8374 ignored_range = self.ignored_errors["empty_cell_reference"] 

8375 self._write_ignored_error("emptyCellReference", ignored_range) 

8376 

8377 if self.ignored_errors.get("list_data_validation"): 

8378 ignored_range = self.ignored_errors["list_data_validation"] 

8379 self._write_ignored_error("listDataValidation", ignored_range) 

8380 

8381 if self.ignored_errors.get("calculated_column"): 

8382 ignored_range = self.ignored_errors["calculated_column"] 

8383 self._write_ignored_error("calculatedColumn", ignored_range) 

8384 

8385 if self.ignored_errors.get("two_digit_text_year"): 

8386 ignored_range = self.ignored_errors["two_digit_text_year"] 

8387 self._write_ignored_error("twoDigitTextYear", ignored_range) 

8388 

8389 self._xml_end_tag("ignoredErrors") 

8390 

8391 def _write_ignored_error(self, error_type, ignored_range) -> None: 

8392 # Write the <ignoredError> element. 

8393 attributes = [ 

8394 ("sqref", ignored_range), 

8395 (error_type, 1), 

8396 ] 

8397 

8398 self._xml_empty_tag("ignoredError", attributes)