text: #!/usr/bin/env python
  
  import datetime
  
  import six
  import xlrd
  
  from csvkit import table
  from csvkit.exceptions import XLSDataError
  
  def normalize_empty(values, **kwargs):
      """
      Normalize a column which contains only empty cells.
      """
      return None, [None] * len(values)
  
  def normalize_text(values, **kwargs):
      """
      Normalize a column of text cells.
      """
      return six.text_type, [six.text_type(v) if v else None for v in values]
  
  def normalize_numbers(values, **kwargs):
      """
      Normalize a column of numeric cells.
      """
      # Test if all values are whole numbers, if so coerce floats it ints
      integral = True
  
      for v in values:
          if v and v % 1 != 0:
              integral = False
              break
  
      if integral:
          return int, [int(v) if v != '' else None for v in values]
      else:
          # Convert blanks to None
          return float, [v if v else None for v in values]
  
  def normalize_dates(values, datemode=0, **kwargs):
      """
      Normalize a column of date cells.
      """
      normal_values = []
      normal_types_set = set()
  
      for v in values:
          # Convert blanks to None
          if v == '':
              normal_values.append(None)
              continue
  
          v_tuple = xlrd.xldate_as_tuple(v, datemode)
  
          if v_tuple == (0, 0, 0, 0, 0, 0):
              # Midnight 
              normal_values.append(datetime.time(*v_tuple[3:]))
              normal_types_set.add(datetime.time)
          elif v_tuple[3:] == (0, 0, 0):
              # Date only
              normal_values.append(datetime.date(*v_tuple[:3]))
              normal_types_set.add(datetime.date)
          elif v_tuple[:3] == (0, 0, 0):
              # Time only
              normal_values.append(datetime.time(*v_tuple[3:]))
              normal_types_set.add(datetime.time)
          else:
              # Date and time
              normal_values.append(datetime.datetime(*v_tuple))
              normal_types_set.add(datetime.datetime)
  
      if len(normal_types_set) == 1:
          # No special handling if column contains only one type
          pass 
      elif normal_types_set == set([datetime.datetime, datetime.date]):
          # If a mix of dates and datetimes, up-convert dates to datetimes
          for i, v in enumerate(normal_values):
              if v.__class__ == datetime.date:
                  normal_values[i] = datetime.datetime.combine(v, datetime.time())
  
          normal_types_set.remove(datetime.date)
      elif normal_types_set == set([datetime.datetime, datetime.time]):
          # Datetimes and times don't mix
          raise XLSDataError('Column contains a mix of times and datetimes (this is not supported).')
      elif normal_types_set == set([datetime.date, datetime.time]):
          # Dates and times don't mix
          raise XLSDataError('Column contains a mix of dates and times (this is not supported).')
  
      # Natural serialization of dates and times by csv.writer is insufficent so they get converted back to strings at this point
      return normal_types_set.pop(), normal_values
  
  def normalize_booleans(values, **kwargs):
      """
      Normalize a column of boolean cells.
      """
      return bool, [bool(v) if v != '' else None for v in values] 
  
  NORMALIZERS = {
      xlrd.biffh.XL_CELL_EMPTY: normalize_empty,
      xlrd.biffh.XL_CELL_TEXT: normalize_text,
      xlrd.biffh.XL_CELL_NUMBER: normalize_numbers,
      xlrd.biffh.XL_CELL_DATE: normalize_dates,
      xlrd.biffh.XL_CELL_BOOLEAN: normalize_booleans
  }
  
  def determine_column_type(types):
      """
      Determine the correct type for a column from a list of cell types.
      """
      types_set = set(types)
      types_set.discard(xlrd.biffh.XL_CELL_EMPTY)
  
      # Normalize mixed types to text
      if len(types_set) > 1:
          return xlrd.biffh.XL_CELL_TEXT
  
      try:
          return types_set.pop()
      except KeyError:
          return xlrd.biffh.XL_CELL_EMPTY
  
  def xls2csv(f, **kwargs):
      """
      Convert an Excel .xls file to csv.
      """
      book = xlrd.open_workbook(file_contents=f.read())
  
      if 'sheet' in kwargs:
          sheet = book.sheet_by_name(kwargs['sheet'])
      else:
          sheet = book.sheet_by_index(0)
  
      tab = table.Table() 
  
      for i in range(sheet.ncols):
          # Trim headers
          column_name = sheet.col_values(i)[0]
  
          values = sheet.col_values(i)[1:]
          types = sheet.col_types(i)[1:]
  
          column_type = determine_column_type(types)
          t, normal_values = NORMALIZERS[column_type](values, datemode=book.datemode)
  
          column = table.Column(i, column_name, normal_values, normal_type=t)
          tab.append(column)
  
      o = six.StringIO()
      output = tab.to_csv(o)
      output = o.getvalue()
      o.close()
  
      return output 
  
  