├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── src ├── main │ └── kotlin │ │ └── co │ │ └── zsmb │ │ └── verbalexpressions │ │ ├── Extensions.kt │ │ └── VerEx.kt └── test │ └── kotlin │ └── co │ └── zsmb │ └── verbalexpressions │ ├── ConstructorTest.kt │ ├── ExceptionTests.kt │ ├── MatcherTests.kt │ ├── RealExampleTests.kt │ └── SimpleTests.kt └── testformatter.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.gradle 3 | /.idea 4 | /local.properties 5 | /build 6 | /gradle.properties 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: oraclejdk8 3 | 4 | script: 5 | - ./gradlew clean build 6 | 7 | notifications: 8 | email: 9 | on_failure: always 10 | on_success: never 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Márton Braun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KotlinVerbalExpressions [![Build Status](https://travis-ci.org/zsmb13/KotlinVerbalExpressions.svg?branch=master)](https://travis-ci.org/zsmb13/KotlinVerbalExpressions) 2 | 3 | This is a Kotlin implementation of [VerbalExpressions](https://github.com/VerbalExpressions), mostly based on the Java, Swift, and Scala implementations. 4 | 5 | ### Examples 6 | 7 | Simple URL test: 8 | ```kotlin 9 | val verex = VerEx() 10 | .startOfLine() 11 | .then("http") 12 | .maybe("s") 13 | .then("://") 14 | .maybe("www") 15 | .anythingBut(" ") 16 | .endOfLine() 17 | 18 | val url = "https://www.google.com" 19 | 20 | // regular test with VerEx method 21 | if(verex.test(url)) { 22 | println("Correct url") 23 | } 24 | 25 | // test with infix extension 26 | if(url matches verex) { 27 | println("Correct url") 28 | } 29 | ``` 30 | 31 | Replacing strings: 32 | ```kotlin 33 | val str = "I like birds and bridges" 34 | 35 | val verex = VerEx() 36 | .then("b") 37 | .anythingBut(" ").zeroOrMore() 38 | 39 | val result = verex.replace(str, "trains") 40 | 41 | println(result) // I like trains and trains 42 | ``` 43 | 44 | For more usage examples, see the included tests. 45 | 46 | ### Installation 47 | 48 | ##### Maven 49 | 50 | ``` 51 | 52 | co.zsmb 53 | kotlinverbalexpressions 54 | 0.1 55 | 56 | ``` 57 | 58 | ##### Gradle 59 | 60 | 61 | ``` 62 | repositories { 63 | jcenter() 64 | // or, alternatively: 65 | maven { url 'https://dl.bintray.com/zsmb13/KotlinVerbalExpressions/' } 66 | } 67 | 68 | dependencies { 69 | compile 'co.zsmb:kotlinverbalexpressions:0.1' 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.2.30' 3 | 4 | repositories { 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 9 | } 10 | } 11 | 12 | plugins { 13 | id "com.jfrog.bintray" version "1.7.3" 14 | } 15 | 16 | def GROUP_ID = 'co.zsmb' 17 | def ARTIFACT_ID = 'kotlinverbalexpressions' 18 | def VERSION = '0.1' 19 | 20 | group GROUP_ID 21 | version VERSION 22 | 23 | apply plugin: 'java' 24 | apply plugin: 'kotlin' 25 | apply plugin: 'maven-publish' 26 | 27 | apply from: 'testformatter.gradle' 28 | 29 | sourceCompatibility = 1.8 30 | 31 | repositories { 32 | mavenCentral() 33 | } 34 | 35 | dependencies { 36 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" 37 | testCompile group: 'junit', name: 'junit', version: '4.12' 38 | } 39 | 40 | bintray { 41 | user = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('bintrayUser') 42 | key = project.hasProperty('bintrayKey') ? project.property('bintrayKey') : System.getenv('bintrayKey') 43 | publications = ['mavenJava'] 44 | 45 | pkg { 46 | repo = 'KotlinVerbalExpressions' 47 | name = 'KotlinVerbalExpressions' 48 | 49 | version { 50 | name = VERSION 51 | released = new Date() 52 | } 53 | } 54 | } 55 | 56 | task sourcesJar(type: Jar, dependsOn: project.classes) { 57 | from sourceSets.main.allSource 58 | } 59 | 60 | artifacts { 61 | archives sourcesJar 62 | } 63 | publishing { 64 | publications { 65 | mavenJava(MavenPublication) { 66 | groupId GROUP_ID 67 | artifactId ARTIFACT_ID 68 | version VERSION 69 | 70 | from components.java 71 | 72 | artifact sourcesJar { 73 | classifier = 'sources' 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VerbalExpressions/KotlinVerbalExpressions/425cb63ff7256a3c3fac8ff0eddc6098c0ccf91c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 28 10:46:29 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kotlinverbalexpressions' 2 | -------------------------------------------------------------------------------- /src/main/kotlin/co/zsmb/verbalexpressions/Extensions.kt: -------------------------------------------------------------------------------- 1 | package co.zsmb.verbalexpressions 2 | 3 | infix fun String?.matches(verex: VerEx) = verex.test(this) 4 | 5 | infix fun String?.matchesExact(verex: VerEx) = verex.testExact(this) 6 | -------------------------------------------------------------------------------- /src/main/kotlin/co/zsmb/verbalexpressions/VerEx.kt: -------------------------------------------------------------------------------- 1 | package co.zsmb.verbalexpressions 2 | 3 | import java.util.regex.Pattern 4 | 5 | class VerEx(construct: VerEx.() -> Unit = {}) { 6 | 7 | companion object { 8 | private val symbols = mapOf( 9 | 'd' to Pattern.UNIX_LINES, 10 | 'i' to Pattern.CASE_INSENSITIVE, 11 | 'x' to Pattern.COMMENTS, 12 | 'm' to Pattern.MULTILINE, 13 | 's' to Pattern.DOTALL, 14 | 'u' to Pattern.UNICODE_CASE, 15 | 'U' to Pattern.UNICODE_CHARACTER_CLASS 16 | ) 17 | } 18 | 19 | private var prefixes = StringBuilder() 20 | private var source = StringBuilder() 21 | private var suffixes = StringBuilder() 22 | private var modifiers = Pattern.MULTILINE 23 | 24 | init { 25 | construct() 26 | } 27 | 28 | //// COMPUTED PROPERTIES //// 29 | 30 | val pattern: Pattern 31 | get() = Pattern.compile("$prefixes$source$suffixes", modifiers) 32 | 33 | //// TESTS //// 34 | 35 | fun test(toTest: String?) = if (toTest == null) false else pattern.matcher(toTest).find() 36 | 37 | fun testExact(toTest: String?) = if (toTest == null) false else pattern.matcher(toTest).matches() 38 | 39 | //// COMPOSITION //// 40 | 41 | fun startOfLine(enabled: Boolean = true): VerEx { 42 | prefixes = StringBuilder(if (enabled) "^" else "") 43 | return this 44 | } 45 | 46 | fun endOfLine(enabled: Boolean = true): VerEx { 47 | suffixes = StringBuilder(if (enabled) "$" else "") 48 | return this 49 | } 50 | 51 | fun find(str: String) = then(str) 52 | 53 | fun then(str: String) = add("(?:${sanitize(str)})") 54 | 55 | fun maybe(str: String) = add("(?:${sanitize(str)})?") 56 | 57 | fun anything() = add("(?:.*)") 58 | 59 | fun anythingBut(str: String) = add("(?:[^${sanitize(str)}]*)") 60 | 61 | fun something() = add("(?:.+)") 62 | 63 | fun somethingBut(str: String) = add("(?:[^${sanitize(str)}]+)") 64 | 65 | fun lineBreak() = add("""(?:(?:\n)|(?:\r\n))""") 66 | 67 | fun br() = lineBreak() 68 | 69 | fun tab() = add("""\t""") 70 | 71 | fun word() = add("""\w+""") 72 | 73 | fun anyOf(str: String) = add("(?:[${sanitize(str)}])") 74 | 75 | fun any(str: String) = anyOf(str) 76 | 77 | fun withAnyCase(enabled: Boolean = true) = updateModifier('i', enabled) 78 | 79 | fun searchOneLine(enabled: Boolean = true) = updateModifier('m', !enabled) 80 | 81 | fun or(str: String): VerEx { 82 | prefixes.append("(") 83 | source.append(")|(").append(str).append(")").append(suffixes) 84 | suffixes = StringBuilder() 85 | 86 | return this 87 | } 88 | 89 | fun multiple(str: String, min: Int? = null, max: Int? = null): VerEx { 90 | then(str) 91 | return times(min ?: 1, max) 92 | } 93 | 94 | fun times(min: Int, max: Int? = null): VerEx { 95 | if (max != null && min > max) { 96 | throw IllegalArgumentException("Min count ($min) can't be less than max count ($max).") 97 | } 98 | return add("{$min,${max ?: ""}}") 99 | } 100 | 101 | fun exactly(count: Int) = times(count, count) 102 | 103 | fun atLeast(min: Int) = times(min) 104 | 105 | fun replace(source: String, replacement: String): String = pattern.matcher(source).replaceAll(replacement) 106 | 107 | fun range(vararg args: Pair) = 108 | add(args.joinToString(prefix = "[", postfix = "]", separator = "") { "${it.first}-${it.second}" }) 109 | 110 | fun beginCapture() = add("(") 111 | 112 | fun endCapture() = add(")") 113 | 114 | fun whiteSpace() = add("""\s""") 115 | 116 | fun oneOrMore() = add("+") 117 | 118 | fun zeroOrMore() = add("*") 119 | 120 | fun addModifier(modifier: Char): VerEx { 121 | symbols[modifier]?.let { 122 | modifiers = modifiers or it 123 | } 124 | return this 125 | } 126 | 127 | fun removeModifier(modifier: Char): VerEx { 128 | symbols[modifier]?.let { 129 | modifiers = modifiers and it.inv() 130 | } 131 | return this 132 | } 133 | 134 | fun addModifier(modifier: String): VerEx { 135 | if (modifier.length != 1) { 136 | throw IllegalArgumentException("Modifier has to be a single character") 137 | } 138 | return addModifier(modifier[0]) 139 | } 140 | 141 | fun removeModifier(modifier: String): VerEx { 142 | if (modifier.length != 1) { 143 | throw IllegalArgumentException("Modifier has to be a single character") 144 | } 145 | return removeModifier(modifier[0]) 146 | } 147 | 148 | //// PRIVATE HELPERS //// 149 | 150 | private fun add(str: String): VerEx { 151 | source.append(str) 152 | return this 153 | } 154 | 155 | private fun sanitize(str: String) = str.replace("[\\W]".toRegex(), """\\$0""") 156 | 157 | private fun updateModifier(modifier: Char, enabled: Boolean) = 158 | if (enabled) addModifier(modifier) 159 | else removeModifier(modifier) 160 | 161 | } 162 | -------------------------------------------------------------------------------- /src/test/kotlin/co/zsmb/verbalexpressions/ConstructorTest.kt: -------------------------------------------------------------------------------- 1 | package co.zsmb.verbalexpressions 2 | 3 | import org.junit.Assert.assertTrue 4 | import org.junit.Test 5 | 6 | class ConstructorTest { 7 | 8 | @Test 9 | fun useConstructor() { 10 | 11 | val verex = VerEx { 12 | startOfLine() 13 | then("http") 14 | maybe("s") 15 | then("://") 16 | maybe("www") 17 | anythingBut(" ") 18 | endOfLine() 19 | } 20 | 21 | assertTrue("https://www.google.com" matches verex) 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/kotlin/co/zsmb/verbalexpressions/ExceptionTests.kt: -------------------------------------------------------------------------------- 1 | package co.zsmb.verbalexpressions 2 | 3 | import org.junit.Assert 4 | import org.junit.Assert.assertFalse 5 | import org.junit.Assert.assertTrue 6 | import org.junit.Test 7 | import java.util.regex.PatternSyntaxException 8 | 9 | class ExceptionTests { 10 | 11 | @Test(expected = IllegalArgumentException::class) 12 | fun `times called with larger min value than max`() { 13 | val verex = VerEx() 14 | .then("a") 15 | .times(2, 1) 16 | } 17 | 18 | @Test(expected = PatternSyntaxException::class) 19 | fun `non matching number of begin and end capture calls 1`() { 20 | val verex = VerEx() 21 | .beginCapture() 22 | .then("a") 23 | 24 | "a" matches verex 25 | } 26 | 27 | @Test(expected = PatternSyntaxException::class) 28 | fun `non matching number of begin and end capture calls 2`() { 29 | val verex = VerEx() 30 | .beginCapture() 31 | .then("a") 32 | .endCapture() 33 | .beginCapture() 34 | 35 | "a" matches verex 36 | } 37 | 38 | @Test(expected = PatternSyntaxException::class) 39 | fun `invalid nesting of capture parentheses`() { 40 | val verex = VerEx() 41 | .beginCapture() 42 | .endCapture() 43 | .endCapture() 44 | .beginCapture() 45 | 46 | "a" matches verex 47 | } 48 | 49 | @Test(expected = IllegalArgumentException::class) 50 | fun `attempting to add modifier of zero length`() { 51 | val verex = VerEx() 52 | .addModifier("") 53 | } 54 | 55 | @Test(expected = IllegalArgumentException::class) 56 | fun `attempting to add modifier longer than one character`() { 57 | val verex = VerEx() 58 | .addModifier("aa") 59 | } 60 | 61 | @Test(expected = IllegalArgumentException::class) 62 | fun `attempting to remove modifier of zero length`() { 63 | val verex = VerEx() 64 | .removeModifier("") 65 | } 66 | 67 | @Test(expected = IllegalArgumentException::class) 68 | fun `attempting to remove modifier longer than one character`() { 69 | val verex = VerEx() 70 | .removeModifier("aa") 71 | } 72 | 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/test/kotlin/co/zsmb/verbalexpressions/MatcherTests.kt: -------------------------------------------------------------------------------- 1 | package co.zsmb.verbalexpressions 2 | 3 | import org.junit.Assert.assertFalse 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Test 6 | 7 | class MatcherTests { 8 | 9 | @Test 10 | fun matches() { 11 | val verex = VerEx() 12 | .startOfLine() 13 | .then("a") 14 | 15 | assertTrue("apple" matches verex) 16 | assertTrue("art" matches verex) 17 | 18 | assertFalse("banana" matches verex) 19 | } 20 | 21 | @Test 22 | fun matchesExact() { 23 | val verex = VerEx() 24 | .startOfLine() 25 | .then("a") 26 | 27 | assertTrue("a" matchesExact verex) 28 | 29 | assertFalse("apple" matchesExact verex) 30 | assertFalse("art" matchesExact verex) 31 | assertFalse("banana" matchesExact verex) 32 | } 33 | 34 | @Test 35 | fun test() { 36 | val verex = VerEx() 37 | .startOfLine() 38 | .then("a") 39 | 40 | assertTrue(verex.test("apple")) 41 | assertTrue(verex.test("art")) 42 | 43 | assertFalse(verex.test("banana")) 44 | } 45 | 46 | @Test 47 | fun testExact() { 48 | val verex = VerEx() 49 | .startOfLine() 50 | .then("a") 51 | 52 | assertTrue(verex.testExact("a")) 53 | 54 | assertFalse(verex.testExact("apple")) 55 | assertFalse(verex.testExact("art")) 56 | assertFalse(verex.testExact("banana")) 57 | } 58 | 59 | @Test 60 | fun `empty verex matches anything except null`() { 61 | val verex = VerEx() 62 | 63 | assertTrue("" matches verex) 64 | assertTrue("abc" matches verex) 65 | assertTrue("123" matches verex) 66 | 67 | assertFalse(null matches verex) 68 | } 69 | 70 | @Test 71 | fun `empty verex mathes empty string exactly`() { 72 | val verex = VerEx() 73 | 74 | assertTrue("" matchesExact verex) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/test/kotlin/co/zsmb/verbalexpressions/RealExampleTests.kt: -------------------------------------------------------------------------------- 1 | package co.zsmb.verbalexpressions 2 | 3 | import org.junit.Assert.assertFalse 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Test 6 | 7 | class RealExampleTests { 8 | 9 | @Test 10 | fun urls() { 11 | val verex = VerEx() 12 | .startOfLine() 13 | .then("http") 14 | .maybe("s") 15 | .then("://") 16 | .maybe("www") 17 | .anythingBut(" ") 18 | .endOfLine() 19 | 20 | assertTrue("https://www.google.com" matches verex) 21 | assertTrue("http://zsmb.co" matches verex) 22 | assertTrue("https://www.wikipedia.org/" matches verex) 23 | } 24 | 25 | @Test 26 | fun emails() { 27 | val verex = VerEx() 28 | .startOfLine() 29 | .anything().oneOrMore() 30 | .then("@") 31 | .anything().oneOrMore() 32 | .then(".") 33 | .anything().oneOrMore() 34 | .endOfLine() 35 | 36 | assertTrue("jenny@gmail.com" matches verex) 37 | assertTrue("steve@gmail.com" matches verex) 38 | assertTrue("jimmy@yahoo.org" matches verex) 39 | assertTrue("foo@bar.io" matches verex) 40 | 41 | assertFalse("1234@5678" matches verex) 42 | } 43 | 44 | @Test 45 | fun phoneNumbers() { 46 | val verex = VerEx() 47 | .startOfLine() 48 | .beginCapture() 49 | .range(0 to 9) 50 | .or("-") 51 | .endCapture() 52 | .times(7, 14) 53 | .endOfLine() 54 | 55 | assertTrue("12345678" matches verex) 56 | assertTrue("123-4567" matches verex) 57 | assertTrue("1-800-1234-567" matches verex) 58 | assertTrue("123-456" matches verex) 59 | 60 | assertFalse("phone number" matches verex) 61 | assertFalse("123456" matches verex) 62 | assertFalse("123456789012345" matches verex) 63 | assertFalse("123-5-789-12345" matches verex) 64 | } 65 | 66 | @Test 67 | fun dates() { 68 | val verex = VerEx() 69 | .startOfLine() 70 | .range(1 to 2) 71 | .range(0 to 9).exactly(3) 72 | .then("-") 73 | .range(0 to 1) 74 | .range(0 to 9) 75 | .then("-") 76 | .range(0 to 3) 77 | .range(0 to 9) 78 | .endOfLine() 79 | 80 | assertTrue("1234-12-12" matches verex) 81 | assertTrue("1995-04-25" matches verex) 82 | assertTrue("1995-06-27" matches verex) 83 | assertTrue("1999-12-31" matches verex) 84 | assertTrue("2017-04-04" matches verex) 85 | assertTrue("2017-04-04" matches verex) 86 | 87 | assertFalse("123-12-12" matches verex) 88 | assertFalse("1234-20-20" matches verex) 89 | assertFalse("1000-10-40" matches verex) 90 | assertFalse("3333-12-12" matches verex) 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/test/kotlin/co/zsmb/verbalexpressions/SimpleTests.kt: -------------------------------------------------------------------------------- 1 | package co.zsmb.verbalexpressions 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | class SimpleTests { 7 | 8 | @Test 9 | fun startOfLine() { 10 | val verex = VerEx() 11 | .startOfLine() 12 | .then("a") 13 | 14 | assertTrue("apple" matches verex) 15 | assertFalse("banana" matches verex) 16 | } 17 | 18 | @Test 19 | fun endOfLine() { 20 | val verex = VerEx() 21 | .then("a") 22 | .endOfLine() 23 | 24 | assertFalse("apple" matches verex) 25 | assertTrue("banana" matches verex) 26 | } 27 | 28 | @Test 29 | fun find1() { 30 | val verex = VerEx() 31 | .find("b") 32 | 33 | assertTrue("abc" matches verex) 34 | } 35 | 36 | @Test 37 | fun find2() { 38 | val verex = VerEx() 39 | .find("world") 40 | 41 | assertTrue("Hello world" matches verex) 42 | } 43 | 44 | @Test 45 | fun then1() { 46 | val verex = VerEx() 47 | .then("b") 48 | 49 | assertTrue("abc" matches verex) 50 | } 51 | 52 | @Test 53 | fun then2() { 54 | val verex = VerEx() 55 | .then("llo wor") 56 | 57 | assertTrue("Hello world" matches verex) 58 | } 59 | 60 | @Test 61 | fun maybe() { 62 | val verex = VerEx() 63 | .then("apple") 64 | .maybe(" tree") 65 | 66 | assertTrue("apple tree" matches verex) 67 | assertTrue("apple" matches verex) 68 | } 69 | 70 | @Test 71 | fun anything1() { 72 | val verex = VerEx() 73 | .anything() 74 | 75 | assertTrue("" matches verex) 76 | assertTrue("qwerty" matches verex) 77 | assertTrue("123456789" matches verex) 78 | } 79 | 80 | @Test 81 | fun anything2() { 82 | val verex = VerEx() 83 | .then("a") 84 | .anything() 85 | .then("a") 86 | 87 | assertTrue("aa" matches verex) 88 | assertTrue("aqwertya" matches verex) 89 | assertTrue("a123456789a" matches verex) 90 | } 91 | 92 | @Test 93 | fun anythingBut1() { 94 | val verex = VerEx() 95 | .anythingBut("abcd") 96 | 97 | assertTrue("" matches verex) 98 | assertTrue("eleven" matches verex) 99 | assertTrue("123456" matches verex) 100 | } 101 | 102 | @Test 103 | fun anythingBut2() { 104 | val verex = VerEx() 105 | .startOfLine() 106 | .anythingBut("abcd") 107 | .endOfLine() 108 | 109 | assertFalse("apple" matches verex) 110 | assertFalse("banana" matches verex) 111 | } 112 | 113 | @Test 114 | fun something1() { 115 | val verex = VerEx() 116 | .something() 117 | 118 | assertTrue("qwerty" matches verex) 119 | assertTrue("123456789" matches verex) 120 | 121 | assertFalse("" matches verex) 122 | } 123 | 124 | @Test 125 | fun something2() { 126 | val verex = VerEx() 127 | .then("a") 128 | .something() 129 | .then("a") 130 | 131 | assertTrue("aqwertya" matches verex) 132 | assertTrue("a123456789a" matches verex) 133 | 134 | assertFalse("aa" matches verex) 135 | } 136 | 137 | @Test 138 | fun somethingBut1() { 139 | val verex = VerEx() 140 | .somethingBut("abcd") 141 | 142 | assertTrue("eleven" matches verex) 143 | assertTrue("123456" matches verex) 144 | 145 | assertFalse("" matches verex) 146 | } 147 | 148 | @Test 149 | fun somethingBut2() { 150 | val verex = VerEx() 151 | .startOfLine() 152 | .somethingBut("abcd") 153 | .endOfLine() 154 | 155 | assertFalse("apple" matches verex) 156 | assertFalse("banana" matches verex) 157 | } 158 | 159 | @Test 160 | fun lineBreak() { 161 | val verex = VerEx() 162 | .then("a") 163 | .lineBreak() 164 | .then("b") 165 | 166 | assertTrue("a\nb" matches verex) 167 | } 168 | 169 | @Test 170 | fun br() { 171 | val verex = VerEx() 172 | .then("a") 173 | .br() 174 | .then("b") 175 | 176 | assertTrue("a\nb" matches verex) 177 | } 178 | 179 | @Test 180 | fun tab() { 181 | val verex = VerEx() 182 | .then("a") 183 | .tab() 184 | .then("b") 185 | 186 | assertTrue("a\tb" matches verex) 187 | } 188 | 189 | @Test 190 | fun word() { 191 | val verex = VerEx() 192 | .then("Hello ") 193 | .word() 194 | 195 | assertTrue("Hello world" matches verex) 196 | assertTrue("Hello Jane" matches verex) 197 | assertTrue("Hello 123" matches verex) 198 | assertTrue("Hello 1_2_3_a_b_c" matches verex) 199 | 200 | assertFalse("Hello " matches verex) 201 | assertFalse("Hello +++" matches verex) 202 | assertFalse("Hello ***" matches verex) 203 | } 204 | 205 | @Test 206 | fun anyOf() { 207 | val verex = VerEx() 208 | .anyOf("abcd") 209 | 210 | assertTrue("a" matches verex) 211 | assertTrue("d" matches verex) 212 | 213 | assertFalse("hello" matches verex) 214 | assertFalse("12345" matches verex) 215 | } 216 | 217 | @Test 218 | fun any() { 219 | val verex = VerEx() 220 | .any("abcd") 221 | 222 | assertTrue("a" matches verex) 223 | assertTrue("d" matches verex) 224 | 225 | assertFalse("hello" matches verex) 226 | assertFalse("12345" matches verex) 227 | } 228 | 229 | @Test 230 | fun withAnyCase1() { 231 | val verex = VerEx() 232 | .then("apple") 233 | .withAnyCase() 234 | 235 | assertTrue("apple" matches verex) 236 | assertTrue("Apple" matches verex) 237 | assertTrue("aPpLe" matches verex) 238 | } 239 | 240 | @Test 241 | fun withAnyCase2() { 242 | val verex = VerEx() 243 | .then("apple") 244 | .withAnyCase(false) 245 | 246 | assertTrue("apple" matches verex) 247 | 248 | assertFalse("Apple" matches verex) 249 | assertFalse("aPpLe" matches verex) 250 | } 251 | 252 | @Test 253 | fun searchOneLine1() { 254 | val verex = VerEx() 255 | .startOfLine() 256 | .anything() 257 | .then("b") 258 | .anything() 259 | .endOfLine() 260 | .searchOneLine() 261 | 262 | assertFalse("a\nb" matches verex) 263 | } 264 | 265 | @Test 266 | fun searchOneLine2() { 267 | val verex = VerEx() 268 | .startOfLine() 269 | .anything() 270 | .then("b") 271 | .anything() 272 | .endOfLine() 273 | .searchOneLine(false) 274 | 275 | assertTrue("a\nb" matches verex) 276 | } 277 | 278 | @Test 279 | fun or() { 280 | val verex = VerEx() 281 | .startOfLine() 282 | .then("a") 283 | .or("b") 284 | .then("c") 285 | 286 | assertTrue("ac" matches verex) 287 | assertTrue("bc" matches verex) 288 | } 289 | 290 | @Test 291 | fun multiple1() { 292 | val verex = VerEx() 293 | .startOfLine() 294 | .multiple("a") 295 | .endOfLine() 296 | 297 | assertTrue("a" matches verex) 298 | assertTrue("aaaa" matches verex) 299 | assertTrue("aaaaaaa" matches verex) 300 | 301 | assertFalse("" matches verex) 302 | assertFalse("b" matches verex) 303 | } 304 | 305 | @Test 306 | fun multiple2() { 307 | val verex = VerEx() 308 | .startOfLine() 309 | .multiple("a", 3) 310 | .endOfLine() 311 | 312 | assertTrue("aaa" matches verex) 313 | assertTrue("aaaaa" matches verex) 314 | 315 | assertFalse("" matches verex) 316 | assertFalse("a" matches verex) 317 | assertFalse("b" matches verex) 318 | } 319 | 320 | @Test 321 | fun multiple3() { 322 | val verex = VerEx() 323 | .startOfLine() 324 | .multiple("a", 3, 5) 325 | .endOfLine() 326 | 327 | assertTrue("aaa" matches verex) 328 | assertTrue("aaaa" matches verex) 329 | assertTrue("aaaaa" matches verex) 330 | 331 | assertFalse("" matches verex) 332 | assertFalse("aa" matches verex) 333 | assertFalse("aaaaaa" matches verex) 334 | assertFalse("b" matches verex) 335 | } 336 | 337 | @Test 338 | fun times1() { 339 | val verex = VerEx() 340 | .startOfLine() 341 | .then("a").times(3) 342 | .endOfLine() 343 | 344 | assertTrue("aaa" matches verex) 345 | assertTrue("aaaaa" matches verex) 346 | 347 | assertFalse("" matches verex) 348 | assertFalse("a" matches verex) 349 | assertFalse("b" matches verex) 350 | } 351 | 352 | @Test 353 | fun times2() { 354 | val verex = VerEx() 355 | .startOfLine() 356 | .then("a").times(3, 5) 357 | .endOfLine() 358 | 359 | assertTrue("aaa" matches verex) 360 | assertTrue("aaaa" matches verex) 361 | assertTrue("aaaaa" matches verex) 362 | 363 | assertFalse("" matches verex) 364 | assertFalse("aa" matches verex) 365 | assertFalse("aaaaaa" matches verex) 366 | assertFalse("b" matches verex) 367 | } 368 | 369 | @Test 370 | fun atLeast() { 371 | val verex = VerEx() 372 | .then("a") 373 | .atLeast(3) 374 | 375 | assertTrue("aaa" matches verex) 376 | assertTrue("aaaaa" matches verex) 377 | 378 | assertFalse("" matches verex) 379 | assertFalse("aa" matches verex) 380 | } 381 | 382 | @Test 383 | fun replace() { 384 | val verex = VerEx() 385 | .then("a").oneOrMore() 386 | 387 | assertEquals("hello", verex.replace("haaaaallo", "e")) 388 | } 389 | 390 | @Test 391 | fun range1() { 392 | val verex = VerEx() 393 | .range(1 to 5) 394 | 395 | assertTrue("1" matches verex) 396 | assertTrue("3" matches verex) 397 | assertTrue("5" matches verex) 398 | 399 | assertFalse("0" matches verex) 400 | assertFalse("6" matches verex) 401 | assertFalse("a" matches verex) 402 | } 403 | 404 | @Test 405 | fun range2() { 406 | val verex = VerEx() 407 | .startOfLine() 408 | .range(1 to 5, "a" to "d").oneOrMore() 409 | .endOfLine() 410 | 411 | assertTrue("12345" matches verex) 412 | assertTrue("abcd" matches verex) 413 | 414 | assertFalse("0" matches verex) 415 | assertFalse("6" matches verex) 416 | assertFalse("e" matches verex) 417 | } 418 | 419 | @Test 420 | fun beginCapture_endCapture() { 421 | val verex = VerEx() 422 | .beginCapture() 423 | .then("aaa") 424 | .endCapture() 425 | val matcher = verex.pattern.matcher("baaab") 426 | matcher.find() 427 | 428 | assertEquals("aaa", matcher.group()) 429 | } 430 | 431 | @Test 432 | fun whiteSpace() { 433 | val verex = VerEx() 434 | .startOfLine() 435 | .whiteSpace() 436 | .endOfLine() 437 | 438 | assertTrue(" " matches verex) 439 | assertTrue("\t" matches verex) 440 | assertTrue("\n" matches verex) 441 | assertTrue("\n\r" matches verex) 442 | assertTrue("\r" matches verex) 443 | 444 | assertFalse("a" matches verex) 445 | assertFalse("1" matches verex) 446 | } 447 | 448 | @Test 449 | fun oneOrMore() { 450 | val verex = VerEx() 451 | .then("a") 452 | .then("b") 453 | .oneOrMore() 454 | .then("a") 455 | 456 | assertTrue("aba" matches verex) 457 | assertTrue("abba" matches verex) 458 | 459 | assertFalse("aa" matches verex) 460 | } 461 | 462 | @Test 463 | fun zeroOrMore() { 464 | val verex = VerEx() 465 | .then("a") 466 | .then("b") 467 | .zeroOrMore() 468 | .then("a") 469 | 470 | assertTrue("aa" matches verex) 471 | assertTrue("aba" matches verex) 472 | assertTrue("abba" matches verex) 473 | } 474 | 475 | @Test 476 | fun addModifier() { 477 | val verex = VerEx() 478 | .then("apple") 479 | .addModifier("i") 480 | .removeModifier("i") 481 | .addModifier("i") 482 | 483 | assertTrue("apple" matches verex) 484 | assertTrue("Apple" matches verex) 485 | assertTrue("aPpLe" matches verex) 486 | } 487 | 488 | @Test 489 | fun removeModifier() { 490 | val verex = VerEx() 491 | .then("apple") 492 | .removeModifier('i') 493 | .addModifier('i') 494 | .removeModifier('i') 495 | 496 | assertTrue("apple" matches verex) 497 | 498 | assertFalse("Apple" matches verex) 499 | assertFalse("aPpLe" matches verex) 500 | } 501 | 502 | } 503 | -------------------------------------------------------------------------------- /testformatter.gradle: -------------------------------------------------------------------------------- 1 | tasks.withType(Test) { 2 | testLogging { 3 | // set options for log level LIFECYCLE 4 | events "passed", "skipped", "failed", "standardOut" 5 | showExceptions true 6 | exceptionFormat "full" 7 | showCauses true 8 | showStackTraces true 9 | 10 | // set options for log level DEBUG and INFO 11 | debug { 12 | events "started", "passed", "skipped", "failed", "standardOut", "standardError" 13 | exceptionFormat "full" 14 | } 15 | info.events = debug.events 16 | info.exceptionFormat = debug.exceptionFormat 17 | 18 | afterSuite { desc, result -> 19 | if (!desc.parent) { // will match the outermost suite 20 | def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" 21 | def startItem = '| ', endItem = ' |' 22 | def repeatLength = startItem.length() + output.length() + endItem.length() 23 | println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength)) 24 | } 25 | } 26 | } 27 | } 28 | --------------------------------------------------------------------------------