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

fm.common.rich.RichCharSequence.scala Maven / Gradle / Ivy

/*
 * Copyright 2014 Frugal Mechanic (http://frugalmechanic.com)
 *
 * 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 fm.common.rich

import fm.common.{ASCIIUtil, ImmutableArray, ImmutableArrayBuilder}

/**
 * Provides additional functionality for java.lang.CharSequence
 */
final class RichCharSequence(val s: CharSequence) extends AnyVal {

  /**
   * Returns true if the string is null or only whitespace
   *
   */
  def isNullOrBlank: Boolean = {
    if (null == s) return true
    
    var i: Int = 0
    val len: Int = s.length()
    while (i < len) {
      if (!Character.isWhitespace(s.charAt(i))) return false 
      i += 1
    }
    
    true
  }

  /**
   * Opposite of isBlank
   */
  def isNotNullOrBlank: Boolean = !isNullOrBlank
  
  /**
   * Opposite of isBlank (alias for isNotBlank)
   */
  def nonNullOrBlank: Boolean = !isNullOrBlank

  /**
   * Do the next characters starting at idx match the target
   */
  def nextCharsMatch(target: CharSequence, idx: Int = 0): Boolean = {
    if (idx < 0) throw new IllegalArgumentException(s"RichSequence.nextCharsMatch - Negative Idx: $idx")
    if (null == target || target.length == 0) return false

    var i: Int = 0
    while (i < target.length && i+idx < s.length && target.charAt(i) == s.charAt(i+idx)) {
      i += 1
    }

    i == target.length
  }

  /**
   * Same as String.startsWith(prefix) but for a CharSequence
   */
  def startsWith(target: CharSequence): Boolean = nextCharsMatch(target)
  
  /**
   * Count the occurrences of the character
   */
  def countOccurrences(ch: Char): Int = {
    var count: Int = 0
    var i: Int = 0
    
    while (i < s.length) {
      if (s.charAt(i) == ch) count += 1
      i += 1
    }
    
    count
  }
  
  def indexesOf(target: CharSequence, withOverlaps: Boolean): IndexedSeq[Int] = indexesOf(target, 0, withOverlaps)
  
  def indexesOf(target: CharSequence, fromIdx: Int, withOverlaps: Boolean): IndexedSeq[Int] = {
    if (target == null) return ImmutableArray.empty[Int]
    
    val builder = new ImmutableArrayBuilder[Int](0)
    
    var i: Int = fromIdx
    
    while (i < s.length) {
      if (nextCharsMatch(target, i)) {
        builder += i
        i += (if (withOverlaps) 1 else target.length)
      } else {
        i += 1
      }
    }
   
    builder.result()
  }
  
  def matches(pattern: java.util.regex.Pattern): Boolean = pattern.matcher(s).matches()
  def matches(regex: scala.util.matching.Regex): Boolean = regex.pattern.matcher(s).matches()

  def containsNormalized(target: CharSequence): Boolean = {
    containsWithTransform(target, Character.isLetterOrDigit(_), (c: Char) => Character.toLowerCase(ASCIIUtil.toASCIIChar(c)))
  }

  def containsIgnoreCase(target: CharSequence): Boolean = {
    containsWithTransform(target, (_: Char) => true, (c: Char) => Character.toLowerCase(c))
  }

  @inline def containsWithTransform(target: CharSequence, filter: Char => Boolean, map: Char => Char): Boolean = {
    indexOfWithTransform(target, filter, map) > -1
  }

  def indexOfNormalized(target: CharSequence): Int = {
    indexOfWithTransform(target, Character.isLetterOrDigit(_), (c: Char) => Character.toLowerCase(ASCIIUtil.toASCIIChar(c)))
  }

  def indexOfIgnoreCase(target: CharSequence): Int = {
    indexOfWithTransform(target, (_: Char) => true, (c: Char) => Character.toLowerCase(c))
  }

  @inline def indexOfWithTransform(target: CharSequence, filter: Char => Boolean, map: Char => Char): Int = {
    if (null == s || null == target) return -1
    if (target.length == 0) return 0

    //
    // Skip past any chars that we do not care about in the target
    //
    var targetStartingIdx: Int = 0
    while (targetStartingIdx < target.length && !filter(target.charAt(targetStartingIdx))) targetStartingIdx += 1

    //
    // Loop over the source and check for matches of the target
    //
    var sourceIdx: Int = 0

    while (sourceIdx < s.length) {
      // Only check starting from this index if the char is included in our filter (so we have an accurate startIdx
      // that can be returned from indexOfWithTransformImpl)
      if (filter(s.charAt(sourceIdx))) {
        val res: Int = indexOfWithTransformImpl(target, targetStartingIdx, sourceIdx, filter, map)
        if (-1 != res) return res
      }

      sourceIdx += 1
    }

    -1
  }

  @inline private def indexOfWithTransformImpl(target: CharSequence, targetStartIdx: Int, sourceStartIdx: Int, filter: Char => Boolean, map: Char => Char): Int = {
    var targetIdx: Int = targetStartIdx
    var sourceIdx: Int = sourceStartIdx

    // Was originally a "do  while " which was removed in Scala 3 so this was converted to the recommended
    // "while ({  ;  }) ()" equivalent: https://docs.scala-lang.org/scala3/reference/dropped-features/do-while.html
    while ({
      //
      // 1 - Check if the chars match.
      //
      // Note: At this point we have already filtered past bad chars either in the indexOfWithTransform method
      //       or via the code below.
      //
      val sourceChar: Char = map(s.charAt(sourceIdx))
      val targetChar: Char = map(target.charAt(targetIdx))

      if (sourceChar != targetChar) return -1

      //
      // 2 - Advance our indexes
      //
      targetIdx += 1
      sourceIdx += 1

      //
      // 3 - Skip past any chars we do not care about
      //
      while (targetIdx < target.length && !filter(target.charAt(targetIdx))) targetIdx += 1
      while (sourceIdx < s.length && !filter(s.charAt(sourceIdx))) sourceIdx += 1

      targetIdx < target.length && sourceIdx < s.length
    }) ()

    // If we have reached the end of the target then we have matches everything
    if (targetIdx == target.length) sourceStartIdx else -1
  }

  def equalsNormalized(target: CharSequence): Boolean = {
    equalsWithTransform(target, Character.isLetterOrDigit(_), (c: Char) => Character.toLowerCase(ASCIIUtil.toASCIIChar(c)))
  }

  @inline def equalsWithTransform(target: CharSequence, filter: Char => Boolean, map: Char => Char): Boolean = {
    if (null == s || null == target) return false

    var sourceIdx: Int = 0
    var targetIdx: Int = 0

    while (sourceIdx < s.length && targetIdx < target.length) {
      //
      // 1 - Skip past any chars we do not care about
      //
      while (targetIdx < target.length && !filter(target.charAt(targetIdx))) targetIdx += 1
      while (sourceIdx < s.length && !filter(s.charAt(sourceIdx))) sourceIdx += 1

      if (sourceIdx < s.length && targetIdx < target.length) {
        //
        // 2 - Check if the chars match
        //
        val sourceChar: Char = map(s.charAt(sourceIdx))
        val targetChar: Char = map(target.charAt(targetIdx))

        if (sourceChar != targetChar) return false

        //
        // 3 - Advance our indexes
        //
        targetIdx += 1
        sourceIdx += 1
      }
    }

    // Skip over any remaining chars that should be filtered out
    while (targetIdx < target.length && !filter(target.charAt(targetIdx))) targetIdx += 1
    while (sourceIdx < s.length && !filter(s.charAt(sourceIdx))) sourceIdx += 1

    // It is a match if we are at the end of both strings
    sourceIdx == s.length && targetIdx == target.length
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy