text: """
      pyexcel_xlsr
      ~~~~~~~~~~~~~~~~~~~
  
      The lower level xls/xlsm file format handler using xlrd
  
      :copyright: (c) 2016-2020 by Onni Software Ltd
      :license: New BSD License
  """
  import datetime
  
  import xlrd
  from pyexcel_io.service import has_no_digits_in_float
  from pyexcel_io.plugin_api import ISheet, IReader
  
  XLS_KEYWORDS = [
      "filename",
      "logfile",
      "verbosity",
      "use_mmap",
      "file_contents",
      "encoding_override",
      "formatting_info",
      "on_demand",
      "ragged_rows",
  ]
  DEFAULT_ERROR_VALUE = "#N/A"
  
  
  class MergedCell(object):
      def __init__(self, row_low, row_high, column_low, column_high):
          self.__rl = row_low
          self.__rh = row_high
          self.__cl = column_low
          self.__ch = column_high
          self.value = None
  
      def register_cells(self, registry):
          for rowx in range(self.__rl, self.__rh):
              for colx in range(self.__cl, self.__ch):
                  key = "%s-%s" % (rowx, colx)
                  registry[key] = self
  
  
  class XLSheet(ISheet):
      """
      xls, xlsx, xlsm sheet reader
  
      Currently only support first sheet in the file
      """
  
      def __init__(self, sheet, auto_detect_int=True, date_mode=0, **keywords):
          self.__auto_detect_int = auto_detect_int
          self.__hidden_cols = []
          self.__hidden_rows = []
          self.__merged_cells = {}
          self._book_date_mode = date_mode
          self.xls_sheet = sheet
          self._keywords = keywords
          if keywords.get("detect_merged_cells") is True:
              for merged_cell_ranges in sheet.merged_cells:
                  merged_cells = MergedCell(*merged_cell_ranges)
                  merged_cells.register_cells(self.__merged_cells)
          if keywords.get("skip_hidden_row_and_column") is True:
              for col_index, info in self.xls_sheet.colinfo_map.items():
                  if info.hidden == 1:
                      self.__hidden_cols.append(col_index)
              for row_index, info in self.xls_sheet.rowinfo_map.items():
                  if info.hidden == 1:
                      self.__hidden_rows.append(row_index)
  
      @property
      def name(self):
          return self.xls_sheet.name
  
      def row_iterator(self):
          number_of_rows = self.xls_sheet.nrows - len(self.__hidden_rows)
          return range(number_of_rows)
  
      def column_iterator(self, row):
          number_of_columns = self.xls_sheet.ncols - len(self.__hidden_cols)
          for column in range(number_of_columns):
              yield self.cell_value(row, column)
  
      def cell_value(self, row, column):
          """
          Random access to the xls cells
          """
          if self._keywords.get("skip_hidden_row_and_column") is True:
              row, column = self._offset_hidden_indices(row, column)
          cell_type = self.xls_sheet.cell_type(row, column)
          value = self.xls_sheet.cell_value(row, column)
  
          if cell_type == xlrd.XL_CELL_DATE:
              value = xldate_to_python_date(value, self._book_date_mode)
          elif cell_type == xlrd.XL_CELL_NUMBER and self.__auto_detect_int:
              if has_no_digits_in_float(value):
                  value = int(value)
          elif cell_type == xlrd.XL_CELL_ERROR:
              value = DEFAULT_ERROR_VALUE
  
          if self.__merged_cells:
              merged_cell = self.__merged_cells.get("%s-%s" % (row, column))
              if merged_cell:
                  if merged_cell.value:
                      value = merged_cell.value
                  else:
                      merged_cell.value = value
          return value
  
      def _offset_hidden_indices(self, row, column):
          row = calculate_offsets(row, self.__hidden_rows)
          column = calculate_offsets(column, self.__hidden_cols)
          return row, column
  
  
  def calculate_offsets(incoming_index, hidden_indices):
      offset = 0
      for index in hidden_indices:
          if index <= (incoming_index + offset):
              offset += 1
      return incoming_index + offset
  
  
  class XLSReader(IReader):
      """
      XLSBook reader
  
      It reads xls, xlsm, xlsx work book
      """
  
      def __init__(self, file_type, **keywords):
          self.__skip_hidden_sheets = keywords.get("skip_hidden_sheets", True)
          self.__skip_hidden_row_column = keywords.get(
              "skip_hidden_row_and_column", True
          )
          self.__detect_merged_cells = keywords.get("detect_merged_cells", False)
          self._keywords = keywords
          xlrd_params = self._extract_xlrd_params()
          if self.__skip_hidden_row_column and file_type == "xls":
              xlrd_params["formatting_info"] = True
          if self.__detect_merged_cells:
              xlrd_params["formatting_info"] = True
  
          self.content_array = []
          self.xls_book = self.get_xls_book(**xlrd_params)
          for sheet in self.xls_book.sheets():
              if self.__skip_hidden_sheets and sheet.visibility != 0:
                  continue
              self.content_array.append(sheet)
  
      def read_sheet(self, index):
          native_sheet = self.content_array[index]
          sheet = XLSheet(
              native_sheet,
              date_mode=self.xls_book.datemode,
              **self._keywords
          )
          return sheet
  
      def close(self):
          if self.xls_book:
              self.xls_book.release_resources()
              self.xls_book = None
  
      def get_xls_book(self, **xlrd_params):
          xls_book = xlrd.open_workbook(**xlrd_params)
          return xls_book
  
      def _extract_xlrd_params(self):
          params = {}
          if self._keywords is not None:
              for key in list(self._keywords.keys()):
                  if key in XLS_KEYWORDS:
                      params[key] = self._keywords.pop(key)
          return params
  
  
  class XLSInFile(XLSReader):
      def __init__(self, file_name, file_type, **keywords):
          super().__init__(file_type, filename=file_name, **keywords)
  
  
  class XLSInContent(XLSReader):
      def __init__(self, file_content, file_type, **keywords):
          super().__init__(file_type, file_contents=file_content, **keywords)
  
  
  class XLSInMemory(XLSReader):
      def __init__(self, file_stream, file_type, **keywords):
          file_stream.seek(0)
          super().__init__(
              file_type, file_contents=file_stream.read(), **keywords
          )
  
  
  def xldate_to_python_date(value, date_mode):
      """
      convert xl date to python date
      """
      date_tuple = xlrd.xldate_as_tuple(value, date_mode)
  
      ret = None
      if date_tuple == (0, 0, 0, 0, 0, 0):
          ret = datetime.datetime(1900, 1, 1, 0, 0, 0)
      elif date_tuple[0:3] == (0, 0, 0):
          ret = datetime.time(date_tuple[3], date_tuple[4], date_tuple[5])
      elif date_tuple[3:6] == (0, 0, 0):
          ret = datetime.date(date_tuple[0], date_tuple[1], date_tuple[2])
      else:
          ret = datetime.datetime(
              date_tuple[0],
              date_tuple[1],
              date_tuple[2],
              date_tuple[3],
              date_tuple[4],
              date_tuple[5],
          )
      return ret
  