nthportal / convert

A Scala library for handling conversions between types by throwing exceptions or returning Options containing the results

GitHub

convert

A Scala library for handling conversions between types by throwing exceptions or returning Options containing the results

Build Status Coverage Status Maven Central Versioning Docs

Add as a Dependency

SBT (Scala 2.11 and 2.12)

"com.nthportal" %% "convert" % "0.5.0"

Maven

Scala 2.12

<dependency>
  <groupId>com.nthportal</groupId>
  <artifactId>convert_2.12</artifactId>
  <version>0.5.0</version>
</dependency>

Scala 2.11

<dependency>
  <groupId>com.nthportal</groupId>
  <artifactId>convert_2.11</artifactId>
  <version>0.5.0</version>
</dependency>

Usage: As a Client

Suppose you wish to convert the a String to a BigDecimal using the following method:

import com.nthportal.convert.Convert

def parseBigDecimal(s: String)(implicit c: Convert): c.Result[BigDecimal] = ???

If you would like the method to throw an exception if the string does not represent a valid BigDecimal, invoke the method as follows (using "2.718281828" as an example string):

val e: BigDecimal = parseBigDecimal("2.718281828")(Convert.Throwing)

When using Convert.Throwing, the return type c.Result[BigDecimal] in the above method is just BigDecimal.

If you would like the method to return an Option containing the result if the string represents a valid BigDecimal, and None otherwise, invoke the method as follows (again, using "2.718281828" as an example string):

val e: Option[BigDecimal] = parseBigDecimal("2.718281828")(Convert.AsOption)

When using Convert.AsOption, the return type c.Result[BigDecimal] in the above method is Option[BigDecimal].

Alternatively, the Convert with the desired behaviour can be imported as an implicit, as in the following two examples:

import com.nthportal.convert.Convert.Throwing.Implicit.ref

val e: BigDecimal = parseBigDecimal("2.718281828")
import com.nthportal.convert.Convert.AsOption.Implicit.ref

val e: Option[BigDecimal] = parseBigDecimal("2.718281828")

Usage: In a Library

Basics

The core methods of Convert are conversion and fail. Additionally, unwrap is essential when chaining multiple conversions together.

To demonstrate how to use the first two of these methods, let's write a simple method to parse Booleans from Strings

import com.nthportal.convert.Convert

def parseBoolean(s: String)(implicit c: Convert): c.Result[Boolean] = {
  ??? // do parsing here
}

When using Convert, it is imperative that all parsing or conversion operations take place inside a conversion block

import com.nthportal.convert.Convert

def parseBoolean(s: String)(implicit c: Convert): c.Result[Boolean] = {
  c.conversion {
    ??? // do parsing here
  }
}

Now, let us implement the conversion by just checking if the (lowercase) string is equal to "true" or "false"

import com.nthportal.convert.Convert

def parseBoolean(s: String)(implicit c: Convert): c.Result[Boolean] = {
  c.conversion {
    s.toLowerCase match {
      case "true" => true
      case "false" => false
      case _ => ??? // it's not a valid boolean - now what? 
    }
  }
}

The final thing to do for this parsing method is to handle invalid inputs; to 'fail' the conversion. In Java, one would usually throw an exception. With Convert, instead of throwing the exception, you call the method fail with the exception you would have thrown

import com.nthportal.convert.Convert

def parseBoolean(s: String)(implicit c: Convert): c.Result[Boolean] = {
  c.conversion {
    s.toLowerCase match {
      case "true" => true
      case "false" => false
      case _ => c.fail(new IllegalArgumentException(s"$s wasn't 'true' or 'false'"))
    }
  }
}

What if you need to use the result of another conversion inside of yours? Suppose you want to be able to parse a comma-separated pair of Booleans. Let us write a simple method to do so which uses unwrap (note: unwrap MUST be called inside of the conversion block)

import com.nthportal.convert.Convert

def parseBoolean(s: String)(implicit c: Convert): c.Result[Boolean] = ??? // The same as defined above

def parseBooleanPair(s: String)(implicit c: Convert): c.Result[(Boolean, Boolean)] = {
  c.conversion {
    s split ',' match {
      case Array(a, b) => (c.unwrap(parseBoolean(a)), c.unwrap(parseBoolean(b)))
      case _ => c.fail(new IllegalArgumentException("Not a pair"))
    }
  }
}

Now you know how to write a conversion, and how to chain multiple conversions together!

Utility Methods

require

A common method used when parsing is require (defined in Predef). Unfortunately, Predef.require always throws an exception when it fails, which is not desirable if a caller wants an Option back. To solve this, Convert defines two require methods with signatures identical to the signatures of those in Predef. They can be used as drop-in replacements for latter (but MUST be called inside of a conversion block).

For example, let us write a simple method to convert a List of two elements to a Tuple2

import com.nthportal.convert.Convert

def list2Tuple[A](list: List[A])(implicit c: Convert): c.Result[(A, A)] = {
  c.conversion {
    c.require(list.size == 2, "List did not have exactly two elements")
    (list.head, list.tail.head)
  }
}

wrapException

Sometimes you may wish to use a conversion function which always throws exceptions, because you either cannot or do not want to re-implement it. For example, you may not wish to implement integer parsing, but instead use Java's Integer.parseInt. This can be accomplished easily using wrapException (which MUST be called inside of a conversion block) in either of the following ways:

import com.nthportal.convert.Convert

def parseInt1(s: String)(implicit c: Convert): c.Result[Int] = {
  c.conversion {
    c.wrapException[NumberFormatException, Int](Integer.parseInt(s))
  }
}

def parseInt2(s: String)(implicit c: Convert): c.Result[Int] = {
  c.conversion {
    c.wrapException(_.isInstanceOf[NumberFormatException])(Integer.parseInt(s))
  }
}

Synthesizing Conversions

Two functions, one of which throws exceptions, and one of which returns Options, can be synthesized into a single conversion function using Convert.synthesize. For example, suppose the following two methods to parse Booleans from Strings are defined:

def parseBooleanThrowing(s: String): Boolean = ???
def parseBooleanAsOption(s: String): Option[Boolean] = ???

They can be combined into a conversion function with Convert.synthesize

import com.nthportal.convert.Convert

val booleanParser: Convert.Conversion[String, Boolean] =
  Convert.synthesize(parseBooleanThrowing, parseBooleanAsOption)

Automatic Unwrapping (USE WITH CARE)

Sometimes, an excess of calls to Convert.unwrap can reduce code readability. Readability can be improved by importing Convert.AutoUnwrap.autoUnwrap - an implicit conversion from Convert#Result[T] to T. However, use of this implicit conversion is not without risks.

USE THIS IMPLICIT CONVERSION AT YOUR OWN RISK

This implicit conversion can improve code readability; however, it should be used with care. As with all implicit conversions, it may be difficult to tell when it is applied by the compiler, leading to unexpected behavior which is difficult to debug.

Additionally, because unwrap MUST be called within a conversion block, this implicit conversion MUST be called within a conversion block as well. However, if imported in the wrong scope, the compiler may insert a call to this implicit conversion outside of any conversion block.

Use this implicit conversion with care.

If you have decided to use automatic unwrapping, its benefits can be seen by revisiting the parseBooleanPair method from earlier. We originally defined the method as

import com.nthportal.convert.Convert

def parseBoolean(s: String)(implicit c: Convert): c.Result[Boolean] = ??? // The same as defined above

def parseBooleanPair(s: String)(implicit c: Convert): c.Result[(Boolean, Boolean)] = {
  c.conversion {
    s split ',' match {
      case Array(a, b) => (c.unwrap(parseBoolean(a)), c.unwrap(parseBoolean(b)))
      case _ => c.fail(new IllegalArgumentException("Not a pair"))
    }
  }
}

Using AutoUnwrap allows us to tidy it up a little bit

def parseBooleanPair(s: String)(implicit c: Convert): c.Result[(Boolean, Boolean)] = {
  c.conversion {
    import c.AutoUnwrap.autoUnwrap
    s split ',' match {
      case Array(a, b) => (parseBoolean(a), parseBoolean(b))
      case _ => c.fail(new IllegalArgumentException("Not a pair"))
    }
  }
}

AutoUnwrap makes it more clear that we are just returning a tuple from the results of calling parseBoolean on the two elements.