《Scala实用指南》读书笔记八:创建应用程序

#scala

1. XML作为一等公民

Scala提供了一种类似于XPath的查询能力,它和XPath只有一点细微的差别。Scala不使用熟悉的XPath正斜杠(/或者//)来查询,而是使用反斜杠(\和\\)来作为分析和提取内容的方法。这种差别是必要的,因为Scala遵循Java的传统,使用两个正斜杠来进行注释,而单个正斜杠则是除法操作符。

  val xmlFragment =
      <symbols>
      <symbol ticker="AAPL"><units>200</units></symbol>
      <symbol ticker="IBM"><units>215</units></symbol>
    </symbols>  //> xmlFragment  : scala.xml.Elem = <symbols>
  // 天然支持 xml 的类
  println(xmlFragment.getClass)             //> class scala.xml.Elem

  // 取出 symbol 节点
  val symbolNodes = xmlFragment \ "symbol"  //> symbolNodes  : scala.xml.NodeSeq = NodeSeq(<symbol ticker="AAPL"><units>200</units></symbol>, <symbol ticker="IBM"><units>215</units></symbol>)
  // 逐个打印 symbol 节点
  symbolNodes foreach println
  //<symbol ticker="AAPL"><units>200</units></symbol>
  //<symbol ticker="IBM"><units>215</units></symbol>
  println(symbolNodes.getClass)             //> class scala.xml.NodeSeq$$anon$1

  // \() 方法只查找目标元素的直接子元素,如果要从目标元素开始的层次结构中搜索所有元素,应使用 \\()方法
  val unitsNodes = xmlFragment \\ "units"   //> unitsNodes  : scala.xml.NodeSeq = NodeSeq(<units>200</units>, <units>215</un
                                                  //| its>)

  unitsNodes foreach println                //> <units>200</units>
                                            //| <units>215</units>
  println(unitsNodes.getClass)              //> class scala.xml.NodeSeq$$anon$1
  println(unitsNodes.head.text)             //> 200

Scala 有强大的模式匹配能力。Scala 也将这一能力扩展到了匹配 XML 片段中:

  unitsNodes.head match {
    case <units>{ numberOfUnits}</units> => println(s"Units: $numberOfUnits")
  }                                         //> Units: 200

通过使用_*通配符,我们要求将<symbols>和</symbols>元素之间的所有内容都读到了占位符变量 symbolNodes里。

  xmlFragment match {
    case <symbols>{ symbolNodes @ _* }</symbols> =>
      for (symbolNode @ <symbol>{ _* }</symbol> <- symbolNodes) {
        println("%-7s %s".format(
          symbolNode \ "@ticker", (symbolNode \ "units").text))
      }
  }                                         //> AAPL    200
                                            //| IBM     215

同样,也可以对 node 像列表一样执行map方法:

  def updateUnitsAndCreateXML(element: (String, Int)) = {
    val (ticker, units) = element
    <symbol ticker={ ticker }>
      <units>{ units + 1 }</units>
    </symbol>
  }    //> updateUnitsAndCreateXML: (element: (String, Int))scala.xml.Elem
  val updatedStocksAndUnitsXML =
    <symbols>
      { stocksAndUnitsMap map updateUnitsAndCreateXML }
    </symbols>  

2. 从 Web 获取股票价格

本地文件 stocks.xml 记录了股票代码的列表及持有的数量,同时,我们记录了公司最近一段时间的股价,例如GOOG的股价,要获得最新的收盘价,可以取第二行的数据。

因此,我们首先定义getLatestClosingPrice,该方法接收公司名作为参数,返回其时间及对应的收盘价(Record).

  import scala.io.Source

  case class Record(year: Int, month: Int, date: Int, closePrice: BigDecimal)

  def getLatestClosingPrice(symbol: String): BigDecimal = {
      // 访问有时超时,git clone 到本地,同时+了sleep 1s来代替网络延时
      /*
      val url = "https://raw.githubusercontent.com/ReactivePlatform/" +
        s"Pragmatic-Scala-StaticResources/master/src/main/resources" +
        s"/stocks/daily/daily_${symbol}.csv"
      */
      Thread.sleep(1000)
      val url = s"http://0.0.0.0:8000/" +
        s"Pragmatic-Scala-StaticResources/src/main/resources/stocks/daily/daily_${symbol}.csv"
      val data = Source.fromURL(url).mkString
      // timestamp ,open     ,high     ,low      ,close    ,volume
      val latestClosePrize = data.split("\n")
        .slice(1, 2)
        .map(record => {
          val Array(timestamp, open, high, low, close, volume) = record.split(",")
          val Array(year, month, date) = timestamp.split("-")
          Record(year.toInt, month.toInt, date.toInt, BigDecimal(close.trim))
        })
        .map(_.closePrice)
        .head

      latestClosePrize
  }                                               //> getLatestClosingPrice: (symbol: String)BigDecimal

接着定义读取本地xml的方法,获取持有的股票名称及数量:

  def getTickersAndUnits: Map[String, Int] = {
      val stocksAndUnitsXML = scala.xml.XML.load("./stocks.xml")
      (Map[String, Int]() /: (stocksAndUnitsXML \ "symbol")) {
        (map, symbolNode) =>
          val ticker = (symbolNode \ "@ticker").toString
          val units = (symbolNode \ "units").text.toInt
          map + (ticker -> units)
      }
  }                                               //> getTickersAndUnits: => Map[String,Int]
  
  val symbolsAndUnits = getTickersAndUnits        //> symbolsAndUnits  : Map[String,Int] = Map(MSFT -> 190, AAPL -> 200, AMD -> 150, HPQ -> 225, ORCL -> 200, INTC -> 160, IBM -> 215, ALU -> 150, VRSN -> 200, CSCO -> 250, TXN -> 190, ADBE -> 125, NSM -> 200, SYMC -> 230, XRX -> 240)

最后将两者的结果结合,并且按照股票名称排序:

  println("Ticker Units Closing Prics($) Total Value($)")
          //> Ticker Units Closing Prics($) Total Value($)

  val startTime = System.nanoTime()
  // 串行
  // val valuesAndWorth = symbolsAndUnits.keys.map { symbol =>
  // 并行
  val valuesAndWorth = symbolsAndUnits.keys.par.map { symbol =>
      val units = symbolsAndUnits(symbol)
      val latestClosingPrice = getLatestClosingPrice(symbol)
      val value = units * latestClosingPrice

      (symbol, units, latestClosingPrice, value)
  }

  val newWorth = ((BigDecimal(0.0)) /: valuesAndWorth) { (worth, valueAndWorth) =>
      val (_, _, _, value) = valueAndWorth
      worth + value
  }                                               //> newWorth  : scala.math.BigDecimal = 212071.3000

  val endTime = System.nanoTime()                 //> endTime  : Long = 63624493516148

  valuesAndWorth.toList.sortBy { _._1 } foreach { valueAndWorth =>
        println(valueAndWorth)
      }

  println(f"$$$newWorth%.2f")                     //> $212071.30
  println(f"taken ${(endTime - startTime)/1.0e9}%.2f seconds")
                                                  //> taken 5.54 seconds