/* Copyright 2010 Aaron J. Radke 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 cc.drx import java.nio.charset.StandardCharsets.UTF_8 final class DrxRegex(val regex:Regex) extends AnyVal{ def existsMatch(string:String):Boolean = regex.findFirstMatchIn(string).isDefined def matchesFully(string:String):Boolean = regex.unapplySeq(string).isDefined def ignoreCase = new Regex("(?i)" + regex.regex) } object DrxRegex{ val whitespace = """\s+""".r } trait Opposite[A,B]{ def oppositeOf(orig:A):Option[B] } object Opposite{ trait OppositeWords extends Opposite[String,String]{ protected def words:Iterable[Array[String]] private lazy val wordMap:Map[String,String] = { val pairs = words.filter{_.size == 2} def alt(f:String=>String) = pairs.map(_ map f) val alts = alt(_.toLowerCase) ++ alt(_.toUpperCase) ++ alt(_.toLowerCase.capitalize) (alts ++ alts.map(_.reverse)).map{case Array(a,b) => a -> b}.toMap } private lazy val sortedKeys = wordMap.keys.toList.sortBy(-_.size) def oppositeOf(orig:String):Option[String] = { // Log(orig,wordMap) wordMap.get(orig) orElse sortedKeys.find{orig contains _}.map{k => orig.replace(k, wordMap(k) ) } } } implicit object OppositeCommonWords extends OppositeWords{ protected lazy val words:Iterable[Array[String]] = """ |true false|on off|yes no |start end|min max|begin finish|head tail|first last |some none|many few |good bad|great poor|awesome terrible|easy difficult|simple complex |fast slow|hot cold|warm cool|large small|big tiny|deep shallow|heavy light|soft hard |left right|top bottom|north south|east west|high low|hi low|in out|inside outside |light dark|brighter darker|new old|young old|strong weak|loud quiet |white black|green red """.split("""\s*\|\s*""").map{_ split ' ' map (_.trim)} } } trait Phonetic{ def phoneticTerm(str:String):String final def phonetic(str:String):String = str.splitTerms.map{phoneticTerm}.mkString(" ") //spaces are not required for soundex but others may } object Phonetic{ implicit val defaultPhonetic:Phonetic = Soundex } // https://www.wikiwand.com/en/Soundex object Soundex extends Phonetic{ def apply(str:String) = phonetic(str) private val codes = "BFPV CGJKQSXZ DT L MN R".split(' ') //char codes for: 1 2 3 4 5 6 vowels and HW intentionally undefined private def code(c:Char):Option[Int] = codes.someIndexWhere(_ contains c).map{_+1} final def phoneticTerm(term:String):String = { val uTerm = term.toUpperCase uTerm.toList.drop(1).flatMap(code).filterDuplicates.mkString(uTerm.take(1),"","0000").take(4) } } final class SymbolStringContext(val sc:StringContext) extends AnyVal{ def symbol(args:Any*):Symbol = Symbol(sc.s(args:_*)) //alternate way to construct a symbol since Symbols are deprecated in dotty } final class DrxString(val string:String) extends AnyVal{ //MOVED def ~(vec:Vec):Text = Text(string,vec) //moved as an implicit brought in through style because style has some of these already def words = (string.trim split """\s+""").toIndexedSeq //make it return an immutable structure def wordMap[B](f:String=>B) = (words.iterator zipWith f).toMap def removeVowels = string filterNot {Data.Vowels contains _} def levenshtein(that:String):Int = Alg.levenshtein(this.string, that) def lcp(that:String):String = (string.toIterable lcp that.toIterable).mkString def longestCommonSuffix(that:String):String = (string.toSeq longestCommonSuffix that.toSeq).mkString def singularize(str:String):String = if((str.size > 1) && (str endsWith "s")) str.dropRight(1) else str //def decapitalize = java.beans.Introspector.decapitalize(string) def decapitalize = string.head.toString.toLowerCase + string.tail def overlay(that:String):String = this.string zip that map {case (a,b) => if(a == ' ') b else a} mkString "" def base(radix:Int):Long = (radix <= 36).getOrElse(string.toLowerCase, string).base(Data.Radix36 take radix) def base(charset:IndexedSeq[Char]):Long = string.reverse.zipWithIndex.foldLeft(0L){ case (x,(c,i)) => x + charset.indexOf(c)*(charset.size ** i).toLong } def toOption:Option[String] = if(string.isEmpty) None else Some(string) /**assuming utf8 now that this is nearly standard*/ def toBytes:Array[Byte] = string.getBytes(UTF_8) /**assuming utf8 now that this is nearly standard*/ def toByteBuffer:java.nio.ByteBuffer = java.nio.ByteBuffer.wrap(string.getBytes(UTF_8)) def camelCase = { string.head +: (string zip string.tail).flatMap{ case ('_','_') => None case ('_',c) => Some(c.toString.toUpperCase) case (_,'_') => None case (_,c) => Some(c) } }.mkString //regex pat from stackoverflow https://stackoverflow.com/a/7594052/622016 //TODO don't use a regex on this use a some stream process, it is a simple problem and can be made more general def splitTerms:Array[String] = string.split("""[^a-zA-Z0-9]+""").flatMap{_ split """(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[0-9])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=[0-9])"""}.filter(_.nonEmpty) //string split """(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])""" //def toInputStream:java.io.InputStream = File.Normal.is(string) /**replace spaces with underscores*/ def underscore:String = DrxRegex.whitespace.replaceAllIn(string.trim, "_") def truncate(len:Int=70,postfix:String="...") = if(string.size <= len) string else { string.take(len-postfix.size) + postfix } def indent:String = indent(" ") //default indent is two spaces def indent(margin:String):String = string.split("\n").map{margin + _}.mkString("\n") def undent(margin:String):String = string.split("\n").map{s => if(s startsWith margin) s.drop(margin.size) else s}.mkString("\n") def undent:String = string.split("\n").map{_.unpadHead}.mkString("\n") def takeRightWhile(p:Char => Boolean):String = string.reverse.takeWhile(p).reverse def dropRightWhile(p:Char => Boolean):String = string.reverse.dropWhile(p).reverse def unpadHead = string.dropWhile(_.isWhitespace) /**a better `trim`, the java.lang.St strips out to much (including ansicodes)*/ def unpad = unpadHead.reverse.unpadHead.reverse import Style.{Left,Right,Center,AlignHorizontal} def fit(n:Int,align:AlignHorizontal=Left,padChar:String=" "):String = { val trimmed = unpad val displayedWidth = Ansi.strip(trimmed).size //don't count (zero print width) ansi codes val delta = n - displayedWidth if(delta < 0) align match { case Left => trimmed.take(n) case Right => trimmed.takeRight(n) case Center => val j = -delta/2; trimmed.drop(j).dropRight(j + delta.isOdd.getOrElse(1,0)) } else align match { case Left => trimmed + padChar*delta case Right => padChar*delta + trimmed case Center => padChar*(delta/2) + trimmed + padChar*(delta/2) + (if(delta.isOdd) padChar else "") } } //--wrap help def quote:String = "\""+ string + "\"" //because escaping is so onerous def unQuote:String = string.dropWhile(_ == '"').dropRightWhile(_ == '"') def wrap(pre:String):String = wrap(pre, (pre map Data.Brackets.apply _).reverse) def wrap(pre:String,post:String):String = pre + string + post def unWrap(pre:String,post:String):String = ??? //--common html tag help def tag(c:Color):String = string.tag("span", "style"-> s"color:${c.cssHex}" ) def tag(tag:String,attrs:(String,String)*):String = { val tags = tag.split("""\.""") val classes = tags.drop(1) val baseTag = if(tags.head == "") "div" else tags.head val attrMap = if(classes.isEmpty) attrs.toMap else attrs.toMap + ("class" -> classes.mkString(" ")) if(attrMap.isEmpty) s"<$baseTag>$string</$baseTag>" else{ val attrString = attrMap.map{case (k,v) => s"""$k="$v""""}.mkString(" ") s"<$baseTag $attrString>$string</$baseTag>" } } def href(path:String):String = string.tag("a", "href" -> path) def escapeXML:String = string.replace("&","&") .replace("<","<") .replace(">",">") //--containment tests def doesNotContain(x:String):Boolean = !string.contains(x) def doesNotStartWith(x:String):Boolean = !string.startsWith(x) def doesNotEndWith(x:String):Boolean = !string.endsWith(x) def containsSubsequence(x:String) = string.toSeq containsSubsequence x.toSeq //--replace def replaceAllWith(regex:Regex,replacement:String=""):String = regex.replaceAllIn(string, replacement) def replaceAllWith(regex:Regex)(replacer:String=>String):String = regex.replaceAllIn(string, m => Regex.quoteReplacement(replacer(m.matched))) def replaceIf(cond:String => Boolean, replacement: => String):String = if(cond(string)) replacement else string def strip(regex:Regex):String = regex.replaceAllIn(string,"") def strip(strings:Iterable[String]):String = strings.foldLeft(string){case (a, pat) => a.replace(pat,"")} // private def twoGroups(m:Regex.Match):(String,String) = (m group m.groupCount-1, m group m.groupCount) // private def allGroups(m:Regex.Match):List[String] = m.subgroups /**easy way to grab all matches of a string, an alternative name would be pluckAllFirstMatchGroup*/ def pluck[A](p:Pluck[String,A]):Option[A] = p.pluck(string) def pluckAll[A](p:Pluck[String,A]):Iterator[A] = p.pluckAll(string) def dos2unix:String = string.replaceAll("\r\n", "\n") //FIXME why are these ansi's deprecated? They seem nice.??? 2017-07-30("use Ansi.strip","v0.2.15") def stripAnsi:String = Ansi.strip(string) // 2017-07-30("use Ansi.color(color,string) or color.ansi(string)","v0.2.15") // def ansi(c:Color):String = c.ansi(string) def ansi(c:Color):String = c.ansi(string) 2017-07-30("use part of the Pluck typeclass transformer now", "0.2.13") private def firstGroup(m:Regex.Match):String = m group m.groupCount 2017-07-30("use pluck(Pluck(regex)) istead", "0.2.13") def pluck(regex:Regex):Option[String] = regex findFirstMatchIn string map firstGroup 2017-07-30("use pluckAll(Pluck(regex)) istead", "0.2.13") def pluckAll(regex:Regex):Iterator[String] = regex findAllMatchIn string map firstGroup /**an left to right version of regex findAllMatchIn string*/ 2017-07-30("use the Pluck typeclass instead which better encodes the Regex -> Match -> Extrator -> Type pattern in a typesafe inference way", "0.2.13") def findAllMatches(regex:Regex) = regex findAllMatchIn string /**fix "bug" of intent for string concat with a +++ for flatConcat or ++ for a concat of seq of chars*/ def flatConcat(xs:Iterable[String]):String = string + xs.mkString("") def flatConcat(xs:Option[String]):String = flatConcat(xs.toList) /**alias for flatConcat is needed so left to right operator precidence works when named opperators are higher than operators*/ def +++(opt:Option[String]):String = flatConcat(opt) def +++(xs:Iterable[String]):String = flatConcat(xs) def soundsLike(str:String)(implicit p:Phonetic):Boolean = p.phonetic(string) == p.phonetic(str) def containsSound(str:String)(implicit p:Phonetic):Boolean = p.phonetic(string) contains p.phonetic(str) def opposite(implicit p:Opposite[String,String]):Option[String] = p.oppositeOf(string) def println():Unit = System.out.println(string) // def println:String = {println(string); string} }