cell.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. # Copyright (c) 2010-2024 openpyxl
  2. """Manage individual cells in a spreadsheet.
  3. The Cell class is required to know its value and type, display options,
  4. and any other features of an Excel cell. Utilities for referencing
  5. cells using Excel's 'A1' column/row nomenclature are also provided.
  6. """
  7. __docformat__ = "restructuredtext en"
  8. # Python stdlib imports
  9. from copy import copy
  10. import datetime
  11. import re
  12. from openpyxl.compat import (
  13. NUMERIC_TYPES,
  14. )
  15. from openpyxl.utils.exceptions import IllegalCharacterError
  16. from openpyxl.utils import get_column_letter
  17. from openpyxl.styles import numbers, is_date_format
  18. from openpyxl.styles.styleable import StyleableObject
  19. from openpyxl.worksheet.hyperlink import Hyperlink
  20. from openpyxl.worksheet.formula import DataTableFormula, ArrayFormula
  21. from openpyxl.cell.rich_text import CellRichText
  22. # constants
  23. TIME_TYPES = (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
  24. TIME_FORMATS = {
  25. datetime.datetime:numbers.FORMAT_DATE_DATETIME,
  26. datetime.date:numbers.FORMAT_DATE_YYYYMMDD2,
  27. datetime.time:numbers.FORMAT_DATE_TIME6,
  28. datetime.timedelta:numbers.FORMAT_DATE_TIMEDELTA,
  29. }
  30. STRING_TYPES = (str, bytes, CellRichText)
  31. KNOWN_TYPES = NUMERIC_TYPES + TIME_TYPES + STRING_TYPES + (bool, type(None))
  32. ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]')
  33. ERROR_CODES = ('#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!',
  34. '#N/A')
  35. TYPE_STRING = 's'
  36. TYPE_FORMULA = 'f'
  37. TYPE_NUMERIC = 'n'
  38. TYPE_BOOL = 'b'
  39. TYPE_NULL = 'n'
  40. TYPE_INLINE = 'inlineStr'
  41. TYPE_ERROR = 'e'
  42. TYPE_FORMULA_CACHE_STRING = 'str'
  43. VALID_TYPES = (TYPE_STRING, TYPE_FORMULA, TYPE_NUMERIC, TYPE_BOOL,
  44. TYPE_NULL, TYPE_INLINE, TYPE_ERROR, TYPE_FORMULA_CACHE_STRING)
  45. _TYPES = {int:'n', float:'n', str:'s', bool:'b'}
  46. def get_type(t, value):
  47. if isinstance(value, NUMERIC_TYPES):
  48. dt = 'n'
  49. elif isinstance(value, STRING_TYPES):
  50. dt = 's'
  51. elif isinstance(value, TIME_TYPES):
  52. dt = 'd'
  53. elif isinstance(value, (DataTableFormula, ArrayFormula)):
  54. dt = 'f'
  55. else:
  56. return
  57. _TYPES[t] = dt
  58. return dt
  59. def get_time_format(t):
  60. value = TIME_FORMATS.get(t)
  61. if value:
  62. return value
  63. for base in t.mro()[1:]:
  64. value = TIME_FORMATS.get(base)
  65. if value:
  66. TIME_FORMATS[t] = value
  67. return value
  68. raise ValueError("Could not get time format for {0!r}".format(value))
  69. class Cell(StyleableObject):
  70. """Describes cell associated properties.
  71. Properties of interest include style, type, value, and address.
  72. """
  73. __slots__ = (
  74. 'row',
  75. 'column',
  76. '_value',
  77. 'data_type',
  78. 'parent',
  79. '_hyperlink',
  80. '_comment',
  81. )
  82. def __init__(self, worksheet, row=None, column=None, value=None, style_array=None):
  83. super().__init__(worksheet, style_array)
  84. self.row = row
  85. """Row number of this cell (1-based)"""
  86. self.column = column
  87. """Column number of this cell (1-based)"""
  88. # _value is the stored value, while value is the displayed value
  89. self._value = None
  90. self._hyperlink = None
  91. self.data_type = 'n'
  92. if value is not None:
  93. self.value = value
  94. self._comment = None
  95. @property
  96. def coordinate(self):
  97. """This cell's coordinate (ex. 'A5')"""
  98. col = get_column_letter(self.column)
  99. return f"{col}{self.row}"
  100. @property
  101. def col_idx(self):
  102. """The numerical index of the column"""
  103. return self.column
  104. @property
  105. def column_letter(self):
  106. return get_column_letter(self.column)
  107. @property
  108. def encoding(self):
  109. return self.parent.encoding
  110. @property
  111. def base_date(self):
  112. return self.parent.parent.epoch
  113. def __repr__(self):
  114. return "<Cell {0!r}.{1}>".format(self.parent.title, self.coordinate)
  115. def check_string(self, value):
  116. """Check string coding, length, and line break character"""
  117. if value is None:
  118. return
  119. # convert to str string
  120. if not isinstance(value, str):
  121. value = str(value, self.encoding)
  122. value = str(value)
  123. # string must never be longer than 32,767 characters
  124. # truncate if necessary
  125. value = value[:32767]
  126. if next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
  127. raise IllegalCharacterError(f"{value} cannot be used in worksheets.")
  128. return value
  129. def check_error(self, value):
  130. """Tries to convert Error" else N/A"""
  131. try:
  132. return str(value)
  133. except UnicodeDecodeError:
  134. return u'#N/A'
  135. def _bind_value(self, value):
  136. """Given a value, infer the correct data type"""
  137. self.data_type = "n"
  138. t = type(value)
  139. try:
  140. dt = _TYPES[t]
  141. except KeyError:
  142. dt = get_type(t, value)
  143. if dt is None and value is not None:
  144. raise ValueError("Cannot convert {0!r} to Excel".format(value))
  145. if dt:
  146. self.data_type = dt
  147. if dt == 'd':
  148. if not is_date_format(self.number_format):
  149. self.number_format = get_time_format(t)
  150. elif dt == "s" and not isinstance(value, CellRichText):
  151. value = self.check_string(value)
  152. if len(value) > 1 and value.startswith("="):
  153. self.data_type = 'f'
  154. elif value in ERROR_CODES:
  155. self.data_type = 'e'
  156. self._value = value
  157. @property
  158. def value(self):
  159. """Get or set the value held in the cell.
  160. :type: depends on the value (string, float, int or
  161. :class:`datetime.datetime`)
  162. """
  163. return self._value
  164. @value.setter
  165. def value(self, value):
  166. """Set the value and infer type and display options."""
  167. self._bind_value(value)
  168. @property
  169. def internal_value(self):
  170. """Always returns the value for excel."""
  171. return self._value
  172. @property
  173. def hyperlink(self):
  174. """Return the hyperlink target or an empty string"""
  175. return self._hyperlink
  176. @hyperlink.setter
  177. def hyperlink(self, val):
  178. """Set value and display for hyperlinks in a cell.
  179. Automatically sets the `value` of the cell with link text,
  180. but you can modify it afterwards by setting the `value`
  181. property, and the hyperlink will remain.
  182. Hyperlink is removed if set to ``None``."""
  183. if val is None:
  184. self._hyperlink = None
  185. else:
  186. if not isinstance(val, Hyperlink):
  187. val = Hyperlink(ref="", target=val)
  188. val.ref = self.coordinate
  189. self._hyperlink = val
  190. if self._value is None:
  191. self.value = val.target or val.location
  192. @property
  193. def is_date(self):
  194. """True if the value is formatted as a date
  195. :type: bool
  196. """
  197. return self.data_type == 'd' or (
  198. self.data_type == 'n' and is_date_format(self.number_format)
  199. )
  200. def offset(self, row=0, column=0):
  201. """Returns a cell location relative to this cell.
  202. :param row: number of rows to offset
  203. :type row: int
  204. :param column: number of columns to offset
  205. :type column: int
  206. :rtype: :class:`openpyxl.cell.Cell`
  207. """
  208. offset_column = self.col_idx + column
  209. offset_row = self.row + row
  210. return self.parent.cell(column=offset_column, row=offset_row)
  211. @property
  212. def comment(self):
  213. """ Returns the comment associated with this cell
  214. :type: :class:`openpyxl.comments.Comment`
  215. """
  216. return self._comment
  217. @comment.setter
  218. def comment(self, value):
  219. """
  220. Assign a comment to a cell
  221. """
  222. if value is not None:
  223. if value.parent:
  224. value = copy(value)
  225. value.bind(self)
  226. elif value is None and self._comment:
  227. self._comment.unbind()
  228. self._comment = value
  229. class MergedCell(StyleableObject):
  230. """
  231. Describes the properties of a cell in a merged cell and helps to
  232. display the borders of the merged cell.
  233. The value of a MergedCell is always None.
  234. """
  235. __slots__ = ('row', 'column')
  236. _value = None
  237. data_type = "n"
  238. comment = None
  239. hyperlink = None
  240. def __init__(self, worksheet, row=None, column=None):
  241. super().__init__(worksheet)
  242. self.row = row
  243. self.column = column
  244. def __repr__(self):
  245. return "<MergedCell {0!r}.{1}>".format(self.parent.title, self.coordinate)
  246. coordinate = Cell.coordinate
  247. _comment = comment
  248. value = _value
  249. def WriteOnlyCell(ws=None, value=None):
  250. return Cell(worksheet=ws, column=1, row=1, value=value)