1

I am solving a programming exercise that goes like this:

A file contains a list of equalities between positive integers sums, one for each line, each one terminated by a semicolon, without whitespaces. These equalities can either be right or be wrong. For example, consider the following file:

2+3+12=9+8;
2+3+4=9;
22=3+4+5+10;
3+5+1=4+44;

Write a program that takes the filename as its first argument and outputs the ratio of correct lines. For example, given the file above the program should output 0.75.

My solution in Scala is the following, and it works, but I am looking for a way to rewrite it without the var's.

import scala.io.Source

object Hello {
  def main(args: Array[String]): Unit = {
    var count: Int = 0
    var correct: Int = 0

    Source.fromFile(args(0)).getLines().zipWithIndex.foreach { case (line, i) =>
      val regex = """^[1-9][0-9]*(\+[1-9][0-9]*)*=[1-9][0-9]*(\+[1-9][0-9]*)*;$""".r
      regex findFirstIn line match {
        case Some(_) =>
        case None => throw new Exception("Error reading file, line " + i)
      }
      val sums = line.substring(0, line.length - 1).split('=').map {
        _.split('+').map(Integer.parseInt).sum
      }
      count += 1
      correct += (if (sums(0) == sums(1)) 1 else 0)
    }
    println("The ratio is " + correct.toFloat/count)
  }
}

I tried to turn that foreach into a map, like so:

import scala.io.Source

object Hello {
  def main(args: Array[String]): Unit = {
    val file = Source.fromFile(args(0))
    val correct = file.getLines().zipWithIndex.map({ case (line, i) =>
      val regex = """^[1-9][0-9]*(\+[1-9][0-9]*)*=[1-9][0-9]*(\+[1-9][0-9]*)*;$""".r
      regex findFirstIn line match {
        case Some(_) =>
        case None => throw new Exception("Error reading file, line " + i)
      }
      val sums = line.substring(0, line.length - 1).split('=').map {
        _.split('+').map(Integer.parseInt).sum
      }
      return if (sums(0) == sums(1)) 1 else 0
    }).sum
    println("The ratio is " + correct/file.getLines().length)
  }
}

The compiler complains:

Warning:(15, 38) a pure expression does nothing in statement position; you may be omitting necessary parentheses
      return if (sums(0) == sums(1)) 1 else 0
                                     ^
Warning:(15, 45) a pure expression does nothing in statement position; you may be omitting necessary parentheses
      return if (sums(0) == sums(1)) 1 else 0
                                            ^
Warning:(15, 7) enclosing method main has result type Unit: return value discarded
      return if (sums(0) == sums(1)) 1 else 0
      ^
Error:(16, 8) ambiguous implicit values:
 both object BigIntIsIntegral in object Numeric of type scala.math.Numeric.BigIntIsIntegral.type
 and object ShortIsIntegral in object Numeric of type scala.math.Numeric.ShortIsIntegral.type
 match expected type Numeric[B]
    }).sum
       ^
Error:(16, 8) could not find implicit value for parameter num: Numeric[B]
    }).sum
       ^
Error:(16, 8) not enough arguments for method sum: (implicit num: Numeric[B])B.
Unspecified value parameter num.
    }).sum
       ^
Michele De Pascalis
  • 932
  • 2
  • 9
  • 26
  • Look at the warnings: keyword `return` means returning from a surrounding function, that is from `main`. It also forces the type of `correct` to be `Seq[Unit]` instead if `Seq[Int]`. To fix it just remove `return` word completely. – Kolmar Jun 01 '15 at 14:02
  • That does solve the map problem, but then the getLines().length in the last expressions resolves to 0. – Michele De Pascalis Jun 01 '15 at 14:07
  • That's because it has already read all the lines from `file`, when you calculated `correct`. You shouldn't call `getLines()` twice on the same `Source`. To fix this you can for example replace `file` with `val file = Source.fromFile(args(0)).getLines()` and remove all other `getLines` calls, or create a new `Source` for the second `getLines` – Kolmar Jun 01 '15 at 14:15
  • I turned out using two Source's. If you turned all of this into an answer I'd be glad to accept it. – Michele De Pascalis Jun 01 '15 at 14:28

1 Answers1

5

You can define two case classes to simplify your task:

case class Equation(left:Sum,right:Sum) {
  // to check if both sides of equation are equal
  def isCorrect = left.sum == right.sum
}
object Equation {
  // a pattern to extract left and right part of the equation
  val equationPattern = "([0-9\\+]+)=([0-9\\+]+);".r
  // apply method for building equation from string
  def apply(line:String) = line match {
    // building new equation, but use Sum for left/right side parsing
    case equationPattern(left,right) => new Equation(Sum(left), Sum(right))
    case _ => throw new IllegalArgumentException("cannot parse equation")
  }
}

Equation class is used to parse and analyze your equation, but it uses nested case class Sum to parse left or right part of the equation.

case class Sum(elements:List[Int]) {
  // just sum all elements in this equation side
  def sum:Int = elements.sum
}
object Sum {
  // construct Sum from a String like "1+2+3+4"
  def apply(line:String) = new Sum(line.split("\\+").map(_.toInt).toList)
}

Using these simple case classes we can easily parse and analyze equations like yours:

object Main {
  def main(args: Array[String]): Unit = {
    println(Equation("1+2=3+4;").isCorrect) // prints 'false'
    println(Equation("1+2+4=3+4;").isCorrect) // prints 'true'
  }
}
shutty
  • 3,298
  • 16
  • 27