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

sangria.util.StringUtil.scala Maven / Gradle / Ivy

The newest version!
package sangria.util

import sangria.since3_0_0

import java.util.Locale
import scala.collection.AbstractIterator
import scala.collection.mutable.ListBuffer

object StringUtil {
  private val camelToUpper = "_*([A-Z][a-z\\d]+)".r

  def camelCaseToUnderscore(name: String): String =
    camelToUpper.findAllMatchIn(name).map(_.group(1).toLowerCase).mkString("_")

  def orList(items: Seq[String], limit: Int = 5): String =
    if (items.isEmpty)
      throw new IllegalArgumentException("List is empty")
    else {
      val limited = items.take(limit)
      val start = limited.dropRight(1)
      val last = limited.last

      if (start.nonEmpty) s"${start.mkString(", ")} or $last"
      else last
    }

  /** Given [ A, B, C ] return '"A", "B" or "C"'.
    */
  def quotedOrList(items: Seq[String], limit: Int = 5): String =
    orList(items.map("'" + _ + "'"))

  /** Given an invalid input string and a list of valid options, returns a filtered list of valid
    * options sorted based on their similarity with the input.
    */
  def suggestionList(input: String, options: Seq[String]): Seq[String] = {
    val inputLength = input.length
    val inputThreshold = math.max(inputLength / 2, 1)

    options.iterator
      // filter out options where the length is too different to avoid computing the lexical distance
      .filter { opt =>
        val length = opt.length
        length >= inputLength - inputThreshold && length <= inputLength + inputThreshold
      }
      .map(opt => opt -> lexicalDistance(input, opt))
      .filter(opt => opt._2 <= math.max(inputThreshold, opt._1.length / 2))
      .toList
      .sortBy(_._2)
      .map(_._1)
  }

  /** Computes the lexical distance between strings A and B.
    *
    * The "distance" between two strings is given by counting the minimum number of edits needed to
    * transform string A into string B. An edit can be an insertion, deletion, or substitution of a
    * single character, or a swap of two adjacent characters.
    *
    * This distance can be useful for detecting typos in input or sorting
    *
    * @return
    *   distance in number of edits
    */
  def lexicalDistance(a: String, b: String): Int = {
    val d = for (i <- 0 to a.length) yield ListBuffer.fill(b.length + 1)(i)

    for (j <- 1 to b.length)
      d(0)(j) = j

    for {
      i <- 1 to a.length
      j <- 1 to b.length
    } {
      val cost = if (a(i - 1) == b(j - 1)) 0 else 1

      d(i)(j) = math.min(math.min(d(i - 1)(j) + 1, d(i)(j - 1) + 1), d(i - 1)(j - 1) + cost)

      if (i > 1 && j > 1 && a(i - 1) == b(j - 2) && a(i - 2) == b(j - 1)) {
        d(i)(j) = math.min(d(i)(j), d(i - 2)(j - 2) + cost)
      }
    }

    d(a.length)(b.length)
  }

  def escapeBlockString(str: String): String = str.replaceAll("\"\"\"", "\\\\\"\"\"")

  def escapeString(str: String): String =
    str
      .foldLeft(new StringBuilder) { (builder, e) =>
        e match {
          case ch if ch > 0xfff => builder.append("\\u").append(charHex(ch))
          case ch if ch > 0xff => builder.append("\\u0").append(charHex(ch))
          case ch if ch > 0x7f => builder.append("\\u00").append(charHex(ch))
          case ch if ch < 32 =>
            ch match {
              case '\b' => builder.append("\\b")
              case '\n' => builder.append("\\n")
              case '\t' => builder.append("\\t")
              case '\f' => builder.append("\\f")
              case '\r' => builder.append("\\r")
              case ch if ch > 0xf => builder.append("\\u00").append(charHex(ch))
              case ch => builder.append("\\u000").append(charHex(ch))
            }
          case '"' => builder.append("\\\"")
          case '\\' => builder.append("\\\\")
          case ch => builder.append(ch.toString)
        }
      }
      .result()

  def charHex(ch: Char): String =
    Integer.toHexString(ch).toUpperCase(Locale.ENGLISH)

  // Redefine `linesIterator`, since the implementation provided by Scala is not consistent for our
  // cross-compiled versions
  def linesIterator(string: String): Iterator[String] = new AbstractIterator[String] {
    def hasNext: Boolean = !done
    def next(): String = if (done) Iterator.empty.next() else advance()

    private def isLineBreak(c: Char) = c == '\r' || c == '\n'
    private def isWindowsLineBreak(c1: Char, c2: Char) = c1 == '\r' && c2 == '\n'
    private[this] val len = string.length
    private[this] var index = 0
    @`inline` private def done: Boolean = index >= len
    private def advance(): String = {
      val start = index
      while (!done && !isLineBreak(string.charAt(index))) index += 1
      val end = index
      if (!done) {
        val c = string.charAt(index)
        index += 1
        if (!done && isWindowsLineBreak(c, string.charAt(index))) index += 1
      }
      string.substring(start, end)
    }
  }

  /** Produces the value of a block string from its parsed raw value, similar to Coffeescript's
    * block string, Python's docstring trim or Ruby's strip_heredoc.
    *
    * This implements the GraphQL spec's BlockStringValue() static algorithm.
    */
  @deprecated("This method will be removed.", since3_0_0)
  def blockStringValue(rawString: String): String = {
    val lines = rawString.split("""\r\n|[\n\r]""")
    val lineSizes = lines.map(l => l -> leadingWhitespace(l))
    val commonIndentLines =
      lineSizes.drop(1).collect { case (line, size) if size != line.length => size }
    val strippedLines =
      if (commonIndentLines.nonEmpty) {
        val commonIndent = commonIndentLines.min

        lines.take(1) ++ lines.drop(1).map(_.drop(commonIndent))
      } else lines
    val trimmedLines = strippedLines.reverse.dropWhile(isBlank).reverse.dropWhile(isBlank)

    trimmedLines.mkString("\n")
  }

  private def leadingWhitespace(str: String) = {
    var i = 0

    while (i < str.length && (str.charAt(i) == ' ' || str.charAt(i) == '\t')) i += 1

    i
  }

  private def isBlank(str: String) = leadingWhitespace(str) == str.length
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy