Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/calc #1

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion api/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
group 'net.starype.quiz.api'

apply plugin: 'scala'

dependencies {
implementation 'org.scala-lang:scala-library:2.13.1'
implementation 'org.scala-lang.modules:scala-parser-combinators_2.11:1.0.2'
implementation 'com.electronwill.night-config:toml:3.6.3'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
testCompile "org.scalatest:scalatest_2.11:3.0.8"
}
14 changes: 14 additions & 0 deletions api/src/main/scala/calc/CalcAST.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package calc

sealed trait CalcAST
sealed trait CalcExpression extends CalcAST
sealed trait CalcLiteral extends CalcExpression

case class ASTFunctionId(value: String) extends CalcAST

case class FunctionCall(name: ASTFunctionId, args: List[CalcExpression]) extends CalcExpression
case class BinaryOperation(leftExpression: CalcExpression, operator: CalcOperator, rightExpression: CalcExpression) extends CalcExpression
case class UnaryOperation(operator: CalcOperator, expression: CalcExpression) extends CalcExpression

case class IntLit(value: Int) extends CalcLiteral
case class FloatLit(value: Float) extends CalcLiteral
56 changes: 56 additions & 0 deletions api/src/main/scala/calc/CalcEvaluator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package calc

class CalcEvaluator {

def evalLiteral(literal: CalcLiteral): Number = {
literal match {
case IntLit(value) => value
case FloatLit(value) => value
}
}

def evalFunctionCall(name: ASTFunctionId, args: List[CalcExpression]): Option[Number] = {
name.value match {
case "log" => Option(Math.log(this.evalCalcExpression(args.head) match {
case Some(value) => value.doubleValue()
case None => return Option.empty
}))
case _ => Option.empty
}
}

//TODO: re-think about .floatValue() because it transform '5+3' in '8.0' in example.
def evalBinaryExpression(leftExpression: CalcExpression, operator: CalcOperator, rightExpression: CalcExpression): Option[Number] = {
val leftOptLit = this.evalCalcExpression(leftExpression)
val rightOptLit = this.evalCalcExpression(rightExpression)
if (leftOptLit.isEmpty || rightOptLit.isEmpty) Option.empty
val leftLit = leftOptLit.get
val rightLit = rightOptLit.get
operator match {
case Plus => Option(leftLit.floatValue() + rightLit.floatValue())
case Minus => Option(leftLit.floatValue() - rightLit.floatValue())
case Mul => Option(leftLit.floatValue() * rightLit.floatValue())
case Div => Option(leftLit.floatValue() / rightLit.floatValue())
case Mod => Option(leftLit.floatValue() % rightLit.floatValue())
}
}

def evalUnaryExpression(operator: CalcOperator, expression: CalcExpression): Option[Number] = {
val optExpr = this.evalCalcExpression(expression)
if(optExpr.isEmpty) Option.empty
operator match {
case Minus => Option(-optExpr.get.floatValue())
case _ => Option.empty
}
}

def evalCalcExpression(expression: CalcExpression): Option[Number] = {
expression match {
case literal: CalcLiteral => Option(evalLiteral(literal))
case FunctionCall(name, args) => evalFunctionCall(name, args)
case BinaryOperation(leftExpression, operator, rightExpression) => evalBinaryExpression(leftExpression, operator, rightExpression)
case UnaryOperation(operator, expression) => evalUnaryExpression(operator, expression)
}
}

}
47 changes: 47 additions & 0 deletions api/src/main/scala/calc/CalcLexer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package calc

import scala.util.matching.Regex
import scala.util.parsing.combinator.RegexParsers

class CalcLexer extends RegexParsers {

override def skipWhitespace = true

override val whiteSpace: Regex = "[^\\S\r\n]+".r

private def parseIntegerToken: Parser[IntToken] = "[0-9]+".r ^^ { str => IntToken(str.toInt) }

private def parseFloatToken: Parser[FloatToken] = "([+-]?[0-9]*\\.[0-9]*)".r ^^ { str => FloatToken(str.toFloat)}

private def parseNumber: Parser[NumberToken] = this.parseFloatToken | this.parseIntegerToken

private def parseOperator: Parser[CalcOperator] = {
"+" ^^^ Plus |
"-" ^^^ Minus |
"*" ^^^ Mul |
"/" ^^^ Div |
"mod" ^^^ Mod
}

private def parseKeyword: Parser[CalcKeyword] = {
"(" ^^^ OpeningBracket |
")" ^^^ ClosingBracket |
"." ^^^ Dot |
"," ^^^ Comma
}

private def parseFunctionIdentifier: Parser[CalcFunctionId] = "[a-zA-Z_][a-zA-Z0-9_]*".r ^^ { str => FunctionId(str) }


private def parseTokens: Parser[List[CalcToken]] = {
rep1(this.parseNumber | this.parseOperator | this.parseOperator | this.parseKeyword | this.parseFunctionIdentifier)
}

def apply(input: String): Option[List[CalcToken]] = {
parse(this.parseTokens, input) match {
case NoSuccess(_, _) => Option.empty
case Success(result, _) => Option(result)
}
}

}
46 changes: 46 additions & 0 deletions api/src/main/scala/calc/CalcParser.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package calc

