diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index b924ba0..2d17c45 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -15,13 +15,13 @@ jobs:
needs: tests
runs-on: macos-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Set Up JDK
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
- java-version: 11
+ java-version: 17
cache: gradle
- name: Grant execute permission for gradlew
@@ -38,7 +38,7 @@ jobs:
shell: bash
- name: Publish
- run: ./gradlew publishAllPublicationsToMavenCentralRepository -Psigning.keyId=${{ secrets.SIGNING_KEY_ID }} -Psigning.password=${{ secrets.SIGNING_PASSWORD }} -Psigning.secretKeyRingFile=$(echo ~/.gradle/secring.gpg)
+ run: ./gradlew publishToMavenCentral -Psigning.keyId=${{ secrets.SIGNING_KEY_ID }} -Psigning.password=${{ secrets.SIGNING_PASSWORD }} -Psigning.secretKeyRingFile=$(echo ~/.gradle/secring.gpg)
env:
- MAVEN_USERNAME: ${{ secrets.MAVEN_TOKEN_USERNAME }}
- MAVEN_PASSWORD: ${{ secrets.MAVEN_TOKEN_PASSWORD }}
\ No newline at end of file
+ ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_TOKEN_USERNAME }}
+ ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_TOKEN_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a98c0e4..65af971 100755
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -14,13 +14,13 @@ jobs:
os: [macos-13, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: "Set Up JDK"
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
- java-version: 11
+ java-version: 17
cache: gradle
- name: "Grant execute permission for gradlew"
@@ -44,7 +44,7 @@ jobs:
run: ./gradlew cleanMacosX64Test macosX64Test --tests "com.ucasoft.kcron.*"
- name: "Tests Report"
- uses: dorny/test-reporter@v1
+ uses: dorny/test-reporter@v2
if: success() || failure()
with:
name: jUnit Tests
diff --git a/README.md b/README.md
index d4a59e9..e454d1f 100755
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# KCron
Cron realization for Kotlin Multiplatform
-[](https://sonarcloud.io/summary/new_code?id=Scogun_kcron-common)   [](https://search.maven.org/artifact/com.ucasoft.kcron/kcron-common/0.23.0/jar)
+[](https://sonarcloud.io/summary/new_code?id=Scogun_kcron-common)   [](https://search.maven.org/artifact/com.ucasoft.kcron/kcron-common/0.27.4/jar)
### Features
* Kotlin Multiplatform library
@@ -43,6 +43,10 @@ builder
* Wasm
* iOS
* Support different DateTime libraries (via DateTime Provider Abstractions)
+* Provide UI builder for JVM, macOS, JavaScript and iOS on Compose UI multiplatform
+ * Multilanguage support (English and Russian now)
+

+
### Usage
#### KCron-Common library as default implementation uses [Kotlinx-DateTime](https://github.com/Kotlin/kotlinx-datetime) library
***Add with Gradle***
@@ -51,7 +55,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
- implementation 'com.ucasoft.kcron:kcron-common:0.23.0'
+ implementation 'com.ucasoft.kcron:kcron-common:0.27.4'
}
}
}
@@ -116,6 +120,6 @@ builder.years(2021..2025)
println(builder.expression) // 0/10 5-25 5,12 ? * SUN#5 2021-2025
```
### Current status
-This library is on beta version `0.23.0`.
+This library is on beta version `0.27.4`.
It is continuing to develop.
Check the news!
diff --git a/build.gradle.kts b/build.gradle.kts
index 742c600..d473ff1 100755
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
alias(libs.plugins.multiplatform) apply false
- id("publish") apply false
+ alias(libs.plugins.maven.publish) apply false
alias(libs.plugins.kover) apply false
alias(libs.plugins.benchmark) apply false
}
@@ -9,11 +9,11 @@ allprojects {
group = "com.ucasoft.kcron"
- version = "0.23.0"
-
repositories {
+ google()
mavenCentral()
}
+ version = "0.27.4"
tasks.withType {
reports {
diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts
new file mode 100644
index 0000000..a8779f7
--- /dev/null
+++ b/buildSrc/settings.gradle.kts
@@ -0,0 +1,9 @@
+dependencyResolutionManagement {
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
+
+rootProject.name = "kcron"
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/PublishingExtension.kt b/buildSrc/src/main/kotlin/PublishingExtension.kt
index 812c782..38329b7 100644
--- a/buildSrc/src/main/kotlin/PublishingExtension.kt
+++ b/buildSrc/src/main/kotlin/PublishingExtension.kt
@@ -1,7 +1,30 @@
-import org.gradle.api.model.ObjectFactory
-import org.gradle.kotlin.dsl.property
+import org.gradle.api.publish.maven.MavenPom
-open class PublishingExtension(factory: ObjectFactory) {
- val name = factory.property()
- val description = factory.property()
+fun configurePom(name: String, description: String, pom: MavenPom) {
+ pom.name.set(name)
+ pom.description.set(description)
+ pom.url.set("https://github.com/Scogun/kcron-common")
+ pom.licenses {
+ license {
+ this.name.set("The Apache License, Version 2.0")
+ url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
+ }
+ }
+ pom.developers {
+ developer {
+ id.set("Scogun")
+ this.name.set("Sergey Antonov")
+ email.set("SAntonov@ucasoft.com")
+ }
+ developer {
+ id.set("Myshkouski")
+ this.name.set("Alexei Myshkouski")
+ email.set("alexeimyshkouski@gmail.com")
+ }
+ }
+ pom.scm {
+ connection.set("scm:git:git://github.com/Scogun/kcron-common.git")
+ developerConnection.set("scm:git:ssh://github.com:Scogun/kcron-common.git")
+ url.set("https://github.com/Scogun/kcron-common")
+ }
}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/publish.gradle.kts b/buildSrc/src/main/kotlin/publish.gradle.kts
deleted file mode 100644
index ebdb19e..0000000
--- a/buildSrc/src/main/kotlin/publish.gradle.kts
+++ /dev/null
@@ -1,67 +0,0 @@
-plugins {
- `maven-publish`
- signing
-}
-
-val libraryData = extensions.create("libraryData", PublishingExtension::class)
-
-val stubJavadoc by tasks.creating(Jar::class) {
- archiveClassifier.set("javadoc")
-}
-
-publishing {
- publications.configureEach {
- if (this is MavenPublication) {
- if (name != "kotlinMultiplatform") {
- artifact(stubJavadoc)
- }
- pom {
- name.set(libraryData.name)
- description.set(libraryData.description)
- url.set("https://github.com/Scogun/kcron-common")
- licenses {
- license {
- name.set("The Apache License, Version 2.0")
- url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
- }
- }
- developers {
- developer {
- id.set("Scogun")
- name.set("Sergey Antonov")
- email.set("SAntonov@ucasoft.com")
- }
- developer {
- id.set("Myshkouski")
- name.set("Alexei Myshkouski")
- email.set("alexeimyshkouski@gmail.com")
- }
- }
- scm {
- connection.set("scm:git:git://github.com/Scogun/kcron-common.git")
- developerConnection.set("scm:git:ssh://github.com:Scogun/kcron-common.git")
- url.set("https://github.com/Scogun/kcron-common")
- }
- }
- }
- }
- repositories {
- maven {
- name = "MavenCentral"
- url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
- credentials {
- username = System.getenv("MAVEN_USERNAME")
- password = System.getenv("MAVEN_PASSWORD")
- }
- }
- }
-}
-
-signing {
- sign(publishing.publications)
-}
-
-tasks.withType().configureEach {
- val signingTasks = tasks.withType()
- mustRunAfter(signingTasks)
-}
\ No newline at end of file
diff --git a/docs/images/CronUiBuilderEn.png b/docs/images/CronUiBuilderEn.png
new file mode 100644
index 0000000..aa7d09d
Binary files /dev/null and b/docs/images/CronUiBuilderEn.png differ
diff --git a/gradle.properties b/gradle.properties
index 7fc6f1f..47c1aa5 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1,3 @@
kotlin.code.style=official
+org.jetbrains.compose.experimental.jscanvas.enabled=true
+org.jetbrains.compose.experimental.macos.enabled=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b15a5ea..dde2b28 100755
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,9 +1,11 @@
[versions]
-kotlin = "2.1.0"
-kotlinx-datetime = "0.6.1"
+kotlin = "2.2.0"
+kotlinx-datetime = "0.7.1"
kotest = "5.9.1"
-kover = "0.9.0"
-benchmark = "0.4.13"
+kover = "0.9.1"
+benchmark = "0.4.14"
+compose = "1.8.2"
+maven-publish = "0.34.0"
[libraries]
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" }
@@ -14,4 +16,7 @@ kotest-assetions = { group = "io.kotest", name = "kotest-assertions-core", versi
[plugins]
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "benchmark" }
\ No newline at end of file
+benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "benchmark" }
+compose = { id = "org.jetbrains.compose", version.ref = "compose" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index a4b76b9..8bdaf60 100755
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index cea7a79..2a84e18 100755
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index f3b75f3..ef07e01 100755
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
#!/bin/sh
#
-# Copyright © 2015-2021 the original authors.
+# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -205,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
diff --git a/gradlew.bat b/gradlew.bat
index 9b42019..5eed7ee 100755
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+set CLASSPATH=
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
diff --git a/kcron-abstractions/build.gradle.kts b/kcron-abstractions/build.gradle.kts
index bec208b..17d82c4 100644
--- a/kcron-abstractions/build.gradle.kts
+++ b/kcron-abstractions/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
alias(libs.plugins.multiplatform)
- id("publish")
+ alias(libs.plugins.maven.publish)
}
kotlin {
@@ -28,7 +28,11 @@ kotlin {
}
}
-libraryData {
- name.set("KCron Abstractions")
- description.set("Abstractions for Kotlin Multiplatform Cron realization")
+mavenPublishing {
+ publishToMavenCentral()
+ signAllPublications()
+
+ pom {
+ configurePom("KCron Abstractions", "Abstractions for Kotlin Multiplatform Cron realization", this)
+ }
}
\ No newline at end of file
diff --git a/kcron-common/build.gradle.kts b/kcron-common/build.gradle.kts
index bc1dffe..2de30c6 100644
--- a/kcron-common/build.gradle.kts
+++ b/kcron-common/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
alias(libs.plugins.multiplatform)
- id("publish")
+ alias(libs.plugins.maven.publish)
alias(libs.plugins.kover)
}
@@ -40,7 +40,11 @@ kotlin {
}
}
-libraryData {
- name.set("KCron Common")
- description.set("Cron realization for Kotlin Multiplatform with Kotlinx DateTime Provider")
+mavenPublishing {
+ publishToMavenCentral()
+ signAllPublications()
+
+ pom {
+ configurePom("KCron Common", "Cron realization for Kotlin Multiplatform with Kotlinx DateTime Provider", this)
+ }
}
\ No newline at end of file
diff --git a/kcron-common/src/commonTest/kotlin/com/ucasoft/kcron/BuildAndParseTests.kt b/kcron-common/src/commonTest/kotlin/com/ucasoft/kcron/BuildAndParseTests.kt
index a901cfa..f5fc94e 100644
--- a/kcron-common/src/commonTest/kotlin/com/ucasoft/kcron/BuildAndParseTests.kt
+++ b/kcron-common/src/commonTest/kotlin/com/ucasoft/kcron/BuildAndParseTests.kt
@@ -5,11 +5,12 @@ import com.ucasoft.kcron.core.settings.Version
import io.kotest.assertions.throwables.shouldThrowWithMessage
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
-import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.test.BeforeTest
import kotlin.test.Test
+import kotlin.time.Clock
+import kotlin.time.ExperimentalTime
class BuildAndParseTests {
@@ -17,6 +18,7 @@ class BuildAndParseTests {
private val modernCronExpression = "30 * * ? * * 2050"
+ @OptIn(ExperimentalTime::class)
@BeforeTest
fun setupOnce() {
currentYear = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).year
diff --git a/kcron-core/build.gradle.kts b/kcron-core/build.gradle.kts
index 97279b2..cd76e9d 100644
--- a/kcron-core/build.gradle.kts
+++ b/kcron-core/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
alias(libs.plugins.multiplatform)
- id("publish")
+ alias(libs.plugins.maven.publish)
alias(libs.plugins.kover)
}
@@ -40,7 +40,11 @@ kotlin {
}
}
-libraryData {
- name.set("KCron Core")
- description.set("Cron realization for Kotlin Multiplatform")
+mavenPublishing {
+ publishToMavenCentral()
+ signAllPublications()
+
+ pom {
+ configurePom("KCron Core", "Cron realization for Kotlin Multiplatform", this)
+ }
}
\ No newline at end of file
diff --git a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/exceptions/WrongPartCombination.kt b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/exceptions/WrongPartCombination.kt
index 0e573fe..20c99a2 100644
--- a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/exceptions/WrongPartCombination.kt
+++ b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/exceptions/WrongPartCombination.kt
@@ -5,7 +5,7 @@ import com.ucasoft.kcron.core.common.PartValue
import com.ucasoft.kcron.core.parsers.CombinationRule
class WrongPartCombination(
- private val part: MutableMap.MutableEntry,
+ private val part: Map.Entry,
private val dependency: CombinationRule,
secondPart: PartValue
) : Throwable("Wrong part combination: part ${part.key.partName} with ${part.value.type} type requires that part ${dependency.part.partName} has ${dependency.type} type but it was ${secondPart.type}!") {
diff --git a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/ParseResult.kt b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/ParseResult.kt
index 81f5960..19d21f3 100644
--- a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/ParseResult.kt
+++ b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/ParseResult.kt
@@ -3,7 +3,4 @@ package com.ucasoft.kcron.core.parsers
import com.ucasoft.kcron.core.common.CronPart
import com.ucasoft.kcron.core.common.PartValue
-class ParseResult {
-
- val parts = mutableMapOf()
-}
\ No newline at end of file
+data class ParseResult(val parts: Map)
\ No newline at end of file
diff --git a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/Parser.kt b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/Parser.kt
index 9772a77..6b12048 100644
--- a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/Parser.kt
+++ b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/Parser.kt
@@ -70,14 +70,16 @@ class Parser {
throw WrongPartsExpression(partExceptions)
}
- val result = ParseResult()
- for ((index, parser) in partParsers.withIndex()) {
- result.parts[parser.part] = PartValue(parser.group as CronGroups, expressionParts[index])
- }
- return result
+ return ParseResult(
+ partParsers.withIndex().associate { (index, part) ->
+ part.part to PartValue(
+ part.group as CronGroups,
+ expressionParts[index]
+ )
+ })
}
- private fun ensureCombinationRules(parts: MutableMap) {
+ private fun ensureCombinationRules(parts: Map) {
val combinationExceptions = mutableListOf()
for (part in parts) {
val rule = combinationRules.firstOrNull { r -> r.part == part.key && r.type == part.value.type }
diff --git a/kcron-kotlinx-datetime/build.gradle.kts b/kcron-kotlinx-datetime/build.gradle.kts
index 8debe37..5977b32 100644
--- a/kcron-kotlinx-datetime/build.gradle.kts
+++ b/kcron-kotlinx-datetime/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
alias(libs.plugins.multiplatform)
- id("publish")
+ alias(libs.plugins.maven.publish)
}
kotlin {
@@ -33,7 +33,11 @@ kotlin {
}
}
-libraryData {
- name.set("KCron Kotlinx DateTime")
- description.set("Kotlinx DateTime Provider for Kotlin Multiplatform Cron realization")
+mavenPublishing {
+ publishToMavenCentral()
+ signAllPublications()
+
+ pom {
+ configurePom("KCron Kotlinx DateTime", "Kotlinx DateTime Provider for Kotlin Multiplatform Cron realization", this)
+ }
}
\ No newline at end of file
diff --git a/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTime.kt b/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTime.kt
index 7020f16..c596be7 100644
--- a/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTime.kt
+++ b/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTime.kt
@@ -2,12 +2,15 @@ package com.ucasoft.kcron.kotlinx.datetime
import com.ucasoft.kcron.abstractions.CronDateTime
import kotlinx.datetime.*
+import kotlin.time.Clock
+import kotlin.time.ExperimentalTime
class CronLocalDateTime: CronDateTime {
private val dateTime: LocalDateTime
+ @OptIn(ExperimentalTime::class)
constructor() {
dateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
}
@@ -20,10 +23,10 @@ class CronLocalDateTime: CronDateTime {
get() = dateTime.year
override val month: Int
- get() = dateTime.monthNumber
+ get() = dateTime.month.number
override val dayOfMonth: Int
- get() = dateTime.dayOfMonth
+ get() = dateTime.day
override val isoDayOfWeek: Int
get() = dateTime.dayOfWeek.isoDayNumber
diff --git a/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTimeExtensions.kt b/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTimeExtensions.kt
index 6ba7c54..db3a1ec 100644
--- a/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTimeExtensions.kt
+++ b/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTimeExtensions.kt
@@ -1,9 +1,11 @@
package com.ucasoft.kcron.kotlinx.datetime
import kotlinx.datetime.*
+import kotlinx.datetime.number
+import kotlin.time.ExperimentalTime
fun LocalDateTime.toCronLocalDateTime() =
- CronLocalDateTime(this.year, this.monthNumber, this.dayOfMonth, this.hour, this.minute, this.second)
+ CronLocalDateTime(this.year, this.month.number, this.day, this.hour, this.minute, this.second)
fun LocalDateTime.plusHours(hours: Int, timeZone: TimeZone = TimeZone.currentSystemDefault()): LocalDateTime {
return plus(this, hours, DateTimeUnit.HOUR, timeZone)
@@ -17,6 +19,7 @@ fun LocalDateTime.minusDays(days: Int, timeZone: TimeZone = TimeZone.currentSyst
return plus(this, if (days > 0) { days * -1 } else { days }, DateTimeUnit.DAY, timeZone)
}
+@OptIn(ExperimentalTime::class)
private fun plus(self: LocalDateTime, value: Int, unit: DateTimeUnit, timeZone: TimeZone) : LocalDateTime {
return self.toInstant(timeZone).plus(value, unit, timeZone).toLocalDateTime(timeZone)
}
\ No newline at end of file
diff --git a/kcron-ui-builder/build.gradle.kts b/kcron-ui-builder/build.gradle.kts
new file mode 100644
index 0000000..1fe2f02
--- /dev/null
+++ b/kcron-ui-builder/build.gradle.kts
@@ -0,0 +1,55 @@
+plugins {
+ alias(libs.plugins.multiplatform)
+ alias(libs.plugins.maven.publish)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.kover)
+}
+
+kotlin {
+ jvmToolchain(11)
+ jvm()
+ macosX64()
+ macosArm64()
+ js(IR) {
+ browser()
+ nodejs()
+ }
+ @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
+ wasmJs {
+ browser()
+ nodejs()
+ }
+ iosX64()
+ iosArm64()
+ iosSimulatorArm64()
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.components.resources)
+ implementation(project(":kcron-common"))
+ }
+ }
+ // TODO UI Tests on Compose UI are unstable, enable when stable
+ /*commonTest {
+ dependencies {
+ implementation(kotlin("test"))
+ @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
+ implementation(compose.uiTest)
+ }
+ }*/
+ }
+}
+
+mavenPublishing {
+ publishToMavenCentral()
+ signAllPublications()
+
+ pom {
+ configurePom("KCron UI Builder", "UI Builder for KCron using JetBrains Compose Multiplatform", this)
+ }
+}
\ No newline at end of file
diff --git a/kcron-ui-builder/src/commonMain/composeResources/values-ru/strings.xml b/kcron-ui-builder/src/commonMain/composeResources/values-ru/strings.xml
new file mode 100644
index 0000000..90fb818
--- /dev/null
+++ b/kcron-ui-builder/src/commonMain/composeResources/values-ru/strings.xml
@@ -0,0 +1,60 @@
+
+ Минута
+ Час
+ День месяца
+ Месяц
+ День недели
+
+ Вручную
+
+
+ - Каждый
+ - Каждые %1$d
+
+
+
+ - Каждая
+
+
+ Начало часа
+
+ Полночь
+ 6
+ 9
+ Полдень
+ 18
+ 21
+
+ 1-е
+ 15-е
+ Последний
+ Первый будний день
+ Последний будний день
+
+
+ - Январь
+ - Февраль
+ - Март
+ - Апрель
+ - Май
+ - Июнь
+ - Июль
+ - Август
+ - Сентябрь
+ - Октябрь
+ - Ноябрь
+ - Декабрь
+
+
+
+ - Понедельник
+ - Вторник
+ - Среда
+ - Четверг
+ - Пятница
+ - Суббота
+ - Воскресенье
+
+
+ Отмена
+
\ No newline at end of file
diff --git a/kcron-ui-builder/src/commonMain/composeResources/values/strings.xml b/kcron-ui-builder/src/commonMain/composeResources/values/strings.xml
new file mode 100644
index 0000000..7414a24
--- /dev/null
+++ b/kcron-ui-builder/src/commonMain/composeResources/values/strings.xml
@@ -0,0 +1,61 @@
+
+ Minute
+ Hour
+ Day of Month
+ Month
+ Day of Week
+
+ Custom
+
+
+ - Every
+ - Every %1$d
+
+
+
+ - Every
+
+
+ Hour Start
+
+ Midnight
+ 6 AM
+ 9 AM
+ Noon
+ 6 PM
+ 9 PM
+
+ 1st
+ 15th
+ Last
+ First Weekday
+ Last Weekday
+
+
+ - January
+ - February
+ - March
+ - April
+ - May
+ - June
+ - July
+ - August
+ - September
+ - October
+ - November
+ - December
+
+
+
+ - Monday
+ - Tuesday
+ - Wednesday
+ - Thursday
+ - Friday
+ - Saturday
+ - Sunday
+
+
+ Cancel
+ OK
+
\ No newline at end of file
diff --git a/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronField.kt b/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronField.kt
new file mode 100644
index 0000000..c4ea244
--- /dev/null
+++ b/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronField.kt
@@ -0,0 +1,93 @@
+package com.ucasoft.kcron.ui.builder
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuAnchorType
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun CronField(
+ label: String,
+ value: String,
+ options: List> = emptyList(),
+ customPlaceholder: String = "Enter custom value",
+ isError: Boolean = false,
+ onValueChanged: (String) -> Unit = {}
+) {
+ var open by remember { mutableStateOf(false) }
+ var prebuildPattern by remember { mutableStateOf(options.firstOrNull { it.second == value } ?: options.last()) }
+ var selectedPattern by remember { mutableStateOf(value) }
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ label,
+ style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
+ )
+ ExposedDropdownMenuBox(
+ expanded = open,
+ onExpandedChange = { open = it },
+ ) {
+ OutlinedTextField(
+ prebuildPattern.first,
+ onValueChange = {},
+ readOnly = true,
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(open)
+ },
+ modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable)
+ )
+ if (options.isNotEmpty()) {
+ ExposedDropdownMenu(
+ expanded = open,
+ onDismissRequest = { open = false }
+ ) {
+ options.map {
+ DropdownMenuItem(
+ text = { Text(it.first) },
+ onClick = {
+ prebuildPattern = it
+ if (it.second.isNotEmpty()) {
+ selectedPattern = it.second
+ onValueChanged(it.second)
+ }
+ open = false
+ }
+ )
+ }
+ }
+ }
+ }
+ if (prebuildPattern.second == "") {
+ OutlinedTextField(
+ selectedPattern,
+ onValueChange = {
+ selectedPattern = it
+ onValueChanged(it)
+ },
+ placeholder = { Text(customPlaceholder) },
+ isError = isError
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronUiBuilder.kt b/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronUiBuilder.kt
new file mode 100644
index 0000000..468921e
--- /dev/null
+++ b/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronUiBuilder.kt
@@ -0,0 +1,203 @@
+package com.ucasoft.kcron.ui.builder
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.ucasoft.kcron.Cron
+import com.ucasoft.kcron.core.builders.Builder
+import com.ucasoft.kcron.core.common.CronPart
+import com.ucasoft.kcron.core.common.PartValue
+import com.ucasoft.kcron.core.common.WeekDays
+import com.ucasoft.kcron.core.exceptions.WrongPartCombinations
+import com.ucasoft.kcron.core.exceptions.WrongPartExpression
+import com.ucasoft.kcron.core.parsers.Parser
+import com.ucasoft.kcron.kcron_ui_builder.generated.resources.*
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.pluralStringResource
+import org.jetbrains.compose.resources.stringArrayResource
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun CronUiBuilder(
+ expression: String = "* * * * *",
+ modifier: Modifier = Modifier,
+ allowCustom: Boolean = true,
+ firstDayOfWeek: WeekDays = WeekDays.Monday,
+ onBuild: (Builder<*, *, *>) -> Unit = {},
+) {
+ val parser = Parser()
+ var parts by remember { mutableStateOf(parser.parse(expression).parts) }
+
+ Card(
+ modifier = modifier.padding(12.dp)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val daysOfWeek = stringArrayResource(Res.array.days_of_week).let {
+ it.drop(firstDayOfWeek.ordinal) + it.take(firstDayOfWeek.ordinal)
+ }
+ val fields = listOf(
+ Res.string.minute to listOf(
+ pluralStringResource(Res.plurals.every_feminine, 1) to "*",
+ stringResource(Res.string.hour_start) to "0",
+ "15" to "15",
+ "30" to "30",
+ "45" to "45",
+ pluralStringResource(Res.plurals.every, 5, 5) to "0/5",
+ pluralStringResource(Res.plurals.every, 15, 15) to "0/15",
+ pluralStringResource(Res.plurals.every, 30, 30) to "0/30"
+ ),
+ Res.string.hour to listOf(
+ pluralStringResource(Res.plurals.every, 1) to "*",
+ stringResource(Res.string.midnight) to "0",
+ stringResource(Res.string._6_am) to "6",
+ stringResource(Res.string._9_am) to "9",
+ stringResource(Res.string.noon) to "12",
+ stringResource(Res.string._6_pm) to "18",
+ stringResource(Res.string._9_pm) to "21",
+ pluralStringResource(Res.plurals.every, 2, 2) to "0/2",
+ pluralStringResource(Res.plurals.every, 6, 6) to "0/6",
+ pluralStringResource(Res.plurals.every, 12, 12) to "0/12"
+ ),
+ Res.string.day_of_month to listOf(
+ pluralStringResource(Res.plurals.every, 1) to "*",
+ stringResource(Res.string.first) to "1",
+ stringResource(Res.string.fifteenth) to "15",
+ stringResource(Res.string.last) to "L",
+ stringResource(Res.string.first_weekday) to "1W",
+ stringResource(Res.string.last_weekday) to "LW",
+ pluralStringResource(Res.plurals.every, 2, 2) to "1/2",
+ pluralStringResource(Res.plurals.every, 7, 7) to "1/7",
+ pluralStringResource(Res.plurals.every, 14, 14) to "1/14"
+ ),
+ Res.string.month to listOf(
+ pluralStringResource(Res.plurals.every, 1) to "*"
+ ) + stringArrayResource(Res.array.months).withIndex().map { it.value to (it.index + 1).toString() },
+ Res.string.day_of_week to listOf(
+ pluralStringResource(Res.plurals.every, 1) to "*"
+ ) + daysOfWeek.withIndex().map { (index, name) -> name to (index + 1).toString() }
+ ).map { (label, options) ->
+ label to if (allowCustom)
+ options + (stringResource(Res.string.custom) to "")
+ else
+ options
+ }
+
+ fields.map { (labelRes, options) ->
+ var isError by remember { mutableStateOf(false) }
+ CronField(
+ stringResource(labelRes),
+ getPartsValue(parts, labelRes),
+ options,
+ isError = isError
+ ) {
+ try {
+ val copy = copyParts(parts, labelRes, it)
+ parser.parse(copy.entries.joinToString(" ") { it.value.value })
+ parts = copy
+ isError = false
+ } catch (wrong: WrongPartExpression) {
+ println(wrong)
+ isError = true
+ } catch (wrong: WrongPartCombinations) {
+ throw wrong
+ }
+ }
+ }
+ Spacer(
+ Modifier.weight(1f)
+ )
+ OutlinedTextField(
+ parts.entries.drop(1).take(5).joinToString(" ") { it.value.value },
+ onValueChange = {},
+ readOnly = true,
+ modifier = Modifier.background(MaterialTheme.colorScheme.primary),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedTextColor = MaterialTheme.colorScheme.onPrimary,
+ unfocusedTextColor = MaterialTheme.colorScheme.onPrimary
+ ),
+ textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center)
+ )
+ Spacer(
+ Modifier.weight(1f)
+ )
+ Row {
+ Box(
+ modifier = Modifier.weight(1f),
+ contentAlignment = Alignment.Center
+ ) {
+ Button(
+ onClick = {},
+ ) {
+ Text(stringResource(Res.string.cancel))
+ }
+ }
+ Box(
+ modifier = Modifier.weight(1f),
+ contentAlignment = Alignment.Center
+ ) {
+ Button(
+ onClick = {
+ onBuild(Cron.parseAndBuild(parts.entries.joinToString(" ") { it.value.value }) {
+ it.firstDayOfWeek = firstDayOfWeek
+ })
+ },
+ ) {
+ Text(stringResource(Res.string.ok))
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun getPartsValue(parts: Map, labelRes: StringResource) = when (labelRes) {
+ Res.string.minute -> parts[CronPart.Minutes]!!.value
+ Res.string.hour -> parts[CronPart.Hours]!!.value
+ Res.string.day_of_month -> parts[CronPart.Days]!!.value
+ Res.string.month -> parts[CronPart.Months]!!.value
+ Res.string.day_of_week -> parts[CronPart.DaysOfWeek]!!.value
+ else -> ""
+}
+
+private fun copyParts(parts: Map, labelRes: StringResource, newValue: String) = when (labelRes) {
+ Res.string.minute -> copyParts(parts, minutes = newValue)
+ Res.string.hour -> copyParts(parts, hours = newValue)
+ Res.string.day_of_month -> copyParts(parts, dayOfMonth = newValue)
+ Res.string.month -> copyParts(parts, month = newValue)
+ Res.string.day_of_week -> copyParts(parts, dayOfWeek = newValue)
+ else -> parts
+}
+
+private fun copyParts(
+ parts: Map,
+ minutes: String? = null,
+ hours: String? = null,
+ dayOfMonth: String? = null,
+ month: String? = null,
+ dayOfWeek: String? = null
+) : Map {
+ return parts.entries.associate {
+ it.key to when (it.key) {
+ CronPart.Minutes -> PartValue(it.value.type, minutes ?: it.value.value)
+ CronPart.Hours -> PartValue(it.value.type, hours ?: it.value.value)
+ CronPart.Days -> PartValue(it.value.type, dayOfMonth ?: it.value.value)
+ CronPart.Months -> PartValue(it.value.type, month ?: it.value.value)
+ CronPart.DaysOfWeek -> PartValue(it.value.type, dayOfWeek ?: it.value.value)
+ else -> it.value
+ }
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a83cd1e..f7824fb 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,5 +1,5 @@
plugins {
- id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
rootProject.name = "kcron"
@@ -9,5 +9,6 @@ include(
"kcron-common",
"kcron-common-benchmark",
"kcron-core",
- "kcron-kotlinx-datetime"
+ "kcron-kotlinx-datetime",
+ "kcron-ui-builder"
)
\ No newline at end of file