Created
November 17, 2017 10:56
-
-
Save ubourdon/7f5f7979e2d673801ad276ac8541b6d8 to your computer and use it in GitHub Desktop.
Define newtype in Scala. Example with positive Int.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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