Scala - scala examples : Scala spreasheet

In this post, we will check an example that create a SCells Spreadsheet.  java

So we first check on the visual framework.  react

The Visual framework

extends the ScrollPane, which gives it a Scroll-bars at the bottom and right. It contains two sub-component named Table and rowHeader.
Table display the data, while the rowHeader component contains the row-number headers at the left of the spreadsheet. 
the GUI code that forms the skeleton is as follow.  app

// the code as below 
package org.stairwaybook.scelles

import swing._

class Spreadsheet(val height : Int, val width : Int) 
	extends ScrollPane { 
  val table = new Table(height, width) {
    rowHeight = 25
    autoResizeMode = Table.AutoResizeMode.Off
    showGrid = true
    gridColor = new java.awt.Color(150, 150, 150)
  }
  
  val rowHeader = 
    new ListView((0 until height) map (_.toString)) {
      fixedCellWidth = 30
      fixedCellHeight = table.rowHeight
  }
  
  viewportView = table
  rowHeaderView = rowHeader
}
To try this rudimentary spreadsheet out, here is the main class. 

// the code as below 
package org.stairwaybook.scelles

import swing._

object Main extends SimpleGUIApplication { 
  def top = new MainFrame { 
    title  = "ScalaSheet"
    contents = new Spreadsheet(100, 20)
  }
}

Disconnectng data entry and display 

A good UI code, may have demands that the code separation between UI and model. This is the same in Scala as well in elsewhere.  ide

First, we need to refactor the UI code, we want to show the contents with meaning value when it is beeing edited or when it is just display the result. so we will use the renderedComponent method, it is an override method, and it works as follow.  post

// the code as below 
package org.stairwaybook.scelles

import swing._

class Spreadsheet(val height : Int, val width : Int) 
	extends ScrollPane { 
  // the data
  val cellMode = new Model(height, width)
  import cellMode._
  
  
  val table = new Table(height, width) {
    rowHeight = 25
    autoResizeMode = Table.AutoResizeMode.Off
    showGrid = true
    gridColor = new java.awt.Color(150, 150, 150)
    
    override def rendererComponent(isSelected : Boolean, hasFocus : Boolean, row : Int, column : Int) : Component = {
	    if (hasFocus) new TextField(userData(row, column))
	    else 
	      new Label(cells(row)(column).toString) {
	      xAlignment = Alignment.Right
	    }
	}
	  
	  def userData(row : Int, column : Int) : String = { 
	    val v = this(row, column)
	    if (v == null) "" else v.toString
	}
  
  }
  
  val rowHeader = 
    new ListView((0 until height) map (_.toString)) {
      fixedCellWidth = 30
      fixedCellHeight = table.rowHeight
  }
  
  viewportView = table
  rowHeaderView = rowHeader
}
as we see that we uses userData, and that uses cells from the Model class. here is the code of the Model class. 

package org.stairwaybook.scelles

import swing._

class Model(val height : Int, val width: Int) {
  case class Cell(row :Int, column : Int)
  
  val cells = new Array[Array[Cell]](height, width)
  
  for (i <- 0 until height; j <- 0 until width) 
    cells(i)(j) = new Cell(i, j)
}
now we have basic Spreadsheet, but it does not allow us to do useful things such as computation, it just display string as you hasve inputed it. 

Formulas

In reality , a spread sheet cell holds two things : An actual value and a formula to compute this value. there are three types of formulas in a spreadsheets

1. Numeric  value such as 1.22, -3, or 0 
2. Textual value such as Annual sales, Deprecation, or total
3. Formula that compute a new value from the content of cells, such as "=add(A1, B2)" or "=sum((mul(2, A2), C1: D16)"

Coord       for cell coordinates such as A3
Range       for cell ranges such as A3:B17
Number      for floating-point numbers such as 3.1415
Textual     for textual labels such as Deprecation
Application for function applications such as sum(A1, A2).
this


so we have defined the following Formula classes.  lua

package org.stairwaybook.scelles

trait Formula

case class Coord(row :Int, column : Int) extends Formula {
  override def toString = ('A' + column).toChar .toString + row
}

case class Range(c1 : Coord, c2: Coord) extends Formula { 
  override def toString = c1.toString + ":" + c2.toString
}

case class Number(value : Double) extends Formula { 
  override def toString = value.toString
}

case class Textual(value : String) extends Formula { 
  override def toString = value
}

// stands for a formula which should have a function name and a list of arguments. 
case class Application(function: String, arguments : List[Formula]) extends Formula {
  override def toString = function + arguments.mkString("(", ",", ")")
}

object Empty extends Textual("")

Parsing formulas 

we need to have a way to parse the formula into a formula tree.  spa

