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

3760 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 = 1 

306 self.print_scale = 100 

307 self.is_right_to_left = False 

308 self.show_zeros = 1 

309 self.leading_zeros = 0 

310 

311 self.outline_row_level = 0 

312 self.outline_col_level = 0 

313 self.outline_style = 0 

314 self.outline_below = 1 

315 self.outline_right = 1 

316 self.outline_on = 1 

317 self.outline_changed = False 

318 

319 self.original_row_height = 15 

320 self.default_row_height = 15 

321 self.default_row_pixels = 20 

322 self.default_col_width = 8.43 

323 self.default_col_pixels = 64 

324 self.default_date_pixels = 68 

325 self.default_row_zeroed = 0 

326 

327 self.names = {} 

328 self.write_match = [] 

329 self.table = defaultdict(dict) 

330 self.merge = [] 

331 self.merged_cells = {} 

332 self.table_cells = {} 

333 self.row_spans = {} 

334 

335 self.has_vml = False 

336 self.has_header_vml = False 

337 self.has_comments = False 

338 self.comments = defaultdict(dict) 

339 self.comments_list = [] 

340 self.comments_author = "" 

341 self.comments_visible = False 

342 self.vml_shape_id = 1024 

343 self.buttons_list = [] 

344 self.vml_header_id = 0 

345 

346 self.autofilter_area = "" 

347 self.autofilter_ref = None 

348 self.filter_range = [0, 9] 

349 self.filter_on = 0 

350 self.filter_cols = {} 

351 self.filter_type = {} 

352 self.filter_cells = {} 

353 

354 self.row_sizes = {} 

355 self.col_size_changed = False 

356 self.row_size_changed = False 

357 

358 self.last_shape_id = 1 

359 self.rel_count = 0 

360 self.hlink_count = 0 

361 self.hlink_refs = [] 

362 self.external_hyper_links = [] 

363 self.external_drawing_links = [] 

364 self.external_comment_links = [] 

365 self.external_vml_links = [] 

366 self.external_table_links = [] 

367 self.external_background_links = [] 

368 self.drawing_links = [] 

369 self.vml_drawing_links = [] 

370 self.charts = [] 

371 self.images = [] 

372 self.tables = [] 

373 self.sparklines = [] 

374 self.shapes = [] 

375 self.shape_hash = {} 

376 self.drawing = 0 

377 self.drawing_rels = {} 

378 self.drawing_rels_id = 0 

379 self.vml_drawing_rels = {} 

380 self.vml_drawing_rels_id = 0 

381 self.background_image = None 

382 

383 self.rstring = "" 

384 self.previous_row = 0 

385 

386 self.validations = [] 

387 self.cond_formats = {} 

388 self.data_bars_2010 = [] 

389 self.use_data_bars_2010 = False 

390 self.dxf_priority = 1 

391 self.page_view = 0 

392 

393 self.vba_codename = None 

394 

395 self.date_1904 = False 

396 self.hyperlinks = defaultdict(dict) 

397 

398 self.strings_to_numbers = False 

399 self.strings_to_urls = True 

400 self.nan_inf_to_errors = False 

401 self.strings_to_formulas = True 

402 

403 self.default_date_format = None 

404 self.default_url_format = None 

405 self.default_checkbox_format = None 

406 self.workbook_add_format = None 

407 self.remove_timezone = False 

408 self.max_url_length = 2079 

409 

410 self.row_data_filename = None 

411 self.row_data_fh = None 

412 self.worksheet_meta = None 

413 self.vml_data_id = None 

414 self.vml_shape_id = None 

415 

416 self.row_data_filename = None 

417 self.row_data_fh = None 

418 self.row_data_fh_closed = False 

419 

420 self.vertical_dpi = 0 

421 self.horizontal_dpi = 0 

422 

423 self.write_handlers = {} 

424 

425 self.ignored_errors = None 

426 

427 self.has_dynamic_arrays = False 

428 self.use_future_functions = False 

429 self.ignore_write_string = False 

430 self.embedded_images = None 

431 

432 # Utility function for writing different types of strings. 

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

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

435 if token == "": 

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

437 

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

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

440 

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

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

443 

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

445 if ( 

446 ":" in token 

447 and self.strings_to_urls 

448 and ( 

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

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

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

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

453 ) 

454 ): 

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

456 

457 if self.strings_to_numbers: 

458 try: 

459 f = float(token) 

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

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

462 except ValueError: 

463 # Not a number, write as a string. 

464 pass 

465 

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

467 

468 # We have a plain string. 

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

470 

471 @convert_cell_args 

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

473 """ 

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

475 method based on the type of data being passed. 

476 

477 Args: 

478 row: The cell row (zero indexed). 

479 col: The cell column (zero indexed). 

480 *args: Args to pass to sub functions. 

481 

482 Returns: 

483 0: Success. 

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

485 other: Return value of called method. 

486 

487 """ 

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

489 

490 # Undecorated version of write(). 

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

492 # pylint: disable=raise-missing-from 

493 # Check the number of args passed. 

494 if not args: 

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

496 

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

498 token = args[0] 

499 

500 # Avoid isinstance() for better performance. 

501 token_type = token.__class__ 

502 

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

504 if token_type in self.write_handlers: 

505 write_handler = self.write_handlers[token_type] 

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

507 

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

509 # control to this function and we should continue as 

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

511 if function_return is None: 

512 pass 

513 else: 

514 return function_return 

515 

516 # Write None as a blank cell. 

517 if token is None: 

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

519 

520 # Check for standard Python types. 

521 if token_type is bool: 

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

523 

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

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

526 

527 if token_type is str: 

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

529 

530 if token_type in ( 

531 datetime.datetime, 

532 datetime.date, 

533 datetime.time, 

534 datetime.timedelta, 

535 ): 

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

537 

538 # Resort to isinstance() for subclassed primitives. 

539 

540 # Write number types. 

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

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

543 

544 # Write string types. 

545 if isinstance(token, str): 

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

547 

548 # Write boolean types. 

549 if isinstance(token, bool): 

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

551 

552 # Write datetime objects. 

553 if _supported_datetime(token): 

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

555 

556 # Write Url type. 

557 if isinstance(token, Url): 

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

559 

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

561 try: 

562 f = float(token) 

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

564 except ValueError: 

565 pass 

566 except TypeError: 

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

568 

569 # Finally try string. 

570 try: 

571 str(token) 

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

573 except ValueError: 

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

575 

576 @convert_cell_args 

577 def write_string( 

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

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

580 """ 

581 Write a string to a worksheet cell. 

582 

583 Args: 

584 row: The cell row (zero indexed). 

585 col: The cell column (zero indexed). 

586 string: Cell data. Str. 

587 format: An optional cell Format object. 

588 

589 Returns: 

590 0: Success. 

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

592 -2: String truncated to 32k characters. 

593 

594 """ 

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

596 

597 # Undecorated version of write_string(). 

598 def _write_string( 

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

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

601 str_error = 0 

602 

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

604 if self._check_dimensions(row, col): 

605 return -1 

606 

607 # Check that the string is < 32767 chars. 

608 if len(string) > self.xls_strmax: 

609 string = string[: self.xls_strmax] 

610 str_error = -2 

611 

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

613 if not self.constant_memory: 

614 string_index = self.str_table._get_shared_string_index(string) 

615 else: 

616 string_index = string 

617 

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

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

620 self._write_single_row(row) 

621 

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

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

624 

625 return str_error 

626 

627 @convert_cell_args 

628 def write_number( 

629 self, 

630 row: int, 

631 col: int, 

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

633 cell_format: Optional[Format] = None, 

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

635 """ 

636 Write a number to a worksheet cell. 

637 

638 Args: 

639 row: The cell row (zero indexed). 

640 col: The cell column (zero indexed). 

641 number: Cell data. Int or float. 

642 cell_format: An optional cell Format object. 

643 

644 Returns: 

645 0: Success. 

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

647 

648 """ 

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

650 

651 # Undecorated version of write_number(). 

652 def _write_number( 

653 self, 

654 row: int, 

655 col: int, 

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

657 cell_format: Optional[Format] = None, 

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

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

660 if self.nan_inf_to_errors: 

661 if isnan(number): 

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

663 

664 if number == math.inf: 

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

666 

667 if number == -math.inf: 

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

669 else: 

670 raise TypeError( 

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

672 "without 'nan_inf_to_errors' Workbook() option" 

673 ) 

674 

675 if number.__class__ is Fraction: 

676 number = float(number) 

677 

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

679 if self._check_dimensions(row, col): 

680 return -1 

681 

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

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

684 self._write_single_row(row) 

685 

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

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

688 

689 return 0 

690 

691 @convert_cell_args 

692 def write_blank( 

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

694 ): 

695 """ 

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

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

698 

699 Args: 

700 row: The cell row (zero indexed). 

701 col: The cell column (zero indexed). 

702 blank: Any value. It is ignored. 

703 cell_format: An optional cell Format object. 

704 

705 Returns: 

706 0: Success. 

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

708 

709 """ 

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

711 

712 # Undecorated version of write_blank(). 

713 def _write_blank( 

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

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

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

717 if cell_format is None: 

718 return 0 

719 

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

721 if self._check_dimensions(row, col): 

722 return -1 

723 

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

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

726 self._write_single_row(row) 

727 

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

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

730 

731 return 0 

732 

733 @convert_cell_args 

734 def write_formula( 

735 self, 

736 row: int, 

737 col: int, 

738 formula: str, 

739 cell_format: Optional[Format] = None, 

740 value=0, 

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

742 """ 

743 Write a formula to a worksheet cell. 

744 

745 Args: 

746 row: The cell row (zero indexed). 

747 col: The cell column (zero indexed). 

748 formula: Cell formula. 

749 cell_format: An optional cell Format object. 

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

751 

752 Returns: 

753 0: Success. 

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

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

756 

757 """ 

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

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

760 

761 # Undecorated version of write_formula(). 

762 def _write_formula( 

763 self, 

764 row: int, 

765 col: int, 

766 formula: str, 

767 cell_format: Optional[Format] = None, 

768 value=0, 

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

770 if self._check_dimensions(row, col): 

771 return -1 

772 

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

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

775 return -1 

776 

777 # Check for dynamic array functions. 

778 if re_dynamic_function.search(formula): 

779 return self.write_dynamic_array_formula( 

780 row, col, row, col, formula, cell_format, value 

781 ) 

782 

783 # Hand off array formulas. 

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

785 return self._write_array_formula( 

786 row, col, row, col, formula, cell_format, value 

787 ) 

788 

789 # Modify the formula string, as needed. 

790 formula = self._prepare_formula(formula) 

791 

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

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

794 self._write_single_row(row) 

795 

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

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

798 

799 return 0 

800 

801 @convert_range_args 

802 def write_array_formula( 

803 self, 

804 first_row: int, 

805 first_col: int, 

806 last_row: int, 

807 last_col: int, 

808 formula: str, 

809 cell_format: Optional[Format] = None, 

810 value=0, 

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

812 """ 

813 Write a formula to a worksheet cell/range. 

814 

815 Args: 

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

817 first_col: The first column of the cell range. 

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

819 last_col: The last column of the cell range. 

820 formula: Cell formula. 

821 cell_format: An optional cell Format object. 

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

823 

824 Returns: 

825 0: Success. 

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

827 

828 """ 

829 # Check for dynamic array functions. 

830 if re_dynamic_function.search(formula): 

831 return self.write_dynamic_array_formula( 

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

833 ) 

834 

835 return self._write_array_formula( 

836 first_row, 

837 first_col, 

838 last_row, 

839 last_col, 

840 formula, 

841 cell_format, 

842 value, 

843 "static", 

844 ) 

845 

846 @convert_range_args 

847 def write_dynamic_array_formula( 

848 self, 

849 first_row: int, 

850 first_col: int, 

851 last_row: int, 

852 last_col: int, 

853 formula: str, 

854 cell_format: Optional[Format] = None, 

855 value=0, 

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

857 """ 

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

859 

860 Args: 

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

862 first_col: The first column of the cell range. 

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

864 last_col: The last column of the cell range. 

865 formula: Cell formula. 

866 cell_format: An optional cell Format object. 

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

868 

869 Returns: 

870 0: Success. 

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

872 

873 """ 

874 error = self._write_array_formula( 

875 first_row, 

876 first_col, 

877 last_row, 

878 last_col, 

879 formula, 

880 cell_format, 

881 value, 

882 "dynamic", 

883 ) 

884 

885 if error == 0: 

886 self.has_dynamic_arrays = True 

887 

888 return error 

889 

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

891 # also expand out future and dynamic array formulas. 

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

893 # Remove array formula braces and the leading =. 

894 if formula.startswith("{"): 

895 formula = formula[1:] 

896 if formula.startswith("="): 

897 formula = formula[1:] 

898 if formula.endswith("}"): 

899 formula = formula[:-1] 

900 

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

902 if "_xlfn." in formula: 

903 return formula 

904 

905 # Expand dynamic formulas. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

935 

936 if not self.use_future_functions and not expand_future_functions: 

937 return formula 

938 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

985 formula = re.sub( 

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

987 ) 

988 formula = re.sub( 

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

990 ) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1069 

1070 return formula 

1071 

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

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

1074 # replacements in string literals within the formula. 

1075 @staticmethod 

1076 def _prepare_table_formula(formula): 

1077 if "@" not in formula: 

1078 # No escaping required. 

1079 return formula 

1080 

1081 escaped_formula = [] 

1082 in_string_literal = False 

1083 

1084 for char in formula: 

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

1086 # references in strings. 

1087 if char == '"': 

1088 in_string_literal = not in_string_literal 

1089 

1090 # Copy the string literal. 

1091 if in_string_literal: 

1092 escaped_formula.append(char) 

1093 continue 

1094 

1095 # Replace table reference. 

1096 if char == "@": 

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

1098 else: 

1099 escaped_formula.append(char) 

1100 

1101 return ("").join(escaped_formula) 

1102 

1103 # Undecorated version of write_array_formula() and 

1104 # write_dynamic_array_formula(). 

1105 def _write_array_formula( 

1106 self, 

1107 first_row, 

1108 first_col, 

1109 last_row, 

1110 last_col, 

1111 formula, 

1112 cell_format=None, 

1113 value=0, 

1114 atype="static", 

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

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

1117 if first_row > last_row: 

1118 first_row, last_row = last_row, first_row 

1119 if first_col > last_col: 

1120 first_col, last_col = last_col, first_col 

1121 

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

1123 if self._check_dimensions(first_row, first_col): 

1124 return -1 

1125 if self._check_dimensions(last_row, last_col): 

1126 return -1 

1127 

1128 # Define array range 

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

1130 cell_range = xl_rowcol_to_cell(first_row, first_col) 

1131 else: 

1132 cell_range = ( 

1133 xl_rowcol_to_cell(first_row, first_col) 

1134 + ":" 

1135 + xl_rowcol_to_cell(last_row, last_col) 

1136 ) 

1137 

1138 # Modify the formula string, as needed. 

1139 formula = self._prepare_formula(formula) 

1140 

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

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

1143 self._write_single_row(first_row) 

1144 

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

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

1147 formula, cell_format, value, cell_range, atype 

1148 ) 

1149 

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

1151 if not self.constant_memory: 

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

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

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

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

1156 

1157 return 0 

1158 

1159 @convert_cell_args 

1160 def write_datetime( 

1161 self, 

1162 row: int, 

1163 col: int, 

1164 date: datetime.datetime, 

1165 cell_format: Optional[Format] = None, 

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

1167 """ 

1168 Write a date or time to a worksheet cell. 

1169 

1170 Args: 

1171 row: The cell row (zero indexed). 

1172 col: The cell column (zero indexed). 

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

1174 cell_format: A cell Format object. 

1175 

1176 Returns: 

1177 0: Success. 

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

1179 

1180 """ 

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

1182 

1183 # Undecorated version of write_datetime(). 

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

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

1186 if self._check_dimensions(row, col): 

1187 return -1 

1188 

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

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

1191 self._write_single_row(row) 

1192 

1193 # Convert datetime to an Excel date. 

1194 number = self._convert_date_time(date) 

1195 

1196 # Add the default date format. 

1197 if cell_format is None: 

1198 cell_format = self.default_date_format 

1199 

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

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

1202 

1203 return 0 

1204 

1205 @convert_cell_args 

1206 def write_boolean( 

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

1208 ): 

1209 """ 

1210 Write a boolean value to a worksheet cell. 

1211 

1212 Args: 

1213 row: The cell row (zero indexed). 

1214 col: The cell column (zero indexed). 

1215 boolean: Cell data. bool type. 

1216 cell_format: An optional cell Format object. 

1217 

1218 Returns: 

1219 0: Success. 

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

1221 

1222 """ 

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

1224 

1225 # Undecorated version of write_boolean(). 

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

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

1228 if self._check_dimensions(row, col): 

1229 return -1 

1230 

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

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

1233 self._write_single_row(row) 

1234 

1235 if boolean: 

1236 value = 1 

1237 else: 

1238 value = 0 

1239 

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

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

1242 

1243 return 0 

1244 

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

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

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

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

1249 # string limit applies. 

1250 # 

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

1252 # directory urls. 

1253 @convert_cell_args 

1254 def write_url( 

1255 self, 

1256 row: int, 

1257 col: int, 

1258 url: str, 

1259 cell_format: Optional[Format] = None, 

1260 string: Optional[str] = None, 

1261 tip: Optional[str] = None, 

1262 ): 

1263 """ 

1264 Write a hyperlink to a worksheet cell. 

1265 

1266 Args: 

1267 row: The cell row (zero indexed). 

1268 col: The cell column (zero indexed). 

1269 url: Hyperlink url. 

1270 format: An optional cell Format object. 

1271 string: An optional display string for the hyperlink. 

1272 tip: An optional tooltip. 

1273 Returns: 

1274 0: Success. 

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

1276 -2: String longer than 32767 characters. 

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

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

1279 """ 

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

1281 

1282 # Undecorated version of write_url(). 

1283 def _write_url( 

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

1285 ) -> int: 

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

1287 if self._check_dimensions(row, col): 

1288 return -1 

1289 

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

1291 if not isinstance(url, Url): 

1292 

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

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

1295 max_url = self.max_url_length 

1296 if "#" in url: 

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

1298 else: 

1299 url_str = url 

1300 anchor_str = "" 

1301 

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

1303 warn( 

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

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

1306 ) 

1307 return -3 

1308 

1309 url = Url(url) 

1310 

1311 if string is not None: 

1312 url._text = string 

1313 

1314 if tip is not None: 

1315 url._tip = tip 

1316 

1317 # Check the limit of URLs per worksheet. 

1318 self.hlink_count += 1 

1319 

1320 if self.hlink_count > 65530: 

1321 warn( 

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

1323 f"65,530 URLs per worksheet." 

1324 ) 

1325 return -4 

1326 

1327 # Add the default URL format. 

1328 if cell_format is None: 

1329 cell_format = self.default_url_format 

1330 

1331 if not self.ignore_write_string: 

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

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

1334 self._write_single_row(row) 

1335 

1336 # Write the hyperlink string. 

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

1338 

1339 # Store the hyperlink data in a separate structure. 

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

1341 

1342 return 0 

1343 

1344 @convert_cell_args 

1345 def write_rich_string( 

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

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

1348 """ 

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

1350 

1351 Args: 

1352 row: The cell row (zero indexed). 

1353 col: The cell column (zero indexed). 

1354 string_parts: String and format pairs. 

1355 cell_format: Optional Format object. 

1356 

1357 Returns: 

1358 0: Success. 

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

1360 -2: String truncated to 32k characters. 

1361 -3: 2 consecutive formats used. 

1362 -4: Empty string used. 

1363 -5: Insufficient parameters. 

1364 

1365 """ 

1366 

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

1368 

1369 # Undecorated version of write_rich_string(). 

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

1371 tokens = list(args) 

1372 cell_format = None 

1373 string_index = 0 

1374 raw_string = "" 

1375 

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

1377 if self._check_dimensions(row, col): 

1378 return -1 

1379 

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

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

1382 cell_format = tokens.pop() 

1383 

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

1385 # XML to a string. 

1386 fh = StringIO() 

1387 self.rstring = XMLwriter() 

1388 self.rstring._set_filehandle(fh) 

1389 

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

1391 default = Format() 

1392 

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

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

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

1396 fragments = [] 

1397 previous = "format" 

1398 pos = 0 

1399 

1400 if len(tokens) <= 2: 

1401 warn( 

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

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

1404 ) 

1405 return -5 

1406 

1407 for token in tokens: 

1408 if not isinstance(token, Format): 

1409 # Token is a string. 

1410 if previous != "format": 

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

1412 fragments.append(default) 

1413 fragments.append(token) 

1414 else: 

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

1416 fragments.append(token) 

1417 

1418 if token == "": 

1419 warn( 

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

1421 "Ignoring input in write_rich_string()." 

1422 ) 

1423 return -4 

1424 

1425 # Keep track of unformatted string. 

1426 raw_string += token 

1427 previous = "string" 

1428 else: 

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

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

1431 warn( 

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

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

1434 ) 

1435 return -3 

1436 

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

1438 fragments.append(token) 

1439 previous = "format" 

1440 

1441 pos += 1 

1442 

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

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

1445 self.rstring._xml_start_tag("r") 

1446 

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

1448 for token in fragments: 

1449 if isinstance(token, Format): 

1450 # Write the font run. 

1451 self.rstring._xml_start_tag("r") 

1452 self._write_font(token) 

1453 else: 

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

1455 attributes = [] 

1456 

1457 if _preserve_whitespace(token): 

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

1459 

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

1461 self.rstring._xml_end_tag("r") 

1462 

1463 # Read the in-memory string. 

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

1465 

1466 # Check that the string is < 32767 chars. 

1467 if len(raw_string) > self.xls_strmax: 

1468 warn( 

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

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

1471 ) 

1472 return -2 

1473 

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

1475 if not self.constant_memory: 

1476 string_index = self.str_table._get_shared_string_index(string) 

1477 else: 

1478 string_index = string 

1479 

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

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

1482 self._write_single_row(row) 

1483 

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

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

1486 string_index, cell_format, raw_string 

1487 ) 

1488 

1489 return 0 

1490 

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

1492 """ 

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

1494 types. 

1495 

1496 Args: 

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

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

1499 Returns: 

1500 Nothing. 

1501 

1502 """ 

1503 

1504 self.write_handlers[user_type] = user_function 

1505 

1506 @convert_cell_args 

1507 def write_row( 

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

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

1510 """ 

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

1512 

1513 Args: 

1514 row: The cell row (zero indexed). 

1515 col: The cell column (zero indexed). 

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

1517 format: An optional cell Format object. 

1518 Returns: 

1519 0: Success. 

1520 other: Return value of write() method. 

1521 

1522 """ 

1523 for token in data: 

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

1525 if error: 

1526 return error 

1527 col += 1 

1528 

1529 return 0 

1530 

1531 @convert_cell_args 

1532 def write_column( 

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

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

1535 """ 

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

1537 

1538 Args: 

1539 row: The cell row (zero indexed). 

1540 col: The cell column (zero indexed). 

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

1542 format: An optional cell Format object. 

1543 Returns: 

1544 0: Success. 

1545 other: Return value of write() method. 

1546 

1547 """ 

1548 for token in data: 

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

1550 if error: 

1551 return error 

1552 row += 1 

1553 

1554 return 0 

1555 

1556 @convert_cell_args 

1557 def insert_image( 

1558 self, 

1559 row: int, 

1560 col: int, 

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

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

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

1564 """ 

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

1566 

1567 Args: 

1568 row: The cell row (zero indexed). 

1569 col: The cell column (zero indexed). 

1570 source: Filename, BytesIO, or Image object. 

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

1572 

1573 Returns: 

1574 0: Success. 

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

1576 

1577 """ 

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

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

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

1581 return -1 

1582 

1583 # Convert the source to an Image object. 

1584 image = self._image_from_source(source, options) 

1585 

1586 image._row = row 

1587 image._col = col 

1588 image._set_user_options(options) 

1589 

1590 self.images.append(image) 

1591 

1592 return 0 

1593 

1594 @convert_cell_args 

1595 def embed_image( 

1596 self, 

1597 row: int, 

1598 col: int, 

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

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

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

1602 """ 

1603 Embed an image in a worksheet cell. 

1604 

1605 Args: 

1606 row: The cell row (zero indexed). 

1607 col: The cell column (zero indexed). 

1608 source: Filename, BytesIO, or Image object. 

1609 options: Url and data stream of the image. 

1610 

1611 Returns: 

1612 0: Success. 

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

1614 

1615 """ 

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

1617 if self._check_dimensions(row, col): 

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

1619 return -1 

1620 

1621 if options is None: 

1622 options = {} 

1623 

1624 # Convert the source to an Image object. 

1625 image = self._image_from_source(source, options) 

1626 image._set_user_options(options) 

1627 

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

1629 

1630 if image.url: 

1631 if cell_format is None: 

1632 cell_format = self.default_url_format 

1633 

1634 self.ignore_write_string = True 

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

1636 self.ignore_write_string = False 

1637 

1638 image_index = self.embedded_images.get_image_index(image) 

1639 

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

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

1642 

1643 return 0 

1644 

1645 @convert_cell_args 

1646 def insert_textbox( 

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

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

1649 """ 

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

1651 

1652 Args: 

1653 row: The cell row (zero indexed). 

1654 col: The cell column (zero indexed). 

1655 text: The text for the textbox. 

1656 options: Textbox options. 

1657 

1658 Returns: 

1659 0: Success. 

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

1661 

1662 """ 

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

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

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

1666 return -1 

1667 

1668 if text is None: 

1669 text = "" 

1670 

1671 if options is None: 

1672 options = {} 

1673 

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

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

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

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

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

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

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

1681 

1682 self.shapes.append( 

1683 [ 

1684 row, 

1685 col, 

1686 x_offset, 

1687 y_offset, 

1688 x_scale, 

1689 y_scale, 

1690 text, 

1691 anchor, 

1692 options, 

1693 description, 

1694 decorative, 

1695 ] 

1696 ) 

1697 return 0 

1698 

1699 @convert_cell_args 

1700 def insert_chart( 

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

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

1703 """ 

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

1705 

1706 Args: 

1707 row: The cell row (zero indexed). 

1708 col: The cell column (zero indexed). 

1709 chart: Chart object. 

1710 options: Position and scale of the chart. 

1711 

1712 Returns: 

1713 0: Success. 

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

1715 

1716 """ 

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

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

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

1720 return -1 

1721 

1722 if options is None: 

1723 options = {} 

1724 

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

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

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

1728 return -2 

1729 

1730 chart.already_inserted = True 

1731 

1732 if chart.combined: 

1733 chart.combined.already_inserted = True 

1734 

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

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

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

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

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

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

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

1742 

1743 # Allow Chart to override the scale and offset. 

1744 if chart.x_scale != 1: 

1745 x_scale = chart.x_scale 

1746 

1747 if chart.y_scale != 1: 

1748 y_scale = chart.y_scale 

1749 

1750 if chart.x_offset: 

1751 x_offset = chart.x_offset 

1752 

1753 if chart.y_offset: 

1754 y_offset = chart.y_offset 

1755 

1756 self.charts.append( 

1757 [ 

1758 row, 

1759 col, 

1760 chart, 

1761 x_offset, 

1762 y_offset, 

1763 x_scale, 

1764 y_scale, 

1765 anchor, 

1766 description, 

1767 decorative, 

1768 ] 

1769 ) 

1770 return 0 

1771 

1772 @convert_cell_args 

1773 def write_comment( 

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

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

1776 """ 

1777 Write a comment to a worksheet cell. 

1778 

1779 Args: 

1780 row: The cell row (zero indexed). 

1781 col: The cell column (zero indexed). 

1782 comment: Cell comment. Str. 

1783 options: Comment formatting options. 

1784 

1785 Returns: 

1786 0: Success. 

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

1788 -2: String longer than 32k characters. 

1789 

1790 """ 

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

1792 if self._check_dimensions(row, col): 

1793 return -1 

1794 

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

1796 if len(comment) > self.xls_strmax: 

1797 return -2 

1798 

1799 self.has_vml = True 

1800 self.has_comments = True 

1801 

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

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

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

1805 

1806 return 0 

1807 

1808 def show_comments(self) -> None: 

1809 """ 

1810 Make any comments in the worksheet visible. 

1811 

1812 Args: 

1813 None. 

1814 

1815 Returns: 

1816 Nothing. 

1817 

1818 """ 

1819 self.comments_visible = True 

1820 

1821 def set_background( 

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

1823 ) -> Literal[0]: 

1824 """ 

1825 Set a background image for a worksheet. 

1826 

1827 Args: 

1828 source: Filename, BytesIO, or Image object. 

1829 is_byte_stream: Deprecated. Use a BytesIO object instead. 

1830 

1831 Returns: 

1832 0: Success. 

1833 

1834 """ 

1835 # Convert the source to an Image object. 

1836 image = self._image_from_source(source) 

1837 

1838 self.background_image = image 

1839 

1840 if is_byte_stream: 

1841 warn( 

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

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

1844 ) 

1845 

1846 return 0 

1847 

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

1849 """ 

1850 Set the default author of the cell comments. 

1851 

1852 Args: 

1853 author: Comment author name. String. 

1854 

1855 Returns: 

1856 Nothing. 

1857 

1858 """ 

1859 self.comments_author = author 

1860 

1861 def get_name(self): 

1862 """ 

1863 Retrieve the worksheet name. 

1864 

1865 Args: 

1866 None. 

1867 

1868 Returns: 

1869 Nothing. 

1870 

1871 """ 

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

1873 return self.name 

1874 

1875 def activate(self) -> None: 

1876 """ 

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

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

1879 

1880 Note: An active worksheet cannot be hidden. 

1881 

1882 Args: 

1883 None. 

1884 

1885 Returns: 

1886 Nothing. 

1887 

1888 """ 

1889 self.hidden = 0 

1890 self.selected = 1 

1891 self.worksheet_meta.activesheet = self.index 

1892 

1893 def select(self) -> None: 

1894 """ 

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

1896 has its tab highlighted. 

1897 

1898 Note: A selected worksheet cannot be hidden. 

1899 

1900 Args: 

1901 None. 

1902 

1903 Returns: 

1904 Nothing. 

1905 

1906 """ 

1907 self.selected = 1 

1908 self.hidden = 0 

1909 

1910 def hide(self) -> None: 

1911 """ 

1912 Hide the current worksheet. 

1913 

1914 Args: 

1915 None. 

1916 

1917 Returns: 

1918 Nothing. 

1919 

1920 """ 

1921 self.hidden = 1 

1922 

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

1924 self.selected = 0 

1925 

1926 def very_hidden(self) -> None: 

1927 """ 

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

1929 

1930 Args: 

1931 None. 

1932 

1933 Returns: 

1934 Nothing. 

1935 

1936 """ 

1937 self.hidden = 2 

1938 

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

1940 self.selected = 0 

1941 

1942 def set_first_sheet(self) -> None: 

1943 """ 

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

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

1946 worksheet is not visible on the screen. 

1947 

1948 Note: A selected worksheet cannot be hidden. 

1949 

1950 Args: 

1951 None. 

1952 

1953 Returns: 

1954 Nothing. 

1955 

1956 """ 

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

1958 self.worksheet_meta.firstsheet = self.index 

1959 

1960 @convert_column_args 

1961 def set_column( 

1962 self, 

1963 first_col: int, 

1964 last_col: int, 

1965 width: Optional[float] = None, 

1966 cell_format: Optional[Format] = None, 

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

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

1969 """ 

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

1971 range of columns. 

1972 

1973 Args: 

1974 first_col: First column (zero-indexed). 

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

1976 width: Column width. (optional). 

1977 cell_format: Column cell_format. (optional). 

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

1979 

1980 Returns: 

1981 0: Success. 

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

1983 

1984 """ 

1985 if options is None: 

1986 options = {} 

1987 

1988 # Ensure 2nd col is larger than first. 

1989 if first_col > last_col: 

1990 (first_col, last_col) = (last_col, first_col) 

1991 

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

1993 ignore_row = True 

1994 

1995 # Set optional column values. 

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

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

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

1999 

2000 # Store the column dimension only in some conditions. 

2001 if cell_format or (width and hidden): 

2002 ignore_col = False 

2003 else: 

2004 ignore_col = True 

2005 

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

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

2008 return -1 

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

2010 return -1 

2011 

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

2013 level = max(level, 0) 

2014 level = min(level, 7) 

2015 

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

2017 

2018 # Store the column data. 

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

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

2021 

2022 # Store the column change to allow optimizations. 

2023 self.col_size_changed = True 

2024 

2025 return 0 

2026 

2027 @convert_column_args 

2028 def set_column_pixels( 

2029 self, 

2030 first_col: int, 

2031 last_col: int, 

2032 width: Optional[float] = None, 

2033 cell_format: Optional[Format] = None, 

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

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

2036 """ 

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

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

2039 

2040 Args: 

2041 first_col: First column (zero-indexed). 

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

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

2044 cell_format: Column cell_format. (optional). 

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

2046 

2047 Returns: 

2048 0: Success. 

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

2050 

2051 """ 

2052 if width is not None: 

2053 width = self._pixels_to_width(width) 

2054 

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

2056 

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

2058 """ 

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

2060 

2061 Args: 

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

2063 

2064 Returns: 

2065 Nothing. 

2066 

2067 """ 

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

2069 if self.constant_memory: 

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

2071 return 

2072 

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

2074 if self.dim_rowmax is None: 

2075 return 

2076 

2077 # Store the max pixel width for each column. 

2078 col_width_max = {} 

2079 

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

2081 # but limit it to the Excel max limit. 

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

2083 

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

2085 # the string id back to the original string. 

2086 strings = sorted( 

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

2088 ) 

2089 

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

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

2092 continue 

2093 

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

2095 if col_num in self.table[row_num]: 

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

2097 cell_type = cell.__class__.__name__ 

2098 length = 0 

2099 

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

2101 # Handle strings and rich strings. 

2102 # 

2103 # For standard shared strings we do a reverse lookup 

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

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

2106 # split multi-line strings and handle each part 

2107 # separately. 

2108 if cell_type == "String": 

2109 string_id = cell.string 

2110 string = strings[string_id] 

2111 else: 

2112 string = cell.raw_string 

2113 

2114 if "\n" not in string: 

2115 # Single line string. 

2116 length = xl_pixel_width(string) 

2117 else: 

2118 # Handle multi-line strings. 

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

2120 seg_length = xl_pixel_width(string) 

2121 length = max(length, seg_length) 

2122 

2123 elif cell_type == "Number": 

2124 # Handle numbers. 

2125 # 

2126 # We use a workaround/optimization for numbers since 

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

2128 # slightly greater width for the decimal place and 

2129 # minus sign but only by a few pixels and 

2130 # over-estimation is okay. 

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

2132 

2133 elif cell_type == "Datetime": 

2134 # Handle dates. 

2135 # 

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

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

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

2139 length = self.default_date_pixels 

2140 

2141 elif cell_type == "Boolean": 

2142 # Handle boolean values. 

2143 # 

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

2145 if cell.boolean: 

2146 length = 31 

2147 else: 

2148 length = 36 

2149 

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

2151 # Handle formulas. 

2152 # 

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

2154 # non-zero value. 

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

2156 if cell.value > 0: 

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

2158 

2159 elif isinstance(cell.value, str): 

2160 length = xl_pixel_width(cell.value) 

2161 

2162 elif isinstance(cell.value, bool): 

2163 if cell.value: 

2164 length = 31 

2165 else: 

2166 length = 36 

2167 

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

2169 # additional 16 pixels for the dropdown arrow. 

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

2171 length += 16 

2172 

2173 # Add the string length to the lookup table. 

2174 width_max = col_width_max.get(col_num, 0) 

2175 if length > width_max: 

2176 col_width_max[col_num] = length 

2177 

2178 # Apply the width to the column. 

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

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

2181 # additional padding of 7 pixels, like Excel. 

2182 width = self._pixels_to_width(pixel_width + 7) 

2183 

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

2185 width = min(width, max_width) 

2186 

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

2188 if self.col_info.get(col_num): 

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

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

2191 # to pre-load a minimum col width. 

2192 col_info = self.col_info.get(col_num) 

2193 user_width = col_info[0] 

2194 hidden = col_info[5] 

2195 if user_width is not None and not hidden: 

2196 # Col info is user defined. 

2197 if width > user_width: 

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

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

2200 else: 

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

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

2203 else: 

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

2205 

2206 def set_row( 

2207 self, 

2208 row: int, 

2209 height: Optional[float] = None, 

2210 cell_format: Optional[Format] = None, 

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

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

2213 """ 

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

2215 

2216 Args: 

2217 row: Row number (zero-indexed). 

2218 height: Row height. (optional). 

2219 cell_format: Row cell_format. (optional). 

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

2221 

2222 Returns: 

2223 0: Success. 

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

2225 

2226 """ 

2227 if options is None: 

2228 options = {} 

2229 

2230 # Use minimum col in _check_dimensions(). 

2231 if self.dim_colmin is not None: 

2232 min_col = self.dim_colmin 

2233 else: 

2234 min_col = 0 

2235 

2236 # Check that row is valid. 

2237 if self._check_dimensions(row, min_col): 

2238 return -1 

2239 

2240 if height is None: 

2241 height = self.default_row_height 

2242 

2243 # Set optional row values. 

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

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

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

2247 

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

2249 if height == 0: 

2250 hidden = 1 

2251 height = self.default_row_height 

2252 

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

2254 level = max(level, 0) 

2255 level = min(level, 7) 

2256 

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

2258 

2259 # Store the row properties. 

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

2261 

2262 # Store the row change to allow optimizations. 

2263 self.row_size_changed = True 

2264 

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

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

2267 

2268 return 0 

2269 

2270 def set_row_pixels( 

2271 self, 

2272 row: int, 

2273 height: Optional[float] = None, 

2274 cell_format: Optional[Format] = None, 

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

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

2277 """ 

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

2279 

2280 Args: 

2281 row: Row number (zero-indexed). 

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

2283 cell_format: Row cell_format. (optional). 

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

2285 

2286 Returns: 

2287 0: Success. 

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

2289 

2290 """ 

2291 if height is not None: 

2292 height = self._pixels_to_height(height) 

2293 

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

2295 

2296 def set_default_row( 

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

2298 ) -> None: 

2299 """ 

2300 Set the default row properties. 

2301 

2302 Args: 

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

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

2305 

2306 Returns: 

2307 Nothing. 

2308 

2309 """ 

2310 if height is None: 

2311 height = self.default_row_height 

2312 

2313 if height != self.original_row_height: 

2314 # Store the row change to allow optimizations. 

2315 self.row_size_changed = True 

2316 self.default_row_height = height 

2317 

2318 if hide_unused_rows: 

2319 self.default_row_zeroed = 1 

2320 

2321 @convert_range_args 

2322 def merge_range( 

2323 self, 

2324 first_row: int, 

2325 first_col: int, 

2326 last_row: int, 

2327 last_col: int, 

2328 data: Any, 

2329 cell_format: Optional[Format] = None, 

2330 ) -> int: 

2331 """ 

2332 Merge a range of cells. 

2333 

2334 Args: 

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

2336 first_col: The first column of the cell range. 

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

2338 last_col: The last column of the cell range. 

2339 data: Cell data. 

2340 cell_format: Cell Format object. 

2341 

2342 Returns: 

2343 0: Success. 

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

2345 other: Return value of write(). 

2346 

2347 """ 

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

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

2350 

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

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

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

2354 return -1 

2355 

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

2357 if first_row > last_row: 

2358 (first_row, last_row) = (last_row, first_row) 

2359 if first_col > last_col: 

2360 (first_col, last_col) = (last_col, first_col) 

2361 

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

2363 if self._check_dimensions(first_row, first_col): 

2364 return -1 

2365 if self._check_dimensions(last_row, last_col): 

2366 return -1 

2367 

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

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

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

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

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

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

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

2375 raise OverlappingRange( 

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

2377 f"range '{previous_range}'." 

2378 ) 

2379 

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

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

2382 raise OverlappingRange( 

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

2384 f"range '{previous_range}'." 

2385 ) 

2386 

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

2388 

2389 # Store the merge range. 

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

2391 

2392 # Write the first cell 

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

2394 

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

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

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

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

2399 continue 

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

2401 

2402 return 0 

2403 

2404 @convert_range_args 

2405 def autofilter( 

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

2407 ) -> None: 

2408 """ 

2409 Set the autofilter area in the worksheet. 

2410 

2411 Args: 

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

2413 first_col: The first column of the cell range. 

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

2415 last_col: The last column of the cell range. 

2416 

2417 Returns: 

2418 Nothing. 

2419 

2420 """ 

2421 # Reverse max and min values if necessary. 

2422 if last_row < first_row: 

2423 (first_row, last_row) = (last_row, first_row) 

2424 if last_col < first_col: 

2425 (first_col, last_col) = (last_col, first_col) 

2426 

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

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

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

2430 

2431 self.autofilter_area = area 

2432 self.autofilter_ref = ref 

2433 self.filter_range = [first_col, last_col] 

2434 

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

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

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

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

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

2440 if filter_type == "table": 

2441 raise OverlappingRange( 

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

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

2444 ) 

2445 

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

2447 

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

2449 """ 

2450 Set the column filter criteria. 

2451 

2452 Args: 

2453 col: Filter column (zero-indexed). 

2454 criteria: Filter criteria. 

2455 

2456 Returns: 

2457 Nothing. 

2458 

2459 """ 

2460 if not self.autofilter_area: 

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

2462 return 

2463 

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

2465 try: 

2466 int(col) 

2467 except ValueError: 

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

2469 col_letter = col 

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

2471 

2472 if col >= self.xls_colmax: 

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

2474 return 

2475 

2476 (col_first, col_last) = self.filter_range 

2477 

2478 # Reject column if it is outside filter range. 

2479 if col < col_first or col > col_last: 

2480 warn( 

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

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

2483 ) 

2484 return 

2485 

2486 tokens = self._extract_filter_tokens(criteria) 

2487 

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

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

2490 

2491 tokens = self._parse_filter_expression(criteria, tokens) 

2492 

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

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

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

2496 # Single equality. 

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

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

2499 # Double equality with "or" operator. 

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

2501 else: 

2502 # Non default custom filter. 

2503 self.filter_cols[col] = tokens 

2504 self.filter_type[col] = 0 

2505 

2506 self.filter_on = 1 

2507 

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

2509 """ 

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

2511 

2512 Args: 

2513 col: Filter column (zero-indexed). 

2514 filters: List of filter criteria to match. 

2515 

2516 Returns: 

2517 Nothing. 

2518 

2519 """ 

2520 if not self.autofilter_area: 

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

2522 return 

2523 

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

2525 try: 

2526 int(col) 

2527 except ValueError: 

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

2529 col_letter = col 

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

2531 

2532 if col >= self.xls_colmax: 

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

2534 return 

2535 

2536 (col_first, col_last) = self.filter_range 

2537 

2538 # Reject column if it is outside filter range. 

2539 if col < col_first or col > col_last: 

2540 warn( 

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

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

2543 ) 

2544 return 

2545 

2546 self.filter_cols[col] = filters 

2547 self.filter_type[col] = 1 

2548 self.filter_on = 1 

2549 

2550 @convert_range_args 

2551 def data_validation( 

2552 self, 

2553 first_row: int, 

2554 first_col: int, 

2555 last_row: int, 

2556 last_col: int, 

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

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

2559 """ 

2560 Add a data validation to a worksheet. 

2561 

2562 Args: 

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

2564 first_col: The first column of the cell range. 

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

2566 last_col: The last column of the cell range. 

2567 options: Data validation options. 

2568 

2569 Returns: 

2570 0: Success. 

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

2572 -2: Incorrect parameter or option. 

2573 """ 

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

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

2576 return -1 

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

2578 return -1 

2579 

2580 if options is None: 

2581 options = {} 

2582 else: 

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

2584 options = options.copy() 

2585 

2586 # Valid input parameters. 

2587 valid_parameters = { 

2588 "validate", 

2589 "criteria", 

2590 "value", 

2591 "source", 

2592 "minimum", 

2593 "maximum", 

2594 "ignore_blank", 

2595 "dropdown", 

2596 "show_input", 

2597 "input_title", 

2598 "input_message", 

2599 "show_error", 

2600 "error_title", 

2601 "error_message", 

2602 "error_type", 

2603 "other_cells", 

2604 "multi_range", 

2605 } 

2606 

2607 # Check for valid input parameters. 

2608 for param_key in options.keys(): 

2609 if param_key not in valid_parameters: 

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

2611 return -2 

2612 

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

2614 if "source" in options: 

2615 options["value"] = options["source"] 

2616 if "minimum" in options: 

2617 options["value"] = options["minimum"] 

2618 

2619 # 'validate' is a required parameter. 

2620 if "validate" not in options: 

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

2622 return -2 

2623 

2624 # List of valid validation types. 

2625 valid_types = { 

2626 "any": "none", 

2627 "any value": "none", 

2628 "whole number": "whole", 

2629 "whole": "whole", 

2630 "integer": "whole", 

2631 "decimal": "decimal", 

2632 "list": "list", 

2633 "date": "date", 

2634 "time": "time", 

2635 "text length": "textLength", 

2636 "length": "textLength", 

2637 "custom": "custom", 

2638 } 

2639 

2640 # Check for valid validation types. 

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

2642 warn( 

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

2644 f"'validate' in data_validation()" 

2645 ) 

2646 return -2 

2647 

2648 options["validate"] = valid_types[options["validate"]] 

2649 

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

2651 # input messages to display. 

2652 if ( 

2653 options["validate"] == "none" 

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

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

2656 ): 

2657 return -2 

2658 

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

2660 # a default of 'between'. 

2661 if ( 

2662 options["validate"] == "none" 

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

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

2665 ): 

2666 options["criteria"] = "between" 

2667 options["maximum"] = None 

2668 

2669 # 'criteria' is a required parameter. 

2670 if "criteria" not in options: 

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

2672 return -2 

2673 

2674 # Valid criteria types. 

2675 criteria_types = { 

2676 "between": "between", 

2677 "not between": "notBetween", 

2678 "equal to": "equal", 

2679 "=": "equal", 

2680 "==": "equal", 

2681 "not equal to": "notEqual", 

2682 "!=": "notEqual", 

2683 "<>": "notEqual", 

2684 "greater than": "greaterThan", 

2685 ">": "greaterThan", 

2686 "less than": "lessThan", 

2687 "<": "lessThan", 

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

2689 ">=": "greaterThanOrEqual", 

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

2691 "<=": "lessThanOrEqual", 

2692 } 

2693 

2694 # Check for valid criteria types. 

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

2696 warn( 

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

2698 f"'criteria' in data_validation()" 

2699 ) 

2700 return -2 

2701 

2702 options["criteria"] = criteria_types[options["criteria"]] 

2703 

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

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

2706 if "maximum" not in options: 

2707 warn( 

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

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

2710 ) 

2711 return -2 

2712 else: 

2713 options["maximum"] = None 

2714 

2715 # Valid error dialog types. 

2716 error_types = { 

2717 "stop": 0, 

2718 "warning": 1, 

2719 "information": 2, 

2720 } 

2721 

2722 # Check for valid error dialog types. 

2723 if "error_type" not in options: 

2724 options["error_type"] = 0 

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

2726 warn( 

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

2728 f"for parameter 'error_type'." 

2729 ) 

2730 return -2 

2731 else: 

2732 options["error_type"] = error_types[options["error_type"]] 

2733 

2734 # Convert date/times value if required. 

2735 if ( 

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

2737 and options["value"] 

2738 and _supported_datetime(options["value"]) 

2739 ): 

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

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

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

2743 

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

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

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

2747 

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

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

2750 warn( 

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

2752 f"exceeds Excel's limit of 32" 

2753 ) 

2754 return -2 

2755 

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

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

2758 warn( 

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

2760 f"exceeds Excel's limit of 32" 

2761 ) 

2762 return -2 

2763 

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

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

2766 warn( 

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

2768 f"exceeds Excel's limit of 255" 

2769 ) 

2770 return -2 

2771 

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

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

2774 warn( 

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

2776 f"exceeds Excel's limit of 255" 

2777 ) 

2778 return -2 

2779 

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

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

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

2783 if len(formula) > 255: 

2784 warn( 

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

2786 f"255, use a formula range instead" 

2787 ) 

2788 return -2 

2789 

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

2791 if "ignore_blank" not in options: 

2792 options["ignore_blank"] = 1 

2793 if "dropdown" not in options: 

2794 options["dropdown"] = 1 

2795 if "show_input" not in options: 

2796 options["show_input"] = 1 

2797 if "show_error" not in options: 

2798 options["show_error"] = 1 

2799 

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

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

2802 

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

2804 if "other_cells" in options: 

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

2806 

2807 # Override with user defined multiple range if provided. 

2808 if "multi_range" in options: 

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

2810 

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

2812 self.validations.append(options) 

2813 

2814 return 0 

2815 

2816 @convert_range_args 

2817 def conditional_format( 

2818 self, 

2819 first_row: int, 

2820 first_col: int, 

2821 last_row: int, 

2822 last_col: int, 

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

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

2825 """ 

2826 Add a conditional format to a worksheet. 

2827 

2828 Args: 

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

2830 first_col: The first column of the cell range. 

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

2832 last_col: The last column of the cell range. 

2833 options: Conditional format options. 

2834 

2835 Returns: 

2836 0: Success. 

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

2838 -2: Incorrect parameter or option. 

2839 """ 

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

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

2842 return -1 

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

2844 return -1 

2845 

2846 if options is None: 

2847 options = {} 

2848 else: 

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

2850 options = options.copy() 

2851 

2852 # Valid input parameters. 

2853 valid_parameter = { 

2854 "type", 

2855 "format", 

2856 "criteria", 

2857 "value", 

2858 "minimum", 

2859 "maximum", 

2860 "stop_if_true", 

2861 "min_type", 

2862 "mid_type", 

2863 "max_type", 

2864 "min_value", 

2865 "mid_value", 

2866 "max_value", 

2867 "min_color", 

2868 "mid_color", 

2869 "max_color", 

2870 "min_length", 

2871 "max_length", 

2872 "multi_range", 

2873 "bar_color", 

2874 "bar_negative_color", 

2875 "bar_negative_color_same", 

2876 "bar_solid", 

2877 "bar_border_color", 

2878 "bar_negative_border_color", 

2879 "bar_negative_border_color_same", 

2880 "bar_no_border", 

2881 "bar_direction", 

2882 "bar_axis_position", 

2883 "bar_axis_color", 

2884 "bar_only", 

2885 "data_bar_2010", 

2886 "icon_style", 

2887 "reverse_icons", 

2888 "icons_only", 

2889 "icons", 

2890 } 

2891 

2892 # Check for valid input parameters. 

2893 for param_key in options.keys(): 

2894 if param_key not in valid_parameter: 

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

2896 return -2 

2897 

2898 # 'type' is a required parameter. 

2899 if "type" not in options: 

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

2901 return -2 

2902 

2903 # Valid types. 

2904 valid_type = { 

2905 "cell": "cellIs", 

2906 "date": "date", 

2907 "time": "time", 

2908 "average": "aboveAverage", 

2909 "duplicate": "duplicateValues", 

2910 "unique": "uniqueValues", 

2911 "top": "top10", 

2912 "bottom": "top10", 

2913 "text": "text", 

2914 "time_period": "timePeriod", 

2915 "blanks": "containsBlanks", 

2916 "no_blanks": "notContainsBlanks", 

2917 "errors": "containsErrors", 

2918 "no_errors": "notContainsErrors", 

2919 "2_color_scale": "2_color_scale", 

2920 "3_color_scale": "3_color_scale", 

2921 "data_bar": "dataBar", 

2922 "formula": "expression", 

2923 "icon_set": "iconSet", 

2924 } 

2925 

2926 # Check for valid types. 

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

2928 warn( 

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

2930 f"in conditional_format()" 

2931 ) 

2932 return -2 

2933 

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

2935 options["direction"] = "bottom" 

2936 options["type"] = valid_type[options["type"]] 

2937 

2938 # Valid criteria types. 

2939 criteria_type = { 

2940 "between": "between", 

2941 "not between": "notBetween", 

2942 "equal to": "equal", 

2943 "=": "equal", 

2944 "==": "equal", 

2945 "not equal to": "notEqual", 

2946 "!=": "notEqual", 

2947 "<>": "notEqual", 

2948 "greater than": "greaterThan", 

2949 ">": "greaterThan", 

2950 "less than": "lessThan", 

2951 "<": "lessThan", 

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

2953 ">=": "greaterThanOrEqual", 

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

2955 "<=": "lessThanOrEqual", 

2956 "containing": "containsText", 

2957 "not containing": "notContains", 

2958 "begins with": "beginsWith", 

2959 "ends with": "endsWith", 

2960 "yesterday": "yesterday", 

2961 "today": "today", 

2962 "last 7 days": "last7Days", 

2963 "last week": "lastWeek", 

2964 "this week": "thisWeek", 

2965 "next week": "nextWeek", 

2966 "last month": "lastMonth", 

2967 "this month": "thisMonth", 

2968 "next month": "nextMonth", 

2969 # For legacy, but incorrect, support. 

2970 "continue week": "nextWeek", 

2971 "continue month": "nextMonth", 

2972 } 

2973 

2974 # Check for valid criteria types. 

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

2976 options["criteria"] = criteria_type[options["criteria"]] 

2977 

2978 # Convert boolean values if required. 

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

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

2981 

2982 # Convert date/times value if required. 

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

2984 options["type"] = "cellIs" 

2985 

2986 if "value" in options: 

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

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

2989 return -2 

2990 

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

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

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

2994 

2995 if "minimum" in options: 

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

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

2998 return -2 

2999 

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

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

3002 

3003 if "maximum" in options: 

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

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

3006 return -2 

3007 

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

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

3010 

3011 # Valid icon styles. 

3012 valid_icons = { 

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

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

3015 "3_traffic_lights_rimmed": "3TrafficLights2", # 3 

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

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

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

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

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

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

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

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

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

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

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

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

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

3029 "5_ratings": "5Rating", 

3030 } # 17 

3031 

3032 # Set the icon set properties. 

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

3034 # An icon_set must have an icon style. 

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

3036 warn( 

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

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

3039 ) 

3040 return -3 

3041 

3042 # Check for valid icon styles. 

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

3044 warn( 

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

3046 f"in conditional_format()." 

3047 ) 

3048 return -2 

3049 

3050 options["icon_style"] = valid_icons[options["icon_style"]] 

3051 

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

3053 options["total_icons"] = 3 

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

3055 options["total_icons"] = 4 

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

3057 options["total_icons"] = 5 

3058 

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

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

3061 ) 

3062 

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

3064 if first_row > last_row: 

3065 first_row, last_row = last_row, first_row 

3066 

3067 if first_col > last_col: 

3068 first_col, last_col = last_col, first_col 

3069 

3070 # Set the formatting range. 

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

3072 start_cell = xl_rowcol_to_cell(first_row, first_col) 

3073 

3074 # Override with user defined multiple range if provided. 

3075 if "multi_range" in options: 

3076 cell_range = options["multi_range"] 

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

3078 

3079 # Get the dxf format index. 

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

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

3082 

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

3084 options["priority"] = self.dxf_priority 

3085 self.dxf_priority += 1 

3086 

3087 # Check for 2010 style data_bar parameters. 

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

3089 if ( 

3090 self.use_data_bars_2010 

3091 or options.get("data_bar_2010") 

3092 or options.get("bar_solid") 

3093 or options.get("bar_border_color") 

3094 or options.get("bar_negative_color") 

3095 or options.get("bar_negative_color_same") 

3096 or options.get("bar_negative_border_color") 

3097 or options.get("bar_negative_border_color_same") 

3098 or options.get("bar_no_border") 

3099 or options.get("bar_axis_position") 

3100 or options.get("bar_axis_color") 

3101 or options.get("bar_direction") 

3102 ): 

3103 options["is_data_bar_2010"] = True 

3104 

3105 # Special handling of text criteria. 

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

3107 value = options["value"] 

3108 length = len(value) 

3109 criteria = options["criteria"] 

3110 

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

3112 options["type"] = "containsText" 

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

3114 elif options["criteria"] == "notContains": 

3115 options["type"] = "notContainsText" 

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

3117 elif options["criteria"] == "beginsWith": 

3118 options["type"] = "beginsWith" 

3119 options["formula"] = f'LEFT({start_cell},{length})="{value}"' 

3120 elif options["criteria"] == "endsWith": 

3121 options["type"] = "endsWith" 

3122 options["formula"] = f'RIGHT({start_cell},{length})="{value}"' 

3123 else: 

3124 warn(f"Invalid text criteria '{criteria}' in conditional_format()") 

3125 

3126 # Special handling of time time_period criteria. 

3127 if options["type"] == "timePeriod": 

3128 if options["criteria"] == "yesterday": 

3129 options["formula"] = f"FLOOR({start_cell},1)=TODAY()-1" 

3130 

3131 elif options["criteria"] == "today": 

3132 options["formula"] = f"FLOOR({start_cell},1)=TODAY()" 

3133 

3134 elif options["criteria"] == "tomorrow": 

3135 options["formula"] = f"FLOOR({start_cell},1)=TODAY()+1" 

3136 

3137 # fmt: off 

3138 elif options["criteria"] == "last7Days": 

3139 options["formula"] = ( 

3140 f"AND(TODAY()-FLOOR({start_cell},1)<=6," 

3141 f"FLOOR({start_cell},1)<=TODAY())" 

3142 ) 

3143 # fmt: on 

3144 

3145 elif options["criteria"] == "lastWeek": 

3146 options["formula"] = ( 

3147 f"AND(TODAY()-ROUNDDOWN({start_cell},0)>=(WEEKDAY(TODAY()))," 

3148 f"TODAY()-ROUNDDOWN({start_cell},0)<(WEEKDAY(TODAY())+7))" 

3149 ) 

3150 

3151 elif options["criteria"] == "thisWeek": 

3152 options["formula"] = ( 

3153 f"AND(TODAY()-ROUNDDOWN({start_cell},0)<=WEEKDAY(TODAY())-1," 

3154 f"ROUNDDOWN({start_cell},0)-TODAY()<=7-WEEKDAY(TODAY()))" 

3155 ) 

3156 

3157 elif options["criteria"] == "nextWeek": 

3158 options["formula"] = ( 

3159 f"AND(ROUNDDOWN({start_cell},0)-TODAY()>(7-WEEKDAY(TODAY()))," 

3160 f"ROUNDDOWN({start_cell},0)-TODAY()<(15-WEEKDAY(TODAY())))" 

3161 ) 

3162 

3163 elif options["criteria"] == "lastMonth": 

3164 options["formula"] = ( 

3165 f"AND(MONTH({start_cell})=MONTH(TODAY())-1," 

3166 f"OR(YEAR({start_cell})=YEAR(" 

3167 f"TODAY()),AND(MONTH({start_cell})=1,YEAR(A1)=YEAR(TODAY())-1)))" 

3168 ) 

3169 

3170 # fmt: off 

3171 elif options["criteria"] == "thisMonth": 

3172 options["formula"] = ( 

3173 f"AND(MONTH({start_cell})=MONTH(TODAY())," 

3174 f"YEAR({start_cell})=YEAR(TODAY()))" 

3175 ) 

3176 # fmt: on 

3177 

3178 elif options["criteria"] == "nextMonth": 

3179 options["formula"] = ( 

3180 f"AND(MONTH({start_cell})=MONTH(TODAY())+1," 

3181 f"OR(YEAR({start_cell})=YEAR(" 

3182 f"TODAY()),AND(MONTH({start_cell})=12," 

3183 f"YEAR({start_cell})=YEAR(TODAY())+1)))" 

3184 ) 

3185 

3186 else: 

3187 warn( 

3188 f"Invalid time_period criteria '{options['criteria']}' " 

3189 f"in conditional_format()" 

3190 ) 

3191 

3192 # Special handling of blanks/error types. 

3193 if options["type"] == "containsBlanks": 

3194 options["formula"] = f"LEN(TRIM({start_cell}))=0" 

3195 

3196 if options["type"] == "notContainsBlanks": 

3197 options["formula"] = f"LEN(TRIM({start_cell}))>0" 

3198 

3199 if options["type"] == "containsErrors": 

3200 options["formula"] = f"ISERROR({start_cell})" 

3201 

3202 if options["type"] == "notContainsErrors": 

3203 options["formula"] = f"NOT(ISERROR({start_cell}))" 

3204 

3205 # Special handling for 2 color scale. 

3206 if options["type"] == "2_color_scale": 

3207 options["type"] = "colorScale" 

3208 

3209 # Color scales don't use any additional formatting. 

3210 options["format"] = None 

3211 

3212 # Turn off 3 color parameters. 

3213 options["mid_type"] = None 

3214 options["mid_color"] = None 

3215 

3216 options.setdefault("min_type", "min") 

3217 options.setdefault("max_type", "max") 

3218 options.setdefault("min_value", 0) 

3219 options.setdefault("max_value", 0) 

3220 options.setdefault("min_color", Color("#FF7128")) 

3221 options.setdefault("max_color", Color("#FFEF9C")) 

3222 

3223 options["min_color"] = Color._from_value(options["min_color"]) 

3224 options["max_color"] = Color._from_value(options["max_color"]) 

3225 

3226 # Special handling for 3 color scale. 

3227 if options["type"] == "3_color_scale": 

3228 options["type"] = "colorScale" 

3229 

3230 # Color scales don't use any additional formatting. 

3231 options["format"] = None 

3232 

3233 options.setdefault("min_type", "min") 

3234 options.setdefault("mid_type", "percentile") 

3235 options.setdefault("max_type", "max") 

3236 options.setdefault("min_value", 0) 

3237 options.setdefault("max_value", 0) 

3238 options.setdefault("min_color", Color("#F8696B")) 

3239 options.setdefault("mid_color", Color("#FFEB84")) 

3240 options.setdefault("max_color", Color("#63BE7B")) 

3241 

3242 options["min_color"] = Color._from_value(options["min_color"]) 

3243 options["mid_color"] = Color._from_value(options["mid_color"]) 

3244 options["max_color"] = Color._from_value(options["max_color"]) 

3245 

3246 # Set a default mid value. 

3247 if "mid_value" not in options: 

3248 options["mid_value"] = 50 

3249 

3250 # Special handling for data bar. 

3251 if options["type"] == "dataBar": 

3252 # Color scales don't use any additional formatting. 

3253 options["format"] = None 

3254 

3255 if not options.get("min_type"): 

3256 options["min_type"] = "min" 

3257 options["x14_min_type"] = "autoMin" 

3258 else: 

3259 options["x14_min_type"] = options["min_type"] 

3260 

3261 if not options.get("max_type"): 

3262 options["max_type"] = "max" 

3263 options["x14_max_type"] = "autoMax" 

3264 else: 

3265 options["x14_max_type"] = options["max_type"] 

3266 

3267 options.setdefault("min_value", 0) 

3268 options.setdefault("max_value", 0) 

3269 options.setdefault("bar_color", Color("#638EC6")) 

3270 options.setdefault("bar_border_color", options["bar_color"]) 

3271 options.setdefault("bar_only", False) 

3272 options.setdefault("bar_no_border", False) 

3273 options.setdefault("bar_solid", False) 

3274 options.setdefault("bar_direction", "") 

3275 options.setdefault("bar_negative_color", Color("#FF0000")) 

3276 options.setdefault("bar_negative_border_color", Color("#FF0000")) 

3277 options.setdefault("bar_negative_color_same", False) 

3278 options.setdefault("bar_negative_border_color_same", False) 

3279 options.setdefault("bar_axis_position", "") 

3280 options.setdefault("bar_axis_color", Color("#000000")) 

3281 

3282 options["bar_color"] = Color._from_value(options["bar_color"]) 

3283 options["bar_border_color"] = Color._from_value(options["bar_border_color"]) 

3284 options["bar_axis_color"] = Color._from_value(options["bar_axis_color"]) 

3285 options["bar_negative_color"] = Color._from_value( 

3286 options["bar_negative_color"] 

3287 ) 

3288 options["bar_negative_border_color"] = Color._from_value( 

3289 options["bar_negative_border_color"] 

3290 ) 

3291 

3292 # Adjust for 2010 style data_bar parameters. 

3293 if options.get("is_data_bar_2010"): 

3294 self.excel_version = 2010 

3295 

3296 if options["min_type"] == "min" and options["min_value"] == 0: 

3297 options["min_value"] = None 

3298 

3299 if options["max_type"] == "max" and options["max_value"] == 0: 

3300 options["max_value"] = None 

3301 

3302 options["range"] = cell_range 

3303 

3304 # Strip the leading = from formulas. 

3305 try: 

3306 options["min_value"] = options["min_value"].lstrip("=") 

3307 except (KeyError, AttributeError): 

3308 pass 

3309 try: 

3310 options["mid_value"] = options["mid_value"].lstrip("=") 

3311 except (KeyError, AttributeError): 

3312 pass 

3313 try: 

3314 options["max_value"] = options["max_value"].lstrip("=") 

3315 except (KeyError, AttributeError): 

3316 pass 

3317 

3318 # Store the conditional format until we close the worksheet. 

3319 if cell_range in self.cond_formats: 

3320 self.cond_formats[cell_range].append(options) 

3321 else: 

3322 self.cond_formats[cell_range] = [options] 

3323 

3324 return 0 

3325 

3326 @convert_range_args 

3327 def add_table( 

3328 self, 

3329 first_row: int, 

3330 first_col: int, 

3331 last_row: int, 

3332 last_col: int, 

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

3334 ) -> Literal[0, -1, -2, -3]: 

3335 """ 

3336 Add an Excel table to a worksheet. 

3337 

3338 Args: 

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

3340 first_col: The first column of the cell range. 

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

3342 last_col: The last column of the cell range. 

3343 options: Table format options. (Optional) 

3344 

3345 Returns: 

3346 0: Success. 

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

3348 -2: Incorrect parameter or option. 

3349 -3: Not supported in constant_memory mode. 

3350 """ 

3351 table = {} 

3352 col_formats = {} 

3353 

3354 if options is None: 

3355 options = {} 

3356 else: 

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

3358 options = options.copy() 

3359 

3360 if self.constant_memory: 

3361 warn("add_table() isn't supported in 'constant_memory' mode") 

3362 return -3 

3363 

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

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

3366 return -1 

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

3368 return -1 

3369 

3370 # Swap last row/col for first row/col as necessary. 

3371 if first_row > last_row: 

3372 (first_row, last_row) = (last_row, first_row) 

3373 if first_col > last_col: 

3374 (first_col, last_col) = (last_col, first_col) 

3375 

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

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

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

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

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

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

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

3383 raise OverlappingRange( 

3384 f"Table range '{cell_range}' overlaps previous " 

3385 f"table range '{previous_range}'." 

3386 ) 

3387 

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

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

3390 raise OverlappingRange( 

3391 f"Table range '{cell_range}' overlaps previous " 

3392 f"merge range '{previous_range}'." 

3393 ) 

3394 

3395 self.table_cells[(row, col)] = cell_range 

3396 

3397 # Valid input parameters. 

3398 valid_parameter = { 

3399 "autofilter", 

3400 "banded_columns", 

3401 "banded_rows", 

3402 "columns", 

3403 "data", 

3404 "first_column", 

3405 "header_row", 

3406 "last_column", 

3407 "name", 

3408 "style", 

3409 "total_row", 

3410 } 

3411 

3412 # Check for valid input parameters. 

3413 for param_key in options.keys(): 

3414 if param_key not in valid_parameter: 

3415 warn(f"Unknown parameter '{param_key}' in add_table()") 

3416 return -2 

3417 

3418 # Turn on Excel's defaults. 

3419 options["banded_rows"] = options.get("banded_rows", True) 

3420 options["header_row"] = options.get("header_row", True) 

3421 options["autofilter"] = options.get("autofilter", True) 

3422 

3423 # Check that there are enough rows. 

3424 num_rows = last_row - first_row 

3425 if options["header_row"]: 

3426 num_rows -= 1 

3427 

3428 if num_rows < 0: 

3429 warn("Must have at least one data row in in add_table()") 

3430 return -2 

3431 

3432 # Set the table options. 

3433 table["show_first_col"] = options.get("first_column", False) 

3434 table["show_last_col"] = options.get("last_column", False) 

3435 table["show_row_stripes"] = options.get("banded_rows", False) 

3436 table["show_col_stripes"] = options.get("banded_columns", False) 

3437 table["header_row_count"] = options.get("header_row", 0) 

3438 table["totals_row_shown"] = options.get("total_row", False) 

3439 

3440 # Set the table name. 

3441 if "name" in options: 

3442 name = options["name"] 

3443 table["name"] = name 

3444 

3445 if " " in name: 

3446 warn(f"Name '{name}' in add_table() cannot contain spaces") 

3447 return -2 

3448 

3449 # Warn if the name contains invalid chars as defined by Excel. 

3450 if not re.match(r"^[\w\\][\w\\.]*$", name, re.UNICODE) or re.match( 

3451 r"^\d", name 

3452 ): 

3453 warn(f"Invalid Excel characters in add_table(): '{name}'") 

3454 return -2 

3455 

3456 # Warn if the name looks like a cell name. 

3457 if re.match(r"^[a-zA-Z][a-zA-Z]?[a-dA-D]?\d+$", name): 

3458 warn(f"Name looks like a cell name in add_table(): '{name}'") 

3459 return -2 

3460 

3461 # Warn if the name looks like a R1C1 cell reference. 

3462 if re.match(r"^[rcRC]$", name) or re.match(r"^[rcRC]\d+[rcRC]\d+$", name): 

3463 warn(f"Invalid name '{name}' like a RC cell ref in add_table()") 

3464 return -2 

3465 

3466 # Set the table style. 

3467 if "style" in options: 

3468 table["style"] = options["style"] 

3469 

3470 if table["style"] is None: 

3471 table["style"] = "" 

3472 

3473 # Remove whitespace from style name. 

3474 table["style"] = table["style"].replace(" ", "") 

3475 else: 

3476 table["style"] = "TableStyleMedium9" 

3477 

3478 # Set the data range rows (without the header and footer). 

3479 first_data_row = first_row 

3480 last_data_row = last_row 

3481 

3482 if options.get("header_row"): 

3483 first_data_row += 1 

3484 

3485 if options.get("total_row"): 

3486 last_data_row -= 1 

3487 

3488 # Set the table and autofilter ranges. 

3489 table["range"] = xl_range(first_row, first_col, last_row, last_col) 

3490 

3491 table["a_range"] = xl_range(first_row, first_col, last_data_row, last_col) 

3492 

3493 # If the header row if off the default is to turn autofilter off. 

3494 if not options["header_row"]: 

3495 options["autofilter"] = 0 

3496 

3497 # Set the autofilter range. 

3498 if options["autofilter"]: 

3499 table["autofilter"] = table["a_range"] 

3500 

3501 # Add the table columns. 

3502 col_id = 1 

3503 table["columns"] = [] 

3504 seen_names = {} 

3505 

3506 for col_num in range(first_col, last_col + 1): 

3507 # Set up the default column data. 

3508 col_data = { 

3509 "id": col_id, 

3510 "name": "Column" + str(col_id), 

3511 "total_string": "", 

3512 "total_function": "", 

3513 "custom_total": "", 

3514 "total_value": 0, 

3515 "formula": "", 

3516 "format": None, 

3517 "name_format": None, 

3518 } 

3519 

3520 # Overwrite the defaults with any user defined values. 

3521 if "columns" in options: 

3522 # Check if there are user defined values for this column. 

3523 if col_id <= len(options["columns"]): 

3524 user_data = options["columns"][col_id - 1] 

3525 else: 

3526 user_data = None 

3527 

3528 if user_data: 

3529 # Get the column format. 

3530 xformat = user_data.get("format", None) 

3531 

3532 # Map user defined values to internal values. 

3533 if user_data.get("header"): 

3534 col_data["name"] = user_data["header"] 

3535 

3536 # Excel requires unique case insensitive header names. 

3537 header_name = col_data["name"] 

3538 name = header_name.lower() 

3539 if name in seen_names: 

3540 warn(f"Duplicate header name in add_table(): '{name}'") 

3541 return -2 

3542 

3543 seen_names[name] = True 

3544 

3545 col_data["name_format"] = user_data.get("header_format") 

3546 

3547 # Handle the column formula. 

3548 if "formula" in user_data and user_data["formula"]: 

3549 formula = user_data["formula"] 

3550 

3551 # Remove the formula '=' sign if it exists. 

3552 if formula.startswith("="): 

3553 formula = formula.lstrip("=") 

3554 

3555 # Convert Excel 2010 "@" ref to 2007 "#This Row". 

3556 formula = self._prepare_table_formula(formula) 

3557 

3558 # Escape any future functions. 

3559 formula = self._prepare_formula(formula, True) 

3560 

3561 col_data["formula"] = formula 

3562 # We write the formulas below after the table data. 

3563 

3564 # Handle the function for the total row. 

3565 if user_data.get("total_function"): 

3566 function = user_data["total_function"] 

3567 if function == "count_nums": 

3568 function = "countNums" 

3569 if function == "std_dev": 

3570 function = "stdDev" 

3571 

3572 subtotals = set( 

3573 [ 

3574 "average", 

3575 "countNums", 

3576 "count", 

3577 "max", 

3578 "min", 

3579 "stdDev", 

3580 "sum", 

3581 "var", 

3582 ] 

3583 ) 

3584 

3585 if function in subtotals: 

3586 formula = self._table_function_to_formula( 

3587 function, col_data["name"] 

3588 ) 

3589 else: 

3590 formula = self._prepare_formula(function, True) 

3591 col_data["custom_total"] = formula 

3592 function = "custom" 

3593 

3594 col_data["total_function"] = function 

3595 

3596 value = user_data.get("total_value", 0) 

3597 

3598 self._write_formula(last_row, col_num, formula, xformat, value) 

3599 

3600 elif user_data.get("total_string"): 

3601 # Total label only (not a function). 

3602 total_string = user_data["total_string"] 

3603 col_data["total_string"] = total_string 

3604 

3605 self._write_string( 

3606 last_row, col_num, total_string, user_data.get("format") 

3607 ) 

3608 

3609 # Get the dxf format index. 

3610 if xformat is not None: 

3611 col_data["format"] = xformat._get_dxf_index() 

3612 

3613 # Store the column format for writing the cell data. 

3614 # It doesn't matter if it is undefined. 

3615 col_formats[col_id - 1] = xformat 

3616 

3617 # Store the column data. 

3618 table["columns"].append(col_data) 

3619 

3620 # Write the column headers to the worksheet. 

3621 if options["header_row"]: 

3622 self._write_string( 

3623 first_row, col_num, col_data["name"], col_data["name_format"] 

3624 ) 

3625 

3626 col_id += 1 

3627 

3628 # Write the cell data if supplied. 

3629 if "data" in options: 

3630 data = options["data"] 

3631 

3632 i = 0 # For indexing the row data. 

3633 for row in range(first_data_row, last_data_row + 1): 

3634 j = 0 # For indexing the col data. 

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

3636 if i < len(data) and j < len(data[i]): 

3637 token = data[i][j] 

3638 if j in col_formats: 

3639 self._write(row, col, token, col_formats[j]) 

3640 else: 

3641 self._write(row, col, token, None) 

3642 j += 1 

3643 i += 1 

3644 

3645 # Write any columns formulas after the user supplied table data to 

3646 # overwrite it if required. 

3647 for col_id, col_num in enumerate(range(first_col, last_col + 1)): 

3648 column_data = table["columns"][col_id] 

3649 if column_data and column_data["formula"]: 

3650 formula_format = col_formats.get(col_id) 

3651 formula = column_data["formula"] 

3652 

3653 for row in range(first_data_row, last_data_row + 1): 

3654 self._write_formula(row, col_num, formula, formula_format) 

3655 

3656 # Store the table data. 

3657 self.tables.append(table) 

3658 

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

3660 if options["autofilter"]: 

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

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

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

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

3665 if filter_type == "worksheet": 

3666 raise OverlappingRange( 

3667 f"Table autofilter range '{cell_range}' overlaps previous " 

3668 f"Worksheet autofilter range '{filter_range}'." 

3669 ) 

3670 

3671 self.filter_cells[(first_row, col)] = ("table", cell_range) 

3672 

3673 return 0 

3674 

3675 @convert_cell_args 

3676 def add_sparkline( 

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

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

3679 """ 

3680 Add sparklines to the worksheet. 

3681 

3682 Args: 

3683 row: The cell row (zero indexed). 

3684 col: The cell column (zero indexed). 

3685 options: Sparkline formatting options. 

3686 

3687 Returns: 

3688 0: Success. 

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

3690 -2: Incorrect parameter or option. 

3691 

3692 """ 

3693 

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

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

3696 return -1 

3697 

3698 sparkline = {"locations": [xl_rowcol_to_cell(row, col)]} 

3699 

3700 if options is None: 

3701 options = {} 

3702 

3703 # Valid input parameters. 

3704 valid_parameters = { 

3705 "location", 

3706 "range", 

3707 "type", 

3708 "high_point", 

3709 "low_point", 

3710 "negative_points", 

3711 "first_point", 

3712 "last_point", 

3713 "markers", 

3714 "style", 

3715 "series_color", 

3716 "negative_color", 

3717 "markers_color", 

3718 "first_color", 

3719 "last_color", 

3720 "high_color", 

3721 "low_color", 

3722 "max", 

3723 "min", 

3724 "axis", 

3725 "reverse", 

3726 "empty_cells", 

3727 "show_hidden", 

3728 "plot_hidden", 

3729 "date_axis", 

3730 "weight", 

3731 } 

3732 

3733 # Check for valid input parameters. 

3734 for param_key in options.keys(): 

3735 if param_key not in valid_parameters: 

3736 warn(f"Unknown parameter '{param_key}' in add_sparkline()") 

3737 return -1 

3738 

3739 # 'range' is a required parameter. 

3740 if "range" not in options: 

3741 warn("Parameter 'range' is required in add_sparkline()") 

3742 return -2 

3743 

3744 # Handle the sparkline type. 

3745 spark_type = options.get("type", "line") 

3746 

3747 if spark_type not in ("line", "column", "win_loss"): 

3748 warn( 

3749 "Parameter 'type' must be 'line', 'column' " 

3750 "or 'win_loss' in add_sparkline()" 

3751 ) 

3752 return -2 

3753 

3754 if spark_type == "win_loss": 

3755 spark_type = "stacked" 

3756 sparkline["type"] = spark_type 

3757 

3758 # We handle single location/range values or list of values. 

3759 if "location" in options: 

3760 if isinstance(options["location"], list): 

3761 sparkline["locations"] = options["location"] 

3762 else: 

3763 sparkline["locations"] = [options["location"]] 

3764 

3765 if isinstance(options["range"], list): 

3766 sparkline["ranges"] = options["range"] 

3767 else: 

3768 sparkline["ranges"] = [options["range"]] 

3769 

3770 range_count = len(sparkline["ranges"]) 

3771 location_count = len(sparkline["locations"]) 

3772 

3773 # The ranges and locations must match. 

3774 if range_count != location_count: 

3775 warn( 

3776 "Must have the same number of location and range " 

3777 "parameters in add_sparkline()" 

3778 ) 

3779 return -2 

3780 

3781 # Store the count. 

3782 sparkline["count"] = len(sparkline["locations"]) 

3783 

3784 # Get the worksheet name for the range conversion below. 

3785 sheetname = quote_sheetname(self.name) 

3786 

3787 # Cleanup the input ranges. 

3788 new_ranges = [] 

3789 for spark_range in sparkline["ranges"]: 

3790 # Remove the absolute reference $ symbols. 

3791 spark_range = spark_range.replace("$", "") 

3792 

3793 # Remove the = from formula. 

3794 spark_range = spark_range.lstrip("=") 

3795 

3796 # Convert a simple range into a full Sheet1!A1:D1 range. 

3797 if "!" not in spark_range: 

3798 spark_range = sheetname + "!" + spark_range 

3799 

3800 new_ranges.append(spark_range) 

3801 

3802 sparkline["ranges"] = new_ranges 

3803 

3804 # Cleanup the input locations. 

3805 new_locations = [] 

3806 for location in sparkline["locations"]: 

3807 location = location.replace("$", "") 

3808 new_locations.append(location) 

3809 

3810 sparkline["locations"] = new_locations 

3811 

3812 # Map options. 

3813 sparkline["high"] = options.get("high_point") 

3814 sparkline["low"] = options.get("low_point") 

3815 sparkline["negative"] = options.get("negative_points") 

3816 sparkline["first"] = options.get("first_point") 

3817 sparkline["last"] = options.get("last_point") 

3818 sparkline["markers"] = options.get("markers") 

3819 sparkline["min"] = options.get("min") 

3820 sparkline["max"] = options.get("max") 

3821 sparkline["axis"] = options.get("axis") 

3822 sparkline["reverse"] = options.get("reverse") 

3823 sparkline["hidden"] = options.get("show_hidden") 

3824 sparkline["weight"] = options.get("weight") 

3825 

3826 # Map empty cells options. 

3827 empty = options.get("empty_cells", "") 

3828 

3829 if empty == "zero": 

3830 sparkline["empty"] = 0 

3831 elif empty == "connect": 

3832 sparkline["empty"] = "span" 

3833 else: 

3834 sparkline["empty"] = "gap" 

3835 

3836 # Map the date axis range. 

3837 date_range = options.get("date_axis") 

3838 

3839 if date_range and "!" not in date_range: 

3840 date_range = sheetname + "!" + date_range 

3841 

3842 sparkline["date_axis"] = date_range 

3843 

3844 # Set the sparkline styles. 

3845 style_id = options.get("style", 0) 

3846 style = _get_sparkline_style(style_id) 

3847 

3848 sparkline["series_color"] = style["series"] 

3849 sparkline["negative_color"] = style["negative"] 

3850 sparkline["markers_color"] = style["markers"] 

3851 sparkline["first_color"] = style["first"] 

3852 sparkline["last_color"] = style["last"] 

3853 sparkline["high_color"] = style["high"] 

3854 sparkline["low_color"] = style["low"] 

3855 

3856 # Override the style colors with user defined colors. 

3857 self._set_spark_color(sparkline, options, "series_color") 

3858 self._set_spark_color(sparkline, options, "negative_color") 

3859 self._set_spark_color(sparkline, options, "markers_color") 

3860 self._set_spark_color(sparkline, options, "first_color") 

3861 self._set_spark_color(sparkline, options, "last_color") 

3862 self._set_spark_color(sparkline, options, "high_color") 

3863 self._set_spark_color(sparkline, options, "low_color") 

3864 

3865 self.sparklines.append(sparkline) 

3866 

3867 return 0 

3868 

3869 @convert_range_args 

3870 def set_selection( 

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

3872 ) -> None: 

3873 """ 

3874 Set the selected cell or cells in a worksheet 

3875 

3876 Args: 

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

3878 first_col: The first column of the cell range. 

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

3880 last_col: The last column of the cell range. 

3881 

3882 Returns: 

3883 0: Nothing. 

3884 """ 

3885 pane = None 

3886 

3887 # Range selection. Do this before swapping max/min to allow the 

3888 # selection direction to be reversed. 

3889 active_cell = xl_rowcol_to_cell(first_row, first_col) 

3890 

3891 # Swap last row/col for first row/col if necessary 

3892 if first_row > last_row: 

3893 (first_row, last_row) = (last_row, first_row) 

3894 

3895 if first_col > last_col: 

3896 (first_col, last_col) = (last_col, first_col) 

3897 

3898 sqref = xl_range(first_row, first_col, last_row, last_col) 

3899 

3900 # Selection isn't set for cell A1. 

3901 if sqref == "A1": 

3902 return 

3903 

3904 self.selections = [[pane, active_cell, sqref]] 

3905 

3906 @convert_cell_args 

3907 def set_top_left_cell(self, row: int = 0, col: int = 0) -> None: 

3908 """ 

3909 Set the first visible cell at the top left of a worksheet. 

3910 

3911 Args: 

3912 row: The cell row (zero indexed). 

3913 col: The cell column (zero indexed). 

3914 

3915 Returns: 

3916 0: Nothing. 

3917 """ 

3918 

3919 if row == 0 and col == 0: 

3920 return 

3921 

3922 self.top_left_cell = xl_rowcol_to_cell(row, col) 

3923 

3924 def outline_settings( 

3925 self, 

3926 visible: bool = 1, 

3927 symbols_below: bool = 1, 

3928 symbols_right: bool = 1, 

3929 auto_style: bool = 0, 

3930 ) -> None: 

3931 """ 

3932 Control outline settings. 

3933 

3934 Args: 

3935 visible: Outlines are visible. Optional, defaults to True. 

3936 symbols_below: Show row outline symbols below the outline bar. 

3937 Optional, defaults to True. 

3938 symbols_right: Show column outline symbols to the right of the 

3939 outline bar. Optional, defaults to True. 

3940 auto_style: Use Automatic style. Optional, defaults to False. 

3941 

3942 Returns: 

3943 0: Nothing. 

3944 """ 

3945 self.outline_on = visible 

3946 self.outline_below = symbols_below 

3947 self.outline_right = symbols_right 

3948 self.outline_style = auto_style 

3949 

3950 self.outline_changed = True 

3951 

3952 @convert_cell_args 

3953 def freeze_panes( 

3954 self, 

3955 row: int, 

3956 col: int, 

3957 top_row: Optional[int] = None, 

3958 left_col: Optional[int] = None, 

3959 pane_type: int = 0, 

3960 ) -> None: 

3961 """ 

3962 Create worksheet panes and mark them as frozen. 

3963 

3964 Args: 

3965 row: The cell row (zero indexed). 

3966 col: The cell column (zero indexed). 

3967 top_row: Topmost visible row in scrolling region of pane. 

3968 left_col: Leftmost visible row in scrolling region of pane. 

3969 

3970 Returns: 

3971 0: Nothing. 

3972 

3973 """ 

3974 if top_row is None: 

3975 top_row = row 

3976 

3977 if left_col is None: 

3978 left_col = col 

3979 

3980 self.panes = [row, col, top_row, left_col, pane_type] 

3981 

3982 @convert_cell_args 

3983 def split_panes( 

3984 self, 

3985 x: float, 

3986 y: float, 

3987 top_row: Optional[int] = None, 

3988 left_col: Optional[int] = None, 

3989 ) -> None: 

3990 """ 

3991 Create worksheet panes and mark them as split. 

3992 

3993 Args: 

3994 x: The position for the vertical split. 

3995 y: The position for the horizontal split. 

3996 top_row: Topmost visible row in scrolling region of pane. 

3997 left_col: Leftmost visible row in scrolling region of pane. 

3998 

3999 Returns: 

4000 0: Nothing. 

4001 

4002 """ 

4003 # Same as freeze panes with a different pane type. 

4004 self.freeze_panes(x, y, top_row, left_col, 2) 

4005 

4006 def set_zoom(self, zoom: int = 100) -> None: 

4007 """ 

4008 Set the worksheet zoom factor. 

4009 

4010 Args: 

4011 zoom: Scale factor: 10 <= zoom <= 400. 

4012 

4013 Returns: 

4014 Nothing. 

4015 

4016 """ 

4017 # Ensure the zoom scale is in Excel's range. 

4018 if zoom < 10 or zoom > 400: 

4019 warn(f"Zoom factor '{zoom}' outside range: 10 <= zoom <= 400") 

4020 zoom = 100 

4021 

4022 self.zoom = int(zoom) 

4023 

4024 def right_to_left(self) -> None: 

4025 """ 

4026 Display the worksheet right to left for some versions of Excel. 

4027 

4028 Args: 

4029 None. 

4030 

4031 Returns: 

4032 Nothing. 

4033 

4034 """ 

4035 self.is_right_to_left = True 

4036 

4037 def hide_zero(self) -> None: 

4038 """ 

4039 Hide zero values in worksheet cells. 

4040 

4041 Args: 

4042 None. 

4043 

4044 Returns: 

4045 Nothing. 

4046 

4047 """ 

4048 self.show_zeros = 0 

4049 

4050 def set_tab_color(self, color: Union[str, Color]) -> None: 

4051 """ 

4052 Set the color of the worksheet tab. 

4053 

4054 Args: 

4055 color: A #RGB color index. 

4056 

4057 Returns: 

4058 Nothing. 

4059 

4060 """ 

4061 self.tab_color = Color._from_value(color) 

4062 

4063 def protect( 

4064 self, password: str = "", options: Optional[Dict[str, Any]] = None 

4065 ) -> None: 

4066 """ 

4067 Set the password and protection options of the worksheet. 

4068 

4069 Args: 

4070 password: An optional password string. 

4071 options: A dictionary of worksheet objects to protect. 

4072 

4073 Returns: 

4074 Nothing. 

4075 

4076 """ 

4077 if password != "": 

4078 password = self._encode_password(password) 

4079 

4080 if not options: 

4081 options = {} 

4082 

4083 # Default values for objects that can be protected. 

4084 defaults = { 

4085 "sheet": True, 

4086 "content": False, 

4087 "objects": False, 

4088 "scenarios": False, 

4089 "format_cells": False, 

4090 "format_columns": False, 

4091 "format_rows": False, 

4092 "insert_columns": False, 

4093 "insert_rows": False, 

4094 "insert_hyperlinks": False, 

4095 "delete_columns": False, 

4096 "delete_rows": False, 

4097 "select_locked_cells": True, 

4098 "sort": False, 

4099 "autofilter": False, 

4100 "pivot_tables": False, 

4101 "select_unlocked_cells": True, 

4102 } 

4103 

4104 # Overwrite the defaults with user specified values. 

4105 for key in options.keys(): 

4106 if key in defaults: 

4107 defaults[key] = options[key] 

4108 else: 

4109 warn(f"Unknown protection object: '{key}'") 

4110 

4111 # Set the password after the user defined values. 

4112 defaults["password"] = password 

4113 

4114 self.protect_options = defaults 

4115 

4116 def unprotect_range( 

4117 self, 

4118 cell_range: str, 

4119 range_name: Optional[str] = None, 

4120 password: Optional[str] = None, 

4121 ) -> int: 

4122 """ 

4123 Unprotect ranges within a protected worksheet. 

4124 

4125 Args: 

4126 cell_range: The cell or cell range to unprotect. 

4127 range_name: An optional name for the range. 

4128 password: An optional password string. (undocumented) 

4129 

4130 Returns: 

4131 0: Success. 

4132 -1: Parameter error. 

4133 

4134 """ 

4135 if cell_range is None: 

4136 warn("Cell range must be specified in unprotect_range()") 

4137 return -1 

4138 

4139 # Sanitize the cell range. 

4140 cell_range = cell_range.lstrip("=") 

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

4142 

4143 self.num_protected_ranges += 1 

4144 

4145 if range_name is None: 

4146 range_name = "Range" + str(self.num_protected_ranges) 

4147 

4148 if password: 

4149 password = self._encode_password(password) 

4150 

4151 self.protected_ranges.append((cell_range, range_name, password)) 

4152 

4153 return 0 

4154 

4155 @convert_cell_args 

4156 def insert_button( 

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

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

4159 """ 

4160 Insert a button form object into the worksheet. 

4161 

4162 Args: 

4163 row: The cell row (zero indexed). 

4164 col: The cell column (zero indexed). 

4165 options: Button formatting options. 

4166 

4167 Returns: 

4168 0: Success. 

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

4170 

4171 """ 

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

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

4174 warn(f"Cannot insert button at ({row}, {col}).") 

4175 return -1 

4176 

4177 if options is None: 

4178 options = {} 

4179 

4180 # Create a new button object. 

4181 height = self.default_row_pixels 

4182 width = self.default_col_pixels 

4183 button_number = 1 + len(self.buttons_list) 

4184 

4185 button = ButtonType(row, col, height, width, button_number, options) 

4186 

4187 self.buttons_list.append(button) 

4188 

4189 self.has_vml = True 

4190 

4191 return 0 

4192 

4193 @convert_cell_args 

4194 def insert_checkbox( 

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

4196 ): 

4197 """ 

4198 Insert a boolean checkbox in a worksheet cell. 

4199 

4200 Args: 

4201 row: The cell row (zero indexed). 

4202 col: The cell column (zero indexed). 

4203 boolean: The boolean value to display as a checkbox. 

4204 cell_format: Cell Format object. (optional) 

4205 

4206 Returns: 

4207 0: Success. 

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

4209 

4210 """ 

4211 # Ensure that the checkbox property is set in the user defined format. 

4212 if cell_format and not cell_format.checkbox: 

4213 # This needs to be fixed with a clone. 

4214 cell_format.set_checkbox() 

4215 

4216 # If no format is supplied create and/or use the default checkbox format. 

4217 if not cell_format: 

4218 if not self.default_checkbox_format: 

4219 self.default_checkbox_format = self.workbook_add_format() 

4220 self.default_checkbox_format.set_checkbox() 

4221 

4222 cell_format = self.default_checkbox_format 

4223 

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

4225 

4226 ########################################################################### 

4227 # 

4228 # Public API. Page Setup methods. 

4229 # 

4230 ########################################################################### 

4231 def set_landscape(self) -> None: 

4232 """ 

4233 Set the page orientation as landscape. 

4234 

4235 Args: 

4236 None. 

4237 

4238 Returns: 

4239 Nothing. 

4240 

4241 """ 

4242 self.orientation = 0 

4243 self.page_setup_changed = True 

4244 

4245 def set_portrait(self) -> None: 

4246 """ 

4247 Set the page orientation as portrait. 

4248 

4249 Args: 

4250 None. 

4251 

4252 Returns: 

4253 Nothing. 

4254 

4255 """ 

4256 self.orientation = 1 

4257 self.page_setup_changed = True 

4258 

4259 def set_page_view(self, view: Literal[0, 1, 2] = 1) -> None: 

4260 """ 

4261 Set the page view mode. 

4262 

4263 Args: 

4264 0: Normal view mode 

4265 1: Page view mode (the default) 

4266 2: Page break view mode 

4267 

4268 Returns: 

4269 Nothing. 

4270 

4271 """ 

4272 self.page_view = view 

4273 

4274 def set_pagebreak_view(self) -> None: 

4275 """ 

4276 Set the page view mode. 

4277 

4278 Args: 

4279 None. 

4280 

4281 Returns: 

4282 Nothing. 

4283 

4284 """ 

4285 self.page_view = 2 

4286 

4287 def set_paper(self, paper_size: Union[Literal[1, 9], int]) -> None: 

4288 """ 

4289 Set the paper type. US Letter = 1, A4 = 9. 

4290 

4291 Args: 

4292 paper_size: Paper index. 

4293 

4294 Returns: 

4295 Nothing. 

4296 

4297 """ 

4298 if paper_size: 

4299 self.paper_size = paper_size 

4300 self.page_setup_changed = True 

4301 

4302 def center_horizontally(self) -> None: 

4303 """ 

4304 Center the page horizontally. 

4305 

4306 Args: 

4307 None. 

4308 

4309 Returns: 

4310 Nothing. 

4311 

4312 """ 

4313 self.print_options_changed = True 

4314 self.hcenter = 1 

4315 

4316 def center_vertically(self) -> None: 

4317 """ 

4318 Center the page vertically. 

4319 

4320 Args: 

4321 None. 

4322 

4323 Returns: 

4324 Nothing. 

4325 

4326 """ 

4327 self.print_options_changed = True 

4328 self.vcenter = 1 

4329 

4330 def set_margins( 

4331 self, 

4332 left: float = 0.7, 

4333 right: float = 0.7, 

4334 top: float = 0.75, 

4335 bottom: float = 0.75, 

4336 ) -> None: 

4337 """ 

4338 Set all the page margins in inches. 

4339 

4340 Args: 

4341 left: Left margin. 

4342 right: Right margin. 

4343 top: Top margin. 

4344 bottom: Bottom margin. 

4345 

4346 Returns: 

4347 Nothing. 

4348 

4349 """ 

4350 self.margin_left = left 

4351 self.margin_right = right 

4352 self.margin_top = top 

4353 self.margin_bottom = bottom 

4354 

4355 def set_header( 

4356 self, header: str = "", options: Optional[Dict[str, Any]] = None, margin=None 

4357 ) -> None: 

4358 """ 

4359 Set the page header caption and optional margin. 

4360 

4361 Args: 

4362 header: Header string. 

4363 margin: Header margin. 

4364 options: Header options, mainly for images. 

4365 

4366 Returns: 

4367 Nothing. 

4368 

4369 """ 

4370 header_orig = header 

4371 header = header.replace("&[Picture]", "&G") 

4372 

4373 if len(header) > 255: 

4374 warn("Header string cannot be longer than Excel's limit of 255 characters") 

4375 return 

4376 

4377 if options is not None: 

4378 # For backward compatibility allow options to be the margin. 

4379 if not isinstance(options, dict): 

4380 options = {"margin": options} 

4381 else: 

4382 options = {} 

4383 

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

4385 options = options.copy() 

4386 

4387 # For backward compatibility. 

4388 if margin is not None: 

4389 options["margin"] = margin 

4390 

4391 # Reset the list in case the function is called more than once. 

4392 self.header_images = [] 

4393 

4394 if options.get("image_left"): 

4395 options["image_data"] = options.get("image_data_left") 

4396 image = self._image_from_source(options.get("image_left"), options) 

4397 image._header_position = "LH" 

4398 self.header_images.append(image) 

4399 

4400 if options.get("image_center"): 

4401 options["image_data"] = options.get("image_data_center") 

4402 image = self._image_from_source(options.get("image_center"), options) 

4403 image._header_position = "CH" 

4404 self.header_images.append(image) 

4405 

4406 if options.get("image_right"): 

4407 options["image_data"] = options.get("image_data_right") 

4408 image = self._image_from_source(options.get("image_right"), options) 

4409 image._header_position = "RH" 

4410 self.header_images.append(image) 

4411 

4412 placeholder_count = header.count("&G") 

4413 image_count = len(self.header_images) 

4414 

4415 if placeholder_count != image_count: 

4416 warn( 

4417 f"Number of footer images '{image_count}' doesn't match placeholder " 

4418 f"count '{placeholder_count}' in string: {header_orig}" 

4419 ) 

4420 self.header_images = [] 

4421 return 

4422 

4423 if "align_with_margins" in options: 

4424 self.header_footer_aligns = options["align_with_margins"] 

4425 

4426 if "scale_with_doc" in options: 

4427 self.header_footer_scales = options["scale_with_doc"] 

4428 

4429 self.header = header 

4430 self.margin_header = options.get("margin", 0.3) 

4431 self.header_footer_changed = True 

4432 

4433 if image_count: 

4434 self.has_header_vml = True 

4435 

4436 def set_footer( 

4437 self, footer: str = "", options: Optional[Dict[str, Any]] = None, margin=None 

4438 ) -> None: 

4439 """ 

4440 Set the page footer caption and optional margin. 

4441 

4442 Args: 

4443 footer: Footer string. 

4444 margin: Footer margin. 

4445 options: Footer options, mainly for images. 

4446 

4447 Returns: 

4448 Nothing. 

4449 

4450 """ 

4451 footer_orig = footer 

4452 footer = footer.replace("&[Picture]", "&G") 

4453 

4454 if len(footer) > 255: 

4455 warn("Footer string cannot be longer than Excel's limit of 255 characters") 

4456 return 

4457 

4458 if options is not None: 

4459 # For backward compatibility allow options to be the margin. 

4460 if not isinstance(options, dict): 

4461 options = {"margin": options} 

4462 else: 

4463 options = {} 

4464 

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

4466 options = options.copy() 

4467 

4468 # For backward compatibility. 

4469 if margin is not None: 

4470 options["margin"] = margin 

4471 

4472 # Reset the list in case the function is called more than once. 

4473 self.footer_images = [] 

4474 

4475 if options.get("image_left"): 

4476 options["image_data"] = options.get("image_data_left") 

4477 image = self._image_from_source(options.get("image_left"), options) 

4478 image._header_position = "LF" 

4479 self.footer_images.append(image) 

4480 

4481 if options.get("image_center"): 

4482 options["image_data"] = options.get("image_data_center") 

4483 image = self._image_from_source(options.get("image_center"), options) 

4484 image._header_position = "CF" 

4485 self.footer_images.append(image) 

4486 

4487 if options.get("image_right"): 

4488 options["image_data"] = options.get("image_data_right") 

4489 image = self._image_from_source(options.get("image_right"), options) 

4490 image._header_position = "RF" 

4491 self.footer_images.append(image) 

4492 

4493 placeholder_count = footer.count("&G") 

4494 image_count = len(self.footer_images) 

4495 

4496 if placeholder_count != image_count: 

4497 warn( 

4498 f"Number of footer images '{image_count}' doesn't match placeholder " 

4499 f"count '{placeholder_count}' in string: {footer_orig}" 

4500 ) 

4501 self.footer_images = [] 

4502 return 

4503 

4504 if "align_with_margins" in options: 

4505 self.header_footer_aligns = options["align_with_margins"] 

4506 

4507 if "scale_with_doc" in options: 

4508 self.header_footer_scales = options["scale_with_doc"] 

4509 

4510 self.footer = footer 

4511 self.margin_footer = options.get("margin", 0.3) 

4512 self.header_footer_changed = True 

4513 

4514 if image_count: 

4515 self.has_header_vml = True 

4516 

4517 def repeat_rows(self, first_row: int, last_row: Optional[int] = None) -> None: 

4518 """ 

4519 Set the rows to repeat at the top of each printed page. 

4520 

4521 Args: 

4522 first_row: Start row for range. 

4523 last_row: End row for range. 

4524 

4525 Returns: 

4526 Nothing. 

4527 

4528 """ 

4529 if last_row is None: 

4530 last_row = first_row 

4531 

4532 # Convert rows to 1 based. 

4533 first_row += 1 

4534 last_row += 1 

4535 

4536 # Create the row range area like: $1:$2. 

4537 area = f"${first_row}:${last_row}" 

4538 

4539 # Build up the print titles area "Sheet1!$1:$2" 

4540 sheetname = quote_sheetname(self.name) 

4541 self.repeat_row_range = sheetname + "!" + area 

4542 

4543 @convert_column_args 

4544 def repeat_columns(self, first_col: int, last_col: Optional[int] = None) -> None: 

4545 """ 

4546 Set the columns to repeat at the left hand side of each printed page. 

4547 

4548 Args: 

4549 first_col: Start column for range. 

4550 last_col: End column for range. 

4551 

4552 Returns: 

4553 Nothing. 

4554 

4555 """ 

4556 if last_col is None: 

4557 last_col = first_col 

4558 

4559 # Convert to A notation. 

4560 first_col = xl_col_to_name(first_col, 1) 

4561 last_col = xl_col_to_name(last_col, 1) 

4562 

4563 # Create a column range like $C:$D. 

4564 area = first_col + ":" + last_col 

4565 

4566 # Build up the print area range "=Sheet2!$C:$D" 

4567 sheetname = quote_sheetname(self.name) 

4568 self.repeat_col_range = sheetname + "!" + area 

4569 

4570 def hide_gridlines(self, option: Literal[0, 1, 2] = 1) -> None: 

4571 """ 

4572 Set the option to hide gridlines on the screen and the printed page. 

4573 

4574 Args: 

4575 option: 0 : Don't hide gridlines 

4576 1 : Hide printed gridlines only 

4577 2 : Hide screen and printed gridlines 

4578 

4579 Returns: 

4580 Nothing. 

4581 

4582 """ 

4583 if option == 0: 

4584 self.print_gridlines = 1 

4585 self.screen_gridlines = 1 

4586 self.print_options_changed = True 

4587 elif option == 1: 

4588 self.print_gridlines = 0 

4589 self.screen_gridlines = 1 

4590 else: 

4591 self.print_gridlines = 0 

4592 self.screen_gridlines = 0 

4593 

4594 def print_row_col_headers(self) -> None: 

4595 """ 

4596 Set the option to print the row and column headers on the printed page. 

4597 

4598 Args: 

4599 None. 

4600 

4601 Returns: 

4602 Nothing. 

4603 

4604 """ 

4605 self.print_headers = True 

4606 self.print_options_changed = True 

4607 

4608 def hide_row_col_headers(self) -> None: 

4609 """ 

4610 Set the option to hide the row and column headers on the worksheet. 

4611 

4612 Args: 

4613 None. 

4614 

4615 Returns: 

4616 Nothing. 

4617 

4618 """ 

4619 self.row_col_headers = True 

4620 

4621 @convert_range_args 

4622 def print_area( 

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

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

4625 """ 

4626 Set the print area in the current worksheet. 

4627 

4628 Args: 

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

4630 first_col: The first column of the cell range. 

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

4632 last_col: The last column of the cell range. 

4633 

4634 Returns: 

4635 0: Success. 

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

4637 

4638 """ 

4639 # Set the print area in the current worksheet. 

4640 

4641 # Ignore max print area since it is the same as no area for Excel. 

4642 if ( 

4643 first_row == 0 

4644 and first_col == 0 

4645 and last_row == self.xls_rowmax - 1 

4646 and last_col == self.xls_colmax - 1 

4647 ): 

4648 return -1 

4649 

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

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

4652 self.print_area_range = area 

4653 

4654 return 0 

4655 

4656 def print_across(self) -> None: 

4657 """ 

4658 Set the order in which pages are printed. 

4659 

4660 Args: 

4661 None. 

4662 

4663 Returns: 

4664 Nothing. 

4665 

4666 """ 

4667 self.page_order = 1 

4668 self.page_setup_changed = True 

4669 

4670 def fit_to_pages(self, width: int, height: int) -> None: 

4671 """ 

4672 Fit the printed area to a specific number of pages both vertically and 

4673 horizontally. 

4674 

4675 Args: 

4676 width: Number of pages horizontally. 

4677 height: Number of pages vertically. 

4678 

4679 Returns: 

4680 Nothing. 

4681 

4682 """ 

4683 self.fit_page = 1 

4684 self.fit_width = width 

4685 self.fit_height = height 

4686 self.page_setup_changed = True 

4687 

4688 def set_start_page(self, start_page: int) -> None: 

4689 """ 

4690 Set the start page number when printing. 

4691 

4692 Args: 

4693 start_page: Start page number. 

4694 

4695 Returns: 

4696 Nothing. 

4697 

4698 """ 

4699 self.page_start = start_page 

4700 

4701 def set_print_scale(self, scale: int) -> None: 

4702 """ 

4703 Set the scale factor for the printed page. 

4704 

4705 Args: 

4706 scale: Print scale. 10 <= scale <= 400. 

4707 

4708 Returns: 

4709 Nothing. 

4710 

4711 """ 

4712 # Confine the scale to Excel's range. 

4713 if scale < 10 or scale > 400: 

4714 warn(f"Print scale '{scale}' outside range: 10 <= scale <= 400") 

4715 return 

4716 

4717 # Turn off "fit to page" option when print scale is on. 

4718 self.fit_page = 0 

4719 

4720 self.print_scale = int(scale) 

4721 self.page_setup_changed = True 

4722 

4723 def print_black_and_white(self) -> None: 

4724 """ 

4725 Set the option to print the worksheet in black and white. 

4726 

4727 Args: 

4728 None. 

4729 

4730 Returns: 

4731 Nothing. 

4732 

4733 """ 

4734 self.black_white = True 

4735 self.page_setup_changed = True 

4736 

4737 def set_h_pagebreaks(self, breaks: List[int]) -> None: 

4738 """ 

4739 Set the horizontal page breaks on a worksheet. 

4740 

4741 Args: 

4742 breaks: List of rows where the page breaks should be added. 

4743 

4744 Returns: 

4745 Nothing. 

4746 

4747 """ 

4748 self.hbreaks = breaks 

4749 

4750 def set_v_pagebreaks(self, breaks: List[int]) -> None: 

4751 """ 

4752 Set the horizontal page breaks on a worksheet. 

4753 

4754 Args: 

4755 breaks: List of columns where the page breaks should be added. 

4756 

4757 Returns: 

4758 Nothing. 

4759 

4760 """ 

4761 self.vbreaks = breaks 

4762 

4763 def set_vba_name(self, name: Optional[str] = None) -> None: 

4764 """ 

4765 Set the VBA name for the worksheet. By default this is the 

4766 same as the sheet name: i.e., Sheet1 etc. 

4767 

4768 Args: 

4769 name: The VBA name for the worksheet. 

4770 

4771 Returns: 

4772 Nothing. 

4773 

4774 """ 

4775 if name is not None: 

4776 self.vba_codename = name 

4777 else: 

4778 self.vba_codename = "Sheet" + str(self.index + 1) 

4779 

4780 def ignore_errors(self, options: Optional[Dict[str, Any]] = None) -> Literal[0, -1]: 

4781 """ 

4782 Ignore various Excel errors/warnings in a worksheet for user defined 

4783 ranges. 

4784 

4785 Args: 

4786 options: A dict of ignore errors keys with cell range values. 

4787 

4788 Returns: 

4789 0: Success. 

4790 -1: Incorrect parameter or option. 

4791 

4792 """ 

4793 if options is None: 

4794 return -1 

4795 

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

4797 options = options.copy() 

4798 

4799 # Valid input parameters. 

4800 valid_parameters = { 

4801 "number_stored_as_text", 

4802 "eval_error", 

4803 "formula_differs", 

4804 "formula_range", 

4805 "formula_unlocked", 

4806 "empty_cell_reference", 

4807 "list_data_validation", 

4808 "calculated_column", 

4809 "two_digit_text_year", 

4810 } 

4811 

4812 # Check for valid input parameters. 

4813 for param_key in options.keys(): 

4814 if param_key not in valid_parameters: 

4815 warn(f"Unknown parameter '{param_key}' in ignore_errors()") 

4816 return -1 

4817 

4818 self.ignored_errors = options 

4819 

4820 return 0 

4821 

4822 ########################################################################### 

4823 # 

4824 # Private API. 

4825 # 

4826 ########################################################################### 

4827 def _initialize(self, init_data) -> None: 

4828 self.name = init_data["name"] 

4829 self.index = init_data["index"] 

4830 self.str_table = init_data["str_table"] 

4831 self.worksheet_meta = init_data["worksheet_meta"] 

4832 self.constant_memory = init_data["constant_memory"] 

4833 self.tmpdir = init_data["tmpdir"] 

4834 self.date_1904 = init_data["date_1904"] 

4835 self.strings_to_numbers = init_data["strings_to_numbers"] 

4836 self.strings_to_formulas = init_data["strings_to_formulas"] 

4837 self.strings_to_urls = init_data["strings_to_urls"] 

4838 self.nan_inf_to_errors = init_data["nan_inf_to_errors"] 

4839 self.default_date_format = init_data["default_date_format"] 

4840 self.default_url_format = init_data["default_url_format"] 

4841 self.workbook_add_format = init_data["workbook_add_format"] 

4842 self.excel2003_style = init_data["excel2003_style"] 

4843 self.remove_timezone = init_data["remove_timezone"] 

4844 self.max_url_length = init_data["max_url_length"] 

4845 self.use_future_functions = init_data["use_future_functions"] 

4846 self.embedded_images = init_data["embedded_images"] 

4847 

4848 if self.excel2003_style: 

4849 self.original_row_height = 12.75 

4850 self.default_row_height = 12.75 

4851 self.default_row_pixels = 17 

4852 self.margin_left = 0.75 

4853 self.margin_right = 0.75 

4854 self.margin_top = 1 

4855 self.margin_bottom = 1 

4856 self.margin_header = 0.5 

4857 self.margin_footer = 0.5 

4858 self.header_footer_aligns = False 

4859 

4860 # Open a temp filehandle to store row data in constant_memory mode. 

4861 if self.constant_memory: 

4862 # This is sub-optimal but we need to create a temp file 

4863 # with utf8 encoding in Python < 3. 

4864 (fd, filename) = tempfile.mkstemp(dir=self.tmpdir) 

4865 os.close(fd) 

4866 self.row_data_filename = filename 

4867 # pylint: disable=consider-using-with 

4868 self.row_data_fh = open(filename, mode="w+", encoding="utf-8") 

4869 

4870 # Set as the worksheet filehandle until the file is assembled. 

4871 self.fh = self.row_data_fh 

4872 

4873 def _assemble_xml_file(self) -> None: 

4874 # Assemble and write the XML file. 

4875 

4876 # Write the XML declaration. 

4877 self._xml_declaration() 

4878 

4879 # Write the root worksheet element. 

4880 self._write_worksheet() 

4881 

4882 # Write the worksheet properties. 

4883 self._write_sheet_pr() 

4884 

4885 # Write the worksheet dimensions. 

4886 self._write_dimension() 

4887 

4888 # Write the sheet view properties. 

4889 self._write_sheet_views() 

4890 

4891 # Write the sheet format properties. 

4892 self._write_sheet_format_pr() 

4893 

4894 # Write the sheet column info. 

4895 self._write_cols() 

4896 

4897 # Write the worksheet data such as rows columns and cells. 

4898 if not self.constant_memory: 

4899 self._write_sheet_data() 

4900 else: 

4901 self._write_optimized_sheet_data() 

4902 

4903 # Write the sheetProtection element. 

4904 self._write_sheet_protection() 

4905 

4906 # Write the protectedRanges element. 

4907 self._write_protected_ranges() 

4908 

4909 # Write the phoneticPr element. 

4910 if self.excel2003_style: 

4911 self._write_phonetic_pr() 

4912 

4913 # Write the autoFilter element. 

4914 self._write_auto_filter() 

4915 

4916 # Write the mergeCells element. 

4917 self._write_merge_cells() 

4918 

4919 # Write the conditional formats. 

4920 self._write_conditional_formats() 

4921 

4922 # Write the dataValidations element. 

4923 self._write_data_validations() 

4924 

4925 # Write the hyperlink element. 

4926 self._write_hyperlinks() 

4927 

4928 # Write the printOptions element. 

4929 self._write_print_options() 

4930 

4931 # Write the worksheet page_margins. 

4932 self._write_page_margins() 

4933 

4934 # Write the worksheet page setup. 

4935 self._write_page_setup() 

4936 

4937 # Write the headerFooter element. 

4938 self._write_header_footer() 

4939 

4940 # Write the rowBreaks element. 

4941 self._write_row_breaks() 

4942 

4943 # Write the colBreaks element. 

4944 self._write_col_breaks() 

4945 

4946 # Write the ignoredErrors element. 

4947 self._write_ignored_errors() 

4948 

4949 # Write the drawing element. 

4950 self._write_drawings() 

4951 

4952 # Write the legacyDrawing element. 

4953 self._write_legacy_drawing() 

4954 

4955 # Write the legacyDrawingHF element. 

4956 self._write_legacy_drawing_hf() 

4957 

4958 # Write the picture element, for the background. 

4959 self._write_picture() 

4960 

4961 # Write the tableParts element. 

4962 self._write_table_parts() 

4963 

4964 # Write the extLst elements. 

4965 self._write_ext_list() 

4966 

4967 # Close the worksheet tag. 

4968 self._xml_end_tag("worksheet") 

4969 

4970 # Close the file. 

4971 self._xml_close() 

4972 

4973 def _check_dimensions( 

4974 self, row: int, col: int, ignore_row=False, ignore_col=False 

4975 ) -> int: 

4976 # Check that row and col are valid and store the max and min 

4977 # values for use in other methods/elements. The ignore_row / 

4978 # ignore_col flags is used to indicate that we wish to perform 

4979 # the dimension check without storing the value. The ignore 

4980 # flags are use by set_row() and data_validate. 

4981 

4982 # Check that the row/col are within the worksheet bounds. 

4983 if row < 0 or col < 0: 

4984 return -1 

4985 if row >= self.xls_rowmax or col >= self.xls_colmax: 

4986 return -1 

4987 

4988 # In constant_memory mode we don't change dimensions for rows 

4989 # that are already written. 

4990 if not ignore_row and not ignore_col and self.constant_memory: 

4991 if row < self.previous_row: 

4992 return -2 

4993 

4994 if not ignore_row: 

4995 if self.dim_rowmin is None or row < self.dim_rowmin: 

4996 self.dim_rowmin = row 

4997 if self.dim_rowmax is None or row > self.dim_rowmax: 

4998 self.dim_rowmax = row 

4999 

5000 if not ignore_col: 

5001 if self.dim_colmin is None or col < self.dim_colmin: 

5002 self.dim_colmin = col 

5003 if self.dim_colmax is None or col > self.dim_colmax: 

5004 self.dim_colmax = col 

5005 

5006 return 0 

5007 

5008 def _convert_date_time(self, dt_obj): 

5009 # Convert a datetime object to an Excel serial date and time. 

5010 return _datetime_to_excel_datetime(dt_obj, self.date_1904, self.remove_timezone) 

5011 

5012 def _convert_name_area(self, row_num_1, col_num_1, row_num_2, col_num_2): 

5013 # Convert zero indexed rows and columns to the format required by 

5014 # worksheet named ranges, eg, "Sheet1!$A$1:$C$13". 

5015 

5016 range1 = "" 

5017 range2 = "" 

5018 area = "" 

5019 row_col_only = 0 

5020 

5021 # Convert to A1 notation. 

5022 col_char_1 = xl_col_to_name(col_num_1, 1) 

5023 col_char_2 = xl_col_to_name(col_num_2, 1) 

5024 row_char_1 = "$" + str(row_num_1 + 1) 

5025 row_char_2 = "$" + str(row_num_2 + 1) 

5026 

5027 # We need to handle special cases that refer to rows or columns only. 

5028 if row_num_1 == 0 and row_num_2 == self.xls_rowmax - 1: 

5029 range1 = col_char_1 

5030 range2 = col_char_2 

5031 row_col_only = 1 

5032 elif col_num_1 == 0 and col_num_2 == self.xls_colmax - 1: 

5033 range1 = row_char_1 

5034 range2 = row_char_2 

5035 row_col_only = 1 

5036 else: 

5037 range1 = col_char_1 + row_char_1 

5038 range2 = col_char_2 + row_char_2 

5039 

5040 # A repeated range is only written once (if it isn't a special case). 

5041 if range1 == range2 and not row_col_only: 

5042 area = range1 

5043 else: 

5044 area = range1 + ":" + range2 

5045 

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

5047 sheetname = quote_sheetname(self.name) 

5048 area = sheetname + "!" + area 

5049 

5050 return area 

5051 

5052 def _sort_pagebreaks(self, breaks): 

5053 # This is an internal method used to filter elements of a list of 

5054 # pagebreaks used in the _store_hbreak() and _store_vbreak() methods. 

5055 # It: 

5056 # 1. Removes duplicate entries from the list. 

5057 # 2. Sorts the list. 

5058 # 3. Removes 0 from the list if present. 

5059 if not breaks: 

5060 return [] 

5061 

5062 breaks_set = set(breaks) 

5063 

5064 if 0 in breaks_set: 

5065 breaks_set.remove(0) 

5066 

5067 breaks_list = list(breaks_set) 

5068 breaks_list.sort() 

5069 

5070 # The Excel 2007 specification says that the maximum number of page 

5071 # breaks is 1026. However, in practice it is actually 1023. 

5072 max_num_breaks = 1023 

5073 if len(breaks_list) > max_num_breaks: 

5074 breaks_list = breaks_list[:max_num_breaks] 

5075 

5076 return breaks_list 

5077 

5078 def _extract_filter_tokens(self, expression): 

5079 # Extract the tokens from the filter expression. The tokens are mainly 

5080 # non-whitespace groups. The only tricky part is to extract string 

5081 # tokens that contain whitespace and/or quoted double quotes (Excel's 

5082 # escaped quotes). 

5083 # 

5084 # Examples: 'x < 2000' 

5085 # 'x > 2000 and x < 5000' 

5086 # 'x = "foo"' 

5087 # 'x = "foo bar"' 

5088 # 'x = "foo "" bar"' 

5089 # 

5090 if not expression: 

5091 return [] 

5092 

5093 token_re = re.compile(r'"(?:[^"]|"")*"|\S+') 

5094 tokens = token_re.findall(expression) 

5095 

5096 new_tokens = [] 

5097 # Remove single leading and trailing quotes and un-escape other quotes. 

5098 for token in tokens: 

5099 if token.startswith('"'): 

5100 token = token[1:] 

5101 

5102 if token.endswith('"'): 

5103 token = token[:-1] 

5104 

5105 token = token.replace('""', '"') 

5106 

5107 new_tokens.append(token) 

5108 

5109 return new_tokens 

5110 

5111 def _parse_filter_expression(self, expression, tokens): 

5112 # Converts the tokens of a possibly conditional expression into 1 or 2 

5113 # sub expressions for further parsing. 

5114 # 

5115 # Examples: 

5116 # ('x', '==', 2000) -> exp1 

5117 # ('x', '>', 2000, 'and', 'x', '<', 5000) -> exp1 and exp2 

5118 

5119 if len(tokens) == 7: 

5120 # The number of tokens will be either 3 (for 1 expression) 

5121 # or 7 (for 2 expressions). 

5122 conditional = tokens[3] 

5123 

5124 if re.match("(and|&&)", conditional): 

5125 conditional = 0 

5126 elif re.match(r"(or|\|\|)", conditional): 

5127 conditional = 1 

5128 else: 

5129 warn( 

5130 f"Token '{conditional}' is not a valid conditional " 

5131 f"in filter expression '{expression}'" 

5132 ) 

5133 

5134 expression_1 = self._parse_filter_tokens(expression, tokens[0:3]) 

5135 expression_2 = self._parse_filter_tokens(expression, tokens[4:7]) 

5136 return expression_1 + [conditional] + expression_2 

5137 

5138 return self._parse_filter_tokens(expression, tokens) 

5139 

5140 def _parse_filter_tokens(self, expression, tokens): 

5141 # Parse the 3 tokens of a filter expression and return the operator 

5142 # and token. The use of numbers instead of operators is a legacy of 

5143 # Spreadsheet::WriteExcel. 

5144 operators = { 

5145 "==": 2, 

5146 "=": 2, 

5147 "=~": 2, 

5148 "eq": 2, 

5149 "!=": 5, 

5150 "!~": 5, 

5151 "ne": 5, 

5152 "<>": 5, 

5153 "<": 1, 

5154 "<=": 3, 

5155 ">": 4, 

5156 ">=": 6, 

5157 } 

5158 

5159 operator = operators.get(tokens[1], None) 

5160 token = tokens[2] 

5161 

5162 # Special handling of "Top" filter expressions. 

5163 if re.match("top|bottom", tokens[0].lower()): 

5164 value = int(tokens[1]) 

5165 

5166 if value < 1 or value > 500: 

5167 warn( 

5168 f"The value '{token}' in expression '{expression}' " 

5169 f"must be in the range 1 to 500" 

5170 ) 

5171 

5172 token = token.lower() 

5173 

5174 if token not in ("items", "%"): 

5175 warn( 

5176 f"The type '{token}' in expression '{expression}' " 

5177 f"must be either 'items' or '%%'" 

5178 ) 

5179 

5180 if tokens[0].lower() == "top": 

5181 operator = 30 

5182 else: 

5183 operator = 32 

5184 

5185 if tokens[2] == "%": 

5186 operator += 1 

5187 

5188 token = str(value) 

5189 

5190 if not operator and tokens[0]: 

5191 warn( 

5192 f"Token '{token[0]}' is not a valid operator " 

5193 f"in filter expression '{expression}'." 

5194 ) 

5195 

5196 # Special handling for Blanks/NonBlanks. 

5197 if re.match("blanks|nonblanks", token.lower()): 

5198 # Only allow Equals or NotEqual in this context. 

5199 if operator not in (2, 5): 

5200 warn( 

5201 f"The operator '{tokens[1]}' in expression '{expression}' " 

5202 f"is not valid in relation to Blanks/NonBlanks'." 

5203 ) 

5204 

5205 token = token.lower() 

5206 

5207 # The operator should always be 2 (=) to flag a "simple" equality 

5208 # in the binary record. Therefore we convert <> to =. 

5209 if token == "blanks": 

5210 if operator == 5: 

5211 token = " " 

5212 else: 

5213 if operator == 5: 

5214 operator = 2 

5215 token = "blanks" 

5216 else: 

5217 operator = 5 

5218 token = " " 

5219 

5220 # if the string token contains an Excel match character then change the 

5221 # operator type to indicate a non "simple" equality. 

5222 if operator == 2 and re.search("[*?]", token): 

5223 operator = 22 

5224 

5225 return [operator, token] 

5226 

5227 def _encode_password(self, password) -> str: 

5228 # Hash a worksheet password. Based on the algorithm in 

5229 # ECMA-376-4:2016, Office Open XML File Formats — Transitional 

5230 # Migration Features, Additional attributes for workbookProtection 

5231 # element (Part 1, §18.2.29). 

5232 digest = 0x0000 

5233 

5234 for char in password[::-1]: 

5235 digest = ((digest >> 14) & 0x01) | ((digest << 1) & 0x7FFF) 

5236 digest ^= ord(char) 

5237 

5238 digest = ((digest >> 14) & 0x01) | ((digest << 1) & 0x7FFF) 

5239 digest ^= len(password) 

5240 digest ^= 0xCE4B 

5241 

5242 return f"{digest:X}" 

5243 

5244 def _image_from_source(self, source, options: Optional[Dict[str, Any]] = None): 

5245 # Backward compatibility utility method to convert an input argument to 

5246 # an Image object. The source can be a filename, BytesIO stream or 

5247 # an existing Image object. 

5248 if isinstance(source, Image): 

5249 image = source 

5250 elif options is not None and options.get("image_data"): 

5251 image = Image(options["image_data"]) 

5252 image.image_name = source 

5253 else: 

5254 image = Image(source) 

5255 

5256 return image 

5257 

5258 def _prepare_image( 

5259 self, 

5260 image: Image, 

5261 image_id: int, 

5262 drawing_id: int, 

5263 ) -> None: 

5264 # Set up images/drawings. 

5265 

5266 # Get the effective image width and height in pixels. 

5267 width = image._width * image._x_scale 

5268 height = image._height * image._y_scale 

5269 

5270 # Scale by non 96dpi resolutions. 

5271 width *= 96.0 / image._x_dpi 

5272 height *= 96.0 / image._y_dpi 

5273 

5274 dimensions = self._position_object_emus( 

5275 image._col, 

5276 image._row, 

5277 image._x_offset, 

5278 image._y_offset, 

5279 width, 

5280 height, 

5281 image._anchor, 

5282 ) 

5283 

5284 # Convert from pixels to emus. 

5285 width = int(0.5 + (width * 9525)) 

5286 height = int(0.5 + (height * 9525)) 

5287 

5288 # Create a Drawing obj to use with worksheet unless one already exists. 

5289 if not self.drawing: 

5290 drawing = Drawing() 

5291 drawing.embedded = 1 

5292 self.drawing = drawing 

5293 

5294 self.external_drawing_links.append( 

5295 ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml", None] 

5296 ) 

5297 else: 

5298 drawing = self.drawing 

5299 

5300 drawing_object = DrawingInfo() 

5301 drawing_object._drawing_type = DrawingTypes.IMAGE 

5302 drawing_object._dimensions = dimensions 

5303 drawing_object._description = image.image_name 

5304 drawing_object._width = width 

5305 drawing_object._height = height 

5306 drawing_object._shape = None 

5307 drawing_object._anchor = image._anchor 

5308 drawing_object._rel_index = 0 

5309 drawing_object._decorative = image._decorative 

5310 

5311 if image.description is not None: 

5312 drawing_object._description = image.description 

5313 

5314 if image._url: 

5315 url = image._url 

5316 target = url._target() 

5317 target_mode = url._target_mode() 

5318 

5319 if not self.drawing_rels.get(url._link): 

5320 self.drawing_links.append(["/hyperlink", target, target_mode]) 

5321 

5322 url._rel_index = self._get_drawing_rel_index(url._link) 

5323 drawing_object._url = url 

5324 

5325 if not self.drawing_rels.get(image._digest): 

5326 self.drawing_links.append( 

5327 [ 

5328 "/image", 

5329 "../media/image" + str(image_id) + "." + image._image_extension, 

5330 ] 

5331 ) 

5332 

5333 drawing_object._rel_index = self._get_drawing_rel_index(image._digest) 

5334 drawing._add_drawing_object(drawing_object) 

5335 

5336 def _prepare_shape(self, index, drawing_id) -> None: 

5337 # Set up shapes/drawings. 

5338 ( 

5339 row, 

5340 col, 

5341 x_offset, 

5342 y_offset, 

5343 x_scale, 

5344 y_scale, 

5345 text, 

5346 anchor, 

5347 options, 

5348 description, 

5349 decorative, 

5350 ) = self.shapes[index] 

5351 

5352 width = options.get("width", self.default_col_pixels * 3) 

5353 height = options.get("height", self.default_row_pixels * 6) 

5354 

5355 width *= x_scale 

5356 height *= y_scale 

5357 

5358 dimensions = self._position_object_emus( 

5359 col, row, x_offset, y_offset, width, height, anchor 

5360 ) 

5361 

5362 # Convert from pixels to emus. 

5363 width = int(0.5 + (width * 9525)) 

5364 height = int(0.5 + (height * 9525)) 

5365 

5366 # Create a Drawing obj to use with worksheet unless one already exists. 

5367 if not self.drawing: 

5368 drawing = Drawing() 

5369 drawing.embedded = 1 

5370 self.drawing = drawing 

5371 

5372 self.external_drawing_links.append( 

5373 ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml", None] 

5374 ) 

5375 else: 

5376 drawing = self.drawing 

5377 

5378 shape = Shape("rect", "TextBox", options) 

5379 shape.text = text 

5380 

5381 drawing_object = DrawingInfo() 

5382 drawing_object._drawing_type = DrawingTypes.SHAPE 

5383 drawing_object._dimensions = dimensions 

5384 drawing_object._width = width 

5385 drawing_object._height = height 

5386 drawing_object._description = description 

5387 drawing_object._shape = shape 

5388 drawing_object._anchor = anchor 

5389 drawing_object._rel_index = 0 

5390 drawing_object._decorative = decorative 

5391 

5392 url = Url.from_options(options) 

5393 if url: 

5394 target = url._target() 

5395 target_mode = url._target_mode() 

5396 

5397 if not self.drawing_rels.get(url._link): 

5398 self.drawing_links.append(["/hyperlink", target, target_mode]) 

5399 

5400 url._rel_index = self._get_drawing_rel_index(url._link) 

5401 drawing_object._url = url 

5402 

5403 drawing._add_drawing_object(drawing_object) 

5404 

5405 def _prepare_header_image(self, image_id, image) -> None: 

5406 # Set up an image without a drawing object for header/footer images. 

5407 

5408 # Strip the extension from the filename. 

5409 image.image_name = re.sub(r"\..*$", "", image.image_name) 

5410 

5411 if not self.vml_drawing_rels.get(image._digest): 

5412 self.vml_drawing_links.append( 

5413 [ 

5414 "/image", 

5415 "../media/image" + str(image_id) + "." + image._image_extension, 

5416 ] 

5417 ) 

5418 

5419 image._ref_id = self._get_vml_drawing_rel_index(image._digest) 

5420 

5421 self.header_images_list.append(image) 

5422 

5423 def _prepare_background(self, image_id, image_extension) -> None: 

5424 # Set up an image without a drawing object for backgrounds. 

5425 self.external_background_links.append( 

5426 ["/image", "../media/image" + str(image_id) + "." + image_extension] 

5427 ) 

5428 

5429 def _prepare_chart(self, index, chart_id, drawing_id) -> None: 

5430 # Set up chart/drawings. 

5431 ( 

5432 row, 

5433 col, 

5434 chart, 

5435 x_offset, 

5436 y_offset, 

5437 x_scale, 

5438 y_scale, 

5439 anchor, 

5440 description, 

5441 decorative, 

5442 ) = self.charts[index] 

5443 

5444 chart.id = chart_id - 1 

5445 

5446 # Use user specified dimensions, if any. 

5447 width = int(0.5 + (chart.width * x_scale)) 

5448 height = int(0.5 + (chart.height * y_scale)) 

5449 

5450 dimensions = self._position_object_emus( 

5451 col, row, x_offset, y_offset, width, height, anchor 

5452 ) 

5453 

5454 # Set the chart name for the embedded object if it has been specified. 

5455 name = chart.chart_name 

5456 

5457 # Create a Drawing obj to use with worksheet unless one already exists. 

5458 if not self.drawing: 

5459 drawing = Drawing() 

5460 drawing.embedded = 1 

5461 self.drawing = drawing 

5462 

5463 self.external_drawing_links.append( 

5464 ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml"] 

5465 ) 

5466 else: 

5467 drawing = self.drawing 

5468 

5469 drawing_object = DrawingInfo() 

5470 drawing_object._drawing_type = DrawingTypes.CHART 

5471 drawing_object._dimensions = dimensions 

5472 drawing_object._width = width 

5473 drawing_object._height = height 

5474 drawing_object._name = name 

5475 drawing_object._shape = None 

5476 drawing_object._anchor = anchor 

5477 drawing_object._rel_index = self._get_drawing_rel_index() 

5478 drawing_object._description = description 

5479 drawing_object._decorative = decorative 

5480 

5481 drawing._add_drawing_object(drawing_object) 

5482 

5483 self.drawing_links.append( 

5484 ["/chart", "../charts/chart" + str(chart_id) + ".xml"] 

5485 ) 

5486 

5487 def _position_object_emus( 

5488 self, col_start, row_start, x1, y1, width, height, anchor 

5489 ): 

5490 # Calculate the vertices that define the position of a graphical 

5491 # object within the worksheet in EMUs. 

5492 # 

5493 # The vertices are expressed as English Metric Units (EMUs). There are 

5494 # 12,700 EMUs per point. Therefore, 12,700 * 3 /4 = 9,525 EMUs per 

5495 # pixel 

5496 ( 

5497 col_start, 

5498 row_start, 

5499 x1, 

5500 y1, 

5501 col_end, 

5502 row_end, 

5503 x2, 

5504 y2, 

5505 x_abs, 

5506 y_abs, 

5507 ) = self._position_object_pixels( 

5508 col_start, row_start, x1, y1, width, height, anchor 

5509 ) 

5510 

5511 # Convert the pixel values to EMUs. See above. 

5512 x1 = int(0.5 + 9525 * x1) 

5513 y1 = int(0.5 + 9525 * y1) 

5514 x2 = int(0.5 + 9525 * x2) 

5515 y2 = int(0.5 + 9525 * y2) 

5516 x_abs = int(0.5 + 9525 * x_abs) 

5517 y_abs = int(0.5 + 9525 * y_abs) 

5518 

5519 return (col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs) 

5520 

5521 # Calculate the vertices that define the position of a graphical object 

5522 # within the worksheet in pixels. 

5523 # 

5524 # +------------+------------+ 

5525 # | A | B | 

5526 # +-----+------------+------------+ 

5527 # | |(x1,y1) | | 

5528 # | 1 |(A1)._______|______ | 

5529 # | | | | | 

5530 # | | | | | 

5531 # +-----+----| OBJECT |-----+ 

5532 # | | | | | 

5533 # | 2 | |______________. | 

5534 # | | | (B2)| 

5535 # | | | (x2,y2)| 

5536 # +---- +------------+------------+ 

5537 # 

5538 # Example of an object that covers some of the area from cell A1 to B2. 

5539 # 

5540 # Based on the width and height of the object we need to calculate 8 vars: 

5541 # 

5542 # col_start, row_start, col_end, row_end, x1, y1, x2, y2. 

5543 # 

5544 # We also calculate the absolute x and y position of the top left vertex of 

5545 # the object. This is required for images. 

5546 # 

5547 # The width and height of the cells that the object occupies can be 

5548 # variable and have to be taken into account. 

5549 # 

5550 # The values of col_start and row_start are passed in from the calling 

5551 # function. The values of col_end and row_end are calculated by 

5552 # subtracting the width and height of the object from the width and 

5553 # height of the underlying cells. 

5554 # 

5555 def _position_object_pixels( 

5556 self, col_start, row_start, x1, y1, width, height, anchor 

5557 ): 

5558 # col_start # Col containing upper left corner of object. 

5559 # x1 # Distance to left side of object. 

5560 # 

5561 # row_start # Row containing top left corner of object. 

5562 # y1 # Distance to top of object. 

5563 # 

5564 # col_end # Col containing lower right corner of object. 

5565 # x2 # Distance to right side of object. 

5566 # 

5567 # row_end # Row containing bottom right corner of object. 

5568 # y2 # Distance to bottom of object. 

5569 # 

5570 # width # Width of object frame. 

5571 # height # Height of object frame. 

5572 # 

5573 # x_abs # Absolute distance to left side of object. 

5574 # y_abs # Absolute distance to top side of object. 

5575 x_abs = 0 

5576 y_abs = 0 

5577 

5578 # Adjust start column for negative offsets. 

5579 # pylint: disable=chained-comparison 

5580 while x1 < 0 and col_start > 0: 

5581 x1 += self._size_col(col_start - 1) 

5582 col_start -= 1 

5583 

5584 # Adjust start row for negative offsets. 

5585 while y1 < 0 and row_start > 0: 

5586 y1 += self._size_row(row_start - 1) 

5587 row_start -= 1 

5588 

5589 # Ensure that the image isn't shifted off the page at top left. 

5590 x1 = max(0, x1) 

5591 y1 = max(0, y1) 

5592 

5593 # Calculate the absolute x offset of the top-left vertex. 

5594 if self.col_size_changed: 

5595 for col_id in range(col_start): 

5596 x_abs += self._size_col(col_id) 

5597 else: 

5598 # Optimization for when the column widths haven't changed. 

5599 x_abs += self.default_col_pixels * col_start 

5600 

5601 x_abs += x1 

5602 

5603 # Calculate the absolute y offset of the top-left vertex. 

5604 if self.row_size_changed: 

5605 for row_id in range(row_start): 

5606 y_abs += self._size_row(row_id) 

5607 else: 

5608 # Optimization for when the row heights haven't changed. 

5609 y_abs += self.default_row_pixels * row_start 

5610 

5611 y_abs += y1 

5612 

5613 # Adjust start column for offsets that are greater than the col width. 

5614 while x1 >= self._size_col(col_start, anchor): 

5615 x1 -= self._size_col(col_start) 

5616 col_start += 1 

5617 

5618 # Adjust start row for offsets that are greater than the row height. 

5619 while y1 >= self._size_row(row_start, anchor): 

5620 y1 -= self._size_row(row_start) 

5621 row_start += 1 

5622 

5623 # Initialize end cell to the same as the start cell. 

5624 col_end = col_start 

5625 row_end = row_start 

5626 

5627 # Don't offset the image in the cell if the row/col is hidden. 

5628 if self._size_col(col_start, anchor) > 0: 

5629 width = width + x1 

5630 if self._size_row(row_start, anchor) > 0: 

5631 height = height + y1 

5632 

5633 # Subtract the underlying cell widths to find end cell of the object. 

5634 while width >= self._size_col(col_end, anchor): 

5635 width -= self._size_col(col_end, anchor) 

5636 col_end += 1 

5637 

5638 # Subtract the underlying cell heights to find end cell of the object. 

5639 while height >= self._size_row(row_end, anchor): 

5640 height -= self._size_row(row_end, anchor) 

5641 row_end += 1 

5642 

5643 # The end vertices are whatever is left from the width and height. 

5644 x2 = width 

5645 y2 = height 

5646 

5647 return [col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs] 

5648 

5649 def _size_col(self, col: int, anchor=0): 

5650 # Convert the width of a cell from character units to pixels. Excel 

5651 # rounds the column width to the nearest pixel. If the width hasn't 

5652 # been set by the user we use the default value. A hidden column is 

5653 # treated as having a width of zero unless it has the special 

5654 # "object_position" of 4 (size with cells). 

5655 max_digit_width = 7 # For Calibri 11. 

5656 padding = 5 

5657 pixels = 0 

5658 

5659 # Look up the cell value to see if it has been changed. 

5660 if col in self.col_info: 

5661 width = self.col_info[col][0] 

5662 hidden = self.col_info[col][2] 

5663 

5664 if width is None: 

5665 width = self.default_col_width 

5666 

5667 # Convert to pixels. 

5668 if hidden and anchor != 4: 

5669 pixels = 0 

5670 elif width < 1: 

5671 pixels = int(width * (max_digit_width + padding) + 0.5) 

5672 else: 

5673 pixels = int(width * max_digit_width + 0.5) + padding 

5674 else: 

5675 pixels = self.default_col_pixels 

5676 

5677 return pixels 

5678 

5679 def _size_row(self, row: int, anchor=0): 

5680 # Convert the height of a cell from character units to pixels. If the 

5681 # height hasn't been set by the user we use the default value. A 

5682 # hidden row is treated as having a height of zero unless it has the 

5683 # special "object_position" of 4 (size with cells). 

5684 pixels = 0 

5685 

5686 # Look up the cell value to see if it has been changed 

5687 if row in self.row_sizes: 

5688 height = self.row_sizes[row][0] 

5689 hidden = self.row_sizes[row][1] 

5690 

5691 if hidden and anchor != 4: 

5692 pixels = 0 

5693 else: 

5694 pixels = int(4.0 / 3.0 * height) 

5695 else: 

5696 pixels = int(4.0 / 3.0 * self.default_row_height) 

5697 

5698 return pixels 

5699 

5700 def _pixels_to_width(self, pixels): 

5701 # Convert the width of a cell from pixels to character units. 

5702 max_digit_width = 7.0 # For Calabri 11. 

5703 padding = 5.0 

5704 

5705 if pixels <= 12: 

5706 width = pixels / (max_digit_width + padding) 

5707 else: 

5708 width = (pixels - padding) / max_digit_width 

5709 

5710 return width 

5711 

5712 def _pixels_to_height(self, pixels): 

5713 # Convert the height of a cell from pixels to character units. 

5714 return 0.75 * pixels 

5715 

5716 def _comment_vertices(self, comment: CommentType): 

5717 # Calculate the positions of the comment object. 

5718 anchor = 0 

5719 vertices = self._position_object_pixels( 

5720 comment.start_col, 

5721 comment.start_row, 

5722 comment.x_offset, 

5723 comment.y_offset, 

5724 comment.width, 

5725 comment.height, 

5726 anchor, 

5727 ) 

5728 

5729 # Add the width and height for VML. 

5730 vertices.append(comment.width) 

5731 vertices.append(comment.height) 

5732 

5733 return vertices 

5734 

5735 def _button_vertices(self, button: ButtonType): 

5736 # Calculate the positions of the button object. 

5737 anchor = 0 

5738 vertices = self._position_object_pixels( 

5739 button.col, 

5740 button.row, 

5741 button.x_offset, 

5742 button.y_offset, 

5743 button.width, 

5744 button.height, 

5745 anchor, 

5746 ) 

5747 

5748 # Add the width and height for VML. 

5749 vertices.append(button.width) 

5750 vertices.append(button.height) 

5751 

5752 return vertices 

5753 

5754 def _prepare_vml_objects( 

5755 self, vml_data_id, vml_shape_id, vml_drawing_id, comment_id 

5756 ): 

5757 comments = [] 

5758 # Sort the comments into row/column order for easier comparison 

5759 # testing and set the external links for comments and buttons. 

5760 row_nums = sorted(self.comments.keys()) 

5761 

5762 for row in row_nums: 

5763 col_nums = sorted(self.comments[row].keys()) 

5764 

5765 for col in col_nums: 

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

5767 comment.vertices = self._comment_vertices(comment) 

5768 

5769 # Set comment visibility if required and not user defined. 

5770 if comment.is_visible is None: 

5771 comment.is_visible = self.comments_visible 

5772 

5773 # Set comment author if not already user defined. 

5774 if comment.author is None: 

5775 comment.author = self.comments_author 

5776 

5777 comments.append(comment) 

5778 

5779 for button in self.buttons_list: 

5780 button.vertices = self._button_vertices(button) 

5781 

5782 self.external_vml_links.append( 

5783 ["/vmlDrawing", "../drawings/vmlDrawing" + str(vml_drawing_id) + ".vml"] 

5784 ) 

5785 

5786 if self.has_comments: 

5787 self.comments_list = comments 

5788 

5789 self.external_comment_links.append( 

5790 ["/comments", "../comments" + str(comment_id) + ".xml"] 

5791 ) 

5792 

5793 count = len(comments) 

5794 start_data_id = vml_data_id 

5795 

5796 # The VML o:idmap data id contains a comma separated range when there 

5797 # is more than one 1024 block of comments, like this: data="1,2". 

5798 for i in range(int(count / 1024)): 

5799 data_id = start_data_id + i + 1 

5800 vml_data_id = f"{vml_data_id},{data_id}" 

5801 

5802 self.vml_data_id = vml_data_id 

5803 self.vml_shape_id = vml_shape_id 

5804 

5805 return count 

5806 

5807 def _prepare_header_vml_objects(self, vml_header_id, vml_drawing_id) -> None: 

5808 # Set up external linkage for VML header/footer images. 

5809 

5810 self.vml_header_id = vml_header_id 

5811 

5812 self.external_vml_links.append( 

5813 ["/vmlDrawing", "../drawings/vmlDrawing" + str(vml_drawing_id) + ".vml"] 

5814 ) 

5815 

5816 def _prepare_tables(self, table_id, seen) -> None: 

5817 # Set the table ids for the worksheet tables. 

5818 for table in self.tables: 

5819 table["id"] = table_id 

5820 

5821 if table.get("name") is None: 

5822 # Set a default name. 

5823 table["name"] = "Table" + str(table_id) 

5824 

5825 # Check for duplicate table names. 

5826 name = table["name"].lower() 

5827 

5828 if name in seen: 

5829 raise DuplicateTableName( 

5830 f"Duplicate name '{table['name']}' used in worksheet.add_table()." 

5831 ) 

5832 

5833 seen[name] = True 

5834 

5835 # Store the link used for the rels file. 

5836 self.external_table_links.append( 

5837 ["/table", "../tables/table" + str(table_id) + ".xml"] 

5838 ) 

5839 table_id += 1 

5840 

5841 def _table_function_to_formula(self, function, col_name): 

5842 # Convert a table total function to a worksheet formula. 

5843 formula = "" 

5844 

5845 # Escape special characters, as required by Excel. 

5846 col_name = col_name.replace("'", "''") 

5847 col_name = col_name.replace("#", "'#") 

5848 col_name = col_name.replace("]", "']") 

5849 col_name = col_name.replace("[", "'[") 

5850 

5851 subtotals = { 

5852 "average": 101, 

5853 "countNums": 102, 

5854 "count": 103, 

5855 "max": 104, 

5856 "min": 105, 

5857 "stdDev": 107, 

5858 "sum": 109, 

5859 "var": 110, 

5860 } 

5861 

5862 if function in subtotals: 

5863 func_num = subtotals[function] 

5864 formula = f"SUBTOTAL({func_num},[{col_name}])" 

5865 else: 

5866 warn(f"Unsupported function '{function}' in add_table()") 

5867 

5868 return formula 

5869 

5870 def _set_spark_color(self, sparkline, options, user_color) -> None: 

5871 # Set the sparkline color. 

5872 if user_color not in options: 

5873 return 

5874 

5875 sparkline[user_color] = Color._from_value(options[user_color]) 

5876 

5877 def _get_range_data(self, row_start, col_start, row_end, col_end): 

5878 # Returns a range of data from the worksheet _table to be used in 

5879 # chart cached data. Strings are returned as SST ids and decoded 

5880 # in the workbook. Return None for data that doesn't exist since 

5881 # Excel can chart series with data missing. 

5882 

5883 if self.constant_memory: 

5884 return () 

5885 

5886 data = [] 

5887 

5888 # Iterate through the table data. 

5889 for row_num in range(row_start, row_end + 1): 

5890 # Store None if row doesn't exist. 

5891 if row_num not in self.table: 

5892 data.append(None) 

5893 continue 

5894 

5895 for col_num in range(col_start, col_end + 1): 

5896 if col_num in self.table[row_num]: 

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

5898 

5899 cell_type = cell.__class__.__name__ 

5900 

5901 if cell_type in ("Number", "Datetime"): 

5902 # Return a number with Excel's precision. 

5903 data.append(f"{cell.number:.16g}") 

5904 

5905 elif cell_type == "String": 

5906 # Return a string from it's shared string index. 

5907 index = cell.string 

5908 string = self.str_table._get_shared_string(index) 

5909 

5910 data.append(string) 

5911 

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

5913 # Return the formula value. 

5914 value = cell.value 

5915 

5916 if value is None: 

5917 value = 0 

5918 

5919 data.append(value) 

5920 

5921 elif cell_type == "Blank": 

5922 # Return a empty cell. 

5923 data.append("") 

5924 else: 

5925 # Store None if column doesn't exist. 

5926 data.append(None) 

5927 

5928 return data 

5929 

5930 def _csv_join(self, *items): 

5931 # Create a csv string for use with data validation formulas and lists. 

5932 

5933 # Convert non string types to string. 

5934 items = [str(item) if not isinstance(item, str) else item for item in items] 

5935 

5936 return ",".join(items) 

5937 

5938 def _escape_url(self, url): 

5939 # Don't escape URL if it looks already escaped. 

5940 if re.search("%[0-9a-fA-F]{2}", url): 

5941 return url 

5942 

5943 # Can't use url.quote() here because it doesn't match Excel. 

5944 url = url.replace("%", "%25") 

5945 url = url.replace('"', "%22") 

5946 url = url.replace(" ", "%20") 

5947 url = url.replace("<", "%3c") 

5948 url = url.replace(">", "%3e") 

5949 url = url.replace("[", "%5b") 

5950 url = url.replace("]", "%5d") 

5951 url = url.replace("^", "%5e") 

5952 url = url.replace("`", "%60") 

5953 url = url.replace("{", "%7b") 

5954 url = url.replace("}", "%7d") 

5955 

5956 return url 

5957 

5958 def _get_drawing_rel_index(self, target=None): 

5959 # Get the index used to address a drawing rel link. 

5960 if target is None: 

5961 self.drawing_rels_id += 1 

5962 return self.drawing_rels_id 

5963 

5964 if self.drawing_rels.get(target): 

5965 return self.drawing_rels[target] 

5966 

5967 self.drawing_rels_id += 1 

5968 self.drawing_rels[target] = self.drawing_rels_id 

5969 return self.drawing_rels_id 

5970 

5971 def _get_vml_drawing_rel_index(self, target=None): 

5972 # Get the index used to address a vml drawing rel link. 

5973 if self.vml_drawing_rels.get(target): 

5974 return self.vml_drawing_rels[target] 

5975 

5976 self.vml_drawing_rels_id += 1 

5977 self.vml_drawing_rels[target] = self.vml_drawing_rels_id 

5978 return self.vml_drawing_rels_id 

5979 

5980 ########################################################################### 

5981 # 

5982 # The following font methods are mainly duplicated from the Styles class 

5983 # with appropriate changes for rich string styles. 

5984 # 

5985 ########################################################################### 

5986 def _write_font(self, xf_format) -> None: 

5987 # Write the <font> element. 

5988 xml_writer = self.rstring 

5989 

5990 xml_writer._xml_start_tag("rPr") 

5991 

5992 # Handle the main font properties. 

5993 if xf_format.bold: 

5994 xml_writer._xml_empty_tag("b") 

5995 if xf_format.italic: 

5996 xml_writer._xml_empty_tag("i") 

5997 if xf_format.font_strikeout: 

5998 xml_writer._xml_empty_tag("strike") 

5999 if xf_format.font_outline: 

6000 xml_writer._xml_empty_tag("outline") 

6001 if xf_format.font_shadow: 

6002 xml_writer._xml_empty_tag("shadow") 

6003 

6004 # Handle the underline variants. 

6005 if xf_format.underline: 

6006 self._write_underline(xf_format.underline) 

6007 

6008 # Handle super/subscript. 

6009 if xf_format.font_script == 1: 

6010 self._write_vert_align("superscript") 

6011 if xf_format.font_script == 2: 

6012 self._write_vert_align("subscript") 

6013 

6014 # Write the font size 

6015 xml_writer._xml_empty_tag("sz", [("val", xf_format.font_size)]) 

6016 

6017 # Handle colors. 

6018 if xf_format.theme == -1: 

6019 # Ignore for excel2003_style. 

6020 pass 

6021 elif xf_format.theme: 

6022 self._write_rstring_color("color", [("theme", xf_format.theme)]) 

6023 elif xf_format.color_indexed: 

6024 self._write_rstring_color("color", [("indexed", xf_format.color_indexed)]) 

6025 elif xf_format.font_color: 

6026 color = xf_format.font_color 

6027 if not color._is_automatic: 

6028 self._write_rstring_color("color", color._attributes()) 

6029 else: 

6030 self._write_rstring_color("color", [("theme", 1)]) 

6031 

6032 # Write some other font properties related to font families. 

6033 xml_writer._xml_empty_tag("rFont", [("val", xf_format.font_name)]) 

6034 xml_writer._xml_empty_tag("family", [("val", xf_format.font_family)]) 

6035 

6036 if xf_format.font_name == "Calibri" and not xf_format.hyperlink: 

6037 xml_writer._xml_empty_tag("scheme", [("val", xf_format.font_scheme)]) 

6038 

6039 xml_writer._xml_end_tag("rPr") 

6040 

6041 def _write_underline(self, underline) -> None: 

6042 # Write the underline font element. 

6043 attributes = [] 

6044 

6045 # Handle the underline variants. 

6046 if underline == 2: 

6047 attributes = [("val", "double")] 

6048 elif underline == 33: 

6049 attributes = [("val", "singleAccounting")] 

6050 elif underline == 34: 

6051 attributes = [("val", "doubleAccounting")] 

6052 

6053 self.rstring._xml_empty_tag("u", attributes) 

6054 

6055 def _write_vert_align(self, val) -> None: 

6056 # Write the <vertAlign> font sub-element. 

6057 attributes = [("val", val)] 

6058 

6059 self.rstring._xml_empty_tag("vertAlign", attributes) 

6060 

6061 def _write_rstring_color(self, name, attributes) -> None: 

6062 # Write the <color> element. 

6063 self.rstring._xml_empty_tag(name, attributes) 

6064 

6065 def _opt_close(self) -> None: 

6066 # Close the row data filehandle in constant_memory mode. 

6067 if not self.row_data_fh_closed: 

6068 self.row_data_fh.close() 

6069 self.row_data_fh_closed = True 

6070 

6071 def _opt_reopen(self) -> None: 

6072 # Reopen the row data filehandle in constant_memory mode. 

6073 if self.row_data_fh_closed: 

6074 filename = self.row_data_filename 

6075 # pylint: disable=consider-using-with 

6076 self.row_data_fh = open(filename, mode="a+", encoding="utf-8") 

6077 self.row_data_fh_closed = False 

6078 self.fh = self.row_data_fh 

6079 

6080 def _set_icon_props(self, total_icons, user_props=None): 

6081 # Set the sub-properties for icons. 

6082 props = [] 

6083 

6084 # Set the defaults. 

6085 for _ in range(total_icons): 

6086 props.append({"criteria": False, "value": 0, "type": "percent"}) 

6087 

6088 # Set the default icon values based on the number of icons. 

6089 if total_icons == 3: 

6090 props[0]["value"] = 67 

6091 props[1]["value"] = 33 

6092 

6093 if total_icons == 4: 

6094 props[0]["value"] = 75 

6095 props[1]["value"] = 50 

6096 props[2]["value"] = 25 

6097 

6098 if total_icons == 5: 

6099 props[0]["value"] = 80 

6100 props[1]["value"] = 60 

6101 props[2]["value"] = 40 

6102 props[3]["value"] = 20 

6103 

6104 # Overwrite default properties with user defined properties. 

6105 if user_props: 

6106 # Ensure we don't set user properties for lowest icon. 

6107 max_data = len(user_props) 

6108 if max_data >= total_icons: 

6109 max_data = total_icons - 1 

6110 

6111 for i in range(max_data): 

6112 # Set the user defined 'value' property. 

6113 if user_props[i].get("value") is not None: 

6114 props[i]["value"] = user_props[i]["value"] 

6115 

6116 # Remove the formula '=' sign if it exists. 

6117 tmp = props[i]["value"] 

6118 if isinstance(tmp, str) and tmp.startswith("="): 

6119 props[i]["value"] = tmp.lstrip("=") 

6120 

6121 # Set the user defined 'type' property. 

6122 if user_props[i].get("type"): 

6123 valid_types = ("percent", "percentile", "number", "formula") 

6124 

6125 if user_props[i]["type"] not in valid_types: 

6126 warn( 

6127 f"Unknown icon property type '{user_props[i]['type']}' " 

6128 f"for sub-property 'type' in conditional_format()." 

6129 ) 

6130 else: 

6131 props[i]["type"] = user_props[i]["type"] 

6132 

6133 if props[i]["type"] == "number": 

6134 props[i]["type"] = "num" 

6135 

6136 # Set the user defined 'criteria' property. 

6137 criteria = user_props[i].get("criteria") 

6138 if criteria and criteria == ">": 

6139 props[i]["criteria"] = True 

6140 

6141 return props 

6142 

6143 ########################################################################### 

6144 # 

6145 # XML methods. 

6146 # 

6147 ########################################################################### 

6148 

6149 def _write_worksheet(self) -> None: 

6150 # Write the <worksheet> element. This is the root element. 

6151 

6152 schema = "http://schemas.openxmlformats.org/" 

6153 xmlns = schema + "spreadsheetml/2006/main" 

6154 xmlns_r = schema + "officeDocument/2006/relationships" 

6155 xmlns_mc = schema + "markup-compatibility/2006" 

6156 ms_schema = "http://schemas.microsoft.com/" 

6157 xmlns_x14ac = ms_schema + "office/spreadsheetml/2009/9/ac" 

6158 

6159 attributes = [("xmlns", xmlns), ("xmlns:r", xmlns_r)] 

6160 

6161 # Add some extra attributes for Excel 2010. Mainly for sparklines. 

6162 if self.excel_version == 2010: 

6163 attributes.append(("xmlns:mc", xmlns_mc)) 

6164 attributes.append(("xmlns:x14ac", xmlns_x14ac)) 

6165 attributes.append(("mc:Ignorable", "x14ac")) 

6166 

6167 self._xml_start_tag("worksheet", attributes) 

6168 

6169 def _write_dimension(self) -> None: 

6170 # Write the <dimension> element. This specifies the range of 

6171 # cells in the worksheet. As a special case, empty 

6172 # spreadsheets use 'A1' as a range. 

6173 

6174 if self.dim_rowmin is None and self.dim_colmin is None: 

6175 # If the min dimensions are not defined then no dimensions 

6176 # have been set and we use the default 'A1'. 

6177 ref = "A1" 

6178 

6179 elif self.dim_rowmin is None and self.dim_colmin is not None: 

6180 # If the row dimensions aren't set but the column 

6181 # dimensions are set then they have been changed via 

6182 # set_column(). 

6183 

6184 if self.dim_colmin == self.dim_colmax: 

6185 # The dimensions are a single cell and not a range. 

6186 ref = xl_rowcol_to_cell(0, self.dim_colmin) 

6187 else: 

6188 # The dimensions are a cell range. 

6189 cell_1 = xl_rowcol_to_cell(0, self.dim_colmin) 

6190 cell_2 = xl_rowcol_to_cell(0, self.dim_colmax) 

6191 ref = cell_1 + ":" + cell_2 

6192 

6193 elif self.dim_rowmin == self.dim_rowmax and self.dim_colmin == self.dim_colmax: 

6194 # The dimensions are a single cell and not a range. 

6195 ref = xl_rowcol_to_cell(self.dim_rowmin, self.dim_colmin) 

6196 else: 

6197 # The dimensions are a cell range. 

6198 cell_1 = xl_rowcol_to_cell(self.dim_rowmin, self.dim_colmin) 

6199 cell_2 = xl_rowcol_to_cell(self.dim_rowmax, self.dim_colmax) 

6200 ref = cell_1 + ":" + cell_2 

6201 

6202 self._xml_empty_tag("dimension", [("ref", ref)]) 

6203 

6204 def _write_sheet_views(self) -> None: 

6205 # Write the <sheetViews> element. 

6206 self._xml_start_tag("sheetViews") 

6207 

6208 # Write the sheetView element. 

6209 self._write_sheet_view() 

6210 

6211 self._xml_end_tag("sheetViews") 

6212 

6213 def _write_sheet_view(self) -> None: 

6214 # Write the <sheetViews> element. 

6215 attributes = [] 

6216 

6217 # Hide screen gridlines if required. 

6218 if not self.screen_gridlines: 

6219 attributes.append(("showGridLines", 0)) 

6220 

6221 # Hide screen row/column headers. 

6222 if self.row_col_headers: 

6223 attributes.append(("showRowColHeaders", 0)) 

6224 

6225 # Hide zeroes in cells. 

6226 if not self.show_zeros: 

6227 attributes.append(("showZeros", 0)) 

6228 

6229 # Display worksheet right to left for Hebrew, Arabic and others. 

6230 if self.is_right_to_left: 

6231 attributes.append(("rightToLeft", 1)) 

6232 

6233 # Show that the sheet tab is selected. 

6234 if self.selected: 

6235 attributes.append(("tabSelected", 1)) 

6236 

6237 # Turn outlines off. Also required in the outlinePr element. 

6238 if not self.outline_on: 

6239 attributes.append(("showOutlineSymbols", 0)) 

6240 

6241 # Set the page view/layout mode if required. 

6242 if self.page_view == 1: 

6243 attributes.append(("view", "pageLayout")) 

6244 elif self.page_view == 2: 

6245 attributes.append(("view", "pageBreakPreview")) 

6246 

6247 # Set the first visible cell. 

6248 if self.top_left_cell != "": 

6249 attributes.append(("topLeftCell", self.top_left_cell)) 

6250 

6251 # Set the zoom level. 

6252 if self.zoom != 100: 

6253 attributes.append(("zoomScale", self.zoom)) 

6254 

6255 if self.page_view == 0 and self.zoom_scale_normal: 

6256 attributes.append(("zoomScaleNormal", self.zoom)) 

6257 if self.page_view == 1: 

6258 attributes.append(("zoomScalePageLayoutView", self.zoom)) 

6259 if self.page_view == 2: 

6260 attributes.append(("zoomScaleSheetLayoutView", self.zoom)) 

6261 

6262 attributes.append(("workbookViewId", 0)) 

6263 

6264 if self.panes or self.selections: 

6265 self._xml_start_tag("sheetView", attributes) 

6266 self._write_panes() 

6267 self._write_selections() 

6268 self._xml_end_tag("sheetView") 

6269 else: 

6270 self._xml_empty_tag("sheetView", attributes) 

6271 

6272 def _write_sheet_format_pr(self) -> None: 

6273 # Write the <sheetFormatPr> element. 

6274 default_row_height = self.default_row_height 

6275 row_level = self.outline_row_level 

6276 col_level = self.outline_col_level 

6277 

6278 attributes = [("defaultRowHeight", default_row_height)] 

6279 

6280 if self.default_row_height != self.original_row_height: 

6281 attributes.append(("customHeight", 1)) 

6282 

6283 if self.default_row_zeroed: 

6284 attributes.append(("zeroHeight", 1)) 

6285 

6286 if row_level: 

6287 attributes.append(("outlineLevelRow", row_level)) 

6288 if col_level: 

6289 attributes.append(("outlineLevelCol", col_level)) 

6290 

6291 if self.excel_version == 2010: 

6292 attributes.append(("x14ac:dyDescent", "0.25")) 

6293 

6294 self._xml_empty_tag("sheetFormatPr", attributes) 

6295 

6296 def _write_cols(self) -> None: 

6297 # Write the <cols> element and <col> sub elements. 

6298 

6299 # Exit unless some column have been formatted. 

6300 if not self.col_info: 

6301 return 

6302 

6303 self._xml_start_tag("cols") 

6304 

6305 # Use the first element of the column information structures to set 

6306 # the initial/previous properties. 

6307 first_col = (sorted(self.col_info.keys()))[0] 

6308 last_col = first_col 

6309 prev_col_options = self.col_info[first_col] 

6310 del self.col_info[first_col] 

6311 deleted_col = first_col 

6312 deleted_col_options = prev_col_options 

6313 

6314 for col in sorted(self.col_info.keys()): 

6315 col_options = self.col_info[col] 

6316 # Check if the column number is contiguous with the previous 

6317 # column and if the properties are the same. 

6318 if col == last_col + 1 and col_options == prev_col_options: 

6319 last_col = col 

6320 else: 

6321 # If not contiguous/equal then we write out the current range 

6322 # of columns and start again. 

6323 self._write_col_info(first_col, last_col, prev_col_options) 

6324 first_col = col 

6325 last_col = first_col 

6326 prev_col_options = col_options 

6327 

6328 # We will exit the previous loop with one unhandled column range. 

6329 self._write_col_info(first_col, last_col, prev_col_options) 

6330 

6331 # Put back the deleted first column information structure. 

6332 self.col_info[deleted_col] = deleted_col_options 

6333 

6334 self._xml_end_tag("cols") 

6335 

6336 def _write_col_info(self, col_min, col_max, col_info) -> None: 

6337 # Write the <col> element. 

6338 (width, cell_format, hidden, level, collapsed, autofit) = col_info 

6339 

6340 custom_width = 1 

6341 xf_index = 0 

6342 

6343 # Get the cell_format index. 

6344 if cell_format: 

6345 xf_index = cell_format._get_xf_index() 

6346 

6347 # Set the Excel default column width. 

6348 if width is None: 

6349 if not hidden: 

6350 width = 8.43 

6351 custom_width = 0 

6352 else: 

6353 width = 0 

6354 elif width == 8.43: 

6355 # Width is defined but same as default. 

6356 custom_width = 0 

6357 

6358 # Convert column width from user units to character width. 

6359 if width > 0: 

6360 # For Calabri 11. 

6361 max_digit_width = 7 

6362 padding = 5 

6363 

6364 if width < 1: 

6365 width = ( 

6366 int( 

6367 (int(width * (max_digit_width + padding) + 0.5)) 

6368 / float(max_digit_width) 

6369 * 256.0 

6370 ) 

6371 / 256.0 

6372 ) 

6373 else: 

6374 width = ( 

6375 int( 

6376 (int(width * max_digit_width + 0.5) + padding) 

6377 / float(max_digit_width) 

6378 * 256.0 

6379 ) 

6380 / 256.0 

6381 ) 

6382 

6383 attributes = [ 

6384 ("min", col_min + 1), 

6385 ("max", col_max + 1), 

6386 ("width", f"{width:.16g}"), 

6387 ] 

6388 

6389 if xf_index: 

6390 attributes.append(("style", xf_index)) 

6391 if hidden: 

6392 attributes.append(("hidden", "1")) 

6393 if autofit: 

6394 attributes.append(("bestFit", "1")) 

6395 if custom_width: 

6396 attributes.append(("customWidth", "1")) 

6397 if level: 

6398 attributes.append(("outlineLevel", level)) 

6399 if collapsed: 

6400 attributes.append(("collapsed", "1")) 

6401 

6402 self._xml_empty_tag("col", attributes) 

6403 

6404 def _write_sheet_data(self) -> None: 

6405 # Write the <sheetData> element. 

6406 if self.dim_rowmin is None: 

6407 # If the dimensions aren't defined there is no data to write. 

6408 self._xml_empty_tag("sheetData") 

6409 else: 

6410 self._xml_start_tag("sheetData") 

6411 self._write_rows() 

6412 self._xml_end_tag("sheetData") 

6413 

6414 def _write_optimized_sheet_data(self) -> None: 

6415 # Write the <sheetData> element when constant_memory is on. In this 

6416 # case we read the data stored in the temp file and rewrite it to the 

6417 # XML sheet file. 

6418 if self.dim_rowmin is None: 

6419 # If the dimensions aren't defined then there is no data to write. 

6420 self._xml_empty_tag("sheetData") 

6421 else: 

6422 self._xml_start_tag("sheetData") 

6423 

6424 # Rewind the filehandle that was used for temp row data. 

6425 buff_size = 65536 

6426 self.row_data_fh.seek(0) 

6427 data = self.row_data_fh.read(buff_size) 

6428 

6429 while data: 

6430 self.fh.write(data) 

6431 data = self.row_data_fh.read(buff_size) 

6432 

6433 self.row_data_fh.close() 

6434 os.unlink(self.row_data_filename) 

6435 

6436 self._xml_end_tag("sheetData") 

6437 

6438 def _write_page_margins(self) -> None: 

6439 # Write the <pageMargins> element. 

6440 attributes = [ 

6441 ("left", self.margin_left), 

6442 ("right", self.margin_right), 

6443 ("top", self.margin_top), 

6444 ("bottom", self.margin_bottom), 

6445 ("header", self.margin_header), 

6446 ("footer", self.margin_footer), 

6447 ] 

6448 

6449 self._xml_empty_tag("pageMargins", attributes) 

6450 

6451 def _write_page_setup(self) -> None: 

6452 # Write the <pageSetup> element. 

6453 # 

6454 # The following is an example taken from Excel. 

6455 # 

6456 # <pageSetup 

6457 # paperSize="9" 

6458 # scale="110" 

6459 # fitToWidth="2" 

6460 # fitToHeight="2" 

6461 # pageOrder="overThenDown" 

6462 # orientation="portrait" 

6463 # blackAndWhite="1" 

6464 # draft="1" 

6465 # horizontalDpi="200" 

6466 # verticalDpi="200" 

6467 # r:id="rId1" 

6468 # /> 

6469 # 

6470 attributes = [] 

6471 

6472 # Skip this element if no page setup has changed. 

6473 if not self.page_setup_changed: 

6474 return 

6475 

6476 # Set paper size. 

6477 if self.paper_size: 

6478 attributes.append(("paperSize", self.paper_size)) 

6479 

6480 # Set the print_scale. 

6481 if self.print_scale != 100: 

6482 attributes.append(("scale", self.print_scale)) 

6483 

6484 # Set the "Fit to page" properties. 

6485 if self.fit_page and self.fit_width != 1: 

6486 attributes.append(("fitToWidth", self.fit_width)) 

6487 

6488 if self.fit_page and self.fit_height != 1: 

6489 attributes.append(("fitToHeight", self.fit_height)) 

6490 

6491 # Set the page print direction. 

6492 if self.page_order: 

6493 attributes.append(("pageOrder", "overThenDown")) 

6494 

6495 # Set start page for printing. 

6496 if self.page_start > 1: 

6497 attributes.append(("firstPageNumber", self.page_start)) 

6498 

6499 # Set page orientation. 

6500 if self.orientation: 

6501 attributes.append(("orientation", "portrait")) 

6502 else: 

6503 attributes.append(("orientation", "landscape")) 

6504 

6505 # Set the print in black and white option. 

6506 if self.black_white: 

6507 attributes.append(("blackAndWhite", "1")) 

6508 

6509 # Set start page for printing. 

6510 if self.page_start != 0: 

6511 attributes.append(("useFirstPageNumber", "1")) 

6512 

6513 # Set the DPI. Mainly only for testing. 

6514 if self.is_chartsheet: 

6515 if self.horizontal_dpi: 

6516 attributes.append(("horizontalDpi", self.horizontal_dpi)) 

6517 

6518 if self.vertical_dpi: 

6519 attributes.append(("verticalDpi", self.vertical_dpi)) 

6520 else: 

6521 if self.vertical_dpi: 

6522 attributes.append(("verticalDpi", self.vertical_dpi)) 

6523 

6524 if self.horizontal_dpi: 

6525 attributes.append(("horizontalDpi", self.horizontal_dpi)) 

6526 

6527 self._xml_empty_tag("pageSetup", attributes) 

6528 

6529 def _write_print_options(self) -> None: 

6530 # Write the <printOptions> element. 

6531 attributes = [] 

6532 

6533 if not self.print_options_changed: 

6534 return 

6535 

6536 # Set horizontal centering. 

6537 if self.hcenter: 

6538 attributes.append(("horizontalCentered", 1)) 

6539 

6540 # Set vertical centering. 

6541 if self.vcenter: 

6542 attributes.append(("verticalCentered", 1)) 

6543 

6544 # Enable row and column headers. 

6545 if self.print_headers: 

6546 attributes.append(("headings", 1)) 

6547 

6548 # Set printed gridlines. 

6549 if self.print_gridlines: 

6550 attributes.append(("gridLines", 1)) 

6551 

6552 self._xml_empty_tag("printOptions", attributes) 

6553 

6554 def _write_header_footer(self) -> None: 

6555 # Write the <headerFooter> element. 

6556 attributes = [] 

6557 

6558 if not self.header_footer_scales: 

6559 attributes.append(("scaleWithDoc", 0)) 

6560 

6561 if not self.header_footer_aligns: 

6562 attributes.append(("alignWithMargins", 0)) 

6563 

6564 if self.header_footer_changed: 

6565 self._xml_start_tag("headerFooter", attributes) 

6566 if self.header: 

6567 self._write_odd_header() 

6568 if self.footer: 

6569 self._write_odd_footer() 

6570 self._xml_end_tag("headerFooter") 

6571 elif self.excel2003_style: 

6572 self._xml_empty_tag("headerFooter", attributes) 

6573 

6574 def _write_odd_header(self) -> None: 

6575 # Write the <headerFooter> element. 

6576 self._xml_data_element("oddHeader", self.header) 

6577 

6578 def _write_odd_footer(self) -> None: 

6579 # Write the <headerFooter> element. 

6580 self._xml_data_element("oddFooter", self.footer) 

6581 

6582 def _write_rows(self) -> None: 

6583 # Write out the worksheet data as a series of rows and cells. 

6584 self._calculate_spans() 

6585 

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

6587 if ( 

6588 row_num in self.set_rows 

6589 or row_num in self.comments 

6590 or self.table[row_num] 

6591 ): 

6592 # Only process rows with formatting, cell data and/or comments. 

6593 

6594 span_index = int(row_num / 16) 

6595 

6596 if span_index in self.row_spans: 

6597 span = self.row_spans[span_index] 

6598 else: 

6599 span = None 

6600 

6601 if self.table[row_num]: 

6602 # Write the cells if the row contains data. 

6603 if row_num not in self.set_rows: 

6604 self._write_row(row_num, span) 

6605 else: 

6606 self._write_row(row_num, span, self.set_rows[row_num]) 

6607 

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

6609 if col_num in self.table[row_num]: 

6610 col_ref = self.table[row_num][col_num] 

6611 self._write_cell(row_num, col_num, col_ref) 

6612 

6613 self._xml_end_tag("row") 

6614 

6615 elif row_num in self.comments: 

6616 # Row with comments in cells. 

6617 self._write_empty_row(row_num, span, self.set_rows[row_num]) 

6618 else: 

6619 # Blank row with attributes only. 

6620 self._write_empty_row(row_num, span, self.set_rows[row_num]) 

6621 

6622 def _write_single_row(self, current_row_num=0) -> None: 

6623 # Write out the worksheet data as a single row with cells. 

6624 # This method is used when constant_memory is on. A single 

6625 # row is written and the data table is reset. That way only 

6626 # one row of data is kept in memory at any one time. We don't 

6627 # write span data in the optimized case since it is optional. 

6628 

6629 # Set the new previous row as the current row. 

6630 row_num = self.previous_row 

6631 self.previous_row = current_row_num 

6632 

6633 if row_num in self.set_rows or row_num in self.comments or self.table[row_num]: 

6634 # Only process rows with formatting, cell data and/or comments. 

6635 

6636 # No span data in optimized mode. 

6637 span = None 

6638 

6639 if self.table[row_num]: 

6640 # Write the cells if the row contains data. 

6641 if row_num not in self.set_rows: 

6642 self._write_row(row_num, span) 

6643 else: 

6644 self._write_row(row_num, span, self.set_rows[row_num]) 

6645 

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

6647 if col_num in self.table[row_num]: 

6648 col_ref = self.table[row_num][col_num] 

6649 self._write_cell(row_num, col_num, col_ref) 

6650 

6651 self._xml_end_tag("row") 

6652 else: 

6653 # Row attributes or comments only. 

6654 self._write_empty_row(row_num, span, self.set_rows[row_num]) 

6655 

6656 # Reset table. 

6657 self.table.clear() 

6658 

6659 def _calculate_spans(self) -> None: 

6660 # Calculate the "spans" attribute of the <row> tag. This is an 

6661 # XLSX optimization and isn't strictly required. However, it 

6662 # makes comparing files easier. The span is the same for each 

6663 # block of 16 rows. 

6664 spans = {} 

6665 span_min = None 

6666 span_max = None 

6667 

6668 for row_num in range(self.dim_rowmin, self.dim_rowmax + 1): 

6669 if row_num in self.table: 

6670 # Calculate spans for cell data. 

6671 for col_num in range(self.dim_colmin, self.dim_colmax + 1): 

6672 if col_num in self.table[row_num]: 

6673 if span_min is None: 

6674 span_min = col_num 

6675 span_max = col_num 

6676 else: 

6677 span_min = min(span_min, col_num) 

6678 span_max = max(span_max, col_num) 

6679 

6680 if row_num in self.comments: 

6681 # Calculate spans for comments. 

6682 for col_num in range(self.dim_colmin, self.dim_colmax + 1): 

6683 if row_num in self.comments and col_num in self.comments[row_num]: 

6684 if span_min is None: 

6685 span_min = col_num 

6686 span_max = col_num 

6687 else: 

6688 span_min = min(span_min, col_num) 

6689 span_max = max(span_max, col_num) 

6690 

6691 if ((row_num + 1) % 16 == 0) or row_num == self.dim_rowmax: 

6692 span_index = int(row_num / 16) 

6693 

6694 if span_min is not None: 

6695 span_min += 1 

6696 span_max += 1 

6697 spans[span_index] = f"{span_min}:{span_max}" 

6698 span_min = None 

6699 

6700 self.row_spans = spans 

6701 

6702 def _write_row(self, row: int, spans, properties=None, empty_row=False) -> None: 

6703 # Write the <row> element. 

6704 xf_index = 0 

6705 

6706 if properties: 

6707 height, cell_format, hidden, level, collapsed = properties 

6708 else: 

6709 height, cell_format, hidden, level, collapsed = None, None, 0, 0, 0 

6710 

6711 if height is None: 

6712 height = self.default_row_height 

6713 

6714 attributes = [("r", row + 1)] 

6715 

6716 # Get the cell_format index. 

6717 if cell_format: 

6718 xf_index = cell_format._get_xf_index() 

6719 

6720 # Add row attributes where applicable. 

6721 if spans: 

6722 attributes.append(("spans", spans)) 

6723 

6724 if xf_index: 

6725 attributes.append(("s", xf_index)) 

6726 

6727 if cell_format: 

6728 attributes.append(("customFormat", 1)) 

6729 

6730 if height != self.original_row_height or ( 

6731 height == self.original_row_height and height != self.default_row_height 

6732 ): 

6733 attributes.append(("ht", f"{height:g}")) 

6734 

6735 if hidden: 

6736 attributes.append(("hidden", 1)) 

6737 

6738 if height != self.original_row_height or ( 

6739 height == self.original_row_height and height != self.default_row_height 

6740 ): 

6741 attributes.append(("customHeight", 1)) 

6742 

6743 if level: 

6744 attributes.append(("outlineLevel", level)) 

6745 

6746 if collapsed: 

6747 attributes.append(("collapsed", 1)) 

6748 

6749 if self.excel_version == 2010: 

6750 attributes.append(("x14ac:dyDescent", "0.25")) 

6751 

6752 if empty_row: 

6753 self._xml_empty_tag_unencoded("row", attributes) 

6754 else: 

6755 self._xml_start_tag_unencoded("row", attributes) 

6756 

6757 def _write_empty_row(self, row: int, spans, properties=None) -> None: 

6758 # Write and empty <row> element. 

6759 self._write_row(row, spans, properties, empty_row=True) 

6760 

6761 def _write_cell(self, row: int, col: int, cell) -> None: 

6762 # Write the <cell> element. 

6763 # Note. This is the innermost loop so efficiency is important. 

6764 

6765 cell_range = xl_rowcol_to_cell_fast(row, col) 

6766 attributes = [("r", cell_range)] 

6767 

6768 if cell.format: 

6769 # Add the cell format index. 

6770 xf_index = cell.format._get_xf_index() 

6771 attributes.append(("s", xf_index)) 

6772 elif row in self.set_rows and self.set_rows[row][1]: 

6773 # Add the row format. 

6774 row_xf = self.set_rows[row][1] 

6775 attributes.append(("s", row_xf._get_xf_index())) 

6776 elif col in self.col_info: 

6777 # Add the column format. 

6778 col_xf = self.col_info[col][1] 

6779 if col_xf is not None: 

6780 attributes.append(("s", col_xf._get_xf_index())) 

6781 

6782 type_cell_name = cell.__class__.__name__ 

6783 

6784 # Write the various cell types. 

6785 if type_cell_name in ("Number", "Datetime"): 

6786 # Write a number. 

6787 self._xml_number_element(cell.number, attributes) 

6788 

6789 elif type_cell_name in ("String", "RichString"): 

6790 # Write a string. 

6791 string = cell.string 

6792 

6793 if not self.constant_memory: 

6794 # Write a shared string. 

6795 self._xml_string_element(string, attributes) 

6796 else: 

6797 # Write an optimized in-line string. 

6798 

6799 # Convert control character to a _xHHHH_ escape. 

6800 string = self._escape_control_characters(string) 

6801 

6802 # Write any rich strings without further tags. 

6803 if string.startswith("<r>") and string.endswith("</r>"): 

6804 self._xml_rich_inline_string(string, attributes) 

6805 else: 

6806 # Add attribute to preserve leading or trailing whitespace. 

6807 preserve = _preserve_whitespace(string) 

6808 self._xml_inline_string(string, preserve, attributes) 

6809 

6810 elif type_cell_name == "Formula": 

6811 # Write a formula. First check the formula value type. 

6812 value = cell.value 

6813 if isinstance(cell.value, bool): 

6814 attributes.append(("t", "b")) 

6815 if cell.value: 

6816 value = 1 

6817 else: 

6818 value = 0 

6819 

6820 elif isinstance(cell.value, str): 

6821 error_codes = ( 

6822 "#DIV/0!", 

6823 "#N/A", 

6824 "#NAME?", 

6825 "#NULL!", 

6826 "#NUM!", 

6827 "#REF!", 

6828 "#VALUE!", 

6829 ) 

6830 

6831 if cell.value == "": 

6832 # Allow blank to force recalc in some third party apps. 

6833 pass 

6834 elif cell.value in error_codes: 

6835 attributes.append(("t", "e")) 

6836 else: 

6837 attributes.append(("t", "str")) 

6838 

6839 self._xml_formula_element(cell.formula, value, attributes) 

6840 

6841 elif type_cell_name == "ArrayFormula": 

6842 # Write a array formula. 

6843 

6844 if cell.atype == "dynamic": 

6845 attributes.append(("cm", 1)) 

6846 

6847 # First check if the formula value is a string. 

6848 try: 

6849 float(cell.value) 

6850 except ValueError: 

6851 attributes.append(("t", "str")) 

6852 

6853 # Write an array formula. 

6854 self._xml_start_tag("c", attributes) 

6855 

6856 self._write_cell_array_formula(cell.formula, cell.range) 

6857 self._write_cell_value(cell.value) 

6858 self._xml_end_tag("c") 

6859 

6860 elif type_cell_name == "Blank": 

6861 # Write a empty cell. 

6862 self._xml_empty_tag("c", attributes) 

6863 

6864 elif type_cell_name == "Boolean": 

6865 # Write a boolean cell. 

6866 attributes.append(("t", "b")) 

6867 self._xml_start_tag("c", attributes) 

6868 self._write_cell_value(cell.boolean) 

6869 self._xml_end_tag("c") 

6870 

6871 elif type_cell_name == "Error": 

6872 # Write a boolean cell. 

6873 attributes.append(("t", "e")) 

6874 attributes.append(("vm", cell.value)) 

6875 self._xml_start_tag("c", attributes) 

6876 self._write_cell_value(cell.error) 

6877 self._xml_end_tag("c") 

6878 

6879 def _write_cell_value(self, value) -> None: 

6880 # Write the cell value <v> element. 

6881 if value is None: 

6882 value = "" 

6883 

6884 self._xml_data_element("v", value) 

6885 

6886 def _write_cell_array_formula(self, formula, cell_range) -> None: 

6887 # Write the cell array formula <f> element. 

6888 attributes = [("t", "array"), ("ref", cell_range)] 

6889 

6890 self._xml_data_element("f", formula, attributes) 

6891 

6892 def _write_sheet_pr(self) -> None: 

6893 # Write the <sheetPr> element for Sheet level properties. 

6894 attributes = [] 

6895 

6896 if ( 

6897 not self.fit_page 

6898 and not self.filter_on 

6899 and not self.tab_color 

6900 and not self.outline_changed 

6901 and not self.vba_codename 

6902 ): 

6903 return 

6904 

6905 if self.vba_codename: 

6906 attributes.append(("codeName", self.vba_codename)) 

6907 

6908 if self.filter_on: 

6909 attributes.append(("filterMode", 1)) 

6910 

6911 if self.fit_page or self.tab_color or self.outline_changed: 

6912 self._xml_start_tag("sheetPr", attributes) 

6913 self._write_tab_color() 

6914 self._write_outline_pr() 

6915 self._write_page_set_up_pr() 

6916 self._xml_end_tag("sheetPr") 

6917 else: 

6918 self._xml_empty_tag("sheetPr", attributes) 

6919 

6920 def _write_page_set_up_pr(self) -> None: 

6921 # Write the <pageSetUpPr> element. 

6922 if not self.fit_page: 

6923 return 

6924 

6925 attributes = [("fitToPage", 1)] 

6926 self._xml_empty_tag("pageSetUpPr", attributes) 

6927 

6928 def _write_tab_color(self) -> None: 

6929 # Write the <tabColor> element. 

6930 color = self.tab_color 

6931 

6932 if not color: 

6933 return 

6934 

6935 self._write_color("tabColor", color._attributes()) 

6936 

6937 def _write_outline_pr(self) -> None: 

6938 # Write the <outlinePr> element. 

6939 attributes = [] 

6940 

6941 if not self.outline_changed: 

6942 return 

6943 

6944 if self.outline_style: 

6945 attributes.append(("applyStyles", 1)) 

6946 if not self.outline_below: 

6947 attributes.append(("summaryBelow", 0)) 

6948 if not self.outline_right: 

6949 attributes.append(("summaryRight", 0)) 

6950 if not self.outline_on: 

6951 attributes.append(("showOutlineSymbols", 0)) 

6952 

6953 self._xml_empty_tag("outlinePr", attributes) 

6954 

6955 def _write_row_breaks(self) -> None: 

6956 # Write the <rowBreaks> element. 

6957 page_breaks = self._sort_pagebreaks(self.hbreaks) 

6958 

6959 if not page_breaks: 

6960 return 

6961 

6962 count = len(page_breaks) 

6963 

6964 attributes = [ 

6965 ("count", count), 

6966 ("manualBreakCount", count), 

6967 ] 

6968 

6969 self._xml_start_tag("rowBreaks", attributes) 

6970 

6971 for row_num in page_breaks: 

6972 self._write_brk(row_num, 16383) 

6973 

6974 self._xml_end_tag("rowBreaks") 

6975 

6976 def _write_col_breaks(self) -> None: 

6977 # Write the <colBreaks> element. 

6978 page_breaks = self._sort_pagebreaks(self.vbreaks) 

6979 

6980 if not page_breaks: 

6981 return 

6982 

6983 count = len(page_breaks) 

6984 

6985 attributes = [ 

6986 ("count", count), 

6987 ("manualBreakCount", count), 

6988 ] 

6989 

6990 self._xml_start_tag("colBreaks", attributes) 

6991 

6992 for col_num in page_breaks: 

6993 self._write_brk(col_num, 1048575) 

6994 

6995 self._xml_end_tag("colBreaks") 

6996 

6997 def _write_brk(self, brk_id, brk_max) -> None: 

6998 # Write the <brk> element. 

6999 attributes = [("id", brk_id), ("max", brk_max), ("man", 1)] 

7000 

7001 self._xml_empty_tag("brk", attributes) 

7002 

7003 def _write_merge_cells(self) -> None: 

7004 # Write the <mergeCells> element. 

7005 merged_cells = self.merge 

7006 count = len(merged_cells) 

7007 

7008 if not count: 

7009 return 

7010 

7011 attributes = [("count", count)] 

7012 

7013 self._xml_start_tag("mergeCells", attributes) 

7014 

7015 for merged_range in merged_cells: 

7016 # Write the mergeCell element. 

7017 self._write_merge_cell(merged_range) 

7018 

7019 self._xml_end_tag("mergeCells") 

7020 

7021 def _write_merge_cell(self, merged_range) -> None: 

7022 # Write the <mergeCell> element. 

7023 (row_min, col_min, row_max, col_max) = merged_range 

7024 

7025 # Convert the merge dimensions to a cell range. 

7026 cell_1 = xl_rowcol_to_cell(row_min, col_min) 

7027 cell_2 = xl_rowcol_to_cell(row_max, col_max) 

7028 ref = cell_1 + ":" + cell_2 

7029 

7030 attributes = [("ref", ref)] 

7031 

7032 self._xml_empty_tag("mergeCell", attributes) 

7033 

7034 def _write_hyperlinks(self) -> None: 

7035 # Process any stored hyperlinks in row/col order and write the 

7036 # <hyperlinks> element. The attributes are different for internal 

7037 # and external links. 

7038 

7039 # Sort the hyperlinks into row order. 

7040 row_nums = sorted(self.hyperlinks.keys()) 

7041 

7042 # Exit if there are no hyperlinks to process. 

7043 if not row_nums: 

7044 return 

7045 

7046 # Write the hyperlink elements. 

7047 self._xml_start_tag("hyperlinks") 

7048 

7049 # Iterate over the rows. 

7050 for row_num in row_nums: 

7051 # Sort the hyperlinks into column order. 

7052 col_nums = sorted(self.hyperlinks[row_num].keys()) 

7053 

7054 # Iterate over the columns. 

7055 for col_num in col_nums: 

7056 # Get the link data for this cell. 

7057 url = self.hyperlinks[row_num][col_num] 

7058 

7059 # If the cell was overwritten by the user and isn't a string 

7060 # then we have to add the url as the string to display. 

7061 if self.table and self.table[row_num] and self.table[row_num][col_num]: 

7062 cell = self.table[row_num][col_num] 

7063 if cell.__class__.__name__ != "String": 

7064 url._is_object_link = True 

7065 

7066 if url._link_type in (UrlTypes.URL, UrlTypes.EXTERNAL): 

7067 # External link with rel file relationship. 

7068 self.rel_count += 1 

7069 

7070 self._write_hyperlink_external( 

7071 row_num, col_num, self.rel_count, url 

7072 ) 

7073 

7074 # Links for use by the packager. 

7075 self.external_hyper_links.append( 

7076 ["/hyperlink", url._target(), "External"] 

7077 ) 

7078 else: 

7079 # Internal link with rel file relationship. 

7080 self._write_hyperlink_internal(row_num, col_num, url) 

7081 

7082 self._xml_end_tag("hyperlinks") 

7083 

7084 def _write_hyperlink_external( 

7085 self, row: int, col: int, id_num: int, url: Url 

7086 ) -> None: 

7087 # Write the <hyperlink> element for external links. 

7088 ref = xl_rowcol_to_cell(row, col) 

7089 r_id = "rId" + str(id_num) 

7090 

7091 attributes = [("ref", ref), ("r:id", r_id)] 

7092 

7093 if url._anchor: 

7094 attributes.append(("location", url._anchor)) 

7095 

7096 if url._is_object_link: 

7097 attributes.append(("display", url._text)) 

7098 

7099 if url._tip: 

7100 attributes.append(("tooltip", url._tip)) 

7101 

7102 self._xml_empty_tag("hyperlink", attributes) 

7103 

7104 def _write_hyperlink_internal(self, row: int, col: int, url: Url) -> None: 

7105 # Write the <hyperlink> element for internal links. 

7106 ref = xl_rowcol_to_cell(row, col) 

7107 

7108 attributes = [("ref", ref), ("location", url._link)] 

7109 

7110 if url._tip: 

7111 attributes.append(("tooltip", url._tip)) 

7112 

7113 attributes.append(("display", url._text)) 

7114 

7115 self._xml_empty_tag("hyperlink", attributes) 

7116 

7117 def _write_auto_filter(self) -> None: 

7118 # Write the <autoFilter> element. 

7119 if not self.autofilter_ref: 

7120 return 

7121 

7122 attributes = [("ref", self.autofilter_ref)] 

7123 

7124 if self.filter_on: 

7125 # Autofilter defined active filters. 

7126 self._xml_start_tag("autoFilter", attributes) 

7127 self._write_autofilters() 

7128 self._xml_end_tag("autoFilter") 

7129 

7130 else: 

7131 # Autofilter defined without active filters. 

7132 self._xml_empty_tag("autoFilter", attributes) 

7133 

7134 def _write_autofilters(self) -> None: 

7135 # Function to iterate through the columns that form part of an 

7136 # autofilter range and write the appropriate filters. 

7137 (col1, col2) = self.filter_range 

7138 

7139 for col in range(col1, col2 + 1): 

7140 # Skip if column doesn't have an active filter. 

7141 if col not in self.filter_cols: 

7142 continue 

7143 

7144 # Retrieve the filter tokens and write the autofilter records. 

7145 tokens = self.filter_cols[col] 

7146 filter_type = self.filter_type[col] 

7147 

7148 # Filters are relative to first column in the autofilter. 

7149 self._write_filter_column(col - col1, filter_type, tokens) 

7150 

7151 def _write_filter_column(self, col_id, filter_type, filters) -> None: 

7152 # Write the <filterColumn> element. 

7153 attributes = [("colId", col_id)] 

7154 

7155 self._xml_start_tag("filterColumn", attributes) 

7156 

7157 if filter_type == 1: 

7158 # Type == 1 is the new XLSX style filter. 

7159 self._write_filters(filters) 

7160 else: 

7161 # Type == 0 is the classic "custom" filter. 

7162 self._write_custom_filters(filters) 

7163 

7164 self._xml_end_tag("filterColumn") 

7165 

7166 def _write_filters(self, filters) -> None: 

7167 # Write the <filters> element. 

7168 non_blanks = [filter for filter in filters if str(filter).lower() != "blanks"] 

7169 attributes = [] 

7170 

7171 if len(filters) != len(non_blanks): 

7172 attributes = [("blank", 1)] 

7173 

7174 if len(filters) == 1 and len(non_blanks) == 0: 

7175 # Special case for blank cells only. 

7176 self._xml_empty_tag("filters", attributes) 

7177 else: 

7178 # General case. 

7179 self._xml_start_tag("filters", attributes) 

7180 

7181 for autofilter in sorted(non_blanks): 

7182 self._write_filter(autofilter) 

7183 

7184 self._xml_end_tag("filters") 

7185 

7186 def _write_filter(self, val) -> None: 

7187 # Write the <filter> element. 

7188 attributes = [("val", val)] 

7189 

7190 self._xml_empty_tag("filter", attributes) 

7191 

7192 def _write_custom_filters(self, tokens) -> None: 

7193 # Write the <customFilters> element. 

7194 if len(tokens) == 2: 

7195 # One filter expression only. 

7196 self._xml_start_tag("customFilters") 

7197 self._write_custom_filter(*tokens) 

7198 self._xml_end_tag("customFilters") 

7199 else: 

7200 # Two filter expressions. 

7201 attributes = [] 

7202 

7203 # Check if the "join" operand is "and" or "or". 

7204 if tokens[2] == 0: 

7205 attributes = [("and", 1)] 

7206 else: 

7207 attributes = [("and", 0)] 

7208 

7209 # Write the two custom filters. 

7210 self._xml_start_tag("customFilters", attributes) 

7211 self._write_custom_filter(tokens[0], tokens[1]) 

7212 self._write_custom_filter(tokens[3], tokens[4]) 

7213 self._xml_end_tag("customFilters") 

7214 

7215 def _write_custom_filter(self, operator, val) -> None: 

7216 # Write the <customFilter> element. 

7217 attributes = [] 

7218 

7219 operators = { 

7220 1: "lessThan", 

7221 2: "equal", 

7222 3: "lessThanOrEqual", 

7223 4: "greaterThan", 

7224 5: "notEqual", 

7225 6: "greaterThanOrEqual", 

7226 22: "equal", 

7227 } 

7228 

7229 # Convert the operator from a number to a descriptive string. 

7230 if operators[operator] is not None: 

7231 operator = operators[operator] 

7232 else: 

7233 warn(f"Unknown operator = {operator}") 

7234 

7235 # The 'equal' operator is the default attribute and isn't stored. 

7236 if operator != "equal": 

7237 attributes.append(("operator", operator)) 

7238 attributes.append(("val", val)) 

7239 

7240 self._xml_empty_tag("customFilter", attributes) 

7241 

7242 def _write_sheet_protection(self) -> None: 

7243 # Write the <sheetProtection> element. 

7244 attributes = [] 

7245 

7246 if not self.protect_options: 

7247 return 

7248 

7249 options = self.protect_options 

7250 

7251 if options["password"]: 

7252 attributes.append(("password", options["password"])) 

7253 if options["sheet"]: 

7254 attributes.append(("sheet", 1)) 

7255 if options["content"]: 

7256 attributes.append(("content", 1)) 

7257 if not options["objects"]: 

7258 attributes.append(("objects", 1)) 

7259 if not options["scenarios"]: 

7260 attributes.append(("scenarios", 1)) 

7261 if options["format_cells"]: 

7262 attributes.append(("formatCells", 0)) 

7263 if options["format_columns"]: 

7264 attributes.append(("formatColumns", 0)) 

7265 if options["format_rows"]: 

7266 attributes.append(("formatRows", 0)) 

7267 if options["insert_columns"]: 

7268 attributes.append(("insertColumns", 0)) 

7269 if options["insert_rows"]: 

7270 attributes.append(("insertRows", 0)) 

7271 if options["insert_hyperlinks"]: 

7272 attributes.append(("insertHyperlinks", 0)) 

7273 if options["delete_columns"]: 

7274 attributes.append(("deleteColumns", 0)) 

7275 if options["delete_rows"]: 

7276 attributes.append(("deleteRows", 0)) 

7277 if not options["select_locked_cells"]: 

7278 attributes.append(("selectLockedCells", 1)) 

7279 if options["sort"]: 

7280 attributes.append(("sort", 0)) 

7281 if options["autofilter"]: 

7282 attributes.append(("autoFilter", 0)) 

7283 if options["pivot_tables"]: 

7284 attributes.append(("pivotTables", 0)) 

7285 if not options["select_unlocked_cells"]: 

7286 attributes.append(("selectUnlockedCells", 1)) 

7287 

7288 self._xml_empty_tag("sheetProtection", attributes) 

7289 

7290 def _write_protected_ranges(self) -> None: 

7291 # Write the <protectedRanges> element. 

7292 if self.num_protected_ranges == 0: 

7293 return 

7294 

7295 self._xml_start_tag("protectedRanges") 

7296 

7297 for cell_range, range_name, password in self.protected_ranges: 

7298 self._write_protected_range(cell_range, range_name, password) 

7299 

7300 self._xml_end_tag("protectedRanges") 

7301 

7302 def _write_protected_range(self, cell_range, range_name, password) -> None: 

7303 # Write the <protectedRange> element. 

7304 attributes = [] 

7305 

7306 if password: 

7307 attributes.append(("password", password)) 

7308 

7309 attributes.append(("sqref", cell_range)) 

7310 attributes.append(("name", range_name)) 

7311 

7312 self._xml_empty_tag("protectedRange", attributes) 

7313 

7314 def _write_drawings(self) -> None: 

7315 # Write the <drawing> elements. 

7316 if not self.drawing: 

7317 return 

7318 

7319 self.rel_count += 1 

7320 self._write_drawing(self.rel_count) 

7321 

7322 def _write_drawing(self, drawing_id) -> None: 

7323 # Write the <drawing> element. 

7324 r_id = "rId" + str(drawing_id) 

7325 

7326 attributes = [("r:id", r_id)] 

7327 

7328 self._xml_empty_tag("drawing", attributes) 

7329 

7330 def _write_legacy_drawing(self) -> None: 

7331 # Write the <legacyDrawing> element. 

7332 if not self.has_vml: 

7333 return 

7334 

7335 # Increment the relationship id for any drawings or comments. 

7336 self.rel_count += 1 

7337 r_id = "rId" + str(self.rel_count) 

7338 

7339 attributes = [("r:id", r_id)] 

7340 

7341 self._xml_empty_tag("legacyDrawing", attributes) 

7342 

7343 def _write_legacy_drawing_hf(self) -> None: 

7344 # Write the <legacyDrawingHF> element. 

7345 if not self.has_header_vml: 

7346 return 

7347 

7348 # Increment the relationship id for any drawings or comments. 

7349 self.rel_count += 1 

7350 r_id = "rId" + str(self.rel_count) 

7351 

7352 attributes = [("r:id", r_id)] 

7353 

7354 self._xml_empty_tag("legacyDrawingHF", attributes) 

7355 

7356 def _write_picture(self) -> None: 

7357 # Write the <picture> element. 

7358 if not self.background_image: 

7359 return 

7360 

7361 # Increment the relationship id. 

7362 self.rel_count += 1 

7363 r_id = "rId" + str(self.rel_count) 

7364 

7365 attributes = [("r:id", r_id)] 

7366 

7367 self._xml_empty_tag("picture", attributes) 

7368 

7369 def _write_data_validations(self) -> None: 

7370 # Write the <dataValidations> element. 

7371 validations = self.validations 

7372 count = len(validations) 

7373 

7374 if not count: 

7375 return 

7376 

7377 attributes = [("count", count)] 

7378 

7379 self._xml_start_tag("dataValidations", attributes) 

7380 

7381 for validation in validations: 

7382 # Write the dataValidation element. 

7383 self._write_data_validation(validation) 

7384 

7385 self._xml_end_tag("dataValidations") 

7386 

7387 def _write_data_validation(self, options) -> None: 

7388 # Write the <dataValidation> element. 

7389 sqref = "" 

7390 attributes = [] 

7391 

7392 # Set the cell range(s) for the data validation. 

7393 for cells in options["cells"]: 

7394 # Add a space between multiple cell ranges. 

7395 if sqref != "": 

7396 sqref += " " 

7397 

7398 (row_first, col_first, row_last, col_last) = cells 

7399 

7400 # Swap last row/col for first row/col as necessary 

7401 if row_first > row_last: 

7402 (row_first, row_last) = (row_last, row_first) 

7403 

7404 if col_first > col_last: 

7405 (col_first, col_last) = (col_last, col_first) 

7406 

7407 sqref += xl_range(row_first, col_first, row_last, col_last) 

7408 

7409 if options.get("multi_range"): 

7410 sqref = options["multi_range"] 

7411 

7412 if options["validate"] != "none": 

7413 attributes.append(("type", options["validate"])) 

7414 

7415 if options["criteria"] != "between": 

7416 attributes.append(("operator", options["criteria"])) 

7417 

7418 if "error_type" in options: 

7419 if options["error_type"] == 1: 

7420 attributes.append(("errorStyle", "warning")) 

7421 if options["error_type"] == 2: 

7422 attributes.append(("errorStyle", "information")) 

7423 

7424 if options["ignore_blank"]: 

7425 attributes.append(("allowBlank", 1)) 

7426 

7427 if not options["dropdown"]: 

7428 attributes.append(("showDropDown", 1)) 

7429 

7430 if options["show_input"]: 

7431 attributes.append(("showInputMessage", 1)) 

7432 

7433 if options["show_error"]: 

7434 attributes.append(("showErrorMessage", 1)) 

7435 

7436 if "error_title" in options: 

7437 attributes.append(("errorTitle", options["error_title"])) 

7438 

7439 if "error_message" in options: 

7440 attributes.append(("error", options["error_message"])) 

7441 

7442 if "input_title" in options: 

7443 attributes.append(("promptTitle", options["input_title"])) 

7444 

7445 if "input_message" in options: 

7446 attributes.append(("prompt", options["input_message"])) 

7447 

7448 attributes.append(("sqref", sqref)) 

7449 

7450 if options["validate"] == "none": 

7451 self._xml_empty_tag("dataValidation", attributes) 

7452 else: 

7453 self._xml_start_tag("dataValidation", attributes) 

7454 

7455 # Write the formula1 element. 

7456 self._write_formula_1(options["value"]) 

7457 

7458 # Write the formula2 element. 

7459 if options["maximum"] is not None: 

7460 self._write_formula_2(options["maximum"]) 

7461 

7462 self._xml_end_tag("dataValidation") 

7463 

7464 def _write_formula_1(self, formula) -> None: 

7465 # Write the <formula1> element. 

7466 

7467 if isinstance(formula, list): 

7468 formula = self._csv_join(*formula) 

7469 formula = f'"{formula}"' 

7470 else: 

7471 # Check if the formula is a number. 

7472 try: 

7473 float(formula) 

7474 except ValueError: 

7475 # Not a number. Remove the formula '=' sign if it exists. 

7476 if formula.startswith("="): 

7477 formula = formula.lstrip("=") 

7478 

7479 self._xml_data_element("formula1", formula) 

7480 

7481 def _write_formula_2(self, formula) -> None: 

7482 # Write the <formula2> element. 

7483 

7484 # Check if the formula is a number. 

7485 try: 

7486 float(formula) 

7487 except ValueError: 

7488 # Not a number. Remove the formula '=' sign if it exists. 

7489 if formula.startswith("="): 

7490 formula = formula.lstrip("=") 

7491 

7492 self._xml_data_element("formula2", formula) 

7493 

7494 def _write_conditional_formats(self) -> None: 

7495 # Write the Worksheet conditional formats. 

7496 ranges = sorted(self.cond_formats.keys()) 

7497 

7498 if not ranges: 

7499 return 

7500 

7501 for cond_range in ranges: 

7502 self._write_conditional_formatting( 

7503 cond_range, self.cond_formats[cond_range] 

7504 ) 

7505 

7506 def _write_conditional_formatting(self, cond_range, params) -> None: 

7507 # Write the <conditionalFormatting> element. 

7508 attributes = [("sqref", cond_range)] 

7509 self._xml_start_tag("conditionalFormatting", attributes) 

7510 for param in params: 

7511 # Write the cfRule element. 

7512 self._write_cf_rule(param) 

7513 self._xml_end_tag("conditionalFormatting") 

7514 

7515 def _write_cf_rule(self, params) -> None: 

7516 # Write the <cfRule> element. 

7517 attributes = [("type", params["type"])] 

7518 

7519 if "format" in params and params["format"] is not None: 

7520 attributes.append(("dxfId", params["format"])) 

7521 

7522 attributes.append(("priority", params["priority"])) 

7523 

7524 if params.get("stop_if_true"): 

7525 attributes.append(("stopIfTrue", 1)) 

7526 

7527 if params["type"] == "cellIs": 

7528 attributes.append(("operator", params["criteria"])) 

7529 

7530 self._xml_start_tag("cfRule", attributes) 

7531 

7532 if "minimum" in params and "maximum" in params: 

7533 self._write_formula_element(params["minimum"]) 

7534 self._write_formula_element(params["maximum"]) 

7535 else: 

7536 self._write_formula_element(params["value"]) 

7537 

7538 self._xml_end_tag("cfRule") 

7539 

7540 elif params["type"] == "aboveAverage": 

7541 if re.search("below", params["criteria"]): 

7542 attributes.append(("aboveAverage", 0)) 

7543 

7544 if re.search("equal", params["criteria"]): 

7545 attributes.append(("equalAverage", 1)) 

7546 

7547 if re.search("[123] std dev", params["criteria"]): 

7548 match = re.search("([123]) std dev", params["criteria"]) 

7549 attributes.append(("stdDev", match.group(1))) 

7550 

7551 self._xml_empty_tag("cfRule", attributes) 

7552 

7553 elif params["type"] == "top10": 

7554 if "criteria" in params and params["criteria"] == "%": 

7555 attributes.append(("percent", 1)) 

7556 

7557 if "direction" in params: 

7558 attributes.append(("bottom", 1)) 

7559 

7560 rank = params["value"] or 10 

7561 attributes.append(("rank", rank)) 

7562 

7563 self._xml_empty_tag("cfRule", attributes) 

7564 

7565 elif params["type"] == "duplicateValues": 

7566 self._xml_empty_tag("cfRule", attributes) 

7567 

7568 elif params["type"] == "uniqueValues": 

7569 self._xml_empty_tag("cfRule", attributes) 

7570 

7571 elif ( 

7572 params["type"] == "containsText" 

7573 or params["type"] == "notContainsText" 

7574 or params["type"] == "beginsWith" 

7575 or params["type"] == "endsWith" 

7576 ): 

7577 attributes.append(("operator", params["criteria"])) 

7578 attributes.append(("text", params["value"])) 

7579 self._xml_start_tag("cfRule", attributes) 

7580 self._write_formula_element(params["formula"]) 

7581 self._xml_end_tag("cfRule") 

7582 

7583 elif params["type"] == "timePeriod": 

7584 attributes.append(("timePeriod", params["criteria"])) 

7585 self._xml_start_tag("cfRule", attributes) 

7586 self._write_formula_element(params["formula"]) 

7587 self._xml_end_tag("cfRule") 

7588 

7589 elif ( 

7590 params["type"] == "containsBlanks" 

7591 or params["type"] == "notContainsBlanks" 

7592 or params["type"] == "containsErrors" 

7593 or params["type"] == "notContainsErrors" 

7594 ): 

7595 self._xml_start_tag("cfRule", attributes) 

7596 self._write_formula_element(params["formula"]) 

7597 self._xml_end_tag("cfRule") 

7598 

7599 elif params["type"] == "colorScale": 

7600 self._xml_start_tag("cfRule", attributes) 

7601 self._write_color_scale(params) 

7602 self._xml_end_tag("cfRule") 

7603 

7604 elif params["type"] == "dataBar": 

7605 self._xml_start_tag("cfRule", attributes) 

7606 self._write_data_bar(params) 

7607 

7608 if params.get("is_data_bar_2010"): 

7609 self._write_data_bar_ext(params) 

7610 

7611 self._xml_end_tag("cfRule") 

7612 

7613 elif params["type"] == "expression": 

7614 self._xml_start_tag("cfRule", attributes) 

7615 self._write_formula_element(params["criteria"]) 

7616 self._xml_end_tag("cfRule") 

7617 

7618 elif params["type"] == "iconSet": 

7619 self._xml_start_tag("cfRule", attributes) 

7620 self._write_icon_set(params) 

7621 self._xml_end_tag("cfRule") 

7622 

7623 def _write_formula_element(self, formula) -> None: 

7624 # Write the <formula> element. 

7625 

7626 # Check if the formula is a number. 

7627 try: 

7628 float(formula) 

7629 except ValueError: 

7630 # Not a number. Remove the formula '=' sign if it exists. 

7631 if formula.startswith("="): 

7632 formula = formula.lstrip("=") 

7633 

7634 self._xml_data_element("formula", formula) 

7635 

7636 def _write_color_scale(self, param) -> None: 

7637 # Write the <colorScale> element. 

7638 

7639 self._xml_start_tag("colorScale") 

7640 

7641 self._write_cfvo(param["min_type"], param["min_value"]) 

7642 

7643 if param["mid_type"] is not None: 

7644 self._write_cfvo(param["mid_type"], param["mid_value"]) 

7645 

7646 self._write_cfvo(param["max_type"], param["max_value"]) 

7647 

7648 self._write_color("color", param["min_color"]._attributes()) 

7649 

7650 if param["mid_color"] is not None: 

7651 self._write_color("color", param["mid_color"]._attributes()) 

7652 

7653 self._write_color("color", param["max_color"]._attributes()) 

7654 

7655 self._xml_end_tag("colorScale") 

7656 

7657 def _write_data_bar(self, param) -> None: 

7658 # Write the <dataBar> element. 

7659 attributes = [] 

7660 

7661 # Min and max bar lengths in in the spec but not supported directly by 

7662 # Excel. 

7663 if "min_length" in param: 

7664 attributes.append(("minLength", param["min_length"])) 

7665 

7666 if "max_length" in param: 

7667 attributes.append(("maxLength", param["max_length"])) 

7668 

7669 if param.get("bar_only"): 

7670 attributes.append(("showValue", 0)) 

7671 

7672 self._xml_start_tag("dataBar", attributes) 

7673 

7674 self._write_cfvo(param["min_type"], param["min_value"]) 

7675 self._write_cfvo(param["max_type"], param["max_value"]) 

7676 self._write_color("color", param["bar_color"]._attributes()) 

7677 

7678 self._xml_end_tag("dataBar") 

7679 

7680 def _write_data_bar_ext(self, param) -> None: 

7681 # Write the <extLst> dataBar extension element. 

7682 

7683 # Create a pseudo GUID for each unique Excel 2010 data bar. 

7684 worksheet_count = self.index + 1 

7685 data_bar_count = len(self.data_bars_2010) + 1 

7686 guid = "{DA7ABA51-AAAA-BBBB-%04X-%012X}" % (worksheet_count, data_bar_count) 

7687 

7688 # Store the 2010 data bar parameters to write the extLst elements. 

7689 param["guid"] = guid 

7690 self.data_bars_2010.append(param) 

7691 

7692 self._xml_start_tag("extLst") 

7693 self._write_ext("{B025F937-C7B1-47D3-B67F-A62EFF666E3E}") 

7694 self._xml_data_element("x14:id", guid) 

7695 self._xml_end_tag("ext") 

7696 self._xml_end_tag("extLst") 

7697 

7698 def _write_icon_set(self, param) -> None: 

7699 # Write the <iconSet> element. 

7700 attributes = [] 

7701 

7702 # Don't set attribute for default style. 

7703 if param["icon_style"] != "3TrafficLights": 

7704 attributes = [("iconSet", param["icon_style"])] 

7705 

7706 if param.get("icons_only"): 

7707 attributes.append(("showValue", 0)) 

7708 

7709 if param.get("reverse_icons"): 

7710 attributes.append(("reverse", 1)) 

7711 

7712 self._xml_start_tag("iconSet", attributes) 

7713 

7714 # Write the properties for different icon styles. 

7715 for icon in reversed(param["icons"]): 

7716 self._write_cfvo(icon["type"], icon["value"], icon["criteria"]) 

7717 

7718 self._xml_end_tag("iconSet") 

7719 

7720 def _write_cfvo(self, cf_type, val, criteria=None) -> None: 

7721 # Write the <cfvo> element. 

7722 attributes = [("type", cf_type)] 

7723 

7724 if val is not None: 

7725 attributes.append(("val", val)) 

7726 

7727 if criteria: 

7728 attributes.append(("gte", 0)) 

7729 

7730 self._xml_empty_tag("cfvo", attributes) 

7731 

7732 def _write_color(self, name, attributes) -> None: 

7733 # Write the <color> element. 

7734 self._xml_empty_tag(name, attributes) 

7735 

7736 def _write_selections(self) -> None: 

7737 # Write the <selection> elements. 

7738 for selection in self.selections: 

7739 self._write_selection(*selection) 

7740 

7741 def _write_selection(self, pane, active_cell, sqref) -> None: 

7742 # Write the <selection> element. 

7743 attributes = [] 

7744 

7745 if pane: 

7746 attributes.append(("pane", pane)) 

7747 

7748 if active_cell: 

7749 attributes.append(("activeCell", active_cell)) 

7750 

7751 if sqref: 

7752 attributes.append(("sqref", sqref)) 

7753 

7754 self._xml_empty_tag("selection", attributes) 

7755 

7756 def _write_panes(self) -> None: 

7757 # Write the frozen or split <pane> elements. 

7758 panes = self.panes 

7759 

7760 if not panes: 

7761 return 

7762 

7763 if panes[4] == 2: 

7764 self._write_split_panes(*panes) 

7765 else: 

7766 self._write_freeze_panes(*panes) 

7767 

7768 def _write_freeze_panes( 

7769 self, row: int, col: int, top_row, left_col, pane_type 

7770 ) -> None: 

7771 # Write the <pane> element for freeze panes. 

7772 attributes = [] 

7773 

7774 y_split = row 

7775 x_split = col 

7776 top_left_cell = xl_rowcol_to_cell(top_row, left_col) 

7777 active_pane = "" 

7778 state = "" 

7779 active_cell = "" 

7780 sqref = "" 

7781 

7782 # Move user cell selection to the panes. 

7783 if self.selections: 

7784 (_, active_cell, sqref) = self.selections[0] 

7785 self.selections = [] 

7786 

7787 # Set the active pane. 

7788 if row and col: 

7789 active_pane = "bottomRight" 

7790 

7791 row_cell = xl_rowcol_to_cell(row, 0) 

7792 col_cell = xl_rowcol_to_cell(0, col) 

7793 

7794 self.selections.append(["topRight", col_cell, col_cell]) 

7795 self.selections.append(["bottomLeft", row_cell, row_cell]) 

7796 self.selections.append(["bottomRight", active_cell, sqref]) 

7797 

7798 elif col: 

7799 active_pane = "topRight" 

7800 self.selections.append(["topRight", active_cell, sqref]) 

7801 

7802 else: 

7803 active_pane = "bottomLeft" 

7804 self.selections.append(["bottomLeft", active_cell, sqref]) 

7805 

7806 # Set the pane type. 

7807 if pane_type == 0: 

7808 state = "frozen" 

7809 elif pane_type == 1: 

7810 state = "frozenSplit" 

7811 else: 

7812 state = "split" 

7813 

7814 if x_split: 

7815 attributes.append(("xSplit", x_split)) 

7816 

7817 if y_split: 

7818 attributes.append(("ySplit", y_split)) 

7819 

7820 attributes.append(("topLeftCell", top_left_cell)) 

7821 attributes.append(("activePane", active_pane)) 

7822 attributes.append(("state", state)) 

7823 

7824 self._xml_empty_tag("pane", attributes) 

7825 

7826 def _write_split_panes(self, row: int, col: int, top_row, left_col, _) -> None: 

7827 # Write the <pane> element for split panes. 

7828 attributes = [] 

7829 has_selection = False 

7830 active_pane = "" 

7831 active_cell = "" 

7832 sqref = "" 

7833 

7834 y_split = row 

7835 x_split = col 

7836 

7837 # Move user cell selection to the panes. 

7838 if self.selections: 

7839 (_, active_cell, sqref) = self.selections[0] 

7840 self.selections = [] 

7841 has_selection = True 

7842 

7843 # Convert the row and col to 1/20 twip units with padding. 

7844 if y_split: 

7845 y_split = int(20 * y_split + 300) 

7846 

7847 if x_split: 

7848 x_split = self._calculate_x_split_width(x_split) 

7849 

7850 # For non-explicit topLeft definitions, estimate the cell offset based 

7851 # on the pixels dimensions. This is only a workaround and doesn't take 

7852 # adjusted cell dimensions into account. 

7853 if top_row == row and left_col == col: 

7854 top_row = int(0.5 + (y_split - 300) / 20 / 15) 

7855 left_col = int(0.5 + (x_split - 390) / 20 / 3 * 4 / 64) 

7856 

7857 top_left_cell = xl_rowcol_to_cell(top_row, left_col) 

7858 

7859 # If there is no selection set the active cell to the top left cell. 

7860 if not has_selection: 

7861 active_cell = top_left_cell 

7862 sqref = top_left_cell 

7863 

7864 # Set the Cell selections. 

7865 if row and col: 

7866 active_pane = "bottomRight" 

7867 

7868 row_cell = xl_rowcol_to_cell(top_row, 0) 

7869 col_cell = xl_rowcol_to_cell(0, left_col) 

7870 

7871 self.selections.append(["topRight", col_cell, col_cell]) 

7872 self.selections.append(["bottomLeft", row_cell, row_cell]) 

7873 self.selections.append(["bottomRight", active_cell, sqref]) 

7874 

7875 elif col: 

7876 active_pane = "topRight" 

7877 self.selections.append(["topRight", active_cell, sqref]) 

7878 

7879 else: 

7880 active_pane = "bottomLeft" 

7881 self.selections.append(["bottomLeft", active_cell, sqref]) 

7882 

7883 # Format splits to the same precision as Excel. 

7884 if x_split: 

7885 attributes.append(("xSplit", f"{x_split:.16g}")) 

7886 

7887 if y_split: 

7888 attributes.append(("ySplit", f"{y_split:.16g}")) 

7889 

7890 attributes.append(("topLeftCell", top_left_cell)) 

7891 

7892 if has_selection: 

7893 attributes.append(("activePane", active_pane)) 

7894 

7895 self._xml_empty_tag("pane", attributes) 

7896 

7897 def _calculate_x_split_width(self, width): 

7898 # Convert column width from user units to pane split width. 

7899 

7900 max_digit_width = 7 # For Calabri 11. 

7901 padding = 5 

7902 

7903 # Convert to pixels. 

7904 if width < 1: 

7905 pixels = int(width * (max_digit_width + padding) + 0.5) 

7906 else: 

7907 pixels = int(width * max_digit_width + 0.5) + padding 

7908 

7909 # Convert to points. 

7910 points = pixels * 3 / 4 

7911 

7912 # Convert to twips (twentieths of a point). 

7913 twips = points * 20 

7914 

7915 # Add offset/padding. 

7916 width = twips + 390 

7917 

7918 return width 

7919 

7920 def _write_table_parts(self) -> None: 

7921 # Write the <tableParts> element. 

7922 tables = self.tables 

7923 count = len(tables) 

7924 

7925 # Return if worksheet doesn't contain any tables. 

7926 if not count: 

7927 return 

7928 

7929 attributes = [ 

7930 ( 

7931 "count", 

7932 count, 

7933 ) 

7934 ] 

7935 

7936 self._xml_start_tag("tableParts", attributes) 

7937 

7938 for _ in tables: 

7939 # Write the tablePart element. 

7940 self.rel_count += 1 

7941 self._write_table_part(self.rel_count) 

7942 

7943 self._xml_end_tag("tableParts") 

7944 

7945 def _write_table_part(self, r_id) -> None: 

7946 # Write the <tablePart> element. 

7947 

7948 r_id = "rId" + str(r_id) 

7949 

7950 attributes = [ 

7951 ( 

7952 "r:id", 

7953 r_id, 

7954 ) 

7955 ] 

7956 

7957 self._xml_empty_tag("tablePart", attributes) 

7958 

7959 def _write_ext_list(self) -> None: 

7960 # Write the <extLst> element for data bars and sparklines. 

7961 has_data_bars = len(self.data_bars_2010) 

7962 has_sparklines = len(self.sparklines) 

7963 

7964 if not has_data_bars and not has_sparklines: 

7965 return 

7966 

7967 # Write the extLst element. 

7968 self._xml_start_tag("extLst") 

7969 

7970 if has_data_bars: 

7971 self._write_ext_list_data_bars() 

7972 

7973 if has_sparklines: 

7974 self._write_ext_list_sparklines() 

7975 

7976 self._xml_end_tag("extLst") 

7977 

7978 def _write_ext_list_data_bars(self) -> None: 

7979 # Write the Excel 2010 data_bar subelements. 

7980 self._write_ext("{78C0D931-6437-407d-A8EE-F0AAD7539E65}") 

7981 

7982 self._xml_start_tag("x14:conditionalFormattings") 

7983 

7984 # Write the Excel 2010 conditional formatting data bar elements. 

7985 for data_bar in self.data_bars_2010: 

7986 # Write the x14:conditionalFormatting element. 

7987 self._write_conditional_formatting_2010(data_bar) 

7988 

7989 self._xml_end_tag("x14:conditionalFormattings") 

7990 self._xml_end_tag("ext") 

7991 

7992 def _write_conditional_formatting_2010(self, data_bar) -> None: 

7993 # Write the <x14:conditionalFormatting> element. 

7994 xmlns_xm = "http://schemas.microsoft.com/office/excel/2006/main" 

7995 

7996 attributes = [("xmlns:xm", xmlns_xm)] 

7997 

7998 self._xml_start_tag("x14:conditionalFormatting", attributes) 

7999 

8000 # Write the x14:cfRule element. 

8001 self._write_x14_cf_rule(data_bar) 

8002 

8003 # Write the x14:dataBar element. 

8004 self._write_x14_data_bar(data_bar) 

8005 

8006 # Write the x14 max and min data bars. 

8007 self._write_x14_cfvo(data_bar["x14_min_type"], data_bar["min_value"]) 

8008 self._write_x14_cfvo(data_bar["x14_max_type"], data_bar["max_value"]) 

8009 

8010 if not data_bar["bar_no_border"]: 

8011 # Write the x14:borderColor element. 

8012 self._write_x14_border_color(data_bar["bar_border_color"]) 

8013 

8014 # Write the x14:negativeFillColor element. 

8015 if not data_bar["bar_negative_color_same"]: 

8016 self._write_x14_negative_fill_color(data_bar["bar_negative_color"]) 

8017 

8018 # Write the x14:negativeBorderColor element. 

8019 if ( 

8020 not data_bar["bar_no_border"] 

8021 and not data_bar["bar_negative_border_color_same"] 

8022 ): 

8023 self._write_x14_negative_border_color(data_bar["bar_negative_border_color"]) 

8024 

8025 # Write the x14:axisColor element. 

8026 if data_bar["bar_axis_position"] != "none": 

8027 self._write_x14_axis_color(data_bar["bar_axis_color"]) 

8028 

8029 self._xml_end_tag("x14:dataBar") 

8030 self._xml_end_tag("x14:cfRule") 

8031 

8032 # Write the xm:sqref element. 

8033 self._xml_data_element("xm:sqref", data_bar["range"]) 

8034 

8035 self._xml_end_tag("x14:conditionalFormatting") 

8036 

8037 def _write_x14_cf_rule(self, data_bar) -> None: 

8038 # Write the <x14:cfRule> element. 

8039 rule_type = "dataBar" 

8040 guid = data_bar["guid"] 

8041 attributes = [("type", rule_type), ("id", guid)] 

8042 

8043 self._xml_start_tag("x14:cfRule", attributes) 

8044 

8045 def _write_x14_data_bar(self, data_bar) -> None: 

8046 # Write the <x14:dataBar> element. 

8047 min_length = 0 

8048 max_length = 100 

8049 

8050 attributes = [ 

8051 ("minLength", min_length), 

8052 ("maxLength", max_length), 

8053 ] 

8054 

8055 if not data_bar["bar_no_border"]: 

8056 attributes.append(("border", 1)) 

8057 

8058 if data_bar["bar_solid"]: 

8059 attributes.append(("gradient", 0)) 

8060 

8061 if data_bar["bar_direction"] == "left": 

8062 attributes.append(("direction", "leftToRight")) 

8063 

8064 if data_bar["bar_direction"] == "right": 

8065 attributes.append(("direction", "rightToLeft")) 

8066 

8067 if data_bar["bar_negative_color_same"]: 

8068 attributes.append(("negativeBarColorSameAsPositive", 1)) 

8069 

8070 if ( 

8071 not data_bar["bar_no_border"] 

8072 and not data_bar["bar_negative_border_color_same"] 

8073 ): 

8074 attributes.append(("negativeBarBorderColorSameAsPositive", 0)) 

8075 

8076 if data_bar["bar_axis_position"] == "middle": 

8077 attributes.append(("axisPosition", "middle")) 

8078 

8079 if data_bar["bar_axis_position"] == "none": 

8080 attributes.append(("axisPosition", "none")) 

8081 

8082 self._xml_start_tag("x14:dataBar", attributes) 

8083 

8084 def _write_x14_cfvo(self, rule_type, value) -> None: 

8085 # Write the <x14:cfvo> element. 

8086 attributes = [("type", rule_type)] 

8087 

8088 if rule_type in ("min", "max", "autoMin", "autoMax"): 

8089 self._xml_empty_tag("x14:cfvo", attributes) 

8090 else: 

8091 self._xml_start_tag("x14:cfvo", attributes) 

8092 self._xml_data_element("xm:f", value) 

8093 self._xml_end_tag("x14:cfvo") 

8094 

8095 def _write_x14_border_color(self, color) -> None: 

8096 # Write the <x14:borderColor> element. 

8097 self._write_color("x14:borderColor", color._attributes()) 

8098 

8099 def _write_x14_negative_fill_color(self, color) -> None: 

8100 # Write the <x14:negativeFillColor> element. 

8101 self._xml_empty_tag("x14:negativeFillColor", color._attributes()) 

8102 

8103 def _write_x14_negative_border_color(self, color) -> None: 

8104 # Write the <x14:negativeBorderColor> element. 

8105 self._xml_empty_tag("x14:negativeBorderColor", color._attributes()) 

8106 

8107 def _write_x14_axis_color(self, color) -> None: 

8108 # Write the <x14:axisColor> element. 

8109 self._xml_empty_tag("x14:axisColor", color._attributes()) 

8110 

8111 def _write_ext_list_sparklines(self) -> None: 

8112 # Write the sparkline extension sub-elements. 

8113 self._write_ext("{05C60535-1F16-4fd2-B633-F4F36F0B64E0}") 

8114 

8115 # Write the x14:sparklineGroups element. 

8116 self._write_sparkline_groups() 

8117 

8118 # Write the sparkline elements. 

8119 for sparkline in reversed(self.sparklines): 

8120 # Write the x14:sparklineGroup element. 

8121 self._write_sparkline_group(sparkline) 

8122 

8123 # Write the x14:colorSeries element. 

8124 self._write_color_series(sparkline["series_color"]) 

8125 

8126 # Write the x14:colorNegative element. 

8127 self._write_color_negative(sparkline["negative_color"]) 

8128 

8129 # Write the x14:colorAxis element. 

8130 self._write_color_axis() 

8131 

8132 # Write the x14:colorMarkers element. 

8133 self._write_color_markers(sparkline["markers_color"]) 

8134 

8135 # Write the x14:colorFirst element. 

8136 self._write_color_first(sparkline["first_color"]) 

8137 

8138 # Write the x14:colorLast element. 

8139 self._write_color_last(sparkline["last_color"]) 

8140 

8141 # Write the x14:colorHigh element. 

8142 self._write_color_high(sparkline["high_color"]) 

8143 

8144 # Write the x14:colorLow element. 

8145 self._write_color_low(sparkline["low_color"]) 

8146 

8147 if sparkline["date_axis"]: 

8148 self._xml_data_element("xm:f", sparkline["date_axis"]) 

8149 

8150 self._write_sparklines(sparkline) 

8151 

8152 self._xml_end_tag("x14:sparklineGroup") 

8153 

8154 self._xml_end_tag("x14:sparklineGroups") 

8155 self._xml_end_tag("ext") 

8156 

8157 def _write_sparklines(self, sparkline) -> None: 

8158 # Write the <x14:sparklines> element and <x14:sparkline> sub-elements. 

8159 

8160 # Write the sparkline elements. 

8161 self._xml_start_tag("x14:sparklines") 

8162 

8163 for i in range(sparkline["count"]): 

8164 spark_range = sparkline["ranges"][i] 

8165 location = sparkline["locations"][i] 

8166 

8167 self._xml_start_tag("x14:sparkline") 

8168 self._xml_data_element("xm:f", spark_range) 

8169 self._xml_data_element("xm:sqref", location) 

8170 self._xml_end_tag("x14:sparkline") 

8171 

8172 self._xml_end_tag("x14:sparklines") 

8173 

8174 def _write_ext(self, uri) -> None: 

8175 # Write the <ext> element. 

8176 schema = "http://schemas.microsoft.com/office/" 

8177 xmlns_x14 = schema + "spreadsheetml/2009/9/main" 

8178 

8179 attributes = [ 

8180 ("xmlns:x14", xmlns_x14), 

8181 ("uri", uri), 

8182 ] 

8183 

8184 self._xml_start_tag("ext", attributes) 

8185 

8186 def _write_sparkline_groups(self) -> None: 

8187 # Write the <x14:sparklineGroups> element. 

8188 xmlns_xm = "http://schemas.microsoft.com/office/excel/2006/main" 

8189 

8190 attributes = [("xmlns:xm", xmlns_xm)] 

8191 

8192 self._xml_start_tag("x14:sparklineGroups", attributes) 

8193 

8194 def _write_sparkline_group(self, options) -> None: 

8195 # Write the <x14:sparklineGroup> element. 

8196 # 

8197 # Example for order. 

8198 # 

8199 # <x14:sparklineGroup 

8200 # manualMax="0" 

8201 # manualMin="0" 

8202 # lineWeight="2.25" 

8203 # type="column" 

8204 # dateAxis="1" 

8205 # displayEmptyCellsAs="span" 

8206 # markers="1" 

8207 # high="1" 

8208 # low="1" 

8209 # first="1" 

8210 # last="1" 

8211 # negative="1" 

8212 # displayXAxis="1" 

8213 # displayHidden="1" 

8214 # minAxisType="custom" 

8215 # maxAxisType="custom" 

8216 # rightToLeft="1"> 

8217 # 

8218 empty = options.get("empty") 

8219 attributes = [] 

8220 

8221 if options.get("max") is not None: 

8222 if options["max"] == "group": 

8223 options["cust_max"] = "group" 

8224 else: 

8225 attributes.append(("manualMax", options["max"])) 

8226 options["cust_max"] = "custom" 

8227 

8228 if options.get("min") is not None: 

8229 if options["min"] == "group": 

8230 options["cust_min"] = "group" 

8231 else: 

8232 attributes.append(("manualMin", options["min"])) 

8233 options["cust_min"] = "custom" 

8234 

8235 # Ignore the default type attribute (line). 

8236 if options["type"] != "line": 

8237 attributes.append(("type", options["type"])) 

8238 

8239 if options.get("weight"): 

8240 attributes.append(("lineWeight", options["weight"])) 

8241 

8242 if options.get("date_axis"): 

8243 attributes.append(("dateAxis", 1)) 

8244 

8245 if empty: 

8246 attributes.append(("displayEmptyCellsAs", empty)) 

8247 

8248 if options.get("markers"): 

8249 attributes.append(("markers", 1)) 

8250 

8251 if options.get("high"): 

8252 attributes.append(("high", 1)) 

8253 

8254 if options.get("low"): 

8255 attributes.append(("low", 1)) 

8256 

8257 if options.get("first"): 

8258 attributes.append(("first", 1)) 

8259 

8260 if options.get("last"): 

8261 attributes.append(("last", 1)) 

8262 

8263 if options.get("negative"): 

8264 attributes.append(("negative", 1)) 

8265 

8266 if options.get("axis"): 

8267 attributes.append(("displayXAxis", 1)) 

8268 

8269 if options.get("hidden"): 

8270 attributes.append(("displayHidden", 1)) 

8271 

8272 if options.get("cust_min"): 

8273 attributes.append(("minAxisType", options["cust_min"])) 

8274 

8275 if options.get("cust_max"): 

8276 attributes.append(("maxAxisType", options["cust_max"])) 

8277 

8278 if options.get("reverse"): 

8279 attributes.append(("rightToLeft", 1)) 

8280 

8281 self._xml_start_tag("x14:sparklineGroup", attributes) 

8282 

8283 def _write_spark_color(self, tag, color) -> None: 

8284 # Helper function for the sparkline color functions below. 

8285 if color: 

8286 self._write_color(tag, color._attributes()) 

8287 

8288 def _write_color_series(self, color) -> None: 

8289 # Write the <x14:colorSeries> element. 

8290 self._write_spark_color("x14:colorSeries", color) 

8291 

8292 def _write_color_negative(self, color) -> None: 

8293 # Write the <x14:colorNegative> element. 

8294 self._write_spark_color("x14:colorNegative", color) 

8295 

8296 def _write_color_axis(self) -> None: 

8297 # Write the <x14:colorAxis> element. 

8298 self._write_spark_color("x14:colorAxis", Color("#000000")) 

8299 

8300 def _write_color_markers(self, color) -> None: 

8301 # Write the <x14:colorMarkers> element. 

8302 self._write_spark_color("x14:colorMarkers", color) 

8303 

8304 def _write_color_first(self, color) -> None: 

8305 # Write the <x14:colorFirst> element. 

8306 self._write_spark_color("x14:colorFirst", color) 

8307 

8308 def _write_color_last(self, color) -> None: 

8309 # Write the <x14:colorLast> element. 

8310 self._write_spark_color("x14:colorLast", color) 

8311 

8312 def _write_color_high(self, color) -> None: 

8313 # Write the <x14:colorHigh> element. 

8314 self._write_spark_color("x14:colorHigh", color) 

8315 

8316 def _write_color_low(self, color) -> None: 

8317 # Write the <x14:colorLow> element. 

8318 self._write_spark_color("x14:colorLow", color) 

8319 

8320 def _write_phonetic_pr(self) -> None: 

8321 # Write the <phoneticPr> element. 

8322 attributes = [ 

8323 ("fontId", "0"), 

8324 ("type", "noConversion"), 

8325 ] 

8326 

8327 self._xml_empty_tag("phoneticPr", attributes) 

8328 

8329 def _write_ignored_errors(self) -> None: 

8330 # Write the <ignoredErrors> element. 

8331 if not self.ignored_errors: 

8332 return 

8333 

8334 self._xml_start_tag("ignoredErrors") 

8335 

8336 if self.ignored_errors.get("number_stored_as_text"): 

8337 ignored_range = self.ignored_errors["number_stored_as_text"] 

8338 self._write_ignored_error("numberStoredAsText", ignored_range) 

8339 

8340 if self.ignored_errors.get("eval_error"): 

8341 ignored_range = self.ignored_errors["eval_error"] 

8342 self._write_ignored_error("evalError", ignored_range) 

8343 

8344 if self.ignored_errors.get("formula_differs"): 

8345 ignored_range = self.ignored_errors["formula_differs"] 

8346 self._write_ignored_error("formula", ignored_range) 

8347 

8348 if self.ignored_errors.get("formula_range"): 

8349 ignored_range = self.ignored_errors["formula_range"] 

8350 self._write_ignored_error("formulaRange", ignored_range) 

8351 

8352 if self.ignored_errors.get("formula_unlocked"): 

8353 ignored_range = self.ignored_errors["formula_unlocked"] 

8354 self._write_ignored_error("unlockedFormula", ignored_range) 

8355 

8356 if self.ignored_errors.get("empty_cell_reference"): 

8357 ignored_range = self.ignored_errors["empty_cell_reference"] 

8358 self._write_ignored_error("emptyCellReference", ignored_range) 

8359 

8360 if self.ignored_errors.get("list_data_validation"): 

8361 ignored_range = self.ignored_errors["list_data_validation"] 

8362 self._write_ignored_error("listDataValidation", ignored_range) 

8363 

8364 if self.ignored_errors.get("calculated_column"): 

8365 ignored_range = self.ignored_errors["calculated_column"] 

8366 self._write_ignored_error("calculatedColumn", ignored_range) 

8367 

8368 if self.ignored_errors.get("two_digit_text_year"): 

8369 ignored_range = self.ignored_errors["two_digit_text_year"] 

8370 self._write_ignored_error("twoDigitTextYear", ignored_range) 

8371 

8372 self._xml_end_tag("ignoredErrors") 

8373 

8374 def _write_ignored_error(self, error_type, ignored_range) -> None: 

8375 # Write the <ignoredError> element. 

8376 attributes = [ 

8377 ("sqref", ignored_range), 

8378 (error_type, 1), 

8379 ] 

8380 

8381 self._xml_empty_tag("ignoredError", attributes)