Skip to content

Instantly share code, notes, and snippets.

@Sedose
Last active September 10, 2024 12:32
Show Gist options
  • Save Sedose/a1399ee02968bd5977b7667082a836d1 to your computer and use it in GitHub Desktop.
Save Sedose/a1399ee02968bd5977b7667082a836d1 to your computer and use it in GitHub Desktop.
ChatGPT generated. Working. Scala macro for entities custom comparison logic + Analogs without Macro

Scala Macro for Custom Entity Comparison

This gist contains a Scala 3 macro that compares two entities field by field, automatically skipping any fields where exactly one of the values is null. The comparison logic is generated at compile time using Scala's quoted API, which ensures no runtime reflection overhead.

Features

  • Compare two entities of any type.
  • Skip fields with null values based on predefined business rules.
  • Avoid runtime reflection by generating code at compile time.

Example

The example compares two Address objects:

@main def main(): Unit = {
  val testCases = List(
    (("USA", null, "New York"), ("USA", "NY", null)),                  // true
    ((null, null, null), (null, null, null)),                          // true
    (("USA", null, null), ("USA", null, null)),                        // true
    (("Germany", "Berlin", "Berlin"), ("Germany", "Berlin", null)),    // true
    (("France", "Paris", "Lyon"), ("France", "Paris", "Lyon")),        // true
    (("Canada", null, "Toronto"), ("Canada", "Ontario", "Toronto")),   // true
    ((null, "California", null), (null, "California", "Los Angeles")), // true
    (("Italy", "Rome", null), ("Italy", null, "Rome")),                // true
    (("Japan", "Tokyo", "Shibuya"), ("Japan", "Kyoto", "Osaka")),      // false
    (("Poland", null, "New York"), ("USA", "NY", null)),               // false
  )

  testCases.foreach { case (a, b) =>
    val addrA = Address(a._1, a._2, a._3)
    val addrB = Address(b._1, b._2, b._3)
    println(compareEntity(addrA, addrB))
  }
}

case class Address(country: String, state: String, city: String)
import scala.quoted.*
inline def compareEntity[T](a: T, b: T): Boolean =
${compareEntitiesImpl('a, 'b)}
def compareEntitiesImpl[T: Type](a: Expr[T], b: Expr[T])(using Quotes): Expr[Boolean] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val fields = tpe.typeSymbol.caseFields
val comparisons = fields.map { field =>
val fieldName = field.name
val aField = Select(a.asTerm, field)
val bField = Select(b.asTerm, field)
'{
val aVal = ${aField.asExpr}
val bVal = ${bField.asExpr}
aVal == bVal || aVal == null || bVal == null
}
}
comparisons.reduceLeft((acc, next) => '{ $acc && $next })
}
@main def main(): Unit = {
val testCases = List(
(("USA", null, "New York"), ("USA", "NY", null)), // true
((null, null, null), (null, null, null)), // true
(("USA", null, null), ("USA", null, null)), // true
(("Germany", "Berlin", "Berlin"), ("Germany", "Berlin", null)), // true
(("France", "Paris", "Lyon"), ("France", "Paris", "Lyon")), // true
(("Canada", null, "Toronto"), ("Canada", "Ontario", "Toronto")), // true
((null, "California", null), (null, "California", "Los Angeles")), // true
(("Italy", "Rome", null), ("Italy", null, "Rome")), // true
(("Japan", "Tokyo", "Shibuya"), ("Japan", "Kyoto", "Osaka")), // false
(("Poland", null, "New York"), ("USA", "NY", null)), // false
)
testCases.foreach { case (a, b) =>
val addrA = Address(a._1, a._2, a._3)
val addrB = Address(b._1, b._2, b._3)
println(compareProduct(addrA, addrB))
}
}
def compareProduct[A](a: A, b: A)(using m: Mirror.ProductOf[A]): Boolean = {
a.asInstanceOf[Product].productIterator.zip(b.asInstanceOf[Product].productIterator).forall {
case (aVal, bVal) => aVal == bVal || aVal == null || bVal == null
}
}
case class Address(country: String, state: String, city: String)
@main def main(): Unit = {
val testCases = List(
(("USA", null, "New York"), ("USA", "NY", null)), // true
((null, null, null), (null, null, null)), // true
(("USA", null, null), ("USA", null, null)), // true
(("Germany", "Berlin", "Berlin"), ("Germany", "Berlin", null)), // true
(("France", "Paris", "Lyon"), ("France", "Paris", "Lyon")), // true
(("Canada", null, "Toronto"), ("Canada", "Ontario", "Toronto")), // true
((null, "California", null), (null, "California", "Los Angeles")), // true
(("Italy", "Rome", null), ("Italy", null, "Rome")), // true
(("Japan", "Tokyo", "Shibuya"), ("Japan", "Kyoto", "Osaka")), // false
(("Poland", null, "New York"), ("USA", "NY", null)), // false
)
testCases.foreach { case (a, b) =>
val addrA = Address(a._1, a._2, a._3)
val addrB = Address(b._1, b._2, b._3)
println(compareProduct(addrA, addrB))
}
}
def compareProduct(a: Product, b: Product): Boolean = {
a.productIterator.zip(b.productIterator).forall {
case (aVal, bVal) => aVal == bVal || aVal == null || bVal == null
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment