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

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

188 statements  

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

2# 

3# Worksheet - A class for writing Excel Worksheets. 

4# 

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

6# 

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

8# 

9import datetime 

10import re 

11from typing import Dict, Optional, Tuple, Union 

12from warnings import warn 

13 

14from xlsxwriter.color import Color 

15 

16COL_NAMES: Dict[int, str] = {} 

17 

18CHAR_WIDTHS = { 

19 " ": 3, 

20 "!": 5, 

21 '"': 6, 

22 "#": 7, 

23 "$": 7, 

24 "%": 11, 

25 "&": 10, 

26 "'": 3, 

27 "(": 5, 

28 ")": 5, 

29 "*": 7, 

30 "+": 7, 

31 ",": 4, 

32 "-": 5, 

33 ".": 4, 

34 "/": 6, 

35 "0": 7, 

36 "1": 7, 

37 "2": 7, 

38 "3": 7, 

39 "4": 7, 

40 "5": 7, 

41 "6": 7, 

42 "7": 7, 

43 "8": 7, 

44 "9": 7, 

45 ":": 4, 

46 ";": 4, 

47 "<": 7, 

48 "=": 7, 

49 ">": 7, 

50 "?": 7, 

51 "@": 13, 

52 "A": 9, 

53 "B": 8, 

54 "C": 8, 

55 "D": 9, 

56 "E": 7, 

57 "F": 7, 

58 "G": 9, 

59 "H": 9, 

60 "I": 4, 

61 "J": 5, 

62 "K": 8, 

63 "L": 6, 

64 "M": 12, 

65 "N": 10, 

66 "O": 10, 

67 "P": 8, 

68 "Q": 10, 

69 "R": 8, 

70 "S": 7, 

71 "T": 7, 

72 "U": 9, 

73 "V": 9, 

74 "W": 13, 

75 "X": 8, 

76 "Y": 7, 

77 "Z": 7, 

78 "[": 5, 

79 "\\": 6, 

80 "]": 5, 

81 "^": 7, 

82 "_": 7, 

83 "`": 4, 

84 "a": 7, 

85 "b": 8, 

86 "c": 6, 

87 "d": 8, 

88 "e": 8, 

89 "f": 5, 

90 "g": 7, 

91 "h": 8, 

92 "i": 4, 

93 "j": 4, 

94 "k": 7, 

95 "l": 4, 

96 "m": 12, 

97 "n": 8, 

98 "o": 8, 

99 "p": 8, 

100 "q": 8, 

101 "r": 5, 

102 "s": 6, 

103 "t": 5, 

104 "u": 8, 

105 "v": 7, 

106 "w": 11, 

107 "x": 7, 

108 "y": 7, 

109 "z": 6, 

110 "{": 5, 

111 "|": 7, 

112 "}": 5, 

113 "~": 7, 

114} 

115 

116# The following is a list of Emojis used to decide if worksheet names require 

117# quoting since there is (currently) no native support for matching them in 

118# Python regular expressions. It is probably unnecessary to exclude them since 

119# the default quoting is safe in Excel even when unnecessary (the reverse isn't 

120# true). The Emoji list was generated from: 

121# 

122# https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEmoji%3DYes%3A%5D&abb=on&esc=on&g=&i= 

123# 

124# pylint: disable-next=line-too-long 

125EMOJIS = "\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u261d\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u265f\u2660\u2663\u2665\u2666\u2668\u267b\u267e\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26ce\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\U0001f004\U0001f0cf\U0001f170\U0001f171\U0001f17e\U0001f17f\U0001f18e\U0001f191-\U0001f19a\U0001f1e6-\U0001f1ff\U0001f201\U0001f202\U0001f21a\U0001f22f\U0001f232-\U0001f23a\U0001f250\U0001f251\U0001f300-\U0001f321\U0001f324-\U0001f393\U0001f396\U0001f397\U0001f399-\U0001f39b\U0001f39e-\U0001f3f0\U0001f3f3-\U0001f3f5\U0001f3f7-\U0001f4fd\U0001f4ff-\U0001f53d\U0001f549-\U0001f54e\U0001f550-\U0001f567\U0001f56f\U0001f570\U0001f573-\U0001f57a\U0001f587\U0001f58a-\U0001f58d\U0001f590\U0001f595\U0001f596\U0001f5a4\U0001f5a5\U0001f5a8\U0001f5b1\U0001f5b2\U0001f5bc\U0001f5c2-\U0001f5c4\U0001f5d1-\U0001f5d3\U0001f5dc-\U0001f5de\U0001f5e1\U0001f5e3\U0001f5e8\U0001f5ef\U0001f5f3\U0001f5fa-\U0001f64f\U0001f680-\U0001f6c5\U0001f6cb-\U0001f6d2\U0001f6d5-\U0001f6d7\U0001f6dc-\U0001f6e5\U0001f6e9\U0001f6eb\U0001f6ec\U0001f6f0\U0001f6f3-\U0001f6fc\U0001f7e0-\U0001f7eb\U0001f7f0\U0001f90c-\U0001f93a\U0001f93c-\U0001f945\U0001f947-\U0001f9ff\U0001fa70-\U0001fa7c\U0001fa80-\U0001fa88\U0001fa90-\U0001fabd\U0001fabf-\U0001fac5\U0001face-\U0001fadb\U0001fae0-\U0001fae8\U0001faf0-\U0001faf8" # noqa 

