├── gradle.properties ├── .gitignore ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── example ├── src │ └── main │ │ └── kotlin │ │ ├── Main.kt │ │ ├── units │ │ ├── TimeUnitKey.kt │ │ ├── parse │ │ │ ├── DefaultParseUnitKey.kt │ │ │ ├── ParseTime.kt │ │ │ └── ParseDistance.kt │ │ ├── DistanceUnitKey.kt │ │ └── calculate │ │ │ ├── DefaultUnitsConvert.kt │ │ │ ├── TimeConvert.kt │ │ │ └── DistanceConvert.kt │ │ ├── MathExpressionExample.kt │ │ ├── UnitsExpressionExample.kt │ │ └── operator │ │ └── ModOperator.kt └── build.gradle.kts ├── math ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── me │ └── y9san9 │ └── calkt │ └── math │ ├── InfixKey.kt │ ├── parse │ ├── MathParseInfixKeyFunction.kt │ ├── MathParseOperandFunction.kt │ ├── ParseMathExpression.kt │ ├── MathParse.kt │ ├── DefaultMathParseOperand.kt │ ├── MathParseInfixOperatorList.kt │ ├── MathParseInfixOperator.kt │ ├── MathParseOperandFunctionPlus.kt │ ├── MathParseInfixKeyFunctionPlus.kt │ ├── DefaultMathInfixOperators.kt │ └── MathParseGroupFunction.kt │ ├── annotation │ └── InfixKeySubclass.kt │ ├── calculate │ ├── MathCalculateSuccess.kt │ ├── CalculateMathExpression.kt │ ├── MathCalculateInfixOperatorFunction.kt │ ├── MathCalculateFailure.kt │ ├── MathCalculate.kt │ ├── DefaultMathCalculateInfixOperator.kt │ └── MathCalculateInfixOperatorFunctionPlus.kt │ ├── DefaultInfixKeys.kt │ └── MathExpression.kt ├── units ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── me │ │ └── y9san9 │ │ └── calkt │ │ └── units │ │ ├── UnitKey.kt │ │ ├── annotation │ │ └── UnitKeySubclass.kt │ │ ├── calculate │ │ ├── UnitsCalculateSuccess.kt │ │ ├── UnitsCalculateFailure.kt │ │ ├── UnitsConvertFunction.kt │ │ ├── CalculateUnitsExpression.kt │ │ ├── UnitsCalculate.kt │ │ ├── UnitsConvertFunctionPlus.kt │ │ └── UnitsMathCalculateInfixOperator.kt │ │ ├── parse │ │ ├── ParseUnitsExpression.kt │ │ ├── cause │ │ │ └── ExpectedUnitsCause.kt │ │ ├── UnitsParseUnitKeyFunctionPlus.kt │ │ ├── UnitsParse.kt │ │ ├── UnitsParseUnitKeyFunction.kt │ │ └── UnitsMathParseOperand.kt │ │ └── UnitsExpression.kt │ └── commonTest │ └── kotlin │ └── me │ └── y9san9 │ └── calkt │ └── units │ └── parse │ ├── UnitsParseUnitKeyFunction.kt │ └── UnitsMathParseOperand.kt ├── core ├── src │ └── commonMain │ │ └── kotlin │ │ └── me │ │ └── y9san9 │ │ └── calkt │ │ ├── calculate │ │ ├── CalculateFunction.kt │ │ ├── CalculateResult.kt │ │ ├── CalculateFunctionPlus.kt │ │ └── CalculateContext.kt │ │ ├── parse │ │ ├── base │ │ │ ├── Whitespace.kt │ │ │ ├── StartsWith.kt │ │ │ ├── First.kt │ │ │ ├── Remove.kt │ │ │ ├── TakeWhile.kt │ │ │ ├── Drop.kt │ │ │ ├── Token.kt │ │ │ ├── OverrideCause.kt │ │ │ ├── Consume.kt │ │ │ ├── ParseAny.kt │ │ │ ├── Word.kt │ │ │ └── Integer.kt │ │ ├── ParseFunction.kt │ │ ├── cause │ │ │ ├── FailureCause.kt │ │ │ ├── HasRemainingInput.kt │ │ │ ├── ExpectedInputCause.kt │ │ │ ├── MessageCause.kt │ │ │ ├── MultipleCauses.kt │ │ │ └── ToConsoleString.kt │ │ ├── ParseFunctionPlus.kt │ │ ├── ParserState.kt │ │ ├── ParseContext.kt │ │ ├── TryParse.kt │ │ └── ParseResult.kt │ │ ├── Expression.kt │ │ ├── internal │ │ └── StringBuilder.kt │ │ ├── annotation │ │ ├── ExpressionSubclass.kt │ │ ├── CalculateSubclass.kt │ │ └── FailureCauseSubclass.kt │ │ └── number │ │ ├── PreciseNumberParser.kt │ │ └── PreciseNumber.kt └── build.gradle.kts ├── .github └── workflows │ ├── test.yml │ └── publish.yml ├── settings.gradle.kts ├── LICENSE.md ├── README.md ├── gradlew.bat └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | .idea 4 | .kotlin 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y9san9/calkt/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | fun main() { 2 | mathExpressionExample() 3 | println() 4 | unitsExpressionExample() 5 | } 6 | -------------------------------------------------------------------------------- /example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | } 4 | 5 | dependencies { 6 | implementation(projects.math) 7 | implementation(projects.units) 8 | } 9 | -------------------------------------------------------------------------------- /math/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | } 4 | 5 | version = libs.versions.calkt.get() 6 | 7 | dependencies { 8 | commonMainApi(projects.core) 9 | } 10 | -------------------------------------------------------------------------------- /units/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | } 4 | 5 | version = libs.versions.calkt.get() 6 | 7 | dependencies { 8 | commonMainApi(projects.math) 9 | } 10 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/calculate/CalculateFunction.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.calculate 2 | 3 | public fun interface CalculateFunction { 4 | public operator fun invoke(context: CalculateContext): CalculateResult.Success 5 | } 6 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/Whitespace.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | 5 | public fun ParseContext.whitespace() { 6 | dropWhile { char -> char.isWhitespace() } 7 | } 8 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/ParseFunction.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | 5 | public fun interface ParseFunction { 6 | public operator fun invoke(context: ParseContext): Expression 7 | } 8 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/Expression.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSubclassOptIn::class) 2 | 3 | package me.y9san9.calkt 4 | 5 | import me.y9san9.calkt.annotation.ExpressionSubclass 6 | 7 | @SubclassOptInRequired(ExpressionSubclass::class) 8 | public interface Expression 9 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/InfixKey.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math 2 | 3 | import me.y9san9.calkt.math.annotation.InfixKeySubclass 4 | 5 | @OptIn(ExperimentalSubclassOptIn::class) 6 | @SubclassOptInRequired(InfixKeySubclass::class) 7 | public interface InfixKey 8 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/UnitKey.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units 2 | 3 | import me.y9san9.calkt.units.annotation.UnitKeySubclass 4 | 5 | @OptIn(ExperimentalSubclassOptIn::class) 6 | @SubclassOptInRequired(UnitKeySubclass::class) 7 | public interface UnitKey 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jul 22 16:45:31 MSK 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kmp-library-convention") 3 | } 4 | 5 | version = libs.versions.calkt.get() 6 | 7 | dependencies { 8 | commonMainImplementation(libs.bignum) 9 | commonTestImplementation(projects.math) 10 | commonTestImplementation(projects.units) 11 | } 12 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/MathParseInfixKeyFunction.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.math.InfixKey 4 | import me.y9san9.calkt.parse.ParseContext 5 | 6 | public interface MathParseInfixKeyFunction { 7 | public operator fun invoke(context: ParseContext): InfixKey 8 | } 9 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/MathParseOperandFunction.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.parse.ParseContext 5 | 6 | public fun interface MathParseOperandFunction { 7 | public operator fun invoke(context: ParseContext): Expression 8 | } 9 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/internal/StringBuilder.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.internal 2 | 3 | @PublishedApi 4 | internal inline fun StringBuilder.withIndent( 5 | indent: String = " ", 6 | block: StringBuilder.() -> Unit 7 | ) { 8 | val string = buildString(block).prependIndent(indent) 9 | append(string) 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/annotation/ExpressionSubclass.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.annotation 2 | 3 | @RequiresOptIn( 4 | message = "Usage of Expression type is heavily dependent on knowledge of all subclasses. " + 5 | "So when you subclass Expression those places might brake, be careful", 6 | ) 7 | public annotation class ExpressionSubclass 8 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/annotation/CalculateSubclass.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.annotation 2 | 3 | @RequiresOptIn( 4 | message = "Usage of CalculateResult type is heavily dependent on knowledge of all subclasses. " + 5 | "So when you subclass CalculateResult those places might brake, be careful", 6 | ) 7 | public annotation class CalculateSubclass 8 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/annotation/FailureCauseSubclass.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.annotation 2 | 3 | @RequiresOptIn( 4 | message = "Usage of FailureCause type is heavily dependent on knowledge of all subclasses. " + 5 | "So when you subclass FailureCause those places might brake, be careful", 6 | ) 7 | public annotation class FailureCauseSubclass 8 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/annotation/InfixKeySubclass.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.annotation 2 | 3 | @RequiresOptIn( 4 | message = "Usage of CalktExpression.Infix type is heavily dependent on knowledge of all subclasses. " + 5 | "So when you subclass CalktExpression.Infix those places might brake, be careful", 6 | ) 7 | public annotation class InfixKeySubclass 8 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/annotation/UnitKeySubclass.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.annotation 2 | 3 | @RequiresOptIn( 4 | message = "Usage of UnitsExpression.Unit type is heavily dependent on knowledge of all subclasses. " + 5 | "So when you subclass UnitsExpression.Unit those places might brake, be careful", 6 | ) 7 | public annotation class UnitKeySubclass 8 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/calculate/MathCalculateSuccess.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.calculate 2 | 3 | import me.y9san9.calkt.annotation.CalculateSubclass 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.number.PreciseNumber 6 | 7 | @OptIn(CalculateSubclass::class) 8 | public data class MathCalculateSuccess(val number: PreciseNumber) : CalculateResult.Success 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Library Release Deploy 2 | 3 | on: 4 | push: 5 | branches-ignore: [ "main" ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | 10 | test-jvm: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Gradle Cache Setup 15 | uses: gradle/gradle-build-action@v2.4.2 16 | - name: Gradle Check 17 | run: ./gradlew jvmTest 18 | -------------------------------------------------------------------------------- /example/src/main/kotlin/units/TimeUnitKey.kt: -------------------------------------------------------------------------------- 1 | package units 2 | 3 | import me.y9san9.calkt.units.UnitKey 4 | import me.y9san9.calkt.units.annotation.UnitKeySubclass 5 | 6 | @OptIn(UnitKeySubclass::class) 7 | sealed interface TimeUnitKey : UnitKey { 8 | data object Hours : TimeUnitKey 9 | data object Minutes : TimeUnitKey 10 | data object Seconds : TimeUnitKey 11 | data object Millis : TimeUnitKey 12 | } 13 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/StartsWith.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | 5 | public fun ParseContext.startsWith(string: String): Boolean { 6 | for (i in string.indices) { 7 | val char = this.string.getOrNull(index = position + i) ?: return false 8 | if (char != string[i]) return false 9 | } 10 | return true 11 | } 12 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/cause/FailureCause.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.cause 2 | 3 | import me.y9san9.calkt.annotation.FailureCauseSubclass 4 | import me.y9san9.calkt.parse.ParseResult 5 | 6 | @OptIn(ExperimentalSubclassOptIn::class) 7 | @SubclassOptInRequired(FailureCauseSubclass::class) 8 | public interface FailureCause { 9 | public val failure: ParseResult.Failure? get() = null 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/First.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | import me.y9san9.calkt.parse.cause.FailureCause 5 | 6 | public inline fun ParseContext.firstChar( 7 | cause: () -> FailureCause 8 | ): Char { 9 | return firstCharOrNull() ?: fail(cause()) 10 | } 11 | 12 | public fun ParseContext.firstCharOrNull(): Char? { 13 | return string.getOrNull(position) 14 | } 15 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | kotlin = "2.0.0" 4 | bignum = "0.3.10" 5 | maven-publish = "0.29.0" 6 | 7 | calkt = "0.0.5" 8 | 9 | [libraries] 10 | 11 | bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "bignum" } 12 | 13 | # gradle plugins 14 | kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 15 | maven-publish-gradle-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } 16 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/calculate/UnitsCalculateSuccess.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.calculate 2 | 3 | import me.y9san9.calkt.annotation.CalculateSubclass 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.number.PreciseNumber 6 | import me.y9san9.calkt.units.UnitKey 7 | 8 | @OptIn(CalculateSubclass::class) 9 | public data class UnitsCalculateSuccess( 10 | val number: PreciseNumber, 11 | val key: UnitKey 12 | ) : CalculateResult.Success 13 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/calculate/UnitsCalculateFailure.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.calculate 2 | 3 | import me.y9san9.calkt.annotation.CalculateSubclass 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | 6 | @OptIn(CalculateSubclass::class) 7 | public sealed interface UnitsCalculateFailure : CalculateResult.Failure { 8 | public data object UnsupportedConversion : UnitsCalculateFailure 9 | public data object UnitsCantBeConverted : UnitsCalculateFailure 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/Remove.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | import me.y9san9.calkt.parse.cause.FailureCause 5 | 6 | public inline fun ParseContext.removeFirstChar(cause: () -> FailureCause): Char { 7 | return removeFirstCharOrNull() ?: fail(cause()) 8 | } 9 | 10 | public fun ParseContext.removeFirstCharOrNull(): Char? { 11 | return try { 12 | firstCharOrNull() 13 | } finally { 14 | dropFirst() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/src/main/kotlin/units/parse/DefaultParseUnitKey.kt: -------------------------------------------------------------------------------- 1 | package units.parse 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | import me.y9san9.calkt.units.UnitKey 5 | import me.y9san9.calkt.units.parse.UnitsParseUnitKeyFunction 6 | import me.y9san9.calkt.units.parse.plus 7 | 8 | object DefaultParseUnitKey : UnitsParseUnitKeyFunction { 9 | val function = ParseDistance.function + ParseTime.function 10 | 11 | override fun invoke(context: ParseContext): UnitKey { 12 | return function(context) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/calculate/CalculateMathExpression.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | 6 | public fun CalculateContext.calculateMathExpression( 7 | calculateInfixOperator: MathCalculateInfixOperatorFunction = DefaultMathCalculateInfixOperator 8 | ): CalculateResult.Success { 9 | val calculate = MathCalculate(calculateInfixOperator) 10 | return calculate(context) 11 | } 12 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/ParseMathExpression.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.parse.ParseContext 5 | 6 | public fun ParseContext.parseMathExpression( 7 | parseOperand: MathParseOperandFunction = DefaultMathParseOperand, 8 | infixOperatorList: List = DefaultMathInfixOperators.list, 9 | ): Expression { 10 | val parse = MathParse(parseOperand, infixOperatorList) 11 | return parse(context) 12 | } 13 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/TakeWhile.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | 5 | public inline fun ParseContext.takeWhile(block: (Char) -> Boolean): String { 6 | val string = StringBuilder() 7 | while (true) { 8 | if (position == this.string.length) return string.toString() 9 | val char = this.string[position] 10 | if (!block(char)) return string.toString() 11 | string.append(char) 12 | position++ 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/calculate/MathCalculateInfixOperatorFunction.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.math.InfixKey 6 | 7 | public fun interface MathCalculateInfixOperatorFunction { 8 | public operator fun invoke( 9 | context: CalculateContext, 10 | left: CalculateResult.Success, 11 | right: CalculateResult.Success, 12 | key: InfixKey 13 | ): CalculateResult.Success 14 | } 15 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/Drop.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | 5 | public inline fun ParseContext.dropWhile(block: (Char) -> Boolean) { 6 | while (true) { 7 | if (position == string.length) return 8 | val char = string[position] 9 | if (!block(char)) break 10 | position++ 11 | } 12 | } 13 | 14 | public fun ParseContext.dropFirst() { 15 | drop(n = 1) 16 | } 17 | 18 | public fun ParseContext.drop(n: Int) { 19 | position = (position + n).coerceAtMost(string.length) 20 | } 21 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/Token.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | import me.y9san9.calkt.parse.cause.FailureCause 5 | 6 | public inline fun ParseContext.token(block: () -> R): R { 7 | whitespace() 8 | return try { 9 | block() 10 | } finally { 11 | whitespace() 12 | } 13 | } 14 | 15 | public fun ParseContext.token( 16 | string: String, 17 | vararg strings: String, 18 | cause: () -> FailureCause, 19 | ) { 20 | token { consume(string, *strings, cause = cause) } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/OverrideCause.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseResult 4 | import me.y9san9.calkt.parse.ParseContext 5 | import me.y9san9.calkt.parse.cause.FailureCause 6 | import me.y9san9.calkt.parse.tryParse 7 | 8 | public inline fun ParseContext.overrideCause( 9 | cause: (ParseResult.Failure) -> FailureCause, 10 | block: () -> T 11 | ): T { 12 | return when (val result = tryParse(block)) { 13 | is ParseResult.Failure -> fail(cause(result)) 14 | is ParseResult.Success -> result.value 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/cause/HasRemainingInput.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.cause 2 | 3 | public interface HasRemainingInput : ExpectedInputCause { 4 | public val parsedValue: T 5 | override val string: String 6 | 7 | public companion object { 8 | public fun of(value: T): HasRemainingInput { 9 | return object : HasRemainingInput, ExpectedInputCause by ExpectedInputCause.of("EOF") { 10 | override val parsedValue = value 11 | override fun toString() = string 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/calculate/MathCalculateFailure.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.calculate 2 | 3 | import me.y9san9.calkt.annotation.CalculateSubclass 4 | import me.y9san9.calkt.calculate.CalculateContext 5 | import me.y9san9.calkt.calculate.CalculateResult 6 | 7 | @OptIn(CalculateSubclass::class) 8 | public sealed interface MathCalculateFailure : CalculateResult.Failure { 9 | public data object UnsupportedInfixOperator : MathCalculateFailure 10 | } 11 | 12 | public fun CalculateContext.unsupportedInfixOperator(): Nothing { 13 | fail(MathCalculateFailure.UnsupportedInfixOperator) 14 | } 15 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/MathParse.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.parse.ParseFunction 5 | import me.y9san9.calkt.parse.ParseContext 6 | 7 | public class MathParse( 8 | operand: MathParseOperandFunction, 9 | parseInfixKeyList: List 10 | ) : ParseFunction { 11 | private val parseInfixOperatorList = MathParseInfixOperatorList(operand, parseInfixKeyList) 12 | 13 | override fun invoke(context: ParseContext): Expression { 14 | return parseInfixOperatorList(context) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/Consume.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | import me.y9san9.calkt.parse.cause.FailureCause 5 | 6 | public fun ParseContext.tryConsume(vararg strings: String): Boolean { 7 | for (string in strings) { 8 | if (startsWith(string)) { 9 | drop(string.length) 10 | return true 11 | } 12 | } 13 | return false 14 | } 15 | 16 | public inline fun ParseContext.consume( 17 | vararg strings: String, 18 | cause: () -> FailureCause 19 | ) { 20 | if (!tryConsume(*strings)) fail(cause()) 21 | } 22 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/ParseAny.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | import me.y9san9.calkt.parse.elseTry 5 | import me.y9san9.calkt.parse.getOrFail 6 | import me.y9san9.calkt.parse.tryParse 7 | 8 | public fun ParseContext.parseFirstOf( 9 | blocks: List<() -> T> 10 | ): T { 11 | var result = tryParse(blocks.first()) 12 | var index = 1 13 | 14 | while (true) { 15 | if (blocks.size == index) break 16 | result = result.elseTry(context) { blocks[index]() } 17 | index++ 18 | } 19 | 20 | return result.getOrFail(context) 21 | } 22 | -------------------------------------------------------------------------------- /example/src/main/kotlin/units/DistanceUnitKey.kt: -------------------------------------------------------------------------------- 1 | package units 2 | 3 | import me.y9san9.calkt.units.UnitKey 4 | import me.y9san9.calkt.units.annotation.UnitKeySubclass 5 | 6 | @OptIn(UnitKeySubclass::class) 7 | sealed interface DistanceUnitKey : UnitKey { 8 | // Metric 9 | data object Kilometers : DistanceUnitKey 10 | data object Meters : DistanceUnitKey 11 | data object Centimeters : DistanceUnitKey 12 | data object Millimeters : DistanceUnitKey 13 | 14 | // Imperial 15 | data object Inches : DistanceUnitKey 16 | data object Feet : DistanceUnitKey 17 | data object Yards : DistanceUnitKey 18 | data object Miles : DistanceUnitKey 19 | } 20 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/DefaultMathParseOperand.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.math.MathExpression 5 | import me.y9san9.calkt.number.nextPreciseNumber 6 | import me.y9san9.calkt.parse.ParseContext 7 | 8 | public object DefaultMathParseOperand : MathParseOperandFunction { 9 | 10 | private val operand = MathParseOperandFunction { context -> 11 | MathExpression.Number(context.nextPreciseNumber()) 12 | } 13 | 14 | override fun invoke(context: ParseContext): Expression { 15 | val parseGroup = MathParseGroupFunction(operand) 16 | return parseGroup(context) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | rootProject.name = "calkt" 4 | 5 | pluginManagement { 6 | repositories { 7 | gradlePluginPortal() 8 | mavenCentral() 9 | google() 10 | } 11 | } 12 | 13 | dependencyResolutionManagement { 14 | repositories { 15 | mavenCentral() 16 | google() 17 | maven { 18 | url = uri("https://maven.pkg.github.com/meetacy/di") 19 | credentials { 20 | username = System.getenv("GITHUB_USERNAME") 21 | password = System.getenv("GITHUB_TOKEN") 22 | } 23 | } 24 | } 25 | } 26 | 27 | includeBuild("build-logic") 28 | 29 | include("core", "units", "math", "example") 30 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/cause/ExpectedInputCause.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.cause 2 | 3 | import me.y9san9.calkt.parse.ParseResult 4 | 5 | public interface ExpectedInputCause : MessageCause { 6 | public val expected: String 7 | 8 | public companion object { 9 | public fun of( 10 | expected: String, 11 | failure: ParseResult.Failure? = null 12 | ): ExpectedInputCause { 13 | return object : ExpectedInputCause { 14 | override val expected = expected 15 | override val string = "Expected $expected" 16 | override val failure = failure 17 | override fun toString() = string 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/calculate/UnitsConvertFunction.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.number.PreciseNumber 6 | import me.y9san9.calkt.units.UnitKey 7 | import me.y9san9.calkt.math.calculate.MathCalculateSuccess 8 | 9 | public fun interface UnitsConvertFunction { 10 | /** 11 | * @returns either [MathCalculateSuccess] with number in it, or [UnitsCalculateFailure.UnitsCantBeConverted] 12 | */ 13 | public operator fun invoke( 14 | context: CalculateContext, 15 | value: PreciseNumber, 16 | from: UnitKey, 17 | to: UnitKey 18 | ): CalculateResult 19 | } 20 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/cause/MessageCause.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.cause 2 | 3 | import me.y9san9.calkt.annotation.FailureCauseSubclass 4 | import me.y9san9.calkt.parse.ParseResult 5 | 6 | @OptIn(FailureCauseSubclass::class) 7 | public interface MessageCause : FailureCause { 8 | public val string: String 9 | 10 | public companion object { 11 | public fun of( 12 | string: String, 13 | failure: ParseResult.Failure? = null 14 | ): MessageCause { 15 | return object : MessageCause { 16 | override val string = string 17 | override val failure = failure 18 | override fun toString() = string 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/Word.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | import me.y9san9.calkt.parse.cause.FailureCause 5 | 6 | public fun ParseContext.nextWord(): String { 7 | val string = token { takeWhile { char -> char.isLetter() } } 8 | return string 9 | } 10 | 11 | public fun ParseContext.tryWord( 12 | vararg strings: String 13 | ): Boolean { 14 | val backupPosition = this.position 15 | val word = nextWord() 16 | if (word !in strings) { 17 | this.position = backupPosition 18 | return false 19 | } 20 | return true 21 | } 22 | 23 | public fun ParseContext.word( 24 | vararg strings: String, 25 | cause: () -> FailureCause 26 | ) { 27 | if (!tryWord(*strings)) fail(cause()) 28 | } 29 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/cause/MultipleCauses.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.cause 2 | 3 | public interface MultipleCauses : MessageCause { 4 | public val causes: List 5 | 6 | public companion object { 7 | public fun of(vararg causes: FailureCause): MultipleCauses { 8 | return object : MultipleCauses { 9 | override val causes = causes.flatMap(FailureCause::causes).toList() 10 | override val string = "None of parsers could parse this input. Causes: \n${causes.joinToString(separator = "\n").prependIndent("- ")}" 11 | override fun toString() = string 12 | } 13 | } 14 | } 15 | } 16 | 17 | public val FailureCause.causes: List get() = when (this) { 18 | is MultipleCauses -> causes 19 | else -> listOf(this) 20 | } 21 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/ParseUnitsExpression.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.math.parse.DefaultMathInfixOperators 5 | import me.y9san9.calkt.math.parse.DefaultMathParseOperand 6 | import me.y9san9.calkt.math.parse.MathParse 7 | import me.y9san9.calkt.parse.ParseContext 8 | 9 | public fun ParseContext.parseUnitsExpression(parseUnitKey: UnitsParseUnitKeyFunction): Expression { 10 | val parseOperand = DefaultMathParseOperand 11 | val unitsParseOperand = UnitsMathParseOperand(parseUnitKey, parseOperand) 12 | 13 | val infixOperatorList = DefaultMathInfixOperators.list 14 | 15 | val mathParse = MathParse(unitsParseOperand, infixOperatorList) 16 | val parse = UnitsParse(mathParse, parseUnitKey) 17 | 18 | return parse(context) 19 | } 20 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/DefaultInfixKeys.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math 2 | 3 | import me.y9san9.calkt.math.annotation.InfixKeySubclass 4 | 5 | @OptIn(InfixKeySubclass::class) 6 | public sealed interface DefaultInfixKeys : InfixKey { 7 | public data object Plus : DefaultInfixKeys { 8 | override fun toString(): String { 9 | return "plus" 10 | } 11 | } 12 | public data object Minus : DefaultInfixKeys { 13 | override fun toString(): String { 14 | return "minus" 15 | } 16 | } 17 | public data object Times : DefaultInfixKeys { 18 | override fun toString(): String { 19 | return "times" 20 | } 21 | } 22 | public data object Div : DefaultInfixKeys { 23 | override fun toString(): String { 24 | return "div" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/ParseFunctionPlus.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.parse.base.parseFirstOf 5 | 6 | private class CombinedParseFunction(val functions: List) : ParseFunction { 7 | override fun invoke(context: ParseContext): Expression { 8 | return context.parseFirstOf( 9 | blocks = functions.map { function -> 10 | { function(context) } 11 | } 12 | ) 13 | } 14 | } 15 | 16 | private val ParseFunction.functions: List get() = when (this) { 17 | is CombinedParseFunction -> this.functions 18 | else -> listOf(this) 19 | } 20 | 21 | public operator fun ParseFunction.plus(other: ParseFunction): ParseFunction { 22 | return CombinedParseFunction(functions = this.functions + other.functions) 23 | } 24 | -------------------------------------------------------------------------------- /example/src/main/kotlin/units/parse/ParseTime.kt: -------------------------------------------------------------------------------- 1 | package units.parse 2 | 3 | import me.y9san9.calkt.units.parse.UnitsParseUnitKeyFunction 4 | import me.y9san9.calkt.units.parse.plus 5 | import units.TimeUnitKey 6 | 7 | object ParseTime { 8 | val hours = UnitsParseUnitKeyFunction.ofStrings( 9 | TimeUnitKey.Hours, 10 | "h", "hour", "hours" 11 | ) 12 | val minutes = UnitsParseUnitKeyFunction.ofStrings( 13 | TimeUnitKey.Minutes, 14 | "min", "mins", "minute", "minutes" 15 | ) 16 | val seconds = UnitsParseUnitKeyFunction.ofStrings( 17 | TimeUnitKey.Seconds, 18 | "sec", "second", "seconds" 19 | ) 20 | val milliseconds = UnitsParseUnitKeyFunction.ofStrings( 21 | TimeUnitKey.Millis, 22 | "millis", "millisecond", "milliseconds" 23 | ) 24 | 25 | val function = hours + minutes + seconds + minutes + milliseconds 26 | } 27 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/cause/ExpectedUnitsCause.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.parse.cause 2 | 3 | import me.y9san9.calkt.parse.ParseResult 4 | import me.y9san9.calkt.parse.cause.ExpectedInputCause 5 | 6 | public interface ExpectedUnitsCause : ExpectedInputCause { 7 | public val expectedList: List 8 | 9 | public companion object { 10 | public fun of( 11 | vararg expected: String, 12 | failure: ParseResult.Failure? = null 13 | ): ExpectedUnitsCause { 14 | return object : ExpectedUnitsCause, ExpectedInputCause by ExpectedInputCause.of( 15 | expected = "Unit: ${expected.joinToString()}", 16 | failure = failure 17 | ) { 18 | override val expectedList: List = expected.toList() 19 | override fun toString() = string 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/UnitsExpression.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.annotation.ExpressionSubclass 5 | import me.y9san9.calkt.math.MathExpression 6 | 7 | @OptIn(ExpressionSubclass::class) 8 | public sealed interface UnitsExpression : Expression { 9 | public data class Conversion( 10 | val value: Expression, 11 | val key: UnitKey 12 | ) : UnitsExpression { 13 | public fun optimize(): Conversion { 14 | return when { 15 | value !is Conversion -> this 16 | value.key == key -> value 17 | else -> this 18 | } 19 | } 20 | 21 | override fun toString(): String { 22 | return when (value) { 23 | is MathExpression.Number -> "$value $key" 24 | else -> "($value).convert($key)" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/src/main/kotlin/units/calculate/DefaultUnitsConvert.kt: -------------------------------------------------------------------------------- 1 | package units.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.number.PreciseNumber 6 | import me.y9san9.calkt.units.UnitKey 7 | import me.y9san9.calkt.units.calculate.UnitsCalculateFailure 8 | import me.y9san9.calkt.units.calculate.UnitsConvertFunction 9 | import units.DistanceUnitKey 10 | import units.TimeUnitKey 11 | 12 | object DefaultUnitsConvert : UnitsConvertFunction { 13 | override fun invoke(context: CalculateContext, value: PreciseNumber, from: UnitKey, to: UnitKey): CalculateResult { 14 | return when { 15 | from is DistanceUnitKey && to is DistanceUnitKey -> DistanceConvert(context, value, from, to) 16 | from is TimeUnitKey && to is TimeUnitKey -> TimeConvert(context, value, from, to) 17 | else -> UnitsCalculateFailure.UnitsCantBeConverted 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/calculate/CalculateResult.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.calculate 2 | 3 | import me.y9san9.calkt.annotation.CalculateSubclass 4 | 5 | public sealed interface CalculateResult { 6 | @OptIn(ExperimentalSubclassOptIn::class) 7 | @SubclassOptInRequired(CalculateSubclass::class) 8 | public interface Failure : CalculateResult 9 | 10 | @OptIn(CalculateSubclass::class) 11 | public data object UnsupportedExpression : Failure 12 | 13 | @OptIn(CalculateSubclass::class) 14 | public data object DivisionByZero : Failure 15 | 16 | @OptIn(ExperimentalSubclassOptIn::class) 17 | @SubclassOptInRequired(CalculateSubclass::class) 18 | public interface Success : CalculateResult 19 | } 20 | 21 | public fun CalculateResult.getOrFail(context: CalculateContext): CalculateResult.Success { 22 | return when (this) { 23 | is CalculateResult.Failure -> context.fail(failure = this) 24 | is CalculateResult.Success -> this 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/calculate/CalculateUnitsExpression.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.calculate.plus 6 | import me.y9san9.calkt.math.calculate.DefaultMathCalculateInfixOperator 7 | import me.y9san9.calkt.math.calculate.MathCalculate 8 | import me.y9san9.calkt.math.calculate.MathCalculateInfixOperatorFunction 9 | import me.y9san9.calkt.math.calculate.plus 10 | 11 | private val defaultCalculateInfixOperator = DefaultMathCalculateInfixOperator + UnitsMathCalculateInfixOperator 12 | 13 | public fun CalculateContext.calculateUnitsExpression( 14 | convert: UnitsConvertFunction, 15 | calculateInfixOperator: MathCalculateInfixOperatorFunction = defaultCalculateInfixOperator 16 | ): CalculateResult.Success { 17 | val function = MathCalculate(calculateInfixOperator) + UnitsCalculate(convert) 18 | return function(context) 19 | } 20 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/MathParseInfixOperatorList.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.parse.ParseContext 5 | 6 | public class MathParseInfixOperatorList( 7 | private val operand: MathParseOperandFunction, 8 | private val parseInfixKeyList: List 9 | ) { 10 | public operator fun invoke(context: ParseContext): Expression { 11 | var result: MathParseOperandFunction = operand 12 | 13 | for (parseInfixKey in parseInfixKeyList) { 14 | result = infixOperand(result, parseInfixKey) 15 | } 16 | 17 | return result(context) 18 | } 19 | 20 | private fun infixOperand( 21 | operand: MathParseOperandFunction, 22 | parseInfixKey: MathParseInfixKeyFunction 23 | ): MathParseOperandFunction = MathParseOperandFunction { context -> 24 | val infix = MathParseInfixOperator(operand, parseInfixKey) 25 | infix(context) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2024 Alex Sokol 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /example/src/main/kotlin/units/calculate/TimeConvert.kt: -------------------------------------------------------------------------------- 1 | package units.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.math.calculate.MathCalculateSuccess 6 | import me.y9san9.calkt.number.PreciseNumber 7 | import units.TimeUnitKey 8 | 9 | object TimeConvert { 10 | operator fun invoke( 11 | context: CalculateContext, 12 | value: PreciseNumber, 13 | from: TimeUnitKey, 14 | to: TimeUnitKey 15 | ): CalculateResult { 16 | val result = value.times(multiplier(from)).divide(multiplier(to), context.precision) 17 | return MathCalculateSuccess(result) 18 | } 19 | 20 | private fun multiplier(key: TimeUnitKey): PreciseNumber { 21 | return when (key) { 22 | TimeUnitKey.Hours -> PreciseNumber.of(3_600_000) 23 | TimeUnitKey.Minutes -> PreciseNumber.of(60_000) 24 | TimeUnitKey.Seconds -> PreciseNumber.of(1_000) 25 | TimeUnitKey.Millis -> PreciseNumber.of(1) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/MathExpression.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.annotation.ExpressionSubclass 5 | import me.y9san9.calkt.number.PreciseNumber 6 | 7 | @OptIn(ExpressionSubclass::class) 8 | public sealed interface MathExpression : Expression { 9 | public data class Number(val number: PreciseNumber) : MathExpression { 10 | override fun toString(): String { 11 | return "$number" 12 | } 13 | } 14 | 15 | public data class Infix( 16 | val left: Expression, 17 | val right: Expression, 18 | val key: InfixKey 19 | ) : MathExpression { 20 | override fun toString(): String { 21 | val leftString = when (left) { 22 | is Number -> "$left" 23 | else -> "($left)" 24 | } 25 | val rightString = when (right) { 26 | is Number -> "$right" 27 | else -> "($right)" 28 | } 29 | return "$leftString $key $rightString" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/calculate/MathCalculate.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateFunction 5 | import me.y9san9.calkt.calculate.CalculateResult 6 | import me.y9san9.calkt.math.MathExpression 7 | 8 | public class MathCalculate( 9 | private val calculateInfixOperator: MathCalculateInfixOperatorFunction 10 | ) : CalculateFunction { 11 | override fun invoke(context: CalculateContext): CalculateResult.Success { 12 | val expression = context.expression as? MathExpression ?: context.unsupportedExpression() 13 | 14 | return when (expression) { 15 | is MathExpression.Infix -> { 16 | val left = context.recursive(expression.left) 17 | val right = context.recursive(expression.right) 18 | return calculateInfixOperator(context, left, right, expression.key) 19 | } 20 | is MathExpression.Number -> { 21 | MathCalculateSuccess(expression.number) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/calculate/CalculateFunctionPlus.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateResult.UnsupportedExpression 4 | 5 | private class CombinedCalculateFunction( 6 | val functions: List 7 | ) : CalculateFunction { 8 | override fun invoke(context: CalculateContext): CalculateResult.Success { 9 | for (function in functions) { 10 | val result = context.tryCalculate(function) 11 | if (result is UnsupportedExpression) continue 12 | return result.getOrFail(context) 13 | } 14 | context.unsupportedExpression() 15 | } 16 | } 17 | 18 | public operator fun CalculateFunction.plus( 19 | other: CalculateFunction 20 | ): CalculateFunction { 21 | val functions = when { 22 | this is CombinedCalculateFunction && other is CombinedCalculateFunction -> this.functions + other.functions 23 | this is CombinedCalculateFunction -> this.functions + other 24 | other is CombinedCalculateFunction -> listOf(this) + other.functions 25 | else -> listOf(this, other) 26 | } 27 | return CombinedCalculateFunction(functions) 28 | } 29 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/base/Integer.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.base 2 | 3 | import me.y9san9.calkt.parse.ParseResult 4 | import me.y9san9.calkt.parse.ParseContext 5 | import me.y9san9.calkt.parse.cause.ExpectedInputCause 6 | import me.y9san9.calkt.parse.cause.FailureCause 7 | 8 | public val expectedIntegerCause: (ParseResult.Failure) -> ExpectedInputCause = { failure -> 9 | ExpectedInputCause.of("Integer", failure) 10 | } 11 | 12 | public fun ParseContext.nextIntegerString( 13 | allowLeadingZeros: Boolean = true, 14 | cause: (ParseResult.Failure) -> FailureCause = expectedIntegerCause 15 | ): String { 16 | overrideCause(cause) { 17 | val first = removeFirstChar { fail("Unexpected EOF") } 18 | if (!first.isDigit()) fail("Expected first char to be digit, but was '$first'") 19 | if (first == '0' && !allowLeadingZeros) return "0" 20 | 21 | return buildString { 22 | append(first) 23 | while (true) { 24 | val char = context.firstCharOrNull() ?: break 25 | if (!char.isDigit()) break 26 | append(char) 27 | context.dropFirst() 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/MathParseInfixOperator.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.math.MathExpression 5 | import me.y9san9.calkt.parse.ParseContext 6 | import me.y9san9.calkt.parse.getOrElse 7 | import me.y9san9.calkt.parse.tryParse 8 | 9 | public class MathParseInfixOperator( 10 | private val parseOperand: MathParseOperandFunction, 11 | private val parseInfixKey: MathParseInfixKeyFunction 12 | ) { 13 | public operator fun invoke(context: ParseContext): Expression { 14 | var result = parseOperand(context) 15 | 16 | while (true) { 17 | context.tryParse { 18 | val key = parseInfixKey(context) 19 | context.clearNonTerminalCauses() 20 | val next = parseOperand(context) 21 | result = MathExpression.Infix( 22 | left = result, 23 | right = next, 24 | key = key 25 | ) 26 | }.getOrElse(context) { failure -> 27 | context.pushNonTerminalCause(failure.cause) 28 | return result 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/MathParseOperandFunctionPlus.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.parse.ParseContext 5 | import me.y9san9.calkt.parse.base.parseFirstOf 6 | 7 | private class CombinedMathParseOperandFunction( 8 | val functions: List 9 | ) : MathParseOperandFunction { 10 | override fun invoke( 11 | context: ParseContext 12 | ): Expression { 13 | return context.parseFirstOf( 14 | blocks = functions.map { function -> 15 | { function.invoke(context) } 16 | } 17 | ) 18 | } 19 | } 20 | 21 | public fun MathParseOperandFunction.plus(other: MathParseOperandFunction): MathParseOperandFunction { 22 | val functions = when { 23 | this is CombinedMathParseOperandFunction && 24 | other is CombinedMathParseOperandFunction -> this.functions + other.functions 25 | this is CombinedMathParseOperandFunction -> this.functions + other 26 | other is CombinedMathParseOperandFunction -> listOf(this) + other.functions 27 | else -> listOf(this, other) 28 | } 29 | return CombinedMathParseOperandFunction(functions) 30 | } 31 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunctionPlus.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.parse 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | import me.y9san9.calkt.parse.base.parseFirstOf 5 | import me.y9san9.calkt.units.UnitKey 6 | 7 | private class CombinedUnitsParseUnitKeyFunction( 8 | val functions: List 9 | ) : UnitsParseUnitKeyFunction { 10 | override fun invoke(context: ParseContext): UnitKey { 11 | return context.parseFirstOf( 12 | blocks = functions.map { function -> 13 | { function.invoke(context) } 14 | } 15 | ) 16 | } 17 | } 18 | 19 | public operator fun UnitsParseUnitKeyFunction.plus(other: UnitsParseUnitKeyFunction): UnitsParseUnitKeyFunction { 20 | val extensions = when { 21 | this is CombinedUnitsParseUnitKeyFunction && 22 | other is CombinedUnitsParseUnitKeyFunction -> this.functions + other.functions 23 | this is CombinedUnitsParseUnitKeyFunction -> this.functions + other 24 | other is CombinedUnitsParseUnitKeyFunction -> listOf(this) + other.functions 25 | else -> listOf(this, other) 26 | } 27 | return CombinedUnitsParseUnitKeyFunction(extensions) 28 | } 29 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/MathParseInfixKeyFunctionPlus.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.math.InfixKey 4 | import me.y9san9.calkt.parse.ParseContext 5 | import me.y9san9.calkt.parse.base.parseFirstOf 6 | 7 | private class CombinedMathParseInfixKeyFunction( 8 | val functions: List 9 | ) : MathParseInfixKeyFunction { 10 | override fun invoke( 11 | context: ParseContext 12 | ): InfixKey { 13 | return context.parseFirstOf( 14 | blocks = functions.map { function -> 15 | { function.invoke(context) } 16 | } 17 | ) 18 | } 19 | } 20 | 21 | public operator fun MathParseInfixKeyFunction.plus(other: MathParseInfixKeyFunction): MathParseInfixKeyFunction { 22 | val functions = when { 23 | this is CombinedMathParseInfixKeyFunction && 24 | other is CombinedMathParseInfixKeyFunction -> this.functions + other.functions 25 | this is CombinedMathParseInfixKeyFunction -> this.functions + other 26 | other is CombinedMathParseInfixKeyFunction -> listOf(this) + other.functions 27 | else -> listOf(this, other) 28 | } 29 | return CombinedMathParseInfixKeyFunction(functions) 30 | } 31 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsParse.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.parse.ParseContext 5 | import me.y9san9.calkt.parse.base.consume 6 | import me.y9san9.calkt.parse.ParseFunction 7 | import me.y9san9.calkt.parse.cause.ExpectedInputCause 8 | import me.y9san9.calkt.parse.tryParse 9 | import me.y9san9.calkt.units.UnitsExpression 10 | 11 | public class UnitsParse( 12 | private val parse: ParseFunction, 13 | private val parseUnitKey: UnitsParseUnitKeyFunction, 14 | private val conversion: Conversion = Conversion { context -> context.consume("to") { ExpectedInputCause.of("to") } } 15 | ) : ParseFunction { 16 | 17 | override fun invoke(context: ParseContext): Expression { 18 | val expression = parse(context) 19 | 20 | context.tryParse { 21 | conversion(context) 22 | context.tryParse { 23 | val unitKey = parseUnitKey(context) 24 | return UnitsExpression.Conversion(expression, unitKey) 25 | } 26 | } 27 | 28 | return expression 29 | } 30 | 31 | public fun interface Conversion { 32 | public operator fun invoke(context: ParseContext) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.parse 2 | 3 | import me.y9san9.calkt.parse.ParseContext 4 | import me.y9san9.calkt.parse.base.* 5 | import me.y9san9.calkt.parse.cause.ExpectedInputCause 6 | import me.y9san9.calkt.units.UnitKey 7 | 8 | public fun interface UnitsParseUnitKeyFunction { 9 | public operator fun invoke(context: ParseContext): UnitKey 10 | 11 | public companion object { 12 | public fun ofStrings( 13 | key: UnitKey, 14 | vararg strings: String 15 | ): UnitsParseUnitKeyFunction = UnitsParseUnitKeyFunction { context -> 16 | context.token { 17 | for (string in strings) { 18 | if (context.startsWith(string)) { 19 | val nextChar = context.string.getOrNull(index = context.position + string.length) 20 | if (nextChar?.isLetter() == true) continue 21 | context.drop(string.length) 22 | return@UnitsParseUnitKeyFunction key 23 | } 24 | } 25 | } 26 | context.fail(ExpectedInputCause.of("$key: One of ${strings.joinToString()}")) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.annotation.ExpressionSubclass 5 | import me.y9san9.calkt.calculate.CalculateResult 6 | import me.y9san9.calkt.parse.ParseContext 7 | import me.y9san9.calkt.parse.ParseResult 8 | import me.y9san9.calkt.parse.tryParse 9 | import me.y9san9.calkt.units.UnitKey 10 | import me.y9san9.calkt.units.annotation.UnitKeySubclass 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertIs 14 | 15 | class UnitsParseUnitKeyFunctionTest { 16 | @Test 17 | fun testDefaultImplementationChecksForNextChar() { 18 | val function = create() 19 | 20 | val result = tryParse("prefixtest") { context -> 21 | function(context) as Expression 22 | } 23 | assertIs>(result) 24 | assertEquals(TestKey, result.value) 25 | } 26 | 27 | private fun create(): UnitsParseUnitKeyFunction { 28 | return UnitsParseUnitKeyFunction.ofStrings(TestKey, "prefix", "prefixtest") 29 | } 30 | 31 | @OptIn(UnitKeySubclass::class, ExpressionSubclass::class) 32 | private data object TestKey : UnitKey, Expression 33 | } 34 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/number/PreciseNumberParser.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.number 2 | 3 | import me.y9san9.calkt.parse.ParseResult 4 | import me.y9san9.calkt.parse.ParseContext 5 | import me.y9san9.calkt.parse.base.* 6 | import me.y9san9.calkt.parse.cause.ExpectedInputCause 7 | import me.y9san9.calkt.parse.cause.FailureCause 8 | 9 | public fun ParseContext.nextPreciseNumber( 10 | token: Boolean = true, 11 | cause: (ParseResult.Failure) -> FailureCause = { failure -> ExpectedInputCause.of("Number", failure) }, 12 | ): PreciseNumber { 13 | if (token) return token { nextPreciseNumber(token = false, cause) } 14 | 15 | overrideCause(cause) { 16 | val number = buildString { 17 | if (tryConsume("-")) append("") 18 | 19 | append(nextIntegerString()) 20 | 21 | if (tryConsume(".")) { 22 | append(".") 23 | append(nextIntegerString(allowLeadingZeros = true)) 24 | } 25 | 26 | if (tryConsume("e") || tryConsume("E")) { 27 | append("") 28 | if (tryConsume("-")) { 29 | append("-") 30 | } else if (tryConsume("+")) { 31 | append("+") 32 | } 33 | append(nextIntegerString()) 34 | } 35 | } 36 | 37 | return PreciseNumber.of(number) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/cause/ToConsoleString.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse.cause 2 | 3 | import me.y9san9.calkt.internal.withIndent 4 | 5 | public fun FailureCause.toConsoleOutput( 6 | message: (FailureCause) -> String = FailureCause::toString 7 | ): String { 8 | val messageString = when (this) { 9 | is MultipleCauses -> { 10 | buildString { 11 | appendLine("None of parsers could parse this input. Causes:") 12 | for (cause in causes) { 13 | val causeString = cause 14 | .toConsoleOutput(message) 15 | .prependIndent(" ") 16 | .drop(n = 2) 17 | appendLine("- $causeString") 18 | } 19 | } 20 | } 21 | is MessageCause -> this.string 22 | else -> message(this) 23 | } 24 | return buildString { 25 | val failure = failure 26 | if (failure == null) { 27 | append(messageString) 28 | return@buildString 29 | } 30 | appendLine("FailureCause {") 31 | withIndent { 32 | append("message: ") 33 | append(messageString) 34 | appendLine() 35 | append("failure: ") 36 | append(failure.toConsoleOutput()) 37 | } 38 | appendLine() 39 | append("}") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/src/main/kotlin/units/calculate/DistanceConvert.kt: -------------------------------------------------------------------------------- 1 | package units.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.math.calculate.MathCalculateSuccess 6 | import me.y9san9.calkt.number.PreciseNumber 7 | import units.DistanceUnitKey 8 | 9 | object DistanceConvert { 10 | operator fun invoke( 11 | context: CalculateContext, 12 | value: PreciseNumber, 13 | from: DistanceUnitKey, 14 | to: DistanceUnitKey 15 | ): CalculateResult { 16 | val result = value.times(multiplier(from)).divide(multiplier(to), context.precision) 17 | return MathCalculateSuccess(result) 18 | } 19 | 20 | private fun multiplier( 21 | key: DistanceUnitKey 22 | ): PreciseNumber { 23 | return when (key) { 24 | // Imperial 25 | DistanceUnitKey.Kilometers -> PreciseNumber.of(1_000_000) 26 | DistanceUnitKey.Meters -> PreciseNumber.of(1_000) 27 | DistanceUnitKey.Centimeters -> PreciseNumber.of(10) 28 | DistanceUnitKey.Millimeters -> PreciseNumber.of(1) 29 | // Metric 30 | DistanceUnitKey.Inches -> PreciseNumber.of(25.4) 31 | DistanceUnitKey.Feet -> PreciseNumber.of(304.8) 32 | DistanceUnitKey.Yards -> PreciseNumber.of(914.4) 33 | DistanceUnitKey.Miles -> PreciseNumber.of(1_609_344) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/ParserState.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse 2 | 3 | import me.y9san9.calkt.internal.withIndent 4 | 5 | public data class ParserState( 6 | val string: String, 7 | val position: Int 8 | ) { 9 | public fun toConsolePointer(): String { 10 | return buildString { 11 | val fromIndex = (position - 10).coerceAtLeast(0) 12 | val toIndex = (position + 10).coerceAtMost(string.length) 13 | 14 | val leftOverflow = position - 10 > 0 15 | val rightOverflow = position + 10 < string.length 16 | 17 | if (leftOverflow) { 18 | append("...") 19 | } 20 | append(string.substring(fromIndex, toIndex)) 21 | if (rightOverflow) { 22 | append("...") 23 | } 24 | appendLine() 25 | 26 | if (leftOverflow) { 27 | append(" ") 28 | } 29 | repeat(position - fromIndex) { 30 | append(" ") 31 | } 32 | append("^") 33 | } 34 | } 35 | 36 | public fun toConsoleOutput(): String { 37 | return buildString { 38 | appendLine("ParserState {") 39 | withIndent { 40 | appendLine("initial: $string") 41 | append("position: $position") 42 | } 43 | appendLine() 44 | append("}") 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/calculate/UnitsCalculate.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateFunction 5 | import me.y9san9.calkt.calculate.CalculateResult 6 | import me.y9san9.calkt.math.calculate.MathCalculateSuccess 7 | import me.y9san9.calkt.units.UnitsExpression 8 | 9 | public class UnitsCalculate( 10 | private val convert: UnitsConvertFunction 11 | ) : CalculateFunction { 12 | 13 | override fun invoke( 14 | context: CalculateContext 15 | ): CalculateResult.Success { 16 | val expression = context.expression as? UnitsExpression ?: context.unsupportedExpression() 17 | 18 | return when (expression) { 19 | is UnitsExpression.Conversion -> when (val result = context.recursive(expression.value)) { 20 | is MathCalculateSuccess -> UnitsCalculateSuccess(result.number, expression.key) 21 | is UnitsCalculateSuccess -> { 22 | if (result.key == expression.key) return result 23 | 24 | val converted = convert(context, result.number, result.key, expression.key) as? MathCalculateSuccess 25 | ?: context.unsupportedExpression() 26 | 27 | UnitsCalculateSuccess(converted.number, expression.key) 28 | } 29 | else -> context.unsupportedExpression() 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/src/main/kotlin/MathExpressionExample.kt: -------------------------------------------------------------------------------- 1 | import me.y9san9.calkt.calculate.CalculateResult 2 | import me.y9san9.calkt.calculate.tryCalculate 3 | import me.y9san9.calkt.math.calculate.MathCalculateSuccess 4 | import me.y9san9.calkt.math.calculate.calculateMathExpression 5 | import me.y9san9.calkt.math.parse.parseMathExpression 6 | import me.y9san9.calkt.parse.ParseResult 7 | import me.y9san9.calkt.parse.tryParse 8 | import kotlin.system.exitProcess 9 | 10 | fun mathExpressionExample() { 11 | print("Enter math expression to calculate: ") 12 | val mathExpression = readln() 13 | // Parse expression 14 | val parseResult = tryParse(mathExpression) { context -> 15 | context.parseMathExpression() 16 | } 17 | when (parseResult) { 18 | is ParseResult.Failure -> { 19 | System.err.println("Cannot parse expression:") 20 | System.err.print(parseResult.toConsoleOutput()) 21 | exitProcess(0) 22 | } 23 | is ParseResult.Success -> { 24 | println("Parsed as: ${parseResult.value}") 25 | } 26 | } 27 | 28 | // Calculate expression 29 | val expression = parseResult.value 30 | val result = tryCalculate(expression, precision = 12) { context -> context.calculateMathExpression() } 31 | 32 | when (result) { 33 | is MathCalculateSuccess -> println("Result: ${result.number}") 34 | is CalculateResult.DivisionByZero -> println("Result: Division By Zero") 35 | else -> error("") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/calculate/UnitsConvertFunctionPlus.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.number.PreciseNumber 6 | import me.y9san9.calkt.units.UnitKey 7 | import me.y9san9.calkt.units.calculate.UnitsCalculateFailure.UnitsCantBeConverted 8 | 9 | private class CombinedUnitsConvertFunction( 10 | val functions: List 11 | ): UnitsConvertFunction { 12 | 13 | override fun invoke( 14 | context: CalculateContext, 15 | value: PreciseNumber, 16 | from: UnitKey, 17 | to: UnitKey 18 | ): CalculateResult { 19 | for (function in functions) { 20 | val result = function.invoke(context, value, from, to) 21 | if (result is UnitsCantBeConverted) continue 22 | return result 23 | } 24 | 25 | return UnitsCantBeConverted 26 | } 27 | } 28 | 29 | public operator fun UnitsConvertFunction.plus(other: UnitsConvertFunction): UnitsConvertFunction { 30 | val functions = when { 31 | this is CombinedUnitsConvertFunction && 32 | other is CombinedUnitsConvertFunction -> this.functions + other.functions 33 | this is CombinedUnitsConvertFunction -> this.functions + other 34 | other is CombinedUnitsConvertFunction -> listOf(this) + other.functions 35 | else -> listOf(this, other) 36 | } 37 | return CombinedUnitsConvertFunction(functions) 38 | } 39 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/ParseContext.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.parse.cause.FailureCause 5 | import me.y9san9.calkt.parse.cause.MessageCause 6 | import me.y9san9.calkt.parse.cause.MultipleCauses 7 | 8 | public class ParseContext( 9 | public val string: String, 10 | public val parseFunction: ParseFunction 11 | ) { 12 | public val nonTerminalCauses: MutableList = mutableListOf() 13 | public fun pushNonTerminalCause(cause: FailureCause) { 14 | nonTerminalCauses += cause 15 | } 16 | public fun clearNonTerminalCauses() { 17 | nonTerminalCauses.clear() 18 | } 19 | 20 | public fun failWithNonTerminalCauses(): Nothing { 21 | if (nonTerminalCauses.isEmpty()) error("Can't fail because there is no non-terminal causes") 22 | fail(MultipleCauses.of(causes = nonTerminalCauses.toTypedArray())) 23 | } 24 | 25 | public fun recursive(): Expression { 26 | return parseFunction(context) 27 | } 28 | 29 | public var position: Int = 0 30 | public val context: ParseContext get() = this 31 | 32 | public fun fail(message: String): Nothing = fail(MessageCause.of(message)) 33 | public fun fail(cause: FailureCause): Nothing = throw FailureException(cause) 34 | 35 | public class FailureException( 36 | public val failureCause: FailureCause 37 | ) : Throwable() 38 | } 39 | 40 | public fun ParseContext.toParserState(): ParserState { 41 | return ParserState(string, position) 42 | } 43 | -------------------------------------------------------------------------------- /example/src/main/kotlin/units/parse/ParseDistance.kt: -------------------------------------------------------------------------------- 1 | package units.parse 2 | 3 | import me.y9san9.calkt.units.parse.UnitsParseUnitKeyFunction 4 | import me.y9san9.calkt.units.parse.plus 5 | import units.DistanceUnitKey 6 | 7 | object ParseDistance { 8 | // Metric 9 | val kilometers = UnitsParseUnitKeyFunction.ofStrings( 10 | DistanceUnitKey.Kilometers, 11 | "km", "kilometer", "kilometers", "\$CSACS*(_)(*" 12 | ) 13 | val meters = UnitsParseUnitKeyFunction.ofStrings( 14 | DistanceUnitKey.Meters, 15 | "m", "meter", "meters" 16 | ) 17 | val centimeters = UnitsParseUnitKeyFunction.ofStrings( 18 | DistanceUnitKey.Centimeters, 19 | "cm", "centimeter", "centimeters" 20 | ) 21 | val millimeters = UnitsParseUnitKeyFunction.ofStrings( 22 | DistanceUnitKey.Millimeters, 23 | "mm", "millimeter", "millimeters" 24 | ) 25 | 26 | // Imperial 27 | val mile = UnitsParseUnitKeyFunction.ofStrings( 28 | DistanceUnitKey.Miles, 29 | "mi", "mile" 30 | ) 31 | val yard = UnitsParseUnitKeyFunction.ofStrings( 32 | DistanceUnitKey.Yards, 33 | "yd", "yard" 34 | ) 35 | val foot = UnitsParseUnitKeyFunction.ofStrings( 36 | DistanceUnitKey.Feet, 37 | "ft", "foot", "feet" 38 | ) 39 | val inches = UnitsParseUnitKeyFunction.ofStrings( 40 | DistanceUnitKey.Inches, 41 | "in", "inch", "inches" 42 | ) 43 | 44 | val function = kilometers + meters + centimeters + millimeters + 45 | mile + yard + foot + inches 46 | } 47 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/TryParse.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | 5 | public fun tryParse( 6 | string: String, 7 | failOnRemaining: Boolean = true, 8 | function: ParseFunction 9 | ): ParseResult { 10 | return tryParse(string, failOnRemaining, function, function) 11 | } 12 | 13 | public fun tryParse( 14 | string: String, 15 | failOnRemaining: Boolean = true, 16 | function: ParseFunction, 17 | recursive: ParseFunction 18 | ): ParseResult { 19 | val context = ParseContext(string, recursive) 20 | return try { 21 | val result = function(context) 22 | if (failOnRemaining && context.position != string.length) { 23 | context.failWithNonTerminalCauses() 24 | } 25 | ParseResult.Success(context.toParserState(), result) 26 | } catch (failure: ParseContext.FailureException) { 27 | ParseResult.Failure(context.toParserState(), failure.failureCause) 28 | } 29 | } 30 | 31 | public inline fun ParseContext.tryParse(block: () -> T): ParseResult { 32 | val positionBackup = position 33 | var exception = false 34 | 35 | return try { 36 | val result = block() 37 | ParseResult.Success(context.toParserState(), result) 38 | } catch (failure: ParseContext.FailureException) { 39 | exception = true 40 | ParseResult.Failure(context.toParserState(), failure.failureCause) 41 | } finally { 42 | if (exception) { 43 | this.position = positionBackup 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/calculate/DefaultMathCalculateInfixOperator.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.calculate.CalculateResult.DivisionByZero 6 | import me.y9san9.calkt.math.DefaultInfixKeys 7 | import me.y9san9.calkt.math.InfixKey 8 | import me.y9san9.calkt.math.calculate.MathCalculateFailure.UnsupportedInfixOperator 9 | 10 | public object DefaultMathCalculateInfixOperator : MathCalculateInfixOperatorFunction { 11 | override fun invoke( 12 | context: CalculateContext, 13 | left: CalculateResult.Success, 14 | right: CalculateResult.Success, 15 | key: InfixKey 16 | ): CalculateResult.Success { 17 | if (left !is MathCalculateSuccess) context.fail(UnsupportedInfixOperator) 18 | if (right !is MathCalculateSuccess) context.fail(UnsupportedInfixOperator) 19 | if (key !is DefaultInfixKeys) context.fail(UnsupportedInfixOperator) 20 | 21 | val number = when (key) { 22 | DefaultInfixKeys.Plus -> left.number + right.number 23 | DefaultInfixKeys.Minus -> left.number - right.number 24 | DefaultInfixKeys.Times -> left.number * right.number 25 | DefaultInfixKeys.Div -> { 26 | if (right.number.isZero()) { 27 | context.fail(DivisionByZero) 28 | } else { 29 | left.number.divide(right.number, context.precision) 30 | } 31 | } 32 | } 33 | 34 | return MathCalculateSuccess(number) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/DefaultMathInfixOperators.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.math.DefaultInfixKeys 4 | import me.y9san9.calkt.math.InfixKey 5 | import me.y9san9.calkt.parse.ParseContext 6 | import me.y9san9.calkt.parse.base.token 7 | import me.y9san9.calkt.parse.cause.ExpectedInputCause 8 | 9 | public object DefaultMathInfixOperators { 10 | public val list: List = listOf( 11 | Times + Div, // Times, Div have the same priority 12 | Plus + Minus // Plus, Minus have the same priority 13 | ) 14 | 15 | public object Plus : MathParseInfixKeyFunction { 16 | override fun invoke(context: ParseContext): InfixKey { 17 | context.token("+") { ExpectedInputCause.of("+") } 18 | return DefaultInfixKeys.Plus 19 | } 20 | } 21 | 22 | public object Minus : MathParseInfixKeyFunction { 23 | override fun invoke(context: ParseContext): InfixKey { 24 | context.token("-") { ExpectedInputCause.of("-") } 25 | return DefaultInfixKeys.Minus 26 | } 27 | } 28 | 29 | public object Times : MathParseInfixKeyFunction { 30 | override fun invoke(context: ParseContext): InfixKey { 31 | context.token("*") { ExpectedInputCause.of("*") } 32 | return DefaultInfixKeys.Times 33 | } 34 | } 35 | 36 | public object Div : MathParseInfixKeyFunction { 37 | override fun invoke(context: ParseContext): InfixKey { 38 | context.token("/") { ExpectedInputCause.of("/") } 39 | return DefaultInfixKeys.Div 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/calculate/MathCalculateInfixOperatorFunctionPlus.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.calculate.getOrFail 6 | import me.y9san9.calkt.math.InfixKey 7 | import me.y9san9.calkt.math.calculate.MathCalculateFailure.UnsupportedInfixOperator 8 | 9 | private class CombinedMathCalculateInfixOperatorFunction( 10 | val functions: List 11 | ) : MathCalculateInfixOperatorFunction { 12 | 13 | override fun invoke( 14 | context: CalculateContext, 15 | left: CalculateResult.Success, 16 | right: CalculateResult.Success, 17 | key: InfixKey 18 | ): CalculateResult.Success { 19 | for (function in functions) { 20 | val result = context.tryCalculate { function(context, left, right, key) } 21 | if (result is UnsupportedInfixOperator) continue 22 | return result.getOrFail(context) 23 | } 24 | context.fail(UnsupportedInfixOperator) 25 | } 26 | } 27 | 28 | public operator fun MathCalculateInfixOperatorFunction.plus( 29 | other: MathCalculateInfixOperatorFunction 30 | ): MathCalculateInfixOperatorFunction { 31 | val functions = when { 32 | this is CombinedMathCalculateInfixOperatorFunction && 33 | other is CombinedMathCalculateInfixOperatorFunction -> this.functions + other.functions 34 | this is CombinedMathCalculateInfixOperatorFunction -> this.functions + other 35 | other is CombinedMathCalculateInfixOperatorFunction -> listOf(this) + other.functions 36 | else -> listOf(this, other) 37 | } 38 | return CombinedMathCalculateInfixOperatorFunction(functions) 39 | } 40 | -------------------------------------------------------------------------------- /example/src/main/kotlin/UnitsExpressionExample.kt: -------------------------------------------------------------------------------- 1 | import me.y9san9.calkt.calculate.CalculateResult 2 | import me.y9san9.calkt.calculate.tryCalculate 3 | import me.y9san9.calkt.math.calculate.MathCalculateSuccess 4 | import me.y9san9.calkt.parse.ParseResult 5 | import me.y9san9.calkt.parse.tryParse 6 | import me.y9san9.calkt.units.calculate.UnitsCalculateSuccess 7 | import me.y9san9.calkt.units.calculate.calculateUnitsExpression 8 | import me.y9san9.calkt.units.parse.parseUnitsExpression 9 | import units.calculate.DefaultUnitsConvert 10 | import units.parse.DefaultParseUnitKey 11 | import kotlin.system.exitProcess 12 | 13 | fun unitsExpressionExample() { 14 | print("Enter an expression with units to calculate: ") 15 | val unitsExpression = readln() 16 | // Parse expression 17 | val parseResult = tryParse(unitsExpression) { context -> 18 | context.parseUnitsExpression(DefaultParseUnitKey) 19 | } 20 | 21 | when (parseResult) { 22 | is ParseResult.Failure -> { 23 | System.err.println("Cannot parse expression:") 24 | System.err.print(parseResult.toConsoleOutput()) 25 | exitProcess(0) 26 | } 27 | is ParseResult.Success -> { 28 | println("Parsed as: ${parseResult.value}") 29 | } 30 | } 31 | 32 | // Calculate expression 33 | val expression = parseResult.value 34 | val result = tryCalculate(expression, precision = 12) { context -> context.calculateUnitsExpression(DefaultUnitsConvert) } 35 | 36 | when (result) { 37 | is MathCalculateSuccess -> println("Result: ${result.number}") 38 | is UnitsCalculateSuccess -> println("Result: ${result.number} ${result.key}") 39 | is CalculateResult.DivisionByZero -> println("Result: Division By Zero") 40 | else -> error("") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/calculate/CalculateContext.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.calculate 2 | 3 | import me.y9san9.calkt.Expression 4 | 5 | public class CalculateContext( 6 | public val expression: Expression, 7 | public val calculateFunction: CalculateFunction, 8 | public val precision: Long 9 | ) { 10 | public val context: CalculateContext get() = this 11 | 12 | public fun tryCalculate(function: CalculateFunction): CalculateResult { 13 | return tryCalculate(expression, precision, function, calculateFunction) 14 | } 15 | 16 | public fun recursive(expression: Expression): CalculateResult.Success { 17 | val context = CalculateContext(expression, calculateFunction, precision) 18 | return calculateFunction(context) 19 | } 20 | 21 | public fun unsupportedExpression(): Nothing { 22 | fail(CalculateResult.UnsupportedExpression) 23 | } 24 | 25 | public fun fail( 26 | failure: CalculateResult.Failure 27 | ): Nothing { 28 | throw FailureException(failure) 29 | } 30 | 31 | public class FailureException(public val failure: CalculateResult.Failure) : Throwable() 32 | } 33 | 34 | public fun tryCalculate( 35 | expression: Expression, 36 | precision: Long, 37 | function: CalculateFunction 38 | ): CalculateResult { 39 | return tryCalculate(expression, precision, function, function) 40 | } 41 | 42 | public fun tryCalculate( 43 | expression: Expression, 44 | precision: Long, 45 | function: CalculateFunction, 46 | recursive: CalculateFunction 47 | ): CalculateResult { 48 | return try { 49 | val context = CalculateContext(expression, recursive, precision) 50 | function(context) 51 | } catch (exception: CalculateContext.FailureException) { 52 | exception.failure 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Calkt 2 | 3 | Calkt is a Kotlin library that supports parsing and calculating 4 | various expressions. Parser is written in a way to have an ability 5 | to be extended. 6 | 7 | 8 | * [Calkt](#calkt) 9 | * [Example](#example) 10 | * [Implementation](#implementation) 11 | * [Modules](#modules) 12 | * [core](#core) 13 | * [math](#math) 14 | * [units](#units) 15 | * [example](#example-1) 16 | 17 | 18 | ## Example 19 | 20 | The result of running [example/Main.kt](example/src/main/kotlin/Main.kt): 21 | 22 | ```text 23 | Enter math expression to calculate: 2 + 2 * 2 24 | Parsed as: 2 plus (2 times 2) 25 | Result: 6 26 | 27 | Enter an expression with units to calculate: (1km - 0.5mi) + 2 yd to inches 28 | Parsed as: (((1 Kilometers) minus (0.5 Miles)) plus (2 Yards)).convert(Inches) 29 | Result: 7886.272 Inches 30 | ``` 31 | 32 | ## Dependency 33 | 34 | You can get latest version from `Releases` tab on GitHub. 35 | 36 | ```kotlin 37 | dependencies { 38 | implementation("me.y9san9.calkt:units:$version") 39 | } 40 | ``` 41 | 42 | ## Modules 43 | 44 | The library consists of several different modules: 45 | 46 | ### core 47 | 48 | Module with all basic types for parsing and calculating. Here 49 | you can find `ParseContext` and `CalculateContext` as well as 50 | functions to launch parsers/calculators. 51 | 52 | ### math 53 | 54 | Module with implementation of basic math expressions that any 55 | calculator can calculate. This is where you can find logic to 56 | calculate numbers combined with basic supported operators ( 57 | `+`, `-`, `*`, `/`). You can implement your own operator, like 58 | in [this example](example/src/main/kotlin/operator/ModOperator.kt) (% operator). 59 | 60 | ### units 61 | 62 | Module with implementation of math expressions with units. It 63 | depends on `math` module heavily and does not do any calculations 64 | on its own. There is a logic to support calculation and 65 | conversions of units like `1 km + 2`, `1km to inches`, etc. 66 | 67 | ### example 68 | 69 | Module with all the examples you need to know. If something is missing, 70 | PRs are welcome. 71 | -------------------------------------------------------------------------------- /math/src/commonMain/kotlin/me/y9san9/calkt/math/parse/MathParseGroupFunction.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.math.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.parse.ParseContext 5 | import me.y9san9.calkt.parse.base.token 6 | import me.y9san9.calkt.parse.cause.ExpectedInputCause 7 | import me.y9san9.calkt.parse.ParseResult 8 | import me.y9san9.calkt.parse.base.overrideCause 9 | import me.y9san9.calkt.parse.getOrElse 10 | import me.y9san9.calkt.parse.tryParse 11 | 12 | private val defaultCause = { failure: ParseResult.Failure -> ExpectedInputCause.of("GROUP", failure) } 13 | 14 | public class MathParseGroupFunction( 15 | private val operand: MathParseOperandFunction, 16 | private val beginGroup: Begin = Begin.Default, 17 | private val endGroup: End = End.Default 18 | ) { 19 | public operator fun invoke(context: ParseContext): Expression { 20 | return context.tryParse { 21 | operand(context) 22 | }.getOrElse(context) { 23 | context.overrideCause(defaultCause) { 24 | beginGroup(context) 25 | val result = context.recursive() 26 | context.tryParse { 27 | endGroup(context) 28 | context.clearNonTerminalCauses() 29 | }.getOrElse(context) { 30 | context.failWithNonTerminalCauses() 31 | } 32 | result 33 | } 34 | } 35 | } 36 | 37 | public fun interface Begin { 38 | public operator fun invoke(context: ParseContext) 39 | 40 | public object Default : Begin { 41 | override fun invoke(context: ParseContext) { 42 | context.token("(") { ExpectedInputCause.of("GROUP_START") } 43 | } 44 | } 45 | } 46 | 47 | public fun interface End { 48 | public operator fun invoke(context: ParseContext) 49 | 50 | public object Default : End { 51 | override fun invoke(context: ParseContext) { 52 | context.token(")") { ExpectedInputCause.of("GROUP_END") } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsMathParseOperand.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.parse 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.math.MathExpression 5 | import me.y9san9.calkt.math.parse.MathParseOperandFunction 6 | import me.y9san9.calkt.number.PreciseNumber 7 | import me.y9san9.calkt.parse.* 8 | import me.y9san9.calkt.parse.base.overrideCause 9 | import me.y9san9.calkt.parse.cause.ExpectedInputCause 10 | import me.y9san9.calkt.units.UnitsExpression 11 | 12 | public class UnitsMathParseOperand( 13 | private val parseUnitKey: UnitsParseUnitKeyFunction, 14 | private val parseOperand: MathParseOperandFunction 15 | ) : MathParseOperandFunction { 16 | override fun invoke(context: ParseContext): Expression { 17 | context.tryParse { 18 | context.overrideCause( 19 | { failure -> ExpectedInputCause.of("Unit Before Number", failure = failure) } 20 | ) { 21 | val unitKey = parseUnitKey(context) 22 | val operand = parseOperand(context) 23 | return UnitsExpression.Conversion(operand, unitKey) 24 | } 25 | }.getOrElse(context) { failure -> 26 | context.pushNonTerminalCause(failure.cause) 27 | } 28 | 29 | context.tryParse { 30 | val operand = parseOperand(context) 31 | 32 | context.tryParse { 33 | context.overrideCause( 34 | { failure -> ExpectedInputCause.of("Unit After Number", failure = failure) } 35 | ) { 36 | val unitKey = parseUnitKey(context) 37 | return UnitsExpression.Conversion(operand, unitKey) 38 | } 39 | }.getOrElse(context) { secondFailure -> 40 | context.pushNonTerminalCause(secondFailure.cause) 41 | } 42 | 43 | return operand 44 | }.getOrElse(context) { failure -> 45 | context.pushNonTerminalCause(failure.cause) 46 | } 47 | 48 | context.tryParse { 49 | val unitKey = parseUnitKey(context) 50 | 51 | return UnitsExpression.Conversion( 52 | value = MathExpression.Number(PreciseNumber.of(1)), 53 | key = unitKey 54 | ) 55 | } 56 | 57 | context.failWithNonTerminalCauses() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/parse/ParseResult.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.parse 2 | 3 | import me.y9san9.calkt.internal.withIndent 4 | import me.y9san9.calkt.parse.cause.FailureCause 5 | import me.y9san9.calkt.parse.cause.MultipleCauses 6 | import me.y9san9.calkt.parse.cause.toConsoleOutput 7 | 8 | public sealed interface ParseResult { 9 | public data class Failure( 10 | val state: ParserState, 11 | val cause: FailureCause 12 | ) : ParseResult { 13 | 14 | public fun toConsoleOutput( 15 | failureMessage: (FailureCause) -> String = FailureCause::toString 16 | ): String { 17 | return buildString { 18 | appendLine("ParseResult.Failure {") 19 | withIndent { 20 | appendLine("location: ") 21 | withIndent { 22 | append(state.toConsolePointer()) 23 | } 24 | appendLine() 25 | appendLine("cause: ") 26 | withIndent { 27 | append(cause.toConsoleOutput(failureMessage)) 28 | } 29 | } 30 | 31 | appendLine() 32 | append("}") 33 | } 34 | } 35 | } 36 | 37 | public data class Success( 38 | val state: ParserState, 39 | val value: T, 40 | ) : ParseResult 41 | } 42 | 43 | public fun ParseResult.getOrFail(context: ParseContext): T { 44 | return when (this) { 45 | is ParseResult.Failure -> context.fail(cause) 46 | is ParseResult.Success -> value 47 | } 48 | } 49 | 50 | public fun ParseResult.getOrThrow(): T { 51 | return when (this) { 52 | is ParseResult.Failure -> error("Parse result is expected to be Success") 53 | is ParseResult.Success -> this.value 54 | } 55 | } 56 | 57 | public inline fun ParseResult.getOrElse( 58 | context: ParseContext, 59 | recover: (ParseResult.Failure) -> T 60 | ): T { 61 | return when (this) { 62 | is ParseResult.Success -> value 63 | is ParseResult.Failure -> elseTry(context, recover).getOrFail(context) 64 | } 65 | } 66 | 67 | public inline fun ParseResult.elseTry(context: ParseContext, recover: (ParseResult.Failure) -> T): ParseResult { 68 | return when (this) { 69 | is ParseResult.Success -> this 70 | is ParseResult.Failure -> { 71 | when (val parsed = context.tryParse { recover(this) }) { 72 | is ParseResult.Failure -> ParseResult.Failure( 73 | state = context.toParserState(), 74 | cause = MultipleCauses.of(this.cause, parsed.cause) 75 | ) 76 | is ParseResult.Success -> parsed 77 | } 78 | } 79 | } 80 | } 81 | 82 | public inline fun ParseResult.onSuccess(block: (T) -> Unit) { 83 | if (this is ParseResult.Success) block(value) 84 | } 85 | -------------------------------------------------------------------------------- /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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/me/y9san9/calkt/number/PreciseNumber.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.number 2 | 3 | import com.ionspin.kotlin.bignum.decimal.BigDecimal 4 | import com.ionspin.kotlin.bignum.decimal.toBigDecimal 5 | import kotlin.math.min 6 | 7 | public class PreciseNumber private constructor( 8 | private val bigDecimal: BigDecimal 9 | ) { 10 | public operator fun plus(other: PreciseNumber): PreciseNumber { 11 | val result = bigDecimal + other.bigDecimal 12 | return PreciseNumber(result) 13 | } 14 | 15 | public operator fun minus(other: PreciseNumber): PreciseNumber { 16 | val result = bigDecimal - other.bigDecimal 17 | return PreciseNumber(result) 18 | } 19 | 20 | public fun divide(other: PreciseNumber, precision: Long): PreciseNumber { 21 | val minExponent = -min(this.bigDecimal.exponent, other.bigDecimal.exponent).coerceAtMost(maximumValue = 0) 22 | 23 | val thisBigInteger = this.bigDecimal.moveDecimalPoint(places = minExponent + precision).toBigInteger() 24 | val otherBigInteger = other.bigDecimal.moveDecimalPoint(minExponent).toBigInteger() 25 | 26 | val integerResult = thisBigInteger.divrem(otherBigInteger).quotient 27 | val result = BigDecimal.fromBigInteger(integerResult).moveDecimalPoint(places = -precision) 28 | 29 | return PreciseNumber(result) 30 | } 31 | 32 | public operator fun times(other: PreciseNumber): PreciseNumber { 33 | val result = bigDecimal * other.bigDecimal 34 | return PreciseNumber(result) 35 | } 36 | 37 | public operator fun rem(other: PreciseNumber): PreciseNumber { 38 | val result = bigDecimal % other.bigDecimal 39 | return PreciseNumber(result) 40 | } 41 | 42 | public operator fun unaryPlus(): PreciseNumber = this 43 | public operator fun unaryMinus(): PreciseNumber = PreciseNumber(-bigDecimal) 44 | 45 | override fun equals(other: Any?): Boolean { 46 | if (other !is PreciseNumber) return false 47 | return this.bigDecimal == other.bigDecimal 48 | } 49 | 50 | public fun isZero(): Boolean { 51 | return bigDecimal == BigDecimal.ZERO 52 | } 53 | 54 | override fun hashCode(): Int { 55 | return bigDecimal.hashCode() 56 | } 57 | override fun toString(): String = bigDecimal.toStringExpanded() 58 | 59 | 60 | public companion object { 61 | public fun of(int: Int): PreciseNumber { 62 | return PreciseNumber(int.toBigDecimal()) 63 | } 64 | public fun of(long: Long): PreciseNumber { 65 | return PreciseNumber(long.toBigDecimal()) 66 | } 67 | public fun of(string: String ): PreciseNumber { 68 | return PreciseNumber(string.toBigDecimal()) 69 | } 70 | public fun of(float: Float): PreciseNumber { 71 | return PreciseNumber(float.toBigDecimal()) 72 | } 73 | public fun of(double: Double): PreciseNumber { 74 | return PreciseNumber(double.toBigDecimal()) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/UnitsMathParseOperand.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.parse 2 | 3 | import me.y9san9.calkt.math.MathExpression 4 | import me.y9san9.calkt.math.parse.DefaultMathParseOperand 5 | import me.y9san9.calkt.number.PreciseNumber 6 | import me.y9san9.calkt.parse.base.consume 7 | import me.y9san9.calkt.parse.getOrThrow 8 | import me.y9san9.calkt.parse.tryParse 9 | import me.y9san9.calkt.units.UnitKey 10 | import me.y9san9.calkt.units.UnitsExpression 11 | import me.y9san9.calkt.units.annotation.UnitKeySubclass 12 | import me.y9san9.calkt.units.parse.cause.ExpectedUnitsCause 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | 16 | class UnitsMathParseOperandTest { 17 | 18 | @Test 19 | fun testCanParseNumber() { 20 | val operand = create() 21 | 22 | val result = tryParse("1") { context -> 23 | operand(context) 24 | }.getOrThrow() 25 | 26 | val expected = MathExpression.Number(number = PreciseNumber.of(int = 1)) 27 | 28 | assertEquals(expected, result) 29 | } 30 | 31 | @Test 32 | fun testCanParseGroup() { 33 | val operand = create() 34 | 35 | val result = tryParse("(1 unit)") { context -> 36 | operand(context) 37 | }.getOrThrow() 38 | 39 | val expected = UnitsExpression.Conversion( 40 | value = MathExpression.Number( 41 | number = PreciseNumber.of(int = 1) 42 | ), 43 | key = TestUnitKey 44 | ) 45 | 46 | assertEquals(expected, result) 47 | } 48 | 49 | @Test 50 | fun testCanUnitBefore() { 51 | val operand = create() 52 | 53 | val result = tryParse("unit 1") { context -> 54 | operand(context) 55 | }.getOrThrow() 56 | 57 | val expected = UnitsExpression.Conversion( 58 | value = MathExpression.Number( 59 | number = PreciseNumber.of(int = 1) 60 | ), 61 | key = TestUnitKey 62 | ) 63 | 64 | assertEquals(expected, result) 65 | } 66 | 67 | @Test 68 | fun testCanParseUnitAfter() { 69 | val operand = create() 70 | 71 | val result = tryParse("1 unit") { context -> 72 | operand(context) 73 | }.getOrThrow() 74 | 75 | val expected = UnitsExpression.Conversion( 76 | value = MathExpression.Number( 77 | number = PreciseNumber.of(int = 1) 78 | ), 79 | key = TestUnitKey 80 | ) 81 | 82 | assertEquals(expected, result) 83 | } 84 | 85 | @Test 86 | fun testCanParseUnitOnly() { 87 | val operand = create() 88 | 89 | val result = tryParse("unit") { context -> 90 | operand(context) 91 | }.getOrThrow() 92 | 93 | val expected = UnitsExpression.Conversion( 94 | value = MathExpression.Number( 95 | number = PreciseNumber.of(int = 1) 96 | ), 97 | key = TestUnitKey 98 | ) 99 | 100 | assertEquals(expected, result) 101 | } 102 | 103 | private fun create(): UnitsMathParseOperand { 104 | val parseUnitKey = UnitsParseUnitKeyFunction { context -> 105 | context.consume("unit") { ExpectedUnitsCause.of("unit") } 106 | TestUnitKey 107 | 108 | } 109 | val parseOperand = DefaultMathParseOperand 110 | return UnitsMathParseOperand(parseUnitKey, parseOperand) 111 | } 112 | 113 | @OptIn(UnitKeySubclass::class) 114 | private object TestUnitKey : UnitKey 115 | } 116 | -------------------------------------------------------------------------------- /example/src/main/kotlin/operator/ModOperator.kt: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import me.y9san9.calkt.Expression 4 | import me.y9san9.calkt.calculate.CalculateContext 5 | import me.y9san9.calkt.calculate.CalculateResult 6 | import me.y9san9.calkt.calculate.tryCalculate 7 | import me.y9san9.calkt.math.InfixKey 8 | import me.y9san9.calkt.math.annotation.InfixKeySubclass 9 | import me.y9san9.calkt.math.calculate.* 10 | import me.y9san9.calkt.math.calculate.MathCalculateFailure.UnsupportedInfixOperator 11 | import me.y9san9.calkt.math.parse.DefaultMathInfixOperators.Div 12 | import me.y9san9.calkt.math.parse.DefaultMathInfixOperators.Minus 13 | import me.y9san9.calkt.math.parse.DefaultMathInfixOperators.Plus 14 | import me.y9san9.calkt.math.parse.DefaultMathInfixOperators.Times 15 | import me.y9san9.calkt.math.parse.MathParseInfixKeyFunction 16 | import me.y9san9.calkt.math.parse.parseMathExpression 17 | import me.y9san9.calkt.math.parse.plus 18 | import me.y9san9.calkt.parse.ParseContext 19 | import me.y9san9.calkt.parse.ParseResult 20 | import me.y9san9.calkt.parse.base.token 21 | import me.y9san9.calkt.parse.cause.ExpectedInputCause 22 | import me.y9san9.calkt.parse.tryParse 23 | import kotlin.system.exitProcess 24 | 25 | @OptIn(InfixKeySubclass::class) 26 | data object ModKey : InfixKey { 27 | override fun toString() = "mod" 28 | } 29 | 30 | data object ModParse : MathParseInfixKeyFunction { 31 | override fun invoke(context: ParseContext): InfixKey { 32 | context.token("%") { ExpectedInputCause.of("%") } 33 | return ModKey 34 | } 35 | } 36 | 37 | data object ModCalculate : MathCalculateInfixOperatorFunction { 38 | override fun invoke( 39 | context: CalculateContext, 40 | left: CalculateResult.Success, 41 | right: CalculateResult.Success, 42 | key: InfixKey 43 | ): CalculateResult.Success { 44 | if (left !is MathCalculateSuccess) context.fail(UnsupportedInfixOperator) 45 | if (right !is MathCalculateSuccess) context.fail(UnsupportedInfixOperator) 46 | if (key != ModKey) context.fail(UnsupportedInfixOperator) 47 | return MathCalculateSuccess(number = left.number % right.number) 48 | } 49 | } 50 | 51 | val exampleInfixOperatorList = listOf( 52 | Times + Div, // Times, Div have the same priority 53 | Plus + Minus + ModParse // Plus, Minus, Mod have the same priority 54 | ) 55 | 56 | fun ParseContext.parseExampleExpression(): Expression { 57 | return context.parseMathExpression(infixOperatorList = exampleInfixOperatorList) 58 | } 59 | 60 | val exampleCalculateInfixOperator = DefaultMathCalculateInfixOperator + ModCalculate 61 | 62 | fun CalculateContext.calculateExampleExpression(): CalculateResult.Success { 63 | return context.calculateMathExpression(calculateInfixOperator = exampleCalculateInfixOperator) 64 | } 65 | 66 | fun main() { 67 | print("Enter math expression to calculate: ") 68 | val mathExpression = readln() 69 | // Parse expression 70 | val parseResult = tryParse(mathExpression) { context -> context.parseExampleExpression() } 71 | when (parseResult) { 72 | is ParseResult.Failure -> { 73 | System.err.println("Cannot parse expression:") 74 | System.err.print(parseResult.toConsoleOutput()) 75 | exitProcess(0) 76 | } 77 | is ParseResult.Success -> { 78 | println("Parsed as: ${parseResult.value}") 79 | } 80 | } 81 | 82 | // Calculate expression 83 | val expression = parseResult.value 84 | val result = tryCalculate(expression, precision = 12) { context -> context.calculateExampleExpression() } 85 | 86 | when (result) { 87 | is MathCalculateSuccess -> println("Result: ${result.number}") 88 | is CalculateResult.DivisionByZero -> println("Result: Division By Zero") 89 | else -> error("") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /units/src/commonMain/kotlin/me/y9san9/calkt/units/calculate/UnitsMathCalculateInfixOperator.kt: -------------------------------------------------------------------------------- 1 | package me.y9san9.calkt.units.calculate 2 | 3 | import me.y9san9.calkt.calculate.CalculateContext 4 | import me.y9san9.calkt.calculate.CalculateResult 5 | import me.y9san9.calkt.math.DefaultInfixKeys 6 | import me.y9san9.calkt.math.InfixKey 7 | import me.y9san9.calkt.math.MathExpression 8 | import me.y9san9.calkt.math.calculate.MathCalculateInfixOperatorFunction 9 | import me.y9san9.calkt.math.calculate.MathCalculateSuccess 10 | import me.y9san9.calkt.math.calculate.plus 11 | import me.y9san9.calkt.math.calculate.unsupportedInfixOperator 12 | import me.y9san9.calkt.number.PreciseNumber 13 | import me.y9san9.calkt.units.UnitKey 14 | import me.y9san9.calkt.units.UnitsExpression 15 | 16 | public object UnitsMathCalculateInfixOperator : MathCalculateInfixOperatorFunction { 17 | private val delegate = Sum( 18 | supportedOperators = setOf(DefaultInfixKeys.Plus, DefaultInfixKeys.Minus) 19 | ) + Prod( 20 | supportedOperators = setOf(DefaultInfixKeys.Times, DefaultInfixKeys.Div) 21 | ) 22 | 23 | override fun invoke( 24 | context: CalculateContext, 25 | left: CalculateResult.Success, 26 | right: CalculateResult.Success, 27 | key: InfixKey 28 | ): CalculateResult.Success { 29 | return delegate(context, left, right, key) 30 | } 31 | 32 | private class Sum( 33 | private val supportedOperators: Set 34 | ) : MathCalculateInfixOperatorFunction { 35 | override fun invoke( 36 | context: CalculateContext, 37 | left: CalculateResult.Success, 38 | right: CalculateResult.Success, 39 | key: InfixKey 40 | ): CalculateResult.Success { 41 | if (key !in supportedOperators) context.unsupportedInfixOperator() 42 | 43 | val leftNumber = extractNumber(left) ?: context.unsupportedInfixOperator() 44 | val leftUnits = extractUnits(left) 45 | val rightNumber = extractNumber(right) ?: context.unsupportedInfixOperator() 46 | val rightUnits = extractUnits(right) 47 | 48 | // If none of operands have units, it's not a task for this function 49 | val commonUnits = leftUnits ?: rightUnits ?: context.unsupportedInfixOperator() 50 | 51 | val leftConverted = convertUnits(context, leftNumber, leftUnits, commonUnits) 52 | if (leftConverted !is UnitsCalculateSuccess) context.unsupportedInfixOperator() 53 | 54 | val rightConverted = convertUnits(context, rightNumber, rightUnits, commonUnits) 55 | if (rightConverted !is UnitsCalculateSuccess) context.unsupportedInfixOperator() 56 | 57 | val mathExpression = MathExpression.Infix( 58 | left = MathExpression.Number(leftConverted.number), 59 | right = MathExpression.Number(rightConverted.number), 60 | key = key 61 | ) 62 | val result = context.recursive(mathExpression) as? MathCalculateSuccess 63 | ?: context.unsupportedInfixOperator() 64 | 65 | return UnitsCalculateSuccess(result.number, commonUnits) 66 | } 67 | 68 | private fun extractUnits(result: CalculateResult): UnitKey? { 69 | if (result is UnitsCalculateSuccess) return result.key 70 | return null 71 | } 72 | 73 | private fun convertUnits( 74 | context: CalculateContext, 75 | number: PreciseNumber, 76 | from: UnitKey?, 77 | to: UnitKey 78 | ): CalculateResult { 79 | if (from == null) return UnitsCalculateSuccess(number, to) 80 | val fromExpression = UnitsExpression.Conversion(MathExpression.Number(number), from) 81 | val toExpression = UnitsExpression.Conversion(fromExpression, to) 82 | return context.recursive(toExpression) 83 | } 84 | } 85 | 86 | public class Prod( 87 | private val supportedOperators: Set 88 | ) : MathCalculateInfixOperatorFunction { 89 | 90 | override fun invoke( 91 | context: CalculateContext, 92 | left: CalculateResult.Success, 93 | right: CalculateResult.Success, 94 | key: InfixKey 95 | ): CalculateResult.Success { 96 | if (key !in supportedOperators) context.unsupportedInfixOperator() 97 | 98 | val units = extractUnits(left, right) 99 | ?: context.unsupportedInfixOperator() 100 | 101 | val leftNumber = extractNumber(left)?.let(MathExpression::Number) 102 | ?: context.unsupportedInfixOperator() 103 | 104 | val rightNumber = extractNumber(right)?.let(MathExpression::Number) 105 | ?: context.unsupportedInfixOperator() 106 | 107 | val mathExpression = MathExpression.Infix(leftNumber, rightNumber, key) 108 | 109 | val result = context.recursive(mathExpression) as? MathCalculateSuccess 110 | ?: context.unsupportedInfixOperator() 111 | 112 | return UnitsCalculateSuccess(result.number, units) 113 | } 114 | 115 | private fun extractUnits( 116 | left: CalculateResult, 117 | right: CalculateResult 118 | ): UnitKey? { 119 | // Both operands cannot have units if multiplying 120 | if (left is UnitsCalculateSuccess && right is UnitsCalculateSuccess) return null 121 | if (left is UnitsCalculateSuccess) return left.key 122 | if (right is UnitsCalculateSuccess) return right.key 123 | return null 124 | } 125 | } 126 | 127 | private fun extractNumber(result: CalculateResult): PreciseNumber? { 128 | if (result is UnitsCalculateSuccess) return result.number 129 | if (result is MathCalculateSuccess) return result.number 130 | return null 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Library Release Deploy 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | workflow_dispatch: 7 | 8 | env: 9 | GITHUB_USERNAME: "meetacy" 10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 12 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 13 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY_CONTENTS }} 14 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} 15 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} 16 | 17 | jobs: 18 | 19 | test-jvm: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Gradle Cache Setup 24 | uses: gradle/gradle-build-action@v2.4.2 25 | - name: Gradle Check 26 | run: ./gradlew jvmTest 27 | 28 | deploy-multiplatform: 29 | runs-on: ubuntu-latest 30 | needs: 31 | - test-jvm 32 | outputs: 33 | release_version: ${{ steps.output_version.outputs.release_version }} 34 | steps: 35 | - uses: actions/checkout@v3 36 | - name: Gradle Cache Setup 37 | uses: gradle/gradle-build-action@v2.4.2 38 | - name: Gradle Sync 39 | run: ./gradlew 40 | - name: Add Sdk Version to Env 41 | run: | 42 | release_version=$(./gradlew printVersion -q) 43 | echo "release_version=$release_version" >> $GITHUB_ENV 44 | - name: Publish ${{ env.release_version }} 45 | run: ./gradlew publishKotlinMultiplatformPublicationToMavenCentralRepository 46 | - name: Add Sdk Version to Output 47 | id: output_version 48 | run: echo "release_version=${{ env.release_version }}" >> $GITHUB_OUTPUT 49 | 50 | deploy-jvm: 51 | runs-on: ubuntu-latest 52 | needs: 53 | - test-jvm 54 | steps: 55 | - uses: actions/checkout@v3 56 | - name: Gradle Cache Setup 57 | uses: gradle/gradle-build-action@v2.4.2 58 | - name: Gradle Sync 59 | run: ./gradlew 60 | - name: Add Sdk Version to Env 61 | run: | 62 | release_version=$(./gradlew printVersion -q) 63 | echo "release_version=$release_version" >> $GITHUB_ENV 64 | - name: Publish ${{ env.release_version }} 65 | run: ./gradlew publishJvmPublicationToMavenCentralRepository 66 | 67 | deploy-js: 68 | runs-on: ubuntu-latest 69 | needs: 70 | - test-jvm 71 | steps: 72 | - uses: actions/checkout@v3 73 | - name: Gradle Cache Setup 74 | uses: gradle/gradle-build-action@v2.4.2 75 | - name: Gradle Sync 76 | run: ./gradlew 77 | - name: Add Sdk Version to Env 78 | run: | 79 | release_version=$(./gradlew printVersion -q) 80 | echo "release_version=$release_version" >> $GITHUB_ENV 81 | - name: Publish ${{ env.release_version }} 82 | run: ./gradlew publishJsPublicationToMavenCentralRepository 83 | 84 | deploy-ios-x64: 85 | runs-on: macos-latest 86 | needs: 87 | - test-jvm 88 | steps: 89 | - uses: actions/checkout@v3 90 | - name: Gradle Cache Setup 91 | uses: gradle/gradle-build-action@v2.4.2 92 | - name: Konan Cache Setup 93 | uses: actions/cache@v3 94 | with: 95 | path: ~/.konan 96 | key: konan-cache 97 | - name: Gradle Sync 98 | run: ./gradlew 99 | - name: Add Sdk Version to Env 100 | run: | 101 | release_version=$(./gradlew printVersion -q) 102 | echo "release_version=$release_version" >> $GITHUB_ENV 103 | - name: Publish ${{ env.release_version }} 104 | run: ./gradlew publishIosX64PublicationToMavenCentralRepository 105 | 106 | deploy-ios-arm64: 107 | runs-on: macos-latest 108 | needs: 109 | - test-jvm 110 | steps: 111 | - uses: actions/checkout@v3 112 | - name: Gradle Cache Setup 113 | uses: gradle/gradle-build-action@v2.4.2 114 | - name: Konan Cache Setup 115 | uses: actions/cache@v3 116 | with: 117 | path: ~/.konan 118 | key: konan-cache 119 | - name: Gradle Sync 120 | run: ./gradlew 121 | - name: Add Sdk Version to Env 122 | run: | 123 | release_version=$(./gradlew printVersion -q) 124 | echo "release_version=$release_version" >> $GITHUB_ENV 125 | - name: Publish ${{ env.release_version }} 126 | run: ./gradlew publishIosArm64PublicationToMavenCentralRepository 127 | 128 | deploy-ios-simulator-arm64: 129 | runs-on: macos-latest 130 | needs: 131 | - test-jvm 132 | steps: 133 | - uses: actions/checkout@v3 134 | - name: Gradle Cache Setup 135 | uses: gradle/gradle-build-action@v2.4.2 136 | - name: Konan Cache Setup 137 | uses: actions/cache@v3 138 | with: 139 | path: ~/.konan 140 | key: konan-cache 141 | - name: Gradle Sync 142 | run: ./gradlew 143 | - name: Add Sdk Version to Env 144 | run: | 145 | release_version=$(./gradlew printVersion -q) 146 | echo "release_version=$release_version" >> $GITHUB_ENV 147 | - name: Publish ${{ env.release_version }} 148 | run: ./gradlew publishIosSimulatorArm64PublicationToMavenCentralRepository 149 | 150 | create-release: 151 | runs-on: ubuntu-latest 152 | permissions: 153 | contents: write 154 | needs: 155 | - deploy-multiplatform 156 | - deploy-jvm 157 | - deploy-js 158 | - deploy-ios-x64 159 | - deploy-ios-arm64 160 | - deploy-ios-simulator-arm64 161 | steps: 162 | - uses: actions/checkout@v3 163 | - uses: ncipollo/release-action@v1 164 | with: 165 | tag: ${{ needs.deploy-multiplatform.outputs.release_version }} 166 | name: Release ${{ needs.deploy-multiplatform.outputs.release_version }} 167 | -------------------------------------------------------------------------------- /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/master/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 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || 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 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | --------------------------------------------------------------------------------