############################################################################### # # Worksheet - A class for writing Excel Worksheets. # # Copyright 2013-2019, John McNamara, jmcnamara@cpan.org # import re import datetime from warnings import warn COL_NAMES = {} range_parts = re.compile(r'(\$?)([A-Z]{1,3})(\$?)(\d+)') def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False): """ 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("Row number %d must be >= 0" % row) return None if col < 0: warn("Col number %d must be >= 0" % col) return None row += 1 # Change to 1-index. row_abs = '$' if row_abs else '' col_str = xl_col_to_name(col, col_abs) return col_str + row_abs + str(row) def xl_rowcol_to_cell_fast(row, col): """ 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, col_abs=False): """ 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("Col number %d must be >= 0" % col_num) return None col_num += 1 # Change to 1-index. col_str = '' col_abs = '$' 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 + col_str def xl_cell_to_rowcol(cell_str): """ 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 = range_parts.match(cell_str) 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): """ 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 = range_parts.match(cell_str) col_abs = match.group(1) col_str = match.group(2) row_abs = match.group(3) row_str = match.group(4) if col_abs: col_abs = True else: col_abs = False if row_abs: row_abs = True else: row_abs = False # 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, first_col, last_row, last_col): """ 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 is None or range2 is None: warn("Row and column numbers must be >= 0") return None return range1 + ':' + range2 def xl_range_abs(first_row, first_col, last_row, last_col): """ 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 is None or range2 is None: warn("Row and column numbers must be >= 0") return None return range1 + ':' + range2 def xl_range_formula(sheetname, first_row, first_col, last_row, last_col): """ 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): """ Convert a worksheet name to a quoted name if it contains spaces or special characters. Args: sheetname: The worksheet name. String. Returns: A quoted worksheet string. """ # TODO. Possibly extend this to quote sheetnames that look like ranges. if not sheetname.isalnum() and not sheetname.startswith("'"): # Double quote any single quotes. sheetname = sheetname.replace("'", "''") # Singe quote the sheet name. sheetname = "'%s'" % sheetname return sheetname def xl_color(color): # Used in conjunction with the XlsxWriter *color() methods to convert # a color name into an RGB formatted string. These colors are for # backward compatibility with older versions of Excel. named_colors = { 'black': '#000000', 'blue': '#0000FF', 'brown': '#800000', 'cyan': '#00FFFF', 'gray': '#808080', 'green': '#008000', 'lime': '#00FF00', 'magenta': '#FF00FF', 'navy': '#000080', 'orange': '#FF6600', 'pink': '#FF00FF', 'purple': '#800080', 'red': '#FF0000', 'silver': '#C0C0C0', 'white': '#FFFFFF', 'yellow': '#FFFF00', } if color in named_colors: color = named_colors[color] if not re.match('#[0-9a-fA-F]{6}', color): warn("Color '%s' isn't a valid Excel color" % color) # Convert the RGB color to the Excel ARGB format. return "FF" + color.lstrip('#').upper() def get_rgb_color(color): # Convert the user specified color to an RGB color. rgb_color = xl_color(color) # Remove leading FF from RGB color for charts. rgb_color = re.sub(r'^FF', '', rgb_color) return rgb_color def get_sparkline_style(style_id): styles = [ {'series': {'theme': "4", 'tint': "-0.499984740745262"}, 'negative': {'theme': "5"}, 'markers': {'theme': "4", 'tint': "-0.499984740745262"}, 'first': {'theme': "4", 'tint': "0.39997558519241921"}, 'last': {'theme': "4", 'tint': "0.39997558519241921"}, 'high': {'theme': "4"}, 'low': {'theme': "4"}, }, # 0 {'series': {'theme': "4", 'tint': "-0.499984740745262"}, 'negative': {'theme': "5"}, 'markers': {'theme': "4", 'tint': "-0.499984740745262"}, 'first': {'theme': "4", 'tint': "0.39997558519241921"}, 'last': {'theme': "4", 'tint': "0.39997558519241921"}, 'high': {'theme': "4"}, 'low': {'theme': "4"}, }, # 1 {'series': {'theme': "5", 'tint': "-0.499984740745262"}, 'negative': {'theme': "6"}, 'markers': {'theme': "5", 'tint': "-0.499984740745262"}, 'first': {'theme': "5", 'tint': "0.39997558519241921"}, 'last': {'theme': "5", 'tint': "0.39997558519241921"}, 'high': {'theme': "5"}, 'low': {'theme': "5"}, }, # 2 {'series': {'theme': "6", 'tint': "-0.499984740745262"}, 'negative': {'theme': "7"}, 'markers': {'theme': "6", 'tint': "-0.499984740745262"}, 'first': {'theme': "6", 'tint': "0.39997558519241921"}, 'last': {'theme': "6", 'tint': "0.39997558519241921"}, 'high': {'theme': "6"}, 'low': {'theme': "6"}, }, # 3 {'series': {'theme': "7", 'tint': "-0.499984740745262"}, 'negative': {'theme': "8"}, 'markers': {'theme': "7", 'tint': "-0.499984740745262"}, 'first': {'theme': "7", 'tint': "0.39997558519241921"}, 'last': {'theme': "7", 'tint': "0.39997558519241921"}, 'high': {'theme': "7"}, 'low': {'theme': "7"}, }, # 4 {'series': {'theme': "8", 'tint': "-0.499984740745262"}, 'negative': {'theme': "9"}, 'markers': {'theme': "8", 'tint': "-0.499984740745262"}, 'first': {'theme': "8", 'tint': "0.39997558519241921"}, 'last': {'theme': "8", 'tint': "0.39997558519241921"}, 'high': {'theme': "8"}, 'low': {'theme': "8"}, }, # 5 {'series': {'theme': "9", 'tint': "-0.499984740745262"}, 'negative': {'theme': "4"}, 'markers': {'theme': "9", 'tint': "-0.499984740745262"}, 'first': {'theme': "9", 'tint': "0.39997558519241921"}, 'last': {'theme': "9", 'tint': "0.39997558519241921"}, 'high': {'theme': "9"}, 'low': {'theme': "9"}, }, # 6 {'series': {'theme': "4", 'tint': "-0.249977111117893"}, 'negative': {'theme': "5"}, 'markers': {'theme': "5", 'tint': "-0.249977111117893"}, 'first': {'theme': "5", 'tint': "-0.249977111117893"}, 'last': {'theme': "5", 'tint': "-0.249977111117893"}, 'high': {'theme': "5", 'tint': "-0.249977111117893"}, 'low': {'theme': "5", 'tint': "-0.249977111117893"}, }, # 7 {'series': {'theme': "5", 'tint': "-0.249977111117893"}, 'negative': {'theme': "6"}, 'markers': {'theme': "6", 'tint': "-0.249977111117893"}, 'first': {'theme': "6", 'tint': "-0.249977111117893"}, 'last': {'theme': "6", 'tint': "-0.249977111117893"}, 'high': {'theme': "6", 'tint': "-0.249977111117893"}, 'low': {'theme': "6", 'tint': "-0.249977111117893"}, }, # 8 {'series': {'theme': "6", 'tint': "-0.249977111117893"}, 'negative': {'theme': "7"}, 'markers': {'theme': "7", 'tint': "-0.249977111117893"}, 'first': {'theme': "7", 'tint': "-0.249977111117893"}, 'last': {'theme': "7", 'tint': "-0.249977111117893"}, 'high': {'theme': "7", 'tint': "-0.249977111117893"}, 'low': {'theme': "7", 'tint': "-0.249977111117893"}, }, # 9 {'series': {'theme': "7", 'tint': "-0.249977111117893"}, 'negative': {'theme': "8"}, 'markers': {'theme': "8", 'tint': "-0.249977111117893"}, 'first': {'theme': "8", 'tint': "-0.249977111117893"}, 'last': {'theme': "8", 'tint': "-0.249977111117893"}, 'high': {'theme': "8", 'tint': "-0.249977111117893"}, 'low': {'theme': "8", 'tint': "-0.249977111117893"}, }, # 10 {'series': {'theme': "8", 'tint': "-0.249977111117893"}, 'negative': {'theme': "9"}, 'markers': {'theme': "9", 'tint': "-0.249977111117893"}, 'first': {'theme': "9", 'tint': "-0.249977111117893"}, 'last': {'theme': "9", 'tint': "-0.249977111117893"}, 'high': {'theme': "9", 'tint': "-0.249977111117893"}, 'low': {'theme': "9", 'tint': "-0.249977111117893"}, }, # 11 {'series': {'theme': "9", 'tint': "-0.249977111117893"}, 'negative': {'theme': "4"}, 'markers': {'theme': "4", 'tint': "-0.249977111117893"}, 'first': {'theme': "4", 'tint': "-0.249977111117893"}, 'last': {'theme': "4", 'tint': "-0.249977111117893"}, 'high': {'theme': "4", 'tint': "-0.249977111117893"}, 'low': {'theme': "4", 'tint': "-0.249977111117893"}, }, # 12 {'series': {'theme': "4"}, 'negative': {'theme': "5"}, 'markers': {'theme': "4", 'tint': "-0.249977111117893"}, 'first': {'theme': "4", 'tint': "-0.249977111117893"}, 'last': {'theme': "4", 'tint': "-0.249977111117893"}, 'high': {'theme': "4", 'tint': "-0.249977111117893"}, 'low': {'theme': "4", 'tint': "-0.249977111117893"}, }, # 13 {'series': {'theme': "5"}, 'negative': {'theme': "6"}, 'markers': {'theme': "5", 'tint': "-0.249977111117893"}, 'first': {'theme': "5", 'tint': "-0.249977111117893"}, 'last': {'theme': "5", 'tint': "-0.249977111117893"}, 'high': {'theme': "5", 'tint': "-0.249977111117893"}, 'low': {'theme': "5", 'tint': "-0.249977111117893"}, }, # 14 {'series': {'theme': "6"}, 'negative': {'theme': "7"}, 'markers': {'theme': "6", 'tint': "-0.249977111117893"}, 'first': {'theme': "6", 'tint': "-0.249977111117893"}, 'last': {'theme': "6", 'tint': "-0.249977111117893"}, 'high': {'theme': "6", 'tint': "-0.249977111117893"}, 'low': {'theme': "6", 'tint': "-0.249977111117893"}, }, # 15 {'series': {'theme': "7"}, 'negative': {'theme': "8"}, 'markers': {'theme': "7", 'tint': "-0.249977111117893"}, 'first': {'theme': "7", 'tint': "-0.249977111117893"}, 'last': {'theme': "7", 'tint': "-0.249977111117893"}, 'high': {'theme': "7", 'tint': "-0.249977111117893"}, 'low': {'theme': "7", 'tint': "-0.249977111117893"}, }, # 16 {'series': {'theme': "8"}, 'negative': {'theme': "9"}, 'markers': {'theme': "8", 'tint': "-0.249977111117893"}, 'first': {'theme': "8", 'tint': "-0.249977111117893"}, 'last': {'theme': "8", 'tint': "-0.249977111117893"}, 'high': {'theme': "8", 'tint': "-0.249977111117893"}, 'low': {'theme': "8", 'tint': "-0.249977111117893"}, }, # 17 {'series': {'theme': "9"}, 'negative': {'theme': "4"}, 'markers': {'theme': "9", 'tint': "-0.249977111117893"}, 'first': {'theme': "9", 'tint': "-0.249977111117893"}, 'last': {'theme': "9", 'tint': "-0.249977111117893"}, 'high': {'theme': "9", 'tint': "-0.249977111117893"}, 'low': {'theme': "9", 'tint': "-0.249977111117893"}, }, # 18 {'series': {'theme': "4", 'tint': "0.39997558519241921"}, 'negative': {'theme': "0", 'tint': "-0.499984740745262"}, 'markers': {'theme': "4", 'tint': "0.79998168889431442"}, 'first': {'theme': "4", 'tint': "-0.249977111117893"}, 'last': {'theme': "4", 'tint': "-0.249977111117893"}, 'high': {'theme': "4", 'tint': "-0.499984740745262"}, 'low': {'theme': "4", 'tint': "-0.499984740745262"}, }, # 19 {'series': {'theme': "5", 'tint': "0.39997558519241921"}, 'negative': {'theme': "0", 'tint': "-0.499984740745262"}, 'markers': {'theme': "5", 'tint': "0.79998168889431442"}, 'first': {'theme': "5", 'tint': "-0.249977111117893"}, 'last': {'theme': "5", 'tint': "-0.249977111117893"}, 'high': {'theme': "5", 'tint': "-0.499984740745262"}, 'low': {'theme': "5", 'tint': "-0.499984740745262"}, }, # 20 {'series': {'theme': "6", 'tint': "0.39997558519241921"}, 'negative': {'theme': "0", 'tint': "-0.499984740745262"}, 'markers': {'theme': "6", 'tint': "0.79998168889431442"}, 'first': {'theme': "6", 'tint': "-0.249977111117893"}, 'last': {'theme': "6", 'tint': "-0.249977111117893"}, 'high': {'theme': "6", 'tint': "-0.499984740745262"}, 'low': {'theme': "6", 'tint': "-0.499984740745262"}, }, # 21 {'series': {'theme': "7", 'tint': "0.39997558519241921"}, 'negative': {'theme': "0", 'tint': "-0.499984740745262"}, 'markers': {'theme': "7", 'tint': "0.79998168889431442"}, 'first': {'theme': "7", 'tint': "-0.249977111117893"}, 'last': {'theme': "7", 'tint': "-0.249977111117893"}, 'high': {'theme': "7", 'tint': "-0.499984740745262"}, 'low': {'theme': "7", 'tint': "-0.499984740745262"}, }, # 22 {'series': {'theme': "8", 'tint': "0.39997558519241921"}, 'negative': {'theme': "0", 'tint': "-0.499984740745262"}, 'markers': {'theme': "8", 'tint': "0.79998168889431442"}, 'first': {'theme': "8", 'tint': "-0.249977111117893"}, 'last': {'theme': "8", 'tint': "-0.249977111117893"}, 'high': {'theme': "8", 'tint': "-0.499984740745262"}, 'low': {'theme': "8", 'tint': "-0.499984740745262"}, }, # 23 {'series': {'theme': "9", 'tint': "0.39997558519241921"}, 'negative': {'theme': "0", 'tint': "-0.499984740745262"}, 'markers': {'theme': "9", 'tint': "0.79998168889431442"}, 'first': {'theme': "9", 'tint': "-0.249977111117893"}, 'last': {'theme': "9", 'tint': "-0.249977111117893"}, 'high': {'theme': "9", 'tint': "-0.499984740745262"}, 'low': {'theme': "9", 'tint': "-0.499984740745262"}, }, # 24 {'series': {'theme': "1", 'tint': "0.499984740745262"}, 'negative': {'theme': "1", 'tint': "0.249977111117893"}, 'markers': {'theme': "1", 'tint': "0.249977111117893"}, 'first': {'theme': "1", 'tint': "0.249977111117893"}, 'last': {'theme': "1", 'tint': "0.249977111117893"}, 'high': {'theme': "1", 'tint': "0.249977111117893"}, 'low': {'theme': "1", 'tint': "0.249977111117893"}, }, # 25 {'series': {'theme': "1", 'tint': "0.34998626667073579"}, 'negative': {'theme': "0", 'tint': "-0.249977111117893"}, 'markers': {'theme': "0", 'tint': "-0.249977111117893"}, 'first': {'theme': "0", 'tint': "-0.249977111117893"}, 'last': {'theme': "0", 'tint': "-0.249977111117893"}, 'high': {'theme': "0", 'tint': "-0.249977111117893"}, 'low': {'theme': "0", 'tint': "-0.249977111117893"}, }, # 26 {'series': {'rgb': "FF323232"}, 'negative': {'rgb': "FFD00000"}, 'markers': {'rgb': "FFD00000"}, 'first': {'rgb': "FFD00000"}, 'last': {'rgb': "FFD00000"}, 'high': {'rgb': "FFD00000"}, 'low': {'rgb': "FFD00000"}, }, # 27 {'series': {'rgb': "FF000000"}, 'negative': {'rgb': "FF0070C0"}, 'markers': {'rgb': "FF0070C0"}, 'first': {'rgb': "FF0070C0"}, 'last': {'rgb': "FF0070C0"}, 'high': {'rgb': "FF0070C0"}, 'low': {'rgb': "FF0070C0"}, }, # 28 {'series': {'rgb': "FF376092"}, 'negative': {'rgb': "FFD00000"}, 'markers': {'rgb': "FFD00000"}, 'first': {'rgb': "FFD00000"}, 'last': {'rgb': "FFD00000"}, 'high': {'rgb': "FFD00000"}, 'low': {'rgb': "FFD00000"}, }, # 29 {'series': {'rgb': "FF0070C0"}, 'negative': {'rgb': "FF000000"}, 'markers': {'rgb': "FF000000"}, 'first': {'rgb': "FF000000"}, 'last': {'rgb': "FF000000"}, 'high': {'rgb': "FF000000"}, 'low': {'rgb': "FF000000"}, }, # 30 {'series': {'rgb': "FF5F5F5F"}, 'negative': {'rgb': "FFFFB620"}, 'markers': {'rgb': "FFD70077"}, 'first': {'rgb': "FF5687C2"}, 'last': {'rgb': "FF359CEB"}, 'high': {'rgb': "FF56BE79"}, 'low': {'rgb': "FFFF5055"}, }, # 31 {'series': {'rgb': "FF5687C2"}, 'negative': {'rgb': "FFFFB620"}, 'markers': {'rgb': "FFD70077"}, 'first': {'rgb': "FF777777"}, 'last': {'rgb': "FF359CEB"}, 'high': {'rgb': "FF56BE79"}, 'low': {'rgb': "FFFF5055"}, }, # 32 {'series': {'rgb': "FFC6EFCE"}, 'negative': {'rgb': "FFFFC7CE"}, 'markers': {'rgb': "FF8CADD6"}, 'first': {'rgb': "FFFFDC47"}, 'last': {'rgb': "FFFFEB9C"}, 'high': {'rgb': "FF60D276"}, 'low': {'rgb': "FFFF5367"}, }, # 33 {'series': {'rgb': "FF00B050"}, 'negative': {'rgb': "FFFF0000"}, 'markers': {'rgb': "FF0070C0"}, 'first': {'rgb': "FFFFC000"}, 'last': {'rgb': "FFFFC000"}, 'high': {'rgb': "FF00B050"}, 'low': {'rgb': "FFFF0000"}, }, # 34 {'series': {'theme': "3"}, 'negative': {'theme': "9"}, 'markers': {'theme': "8"}, 'first': {'theme': "4"}, 'last': {'theme': "5"}, 'high': {'theme': "6"}, 'low': {'theme': "7"}, }, # 35 {'series': {'theme': "1"}, 'negative': {'theme': "9"}, 'markers': {'theme': "8"}, 'first': {'theme': "4"}, 'last': {'theme': "5"}, 'high': {'theme': "6"}, 'low': {'theme': "7"}, }, # 36 ] return styles[style_id] def supported_datetime(dt): # 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, remove_timezone): # 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, date_1904, remove_timezone): # 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 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): 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 dt_obj.isocalendar() == (1900, 1, 1)): excel_time -= 1 # Account for Excel erroneously treating 1900 as a leap year. if not date_1904 and excel_time > 59: excel_time += 1 return excel_time