126 

127# Compile performance critical regular expressions. 

128RE_LEADING_WHITESPACE = re.compile(r"^\s") 

129RE_TRAILING_WHITESPACE = re.compile(r"\s$") 

130RE_RANGE_PARTS = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)") 

131RE_QUOTE_RULE1 = re.compile(rf"[^\w\.{EMOJIS}]") 

132RE_QUOTE_RULE2 = re.compile(rf"^[\d\.{EMOJIS}]") 

133RE_QUOTE_RULE3 = re.compile(r"^([A-Z]{1,3}\d+)$") 

134RE_QUOTE_RULE4_ROW = re.compile(r"^R(\d+)") 

135RE_QUOTE_RULE4_COLUMN = re.compile(r"^R?C(\d+)") 

136 

137 

138def xl_rowcol_to_cell( 

139 row: int, 

140 col: int, 

141 row_abs: bool = False, 

142 col_abs: bool = False, 

143) -> str: 

144 """ 

145 Convert a zero indexed row and column cell reference to a A1 style string. 

146 

147 Args: 

148 row: The cell row. Int. 

149 col: The cell column. Int. 

150 row_abs: Optional flag to make the row absolute. Bool. 

151 col_abs: Optional flag to make the column absolute. Bool. 

152 

153 Returns: 

154 A1 style string. 

155 

156 """ 

157 if row < 0: 

158 warn(f"Row number '{row}' must be >= 0") 

159 return "" 

160 

161 if col < 0: 

162 warn(f"Col number '{col}' must be >= 0") 

163 return "" 

164 

165 row += 1 # Change to 1-index. 

166 row_abs_str = "$" if row_abs else "" 

167 

168 col_str = xl_col_to_name(col, col_abs) 

169 

170 return col_str + row_abs_str + str(row) 

171 

172 

173def xl_rowcol_to_cell_fast(row: int, col: int) -> str: 

174 """ 

175 Optimized version of the xl_rowcol_to_cell function. Only used internally. 

176 

177 Args: 

178 row: The cell row. Int. 

179 col: The cell column. Int. 

180 

181 Returns: 

182 A1 style string. 

183 

184 """ 

185 if col in COL_NAMES: 

186 col_str = COL_NAMES[col] 

187 else: 

188 col_str = xl_col_to_name(col) 

189 COL_NAMES[col] = col_str 

190 

191 return col_str + str(row + 1) 

192 

193 

194def xl_col_to_name(col: int, col_abs: bool = False) -> str: 

195 """ 

196 Convert a zero indexed column cell reference to a string. 

197 

198 Args: 

199 col: The cell column. Int. 

200 col_abs: Optional flag to make the column absolute. Bool. 

201 

202 Returns: 

203 Column style string. 

204 

205 """ 

206 col_num = col 

207 if col_num < 0: 

208 warn(f"Col number '{col_num}' must be >= 0") 

209 return "" 

210 

211 col_num += 1 # Change to 1-index. 

212 col_str = "" 

213 col_abs_str = "$" if col_abs else "" 

214 

215 while col_num: 

216 # Set remainder from 1 .. 26 

217 remainder = col_num % 26 

218 

219 if remainder == 0: 

220 remainder = 26 

221 

222 # Convert the remainder to a character. 

223 col_letter = chr(ord("A") + remainder - 1) 

224 

225 # Accumulate the column letters, right to left. 

226 col_str = col_letter + col_str 

227 

228 # Get the next order of magnitude. 

229 col_num = int((col_num - 1) / 26) 

230 

