# file openpyxl/cell.py # Copyright (c) 2010 openpyxl # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # @license: http://www.opensource.org/licenses/mit-license.php # @author: Eric Gazoni """Manage individual cells in a spreadsheet. The Cell class is required to know its value and type, display options, and any other features of an Excel cell. Utilities for referencing cells using Excel's 'A1' column/row nomenclature are also provided. """ __docformat__ = "restructuredtext en" # Python stdlib imports import datetime import re # package imports from .shared.date_time import SharedDate from .shared.exc import CellCoordinatesException, \ ColumnStringIndexException, DataTypeException from .style import NumberFormat # constants COORD_RE = re.compile('^[$]?([A-Z]+)[$]?(\d+)$') ABSOLUTE_RE = re.compile('^[$]?([A-Z]+)[$]?(\d+)(:[$]?([A-Z]+)[$]?(\d+))?$') def coordinate_from_string(coord_string): """Convert a coordinate string like 'B12' to a tuple ('B', 12)""" match = COORD_RE.match(coord_string.upper()) if not match: msg = 'Invalid cell coordinates (%s)' % coord_string raise CellCoordinatesException(msg) column, row = match.groups() return (column, int(row)) def absolute_coordinate(coord_string): """Convert a coordinate to an absolute coordinate string (B12 -> $B$12)""" parts = ABSOLUTE_RE.match(coord_string).groups() if all(parts[-2:]): return '$%s$%s:$%s$%s' % (parts[0], parts[1], parts[3], parts[4]) else: return '$%s$%s' % (parts[0], parts[1]) def column_index_from_string(column, fast = False): """Convert a column letter into a column number (e.g. B -> 2) Excel only supports 1-3 letter column names from A -> ZZZ, so we restrict our column names to 1-3 characters, each in the range A-Z. .. note:: Fast mode is faster but does not check that all letters are capitals between A and Z """ column = column.upper() clen = len(column) if not fast and not all('A' <= char <= 'Z' for char in column): msg = 'Column string must contain only characters A-Z: got %s' % column raise ColumnStringIndexException(msg) if clen == 1: return ord(column[0]) - 64 elif clen == 2: return ((1 + (ord(column[0]) - 65)) * 26) + (ord(column[1]) - 64) elif clen == 3: return ((1 + (ord(column[0]) - 65)) * 676) + ((1 + (ord(column[1]) - 65)) * 26) + (ord(column[2]) - 64) elif clen > 3: raise ColumnStringIndexException('Column string index can not be longer than 3 characters') else: raise ColumnStringIndexException('Column string index can not be empty') def get_column_letter(col_idx): """Convert a column number into a column letter (3 -> 'C') Right shift the column col_idx by 26 to find column letters in reverse order. These numbers are 1-based, and can be converted to ASCII ordinals by adding 64. """ # these indicies corrospond to A -> ZZZ and include all allowed # columns if not 1 <= col_idx <= 18278: msg = 'Column index out of bounds: %s' % col_idx raise ColumnStringIndexException(msg) ordinals = [] temp = col_idx while temp: quotient, remainder = divmod(temp, 26) # check for exact division and borrow if needed if remainder == 0: quotient -= 1 remainder = 26 ordinals.append(remainder + 64) temp = quotient ordinals.reverse() return ''.join([chr(ordinal) for ordinal in ordinals]) class Cell(object): """Describes cell associated properties. Properties of interest include style, type, value, and address. """ __slots__ = ('column', 'row', '_value', '_data_type', 'parent', 'xf_index', '_hyperlink_rel') ERROR_CODES = {'#NULL!': 0, '#DIV/0!': 1, '#VALUE!': 2, '#REF!': 3, '#NAME?': 4, '#NUM!': 5, '#N/A': 6} TYPE_STRING = 's' TYPE_FORMULA = 'f' TYPE_NUMERIC = 'n' TYPE_BOOL = 'b' TYPE_NULL = 's' TYPE_INLINE = 'inlineStr' TYPE_ERROR = 'e' VALID_TYPES = [TYPE_STRING, TYPE_FORMULA, TYPE_NUMERIC, TYPE_BOOL, TYPE_NULL, TYPE_INLINE, TYPE_ERROR] RE_PATTERNS = { 'percentage': re.compile('^\-?[0-9]*\.?[0-9]*\s?\%$'), 'time': re.compile('^(\d|[0-1]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$'), 'numeric': re.compile('^\-?([0-9]+\\.?[0-9]*|[0-9]*\\.?[0-9]+)((E|e)\-?[0-9]+)?$'), } def __init__(self, worksheet, column, row, value = None): self.column = column.upper() self.row = row # _value is the stored value, while value is the displayed value self._value = None self._hyperlink_rel = None self._data_type = self.TYPE_NULL if value: self.value = value self.parent = worksheet self.xf_index = 0 def __repr__(self): return "" % (self.parent.title, self.get_coordinate()) def check_string(self, value): """Check string coding, length, and line break character""" # convert to unicode string value = str(value) # string must never be longer than 32,767 characters # truncate if necessary value = value[:32767] # we require that newline is represented as "\n" in core, # not as "\r\n" or "\r" value = value.replace('\r\n', '\n') return value def check_numeric(self, value): """Cast value to int or float if necessary""" if not isinstance(value, (int, float)): try: value = int(value) except ValueError: value = float(value) return value def set_value_explicit(self, value = None, data_type = TYPE_STRING): """Coerce values according to their explicit type""" type_coercion_map = { self.TYPE_INLINE: self.check_string, self.TYPE_STRING: self.check_string, self.TYPE_FORMULA: str, self.TYPE_NUMERIC: self.check_numeric, self.TYPE_BOOL: bool, } try: self._value = type_coercion_map[data_type](value) except KeyError: if data_type not in self.VALID_TYPES: msg = 'Invalid data type: %s' % data_type raise DataTypeException(msg) self._data_type = data_type def data_type_for_value(self, value): """Given a value, infer the correct data type""" if value is None: data_type = self.TYPE_NULL elif value is True or value is False: data_type = self.TYPE_BOOL elif isinstance(value, (int, float)): data_type = self.TYPE_NUMERIC elif not value: data_type = self.TYPE_STRING elif isinstance(value, (datetime.datetime, datetime.date)): data_type = self.TYPE_NUMERIC elif isinstance(value, str) and value[0] == '=': data_type = self.TYPE_FORMULA elif self.RE_PATTERNS['numeric'].match(value): data_type = self.TYPE_NUMERIC elif value.strip() in self.ERROR_CODES: data_type = self.TYPE_ERROR else: data_type = self.TYPE_STRING return data_type def bind_value(self, value): """Given a value, infer type and display options.""" self._data_type = self.data_type_for_value(value) if value is None: self.set_value_explicit('', self.TYPE_NULL) return True elif self._data_type == self.TYPE_STRING: # percentage detection percentage_search = self.RE_PATTERNS['percentage'].match(value) if percentage_search and value.strip() != '%': value = float(value.replace('%', '')) / 100.0 self.set_value_explicit(value, self.TYPE_NUMERIC) self._set_number_format(NumberFormat.FORMAT_PERCENTAGE) return True # time detection time_search = self.RE_PATTERNS['time'].match(value) if time_search: sep_count = value.count(':') #pylint: disable-msg=E1103 if sep_count == 1: hours, minutes = [int(bit) for bit in value.split(':')] #pylint: disable-msg=E1103 seconds = 0 elif sep_count == 2: hours, minutes, seconds = \ [int(bit) for bit in value.split(':')] #pylint: disable-msg=E1103 days = (hours / 24.0) + (minutes / 1440.0) + \ (seconds / 86400.0) self.set_value_explicit(days, self.TYPE_NUMERIC) self._set_number_format(NumberFormat.FORMAT_DATE_TIME3) return True if self._data_type == self.TYPE_NUMERIC: # date detection # if the value is a date, but not a date time, make it a # datetime, and set the time part to 0 if isinstance(value, datetime.date) and not \ isinstance(value, datetime.datetime): value = datetime.datetime.combine(value, datetime.time()) if isinstance(value, datetime.datetime): value = SharedDate().datetime_to_julian(date = value) self.set_value_explicit(value, self.TYPE_NUMERIC) self._set_number_format(NumberFormat.FORMAT_DATE_YYYYMMDD2) return True self.set_value_explicit(value, self._data_type) def _get_value(self): """Return the value, formatted as a date if needed""" value = self._value if self.is_date(): value = SharedDate().from_julian(value) return value def _set_value(self, value): """Set the value and infer type and display options.""" self.bind_value(value) value = property(_get_value, _set_value, doc = 'Get or set the value held in the cell.\n\n' ':rtype: depends on the value (string, float, int or ' ':class:`datetime.datetime`)') def _set_hyperlink(self, val): """Set value and display for hyperlinks in a cell""" if self._hyperlink_rel is None: self._hyperlink_rel = self.parent.create_relationship("hyperlink") self._hyperlink_rel.target = val self._hyperlink_rel.target_mode = "External" if self._value is None: self.value = val def _get_hyperlink(self): """Return the hyperlink target or an empty string""" return self._hyperlink_rel is not None and \ self._hyperlink_rel.target or '' hyperlink = property(_get_hyperlink, _set_hyperlink, doc = 'Get or set the hyperlink held in the cell. ' 'Automatically sets the `value` of the cell with link text, ' 'but you can modify it afterwards by setting the ' '`value` property, and the hyperlink will remain.\n\n' ':rtype: string') @property def hyperlink_rel_id(self): """Return the id pointed to by the hyperlink, or None""" return self._hyperlink_rel is not None and \ self._hyperlink_rel.id or None def _set_number_format(self, format_code): """Set a new formatting code for numeric values""" self.style.number_format.format_code = format_code @property def has_style(self): """Check if the parent worksheet has a style for this cell""" return self.get_coordinate() in self.parent._styles #pylint: disable-msg=W0212 @property def style(self): """Returns the :class:`.style.Style` object for this cell""" return self.parent.get_style(self.get_coordinate()) @property def data_type(self): """Return the data type represented by this cell""" return self._data_type def get_coordinate(self): """Return the coordinate string for this cell (e.g. 'B12') :rtype: string """ return '%s%s' % (self.column, self.row) @property def address(self): """Return the coordinate string for this cell (e.g. 'B12') :rtype: string """ return self.get_coordinate() def offset(self, row = 0, column = 0): """Returns a cell location relative to this cell. :param row: number of rows to offset :type row: int :param column: number of columns to offset :type column: int :rtype: :class:`.cell.Cell` """ offset_column = get_column_letter(column_index_from_string( column = self.column) + column) offset_row = self.row + row return self.parent.cell('%s%s' % (offset_column, offset_row)) def is_date(self): """Returns whether the value is *probably* a date or not :rtype: bool """ return (self.has_style and self.style.number_format.is_date_format() and isinstance(self._value, (int, float)))