'''
### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###
                   308-304 - Object-Oriented Software Design
                                 ASSIGNMENT  4

   SSheetDICT.py ---
      class SpreadsheetData implementation, based on Python dictionaries.
      REVIEWED FOR ASSIGNMENT 4

   last modified: 04/03/02
                       ===============================
                             Copyright (c)  2002
                            Jean-Sébastien BOLDUC
                             (jseb@cs.mcgill.ca)

                        McGill University  (Montréal)
                       ===============================

### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###
'''

# Import CellData and CellCoordinate implementations
from CData  import *
from CCoord import *

# ***********************   Class SpreadsheetData is the
# **** IMPORTANT NOTE ***   CONCRETE SUBJECT, i.e., it should
# ***********************   inherit from a Subject class
class SpreadsheetData:
  '''
  Encapsulates a dynamically sized spreadsheet structure containing CellData
  data and indexed by CellCoordinate coordinates.
  THIS VERSION BASED ON PYTHON DICTIONARIES
  '''

  ### -------------------------------------------------------------  API -- ###

  def __init__(self):
    ''' ->  -- SpreadsheetData constructor.'''
    # Data stored in private variable __dict, a dictionary, where the keys are
    # tuples (row, column). A nonexisting key means that the corresponding cell
    # is empty (contains None).
    self.__dict = {}

  def __setitem__(self, coord, data):
    '''coord:CellCoordinate, data:CellData ->

    Update the content of cell indexed by coord with data.
    A KeyError is raised on bad coordinate, and a TypeError is raised on
    bad value. Example use:
          sd[CellCoordinate(3,4)] = CellData(33)
    '''
    # First make sure that coord and data are the right type:
    if not isinstance(coord, CellCoordinate):
      raise KeyError, 'coordinate must be CellCoordinate object'
    if not isinstance(data, CellData):
      raise TypeError, 'data must be CellData object'

    # Store the data
    # If __dict[(i, j)] already contained a CellData object, we don't need to
    # explicitely delete that object, thanks to garbage collection.
    self.__dict[(coord.getRow(), coord.getColumn())] = data

  def __getitem__(self, coord):
    '''coord:CellCoordinate -> :CellData | None

    Return the content of a cell indexed by coord (return None if the cell is
    empty).
    A KeyError is raised on bad coordinate. Example use:
          sd[CellCoordinate(3,4)]
    '''
    # Make sure coord is the right type:
    if not isinstance(coord, CellCoordinate):
      raise KeyError, 'coordinate must be CellCoordinate object'

    try:
      return self.__dict[(coord.getRow(), coord.getColumn())]
    except KeyError: # The cell is empty
      return None

  def __delitem__(self, coord):
    '''coord:CellCoordinate ->

    Empty the cell indexed by coord'.
    A KeyError is raised on bad coordinate. Example use:
          del sd[CellCoordinate(3,4)]
    '''
    # Make sure coord is the right type:
    if not isinstance(coord, CellCoordinate):
      raise KeyError, 'coordinate must be CellCoordinate object'

    # Delete the CellData object (if any)
    # If __dict[(i, j)] actually contained a CellData object (which would
    # usually be the case), we don't need to explicitely delete the object,
    # thanks to garbage collection.
    try:
      del self.__dict[(coord.getRow(), coord.getColumn())]
    except KeyError: # The cell was already empty
      pass

  def getLU(self):
    ''' -> :CellCoordinate | None

    Return a CellCoordinate containing the Left-most non-empty column,
    and Upper-most non-emtpy row.
    Return None in case of an empty spreadsheet
    '''
    # Use of the built-in min and map functions is not optimal, since on each
    # call to getLU, a list of size n (the number of nonempty cells in the
    # spreadsheet) has to be scanned twice.
    try:
      row    = min( map(lambda x : x[0], self.__dict.keys()) )
      column = min( map(lambda x : x[1], self.__dict.keys()) )
    except ValueError: # happens when __dict is empty (empty spreadsheet)
      return None
    return CellCoordinate(row, column)

  def getRB(self):
    ''' -> :CellCoordinate | None

    Return a CellCoordinate containing the Right-most non-empty column,
    and Bottom-most non-emtpy row.
    Return None in case of an empty spreadsheet.
    '''
    # See note in method getLU.
    try:
      row    = max( map(lambda x : x[0], self.__dict.keys()) )
      column = max( map(lambda x : x[1], self.__dict.keys()) )
    except ValueError: # happens when __dict is empty (empty spreadsheet)
      return None
    return CellCoordinate(row, column)

  def __str__(self):
    ''' -> :String

    Return the string representation of the SpreadSheet.
    This looks like a table of values with spaces for empty cells.
    The row and column indexes are also shown.
    NOT USED IN ASSIGNMENT 4
    '''
    return "Not Implemented"