import scala.util.parsing.combinator.Parsers

class CalcParser extends Parsers {

override type Elem = CalcToken

private def parseFunctionId: Parser[ASTFunctionId] = accept("function identifier", { case FunctionId(value) => ASTFunctionId(value) })

private def parseInteger: Parser[IntLit] = accept("integer literal", { case IntToken(value) => IntLit(value) })

private def parseFloat: Parser[FloatLit] = accept("float literal", { case FloatToken(value) => FloatLit(value)})

private def parseLiteral: Parser[CalcLiteral] = parseFloat | parseInteger

private def parseGrouping: Parser[CalcExpression] = OpeningBracket ~> parseExpression <~ ClosingBracket

private def parseArgs: Parser[List[CalcExpression]] = OpeningBracket ~> repsep(parseExpression, Comma) <~ ClosingBracket

private def parseFunctionCall: Parser[FunctionCall] = (parseFunctionId ~ parseArgs) ^^ { case identifier ~ args => FunctionCall(identifier, args)}

private def parseUnary: Parser[UnaryOperation] = (Minus ~> parseExpression) ^^ (expression => UnaryOperation(Minus, expression))

private def parseAddition: Parser[(CalcExpression, CalcExpression) => BinaryOperation] = Plus ^^^ { (left, right) => BinaryOperation(left, Plus, right) }
private def parseSubtraction: Parser[(CalcExpression, CalcExpression) => BinaryOperation] = Minus ^^^ { (left, right) => BinaryOperation(left, Minus, right) }
private def parseMultiplication: Parser[(CalcExpression, CalcExpression) => BinaryOperation] = Mul ^^^ { (left, right) => BinaryOperation(left, Mul, right) }
private def parseDivision: Parser[(CalcExpression, CalcExpression) => BinaryOperation] = Div ^^^ { (left, right) => BinaryOperation(left, Div, right) }
private def parseMod: Parser[(CalcExpression, CalcExpression) => BinaryOperation] = Mod ^^^ { (left, right) => BinaryOperation(left, Mod, right) }

private def parseBinary: Parser[CalcExpression] =
chainl1(
chainl1(parseGrouping | parseUnary | parseLiteral | parseFunctionCall, parseMultiplication | parseDivision | parseMod),
parseAddition | parseSubtraction
)

private def parseExpression: Parser[CalcExpression] = parseBinary | parseUnary | parseFunctionCall | parseLiteral | parseGrouping

def apply(tokens: Seq[CalcToken]): Option[CalcExpression] = {
val reader = new CalcTokenReader(tokens)
parseExpression.apply(reader) match {
case NoSuccess(_, _) => Option.empty
case Success(result, _) => Option(result)
}
}
}
22 changes: 22 additions & 0 deletions api/src/main/scala/calc/CalcToken.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package calc

sealed trait CalcToken
sealed trait NumberToken extends CalcToken
sealed trait CalcOperator extends CalcToken
sealed trait CalcKeyword extends CalcToken
sealed trait CalcFunctionId extends CalcToken

case class FunctionId(value: String) extends CalcFunctionId

case class IntToken(value: Int) extends NumberToken
case class FloatToken(value: Float) extends NumberToken

case object Plus extends CalcOperator
case object Minus extends CalcOperator
case object Comma extends CalcKeyword
case object Mul extends CalcOperator
case object Div extends CalcOperator
case object Mod extends CalcOperator
case object Dot extends CalcKeyword
case object OpeningBracket extends CalcKeyword
case object ClosingBracket extends CalcKeyword
14 changes: 14 additions & 0 deletions api/src/main/scala/calc/CalcTokenReader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package calc

import scala.util.parsing.input.{NoPosition, Position, Reader}

class CalcTokenReader(calcTokens: Seq[CalcToken]) extends Reader[CalcToken] {

override def first: CalcToken = calcTokens.head

override def rest: Reader[CalcToken] = new CalcTokenReader(calcTokens.tail)

override def pos: Position = NoPosition

override def atEnd: Boolean = calcTokens.isEmpty
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package net.starype.quiz.api.calc

import calc._
import org.scalatest.FunSuite

class CalcEvaluatorTest extends FunSuite {

val evaluator = new CalcEvaluator()

test("evaluator_should_evaluate_literals") {
assert(evaluator.evalCalcExpression(IntLit(5)) === 5)
assert(evaluator.evalCalcExpression(FloatLit(5)) === 5.0f)
}

test("evaluator_should_parse_function_call") {
assert(evaluator.evalCalcExpression(FunctionCall(ASTFunctionId("log"), List(IntLit(1)))) === 0)
}

test("evaluator_should_evaluate_unary_expressions") {
assert(evaluator.evalCalcExpression(UnaryOperation(Minus, IntLit(-5))) === 5)
assert(evaluator.evalCalcExpression(UnaryOperation(Minus, FloatLit(-5.0f))) === 5.0f)
}

test("evaluator_should_evaluate_binary_expressions") {
assert(evaluator.evalCalcExpression(BinaryOperation(IntLit(5), Plus, FloatLit(5.0f))) === 10.0f)
assert(evaluator.evalCalcExpression(BinaryOperation(IntLit(5), Minus, IntLit(5))) === 0)
assert(evaluator.evalCalcExpression(BinaryOperation(IntLit(5), Div, IntLit(5))) === 1)
assert(evaluator.evalCalcExpression(BinaryOperation(IntLit(5), Mul, IntLit(5))) === 25)
assert(evaluator.evalCalcExpression(BinaryOperation(IntLit(9), Mod, IntLit(4))) === 1)
}

}
33 changes: 33 additions & 0 deletions api/src/test/scala/net/starype/quiz/api/calc/CalcLexerTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package net.starype.quiz.api.calc

import calc._
import org.scalatest.FunSuite

class CalcLexerTest extends FunSuite {

val lexer: CalcLexer = new CalcLexer()

test("lexer_should_parse_numbers") {
assert(lexer.apply("10") === List(IntToken(10)))
assert(lexer.apply("10.0") === List(FloatToken(10.0f)))
}

test("lexer_should_parse_operators") {
assert(lexer.apply("+") === List(Plus))
assert(lexer.apply("-") === List(Minus))
assert(lexer.apply("/") === List(Div))
assert(lexer.apply("*") === List(Mul))
assert(lexer.apply("mod") === List(Mod))
}

test("lexer_should_parse_keyword") {
assert(lexer.apply("(") === List(OpeningBracket))
assert(lexer.apply(")") === List(ClosingBracket))
assert(lexer.apply(",") === List(Comma))
}

test("lexer_should_parse_function_identifier") {
assert(lexer.apply("log(5.0)") === List(FunctionId("log"), OpeningBracket, FloatToken(5.0f), ClosingBracket))
}

}
36 changes: 36 additions & 0 deletions api/src/test/scala/net/starype/quiz/api/calc/CalcParserTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package net.starype.quiz.api.calc

import calc._
import org.scalatest.FunSuite

class CalcParserTest extends FunSuite {

val parser = new CalcParser()

test("parser_should_parse_literals") {
assert(parser.apply(Seq(IntToken(5))) === IntLit(5))
assert(parser.apply(Seq(FloatToken(5))) === FloatLit(5.0f))
}

test("parser_should_parse_function_call") {
assert(parser.apply(Seq(FunctionId("log"), OpeningBracket, FloatToken(5.0f), ClosingBracket)) === FunctionCall(ASTFunctionId("log"), List(FloatLit(5.0f))))
}

test("parser_should_parse_unary_expression") {
assert(parser.apply(Seq(Minus, IntToken(5))) === UnaryOperation(Minus, IntLit(5)))
assert(parser.apply(Seq(Minus, FloatToken(5.0f))) === UnaryOperation(Minus, FloatLit(5.0f)))
}

test("parser_should_parse_binary_expression") {
assert(parser.apply(Seq(IntToken(5), Minus, IntToken(5))) === BinaryOperation(IntLit(5), Minus, IntLit(5)))
assert(parser.apply(Seq(FloatToken(5.0f), Plus, IntToken(5))) === BinaryOperation(FloatLit(5.0f), Plus, IntLit(5)))
assert(parser.apply(Seq(IntToken(5), Mul, IntToken(5))) === BinaryOperation(IntLit(5), Mul, IntLit(5)))
assert(parser.apply(Seq(IntToken(5), Div, IntToken(5))) === BinaryOperation(IntLit(5), Div, IntLit(5)))
assert(parser.apply(Seq(IntToken(5), Mod, IntToken(5))) === BinaryOperation(IntLit(5), Mod, IntLit(5)))
}

test("parser_should_parse_complex_expressions") {
assert(parser.apply(Seq(FunctionId("log"), OpeningBracket, FloatToken(5.0f), ClosingBracket, Plus, IntToken(5), Mul, FloatToken(5.0f))) === BinaryOperation(FunctionCall(ASTFunctionId("log"), List(FloatLit(5.0f))), Plus, BinaryOperation(IntLit(5), Mul, FloatLit(5.0f))))
}

}