import scala.util.parsing.combinator._

object FormulaParsers extends RegexParsers { 
  def ident : Parser[String] = """[a-zA-Z_]\w*""".r
  def decimal : Parser[String] = """-?\d+(\.\d*)?""".r
  // next, we will need to capture Cell
  def cell : Parser[Coord] = 
    """[a-zA-Z_]\d+""".r ^^ { s =>  
    val column = s.charAt(0).toUpper - 'A'
    val row = s.substring(1).toInt
    Coord(row, column)
  }
  
  def range: Parser[Range] = 
    cell ~":"~cell ^^ { 
    case c1~":"~c2 => Range(c1, c2)
  }
  
  def number : Parser[Number] = 
    decimal ^^ (d => Number(d.toDouble))
    
  def application : Parser[Application] = 
    ident ~ "(" ~ repsep(expr, ",") ~ ")" ^^ { 
      case f ~ "(" ~ ps ~ ")" => Application(f, ps)
    }
 
  def expr : Parser[Formula] = 
    range | cell | number | application 
    
  def textual : Parser[Textual] = 
    """[^=].*""".r ^^ Textual // this is very neat... , you can directly apply the Textual (as Function1) on the ParserReult[T] on the Textual case class
  
  def formula : Parser[Formula] = 
    number | textual | "=" ~> expr
    
  def parse(input  : String ) : Formula = 
    parseAll(formula, input) match { 
      case Success (e, _) => e
      case f : NoSuccess => Textual("[" + f.msg + "]")
    }
} // end FormulaParsers
this is using the Combinator framework . 

To support the formula on Cells, here is the new version of Model , which has the formula members.  scala

import swing._

class Model(val height : Int, val width: Int) {
  case class Cell(row :Int, column : Int) {
    var formula : Formula = Empty 
    override def toString = formula.toString
  }
  
  val cells = new Array[Array[Cell]](height, width)
  
  for (i <- 0 until height; j <- 0 until width) 
    cells(i)(j) = new Cell(i, j)
}
now with the Formula, we still display the string just as the formula's string representation. 

Evaluation 

we create a new Evaluation trait, which has the evaluate method.  code

trait Evaluator { this : Model => 
  def evaluate(e: Formula) : Double = try { 
    e match { 
      case Coord(row, column) => 
        cells(row)(column).value 
      case Number(v) => 
        v
      case Textual(_) => 
        0
      case Application(function, arguments) => 
        val argvals = arguments flatMap evalList 
        operations(function)(argvals)
    }
  } catch {
    case ex : Exception => Double.NaN
  }

  type Op = List[Double] => Double
  val operations = new collection.mutable.HashMap[String, Op]
  
  private def evalList(e: Formula) : List[Double] = e match { 
    case Range(_, _) => references(e) map (_.value)
    case _ => List(evaluate(e)) // the recursion goes here
  }
  
  def references(e : Formula) : List[Cell] = e match { 
    case Coord (row, column) => 
      List(cells(row)(column))
    case Range(Coord(r1, c1), Coord(r2, c2)) => 
      for (row <- (r1 to r2).toList; column <- c1 to c2)
        yield cells(row)(column)
    case Application(function, arguments) =>
      arguments flatMap references
    case _ => 
      List()
  }
} // end Evaluator
the key is the Application ,which calles operations, which is a map from function name to the function's application. 

type Op = List[Double] => Double
  val operations = new collection.mutable.HashMap[String, Op]
and to support evaluation on nested arguments, we have evalList. 

private def evalList(e: Formula) : List[Double] = e match { 
    case Range(_, _) => references(e) map (_.value)
    case _ => List(evaluate(e)) // the recursion goes here
  }
and in order to support argument such as [C1:C5] or C1 and others types, we define a method references , which can turn a formula classes into a cell references. as follow. 

def references(e : Formula) : List[Cell] = e match { 
    case Coord (row, column) => 
      List(cells(row)(column))
    case Range(Coord(r1, c1), Coord(r2, c2)) => 
      for (row <- (r1 to r2).toList; column <- c1 to c2)
        yield cells(row)(column)
    case Application(function, arguments) =>
      arguments flatMap references
    case _ => 
      List()
  }

equally, when we 

Operations libraries

To actually convert the formula into the operations, so we can really convert to real calculation. 

// the trait populate the operations table during its initialization. 
trait Arithmetic { this : Evaluator => 
    operations += (
      "add" -> { case List(x, y) => x + y },
      "sub" -> { case List(x, y) => x - y },
      "div" -> { case List(x, y) => x / y },
      "mul" -> { case List(x, y) => x * y },
      "mod" -> { case List(x, y) => x % y },
      "sum" -> { xs => (0.0 /: xs) (_ + _) },
      "prod" -> { xs => (1.0 /: xs) (_ * _) }
    )
}
this trait does not export any exported members, we need to revise the Model class, here ist he model clases that introduced the Formula. 

