diff --git a/core/src/main/scala/cats/data/Ior.scala b/core/src/main/scala/cats/data/Ior.scala index fc3270b5f0..9149f4de8e 100644 --- a/core/src/main/scala/cats/data/Ior.scala +++ b/core/src/main/scala/cats/data/Ior.scala @@ -169,4 +169,26 @@ sealed trait IorFunctions { def left[A, B](a: A): A Ior B = Ior.Left(a) def right[A, B](b: B): A Ior B = Ior.Right(b) def both[A, B](a: A, b: B): A Ior B = Ior.Both(a, b) + + /** + * Create an `Ior` from two Options if at least one of them is defined. + * + * @param oa an element (optional) for the left side of the `Ior` + * @param ob an element (optional) for the right side of the `Ior` + * + * @return `None` if both `oa` and `ob` are `None`. Otherwise `Some` wrapping + * an [[Ior.Left]], [[Ior.Right]], or [[Ior.Both]] if `oa`, `ob`, or both are + * defined (respectively). + */ + def fromOptions[A, B](oa: Option[A], ob: Option[B]): Option[A Ior B] = + oa match { + case Some(a) => ob match { + case Some(b) => Some(Ior.Both(a, b)) + case None => Some(Ior.Left(a)) + } + case None => ob match { + case Some(b) => Some(Ior.Right(b)) + case None => None + } + } } diff --git a/tests/src/test/scala/cats/tests/IorTests.scala b/tests/src/test/scala/cats/tests/IorTests.scala index 7071272e52..2a5d06ca3f 100644 --- a/tests/src/test/scala/cats/tests/IorTests.scala +++ b/tests/src/test/scala/cats/tests/IorTests.scala @@ -116,4 +116,19 @@ class IorTests extends CatsSuite { i.append(j).right should === (i.right.map(_ + j.right.getOrElse("")).orElse(j.right)) } } + + test("fromOptions left/right consistent with input options"){ + forAll { (oa: Option[String], ob: Option[Int]) => + val x = Ior.fromOptions(oa, ob) + x.flatMap(_.left) should === (oa) + x.flatMap(_.right) should === (ob) + } + } + + test("Option roundtrip"){ + forAll { ior: String Ior Int => + val iorMaybe = Ior.fromOptions(ior.left, ior.right) + iorMaybe should === (Some(ior)) + } + } }