《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