Last active
May 16, 2020 09:20
-
-
Save evgkarasev/3d4344a7ef07937c48c29b846907ac49 to your computer and use it in GitHub Desktop.
SpreadSheet in Scala / Swing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name := "ScalaCalcSheet" | |
version := "0.1" | |
scalaVersion := "2.13.2" | |
libraryDependencies += "org.scala-lang.modules" %% "scala-swing" % "2.1.1" | |
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2" | |
libraryDependencies += "com.sksamuel.avro4s" %% "avro4s-core" % "3.1.0" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ScalaCalcSheet | |
import scala.math._ | |
trait Arithmetic { | |
this: Evaluator => | |
operations ++= List( | |
"sin" -> { case List(x) => sin(x) }, | |
"cos" -> { case List(x) => cos(x) }, | |
"tan" -> { case List(x) => tan(x) }, | |
"asin" -> { case List(x) => asin(x) }, | |
"acos" -> { case List(x) => acos(x) }, | |
"atan" -> { case List(x) => atan(x) }, | |
"deg" -> { case List(x) => toDegrees(x) }, // Converts an angle measured in radians to an approximately equivalent angle measured in degrees. | |
"rad" -> { case List(x) => toRadians(x) }, // Converts an angle measured in degrees to an approximately equivalent angle measured in radians. | |
"abs" -> { case List(x) => abs(x) }, // Determine the magnitude of a value by discarding the sign. Results are >= 0 | |
"ln" -> { case List(x) => log(x) }, // Returns the natural logarithm of a Double value. | |
"lg" -> { case List(x) => log10(x) }, // Returns the base 10 logarithm of the given Double value. | |
"pow" -> { case List(x, y) => pow(x, y) }, // Returns the value of the first argument raised to the power of the second argument. | |
"sqrt" -> { case List(x) => sqrt(x) }, // Returns the square root of a Double value. | |
"cbrt" -> { case List(x) => cbrt(x) }, // Returns the cube root of the given Double value. | |
"sig" -> { case List(x) => signum(x) }, // For signum extract the sign of a value. Results are -1, 0 or 1. | |
"rand" -> { case List(x) => random() * x }, // Returns a Double value with a positive sign, greater than or equal to 0.0 and less than argument. | |
"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 => xs.foldLeft(0.0)(_ + _) }, // Returns sum of arguments within the range | |
"prod" -> { xs => xs.foldLeft(1.0)(_ * _) }, // Returns product of arguments within the range | |
"max" -> { xs => xs.foldLeft(xs.head)(max) }, // Returns maximum of arguments within the range | |
"min" -> { xs => xs.foldLeft(xs.head)(min) } // Returns minimum of arguments within the range | |
) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ScalaCalcSheet | |
trait Evaluator { | |
this: Model => | |
type Op = List[Double] => Double | |
val operations = new collection.mutable.HashMap[String, Op] | |
private def evalList(formula: Formula): List[Double] = | |
formula match { | |
case Range(_, _) => | |
references(formula) map (_.value) | |
case _ => | |
List(evaluate(formula)) | |
} | |
def evaluate(formula: Formula): Double = try { | |
formula 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 // List[Formula] => List[Double] | |
operations(function)(argVals) | |
} | |
} catch { | |
// Not-a-number error raised in case of any evaluation exception | |
case ex: Exception => Double.NaN | |
} | |
def references(formula: Formula): List[Cell] = | |
formula match { | |
case Coord(row, column) => | |
List(cells(row)(column)) | |
// this requires for range evaluation | |
case Range(Coord(r1, c1), Coord(r2, c2)) => | |
for (row <- (r1 to r2).toList; column <- c1 to c2) | |
yield cells(row)(column) | |
// this requires for subscribing to references in formula | |
case Application(function, arguments) => | |
arguments flatMap references | |
case _ => | |
List() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ScalaCalcSheet | |
import java.io._ | |
import scala.swing._ | |
import org.apache.avro.Schema | |
import com.sksamuel.avro4s.{AvroInputStream, AvroOutputStream, AvroSchema} | |
// Helper class for serialization purposes | |
case class SheetUserData(table: Array[Array[String]]) | |
class FileOps { | |
val schema: Schema = AvroSchema[SheetUserData] | |
/** Deserialize the Table userdata and load from the file. | |
* | |
* @param file File to load the data. | |
* @return The userdata in helper object | |
*/ | |
def readAvro(file: File): SheetUserData = { | |
val is = AvroInputStream.data[SheetUserData].from(file).build(schema) | |
val data = is.iterator.toList.head | |
is.close() | |
data | |
} | |
/** Serialize the Table userdata and save to the file. | |
* | |
* @param file File to save the data. | |
* @param data Userdata in helper object | |
*/ | |
def writeAvro(file: File, data: SheetUserData): Unit = { | |
val os = AvroOutputStream.data[SheetUserData].to(file).build(schema) | |
os.write(data) | |
os.flush() | |
os.close() | |
} | |
/** Asks the user to choose a file to load date into cells. | |
* | |
* @param title What to put in the file chooser dialog title bar. | |
* @return The chosen file. | |
*/ | |
def chooseOpenFile(title: String = ""): Option[File] = { | |
val chooser = new FileChooser(new File(".")) | |
chooser.title = title | |
if (chooser.showOpenDialog(Main.top) == FileChooser.Result.Approve) | |
Some(chooser.selectedFile) | |
else None | |
} | |
/** Asks the user to choose a file to save date from cells. | |
* | |
* @param title What to put in the file chooser dialog title bar. | |
* @return The chosen file. | |
*/ | |
def chooseSaveFile(title: String = ""): Option[File] = { | |
val chooser = new FileChooser(new File(".")) | |
chooser.title = title | |
if (chooser.showSaveDialog(Main.top) == FileChooser.Result.Approve) { | |
Some(chooser.selectedFile) | |
} else None | |
} | |
/** Asks the user to choose a directory, then returns (as an option) | |
* an array of the files in that directory. | |
* | |
* @param title What to put in the file chooser dialog title bar. | |
* @return The files in the chosen directory. | |
*/ | |
def getDirectoryListing(title: String = ""): Option[Array[File]] = { | |
val chooser = new FileChooser(null) | |
chooser.fileSelectionMode = FileChooser.SelectionMode.DirectoriesOnly | |
chooser.title = title | |
if (chooser.showOpenDialog(Main.top) == FileChooser.Result.Approve) { | |
Some(chooser.selectedFile.listFiles()) | |
} else None | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ScalaCalcSheet | |
import scala.util.parsing.combinator._ | |
object FormulaParsers extends RegexParsers with Formula { | |
def ident: Parser[String] = """[a-zA-Z_]\w*""".r | |
def decimal: Parser[String] = """-?\d+(\.\d*)?""".r | |
def pi: Parser[Number] = """Pi""".r ^^ ( _ => Number(3.141592653589793)) | |
def e: Parser[Number] = """E""".r ^^ ( _ => Number(2.718281828459045)) | |
def const: Parser[Number] = pi | e | |
def empty: Parser[Textual] = | |
"""""".r ^^ (_ => Empty) | |
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 | const | |
def textual: Parser[Textual] = | |
"""[^=].*""".r ^^ Textual | |
def formula: Parser[Formula] = | |
number | textual | "="~>expr | empty | |
def parse(input: String): Formula = | |
parseAll(formula, input) match { | |
case Success(e, _) => e | |
case f: NoSuccess => Textual("[" + f.msg + "]") | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ScalaCalcSheet | |
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 | |
} | |
case class Application(function: String, | |
arguments: List[Formula]) extends Formula { | |
override def toString = | |
function + arguments.mkString("(", ",", ")") | |
} | |
object Empty extends Textual("") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ScalaCalcSheet | |
import swing._ | |
import java.io._ | |
import javax.swing.KeyStroke | |
object Main extends SimpleSwingApplication { | |
val height = 100 | |
val width = 26 | |
val spreadsheet = new Spreadsheet(height, width) | |
var file: File = new File("NewSheet.avro") | |
val fileOps = new FileOps | |
import fileOps._ | |
val menubar: MenuBar = new MenuBar { | |
contents += new Menu("File") { | |
// TODO: Initialize new Spredsheet instance | |
contents += new MenuItem(new Action("New") { | |
accelerator = Some(KeyStroke.getKeyStroke("ctrl N")) | |
def apply { | |
spreadsheet.loadUserData(readAvro(file).table) | |
} | |
}) | |
// Open existing file | |
contents += new MenuItem(new Action("Open") { | |
accelerator = Some(KeyStroke.getKeyStroke("ctrl O")) | |
def apply { | |
val choosedFile = chooseOpenFile("Open File") | |
spreadsheet.loadUserData(readAvro(choosedFile.getOrElse(file)).table) | |
} | |
}) | |
// Save existing file | |
contents += new MenuItem(new Action("Save") { | |
accelerator = Some(KeyStroke.getKeyStroke("ctrl S")) | |
def apply { | |
writeAvro(file, SheetUserData(spreadsheet.getUserData)) | |
} | |
}) | |
// Save as new file | |
contents += new MenuItem(Action("Save As...") { | |
val choosedFile = chooseSaveFile("Save File") | |
writeAvro(choosedFile.getOrElse(file), SheetUserData(spreadsheet.getUserData)) | |
}) | |
contents += new MenuItem(Action("Exit") { | |
sys.exit(0) | |
}) | |
} | |
contents += new Menu("Help") { | |
contents += new MenuItem(new Action("Formulas") { | |
accelerator = Some(KeyStroke.getKeyStroke("ctrl H")) | |
def apply { | |
val message = | |
"""Constants: | |
|=Pi Returns 3.141592653589793 | |
|=E Returns 2.718281828459045 | |
|References: | |
|=A1 Reference to actual value of cell A1 | |
|=max(A1:A20) Returns maximum within the range of cells from A1 to A20 | |
|Formulas: | |
|=sin(div(Pi, 2)) Returns sin of pi/2 | |
|=add(sin(div(Pi, 4)), cos(div(Pi, 4))) Returns sin(Pi/4) + cos(pi/4))""".stripMargin | |
Dialog.showMessage(top, message=message, title="Formulas") | |
} | |
}) | |
contents += new MenuItem(new Action("Functions") { | |
accelerator = Some(KeyStroke.getKeyStroke("ctrl F")) | |
def apply { | |
val message = | |
"""sin(x) | asin(x) | |
|cos(x) | acos(x) | |
|tan(x) | atan(x) | |
|deg(x) Converts an angle in radians to equivalent in degrees | |
|rad(x) Converts an angle in degrees to equivalent in radians | |
|abs(x) Determine the magnitude of a value by discarding the sign | |
|ln(x) Returns the natural logarithm of given value | |
|lg(x) Returns the base 10 logarithm of given value | |
|pow(x, y) Returns the value of the first argument raised to the power of the second argument. | |
|sqrt(x) Returns the square root of given value | |
|cbrt(x) Returns the cube root of given value | |
|sig(x) Extract the sign of a value. Results are -1, 0 or 1. | |
|rand(x) Returns positive value greater than zero and less than argument | |
|add(x, y) Returns x + y | sub(x, y) Returns x - y | |
|mul(x, y) Returns x * y | div(x, y) Returns x / y | |
|mod(x, y) Returns x % y | |
|sum(range) Returns sum of arguments within the range | |
|prod(range) Returns product of arguments within the range | |
|max(range) Returns maximum of arguments within the range | |
|min(range) Returns minimum of arguments within the range""".stripMargin | |
Dialog.showMessage(top, message=message, title="Functions") | |
} | |
}) | |
contents += new MenuItem(Action("About") { | |
val message = | |
"""Scala CalcSheet Demo v0.1 | |
|Scala v2.13.2 | Swing v1.1.2""".stripMargin | |
Dialog.showMessage(top, message=message, title="About") | |
}) | |
} | |
} | |
def top: MainFrame = new MainFrame { | |
title = "ScalaSheet" | |
contents = spreadsheet | |
size = new Dimension(1000, 800) | |
menuBar = menubar | |
centerOnScreen() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ScalaCalcSheet | |
import scala.swing._ | |
class Model(val height: Int, val width: Int) | |
extends Evaluator with Arithmetic { | |
case class ValueChanged(cell: Cell) extends event.Event | |
case class Cell(row: Int, column: Int) extends Publisher { | |
override def toString: String = formula match { | |
case Textual(s) => s | |
case _ => value.toString | |
} | |
private var _value: Double = 0 | |
def value: Double = _value | |
def value_=(w: Double) = { | |
if (!(_value == w || _value.isNaN && w.isNaN)) { | |
_value = w | |
publish(ValueChanged(this)) // New value publishes the change | |
} | |
} | |
private var _formula: Formula = Empty | |
def formula: Formula = _formula | |
def formula_=(f: Formula) = { | |
for (c <- references(formula)) deafTo(c) // Unsubscribes from old references | |
_formula = f | |
for (c <- references(_formula)) listenTo(c) // New formula subscribes to new references | |
value = evaluate(_formula) | |
} | |
reactions += { | |
case ValueChanged(_) => value = evaluate(formula) | |
} | |
} | |
val cells: Array[Array[Cell]] = Array.ofDim[Cell](height, width) | |
for (i <- 0 until height; j <- 0 until width) | |
cells(i)(j) = Cell(i, j) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ScalaCalcSheet | |
import java.awt | |
import java.awt.event.MouseListener | |
import scala.swing._ | |
import scala.swing.event._ | |
class Spreadsheet(val height: Int, val width: Int) | |
extends ScrollPane { | |
val cellModel = new Model(height, width) | |
import cellModel._ | |
val table: Table = new Table(height, width) { | |
rowHeight = 25 | |
autoResizeMode = Table.AutoResizeMode.Off | |
showGrid = true | |
gridColor = new java.awt.Color(150, 150, 150) | |
focusable = true | |
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) | |
} | |
for (row <- cells; cell <- row) listenTo(cell) | |
} | |
val rowHeader: ListView[String] = | |
new ListView((0 until height) map (_.toString)) { | |
fixedCellWidth = 30 | |
fixedCellHeight = table.rowHeight | |
} | |
viewportView = table | |
rowHeaderView = rowHeader | |
def getUserData: Array[Array[String]] = { | |
val tableModel: Array[Array[String]] = Array.ofDim[String](height, width) | |
for (i <- 0 until height; j <- 0 until width) { | |
val v = table.model.getValueAt(i, j) | |
tableModel(i)(j) = if (v == null) "" else v.toString | |
} | |
tableModel | |
} | |
def loadUserData(tableModel: Array[Array[String]]): Unit = { | |
for (i <- tableModel.indices; j <- tableModel(0).indices) { | |
table.model.setValueAt(tableModel(i)(j), i, j) | |
cells(i)(j).formula = FormulaParsers.parse(tableModel(i)(j)) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment