├── settings.gradle ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── LICENSE ├── src ├── main │ └── kotlin │ │ └── ua │ │ └── kurinnyi │ │ └── kotlin │ │ └── patternmatching │ │ ├── extractors │ │ ├── EmptyExtractorPatternMatching.kt │ │ ├── SingleExtractorPatternMatching.kt │ │ ├── PairExtractorPatternMatching.kt │ │ └── TripleExtractorPatternMatching.kt │ │ ├── PatternMatching.kt │ │ └── ListPatterMatching.kt └── test │ └── kotlin │ └── ua │ └── kurinnyi │ └── kotlin │ └── patternmatching │ ├── ListPatterMatchingTest.kt │ └── PatternMatchingTest.kt ├── gradlew.bat ├── README.MD └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "pattern-matching" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /.idea 4 | /build 5 | /out -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurinnyi/Kotlin-Pattern-Matching/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ivan Kurinnyi 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 | -------------------------------------------------------------------------------- /src/main/kotlin/ua/kurinnyi/kotlin/patternmatching/extractors/EmptyExtractorPatternMatching.kt: -------------------------------------------------------------------------------- 1 | package ua.kurinnyi.kotlin.patternmatching.extractors 2 | 3 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching 4 | 5 | 6 | interface EmptyExtractor { 7 | 8 | fun unapply(from: From): Boolean 9 | } 10 | 11 | inline fun PatternMatching.MatchContext.caseE(extractor: EmptyExtractor) 12 | : MatcherBuilderNoElement { 13 | if (!fulfilled && value is FROM) { 14 | if (extractor.unapply(value)) 15 | return MatcherBuilderExtractedNoElement(this, value) 16 | } 17 | return FailedMatcherBuilderExtractedNoElement() 18 | } 19 | 20 | 21 | interface MatcherBuilderNoElement { 22 | fun and(predicate: FROM.() -> Boolean): MatcherBuilderNoElement 23 | 24 | fun then(action: FROM.() -> RESULT) 25 | } 26 | 27 | class MatcherBuilderExtractedNoElement( 28 | private val matchContext: PatternMatching.MatchContext, 29 | private val value: FROM 30 | ) : MatcherBuilderNoElement { 31 | override fun and(predicate: FROM.() -> Boolean): MatcherBuilderNoElement { 32 | return if (!matchContext.fulfilled && predicate(value)) { 33 | this 34 | } else { 35 | FailedMatcherBuilderExtractedNoElement() 36 | } 37 | } 38 | 39 | override fun then(action: FROM.() -> RESULT) { 40 | matchContext.fulfilled = true 41 | matchContext.result = action(value) 42 | } 43 | } 44 | 45 | class FailedMatcherBuilderExtractedNoElement : MatcherBuilderNoElement { 46 | 47 | override fun and(predicate: FROM.() -> Boolean) = this 48 | 49 | override fun then(action: FROM.() -> RESULT) { 50 | //do nothing 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/ua/kurinnyi/kotlin/patternmatching/extractors/SingleExtractorPatternMatching.kt: -------------------------------------------------------------------------------- 1 | package ua.kurinnyi.kotlin.patternmatching.extractors 2 | 3 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching 4 | 5 | interface SingleExtractor { 6 | 7 | fun unapply(from: From): Single? 8 | 9 | data class Single(val value: To) 10 | } 11 | 12 | inline fun PatternMatching.MatchContext.caseE(extractor: SingleExtractor) 13 | : MatcherBuilderOneElement { 14 | if (!fulfilled && value is FROM) { 15 | val unapplied = extractor.unapply(value) 16 | if (unapplied != null) 17 | return MatcherBuilderExtractedOneElement(this, unapplied.value, value) 18 | } 19 | return FailedMatcherBuilderExtractedOneElement() 20 | } 21 | 22 | interface MatcherBuilderOneElement { 23 | fun and(predicate: F.(E) -> Boolean): MatcherBuilderOneElement 24 | 25 | fun then(action: F.(E) -> RESULT) 26 | } 27 | 28 | class MatcherBuilderExtractedOneElement( 29 | private val matchContext: PatternMatching.MatchContext, 30 | private val extractedElement: E, 31 | private val value: F 32 | ) : MatcherBuilderOneElement { 33 | override fun and(predicate: F.(E) -> Boolean): MatcherBuilderOneElement { 34 | return if (!matchContext.fulfilled && value.predicate(extractedElement)) { 35 | this 36 | } else { 37 | FailedMatcherBuilderExtractedOneElement() 38 | } 39 | } 40 | 41 | override fun then(action: F.(E) -> RESULT) { 42 | matchContext.fulfilled = true 43 | matchContext.result = value.action(extractedElement) 44 | } 45 | } 46 | 47 | class FailedMatcherBuilderExtractedOneElement : MatcherBuilderOneElement { 48 | 49 | override fun and(predicate: F.(E) -> Boolean) = this 50 | 51 | override fun then(action: F.(E) -> RESULT) { 52 | //do nothing 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/main/kotlin/ua/kurinnyi/kotlin/patternmatching/extractors/PairExtractorPatternMatching.kt: -------------------------------------------------------------------------------- 1 | package ua.kurinnyi.kotlin.patternmatching.extractors 2 | 3 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching 4 | 5 | 6 | interface PairExtractor { 7 | 8 | fun unapply(from: From): Pair? 9 | 10 | data class Pair(val first: To1, val second: To2) 11 | } 12 | 13 | inline fun PatternMatching.MatchContext.caseE(extractor: PairExtractor) 14 | : MatcherBuilderTwoElement { 15 | if (!fulfilled && value is FROM) { 16 | val unapplied = extractor.unapply(value) 17 | if (unapplied != null) { 18 | return MatcherBuilderExtractedTwoElement(this, unapplied, value) 19 | } 20 | } 21 | return FailedMatcherBuilderExtractedTwoElement() 22 | } 23 | 24 | interface MatcherBuilderTwoElement { 25 | fun and(predicate: F.(E1, E2) -> Boolean): MatcherBuilderTwoElement 26 | 27 | fun then(action: F.(E1, E2) -> RESULT) 28 | } 29 | 30 | class MatcherBuilderExtractedTwoElement( 31 | private val matchContext: PatternMatching.MatchContext, 32 | private val extracted: PairExtractor.Pair, 33 | private val value: F 34 | ) : MatcherBuilderTwoElement { 35 | override fun and(predicate: F.(E1, E2) -> Boolean): MatcherBuilderTwoElement { 36 | return if (!matchContext.fulfilled && value.predicate(extracted.first, extracted.second)) { 37 | this 38 | } else { 39 | FailedMatcherBuilderExtractedTwoElement() 40 | } 41 | } 42 | 43 | override fun then(action: F.(E1, E2) -> RESULT) { 44 | matchContext.fulfilled = true 45 | matchContext.result = value.action(extracted.first, extracted.second) 46 | } 47 | } 48 | 49 | class FailedMatcherBuilderExtractedTwoElement : MatcherBuilderTwoElement { 50 | 51 | override fun and(predicate: F.(E1, E2) -> Boolean) = this 52 | 53 | override fun then(action: F.(E1, E2) -> RESULT) { 54 | //do nothing 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/ua/kurinnyi/kotlin/patternmatching/extractors/TripleExtractorPatternMatching.kt: -------------------------------------------------------------------------------- 1 | package ua.kurinnyi.kotlin.patternmatching.extractors 2 | 3 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching 4 | 5 | 6 | interface TripleExtractor { 7 | 8 | fun unapply(from: From): Triple? 9 | 10 | data class Triple(val first: To1, val second: To2, val third: To3) 11 | } 12 | 13 | inline fun PatternMatching.MatchContext.caseE(extractor: TripleExtractor) 14 | : MatcherBuilderThreeElement { 15 | if (!fulfilled && value is FROM) { 16 | val unapplied = extractor.unapply(value) 17 | if (unapplied != null) { 18 | return MatcherBuilderExtractedThreeElement(this, unapplied, value) 19 | } 20 | } 21 | return FailedMatcherBuilderExtractedThreeElement() 22 | } 23 | 24 | interface MatcherBuilderThreeElement { 25 | fun and(predicate: F.(E1, E2, E3) -> Boolean): MatcherBuilderThreeElement 26 | 27 | fun then(action: F.(E1, E2, E3) -> RESULT) 28 | } 29 | 30 | class MatcherBuilderExtractedThreeElement( 31 | private val matchContext: PatternMatching.MatchContext, 32 | private val extracted: TripleExtractor.Triple, 33 | private val value: F 34 | ) : MatcherBuilderThreeElement { 35 | override fun and(predicate: F.(E1, E2, E3) -> Boolean): MatcherBuilderThreeElement { 36 | return if (!matchContext.fulfilled && value.predicate(extracted.first, extracted.second, extracted.third)) { 37 | this 38 | } else { 39 | FailedMatcherBuilderExtractedThreeElement() 40 | } 41 | } 42 | 43 | override fun then(action: F.(E1, E2, E3) -> RESULT) { 44 | matchContext.fulfilled = true 45 | matchContext.result = value.action(extracted.first, extracted.second, extracted.third) 46 | } 47 | } 48 | 49 | class FailedMatcherBuilderExtractedThreeElement : MatcherBuilderThreeElement { 50 | 51 | override fun and(predicate: F.(E1, E2, E3) -> Boolean) = this 52 | 53 | override fun then(action: F.(E1, E2, E3) -> RESULT) { 54 | //do nothing 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/ua/kurinnyi/kotlin/patternmatching/PatternMatching.kt: -------------------------------------------------------------------------------- 1 | package ua.kurinnyi.kotlin.patternmatching 2 | 3 | import ua.kurinnyi.kotlin.patternmatching.extractors.* 4 | 5 | class MatchError(value: Any?) : Exception("None of provided matchers matches: $value") 6 | 7 | object PatternMatching { 8 | 9 | fun Any?.match(patterns: MatchContext.() -> Unit): RESULT { 10 | val matchContext = MatchContext(this) 11 | patterns(matchContext) 12 | if (!matchContext.fulfilled) 13 | throw MatchError(this) 14 | 15 | return matchContext.result as RESULT 16 | } 17 | 18 | class MatchContext( 19 | val value: Any?, 20 | var result: RESULT? = null, 21 | var fulfilled: Boolean = false 22 | ) { 23 | 24 | inline fun case(): MatcherBuilderOneElement = 25 | case(TypeCheckSingleExtractor()) 26 | 27 | fun caseNull() = case(NullEmptyExtractor()) 28 | 29 | inline fun case(vararg values: T): MatcherBuilderOneElement = 30 | caseE(VarArgAnyMatchSingleExtractor(values)) 31 | 32 | inline fun case(extractor: SingleExtractor) = 33 | caseE(extractor) 34 | 35 | inline fun case(extractor: PairExtractor) = 36 | caseE(extractor) 37 | 38 | inline fun case(extractor: TripleExtractor) = 39 | caseE(extractor) 40 | 41 | inline fun case(extractor: EmptyExtractor) = caseE(extractor) 42 | 43 | fun otherwise(action: () -> RESULT) = case(AlwaysMatchingExtractor()).then { action() } 44 | } 45 | 46 | class TypeCheckSingleExtractor : SingleExtractor { 47 | override fun unapply(from: T): SingleExtractor.Single? = SingleExtractor.Single(from) 48 | } 49 | 50 | class VarArgAnyMatchSingleExtractor(private val values: Array) : SingleExtractor { 51 | 52 | override fun unapply(from: T): SingleExtractor.Single? = 53 | values.takeIf { it.contains(from) }?.let { 54 | SingleExtractor.Single(from) 55 | } 56 | } 57 | 58 | class NullEmptyExtractor : EmptyExtractor { 59 | 60 | override fun unapply(from: Any?): Boolean = from == null 61 | } 62 | 63 | class AlwaysMatchingExtractor : EmptyExtractor { 64 | 65 | override fun unapply(from: Any?) = true 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/kotlin/ua/kurinnyi/kotlin/patternmatching/ListPatterMatching.kt: -------------------------------------------------------------------------------- 1 | package ua.kurinnyi.kotlin.patternmatching 2 | 3 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching.MatchContext 4 | import ua.kurinnyi.kotlin.patternmatching.extractors.* 5 | 6 | typealias head = ListPatterMatching.ListElement.HEAD 7 | typealias tail = ListPatterMatching.ListElement.TAIL 8 | typealias end = ListPatterMatching.ListElement.END 9 | typealias nil = ListPatterMatching.ListElement.END 10 | typealias mid = ListPatterMatching.ListElement.MID 11 | 12 | object ListPatterMatching { 13 | 14 | sealed class ListElement { 15 | object HEAD : ListElement() 16 | object TAIL : ListElement() 17 | object MID : ListElement() 18 | object END : ListElement() 19 | } 20 | 21 | fun List.matchList(patterns: ListMatchContext.() -> Unit): RESULT { 22 | val matchContext = MatchContext(this) 23 | val listMatchContext = ListMatchContext(this, matchContext) 24 | patterns(listMatchContext) 25 | if (!matchContext.fulfilled) 26 | throw MatchError(this) 27 | 28 | return matchContext.result as RESULT 29 | } 30 | 31 | class ListMatchContext( 32 | private val list: List, 33 | private var matchContext: MatchContext 34 | ) { 35 | fun case(first: end) = matchContext.case(NoElementListExtractor()) 36 | 37 | fun case(first: head, last: end) = matchContext.case(OneElementListExtractor()) 38 | 39 | fun case(first: LIST_TYPE, last: end) = 40 | case(head, end).and { head -> head == first } 41 | 42 | fun case(first: head, last: tail) = 43 | matchContext.case(OneElementAndTailListExtractor()) 44 | 45 | fun case(first: head, mid: mid, last: end) = 46 | matchContext.case(TwoElementListExtractor()) 47 | 48 | fun case(first: head, mid: mid, last: tail) = 49 | matchContext.case(TwoElementAndTailListExtractor()) 50 | 51 | fun case(first: LIST_TYPE, last: tail) = 52 | case(head, tail).and { head, _ -> head == first } 53 | 54 | fun case(first: LIST_TYPE, mid: mid, last: end) = 55 | case(head, mid, end).and { head, _ -> head == first } 56 | 57 | fun case(first: LIST_TYPE, second: LIST_TYPE, last: end) = 58 | case(first, mid, end).and { _, mid -> second == mid } 59 | 60 | fun case(first: head, second: LIST_TYPE, last: end) = 61 | case(head, mid, end).and { _, mid -> second == mid } 62 | 63 | fun case(first: LIST_TYPE, mid: mid, last: tail) = 64 | case(head, mid, tail).and { head, _, _ -> first == head } 65 | 66 | fun case(first: LIST_TYPE, second: LIST_TYPE, last: tail) = 67 | case(first, mid, tail).and { _, mid, _ -> mid == second } 68 | 69 | fun case(first: head, second: LIST_TYPE, last: tail) = 70 | case(head, mid, tail).and { _, mid, _ -> mid == second } 71 | 72 | fun otherwise(action: () -> RESULT) = matchContext.case(PatternMatching.AlwaysMatchingExtractor()).then { action() } 73 | } 74 | } 75 | 76 | class OneElementListExtractor() : SingleExtractor, T> { 77 | override fun unapply(from: List) = from.takeIf { it.size == 1 }?.let { 78 | SingleExtractor.Single(it.first()) 79 | } 80 | } 81 | 82 | class NoElementListExtractor() : EmptyExtractor> { 83 | override fun unapply(from: List) = from.isEmpty() 84 | } 85 | 86 | class TwoElementListExtractor() : PairExtractor, T, T> { 87 | override fun unapply(from: List) = from.takeIf { it.size == 2 }?.let { 88 | PairExtractor.Pair(it.first(), it.last()) 89 | } 90 | } 91 | 92 | class OneElementAndTailListExtractor() : PairExtractor, T, List> { 93 | override fun unapply(from: List) = from.takeIf { it.isNotEmpty() }?.let { 94 | PairExtractor.Pair(it.first(), it.takeLast(it.size - 1)) 95 | } 96 | } 97 | 98 | class TwoElementAndTailListExtractor() : TripleExtractor, T, T, List> { 99 | override fun unapply(from: List) = from.takeIf { it.size > 1 }?.let { 100 | TripleExtractor.Triple(it[0], it[1], it.takeLast(it.size - 2)) 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /src/test/kotlin/ua/kurinnyi/kotlin/patternmatching/ListPatterMatchingTest.kt: -------------------------------------------------------------------------------- 1 | package ua.kurinnyi.kotlin.patternmatching 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.Test 5 | import ua.kurinnyi.kotlin.patternmatching.ListPatterMatching.matchList 6 | 7 | class ListPatterMatchingTest { 8 | 9 | @Test 10 | fun shouldMatchIfListIsEmpty() { 11 | val emptyList = emptyList() 12 | val resultNumber: Int = emptyList.matchList { 13 | case(nil).then { 2 + 2 } 14 | } 15 | assertThat(resultNumber).isEqualTo(4) 16 | } 17 | 18 | @Test(expected = MatchError::class) 19 | fun shouldNotMatchIfListIsNotEmpty() { 20 | val list = listOf("") 21 | val resultNumber: Int = list.matchList { 22 | case(nil).then { 2 + 2 } 23 | } 24 | } 25 | 26 | @Test 27 | fun shouldMatchIfListHasOneElement() { 28 | val list = listOf("Hello") 29 | val resultString: String = list.matchList { 30 | case(head, end).then { head -> head.toUpperCase() } 31 | } 32 | assertThat(resultString).isEqualTo("HELLO") 33 | } 34 | 35 | @Test 36 | fun shouldNotMatchIfListHasNoElement() { 37 | val list = listOf() 38 | val resultString: String = list.matchList { 39 | case(head, end).then { head -> head.toUpperCase() } 40 | otherwise { "Goodbye" } 41 | } 42 | assertThat(resultString).isEqualTo("Goodbye") 43 | } 44 | 45 | @Test(expected = MatchError::class) 46 | fun shouldNotMatchIfListHasMoreThanOneElement() { 47 | val list = listOf("Hi", "Hello"); 48 | val resultString: String = list.matchList { 49 | case(head, end).then { head -> head.toUpperCase() } 50 | } 51 | } 52 | 53 | 54 | @Test 55 | fun shouldMatchIfListHasOneExactMatchingElement() { 56 | val list = listOf("Hello") 57 | val resultString: String = list.matchList { 58 | case("Hi", end).then { head -> head } 59 | case("Hello", end).then { head -> head.toUpperCase() } 60 | case("Goodbye", end).then { head -> head } 61 | } 62 | assertThat(resultString).isEqualTo("HELLO") 63 | } 64 | 65 | @Test 66 | fun shouldNotMatchIfListHasNotExactMatchElement() { 67 | val list = listOf("Hi") 68 | val resultString: String = list.matchList { 69 | case("Hello", end).then { head -> head.toUpperCase() } 70 | otherwise { "Goodbye" } 71 | } 72 | assertThat(resultString).isEqualTo("Goodbye") 73 | } 74 | 75 | @Test(expected = MatchError::class) 76 | fun shouldNotMatchIfListHasMoreThanOneElementEvenWithExactMatch() { 77 | val list = listOf("Hi", "Hello"); 78 | val resultString: String = list.matchList { 79 | case("Hi", end).then { head -> head.toUpperCase() } 80 | case("Hello", end).then { head -> head.toUpperCase() } 81 | } 82 | } 83 | 84 | @Test 85 | fun shouldMatchIfListHasAtLeastOneElement() { 86 | val list = listOf("Hello", "Hi") 87 | val resultList: List = list.matchList { 88 | case("Hello", tail).then { head, tail -> 89 | val list = mutableListOf(head) 90 | list.addAll(tail) 91 | list 92 | } 93 | otherwise { emptyList() } 94 | } 95 | assertThat(resultList).isEqualTo(list) 96 | } 97 | 98 | @Test 99 | fun shouldMatchIfListHasExactlyOneElementThenTailIsEmpty() { 100 | val list = listOf("Hello") 101 | val resultString: String = list.matchList { 102 | case("Hello", tail).then { head, tail -> 103 | assertThat(tail).isEmpty() 104 | head.toUpperCase() 105 | } 106 | otherwise { "Goodbye" } 107 | } 108 | assertThat(resultString).isEqualTo("HELLO") 109 | } 110 | 111 | @Test 112 | fun shouldMatchIfListHasExactlyTwoElements() { 113 | val list = listOf("Hello", "Hi") 114 | val resultString: String = list.matchList { 115 | case(head, "Hi", end).then { head, mid -> head + mid } 116 | otherwise { "Goodbye" } 117 | } 118 | assertThat(resultString).isEqualTo("HelloHi") 119 | } 120 | 121 | @Test 122 | fun shouldNotMatchIfListHasMoreThenTwoElements() { 123 | val list = listOf("Hello", "Hi", "End") 124 | val resultString: String = list.matchList { 125 | case(head, "Hi", end).then { head, mid -> head + mid } 126 | otherwise { "Goodbye" } 127 | } 128 | assertThat(resultString).isEqualTo("Goodbye") 129 | } 130 | 131 | @Test 132 | fun shouldMatchIfListHasExactlyTwoElementsThenTailIsEmpty() { 133 | val list = listOf("Hello", "Hi") 134 | val resultString: String = list.matchList { 135 | case("Hello", "Hi", tail).then { head, mid, tail -> 136 | assertThat(tail).isEmpty() 137 | head + mid 138 | } 139 | otherwise { "Goodbye" } 140 | } 141 | assertThat(resultString).isEqualTo("HelloHi") 142 | } 143 | 144 | @Test 145 | fun shouldMatchIfListHasMoreThanTwoElements() { 146 | val list = listOf("Hello", "Hi", "Not end", "End") 147 | val resultString: String = list.matchList { 148 | case(head, mid, tail).then { head, mid, tail -> 149 | assertThat(tail).containsExactly("Not end", "End") 150 | head + mid 151 | } 152 | otherwise { "Goodbye" } 153 | } 154 | assertThat(resultString).isEqualTo("HelloHi") 155 | } 156 | 157 | @Test 158 | fun shouldNotMatchIfAndPartIsNotMatches() { 159 | val list = listOf("Hello", "Hi") 160 | val resultString: String = list.matchList { 161 | case("Hello", mid, end).and { _, mid -> mid != "Hi" }.then { head, mid -> head + mid } 162 | case("Hello", "Hi", end).then { _, _ -> "Result" } 163 | otherwise { "Goodbye" } 164 | } 165 | assertThat(resultString).isEqualTo("Result") 166 | } 167 | 168 | @Test 169 | fun shouldMatchIfAndPartMatches() { 170 | val list = listOf("Hello", "Hi", "HI") 171 | val resultInt: Int = list.matchList { 172 | case("Hello", mid, tail) 173 | .and { _, mid, tail -> tail.contains(mid.toUpperCase()) } 174 | .then { head, _, tail -> head.length + tail.size } 175 | case(head, "Hi", tail).then { _, _, _ -> 4 } 176 | } 177 | assertThat(resultInt).isEqualTo(6) 178 | } 179 | 180 | @Test 181 | fun shouldWorkIfNoReturnValueExpected() { 182 | val list = listOf("Hello", "Hi", "HI") 183 | val resultList = mutableListOf() 184 | list.matchList { 185 | case("Hello", mid, tail).then { head, _, tail -> tail.forEach { resultList.add(it) } } 186 | case(head, "Hi", tail).then { head, _, _ -> resultList.add(head) } 187 | } 188 | assertThat(resultList).containsExactly("HI") 189 | } 190 | 191 | 192 | @Test 193 | fun shouldWorkWithNullableTypesIfReturnValueIsNotNull() { 194 | val list = listOf("Hello", "Hi", "HI") 195 | val resultInt: Int? = list.matchList { 196 | case("Hello", mid, tail).then { head, _, tail -> head.length + tail.size } 197 | case(head, "Hi", tail).then { _, _, _ -> null } 198 | } 199 | assertThat(resultInt).isEqualTo(6) 200 | } 201 | 202 | @Test 203 | fun shouldWorkWithNullableTypesIfReturnValueIsNull() { 204 | val list = listOf("Hello", "Hi", "HI") 205 | val resultInt: Int? = list.matchList { 206 | case("Hello", mid, tail).then { head, _, tail -> null } 207 | case(head, "Hi", tail).then { _, _, _ -> 1 } 208 | } 209 | assertThat(resultInt).isEqualTo(null) 210 | } 211 | 212 | @Test 213 | fun shouldWorkWithListOfNullableValues() { 214 | val list = listOf("Hello", null, "HI") 215 | val resultInt: Int = list.matchList { 216 | case("Hello", mid, tail).then { _, _, _ -> 1 } 217 | case(head, null, tail).then { _, _, _ -> 2 } 218 | } 219 | assertThat(resultInt).isEqualTo(1) 220 | } 221 | 222 | @Test 223 | fun shouldBeAbleToUseNullToMatchAgainstIt() { 224 | val list = listOf("Hello", null, "HI") 225 | val resultInt: Int = list.matchList { 226 | case("Hello", "Hi", tail).then { _, _, _ -> 1 } 227 | case(head, null, tail).then { _, _, _ -> 2 } 228 | } 229 | assertThat(resultInt).isEqualTo(2) 230 | } 231 | } -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ## Pattern matching for kotlin 2 | 3 | This simple library brings functionality of pattern matching to Kotlin. 4 | It uses some complex Kotlin features and combine them in easy to use way. 5 | It is just a library, it does not do any byte code manipulation and it does not 6 | require you to install any plugins to ide or your build tool. 7 | Just add it as a dependency to your project and you are ready to go. 8 | 9 | ### Usage 10 | Best shown with examples. 11 | You can also open tests in this repository to see some more examples. 12 | ```kotlin 13 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching.match 14 | 15 | val result = objectToMatch.match { 16 | case().then { 2 + 2 } //Matches any Int, returns 4 17 | case("Hello, world!").then { 5 } //Matches exact string "Hello world!" 18 | case("a", "b", null, "c").then { 6 } //Matches any of the set "a", "b", "c" or null. Yes you can call match method on null 19 | case().and { contains("Hello") }.then { 7 } //Matches any string with additional guard that it contains word "Hello" 20 | case().then { it.length } //Matches any string that didn't matched before. Returns the size of this string 21 | otherwise { 42 } //In all other cases 22 | } 23 | ``` 24 | Evaluation is completed from top to bottom. 25 | And stops once any case clause matches.
26 | You should always provide `otherwise` handler or ensure you covered 27 | all possible cases. Otherwise `MatchError` is thrown if nothing matches the object. 28 |
29 | The generic parameter of method `match` is the type of returned value. 30 | It can be `Unit` in case if you do not expect any value to be returned. 31 | It can also be nullable type. 32 |
33 | Each `case` usage can be followed by `and` part. 34 | It takes predicate for more precise matching. 35 |
36 | `then` and `and` methods take functions which receive matched objects 37 | as both `this` and `it` values. It gives ability 38 | to write concise code with objects, like bellow: 39 | ```kotlin 40 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching.match 41 | 42 | data class SomeClass(val someString:String, val someInt:Int); 43 | data class SomeOtherClass(val anotherString:String, val anotherInt:Int); 44 | 45 | val objectToMatch = SomeClass("Hello", 15); 46 | objectToMatch.match { 47 | case().and{ someInt < 15}.then{ print(someString + " < ") } 48 | case().and{ anotherInt < 15}.then{ print(anotherString + " other class") } 49 | case().and{ someInt > 15}.then{ print(someString + " > ") } 50 | case().then{ print(someString + " == ") } 51 | } 52 | //prints "Hello ==" 53 | ``` 54 |
55 | 56 | #### Matching of lists 57 | There is some special methods for lists. 58 | Use next available objects, or actual values to define patterns over list: 59 | * `head` - for a first item in list 60 | * `mid` - for a second and next items 61 | * `tail` - for a list of remaining items, may be empty list 62 | * `nil`, `end` - for the end of list 63 | 64 | See example bellow: 65 | ```kotlin 66 | import ua.kurinnyi.kotlin.patternmatching.ListPatterMatching.matchList 67 | import ua.kurinnyi.kotlin.patternmatching.end 68 | import ua.kurinnyi.kotlin.patternmatching.head 69 | import ua.kurinnyi.kotlin.patternmatching.mid 70 | import ua.kurinnyi.kotlin.patternmatching.tail 71 | 72 | val list:List = ... 73 | //Comment bellow show examples that matches corresponding lines 74 | val resultString: String = list.matchList { 75 | // [] 76 | case(end).then { "empty list" } 77 | 78 | // ["It is head"] 79 | case("It is head", end).then { head -> "Exact match of head for list with one element" } 80 | 81 | // ["and part"] 82 | case(head, end).and{ head -> head == "and part" }.then { head -> "Any list with one element with aditional guard" } 83 | 84 | // ["whatever"] 85 | case(head, end).then { head -> "Any lists with one element that left" } 86 | 87 | // ["this is head", "this is mid", "whatever else"] 88 | case(head, "this is mid", tail).then { head, mid, tail -> "Exect math of second element for list with at least two elements" } 89 | 90 | // ["two", "elements"] 91 | case(head, mid, end).then { head, mid -> "Any list with two elements" } 92 | 93 | // ["a", "b", "c", "d"] 94 | case(head, tail).then { head, tail -> "Any list with at least one element" } 95 | } 96 | ``` 97 | Function matchList has two generic parameters `matchList`. 98 | First one is the type of the list and second is returned type which can be `Unit`. 99 | In most cases this parameters can be derived automatically and omitted as in example above. 100 | 101 | #### Using extractors for custom behaviour: 102 | The library uses idea of extractors under the hood. 103 | And it is also open to work with custom extractors. 104 | See example bellow. 105 | ```kotlin 106 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching.match 107 | import ua.kurinnyi.kotlin.patternmatching.extractors.PairExtractor 108 | //define the extractor by implementing PairExtractor interface 109 | object EMAIL : PairExtractor { 110 | override fun unapply(from: String): PairExtractor.Pair? { 111 | val split = from.split("@") 112 | return if (split.size == 2) { 113 | //return PairExtractor.Pair when string is email. Extract two parts from the email 114 | PairExtractor.Pair(split.first(), split.last()) 115 | } else { 116 | //return null if string is not email 117 | null 118 | } 119 | } 120 | } 121 | val result: String = "author@domain.com".match { 122 | //match against EMAIL extractor defined above and use extracted parts 123 | case(EMAIL).then { author, domain -> "Email $this consist from $author and domain $domain" } 124 | otherwise { "Not email" } 125 | } 126 | //result = "Email author@domain.com consist from author and domain domain.com" 127 | ``` 128 | There is next interfaces for extractors: 129 | * *ua.kurinnyi.kotlin.patternmatching.extractors.EmptyExtractor* 130 | use it when you have nothing to extract but still need to match 131 | ```kotlin 132 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching.match 133 | import ua.kurinnyi.kotlin.patternmatching.extractors.EmptyExtractor 134 | 135 | object PALINDROME : EmptyExtractor { 136 | override fun unapply(from: String): Boolean = from.reversed() == from 137 | } 138 | 139 | val result: String = "1234321".match { 140 | //'this' refers to original value 141 | case(PALINDROME).then { "Value $this is palindrome" } 142 | otherwise { "Not palindrome" } 143 | } 144 | //result = "Value 1234321 is palindrome" 145 | ``` 146 | * *ua.kurinnyi.kotlin.patternmatching.extractors.SingleExtractor* 147 | use it when you need to match and convert value 148 | ```kotlin 149 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching.match 150 | import ua.kurinnyi.kotlin.patternmatching.extractors.SingleExtractor 151 | 152 | object INT : SingleExtractor { 153 | override fun unapply(from: String): SingleExtractor.Single? { 154 | return try { 155 | SingleExtractor.Single(from.toInt()) 156 | } catch (e: NumberFormatException) { 157 | null 158 | } 159 | } 160 | } 161 | 162 | val resultNumber: Int = "3".match { 163 | //'this' refers to original value "3" and 'it' to extracted 3 164 | case(INT).then { this.toInt() + it } 165 | otherwise { 2 } 166 | } 167 | //result = 6 168 | ``` 169 | * *ua.kurinnyi.kotlin.patternmatching.extractors.PairExtractor* 170 | use it when you need to match and extract two value from object. 171 | See example with EMAIL above 172 | * *ua.kurinnyi.kotlin.patternmatching.extractors.TripleExtractor* 173 | same as previous but for three extracted values 174 | 175 | 176 | ### Adding to your project 177 | Currently this library is not yet published to any repository. 178 | You can download pre-built jar [from here](https://github.com/Kurinnyi/Kotlin-Pattern-Matching/releases). 179 | Or download this sources and build it yourself with command `gradlew jar`. 180 |
181 | After you got the jar, it is up to you how to manage this dependency. 182 | You can add it to your repository manager, if you have one. 183 | Or link as a local file in your build tool configuration. 184 |
185 | Information for build tools: 186 | * group id `ua.kurinnyi.kotlin` 187 | * artifact name `pattern-matching` 188 | * version `0.1` 189 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /src/test/kotlin/ua/kurinnyi/kotlin/patternmatching/PatternMatchingTest.kt: -------------------------------------------------------------------------------- 1 | package ua.kurinnyi.kotlin.patternmatching 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.Test 5 | import ua.kurinnyi.kotlin.patternmatching.PatternMatching.match 6 | import ua.kurinnyi.kotlin.patternmatching.extractors.EmptyExtractor 7 | import ua.kurinnyi.kotlin.patternmatching.extractors.PairExtractor 8 | import ua.kurinnyi.kotlin.patternmatching.extractors.SingleExtractor 9 | import java.lang.RuntimeException 10 | 11 | class PatternMatchingTest { 12 | 13 | @Test 14 | fun shouldInvokeThenWithObjectWhenTypeMatches() { 15 | var result = "Goodbye" 16 | val someString = "Hello, world!" 17 | someString.match { 18 | case().then { result = this.toUpperCase() } 19 | } 20 | assertThat(result).isEqualTo(someString.toUpperCase()) 21 | } 22 | 23 | @Test(expected = MatchError::class) 24 | fun shouldNotInvokeThenWhenTypeNotMatches() { 25 | var result = "Goodbye" 26 | val someInt = 2 27 | someInt.match { 28 | case().then { result = this.toUpperCase() } 29 | } 30 | } 31 | 32 | @Test 33 | fun shouldInvokeCorrectHandlerByType() { 34 | var resultString = "Goodbye" 35 | var resultInt = 2 36 | 2.match { 37 | case().then { resultString = this.toUpperCase() } 38 | case().then { resultInt += this } 39 | } 40 | assertThat(resultString).isEqualTo("Goodbye") 41 | assertThat(resultInt).isEqualTo(4) 42 | } 43 | 44 | @Test 45 | fun shouldBeAbleToUseInternalFieldOfObjectWhenMatch() { 46 | var resultString = "Goodbye" 47 | SomeDataClass("Hello", 5).match { 48 | case().then { resultString = this.toUpperCase() } 49 | case().then { resultString = someField + someOtherField } 50 | case().then { throw RuntimeException("Fail(((") } 51 | } 52 | assertThat(resultString).isEqualTo("Hello5") 53 | } 54 | 55 | @Test 56 | fun shouldInvokeHandlerWithExactMatch() { 57 | var resultString = "Goodbye" 58 | "Hello".match { 59 | case("Hello").then { resultString = it } 60 | case().then { resultString = this.toUpperCase() } 61 | case().then { resultString = someField + someOtherField } 62 | case().then { throw RuntimeException("Fail(((") } 63 | } 64 | assertThat(resultString).isEqualTo("Hello") 65 | } 66 | 67 | @Test 68 | fun shouldInvokeHandlerWithExactMatchOfFewResult() { 69 | var resultString = "Goodbye" 70 | "Hello".match { 71 | case().then { resultString = someField + someOtherField } 72 | case().then { resultString = someField + someOtherField } 73 | case("Hello1", "Hello2").then { resultString = it } 74 | case("Hello3", "Hello").then { resultString = it } 75 | case().then { throw RuntimeException("Fail(((") } 76 | case().then { resultString = this.toUpperCase() } 77 | } 78 | assertThat(resultString).isEqualTo("Hello") 79 | } 80 | 81 | @Test 82 | fun shouldReturnResultFromThenBlock() { 83 | val resultNumber: Int = "Hello".match { 84 | case().then { 1 } 85 | case("Hello1", "Hello2").then { 2 } 86 | case("Hello3", "Hello").then { length } 87 | case().then { throw RuntimeException("Fail(((") } 88 | } 89 | assertThat(resultNumber).isEqualTo(5) 90 | } 91 | 92 | @Test(expected = MatchError::class) 93 | fun shouldNotReturnResultFromWhenNothingMatch() { 94 | val resultNumber: Int = "Hello".match { 95 | case().then { 1 } 96 | case("Hello1", "Hello2").then { 2 } 97 | case("Hello3").then { length } 98 | case().then { throw RuntimeException("Fail(((") } 99 | } 100 | } 101 | 102 | @Test(expected = MatchError::class) 103 | fun shouldNotMatchTheObjectIfAndBlockFailsToMatch() { 104 | val setOfString = setOf() 105 | val resultNumber: Int = "Hello".match { 106 | case("Hello").and { setOfString.contains(this) }.then { it.length } 107 | } 108 | } 109 | 110 | @Test 111 | fun shouldMatchTheObjectIfAndBlockMatches() { 112 | val setOfString = setOf("Hello") 113 | val resultNumber: Int = "Hello".match { 114 | case("Hello").and { setOfString.contains(this) }.then { it.length } 115 | } 116 | assertThat(resultNumber).isEqualTo(5); 117 | } 118 | 119 | @Test 120 | fun shouldNotMatchTheObjectByTypeIfAndBlockFailsToMatch() { 121 | val objectToMatch = SomeDataClass("Hello", 4) 122 | val resultNumber: Int = objectToMatch.match { 123 | case().then { 3 } 124 | case().and { someField == "Goodbye" }.then { someOtherField + 1 } 125 | otherwise { 6 } 126 | } 127 | assertThat(resultNumber).isEqualTo(6) 128 | } 129 | 130 | @Test 131 | fun shouldMatchTheObjectByTypeIfAndBlockMatches() { 132 | val objectToMatch = SomeDataClass("Hello", 4) 133 | val resultNumber: Int = objectToMatch.match { 134 | case().then { 3 } 135 | case().and { someField == "Hello" }.then { someOtherField + 1 } 136 | otherwise { 6 } 137 | } 138 | assertThat(resultNumber).isEqualTo(5) 139 | } 140 | 141 | @Test 142 | fun shouldNotMatchIfMultipleAndsAndSomeOfThemFailsToMatch() { 143 | val objectToMatch = SomeDataClass("Hello", 4) 144 | val resultNumber = objectToMatch.match { 145 | case().then { 3 } 146 | case() 147 | .and { someField == "Hello" }.and { someOtherField == 5 } 148 | .then { someOtherField + 1 } 149 | otherwise { 6 } 150 | } 151 | assertThat(resultNumber).isEqualTo(6) 152 | } 153 | 154 | @Test 155 | fun shouldMatchIfMultipleAndsAndAllOfThemMatches() { 156 | val objectToMatch = SomeDataClass("Hello", 4) 157 | val resultNumber = objectToMatch.match { 158 | case().then { 3 } 159 | case().and { someField == "Hello" } 160 | .and { someOtherField == 4 }.then { someOtherField + 1 } 161 | otherwise { 6 } 162 | } 163 | assertThat(resultNumber).isEqualTo(5) 164 | } 165 | 166 | @Test 167 | fun shouldWorkWithNullableTypeIfReturnedValueIsNotNull() { 168 | val objectToMatch = SomeDataClass("Hello", 4) 169 | val resultNumber: Int? = objectToMatch.match { 170 | case().then { null } 171 | case().then { someOtherField + 1 } 172 | otherwise { null } 173 | } 174 | assertThat(resultNumber).isEqualTo(5) 175 | } 176 | 177 | @Test 178 | fun shouldWorkWithNullableTypeIfReturnedValueIsNull() { 179 | val objectToMatch = SomeDataClass("Hello", 4) 180 | val resultNumber: Int? = objectToMatch.match { 181 | case().then { 1 } 182 | case().then { null } 183 | otherwise { 2 } 184 | } 185 | assertThat(resultNumber).isEqualTo(null) 186 | } 187 | 188 | @Test 189 | fun shouldBeAbleToUseMatchingOnNull() { 190 | val objectToMatch = null 191 | val resultNumber: Int? = objectToMatch.match { 192 | case().then { 1 } 193 | otherwise { 2 } 194 | } 195 | assertThat(resultNumber).isEqualTo(2) 196 | } 197 | 198 | @Test 199 | fun shouldBeAbleToMatchNull() { 200 | val objectToMatch = null 201 | val resultNumber: Int = objectToMatch.match { 202 | caseNull().then { 1 } 203 | otherwise { 2 } 204 | } 205 | assertThat(resultNumber).isEqualTo(1) 206 | } 207 | 208 | @Test 209 | fun shouldBeAbleToMatchNullInMultipleArguments() { 210 | val objectToMatch = null 211 | val resultNumber: Int = objectToMatch.match { 212 | case(1, null, 2).then { o: Int? -> 1 } 213 | otherwise { 2 } 214 | } 215 | assertThat(resultNumber).isEqualTo(1) 216 | } 217 | 218 | @Test 219 | fun shouldBeAbleToUseCustomExtractor() { 220 | val resultNumber: Int = "3".match { 221 | case(INT).then { this.toInt() + it } 222 | otherwise { 2 } 223 | } 224 | assertThat(resultNumber).isEqualTo(6) 225 | } 226 | 227 | @Test 228 | fun shouldNotMatchCustomExtractorIfItReturnedNull() { 229 | val objectToMatch = "a" 230 | val resultNumber: Int = objectToMatch.match { 231 | case(INT).then { this.toInt() + it } 232 | otherwise { 2 } 233 | } 234 | assertThat(resultNumber).isEqualTo(2) 235 | } 236 | 237 | object INT : SingleExtractor { 238 | override fun unapply(from: String): SingleExtractor.Single? { 239 | return try { 240 | SingleExtractor.Single(from.toInt()) 241 | } catch (e: NumberFormatException) { 242 | null 243 | } 244 | } 245 | } 246 | 247 | @Test 248 | fun shouldBeAbleToUseCustomExtractorWithTwoExtractedValue() { 249 | val objectToMatch = "author@domain.com" 250 | val result: String = objectToMatch.match { 251 | case(EMAIL).then { author, domain -> "Email $this consist from $author and domain $domain" } 252 | otherwise { "Not email" } 253 | } 254 | assertThat(result).isEqualTo("Email author@domain.com consist from author and domain domain.com") 255 | } 256 | 257 | @Test 258 | fun shouldNotMatchCustomExtractorWithTwoExtractedValueIfItReturnsNull() { 259 | val objectToMatch = "authordomain.com" 260 | val result: String = objectToMatch.match { 261 | case(EMAIL).then { author, domain -> "Email $this consist from $author and domain $domain" } 262 | otherwise { "Not email" } 263 | } 264 | assertThat(result).isEqualTo("Not email") 265 | } 266 | 267 | object EMAIL : PairExtractor { 268 | override fun unapply(from: String): PairExtractor.Pair? { 269 | val split = from.split("@") 270 | return if (split.size == 2) { 271 | PairExtractor.Pair(split.first(), split.last()) 272 | } else { 273 | null 274 | } 275 | } 276 | } 277 | 278 | @Test 279 | fun shouldBeAbleToUseCustomExtractorWithNoExtractedValue() { 280 | val result: String = "1234321".match { 281 | case(PALINDROME).then { "Value $this is palindrome" } 282 | otherwise { "Not palindrome" } 283 | } 284 | assertThat(result).isEqualTo("Value 1234321 is palindrome") 285 | } 286 | 287 | 288 | object PALINDROME : EmptyExtractor { 289 | override fun unapply(from: String): Boolean = from.reversed() == from 290 | } 291 | 292 | 293 | data class SomeDataClass( 294 | val someField: String, 295 | val someOtherField: Int 296 | ) 297 | 298 | data class SomeOtherDataClass( 299 | val superField: Int 300 | ) 301 | } --------------------------------------------------------------------------------