Files
bussines_case_automation/venv/lib/python3.12/site-packages/xlsxwriter/utility.py
andrei 0e2e1bddba Add xlsxwriter-based Excel generation scripts with openpyxl implementation
- Created create_excel_xlsxwriter.py and update_excel_xlsxwriter.py
- Uses openpyxl exclusively to preserve Excel formatting and formulas
- Updated server.js to use new xlsxwriter scripts for form submissions
- Maintains all original functionality while ensuring proper Excel file handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 13:53:06 +00:00

955 lines
29 KiB
Python

###############################################################################
#
# Worksheet - A class for writing Excel Worksheets.
#
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
#
import datetime
import re
from typing import Dict, Optional, Tuple, Union
from warnings import warn
from xlsxwriter.color import Color
COL_NAMES: Dict[int, str] = {}
CHAR_WIDTHS = {
" ": 3,
"!": 5,
'"': 6,
"#": 7,
"$": 7,
"%": 11,
"&": 10,
"'": 3,
"(": 5,
")": 5,
"*": 7,
"+": 7,
",": 4,
"-": 5,
".": 4,
"/": 6,
"0": 7,
"1": 7,
"2": 7,
"3": 7,
"4": 7,
"5": 7,
"6": 7,
"7": 7,
"8": 7,
"9": 7,
":": 4,
";": 4,
"<": 7,
"=": 7,
">": 7,
"?": 7,
"@": 13,
"A": 9,
"B": 8,
"C": 8,
"D": 9,
"E": 7,
"F": 7,
"G": 9,
"H": 9,
"I": 4,
"J": 5,
"K": 8,
"L": 6,
"M": 12,
"N": 10,
"O": 10,
"P": 8,
"Q": 10,
"R": 8,
"S": 7,
"T": 7,
"U": 9,
"V": 9,
"W": 13,
"X": 8,
"Y": 7,
"Z": 7,
"[": 5,
"\\": 6,
"]": 5,
"^": 7,
"_": 7,
"`": 4,
"a": 7,
"b": 8,
"c": 6,
"d": 8,
"e": 8,
"f": 5,
"g": 7,
"h": 8,
"i": 4,
"j": 4,
"k": 7,
"l": 4,
"m": 12,
"n": 8,
"o": 8,
"p": 8,
"q": 8,
"r": 5,
"s": 6,
"t": 5,
"u": 8,
"v": 7,
"w": 11,
"x": 7,
"y": 7,
"z": 6,
"{": 5,
"|": 7,
"}": 5,
"~": 7,
}
# The following is a list of Emojis used to decide if worksheet names require
# quoting since there is (currently) no native support for matching them in
# Python regular expressions. It is probably unnecessary to exclude them since
# the default quoting is safe in Excel even when unnecessary (the reverse isn't
# true). The Emoji list was generated from:
#
# https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEmoji%3DYes%3A%5D&abb=on&esc=on&g=&i=
#
# pylint: disable-next=line-too-long
EMOJIS = "\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
# Compile performance critical regular expressions.
RE_LEADING_WHITESPACE = re.compile(r"^\s")
RE_TRAILING_WHITESPACE = re.compile(r"\s$")
RE_RANGE_PARTS = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")
RE_QUOTE_RULE1 = re.compile(rf"[^\w\.{EMOJIS}]")
RE_QUOTE_RULE2 = re.compile(rf"^[\d\.{EMOJIS}]")
RE_QUOTE_RULE3 = re.compile(r"^([A-Z]{1,3}\d+)$")
RE_QUOTE_RULE4_ROW = re.compile(r"^R(\d+)")
RE_QUOTE_RULE4_COLUMN = re.compile(r"^R?C(\d+)")
def xl_rowcol_to_cell(
row: int,
col: int,
row_abs: bool = False,
col_abs: bool = False,
) -> str:
"""
Convert a zero indexed row and column cell reference to a A1 style string.
Args:
row: The cell row. Int.
col: The cell column. Int.
row_abs: Optional flag to make the row absolute. Bool.
col_abs: Optional flag to make the column absolute. Bool.
Returns:
A1 style string.
"""
if row < 0:
warn(f"Row number '{row}' must be >= 0")
return ""
if col < 0:
warn(f"Col number '{col}' must be >= 0")
return ""
row += 1 # Change to 1-index.
row_abs_str = "$" if row_abs else ""
col_str = xl_col_to_name(col, col_abs)
return col_str + row_abs_str + str(row)
def xl_rowcol_to_cell_fast(row: int, col: int) -> str:
"""
Optimized version of the xl_rowcol_to_cell function. Only used internally.
Args:
row: The cell row. Int.
col: The cell column. Int.
Returns:
A1 style string.
"""
if col in COL_NAMES:
col_str = COL_NAMES[col]
else:
col_str = xl_col_to_name(col)
COL_NAMES[col] = col_str
return col_str + str(row + 1)
def xl_col_to_name(col: int, col_abs: bool = False) -> str:
"""
Convert a zero indexed column cell reference to a string.
Args:
col: The cell column. Int.
col_abs: Optional flag to make the column absolute. Bool.
Returns:
Column style string.
"""
col_num = col
if col_num < 0:
warn(f"Col number '{col_num}' must be >= 0")
return ""
col_num += 1 # Change to 1-index.
col_str = ""
col_abs_str = "$" if col_abs else ""
while col_num:
# Set remainder from 1 .. 26
remainder = col_num % 26
if remainder == 0:
remainder = 26
# Convert the remainder to a character.
col_letter = chr(ord("A") + remainder - 1)
# Accumulate the column letters, right to left.
col_str = col_letter + col_str
# Get the next order of magnitude.
col_num = int((col_num - 1) / 26)
return col_abs_str + col_str
def xl_cell_to_rowcol(cell_str: str) -> Tuple[int, int]:
"""
Convert a cell reference in A1 notation to a zero indexed row and column.
Args:
cell_str: A1 style string.
Returns:
row, col: Zero indexed cell row and column indices.
"""
if not cell_str:
return 0, 0
match = RE_RANGE_PARTS.match(cell_str)
if match is None:
warn(f"Invalid cell reference '{cell_str}'")
return 0, 0
col_str = match.group(2)
row_str = match.group(4)
# Convert base26 column string to number.
expn = 0
col = 0
for char in reversed(col_str):
col += (ord(char) - ord("A") + 1) * (26**expn)
expn += 1
# Convert 1-index to zero-index
row = int(row_str) - 1
col -= 1
return row, col
def xl_cell_to_rowcol_abs(cell_str: str) -> Tuple[int, int, bool, bool]:
"""
Convert an absolute cell reference in A1 notation to a zero indexed
row and column, with True/False values for absolute rows or columns.
Args:
cell_str: A1 style string.
Returns:
row, col, row_abs, col_abs: Zero indexed cell row and column indices.
"""
if not cell_str:
return 0, 0, False, False
match = RE_RANGE_PARTS.match(cell_str)
if match is None:
warn(f"Invalid cell reference '{cell_str}'")
return 0, 0, False, False
col_abs = bool(match.group(1))
col_str = match.group(2)
row_abs = bool(match.group(3))
row_str = match.group(4)
# Convert base26 column string to number.
expn = 0
col = 0
for char in reversed(col_str):
col += (ord(char) - ord("A") + 1) * (26**expn)
expn += 1
# Convert 1-index to zero-index
row = int(row_str) - 1
col -= 1
return row, col, row_abs, col_abs
def xl_range(first_row: int, first_col: int, last_row: int, last_col: int) -> str:
"""
Convert zero indexed row and col cell references to a A1:B1 range string.
Args:
first_row: The first cell row. Int.
first_col: The first cell column. Int.
last_row: The last cell row. Int.
last_col: The last cell column. Int.
Returns:
A1:B1 style range string.
"""
range1 = xl_rowcol_to_cell(first_row, first_col)
range2 = xl_rowcol_to_cell(last_row, last_col)
if range1 == "" or range2 == "":
warn("Row and column numbers must be >= 0")
return ""
if range1 == range2:
return range1
return range1 + ":" + range2
def xl_range_abs(first_row: int, first_col: int, last_row: int, last_col: int) -> str:
"""
Convert zero indexed row and col cell references to a $A$1:$B$1 absolute
range string.
Args:
first_row: The first cell row. Int.
first_col: The first cell column. Int.
last_row: The last cell row. Int.
last_col: The last cell column. Int.
Returns:
$A$1:$B$1 style range string.
"""
range1 = xl_rowcol_to_cell(first_row, first_col, True, True)
range2 = xl_rowcol_to_cell(last_row, last_col, True, True)
if range1 == "" or range2 == "":
warn("Row and column numbers must be >= 0")
return ""
if range1 == range2:
return range1
return range1 + ":" + range2
def xl_range_formula(
sheetname: str, first_row: int, first_col: int, last_row: int, last_col: int
) -> str:
"""
Convert worksheet name and zero indexed row and col cell references to
a Sheet1!A1:B1 range formula string.
Args:
sheetname: The worksheet name. String.
first_row: The first cell row. Int.
first_col: The first cell column. Int.
last_row: The last cell row. Int.
last_col: The last cell column. Int.
Returns:
A1:B1 style range string.
"""
cell_range = xl_range_abs(first_row, first_col, last_row, last_col)
sheetname = quote_sheetname(sheetname)
return sheetname + "!" + cell_range
def quote_sheetname(sheetname: str) -> str:
"""
Sheetnames used in references should be quoted if they contain any spaces,
special characters or if they look like a A1 or RC cell reference. The rules
are shown inline below.
Args:
sheetname: The worksheet name. String.
Returns:
A quoted worksheet string.
"""
uppercase_sheetname = sheetname.upper()
requires_quoting = False
col_max = 163_84
row_max = 1048576
# Don't quote sheetname if it is already quoted by the user.
if not sheetname.startswith("'"):
match_rule3 = RE_QUOTE_RULE3.match(uppercase_sheetname)
match_rule4_row = RE_QUOTE_RULE4_ROW.match(uppercase_sheetname)
match_rule4_column = RE_QUOTE_RULE4_COLUMN.match(uppercase_sheetname)
# --------------------------------------------------------------------
# Rule 1. Sheet names that contain anything other than \w and "."
# characters must be quoted.
# --------------------------------------------------------------------
if RE_QUOTE_RULE1.search(sheetname):
requires_quoting = True
# --------------------------------------------------------------------
# Rule 2. Sheet names that start with a digit or "." must be quoted.
# --------------------------------------------------------------------
elif RE_QUOTE_RULE2.search(sheetname):
requires_quoting = True
# --------------------------------------------------------------------
# Rule 3. Sheet names must not be a valid A1 style cell reference.
# Valid means that the row and column range values must also be within
# Excel row and column limits.
# --------------------------------------------------------------------
elif match_rule3:
cell = match_rule3.group(1)
(row, col) = xl_cell_to_rowcol(cell)
if 0 <= row < row_max and 0 <= col < col_max:
requires_quoting = True
# --------------------------------------------------------------------
# Rule 4. Sheet names must not *start* with a valid RC style cell
# reference. Other characters after the valid RC reference are ignored
# by Excel. Valid means that the row and column range values must also
# be within Excel row and column limits.
#
# Note: references without trailing characters like R12345 or C12345
# are caught by Rule 3. Negative references like R-12345 are caught by
# Rule 1 due to the dash.
# --------------------------------------------------------------------
# Rule 4a. Check for sheet names that start with R1 style references.
elif match_rule4_row:
row = int(match_rule4_row.group(1))
if 0 < row <= row_max:
requires_quoting = True
# Rule 4b. Check for sheet names that start with C1 or RC1 style
elif match_rule4_column:
col = int(match_rule4_column.group(1))
if 0 < col <= col_max:
requires_quoting = True
# Rule 4c. Check for some single R/C references.
elif uppercase_sheetname in ("R", "C", "RC"):
requires_quoting = True
if requires_quoting:
# Double quote any single quotes.
sheetname = sheetname.replace("'", "''")
# Single quote the sheet name.
sheetname = f"'{sheetname}'"
return sheetname
def cell_autofit_width(string: str) -> int:
"""
Calculate the width required to auto-fit a string in a cell.
Args:
string: The string to calculate the cell width for. String.
Returns:
The string autofit width in pixels. Returns 0 if the string is empty.
"""
if not string or len(string) == 0:
return 0
# Excel adds an additional 7 pixels of padding to the cell boundary.
return xl_pixel_width(string) + 7
def xl_pixel_width(string: str) -> int:
"""
Get the pixel width of a string based on individual character widths taken
from Excel. UTF8 characters, and other unhandled characters, are given a
default width of 8.
Args:
string: The string to calculate the width for. String.
Returns:
The string width in pixels. Note, Excel adds an additional 7 pixels of
padding in the cell.
"""
length = 0
for char in string:
length += CHAR_WIDTHS.get(char, 8)
return length
def _get_sparkline_style(style_id: int) -> Dict[str, Dict[str, str]]:
"""
Get the numbered sparkline styles.
"""
styles = [
{ # 0
"low": Color.theme(4, 0),
"high": Color.theme(4, 0),
"last": Color.theme(4, 3),
"first": Color.theme(4, 3),
"series": Color.theme(4, 5),
"markers": Color.theme(4, 5),
"negative": Color.theme(5, 0),
},
{ # 1
"low": Color.theme(4, 0),
"high": Color.theme(4, 0),
"last": Color.theme(4, 3),
"first": Color.theme(4, 3),
"series": Color.theme(4, 5),
"markers": Color.theme(4, 5),
"negative": Color.theme(5, 0),
},
{ # 2
"low": Color.theme(5, 0),
"high": Color.theme(5, 0),
"last": Color.theme(5, 3),
"first": Color.theme(5, 3),
"series": Color.theme(5, 5),
"markers": Color.theme(5, 5),
"negative": Color.theme(6, 0),
},
{ # 3
"low": Color.theme(6, 0),
"high": Color.theme(6, 0),
"last": Color.theme(6, 3),
"first": Color.theme(6, 3),
"series": Color.theme(6, 5),
"markers": Color.theme(6, 5),
"negative": Color.theme(7, 0),
},
{ # 4
"low": Color.theme(7, 0),
"high": Color.theme(7, 0),
"last": Color.theme(7, 3),
"first": Color.theme(7, 3),
"series": Color.theme(7, 5),
"markers": Color.theme(7, 5),
"negative": Color.theme(8, 0),
},
{ # 5
"low": Color.theme(8, 0),
"high": Color.theme(8, 0),
"last": Color.theme(8, 3),
"first": Color.theme(8, 3),
"series": Color.theme(8, 5),
"markers": Color.theme(8, 5),
"negative": Color.theme(9, 0),
},
{ # 6
"low": Color.theme(9, 0),
"high": Color.theme(9, 0),
"last": Color.theme(9, 3),
"first": Color.theme(9, 3),
"series": Color.theme(9, 5),
"markers": Color.theme(9, 5),
"negative": Color.theme(4, 0),
},
{ # 7
"low": Color.theme(5, 4),
"high": Color.theme(5, 4),
"last": Color.theme(5, 4),
"first": Color.theme(5, 4),
"series": Color.theme(4, 4),
"markers": Color.theme(5, 4),
"negative": Color.theme(5, 0),
},
{ # 8
"low": Color.theme(6, 4),
"high": Color.theme(6, 4),
"last": Color.theme(6, 4),
"first": Color.theme(6, 4),
"series": Color.theme(5, 4),
"markers": Color.theme(6, 4),
"negative": Color.theme(6, 0),
},
{ # 9
"low": Color.theme(7, 4),
"high": Color.theme(7, 4),
"last": Color.theme(7, 4),
"first": Color.theme(7, 4),
"series": Color.theme(6, 4),
"markers": Color.theme(7, 4),
"negative": Color.theme(7, 0),
},
{ # 10
"low": Color.theme(8, 4),
"high": Color.theme(8, 4),
"last": Color.theme(8, 4),
"first": Color.theme(8, 4),
"series": Color.theme(7, 4),
"markers": Color.theme(8, 4),
"negative": Color.theme(8, 0),
},
{ # 11
"low": Color.theme(9, 4),
"high": Color.theme(9, 4),
"last": Color.theme(9, 4),
"first": Color.theme(9, 4),
"series": Color.theme(8, 4),
"markers": Color.theme(9, 4),
"negative": Color.theme(9, 0),
},
{ # 12
"low": Color.theme(4, 4),
"high": Color.theme(4, 4),
"last": Color.theme(4, 4),
"first": Color.theme(4, 4),
"series": Color.theme(9, 4),
"markers": Color.theme(4, 4),
"negative": Color.theme(4, 0),
},
{ # 13
"low": Color.theme(4, 4),
"high": Color.theme(4, 4),
"last": Color.theme(4, 4),
"first": Color.theme(4, 4),
"series": Color.theme(4, 0),
"markers": Color.theme(4, 4),
"negative": Color.theme(5, 0),
},
{ # 14
"low": Color.theme(5, 4),
"high": Color.theme(5, 4),
"last": Color.theme(5, 4),
"first": Color.theme(5, 4),
"series": Color.theme(5, 0),
"markers": Color.theme(5, 4),
"negative": Color.theme(6, 0),
},
{ # 15
"low": Color.theme(6, 4),
"high": Color.theme(6, 4),
"last": Color.theme(6, 4),
"first": Color.theme(6, 4),
"series": Color.theme(6, 0),
"markers": Color.theme(6, 4),
"negative": Color.theme(7, 0),
},
{ # 16
"low": Color.theme(7, 4),
"high": Color.theme(7, 4),
"last": Color.theme(7, 4),
"first": Color.theme(7, 4),
"series": Color.theme(7, 0),
"markers": Color.theme(7, 4),
"negative": Color.theme(8, 0),
},
{ # 17
"low": Color.theme(8, 4),
"high": Color.theme(8, 4),
"last": Color.theme(8, 4),
"first": Color.theme(8, 4),
"series": Color.theme(8, 0),
"markers": Color.theme(8, 4),
"negative": Color.theme(9, 0),
},
{ # 18
"low": Color.theme(9, 4),
"high": Color.theme(9, 4),
"last": Color.theme(9, 4),
"first": Color.theme(9, 4),
"series": Color.theme(9, 0),
"markers": Color.theme(9, 4),
"negative": Color.theme(4, 0),
},
{ # 19
"low": Color.theme(4, 5),
"high": Color.theme(4, 5),
"last": Color.theme(4, 4),
"first": Color.theme(4, 4),
"series": Color.theme(4, 3),
"markers": Color.theme(4, 1),
"negative": Color.theme(0, 5),
},
{ # 20
"low": Color.theme(5, 5),
"high": Color.theme(5, 5),
"last": Color.theme(5, 4),
"first": Color.theme(5, 4),
"series": Color.theme(5, 3),
"markers": Color.theme(5, 1),
"negative": Color.theme(0, 5),
},
{ # 21
"low": Color.theme(6, 5),
"high": Color.theme(6, 5),
"last": Color.theme(6, 4),
"first": Color.theme(6, 4),
"series": Color.theme(6, 3),
"markers": Color.theme(6, 1),
"negative": Color.theme(0, 5),
},
{ # 22
"low": Color.theme(7, 5),
"high": Color.theme(7, 5),
"last": Color.theme(7, 4),
"first": Color.theme(7, 4),
"series": Color.theme(7, 3),
"markers": Color.theme(7, 1),
"negative": Color.theme(0, 5),
},
{ # 23
"low": Color.theme(8, 5),
"high": Color.theme(8, 5),
"last": Color.theme(8, 4),
"first": Color.theme(8, 4),
"series": Color.theme(8, 3),
"markers": Color.theme(8, 1),
"negative": Color.theme(0, 5),
},
{ # 24
"low": Color.theme(9, 5),
"high": Color.theme(9, 5),
"last": Color.theme(9, 4),
"first": Color.theme(9, 4),
"series": Color.theme(9, 3),
"markers": Color.theme(9, 1),
"negative": Color.theme(0, 5),
},
{ # 25
"low": Color.theme(1, 3),
"high": Color.theme(1, 3),
"last": Color.theme(1, 3),
"first": Color.theme(1, 3),
"series": Color.theme(1, 1),
"markers": Color.theme(1, 3),
"negative": Color.theme(1, 3),
},
{ # 26
"low": Color.theme(0, 3),
"high": Color.theme(0, 3),
"last": Color.theme(0, 3),
"first": Color.theme(0, 3),
"series": Color.theme(1, 2),
"markers": Color.theme(0, 3),
"negative": Color.theme(0, 3),
},
{ # 27
"low": Color("#D00000"),
"high": Color("#D00000"),
"last": Color("#D00000"),
"first": Color("#D00000"),
"series": Color("#323232"),
"markers": Color("#D00000"),
"negative": Color("#D00000"),
},
{ # 28
"low": Color("#0070C0"),
"high": Color("#0070C0"),
"last": Color("#0070C0"),
"first": Color("#0070C0"),
"series": Color("#000000"),
"markers": Color("#0070C0"),
"negative": Color("#0070C0"),
},
{ # 29
"low": Color("#D00000"),
"high": Color("#D00000"),
"last": Color("#D00000"),
"first": Color("#D00000"),
"series": Color("#376092"),
"markers": Color("#D00000"),
"negative": Color("#D00000"),
},
{ # 30
"low": Color("#000000"),
"high": Color("#000000"),
"last": Color("#000000"),
"first": Color("#000000"),
"series": Color("#0070C0"),
"markers": Color("#000000"),
"negative": Color("#000000"),
},
{ # 31
"low": Color("#FF5055"),
"high": Color("#56BE79"),
"last": Color("#359CEB"),
"first": Color("#5687C2"),
"series": Color("#5F5F5F"),
"markers": Color("#D70077"),
"negative": Color("#FFB620"),
},
{ # 32
"low": Color("#FF5055"),
"high": Color("#56BE79"),
"last": Color("#359CEB"),
"first": Color("#777777"),
"series": Color("#5687C2"),
"markers": Color("#D70077"),
"negative": Color("#FFB620"),
},
{ # 33
"low": Color("#FF5367"),
"high": Color("#60D276"),
"last": Color("#FFEB9C"),
"first": Color("#FFDC47"),
"series": Color("#C6EFCE"),
"markers": Color("#8CADD6"),
"negative": Color("#FFC7CE"),
},
{ # 34
"low": Color("#FF0000"),
"high": Color("#00B050"),
"last": Color("#FFC000"),
"first": Color("#FFC000"),
"series": Color("#00B050"),
"markers": Color("#0070C0"),
"negative": Color("#FF0000"),
},
{ # 35
"low": Color.theme(7, 0),
"high": Color.theme(6, 0),
"last": Color.theme(5, 0),
"first": Color.theme(4, 0),
"series": Color.theme(3, 0),
"markers": Color.theme(8, 0),
"negative": Color.theme(9, 0),
},
{ # 36
"low": Color.theme(7, 0),
"high": Color.theme(6, 0),
"last": Color.theme(5, 0),
"first": Color.theme(4, 0),
"series": Color.theme(1, 0),
"markers": Color.theme(8, 0),
"negative": Color.theme(9, 0),
},
]
return styles[style_id]
def _supported_datetime(
dt: Union[datetime.datetime, datetime.time, datetime.date],
) -> bool:
# Determine is an argument is a supported datetime object.
return isinstance(
dt, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
)
def _remove_datetime_timezone(
dt_obj: datetime.datetime, remove_timezone: bool
) -> datetime.datetime:
# Excel doesn't support timezones in datetimes/times so we remove the
# tzinfo from the object if the user has specified that option in the
# constructor.
if remove_timezone:
dt_obj = dt_obj.replace(tzinfo=None)
else:
if dt_obj.tzinfo:
raise TypeError(
"Excel doesn't support timezones in datetimes. "
"Set the tzinfo in the datetime/time object to None or "
"use the 'remove_timezone' Workbook() option"
)
return dt_obj
def _datetime_to_excel_datetime(
dt_obj: Union[datetime.time, datetime.datetime, datetime.timedelta, datetime.date],
date_1904: bool,
remove_timezone: bool,
) -> float:
# Convert a datetime object to an Excel serial date and time. The integer
# part of the number stores the number of days since the epoch and the
# fractional part stores the percentage of the day.
date_type = dt_obj
is_timedelta = False
if date_1904:
# Excel for Mac date epoch.
epoch = datetime.datetime(1904, 1, 1)
else:
# Default Excel epoch.
epoch = datetime.datetime(1899, 12, 31)
# We handle datetime .datetime, .date and .time objects but convert
# them to datetime.datetime objects and process them in the same way.
if isinstance(dt_obj, datetime.datetime):
dt_obj = _remove_datetime_timezone(dt_obj, remove_timezone)
delta = dt_obj - epoch
elif isinstance(dt_obj, datetime.date):
dt_obj = datetime.datetime.fromordinal(dt_obj.toordinal())
delta = dt_obj - epoch
elif isinstance(dt_obj, datetime.time):
dt_obj = datetime.datetime.combine(epoch, dt_obj)
dt_obj = _remove_datetime_timezone(dt_obj, remove_timezone)
delta = dt_obj - epoch
elif isinstance(dt_obj, datetime.timedelta):
is_timedelta = True
delta = dt_obj
else:
raise TypeError("Unknown or unsupported datetime type")
# Convert a Python datetime.datetime value to an Excel date number.
excel_time = delta.days + (
float(delta.seconds) + float(delta.microseconds) / 1e6
) / (60 * 60 * 24)
# The following is a workaround for the fact that in Excel a time only
# value is represented as 1899-12-31+time whereas in datetime.datetime()
# it is 1900-1-1+time so we need to subtract the 1 day difference.
if (
isinstance(date_type, datetime.datetime)
and not isinstance(dt_obj, datetime.timedelta)
and dt_obj.isocalendar()
== (
1900,
1,
1,
)
):
excel_time -= 1
# Account for Excel erroneously treating 1900 as a leap year.
if not date_1904 and not is_timedelta and excel_time > 59:
excel_time += 1
return excel_time
def _preserve_whitespace(string: str) -> Optional[re.Match]:
# Check if a string has leading or trailing whitespace that requires a
# "preserve" attribute.
return RE_LEADING_WHITESPACE.search(string) or RE_TRAILING_WHITESPACE.search(string)