【Scala编程】格式化算术表达式程序

格式化算术表达式程序

为了练习模式匹配的使用,该博文介绍编写格式化算术表达式的程序,最终的呈现结果如下面这个二维布局的数学表达式所示,这里除法运算被垂直打印出来:

1          
- * (x + 1)
2          
-----------
  x   1.5  
  - + ---  
  2    x   

为了实现这个程序,我们需要做一下工作:

1. 编写一个二维布局库来创建和渲染二维布局元素。这里主要应用Scala面向对象编程的一些方法,通过组合与继承来构建简单部件,进而实现库的设计。
2. 编写一个表达式的格式化类,利用二维布局库来渲染二维字符图形,从而实现一个完整的呈现效果。

二维布局库

在二维布局库中,我们将定义类使得元素对象能够由简单部件,包括数组,行,以及长方形进行构造。我们还将定义组合操作符abovebeside。这种组合操作符能把某些区域的元素组合成新的元素。

实现above和beside

Element类中的above方法把一个元素放在另一个上面,实际上是连接两个元素的contents的值。使用++操作符连接两个数组。我们这里还要考虑两个元素的宽度。
beside方法,把两个元素靠在一起,创建一个新的元素,新元素的每一行都来子两个原始元素的串联。这里还要考虑两个元素不同的高度。

实现widen和heighten

widen方法被above调用以确保Element堆叠在一起后有相同的宽度,heighten方法被beside调用以确保靠在一起的元素具有同样的高度。

定义工厂对象

工厂对象包含了构建其他对象的方法。客户可以通过使用工厂方法构建对象而不是直接使用new构造对象。这种方式的好处是可以将对象的创建集中化并且隐藏对象实际代表的类的细节。
这里我们创建Element类的伴生对象并把它作为布局元素的工厂方法。使用这种方式,你唯一暴露给客户的就是Element的类/对象的组合,隐藏ArrayElement,LineElement和UniformElement三个类的实现。
三个elem工厂方法来创建不同的类,这里以参数来去区别不同的类。

完整程序

package com.jason.expr
import Element.elem

abstract class Element {
  def contents: Array[String]

  def width: Int = contents(0).length
  def height: Int = contents.length

  def above(that: Element): Element = {
    val this1 = this widen that.width
    val that1 = that widen this.width
    elem(this1.contents ++ that1.contents)
  }

  def beside(that: Element): Element = {
    val this1 = this heighten that.height
    val that1 = that heighten this.height
    elem(
      for((line1, line2) <- this1.contents zip that1.contents)
        yield line1+line2
    )
  }

  def widen(w: Int): Element = {
    if(w <= width) this
    else{
      val left = elem(' ', (w-width)/2, height)
      val right = elem(' ', w-width-left.width, height)
      left beside this beside right
    }
  }

  def heighten(h: Int): Element = {
    if(h <= height) this
    else {
      val top = elem(' ', width, (h-height)/2)
      val bot = elem(' ', width, h-height-top.height)
      top above this above bot
    }
  }

  override def toString = contents mkString "\n"
}

object Element {
  private class ArrayElement(val contents: Array[String]) extends Element
  private class LineElement(s: String) extends Element {
    val contents = Array(s)
    override def width = s.length
    override def height = 1
  }
  private class UniformElement(
                              ch: Char,
                              override val width: Int,
                              override val height: Int
                                ) extends Element {
    private val line = ch.toString * width
    def contents = Array.fill(height){line}
  }

  def elem(contents: Array[String]): Element =
    new ArrayElement(contents)

  def elem(chr: Char, width: Int, height: Int): Element =
    new UniformElement(chr, width, height)

  def elem(line: String): Element =
    new LineElement(line)
}

格式化表达式类

操作符优先级

首先在水平布局上,结构化的表达式,比如:

BinOp("+",
      BinOp("+",
            BinOp("+", Var("x"), Var("y")),
            Var("z")),
      Number(1))

应该打印成(x+y)*z+1。注意x+y两边的小括号是必加的,不过(x+y)*z的两边就是可选的。为了保证布局最好的可读性,目标是应当去掉冗余的括号,并保留所有必须存在的括号。

在操作符优先级可以采用只定义递增优先级的操作符组,然后通过计算每个操作符的优先级的方式。这样省去了大量对优先级的预计算。
precedence变量是从操作符到它们优先级的映射,从0开始的整数。它是通过带有两个生成器的for表达式计算出来的。第一个生成器产生opGroups数组的每个索引i。第二个生成器产生opGroups(i)里每个操作符op。for表达式对每个这样的操作符创造一个从op操作符到它的索引i的关联。因此,操作符在数组里的相对位置就被作为它的优先级取出来。
操作符优先级的处理是这样的:

val opGroups =
Array(
  Set("|", "||"),
  Set("&", "&&"),
  Set("^"),
  Set("==", "!="),
  Set("<", "<=", ">", ">="),
  Set("+", "-"),
  Set("*", "%")
)

val precedence = {
    val assocs =
      for{
        i <- 0 until opGroups.length
        op <- opGroups(i)
      } yield op -> i
    Map() ++ assocs
}

