All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zio.test.internal.myers.MyersDiff.scala Maven / Gradle / Ivy

There is a newer version: 2.1.9
Show newest version
/*
 * Copyright 2019-2024 John A. De Goes and the ZIO Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package zio.test.internal.myers

import zio.internal.ansi.AnsiStringOps
import zio.stacktracer.TracingImplicits.disableAutoTrace
import zio.test.ConsoleUtils
import zio.{Chunk, ChunkBuilder}

sealed trait Action[A]

object Action {

  final case class Delete[A](s: A) extends Action[A] {
    override def toString: String = s"-$s"
  }

  final case class Insert[A](s: A) extends Action[A] {
    override def toString: String = s"+$s"
  }

  final case class Keep[A](s: A) extends Action[A] {
    override def toString: String = s.toString
  }
}

final case class DiffResult[A](actions: Chunk[Action[A]]) {
  def applyChanges(original: String): String =
    actions
      .foldLeft((new StringBuilder(original), 0)) { case ((s, index), action) =>
        action match {
          case Action.Delete(_) => (s.deleteCharAt(index), index)
          case Action.Keep(_)   => (s, index + 1)
          case Action.Insert(i) => (s.insert(index, i), index + 1)
        }
      }
      ._1
      .result()

  def invert: DiffResult[A] =
    DiffResult(
      actions.map {
        case Action.Delete(s)  => Action.Insert(s)
        case Action.Insert(s)  => Action.Delete(s)
        case k: Action.Keep[_] => k
      }
    )

  def isIdentical: Boolean = actions.forall {
    case Action.Keep(_) => true
    case _              => false
  }

  def renderLine: String =
    actions.map {
      case Action.Delete(s) => s"-${Console.UNDERLINED}$s".red
      case Action.Insert(s) => s"+${Console.UNDERLINED}$s".green
      case Action.Keep(s)   => s"$s".faint
    }
      .mkString("")

  override def toString: String =
    actions.map {
      case Action.Delete(s) => s"-$s".red
      case Action.Insert(s) => s"+$s".green
      case Action.Keep(s)   => Console.RESET + s" $s".faint
    }
      .mkString("\n")
}

object MyersDiff {
  def diffWords(original: String, modified: String): DiffResult[String] =
    diff(
      Chunk.fromIterable(original.split("\\b")),
      Chunk.fromIterable(modified.split("\\b"))
    )

  def diffChars(original: String, modified: String): DiffResult[String] =
    diff(
      Chunk.fromIterable(original.toList.map(_.toString)),
      Chunk.fromIterable(modified.toList.map(_.toString))
    )

  def diff(original: String, modified: String): DiffResult[String] =
    diff(
      Chunk.fromIterable(original.split("\n")),
      Chunk.fromIterable(modified.split("\n"))
    )

  def diff[A](original: Chunk[A], modified: Chunk[A]): DiffResult[A] = {

    var varOriginal = original
    var varModified = modified
    var longestCommonSubstring: Chunk[A] =
      getLongestCommonSubsequence(original, modified)

    var actions: Chunk[Action[A]] = Chunk.empty

    while (longestCommonSubstring.nonEmpty) {
      val headOfLongestCommonSubstring = longestCommonSubstring(0)
      longestCommonSubstring = longestCommonSubstring.drop(1)

      var headOfOriginal = varOriginal(0)
      var loop           = true

      while (loop) {
        headOfOriginal = varOriginal(0)
        varOriginal = varOriginal.drop(1)
        if (headOfOriginal != headOfLongestCommonSubstring)
          actions = actions :+ Action.Delete(headOfOriginal)

        loop = varOriginal.nonEmpty && headOfOriginal != headOfLongestCommonSubstring
      }

      var headOfModified = varModified(0)
      loop = true

      while (loop) {
        headOfModified = varModified(0)
        varModified = varModified.drop(1)
        if (headOfModified != headOfLongestCommonSubstring)
          actions = actions :+ Action.Insert(headOfModified)

        loop = varModified.nonEmpty && headOfModified != headOfLongestCommonSubstring
      }

      actions = actions :+ Action.Keep(headOfLongestCommonSubstring)
    }

    while (varOriginal.nonEmpty) {
      val headOfOriginal = varOriginal(0)
      varOriginal = varOriginal.drop(1)
      actions = actions :+ Action.Delete(headOfOriginal)
    }

    while (varModified.nonEmpty) {
      val headOfModified = varModified(0)
      varModified = varModified.drop(1)
      actions = actions :+ Action.Insert(headOfModified)
    }

    DiffResult(actions)
  }

  def getLongestCommonSubsequence[A](original: Chunk[A], modified: Chunk[A]): Chunk[A] =
    if (original == null || original.isEmpty || modified == null || modified.isEmpty) Chunk.empty
    else if (original == modified) original
    else {

      val myersMatrix: Array[Array[Int]] = initializeMyersMatrix(original, modified)
      val longestCommonSubsequence       = ChunkBuilder.make[A]()

      var originalPosition = original.length
      var modifiedPosition = modified.length

      var loop = true

      while (loop) {
        if (myersMatrix(originalPosition)(modifiedPosition) == myersMatrix(originalPosition - 1)(modifiedPosition)) {
          originalPosition -= 1
        } else if (
          myersMatrix(originalPosition)(modifiedPosition) == myersMatrix(originalPosition)(modifiedPosition - 1)
        ) {
          modifiedPosition -= 1
        } else {
          longestCommonSubsequence += original(originalPosition - 1)
          originalPosition -= 1
          modifiedPosition -= 1
        }

        loop = originalPosition > 0 && modifiedPosition > 0
      }

      longestCommonSubsequence.result().reverse
    }

  private def initializeMyersMatrix[A](original: Chunk[A], modified: Chunk[A]): Array[Array[Int]] = {
    val originalLength = original.length
    val modifiedLength = modified.length

    val myersMatrix = Array.fill[Int](originalLength + 1, modifiedLength + 1)(0)

    for (i <- 0 until originalLength) {
      for (j <- 0 until modifiedLength) {
        if (original(i) == modified(j)) {
          myersMatrix(i + 1)(j + 1) = myersMatrix(i)(j) + 1
        } else {
          if (myersMatrix(i)(j + 1) >= myersMatrix(i + 1)(j)) {
            myersMatrix(i + 1)(j + 1) = myersMatrix(i)(j + 1)
          } else {
            myersMatrix(i + 1)(j + 1) = myersMatrix(i + 1)(j)
          }
        }
      }
    }

    myersMatrix
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy