openpyxl.utils.cell 源代码

# Copyright (c) 2010-2024 openpyxl

"""
Collection of utilities used within the package and also available for client code
"""
from functools import lru_cache
from itertools import chain, product
from string import ascii_uppercase, digits
import re

from .exceptions import CellCoordinatesException

# constants
COORD_RE = re.compile(r'^[$]?([A-Za-z]{1,3})[$]?(\d+)$')
COL_RANGE = """[A-Z]{1,3}:[A-Z]{1,3}:"""
ROW_RANGE = r"""\d+:\d+:"""
RANGE_EXPR = r"""
[$]?(?P<min_col>[A-Za-z]{1,3})?
[$]?(?P<min_row>\d+)?
(:[$]?(?P<max_col>[A-Za-z]{1,3})?
[$]?(?P<max_row>\d+)?)?
"""
ABSOLUTE_RE = re.compile('^' + RANGE_EXPR +'$', re.VERBOSE)
SHEET_TITLE = r"""
(('(?P<quoted>([^']|'')*)')|(?P<notquoted>[^'^ ^!]*))!"""
SHEETRANGE_RE = re.compile("""{0}(?P<cells>{1})(?=,?)""".format(
    SHEET_TITLE, RANGE_EXPR), re.VERBOSE)


def get_column_interval(start, end):
    """
    Given the start and end columns, return all the columns in the series.

    The start and end columns can be either column letters or 1-based
    indexes.
    """
    if isinstance(start, str):
        start = column_index_from_string(start)
    if isinstance(end, str):
        end = column_index_from_string(end)
    return [get_column_letter(x) for x in range(start, end + 1)]


def coordinate_from_string(coord_string):
    """Convert a coordinate string like 'B12' to a tuple ('B', 12)"""
    match = COORD_RE.match(coord_string)
    if not match:
        msg = f"Invalid cell coordinates ({coord_string})"
        raise CellCoordinatesException(msg)
    column, row = match.groups()
    row = int(row)
    if not row:
        msg = f"There is no row 0 ({coord_string})"
        raise CellCoordinatesException(msg)
    return column, row


def absolute_coordinate(coord_string):
    """Convert a coordinate to an absolute coordinate string (B12 -> $B$12)"""
    m = ABSOLUTE_RE.match(coord_string)
    if not m:
        raise ValueError(f"{coord_string} is not a valid coordinate range")

    d = m.groupdict('')
    for k, v in d.items():
        if v:
            d[k] = f"${v}"

    if d['max_col'] or d['max_row']:
        fmt = "{min_col}{min_row}:{max_col}{max_row}"
    else:
        fmt = "{min_col}{min_row}"
    return fmt.format(**d)


__decimal_to_alpha = [""] + list(ascii_uppercase)

[文档]@lru_cache(maxsize=None) def get_column_letter(col_idx): """ Convert decimal column position to its ASCII (base 26) form. Because column indices are 1-based, strides are actually pow(26, n) + 26 Hence, a correction is applied between pow(26, n) and pow(26, 2) + 26 to prevent and additional column letter being prepended "A" == 1 == pow(26, 0) "Z" == 26 == pow(26, 0) + 26 // decimal equivalent 10 "AA" == 27 == pow(26, 1) + 1 "ZZ" == 702 == pow(26, 2) + 26 // decimal equivalent 100 """ if not 1 <= col_idx <= 18278: raise ValueError("Invalid column index {0}".format(col_idx)) result = [] if col_idx < 26: return __decimal_to_alpha[col_idx] while col_idx: col_idx, remainder = divmod(col_idx, 26) result.insert(0, __decimal_to_alpha[remainder]) if not remainder: col_idx -= 1 result.insert(0, "Z") return "".join(result)
__alpha_to_decimal = {letter:pos for pos, letter in enumerate(ascii_uppercase, 1)} __powers = (1, 26, 676)
[文档]@lru_cache(maxsize=None) def column_index_from_string(col): """ Convert ASCII column name (base 26) to decimal with 1-based index Characters represent descending multiples of powers of 26 "AFZ" == 26 * pow(26, 0) + 6 * pow(26, 1) + 1 * pow(26, 2) """ error_msg = f"'{col}' is not a valid column name. Column names are from A to ZZZ" if len(col) > 3: raise ValueError(error_msg) idx = 0 col = reversed(col.upper()) for letter, power in zip(col, __powers): try: pos = __alpha_to_decimal[letter] except KeyError: raise ValueError(error_msg) idx += pos * power if not 0 < idx < 18279: raise ValueError(error_msg) return idx
def range_boundaries(range_string): """ Convert a range string into a tuple of boundaries: (min_col, min_row, max_col, max_row) Cell coordinates will be converted into a range with the cell at both end """ msg = "{0} is not a valid coordinate or range".format(range_string) m = ABSOLUTE_RE.match(range_string) if not m: raise ValueError(msg) min_col, min_row, sep, max_col, max_row = m.groups() if sep: cols = min_col, max_col rows = min_row, max_row if not ( all(cols + rows) or all(cols) and not any(rows) or all(rows) and not any(cols) ): raise ValueError(msg) if min_col is not None: min_col = column_index_from_string(min_col) if min_row is not None: min_row = int(min_row) if max_col is not None: max_col = column_index_from_string(max_col) else: max_col = min_col if max_row is not None: max_row = int(max_row) else: max_row = min_row return min_col, min_row, max_col, max_row def rows_from_range(range_string): """ Get individual addresses for every cell in a range. Yields one row at a time. """ min_col, min_row, max_col, max_row = range_boundaries(range_string) rows = range(min_row, max_row + 1) cols = [get_column_letter(col) for col in range(min_col, max_col + 1)] for row in rows: yield tuple('{0}{1}'.format(col, row) for col in cols) def cols_from_range(range_string): """ Get individual addresses for every cell in a range. Yields one row at a time. """ min_col, min_row, max_col, max_row = range_boundaries(range_string) rows = range(min_row, max_row+1) cols = (get_column_letter(col) for col in range(min_col, max_col+1)) for col in cols: yield tuple('{0}{1}'.format(col, row) for row in rows) def coordinate_to_tuple(coordinate): """ Convert an Excel style coordinate to (row, column) tuple """ for idx, c in enumerate(coordinate): if c in digits: break col = coordinate[:idx] row = coordinate[idx:] return int(row), column_index_from_string(col) def range_to_tuple(range_string): """ Convert a worksheet range to the sheetname and maximum and minimum coordinate indices """ m = SHEETRANGE_RE.match(range_string) if m is None: raise ValueError("Value must be of the form sheetname!A1:E4") sheetname = m.group("quoted") or m.group("notquoted") cells = m.group("cells") boundaries = range_boundaries(cells) return sheetname, boundaries def quote_sheetname(sheetname): """ Add quotes around sheetnames if they contain spaces. """ if "'" in sheetname: sheetname = sheetname.replace("'", "''") sheetname = u"'{0}'".format(sheetname) return sheetname