· 6 years ago · Dec 12, 2019, 07:10 PM
1Command.scala
2
3package tabular
4
5import scala.util.Try
6import introprog.{IO, Dialog}
7
8object Command {
9 var currentSeparator: Char = ','
10
11 var currentTable: Option[Table] = None
12
13 val shortHelpText = "Type 'help' for help on commands and 'quit' to exit app."
14 val longHelpText = """Commands:
15 |help Print this list of commands
16 |ls List files in current directory
17 |load Load a file using introprog.Dialog.file
18 |load filename|url Load a table from filename or url using current separator
19 |save Save a file using introprog.Dialog.file
20 |save filename Save current table to <filepath>
21 |sep Show current column separator
22 |sep c Change current column separator to char c
23 |show Print current table to the console
24 |sort h Sort current table on heading h
25 |filter h a b c ... Keep rows where heading h has values a b c ...
26 |pie h Draw a pie chart of current table's heading h
27 |bar h Draw a bar chart of current table's heading h
28 |quit, Ctrl+D Terminate app
29 |""".stripMargin
30
31 def fromUser(): Vector[String] = {
32 // readLine can give null if Ctrl+D is typed by user (Linux, MacOS)
33 val cmdOpt = Option(scala.io.StdIn.readLine("> ")) // None if null
34 cmdOpt.map(_.split(' ').map(_.trim).toVector).getOrElse(Vector("quit"))
35 }
36
37
38 def load(fileOrUrl: String): String = {
39 currentTable = Some(Table.load(fileOrUrl, currentSeparator))
40 "Table loaded"
41 }
42
43 def save(file: String): String = {
44
45 s"Table saved"
46 }
47
48 def listFiles(): String = IO.currentDir +"\n"+ IO.list(IO.currentDir).mkString("\n")
49
50 def changeSeparator(separator: String): String = {
51 currentSeparator = separator(0)
52 s"Current separator: $separator"
53 }
54
55 def sort(heading: String): String ={
56 currentTable = Some(currentTable.get.sortOn(heading))
57 s"Current table sorted on column $heading"
58 }
59
60 def filter(heading: String, values: String): String = {
61 currentTable = Some(currentTable.get.filter(heading, Set(values.mkString(" "))))
62 s"Current table filtered on values $values on column $heading"
63 }
64
65 def pie(heading: String): String = {
66 Graph.pie(currentTable.get.freq(heading).map(x => (x._1.toString.substring(4, x._1.toString.length-1), x._2)))
67 s"pie chart of heading $heading drawn in another window"
68 }
69
70 def bar(heading: String): String = {
71 Graph.bar(currentTable.get.freq(heading).map(x => (x._1.toString.substring(4, x._1.toString.length-1), x._2)))
72 s"pie chart of heading $heading drawn in another window"
73 }
74
75 def doCommand(cmd: Vector[String]): String = cmd match {
76 case Vector("") | Vector() => shortHelpText
77
78 case Vector("help") => longHelpText
79
80 case Vector("ls") => listFiles()
81
82 //case Vector("load", fileOrUrl) => load(fileOrUrl)
83
84 case Vector("show") => currentTable.get.show
85
86 case Vector("sep") => s"Current separator: '$currentSeparator'"
87
88 case Vector("quit") => "quit"
89
90 case Vector("sep", c) => changeSeparator(c)
91
92 case Vector("sort", heading) => sort(heading)
93
94 case Vector("filter", h, abc) => filter(h, abc)
95
96 case Vector("load") => load(introprog.Dialog.file())
97
98 case Vector("save") => save(introprog.Dialog.file())
99
100 case Vector("save", filename) => save(filename)
101
102 case Vector("pie", heading) => pie(heading)
103
104 case Vector("bar", heading) => bar(heading)
105
106 case _ => s"""Invalid command: ${cmd.mkString(" ")} \n$shortHelpText\n"""
107 }
108
109 def loopUntilQuit(): Unit = {
110 println(shortHelpText)
111 var quit = false
112 while (!quit) {
113 val result = doCommand(fromUser())
114 if (result == "quit") quit = true else println(result)
115 }
116 println("Goodbye!")
117 }
118}
119
120
121
122
123
124
125
126Dialog.scala
127
128package introprog
129
130/** A module with utilities for creating standard GUI dialogs. */
131object Dialog {
132 import javax.swing.{JFileChooser, JOptionPane, JColorChooser}
133
134 Swing.init() // get platform-specific look and feel
135
136 /** Show a file choice dialog starting in `startDir` with confirm `button` text. */
137 def file(button: String = "Open", startDir: String = "~"): String = {
138 val fs = new JFileChooser(new java.io.File(startDir))
139 fs.showDialog(null, button) match {
140 case JFileChooser.APPROVE_OPTION => Option(fs.getSelectedFile.toString).getOrElse("")
141 case _ => ""
142 }
143 }
144
145 /** Show a dialog with a `message` text. */
146 def show(message: String): Unit = JOptionPane.showMessageDialog(null, message)
147
148 /** Show a `message` asking for input with `init` value. Return user input.
149 *
150 * Returns empty string on Cancel. */
151 def input(message: String, init: String = ""): String =
152 Option(JOptionPane.showInputDialog(message, init)).getOrElse("")
153
154 /** Show a confirmation dialog with `question` and OK and Cancel buttons. */
155 def isOK(question: String = "Ok?", title: String = "Confirm"): Boolean =
156 JOptionPane.showConfirmDialog(
157 null, question, title, JOptionPane.OK_CANCEL_OPTION
158 ) == JOptionPane.OK_OPTION
159
160 /** Show a selection dialog with `buttons`. Return a string with the chosen button text. */
161 def select(message: String, buttons: Seq[String], title: String = "Select"): String =
162 scala.util.Try{
163 val chosenIndex =
164 JOptionPane.showOptionDialog(null, message, title, JOptionPane.DEFAULT_OPTION,
165 JOptionPane.QUESTION_MESSAGE, null, buttons.reverse.toArray, null)
166 buttons(buttons.length - 1 - chosenIndex)
167 }.getOrElse("")
168
169 /** Show a color selection dialog and return the color that the user selected. */
170 def selectColor(
171 message: String = "Select a color",
172 default: java.awt.Color = java.awt.Color.red
173 ): java.awt.Color =
174 Option(JColorChooser.showDialog(null, message, default)).getOrElse(default)
175}
176
177
178
179
180
181
182
183
184IO.scala
185package introprog
186
187/** A module with input/output operations from/to the underlying file system. */
188object IO {
189 /** Load a string from a text file called `fileName` using encoding `enc`. */
190 def loadString(fileName: String, enc: String = "UTF-8"): String = {
191 var result: String = ""
192 val source = scala.io.Source.fromFile(fileName, enc)
193 try result = source.mkString finally source.close()
194 result
195 }
196
197 /** Load string lines from a text file called `fileName` using encoding `enc`. */
198 def loadLines(fileName: String, enc: String = "UTF-8"): Vector[String] = {
199 var result = Vector.empty[String]
200 val source = scala.io.Source.fromFile(fileName, enc)
201 try result = source.getLines.toVector finally source.close()
202 result
203 }
204
205 /** Save `text` to a text file called `fileName` using encoding `enc`. */
206 def saveString(text: String, fileName: String, enc: String = "UTF-8"): Unit = {
207 val f = new java.io.File(fileName)
208 val pw = new java.io.PrintWriter(f, enc)
209 try pw.write(text) finally pw.close()
210 }
211
212 /** Save `lines` to a text file called `fileName` using encoding `enc`. */
213 def saveLines(lines: Seq[String], fileName: String, enc: String = "UTF-8"): Unit =
214 saveString(lines.mkString("\n"), fileName, enc)
215
216 /** Load a serialized object from a binary file called `fileName`. */
217 def loadObject[T](fileName: String): T = {
218 val f = new java.io.File(fileName)
219 val ois = new java.io.ObjectInputStream(new java.io.FileInputStream(f))
220 try ois.readObject.asInstanceOf[T] finally ois.close()
221 }
222
223 /** Serialize `obj` to a binary file called `fileName`. */
224 def saveObject[T](obj: T, fileName: String): Unit = {
225 val f = new java.io.File(fileName)
226 val oos = new java.io.ObjectOutputStream(new java.io.FileOutputStream(f))
227 try oos.writeObject(obj) finally oos.close()
228 }
229
230 /** Test if a file with name `fileName` exists. */
231 def isExisting(fileName: String): Boolean = new java.io.File(fileName).exists
232
233 /** Create a directory with name `dir` if it does not exist. */
234 def createDirIfNotExist(dir: String): Boolean = new java.io.File(dir).mkdirs()
235
236 /** Return the path name of the current user's home directory. */
237 def userDir(): String = System.getProperty("user.home")
238
239 /** Return the path name of the current working directory. */
240 def currentDir(): String =
241 java.nio.file.Paths.get(".").toAbsolutePath.normalize.toString
242
243 /** Return a sequence of file names in the directory `dir`. */
244 def list(dir: String = "."): Vector[String] =
245 Option(new java.io.File(dir).list).map(_.toVector).getOrElse(Vector())
246
247 /** Change name of file `from`, DANGER: silently replaces existing `to`. */
248 def move(from: String, to: String): Unit = {
249 import java.nio.file.{Files, Paths, StandardCopyOption}
250 Files.move(Paths.get(from), Paths.get(to), StandardCopyOption.REPLACE_EXISTING)
251 }
252}
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267Main.scala
268package tabular
269
270object Main {
271
272 val welcome = "Welcome to tabular: an app for analysis of data in tables"
273 val usage = "\nFormat of optional main args: <uri> <separator>"
274
275 def main(args: Array[String]): Unit = {
276 scala.util.Try {
277 println(s"$welcome\nCurrent dir: ${introprog.IO.currentDir()}")
278 args.toVector match {
279 case Vector() => println("No args given. Starting with empty table.")
280
281 case Vector(uri) => Command.load(uri)
282
283 case Vector(uri, sep) if sep.nonEmpty =>
284 Command.currentSeparator = sep(0)
285 Command.load(uri)
286
287 case _ => println(s"""Unkown args: ${args.mkString(" ")}$usage""")
288 }
289 Command.loopUntilQuit()
290 }.recover { case e: Throwable =>
291 println("EXCEPTION THROWN! Printing stack trace:")
292 e.printStackTrace(System.out)
293 println("RECOVERING: Table is empty. Restarting command loop...")
294 Command.loopUntilQuit()
295 }
296 }
297}
298
299
300
301
302
303
304
305
306
307Table.scala
308
309package tabular
310import scala.collection.mutable.ArrayBuffer
311import scala.util.Try
312
313case class Table(
314 headings: Vector[String],
315 rows: Vector[Vector[Cell]],
316 separator: Char
317){
318 val nRows = rows.length
319 val nCols = rows.map(_.length).max
320 val dim = (nRows, nCols)
321
322 /** Uppgift: Ändra så att resultatet blir -1 om nyckel inte finns. */
323 val indexOfHeading: Map[String, Int] = headings.zipWithIndex.toMap
324
325 /** Cell på plats (row, col), Cell.empty om indexering utanför gräns. */
326 def apply(row: Int, col: Int): Cell = rows(row)(col) //
327
328 /** Some(cell) på plats (row, col), None om indexering utanför gräns. */
329 def get(row: Int, col: Int): Option[Cell] = Try {
330 Some(rows(row)(col))
331 }.toOption.get //
332
333 def row(index: Int): Vector[Cell] = rows(index)
334
335 def col(index: Int): Vector[Cell] = rows.indices.map(apply(_, index)).toVector
336
337 def col(heading: String): Vector[Cell] = col(indexOfHeading(heading))
338
339 /** Registrering av frekvens för varje unikt värde i kolumn heading.*/
340 def freq(heading: String): Vector[(Cell, Int)] = {
341 val tempVector = rows.map(x => x(indexOfHeading(heading)))
342 val returnBuffer: ArrayBuffer[(Cell, Int)] = ArrayBuffer()
343 for(x <- tempVector.distinct) {
344 returnBuffer.append((x, tempVector.count(_ == x)))
345 }
346 returnBuffer.toVector
347 }
348
349 def filter(heading: String, values: Set[String]): Table =
350 copy(rows = rows.filter(r => values.contains(r(indexOfHeading(heading)).value)))
351
352 def sortOn(heading: String): Table =
353 copy(rows = rows.sortBy(r => r(indexOfHeading(heading)).value))
354
355 def values(heading: String): Vector[String] = col(heading).map(_.value)
356
357 def show: String = {
358 def maxValueLength(heading: String): Int = col(heading).map(_.value.length).max
359 val colSize = headings.map(h => maxValueLength(h) max h.length)
360 val paddedHeadings = headings.map(h => h.padTo(colSize(indexOfHeading(h)), ' '))
361 val heading = paddedHeadings.mkString("|","|","|")
362 val line = "-" * heading.size
363 val paddedRows = rows.map(r =>
364 headings.indices.map(i => r(i).value.padTo(colSize(i),' ')).mkString("|","|","|")
365 ).mkString("\n")
366 s"$line\n$heading\n$line\n$paddedRows\n$line\n (nRows, nCols) == $dim\n"
367 }
368
369 /** Strängvektor för varje rad i `rows`, kolumner skilda med `separator`. */
370 def toLines(separator: Char): Vector[String] = ???
371}
372
373object Table {
374 /** Skapa tabell från fil el. webbadress; kolumner skilda med `separator`. */
375 def load(fileOrUrl: String, separator: Char): Table = {
376 val source = fileOrUrl match {
377 case url if url.startsWith("http") => scala.io.Source.fromURL(url)
378 case path => scala.io.Source.fromFile(path)
379 }
380 val lines = try source.getLines.toVector finally source.close
381 val rows = lines.map(_.split(separator).toVector)
382 Table(rows.head, rows.tail.map(_.map(Cell.apply)), separator)
383 }
384}