fm.common.rich.RichString.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.{Normalize, OptionCache, ThreadLocalHashMap}
import java.io.File
import java.math.{BigDecimal, BigInteger}
import java.text.{DecimalFormat, NumberFormat, ParseException}
import java.util.Locale
import scala.annotation.switch
import scala.util.matching.Regex
object RichString {
def parseBoolean(s: String): Option[Boolean] = {
if (null == s) return None
val lower: String = s.trim.toLowerCase
if (lower == "") return None
lower match {
case "true" | "t" | "yes" | "y" | "1" => OptionCache.valueOf(true)
case "false" | "f" | "no" | "n" | "0" => OptionCache.valueOf(false)
case _ => None
}
}
// NumberFormat.getInstance(locale) is expensive (and not thread-safe) so we need to cache it
private object BigDecimalFormatCache extends ThreadLocalHashMap[Locale,DecimalFormat] {
override protected def initialValue(locale: Locale): Option[DecimalFormat] = {
val bigDecimalFormat: DecimalFormat = NumberFormat.getInstance(locale).asInstanceOf[DecimalFormat]
bigDecimalFormat.setParseBigDecimal(true)
Some(bigDecimalFormat)
}
}
}
final class RichString(val s: String) extends AnyVal {
/**
* Same as String.intern but safe for use when the string is null (i.e. it just returns null)
*/
def internOrNull: String = if (null == s) null else s.intern
/**
* If the string is blank returns None else Some(string)
*/
def toBlankOption: Option[String] = if (new RichCharSequence(s).isNotNullOrBlank) Some(s) else None
/**
* If this string starts with the lead param then return a new string with lead stripped from the start
*
* NOTE: The same functionality is available in Scala's StringOps.stripPrefix
*/
def stripLeading(lead: String): String = if (s.startsWith(lead)) s.substring(lead.length) else s
/**
* If this string ends with the trail param then return a new string with trail stripped from the end
*
* NOTE: The same functionality is available in Scala's StringOps.stripSuffix
*/
def stripTrailing(trail: String): String = if (s.endsWith(trail)) s.substring(0, s.length - trail.length) else s
/**
* If this string does not start with the lead param then return a new string with it added to the start of the string
*
* TODO: is there a better name for this?
*/
def requireLeading(lead: String): String = if (s.startsWith(lead)) s else lead+s
/**
* If this string does not ends with the trail param then return a new string with it added to the end of the string
*
* TODO: is there a better name for this?
*/
def requireTrailing(trail: String): String = if (s.endsWith(trail)) s else s+trail
//
// Scala 2.13 includes implicit toIntOption/toLongOption/etc methods in StringOps which conflicted with these. So
// these have been renamed with a "Cached" suffix to avoid conflicting.
//
def toBooleanOptionCached: Option[Boolean] = try { OptionCache.valueOf(java.lang.Boolean.valueOf(s)) } catch { case _: NumberFormatException => None }
def toByteOptionCached: Option[Byte] = try { OptionCache.valueOf(java.lang.Byte.valueOf(s)) } catch { case _: NumberFormatException => None }
def toShortOptionCached: Option[Short] = try { OptionCache.valueOf(java.lang.Short.valueOf(s)) } catch { case _: NumberFormatException => None }
def toIntOptionCached: Option[Int] = try { OptionCache.valueOf(java.lang.Integer.valueOf(s)) } catch { case _: NumberFormatException => None }
def toLongOptionCached: Option[Long] = try { OptionCache.valueOf(java.lang.Long.valueOf(s)) } catch { case _: NumberFormatException => None }
def toFloatOptionCached: Option[Float] = try { Some(java.lang.Float.valueOf(s)) } catch { case _: NumberFormatException => None }
def toDoubleOptionCached: Option[Double] = try { Some(java.lang.Double.valueOf(s)) } catch { case _: NumberFormatException => None }
def isBoolean: Boolean = toBooleanOptionCached.isDefined
def isByte: Boolean = toByteOptionCached.isDefined
def isShort: Boolean = toShortOptionCached.isDefined
def isInt: Boolean = toIntOptionCached.isDefined
def isLong: Boolean = toLongOptionCached.isDefined
def isFloat: Boolean = toFloatOptionCached.isDefined
def isDouble: Boolean = toDoubleOptionCached.isDefined
def toBigDecimalOption: Option[BigDecimal] = {
try {
if (s == null) None
else Some(new BigDecimal(s))
} catch {
case ex: NumberFormatException => None
}
}
def toBigDecimal: BigDecimal = toBigDecimalOption.getOrElse{ throw new NumberFormatException(s"RichString.toBigDecimal parsing error for value: $s") }
def isBigDecimal: Boolean = toBigDecimalOption.isDefined
def isNotBigDecimal: Boolean = !isBigDecimal
def toBigIntegerOption: Option[BigInteger] = toBigDecimalOption.flatMap{ bd =>
try {
Some(bd.toBigIntegerExact())
} catch {
case ex: ArithmeticException => None
}
}
def toBigInteger: BigInteger = toBigIntegerOption.getOrElse{ throw new NumberFormatException(s"RichString.toBigInteger parsing error on value: $s") }
def parseBigDecimal(implicit locale: Locale): Option[BigDecimal] = if (null == s) None else try {
val res: BigDecimal = RichString.BigDecimalFormatCache(locale).parse(s).asInstanceOf[BigDecimal]
Some(res)
} catch {
case _: ParseException => None
}
/**
* Unlike toBoolean/toBooleanOption/isBoolean this method will
* attempt to parse a boolean value from a string.
*/
def parseBoolean: Option[Boolean] = RichString.parseBoolean(s)
/** A shortcut for "new java.io.File(s)" */
def toFile: File = new File(s)
/**
* Truncate the string to length if it is currently larger than length.
*
* Note: The resulting string will not be longer than length. (i.e the omission counts towards the length)
*
* @param length The length to truncate the string to
* @param omission If the string is truncated then add this to the end (Note: The resulting still will be at most length)
*/
def truncate(length: Int, omission: String = ""): String = {
if (s.length > length) s.substring(0, length-omission.length)+omission else s
}
/** See fm.common.Normalize.lowerAlphaNumeric */
def lowerAlphaNumeric: String = Normalize.lowerAlphanumeric(s)
/** See fm.common.Normalize.lowerAlphaNumericWords */
def lowerAlphaNumericWords: Array[String] = Normalize.lowerAlphaNumericWords(s)
/** See fm.common.Normalize.name */
def urlName: String = Normalize.urlName(s)
/** See org.apache.commons.lang3.text.WordUtils.capitalize */
def capitalizeWords: String = capitalizeWords(null:_*)
/** See org.apache.commons.lang3.text.WordUtils.capitalize */
def capitalizeWords(delimiters: Char*): String = {
val delimLen: Int = if (delimiters == null) -1 else delimiters.length
if (s == null || s.length == 0 || delimLen == 0) return s
val buffer: Array[Char] = s.toCharArray()
var capitalizeNext: Boolean = true
var i: Int = 0
while (i < buffer.length) {
val ch: Char = buffer(i)
if (isDelimiter(ch, delimiters)) {
capitalizeNext = true
} else if (capitalizeNext) {
buffer(i) = ch.toUpper //Character.toTitleCase(ch)
capitalizeNext = false
}
i += 1
}
new String(buffer)
}
/** See org.apache.commons.lang3.text.WordUtils.capitalizeFully */
def capitalizeFully: String = capitalizeFully(null:_*)
/** See org.apache.commons.lang3.text.WordUtils.capitalizeFully */
def capitalizeFully(delimiters: Char*): String = {
val delimLen: Int = if (delimiters == null) -1 else delimiters.length
if (s == null || s.length() == 0 || delimLen == 0) return s
val lower: String = s.toLowerCase()
new RichString(lower).capitalizeWords(delimiters:_*)
}
/** See org.apache.commons.lang3.text.WordUtils.isDelimiter */
private def isDelimiter(ch: Char, delimiters: Seq[Char]): Boolean = {
if (delimiters == null) Character.isWhitespace(ch)
else delimiters.exists{ (delim: Char) => delim == ch }
}
def pad(length: Int, c: Char = ' '): String = rPad(length, c)
def lPad(length: Int, c: Char = ' '): String = {
val target: Int = length - s.length
if (target <= 0) s else repeat(c, target)+s
}
def rPad(length: Int, c: Char = ' '): String = {
val target: Int = length - s.length
if (target <= 0) s else s+repeat(c, target)
}
private def repeat(c: Char, times: Int): String = {
(times: @switch) match {
case 0 => ""
case 1 => String.valueOf(c)
case _ =>
val arr = new Array[Char](times)
java.util.Arrays.fill(arr, c)
new String(arr)
}
}
def replaceAll(regex: Regex, replacement: String): String = regex.replaceAllIn(s, replacement)
def replaceFirst(regex: Regex, replacement: String): String = regex.replaceFirstIn(s, replacement)
def stripAccents: String = Normalize.stripAccents(s)
def toCodePointArray: Array[Int] = {
if (null == s) throw new NullPointerException()
val arr: Array[Int] = new Array(s.codePointCount(0, s.length))
var arrIdx: Int = 0
var strIdx: Int = 0
val len: Int = s.length
while(strIdx < len) {
val ch: Char = s.charAt(strIdx)
if (Character.isHighSurrogate(ch)) {
if (strIdx + 1 < len && Character.isLowSurrogate(s.charAt(strIdx + 1))) {
arr(arrIdx) = Character.toCodePoint(ch, s.charAt(strIdx + 1))
strIdx += 1
} else {
// Output the un-paired high surrogate as-is
arr(arrIdx) = ch
}
} else {
// Output as-is (includes un-paired low surrogates)
arr(arrIdx) = ch
}
strIdx += 1
arrIdx += 1
}
arr
}
def startsWithIgnoreCase(other: String): Boolean = {
if (null == s || null == other) return false
s.regionMatches(true, 0, other, 0, other.length)
}
def endsWithIgnoreCase(other: String): Boolean = {
if (null == s || null == other || s.length < other.length) return false
s.regionMatches(true, s.length - other.length, other, 0, other.length)
}
// /**
// * The plural form of the string
// */
// def plural: String = org.atteo.evo.inflector.English.plural(s)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy