Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Approximate element pairing for sets #67

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions modules/core/src/main/scala/difflicious/DiffResult.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,31 @@ import scala.collection.immutable.ListMap

sealed trait DiffResult {

/**
* Whether this DiffResult was produced from an ignored Differ
/** Whether this DiffResult was produced from an ignored Differ
* @return
*/
def isIgnored: Boolean

/**
* Whether this DiffResult is consider "successful".
* If there are any non-ignored differences found, then this should be false
/** Whether this DiffResult is consider "successful". If there are any non-ignored differences found, then this should
* be false
* @return
*/
def isOk: Boolean

/**
* Whether the input leading to this DiffResult has both sides or just one.
/** Whether the input leading to this DiffResult has both sides or just one.
* @return
*/
def pairType: PairType

/** The number of differences found, regardless of if they were ignored or not
* @return
*/
def differenceCount: Int

/** The number of ignored differences
* @return
*/
def ignoredCount: Int
}

object DiffResult {
Expand All @@ -33,6 +40,8 @@ object DiffResult {
pairType: PairType,
isIgnored: Boolean,
isOk: Boolean,
differenceCount: Int,
ignoredCount: Int,
) extends DiffResult

final case class RecordResult(
Expand All @@ -41,6 +50,8 @@ object DiffResult {
pairType: PairType,
isIgnored: Boolean,
isOk: Boolean,
differenceCount: Int,
ignoredCount: Int,
) extends DiffResult

final case class MapResult(
Expand All @@ -49,6 +60,8 @@ object DiffResult {
pairType: PairType,
isIgnored: Boolean,
isOk: Boolean,
differenceCount: Int,
ignoredCount: Int,
) extends DiffResult

object MapResult {
Expand All @@ -64,6 +77,8 @@ object DiffResult {
isIgnored: Boolean,
) extends DiffResult {
override def isOk: Boolean = isIgnored
override def differenceCount: Int = 1
override def ignoredCount: Int = 1
}

sealed trait ValueResult extends DiffResult
Expand All @@ -72,14 +87,20 @@ object DiffResult {
final case class Both(obtained: String, expected: String, isSame: Boolean, isIgnored: Boolean) extends ValueResult {
override def pairType: PairType = PairType.Both
override def isOk: Boolean = isIgnored || isSame
override def differenceCount: Int = if (isSame) 0 else 1
override def ignoredCount: Int = if (!isSame && isIgnored) 1 else 0
}
final case class ObtainedOnly(obtained: String, isIgnored: Boolean) extends ValueResult {
override def pairType: PairType = PairType.ObtainedOnly
override def isOk: Boolean = false
override def differenceCount: Int = 1
override def ignoredCount: Int = if (isIgnored) 1 else 0
}
final case class ExpectedOnly(expected: String, isIgnored: Boolean) extends ValueResult {
override def pairType: PairType = PairType.ExpectedOnly
override def isOk: Boolean = false
override def differenceCount: Int = 1
override def ignoredCount: Int = if (isIgnored) 1 else 0
}
}

Expand Down
38 changes: 38 additions & 0 deletions modules/core/src/main/scala/difflicious/PairingFn.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package difflicious

sealed trait PairingFn[A, B] {
def fn: A => B
def matching(a1: A, a2: A)(differ: Differ[A]): Boolean
}

object PairingFn {
def lift[A, B](fn: A => B): PairingFn[A, B] = UsingEquals(fn)

def approximate[A](threshold: Int): PairingFn[A, A] =
Approximate(identity, differenceCountThreshold = threshold)

case class UsingEquals[A, B](fn: A => B) extends PairingFn[A, B] {
override def matching(a1: A, a2: A)(differ: Differ[A]): Boolean = fn(a1) == fn(a2)
}

sealed trait DifferBased[A, B] extends PairingFn[A, B] {
def fn: A => B
def differenceCountThreshold: Int
}

case class Approximate[A](fn: A => A, differenceCountThreshold: Int) extends DifferBased[A, A] {
override def matching(a1: A, a2: A)(differ: Differ[A]): Boolean = {
val diffResult = differ.diff(fn(a1), fn(a2))
spavikevik marked this conversation as resolved.
Show resolved Hide resolved

diffResult.differenceCount - diffResult.ignoredCount <= differenceCountThreshold
}
}

case class Custom[A, B](fn: A => B, differenceCountThreshold: Int, pairDiffer: Differ[B]) extends DifferBased[A, B] {
override def matching(a1: A, a2: A)(differ: Differ[A]): Boolean = {
val diffResult = pairDiffer.diff(fn(a1), fn(a2))

diffResult.differenceCount - diffResult.ignoredCount <= differenceCountThreshold
}
}
}
69 changes: 37 additions & 32 deletions modules/core/src/main/scala/difflicious/differ/MapDiffer.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package difflicious.differ

import difflicious.DiffResult.{ValueResult, MapResult}
import difflicious.DiffResult.{MapResult, ValueResult}

import scala.collection.mutable
import difflicious.ConfigureOp.PairBy
import difflicious.differ.MapDiffer.mapKeyToString
import difflicious.internal.SumCountsSyntax.DiffResultIterableOps
import difflicious.utils.TypeName.SomeTypeName
import difflicious.{Differ, DiffResult, ConfigureOp, ConfigureError, ConfigurePath, DiffInput, PairType}
import difflicious.{ConfigureError, ConfigureOp, ConfigurePath, DiffInput, DiffResult, Differ, PairType}
import difflicious.utils.MapLike

class MapDiffer[M[_, _], K, V](
Expand All @@ -23,60 +24,64 @@ class MapDiffer[M[_, _], K, V](
val obtainedOnly = mutable.ArrayBuffer.empty[MapResult.Entry]
val both = mutable.ArrayBuffer.empty[MapResult.Entry]
val expectedOnly = mutable.ArrayBuffer.empty[MapResult.Entry]
obtained.foreach {
case (k, actualV) =>
expected.get(k) match {
case Some(expectedV) =>
both += MapResult.Entry(
mapKeyToString(k, keyDiffer),
valueDiffer.diff(actualV, expectedV),
)
case None =>
obtainedOnly += MapResult.Entry(
mapKeyToString(k, keyDiffer),
valueDiffer.diff(DiffInput.ObtainedOnly(actualV)),
)
}
}
expected.foreach {
case (k, expectedV) =>
if (obtained.contains(k)) {
// Do nothing, already compared when iterating through obtained
} else {
expectedOnly += MapResult.Entry(
obtained.foreach { case (k, actualV) =>
expected.get(k) match {
case Some(expectedV) =>
both += MapResult.Entry(
mapKeyToString(k, keyDiffer),
valueDiffer.diff(actualV, expectedV),
)
case None =>
obtainedOnly += MapResult.Entry(
mapKeyToString(k, keyDiffer),
valueDiffer.diff(DiffInput.ExpectedOnly(expectedV)),
valueDiffer.diff(DiffInput.ObtainedOnly(actualV)),
)
}
}
}
expected.foreach { case (k, expectedV) =>
if (obtained.contains(k)) {
// Do nothing, already compared when iterating through obtained
} else {
expectedOnly += MapResult.Entry(
mapKeyToString(k, keyDiffer),
valueDiffer.diff(DiffInput.ExpectedOnly(expectedV)),
)
}
}

val bothValues = both.map(_.value)
MapResult(
typeName = typeName,
(obtainedOnly ++ both ++ expectedOnly).toVector,
PairType.Both,
isIgnored = isIgnored,
isOk = isIgnored || obtainedOnly.isEmpty && expectedOnly.isEmpty && both.forall(_.value.isOk),
isOk = isIgnored || obtainedOnly.isEmpty && expectedOnly.isEmpty && bothValues.forall(_.isOk),
differenceCount = bothValues.differenceCount,
ignoredCount = bothValues.ignoredCount,
)
case DiffInput.ObtainedOnly(obtained) =>
DiffResult.MapResult(
typeName = typeName,
entries = obtained.map {
case (k, v) =>
MapResult.Entry(mapKeyToString(k, keyDiffer), valueDiffer.diff(DiffInput.ObtainedOnly(v)))
entries = obtained.map { case (k, v) =>
MapResult.Entry(mapKeyToString(k, keyDiffer), valueDiffer.diff(DiffInput.ObtainedOnly(v)))
}.toVector,
pairType = PairType.ObtainedOnly,
isIgnored = isIgnored,
isOk = isIgnored,
differenceCount = obtained.size,
ignoredCount = if (isIgnored) obtained.size else 0,
)
case DiffInput.ExpectedOnly(expected) =>
DiffResult.MapResult(
typeName = typeName,
entries = expected.map {
case (k, v) =>
MapResult.Entry(mapKeyToString(k, keyDiffer), valueDiffer.diff(DiffInput.ExpectedOnly(v)))
entries = expected.map { case (k, v) =>
MapResult.Entry(mapKeyToString(k, keyDiffer), valueDiffer.diff(DiffInput.ExpectedOnly(v)))
}.toVector,
pairType = PairType.ExpectedOnly,
isIgnored = isIgnored,
isOk = isIgnored,
differenceCount = expected.size,
ignoredCount = if (isIgnored) expected.size else 0,
)
}

Expand Down
52 changes: 28 additions & 24 deletions modules/core/src/main/scala/difflicious/differ/RecordDiffer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package difflicious.differ

import scala.collection.immutable.ListMap
import difflicious._
import difflicious.internal.SumCountsSyntax.DiffResultIterableOps
import difflicious.utils.TypeName.SomeTypeName

/**
* A differ for a record-like data structure such as tuple or case classes.
/** A differ for a record-like data structure such as tuple or case classes.
*/
final class RecordDiffer[T](
fieldDiffers: ListMap[String, (T => Any, Differ[Any])],
Expand All @@ -17,29 +17,31 @@ final class RecordDiffer[T](
override def diff(inputs: DiffInput[T]): R = inputs match {
case DiffInput.Both(obtained, expected) => {
val diffResults = fieldDiffers
.map {
case (fieldName, (getter, differ)) =>
val diffResult = differ.diff(getter(obtained), getter(expected))
.map { case (fieldName, (getter, differ)) =>
val diffResult = differ.diff(getter(obtained), getter(expected))

fieldName -> diffResult
fieldName -> diffResult
}
.to(ListMap)

val diffResultValues = diffResults.values
DiffResult
.RecordResult(
typeName = typeName,
fields = diffResults,
pairType = PairType.Both,
isIgnored = isIgnored,
isOk = isIgnored || diffResults.values.forall(_.isOk),
isOk = isIgnored || diffResultValues.forall(_.isOk),
differenceCount = diffResultValues.differenceCount,
ignoredCount = diffResultValues.ignoredCount,
)
}
case DiffInput.ObtainedOnly(value) => {
val diffResults = fieldDiffers
.map {
case (fieldName, (getter, differ)) =>
val diffResult = differ.diff(DiffInput.ObtainedOnly(getter(value)))
.map { case (fieldName, (getter, differ)) =>
val diffResult = differ.diff(DiffInput.ObtainedOnly(getter(value)))

fieldName -> diffResult
fieldName -> diffResult
}
.to(ListMap)
DiffResult
Expand All @@ -49,15 +51,16 @@ final class RecordDiffer[T](
pairType = PairType.ObtainedOnly,
isIgnored = isIgnored,
isOk = isIgnored,
differenceCount = diffResults.values.size,
ignoredCount = if (isIgnored) diffResults.values.size else 0,
)
}
case DiffInput.ExpectedOnly(expected) => {
val diffResults = fieldDiffers
.map {
case (fieldName, (getter, differ)) =>
val diffResult = differ.diff(DiffInput.ExpectedOnly(getter(expected)))
.map { case (fieldName, (getter, differ)) =>
val diffResult = differ.diff(DiffInput.ExpectedOnly(getter(expected)))

fieldName -> diffResult
fieldName -> diffResult
}
.to(ListMap)
DiffResult
Expand All @@ -67,6 +70,8 @@ final class RecordDiffer[T](
pairType = PairType.ExpectedOnly,
isIgnored = isIgnored,
isOk = isIgnored,
differenceCount = diffResults.values.size,
ignoredCount = if (isIgnored) diffResults.values.size else 0,
)
}
}
Expand All @@ -82,15 +87,14 @@ final class RecordDiffer[T](
fieldDiffers
.get(step)
.toRight(ConfigureError.NonExistentField(nextPath, "RecordDiffer"))
.flatMap {
case (getter, fieldDiffer) =>
fieldDiffer.configureRaw(nextPath, op).map { newFieldDiffer =>
new RecordDiffer[T](
fieldDiffers = fieldDiffers.updated(step, (getter, newFieldDiffer)),
isIgnored = isIgnored,
typeName = typeName,
)
}
.flatMap { case (getter, fieldDiffer) =>
fieldDiffer.configureRaw(nextPath, op).map { newFieldDiffer =>
new RecordDiffer[T](
fieldDiffers = fieldDiffers.updated(step, (getter, newFieldDiffer)),
isIgnored = isIgnored,
typeName = typeName,
)
}
}
override def configurePairBy(path: ConfigurePath, op: ConfigureOp.PairBy[_]): Either[ConfigureError, Differ[T]] =
Left(ConfigureError.InvalidConfigureOp(path, op, "RecordDiffer"))
Expand Down
Loading
Loading