现在还需要考虑两个操作符的优先级,分别是一元操作符和除法操作符的优先级。一元操作符的优先级高于任何二元操作符,其优先级比*和%的优先级大1,设unaryPrecedence为opGroups的长度,设除法操作符的优先级fractionPrecedence为-1,这样方面进行另外的处理和操作。

格式化操作中的模式匹配

主方法format用来产生表达字符的二维数组的布局元素,该方法接受表达式和紧贴表达式的操作符优先级。format方法通过对表达式的类型执行模式匹配来完成它的工作。
我们重点看一下一元操作符和二元操作符所对应的样本。

case UnOp(op, arg) =>
  elem(op) beside format(arg, unaryPrecedence)

解释:一元操作,结果就由操作op和最高环境优先级格式化参数arg的结果组成。

case BinOp("/", left, right) => {
  val top = format(left, fractionPrecedence)
  val bot = format(right, fractionPrecedence)
  val line  = elem('-', top.width max bot.width, 1)
  val frac = top above line above bot
  if (enclPrec != fractionPrecedence) frac
  else elem(" ") beside frac beside elem(" ")
}

解释:如果表达式是分数,中间结果frac就由格式化了的操作元left和right垂直放置,中间用一条水平线元素分隔构成。水平线的宽度是格式化的操作元宽度的最大值。
最后在frac两边各加一个空格,是为了考虑“(a/b) / c”,如果显示的结果是这样的:

a
_
b
_
c

这种方式既可能是“(a/b) / c”,也可能是“a/(b/c)”。为了消除含糊的语义,内嵌的分数a/b的布局两边应该各加一个空格。变成了这样子:

  a
  _
  b
_ _ _ 
  c
case BinOp(op, left, right) => {
  val opPrec = precedence(op)
  val l = format(left, opPrec)
  val r = format(right, opPrec + 1)
  val oper = l beside elem(" "+ op +" ") beside r
  if(enclPrec <=  opPrec) oper
  else elem("(") beside oper beside elem(")")
}

解释:对于普通的二元操作符来说,首先格式化它的操作元left和right,格式化左操作元的优先级是操作符op的opPrec,而格式化右操作元是这个优先级加1。这样设计保证了括号也同样反映正确的联合性。
比如:
BinOp("-", Var("a"), BinOp("-", Var("b"), Var("c")))
将被正确划分为“a-(b-c)”,之后中间结果oper由格式化了的左右操作元依次放好,中间加操作符构成。如果当前操作符的优先级小于外围操作符的优先级,那么oper就被放在括号当中,否则就按原样返回。

完整程序

package com.jason.expr
import Element.elem

sealed  abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String,
                  left: Expr, right: Expr) extends Expr

class ExprFormatter {
  private val opGroups =
    Array(
      Set("|", "||"),
      Set("&", "&&"),
      Set("^"),
      Set("==", "!="),
      Set("<", "<=", ">", ">="),
      Set("+", "-"),
      Set("*", "%")
    )

  private val precedence = {
    val assocs =
      for{
        i <- 0 until opGroups.length
        op <- opGroups(i)
      } yield op -> i
    Map() ++ assocs
  }

  private val unaryPrecedence = opGroups.length
  private val fractionPrecedence = -1

  private def format(e: Expr, enclPrec: Int): Element = e match {
    case Var(name) => elem(name)

    case Number(num) => {
      def stripDot(s: String) = {
        if (s endsWith ".0") s.substring(0, s.length - 2)
        else s
      }
      elem(stripDot(num.toString))
    }

    case UnOp(op, arg) =>
      elem(op) beside format(arg, unaryPrecedence)

    case BinOp("/", left, right) => {
      val top = format(left, fractionPrecedence)
      val bot = format(right, fractionPrecedence)
      val line  = elem('-', top.width max bot.width, 1)
      val frac = top above line above bot
      if (enclPrec != fractionPrecedence) frac
      else elem(" ") beside frac beside elem(" ")
    }

    case BinOp(op, left, right) => {
      val opPrec = precedence(op)
      val l = format(left, opPrec)
      val r = format(right, opPrec + 1)
      val oper = l beside elem(" "+ op +" ") beside r
      if(enclPrec <=  opPrec) oper
      else elem("(") beside oper beside elem(")")
    }
  }

  def format(e: Expr): Element = format(e, 0)
}

测试结果

package com.jason.expr

object Expression extends  App{
  val f = new ExprFormatter
  val e1 = BinOp("*", BinOp("/", Number(1), Number(2)),
                      BinOp("+", Var("x"), Number(1)))
  val e2 = BinOp("+", BinOp("/", Var("x"), Number(2)),
                      BinOp("/", Number(1.5), Var("x")))
  val e3 = BinOp("/", e1, e2)

  def show(e: Expr) = println(f.format(e) + "\n\n")
  for(e <- Array(e1, e2, e3)) show(e)
}

最终打印效果:

1          
- * (x + 1)
2          


x   1.5
- + ---
2    x 


1          
- * (x + 1)
2          
-----------
  x   1.5  
  - + ---  
  2    x   

转载请注明作者Jason Ding及其出处
GitCafe博客主页(http://jasonding1354.gitcafe.io/)
Github博客主页(http://jasonding1354.github.io/)
CSDN博客(http://blog.csdn.net/jasonding1354)
简书主页(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
Google搜索jasonding1354进入我的博客主页

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容