231 return col_abs_str + col_str 

232 

233 

234def xl_cell_to_rowcol(cell_str: str) -> Tuple[int, int]: 

235 """ 

236 Convert a cell reference in A1 notation to a zero indexed row and column. 

237 

238 Args: 

239 cell_str: A1 style string. 

240 

241 Returns: 

242 row, col: Zero indexed cell row and column indices. 

243 

244 """ 

245 if not cell_str: 

246 return 0, 0 

247 

248 match = RE_RANGE_PARTS.match(cell_str) 

249 if match is None: 

250 warn(f"Invalid cell reference '{cell_str}'") 

251 return 0, 0 

252 

253 col_str = match.group(2) 

254 row_str = match.group(4) 

255 

256 # Convert base26 column string to number. 

257 expn = 0 

258 col = 0 

259 for char in reversed(col_str): 

260 col += (ord(char) - ord("A") + 1) * (26**expn) 

261 expn += 1 

262 

263 # Convert 1-index to zero-index 

264 row = int(row_str) - 1 

265 col -= 1 

266 

267 return row, col 

268 

269 

270def xl_cell_to_rowcol_abs(cell_str: str) -> Tuple[int, int, bool, bool]: 

271 """ 

272 Convert an absolute cell reference in A1 notation to a zero indexed 

273 row and column, with True/False values for absolute rows or columns. 

274 

275 Args: 

276 cell_str: A1 style string. 

277 

278 Returns: 

279 row, col, row_abs, col_abs: Zero indexed cell row and column indices. 

280 

281 """ 

282 if not cell_str: 

283 return 0, 0, False, False 

284 

285 match = RE_RANGE_PARTS.match(cell_str) 

286 if match is None: 

287 warn(f"Invalid cell reference '{cell_str}'") 

288 return 0, 0, False, False 

289 

290 col_abs = bool(match.group(1)) 

291 col_str = match.group(2) 

292 row_abs = bool(match.group(3)) 

293 row_str = match.group(4) 

294 

295 # Convert base26 column string to number. 

296 expn = 0 

297 col = 0 

298 for char in reversed(col_str): 

299 col += (ord(char) - ord("A") + 1) * (26**expn) 

300 expn += 1 

301 

302 # Convert 1-index to zero-index 

303 row = int(row_str) - 1 

304 col -= 1 

305 

306 return row, col, row_abs, col_abs 

307 

308 

309def xl_range(first_row: int, first_col: int, last_row: int, last_col: int) -> str: 

310 """ 

311 Convert zero indexed row and col cell references to a A1:B1 range string. 

312 

313 Args: 

314 first_row: The first cell row. Int. 

315 first_col: The first cell column. Int. 

316 last_row: The last cell row. Int. 

317 last_col: The last cell column. Int. 

318 

319 Returns: 

320 A1:B1 style range string. 

321 

322 """ 

323 range1 = xl_rowcol_to_cell(first_row, first_col) 

324 range2 = xl_rowcol_to_cell(last_row, last_col) 

325 

326 if range1 == "" or range2 == "": 

327 warn("Row and column numbers must be >= 0") 

328 return "" 

329 

330 if range1 == range2: 

331 return range1 

332 

333 return range1 + ":" + range2 

334 

335 

336def xl_range_abs(first_row: int, first_col: int, last_row: int, last_col: int) -> str: 

337 """ 

338 Convert zero indexed row and col cell references to a $A$1:$B$1 absolute 

339 range string. 

340 

341 Args: 

342 first_row: The first cell row. Int. 

343 first_col: The first cell column. Int. 

344 last_row: The last cell row. Int. 

345 last_col: The last cell column. Int. 

346 

347 Returns: 

348 $A$1:$B$1 style range string. 

349 

350 """ 

351 range1 = xl_rowcol_to_cell(first_row, first_col, True, True) 

352 range2 = xl_rowcol_to_cell(last_row, last_col, True, True) 

353 

354 if range1 == "" or range2 == "": 

355 warn("Row and column numbers must be >= 0") 

356 return "" 

357 

358 if range1 == range2: 

359 return range1 

360 

361 return range1 + ":" + range2 

362 

363 

364def xl_range_formula( 

365 sheetname: str, first_row: int, first_col: int, last_row: int, last_col: int 

366) -> str: 

367 """ 

368 Convert worksheet name and zero indexed row and col cell references to 

369 a Sheet1!A1:B1 range formula string. 

370 

371 Args: 

372 sheetname: The worksheet name. String. 

373 first_row: The first cell row. Int. 

374 first_col: The first cell column. Int. 

375 last_row: The last cell row. Int. 

376 last_col: The last cell column. Int. 

377 

378 Returns: 

379 A1:B1 style range string. 

380 

381 """ 

382 cell_range = xl_range_abs(first_row, first_col, last_row, last_col) 

383 sheetname = quote_sheetname(sheetname) 

384 

385 return sheetname + "!" + cell_range 

386 

387 

388def quote_sheetname(sheetname: str) -> str: 

389 """ 

390 Sheetnames used in references should be quoted if they contain any spaces, 

391 special characters or if they look like a A1 or RC cell reference. The rules 

392 are shown inline below. 

393 

394 Args: 

395 sheetname: The worksheet name. String. 

396 

397 Returns: 

398 A quoted worksheet string. 

399 

400 """ 

401 uppercase_sheetname = sheetname.upper() 

402 requires_quoting = False 

403 col_max = 163_84 

404 row_max = 1048576 

405 

406 # Don't quote sheetname if it is already quoted by the user. 

407 if not sheetname.startswith("'"): 

408 

409 match_rule3 = RE_QUOTE_RULE3.match(uppercase_sheetname) 

410 match_rule4_row = RE_QUOTE_RULE4_ROW.match(uppercase_sheetname) 

411 match_rule4_column = RE_QUOTE_RULE4_COLUMN.match(uppercase_sheetname) 

412 

413 # -------------------------------------------------------------------- 

414 # Rule 1. Sheet names that contain anything other than \w and "." 

415 # characters must be quoted. 

416 # -------------------------------------------------------------------- 

417 if RE_QUOTE_RULE1.search(sheetname): 

418 requires_quoting = True 

419 

420 # -------------------------------------------------------------------- 

421 # Rule 2. Sheet names that start with a digit or "." must be quoted. 

422 # -------------------------------------------------------------------- 

423 elif RE_QUOTE_RULE2.search(sheetname): 

424 requires_quoting = True 

425 

426 # -------------------------------------------------------------------- 

427 # Rule 3. Sheet names must not be a valid A1 style cell reference. 

428 # Valid means that the row and column range values must also be within 

429 # Excel row and column limits. 

430 # -------------------------------------------------------------------- 

431 elif match_rule3: 

432 cell = match_rule3.group(1) 

433 (row, col) = xl_cell_to_rowcol(cell) 

434 

435 if 0 <= row < row_max and 0 <= col < col_max: 

436 requires_quoting = True 

437 

438 # -------------------------------------------------------------------- 

439 # Rule 4. Sheet names must not *start* with a valid RC style cell 

440 # reference. Other characters after the valid RC reference are ignored 

441 # by Excel. Valid means that the row and column range values must also 

442 # be within Excel row and column limits. 

443 # 

444 # Note: references without trailing characters like R12345 or C12345 

445 # are caught by Rule 3. Negative references like R-12345 are caught by 

446 # Rule 1 due to the dash. 

447 # -------------------------------------------------------------------- 

448 

449 # Rule 4a. Check for sheet names that start with R1 style references. 

450 elif match_rule4_row: 

451 row = int(match_rule4_row.group(1)) 

452 

453 if 0 < row <= row_max: 

454 requires_quoting = True 

455 

456 # Rule 4b. Check for sheet names that start with C1 or RC1 style 

457 elif match_rule4_column: 

458 col = int(match_rule4_column.group(1)) 

459 

460 if 0 < col <= col_max: 

461 requires_quoting = True 

462 

463 # Rule 4c. Check for some single R/C references. 

464 elif uppercase_sheetname in ("R", "C", "RC"): 

465 requires_quoting = True 

466 

467 if requires_quoting: 

468 # Double quote any single quotes. 

469 sheetname = sheetname.replace("'", "''") 

470 

471 # Single quote the sheet name. 

472 sheetname = f"'{sheetname}'" 

473 

474 return sheetname 

475 

476 

477def cell_autofit_width(string: str) -> int: 

478 """ 

479 Calculate the width required to auto-fit a string in a cell. 

480 

481 Args: 

482 string: The string to calculate the cell width for. String. 

483 

484 Returns: 

485 The string autofit width in pixels. Returns 0 if the string is empty. 

486 

487 """ 

488 if not string or len(string) == 0: 

489 return 0 

490 

491 # Excel adds an additional 7 pixels of padding to the cell boundary. 

492 return xl_pixel_width(string) + 7 

493 

494 

495def xl_pixel_width(string: str) -> int: 

496 """ 

497 Get the pixel width of a string based on individual character widths taken 

498 from Excel. UTF8 characters, and other unhandled characters, are given a 

499 default width of 8. 

500 

501 Args: 

502 string: The string to calculate the width for. String. 

503 

504 Returns: 

505 The string width in pixels. Note, Excel adds an additional 7 pixels of 

506 padding in the cell. 

507 

508 """ 

509 length = 0 

510 for char in string: 

511 length += CHAR_WIDTHS.get(char, 8) 

512 

513 return length 

514 

515 

516def _get_sparkline_style(style_id: int) -> Dict[str, Dict[str, str]]: 

517 """ 

518 Get the numbered sparkline styles. 

519 

520 """ 

521 styles = [ 

522 { # 0 

523 "low": Color.theme(4, 0), 

524 "high": Color.theme(4, 0), 

525 "last": Color.theme(4, 3), 

526 "first": Color.theme(4, 3), 

527 "series": Color.theme(4, 5), 

528 "markers": Color.theme(4, 5), 

529 "negative": Color.theme(5, 0), 

530 }, 

531 { # 1 

532 "low": Color.theme(4, 0), 

533 "high": Color.theme(4, 0), 

534 "last": Color.theme(4, 3), 

535 "first": Color.theme(4, 3), 

536 "series": Color.theme(4, 5), 

537 "markers": Color.theme(4, 5), 

538 "negative": Color.theme(5, 0), 

539 }, 

540 { # 2 

541 "low": Color.theme(5, 0), 

542 "high": Color.theme(5, 0), 

543 "last": Color.theme(5, 3), 

544 "first": Color.theme(5, 3), 

545 "series": Color.theme(5, 5), 

546 "markers": Color.theme(5, 5), 

547 "negative": Color.theme(6, 0), 

548 }, 

549 { # 3 

550 "low": Color.theme(6, 0), 

551 "high": Color.theme(6, 0), 

552 "last": Color.theme(6, 3), 

553 "first": Color.theme(6, 3), 

554 "series": Color.theme(6, 5), 

555 "markers": Color.theme(6, 5), 

556 "negative": Color.theme(7, 0), 

557 }, 

558 { # 4 

559 "low": Color.theme(7, 0), 

560 "high": Color.theme(7, 0), 

561 "last": Color.theme(7, 3), 

562 "first": Color.theme(7, 3), 

563 "series": Color.theme(7, 5), 

564 "markers": Color.theme(7, 5), 

565 "negative": Color.theme(8, 0), 

566 }, 

567 { # 5 

568 "low": Color.theme(8, 0), 

569 "high": Color.theme(8, 0), 

570 "last": Color.theme(8, 3), 

571 "first": Color.theme(8, 3), 

572 "series": Color.theme(8, 5), 

573 "markers": Color.theme(8, 5), 

574 "negative": Color.theme(9, 0), 

575 }, 

576 { # 6 

577 "low": Color.theme(9, 0), 

578 "high": Color.theme(9, 0), 

579 "last": Color.theme(9, 3), 

580 "first": Color.theme(9, 3), 

581 "series": Color.theme(9, 5), 

582 "markers": Color.theme(9, 5), 

583 "negative": Color.theme(4, 0), 

584 }, 

585 { # 7 

586 "low": Color.theme(5, 4), 

587 "high": Color.theme(5, 4), 

588 "last": Color.theme(5, 4), 

589 "first": Color.theme(5, 4), 

590 "series": Color.theme(4, 4), 

591 "markers": Color.theme(5, 4), 

592 "negative": Color.theme(5, 0), 

593 }, 

594 { # 8 

595 "low": Color.theme(6, 4), 

596 "high": Color.theme(6, 4), 

597 "last": Color.theme(6, 4), 

598 "first": Color.theme(6, 4), 

599 "series": Color.theme(5, 4), 

600 "markers": Color.theme(6, 4), 

601 "negative": Color.theme(6, 0), 

602 }, 

603 { # 9 

604 "low": Color.theme(7, 4), 

605 "high": Color.theme(7, 4), 

606 "last": Color.theme(7, 4), 

607 "first": Color.theme(7, 4), 

608 "series": Color.theme(6, 4), 

609 "markers": Color.theme(7, 4), 

610 "negative": Color.theme(7, 0), 

611 }, 

612 { # 10 

613 "low": Color.theme(8, 4), 

614 "high": Color.theme(8, 4), 

615 "last": Color.theme(8, 4), 

616 "first": Color.theme(8, 4), 

617 "series": Color.theme(7, 4), 

618 "markers": Color.theme(8, 4), 

619 "negative": Color.theme(8, 0), 

620 }, 

621 { # 11 

622 "low": Color.theme(9, 4), 

623 "high": Color.theme(9, 4), 

624 "last": Color.theme(9, 4), 

625 "first": Color.theme(9, 4), 

626 "series": Color.theme(8, 4), 

627 "markers": Color.theme(9, 4), 

628 "negative": Color.theme(9, 0), 

629 }, 

630 { # 12 

631 "low": Color.theme(4, 4), 

632 "high": Color.theme(4, 4), 

633 "last": Color.theme(4, 4), 

634 "first": Color.theme(4, 4), 

635 "series": Color.theme(9, 4), 

636 "markers": Color.theme(4, 4), 

637 "negative": Color.theme(4, 0), 

638 }, 

639 { # 13 

640 "low": Color.theme(4, 4), 

641 "high": Color.theme(4, 4), 

642 "last": Color.theme(4, 4), 

643 "first": Color.theme(4, 4), 

644 "series": Color.theme(4, 0), 

645 "markers": Color.theme(4, 4), 

646 "negative": Color.theme(5, 0), 

647 }, 

648 { # 14 

649 "low": Color.theme(5, 4), 

650 "high": Color.theme(5, 4), 

651 "last": Color.theme(5, 4), 

652 "first": Color.theme(5, 4), 

653 "series": Color.theme(5, 0), 

654 "markers": Color.theme(5, 4), 

655 "negative": Color.theme(6, 0), 

656 }, 

657 { # 15 

658 "low": Color.theme(6, 4), 

659 "high": Color.theme(6, 4), 

660 "last": Color.theme(6, 4), 

661 "first": Color.theme(6, 4), 

662 "series": Color.theme(6, 0), 

663 "markers": Color.theme(6, 4), 

664 "negative": Color.theme(7, 0), 

665 }, 

666 { # 16 

667 "low": Color.theme(7, 4), 

668 "high": Color.theme(7, 4), 

669 "last": Color.theme(7, 4), 

670 "first": Color.theme(7, 4), 

671 "series": Color.theme(7, 0), 

672 "markers": Color.theme(7, 4), 

673 "negative": Color.theme(8, 0), 

674 }, 

675 { # 17 

676 "low": Color.theme(8, 4), 

677 "high": Color.theme(8, 4), 

678 "last": Color.theme(8, 4), 

679 "first": Color.theme(8, 4), 

680 "series": Color.theme(8, 0), 

681 "markers": Color.theme(8, 4), 

682 "negative": Color.theme(9, 0), 

683 }, 

684 { # 18 

685 "low": Color.theme(9, 4), 

686 "high": Color.theme(9, 4), 

687 "last": Color.theme(9, 4), 

688 "first": Color.theme(9, 4), 

689 "series": Color.theme(9, 0), 

690 "markers": Color.theme(9, 4), 

691 "negative": Color.theme(4, 0), 

692 }, 

693 { # 19 

694 "low": Color.theme(4, 5), 

695 "high": Color.theme(4, 5), 

696 "last": Color.theme(4, 4), 

697 "first": Color.theme(4, 4), 

698 "series": Color.theme(4, 3), 

699 "markers": Color.theme(4, 1), 

700 "negative": Color.theme(0, 5), 

701 }, 

702 { # 20 

703 "low": Color.theme(5, 5), 

704 "high": Color.theme(5, 5), 

705 "last": Color.theme(5, 4), 

706 "first": Color.theme(5, 4), 

707 "series": Color.theme(5, 3), 

708 "markers": Color.theme(5, 1), 

709 "negative": Color.theme(0, 5), 

710 }, 

711 { # 21 

712 "low": Color.theme(6, 5), 

713 "high": Color.theme(6, 5), 

714 "last": Color.theme(6, 4), 

715 "first": Color.theme(6, 4), 

716 "series": Color.theme(6, 3), 

717 "markers": Color.theme(6, 1), 

718 "negative": Color.theme(0, 5), 

719 }, 

720 { # 22 

721 "low": Color.theme(7, 5), 

722 "high": Color.theme(7, 5), 

723 "last": Color.theme(7, 4), 

724 "first": Color.theme(7, 4), 

725 "series": Color.theme(7, 3), 

726 "markers": Color.theme(7, 1), 

727 "negative": Color.theme(0, 5), 

728 }, 

729 { # 23 

730 "low": Color.theme(8, 5), 

731 "high": Color.theme(8, 5), 

732 "last": Color.theme(8, 4), 

733 "first": Color.theme(8, 4), 

734 "series": Color.theme(8, 3), 

735 "markers": Color.theme(8, 1), 

736 "negative": Color.theme(0, 5), 

737 }, 

738 { # 24 

739 "low": Color.theme(9, 5), 

740 "high": Color.theme(9, 5), 

741 "last": Color.theme(9, 4), 

742 "first": Color.theme(9, 4), 

743 "series": Color.theme(9, 3), 

744 "markers": Color.theme(9, 1), 

745 "negative": Color.theme(0, 5), 

746 }, 

747 { # 25 

748 "low": Color.theme(1, 3), 

749 "high": Color.theme(1, 3), 

750 "last": Color.theme(1, 3), 

751 "first": Color.theme(1, 3), 

752 "series": Color.theme(1, 1), 

753 "markers": Color.theme(1, 3), 

754 "negative": Color.theme(1, 3), 

755 }, 

756 { # 26 

757 "low": Color.theme(0, 3), 

758 "high": Color.theme(0, 3), 

759 "last": Color.theme(0, 3), 

760 "first": Color.theme(0, 3), 

761 "series": Color.theme(1, 2), 

762 "markers": Color.theme(0, 3), 

763 "negative": Color.theme(0, 3), 

764 }, 

765 { # 27 

766 "low": Color("#D00000"), 

767 "high": Color("#D00000"), 

768 "last": Color("#D00000"), 

769 "first": Color("#D00000"), 

770 "series": Color("#323232"), 

771 "markers": Color("#D00000"), 

772 "negative": Color("#D00000"), 

773 }, 

774 { # 28 

775 "low": Color("#0070C0"), 

776 "high": Color("#0070C0"), 

777 "last": Color("#0070C0"), 

778 "first": Color("#0070C0"), 

779 "series": Color("#000000"), 

780 "markers": Color("#0070C0"), 

781 "negative": Color("#0070C0"), 

782 }, 

783 { # 29 

784 "low": Color("#D00000"), 

785 "high": Color("#D00000"), 

786 "last": Color("#D00000"), 

787 "first": Color("#D00000"), 

788 "series": Color("#376092"), 

789 "markers": Color("#D00000"), 

790 "negative": Color("#D00000"), 

791 }, 

792 { # 30 

793 "low": Color("#000000"), 

794 "high": Color("#000000"), 

795 "last": Color("#000000"), 

796 "first": Color("#000000"), 

797 "series": Color("#0070C0"), 

798 "markers": Color("#000000"), 

799 "negative": Color("#000000"), 

800 }, 

801 { # 31 

802 "low": Color("#FF5055"), 

803 "high": Color("#56BE79"), 

804 "last": Color("#359CEB"), 

805 "first": Color("#5687C2"), 

806 "series": Color("#5F5F5F"), 

807 "markers": Color("#D70077"), 

808 "negative": Color("#FFB620"), 

809 }, 

810 { # 32 

811 "low": Color("#FF5055"), 

812 "high": Color("#56BE79"), 

813 "last": Color("#359CEB"), 

814 "first": Color("#777777"), 

815 "series": Color("#5687C2"), 

816 "markers": Color("#D70077"), 

817 "negative": Color("#FFB620"), 

818 }, 

819 { # 33 

820 "low": Color("#FF5367"), 

821 "high": Color("#60D276"), 

822 "last": Color("#FFEB9C"), 

823 "first": Color("#FFDC47"), 

824 "series": Color("#C6EFCE"), 

825 "markers": Color("#8CADD6"), 

826 "negative": Color("#FFC7CE"), 

827 }, 

828 { # 34 

829 "low": Color("#FF0000"), 

830 "high": Color("#00B050"), 

831 "last": Color("#FFC000"), 

832 "first": Color("#FFC000"), 

833 "series": Color("#00B050"), 

834 "markers": Color("#0070C0"), 

835 "negative": Color("#FF0000"), 

836 }, 

837 { # 35 

838 "low": Color.theme(7, 0), 

839 "high": Color.theme(6, 0), 

840 "last": Color.theme(5, 0), 

841 "first": Color.theme(4, 0), 

842 "series": Color.theme(3, 0), 

843 "markers": Color.theme(8, 0), 

844 "negative": Color.theme(9, 0), 

845 }, 

846 { # 36 

847 "low": Color.theme(7, 0), 

848 "high": Color.theme(6, 0), 

849 "last": Color.theme(5, 0), 

850 "first": Color.theme(4, 0), 

851 "series": Color.theme(1, 0), 

852 "markers": Color.theme(8, 0), 

853 "negative": Color.theme(9, 0), 

854 }, 

855 ] 

856 

857 return styles[style_id] 

858 

859 

860def _supported_datetime( 

861 dt: Union[datetime.datetime, datetime.time, datetime.date], 

862) -> bool: 

863 # Determine is an argument is a supported datetime object. 

864 return isinstance( 

865 dt, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) 

866 ) 

867 

868 

869def _remove_datetime_timezone( 

870 dt_obj: datetime.datetime, remove_timezone: bool 

871) -> datetime.datetime: 

872 # Excel doesn't support timezones in datetimes/times so we remove the 

873 # tzinfo from the object if the user has specified that option in the 

874 # constructor. 

875 if remove_timezone: 

876 dt_obj = dt_obj.replace(tzinfo=None) 

877 else: 

878 if dt_obj.tzinfo: 

879 raise TypeError( 

880 "Excel doesn't support timezones in datetimes. " 

881 "Set the tzinfo in the datetime/time object to None or " 

882 "use the 'remove_timezone' Workbook() option" 

883 ) 

884 

885 return dt_obj 

886 

887 

888def _datetime_to_excel_datetime( 

889 dt_obj: Union[datetime.time, datetime.datetime, datetime.timedelta, datetime.date], 

890 date_1904: bool, 

891 remove_timezone: bool, 

892) -> float: 

893 # Convert a datetime object to an Excel serial date and time. The integer 

894 # part of the number stores the number of days since the epoch and the 

895 # fractional part stores the percentage of the day. 

896 date_type = dt_obj 

897 is_timedelta = False 

898 

899 if date_1904: 

900 # Excel for Mac date epoch. 

901 epoch = datetime.datetime(1904, 1, 1) 

902 else: 

903 # Default Excel epoch. 

904 epoch = datetime.datetime(1899, 12, 31) 

905 

906 # We handle datetime .datetime, .date and .time objects but convert 

907 # them to datetime.datetime objects and process them in the same way. 

908 if isinstance(dt_obj, datetime.datetime): 

909 dt_obj = _remove_datetime_timezone(dt_obj, remove_timezone) 

910 delta = dt_obj - epoch 

911 elif isinstance(dt_obj, datetime.date): 

912 dt_obj = datetime.datetime.fromordinal(dt_obj.toordinal()) 

913 delta = dt_obj - epoch 

914 elif isinstance(dt_obj, datetime.time): 

915 dt_obj = datetime.datetime.combine(epoch, dt_obj) 

916 dt_obj = _remove_datetime_timezone(dt_obj, remove_timezone) 

917 delta = dt_obj - epoch 

918 elif isinstance(dt_obj, datetime.timedelta): 

919 is_timedelta = True 

920 delta = dt_obj 

921 else: 

922 raise TypeError("Unknown or unsupported datetime type") 

923 

924 # Convert a Python datetime.datetime value to an Excel date number. 

925 excel_time = delta.days + ( 

926 float(delta.seconds) + float(delta.microseconds) / 1e6 

927 ) / (60 * 60 * 24) 

928 

929 # The following is a workaround for the fact that in Excel a time only 

930 # value is represented as 1899-12-31+time whereas in datetime.datetime() 

931 # it is 1900-1-1+time so we need to subtract the 1 day difference. 

932 if ( 

933 isinstance(date_type, datetime.datetime) 

934 and not isinstance(dt_obj, datetime.timedelta) 

935 and dt_obj.isocalendar() 

936 == ( 

937 1900, 

938 1, 

939 1, 

940 ) 

941 ): 

942 excel_time -= 1 

943 

944 # Account for Excel erroneously treating 1900 as a leap year. 

945 if not date_1904 and not is_timedelta and excel_time > 59: 

946 excel_time += 1 

947 

948 return excel_time 

949 

950 

951def _preserve_whitespace(string: str) -> Optional[re.Match]: 

952 # Check if a string has leading or trailing whitespace that requires a 

953 # "preserve" attribute. 

954 return RE_LEADING_WHITESPACE.search(string) or RE_TRAILING_WHITESPACE.search(string)