Skip to content

Instantly share code, notes, and snippets.

@ubourdon
Created November 17, 2017 10:56
Show Gist options
  • Select an option

  • Save ubourdon/7f5f7979e2d673801ad276ac8541b6d8 to your computer and use it in GitHub Desktop.

Select an option

Save ubourdon/7f5f7979e2d673801ad276ac8541b6d8 to your computer and use it in GitHub Desktop.
Define newtype in Scala. Example with positive Int.
package domain.common.number
import scalaz.{@@, Show, Tag, \/}
import scalaz.Scalaz.ToEitherOps
package object positiveint {
private[positiveint] sealed trait PositiveIntTag
type PositiveInt = Int @@ PositiveIntTag
sealed trait PositiveIntError
case class InvalidRawValue(error: String) extends PositiveIntError
case class OperationError(error: String) extends PositiveIntError
object PositiveInt {
def apply(rawValue: Int): InvalidRawValue \/ PositiveInt = {
if(rawValue > 0) buildType(rawValue).right
else InvalidRawValue("must be an integer > 0").left
}
/**
* a.k.a unwrap function
* @param i
* @return the unwrapped string value
*/
def rawValue(i: PositiveInt): Int = Tag.unwrap(i)
/**
* Allow to use scalaz Show typeclass
*/
implicit val showPositiveInt: Show[PositiveInt] = Show shows { nes => s"PositiveInt(${Tag.unwrap(nes)})"}
private def buildType(i: Int) = Tag.apply[Int, PositiveIntTag](i)
val ONE = buildType(1)
val TWO = buildType(2)
val THREE = buildType(3)
val FOUR = buildType(4)
val FIVE = buildType(5)
val SIX = buildType(6)
val SEVEN = buildType(7)
val EIGHT = buildType(8)
val NINE = buildType(9)
val TEN = buildType(10)
implicit class PositiveIntOps(i: PositiveInt) {
def +(i2: PositiveInt): PositiveInt = buildType(rawValue(i) + rawValue(i2))
def -(i2: PositiveInt): PositiveIntError \/ PositiveInt = {
val v1 = rawValue(i)
val v2 = rawValue(i2)
apply(v1 - v2).leftMap { _ => OperationError(s"The operation [$v1 - $v2] doesn't produce positive Int result") }
}
def >(i2: PositiveInt): Boolean = rawValue(i) > rawValue(i2)
def <(i2: PositiveInt): Boolean = rawValue(i) < rawValue(i2)
def >=(i2: PositiveInt): Boolean = rawValue(i) >= rawValue(i2)
def <=(i2: PositiveInt): Boolean = rawValue(i) <= rawValue(i2)
def ===(i2: PositiveInt): Boolean = rawValue(i) == rawValue(i2)
def !==(i2: PositiveInt): Boolean = rawValue(i) != rawValue(i2)
def compareTo(i2: PositiveInt): CompareResult = {
if(i === i2) IsEqual
else if(i < i2) IsLower(buildType(rawValue(i2) - rawValue(i)))
else IsHigher(buildType(rawValue(i) - rawValue(i2)))
}
}
sealed trait CompareResult
case class IsHigher(value: PositiveInt) extends CompareResult
case class IsLower(value: PositiveInt) extends CompareResult
case object IsEqual extends CompareResult
}
}
import domain.common.number.positiveint.{InvalidRawValue, OperationError, PositiveInt}
import org.scalatest.{FunSuite, Matchers}
import scalaz.Scalaz.ToEitherOps
import PositiveInt.{IsEqual, IsHigher, IsLower, rawValue}
class PositiveIntTest extends FunSuite with Matchers {
test("PositiveInt should be positive") {
PositiveInt(-1) shouldBe InvalidRawValue("must be an integer > 0").left
PositiveInt(0) shouldBe InvalidRawValue("must be an integer > 0").left
PositiveInt(1).fold(
error => fail(error.error),
positiveint => rawValue(positiveint) shouldBe 1
)
}
test("PositiveInt constants") {
rawValue(PositiveInt.ONE) shouldBe 1
rawValue(PositiveInt.TWO) shouldBe 2
rawValue(PositiveInt.THREE) shouldBe 3
rawValue(PositiveInt.FOUR) shouldBe 4
rawValue(PositiveInt.FIVE) shouldBe 5
rawValue(PositiveInt.SIX) shouldBe 6
rawValue(PositiveInt.SEVEN) shouldBe 7
rawValue(PositiveInt.EIGHT) shouldBe 8
rawValue(PositiveInt.NINE) shouldBe 9
rawValue(PositiveInt.TEN) shouldBe 10
}
test("PositiveInt operations") {
import PositiveInt.PositiveIntOps
PositiveInt.ONE + PositiveInt.TWO shouldBe PositiveInt.THREE
PositiveInt.TWO - PositiveInt.ONE shouldBe PositiveInt.ONE.right
PositiveInt.ONE - PositiveInt.ONE shouldBe OperationError(s"The operation [1 - 1] doesn't produce positive Int result").left
}
test("PositiveInt comparisons") {
import PositiveInt.PositiveIntOps
PositiveInt.TWO > PositiveInt.ONE shouldBe true
PositiveInt.TWO >= PositiveInt.ONE shouldBe true
PositiveInt.TWO >= PositiveInt.TWO shouldBe true
PositiveInt.TWO > PositiveInt.TWO shouldBe false
PositiveInt.TWO >= PositiveInt.THREE shouldBe false
PositiveInt.ONE < PositiveInt.TWO shouldBe true
PositiveInt.ONE <= PositiveInt.TWO shouldBe true
PositiveInt.TWO <= PositiveInt.TWO shouldBe true
PositiveInt.TWO < PositiveInt.TWO shouldBe false
PositiveInt.TWO <= PositiveInt.ONE shouldBe false
PositiveInt.ONE === PositiveInt.ONE shouldBe true
PositiveInt.ONE === PositiveInt.TWO shouldBe false
(PositiveInt.ONE !== PositiveInt.TWO) shouldBe true
(PositiveInt.ONE !== PositiveInt.ONE) shouldBe false
}
test("PositiveInt.compareTo") {
import PositiveInt.PositiveIntOps
PositiveInt.ONE compareTo PositiveInt.ONE shouldBe IsEqual
PositiveInt.ONE compareTo PositiveInt.TWO shouldBe IsLower(PositiveInt.ONE)
PositiveInt.NINE compareTo PositiveInt.SEVEN shouldBe IsHigher(PositiveInt.TWO)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment