[add] added full yml parsing to scala collection objects

- added a simple parsing test
 - added conversion utils for Any to Map and nested objects
 - added a quick indented printout util for scala collection objects
This commit is contained in:
2020-01-29 01:19:10 -06:00
parent 575ae2a14a
commit 00669fa9d3
4 changed files with 147 additions and 5 deletions

View File

@@ -1,17 +1,120 @@
package scim
package lib
// java imports
import java.io.{File, FileInputStream}
// scala imports
import scala.collection.convert.wrapAll._
import scala.collection.mutable
import scala.collection.JavaConverters._
// external imports
import org.yaml.snakeyaml.Yaml
package object printUtil {
def indentCollection(s: String) : String = {
var indent = 0
var skip = false
var bracket = false
var output = ""
def closureIndentCollection(c: Char) : Unit = {
if(c == '(') {
indent += 1
output += "(\n" + "\t" * indent
} else if(c == ')') {
indent -= 1
if(bracket) output += "\n" + "\t" * indent
output += ")"
bracket = true
} else if(skip) {
skip = false
} else if(c == ',') {
skip = true
bracket = false
output += "," + "\n" + "\t" * indent
} else {
output += c.toString
}
}
s.foreach(c => closureIndentCollection(c))
output
}
}
package object convertUtil {
def javaArrayListToSeq(javaArray: Any) : Option[Seq[Any]] = {
javaArray match {
/* While LinearSeq(List) for array.length == 2 is faster than than IndexedSeq(Vector) when accessed via tail/head
* this would convert a lot of elements to List that are accessed via foreach and not tail/head lookup
* as we don't know what they're going to be used for at this point, it makes more sense to just use vectors
* https://docs.scala-lang.org/tutorials/FAQ/collections.html
* https://docs.scala-lang.org/overviews/collections/performance-characteristics.html
*/
case array: java.util.ArrayList[_] => Some(array.asScala.toIndexedSeq)
case _ => None
}
}
}
package object mapUtil {
def javaHashMapToScalaMap[K, V](javaMap: java.util.LinkedHashMap[_, _]) : Map[K, V] = {
mapAsScalaMap(javaMap).toMap.asInstanceOf[Map[K, V]]
}
def anyToNestedMap[K](map: Any, depthLimit: Int = 100) : Map[K, Any] = {
def closureAnyToNestedMap(nestedValue: Any, depth: Int = 0) : Any = {
if(depth > depthLimit) return nestedValue // crude way to avoid a circular map
anyToMap[K, Any](nestedValue) match {
case Some(nestedMap) => nestedMap.map(kv => kv._1 -> closureAnyToNestedMap(kv._2, depth + 1))
case _ =>
convertUtil.javaArrayListToSeq(nestedValue) match {
case Some(nestedSeq) => nestedSeq.map(v => closureAnyToNestedMap(v, depth + 1))
case _ => nestedValue
}
}
}
anyToMap[K, Any](map) match {
case Some(nestedMap: Map[K, Any]) => nestedMap.map(kv => (kv._1 -> closureAnyToNestedMap(kv._2)))
case _ => Map[K, Any]() // if no map was given
}
}
def anyToMutableMap[K, V](mapAny: Any) : Option[mutable.Map[K, V]] = {
// TODO: use TypeTag to work around type erasure to check for T[K, V]
// http://squidarth.com/scala/types/2019/01/11/type-erasure-scala.html
mapAny match {
case map: Map[_, _] => Some(mutable.Map() ++ map.asInstanceOf[Map[K, V]])
case map: mutable.Map[_, _] => Some(map.asInstanceOf[mutable.Map[K, V]])
case map: java.util.LinkedHashMap[_, _] => Some(mapAsScalaMap(map).asInstanceOf[mutable.Map[K, V]])
case _ => None
}
}
def anyToMap[K, V](mapAny: Any) : Option[Map[K, V]] = {
// TODO: use TypeTag to work around type erasure to check for T[K, V]
// http://squidarth.com/scala/types/2019/01/11/type-erasure-scala.html
mapAny match {
case map: Map[_, _] => Some(map.asInstanceOf[Map[K, V]])
case map: mutable.Map[_, _] => Some(map.toMap.asInstanceOf[Map[K, V]])
case map: java.util.LinkedHashMap[_, _] => Some(javaHashMapToScalaMap[K, V](map))
case _ => None
}
}
}
package object yml {
val yaml = new Yaml
// TODO: handle errors (invalid file/format)
def parse(file:String) : Map[Nothing, Nothing] = {
def parse(file:String) : Map[String, Any] = {
val fis = new FileInputStream(new File(file))
val list = mapAsScalaMap(yaml.load(fis))
return list.toMap
mapUtil.anyToNestedMap[String](yaml.load(fis))
}
}

View File

@@ -5,7 +5,8 @@ import scim.lib._
object Main {
def main(args: Array[String]): Unit = {
val config = "docs/wiki/config/config.yml"
val list = yml.parse(config)
println(list)
val list: Map[String, Any] = yml.parse(config)
println(printUtil.indentCollection(list.toString()).toString)
}
}

View File

@@ -0,0 +1,28 @@
package scim
import org.scalatest.FunSuite
import org.junit.runner.RunWith
import org.scalatestplus.junit.JUnitRunner
@RunWith(classOf[JUnitRunner])
class TestSuite extends FunSuite {
var testObject = "scim.lib.yml.parse"
test(testObject + ": parsing test") {
// test configuration
val config = "src/test/scala/scim/files/test.yml"
val mapCheck: Map[String, Any] =
Map(
"simulation" -> Map("duration" -> 500, "tickfactor" -> 10, "loop" -> true, "loopcount" -> 20),
"sensors" -> Map("blocks" -> List(Map("start" -> Vector(8.2, 1, 5), "end" -> List(3, 3), "count" -> 30)))
)
// sanity check, to prevent a false positive if both are empty
assert(!mapCheck.isEmpty, testObject + ": check list is empty")
// test
val mapTest: Map[String, Any] = scim.lib.yml.parse(config)
// Note: will assert to true even if numerical type doesnt match (Int, Float, Double; eg: 5.0 == 5 -> true)
assert(mapTest == mapCheck, testObject + ": test map is not equal to check map")
}
}

View File

@@ -0,0 +1,10 @@
simulation:
duration: 500
tickfactor: 10
loop: true
loopcount: 20
sensors:
blocks:
- start: [8.2, 1, 5]
end: [3, 3]
count: 30