To support evaluation, Model now mix in Evaluator trait to support evaluate 

import swing._

class Model(val height : Int, val width: Int) 
      extends Evaluator with Arithmetic {
  case class Cell(row :Int, column : Int) {
    var formula : Formula = Empty 
    
    def value = evaluate(formula)
    
    override def toString = formula match { 
      case Textual(s) => s
      case _ => value.toString
    }
  }
  
  val cells = new Array[Array[Cell]](height, width)
  
  for (i <- 0 until height; j <- 0 until width) 
    cells(i)(j) = new Cell(i, j)
}

Change Propagation 

so we have all the meat and potatos, but we don't have change propagations, if one of hte dependent cell changes, we are not being informed and the cell is not updated. 

we need to extend the Model class. This we extend our Cell case class to let it extend from the Swing.event.Publisher class and we redefine the formula class to let it have the subscription and unsubcription wired up . as follow. 

class Model(val height : Int, val width: Int) 
      extends Evaluator with Arithmetic {
 
  case class Cell(row :Int, column : Int) extends Publisher {
    private var f : Formula = Empty
    def formula_=(f : Formula) { 
      
      for (c <- references(formula)) deafTo(c)
      this.f = f
      for (c <- references(formula)) listenTo(c)
      value = evaluate(f)
    }
    def formula : Formula = f 
    
    private var v : Double = 0
    def value = evaluate(formula)
    def value_= (w: Double) {
      if (!(v == w || v.isNaN && w.isNaN)) {
        v = w
        publish(ValueChanged(this))
      }
    }
    
    override def toString = formula match { 
      case Textual(s) => s
      case _ => value.toString
    }
    
    // reactions is from the Publisher classes
    reactions += {
      case ValueChanged(_) => value = evaluate(formula)
    }
  } // end class Cell
  
  case class ValueChanged(cell : Cell) extends event.Event
  
  val cells = new Array[Array[Cell]](height, width)
  
  for (i <- 0 until height; j <- 0 until width) 
    cells(i)(j) = new Cell(i, j)
}
in this code, we define a new event called ValueChanged, when a cell is given a different value, the event will be fired. and we need as well to handle the ValueChanged event (this is because some upstream cells when it call publich(ValueChanged(this)), the downstream should react and reevaluate the formula. 

Now, the cell has changed, we need as well to extend the Spreadsheet class to 1. listen to all cells 2. create a handler for the ValueChanged event to redraw content for the cell. the key is to call cellUpdate(row, cell). here is the new Spreadsheet class. 

import swing._
import swing.event._

class Spreadsheet(val height : Int, val width : Int) 
	extends ScrollPane { 
  // the data
  val cellMode = new Model(height, width)
  import cellMode._
  
  
  val table = new Table(height, width) {
    rowHeight = 25
    autoResizeMode = Table.AutoResizeMode.Off
    showGrid = true
    gridColor = new java.awt.Color(150, 150, 150)
    
    override def rendererComponent(isSelected : Boolean, hasFocus : Boolean, row : Int, column : Int) : Component = {
	    if (hasFocus) new TextField(userData(row, column))
	    else 
	      new Label(cells(row)(column).toString) {
	      xAlignment = Alignment.Right
	    }
	}
	  
	def userData(row : Int, column : Int) : String = { 
	    val v = this(row, column)
	    if (v == null) "" else v.toString
	}
	  
	reactions += {
	  case TableUpdated(table, rows, column) => 
	    for (row <- rows) 
	      cells(row)(column).formula = FormulaParsers.parse(userData(row, column))
	  case ValueChanged(cell) => 
	    updateCell(cell.row, cell.column) // redraw the cell with the updateCell call 
	}
	
	for (row <- cells; cell <- row) listenTo(cell) // wire up and listen to all the cells 
  
  }
  
  val rowHeader = 
    new ListView((0 until height) map (_.toString)) {
      fixedCellWidth = 30
      fixedCellHeight = table.rowHeight
  }
  
  viewportView = table
  rowHeaderView = rowHeader
}

The main class

To run the code, we need a Main class, it is as follow. 

import swing._

object Main extends SimpleGUIApplication { 
  def top = new MainFrame { 
    title  = "ScalaSheet"
    contents = new Spreadsheet(100, 20)
  }
}
run the following code 
scala Main

and if you have the package statement above, you should run it like this:
scala org.stairwaybook.scelles.Main
or just 

Main.main(Array[String]())
if you are running from the Eclipse interactive shel. 
相關文章
相關標籤/搜索