Skip to content

Commit

Permalink
fix: cron compatibility to unix standard - 5 tokens (#3)
Browse files Browse the repository at this point in the history
* chore: fix cronexp to more standart version

* chore: readme

* chore: scope

* chore: naming

* chore: bump junit

* chore: bump junit

* chore: refactoring and naming
  • Loading branch information
yamilmedina authored Mar 24, 2024
1 parent 211b2c7 commit c9c1a27
Show file tree
Hide file tree
Showing 41 changed files with 1,362 additions and 1,598 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ A parser that converts natural (English) language to a cron expression in Kotlin
You can add the library to your project using gradle:

```kotlin
implementation("io.github.yamilmedina:natural-kron:0.0.1")
implementation("io.github.yamilmedina:natural-kron:0.1.0")
```

## Usage ##

```kotlin
import io.github.yamilmedina.naturalkron.NaturalKron
import io.github.yamilmedina.kron.NaturalKronParser

val expression = "every day at 9am"
val parsed = NaturalKronExpressionParser().parse(expression)
val parsed = NaturalKronParser().parse(expression)

val expectedKronExpressionEveryDayAt9am = "0 0 9 * * *"
val expectedKronExpressionEveryDayAt9am = "0 9 * * *"
assertEquals("*", parsed.dayOfWeek) // --> every day cron expression
assertEquals(expectedKronExpressionEveryDayAt9am, parsed.toString()) // --> TRUE
```
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ repositories {

dependencies {
testImplementation(libs.kotlin.test)
testImplementation(libs.junit.params)
}

tasks.test {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
kotlin.code.style=official
VERSION_NAME=0.0.3
VERSION_NAME=0.1.0

SONATYPE_HOST=CENTRAL_PORTAL
RELEASE_SIGNING_ENABLED=false
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
kotlin = "1.9.23"
maven-publish = "0.28.0"
kover = "0.7.4"
junit = "5.10.2"

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Expand All @@ -10,3 +11,4 @@ kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }

[libraries]
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.github.yamilmedina.kron

internal interface ExpressionElementProvider {
/**
* @param string
* @return Boolean
*/
fun matches(string: String): Boolean

/**
* @return Boolean
*/
fun canProvideMinute(): Boolean

/**
* @return String
*/
fun getMinuteElement(): String?

/**
* @return Boolean
*/
fun isMinuteElementLocked(): Boolean

/**
* @return Boolean
*/
fun canProvideHour(): Boolean

/**
* @return String
*/
fun getHourElement(): String?

/**
* @return Boolean
*/
fun isHourElementLocked(): Boolean

/**
* @return Boolean
*/
fun canProvideDayNumber(): Boolean

/**
* @return String
*/
fun getDayNumberElement(): String?

/**
* @return Boolean
*/
fun isDayNumberElementLocked(): Boolean

/**
* @return Boolean
*/
fun canProvideMonth(): Boolean

/**
* @return String
*/
fun getMonthElement(): String?

/**
* @return Boolean
*/
fun isMonthElementLocked(): Boolean

/**
* @return Boolean
*/
fun canProvideDayOfWeek(): Boolean

/**
* @return String
*/
fun getDayOfWeekElement(): String?

/**
* @return Boolean
*/
fun isDayOfWeekElementLocked(): Boolean
}


57 changes: 57 additions & 0 deletions src/main/kotlin/io/github/yamilmedina/kron/KronExpression.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.github.yamilmedina.kron

class KronExpression(
var minute: String? = null,
var hour: String? = null,
var dayNumber: String? = null,
var month: String? = null,
var dayOfWeek: String? = null
) {

fun hasMinute(): Boolean = minute != null

fun setMinute(minute: String): KronExpression {
this.minute = minute
return this
}

fun hasHour(): Boolean = hour != null

fun setHour(hour: String): KronExpression {
this.hour = hour
return this
}

fun hasDayNumber(): Boolean = dayNumber != null

fun setDayNumber(dayNumber: String): KronExpression {
this.dayNumber = dayNumber
return this
}

fun hasMonth(): Boolean = month != null

fun setMonth(month: String): KronExpression {
this.month = month
return this
}

fun hasDayOfWeek(): Boolean = dayOfWeek != null

fun setDayOfWeek(dayOfWeek: String): KronExpression {
this.dayOfWeek = dayOfWeek
return this
}

fun hasNothing(): Boolean = !hasMinute() && !hasHour() && !hasDayNumber() && !hasMonth() && !hasDayOfWeek()

override fun toString(): String = "%s %s %s %s %s".format(
if (hasMinute()) minute else "0",
if (hasHour()) hour else "0",
if (hasDayNumber()) dayNumber else "*",
if (hasMonth()) month else "*",
if (hasDayOfWeek()) dayOfWeek else "*"
)
}


172 changes: 172 additions & 0 deletions src/main/kotlin/io/github/yamilmedina/kron/NaturalKronParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package io.github.yamilmedina.kron

import io.github.yamilmedina.kron.elementprovider.DayNumber
import io.github.yamilmedina.kron.elementprovider.hour.Base12Hour
import io.github.yamilmedina.kron.elementprovider.hour.Base12HourShort
import io.github.yamilmedina.kron.elementprovider.hour.Base24Hour
import io.github.yamilmedina.kron.elementprovider.hour.Midnight
import io.github.yamilmedina.kron.elementprovider.hour.Noon
import io.github.yamilmedina.kron.elementprovider.recurring.EveryDay
import io.github.yamilmedina.kron.elementprovider.recurring.EveryDayNumber
import io.github.yamilmedina.kron.elementprovider.recurring.EveryHour
import io.github.yamilmedina.kron.elementprovider.recurring.EveryMinute
import io.github.yamilmedina.kron.elementprovider.recurring.EveryMonth
import io.github.yamilmedina.kron.elementprovider.recurring.EveryWeek
import io.github.yamilmedina.kron.elementprovider.recurring.EveryYear
import java.text.ParseException
import java.util.regex.Pattern

class NaturalKronParser {

companion object {
const val VALID_PATTERN =
"^(@reboot|@yearly|@annually|@monthly|@weekly|@daily|@midnight|@hourly|((?:[1-9]?\\d|\\*)\\s*(?:(?:[\\/\\-][1-9]?\\d)|(?:,[1-9]?\\d)+)?\\s*){5})$"
}

private val elementProviders: List<ExpressionElementProvider> = listOf(
EveryYear(),
EveryMonth(),
EveryWeek(),
EveryDayNumber(),
EveryDay(),
EveryHour(),
EveryMinute(),
DayNumber(),
Noon(),
Midnight(),
Base12Hour(),
Base12HourShort(),
Base24Hour()
)

/**
* Parses a string to a KronExpression
*
* @param string the string to parse
* @throws ParseException if the string is not a valid cron expression
* @return a [KronExpression] instance
*/
fun parse(string: String): KronExpression {
val lowercaseString = string.toLowerCase()
val mappings = mapOf(
"@yearly" to KronExpression("0", "0", "1", "1", "*"),
"@annually" to KronExpression("0", "0", "1", "1", "*"),
"@monthly" to KronExpression("0", "0", "1", "*", "*"),
"@weekly" to KronExpression("0", "0", "*", "*", "0"),
"@midnight" to KronExpression("0", "0", "*", "*", "*"),
"@daily" to KronExpression("0", "0", "*", "*", "*"),
"@hourly" to KronExpression("0", "*", "*", "*", "*")
)

if (mappings.containsKey(lowercaseString)) {
return mappings[lowercaseString]!!
}

val expression = KronExpression()
var isMinuteElementLocked = false
var isHourElementLocked = false
var isDayNumberElementLocked = false
var isMonthElementLocked = false
var isDayOfWeekElementLocked = false

val shouldUpdateMinute: (KronExpression, ExpressionElementProvider) -> Boolean = { expression, subParser ->
subParser.canProvideMinute() && !isMinuteElementLocked
}

val shouldUpdateHour: (KronExpression, ExpressionElementProvider) -> Boolean = { expression, subParser ->
subParser.canProvideHour() && !isHourElementLocked
}

val shouldUpdateDayNumber: (KronExpression, ExpressionElementProvider) -> Boolean = { expression, subParser ->
subParser.canProvideDayNumber() && !isDayNumberElementLocked
}

val shouldUpdateMonth: (KronExpression, ExpressionElementProvider) -> Boolean = { expression, subParser ->
subParser.canProvideMonth() && !isMonthElementLocked
}

val shouldUpdateDayOfWeek: (KronExpression, ExpressionElementProvider) -> Boolean = { expression, subParser ->
subParser.canProvideDayOfWeek() && !isDayOfWeekElementLocked
}

for (elementProvider in elementProviders) {
if (elementProvider.matches(lowercaseString)) {
if (shouldUpdateMinute(expression, elementProvider)) {
expression.minute = elementProvider.getMinuteElement()
}

if (shouldUpdateHour(expression, elementProvider)) {
expression.hour = elementProvider.getHourElement()
}

if (shouldUpdateDayNumber(expression, elementProvider)) {
expression.dayNumber = elementProvider.getDayNumberElement()
}

if (shouldUpdateMonth(expression, elementProvider)) {
expression.month = elementProvider.getMonthElement()
}

if (shouldUpdateDayOfWeek(expression, elementProvider)) {
expression.dayOfWeek = elementProvider.getDayOfWeekElement()
}

if (elementProvider.isMinuteElementLocked()) {
isMinuteElementLocked = true
}

if (elementProvider.isHourElementLocked()) {
isHourElementLocked = true
}

if (elementProvider.isDayNumberElementLocked()) {
isDayNumberElementLocked = true
}

if (elementProvider.isMonthElementLocked()) {
isMonthElementLocked = true
}

if (elementProvider.isDayOfWeekElementLocked()) {
isDayOfWeekElementLocked = true
}
}
}

val validPattern = Pattern.compile(VALID_PATTERN)
if (expression.hasNothing() || !validPattern.matcher(expression.toString()).matches()) {
throw ParseException("Unable to parse \"$string\", expression is: $expression", Int.MIN_VALUE)
}

return expression
}

/**
* Converts a string to a valid cron expression
*
* @param string the string to convert
* @throws ParseException if the string is not a valid cron expression
* @return a valid cron expression in string format
*/
fun fromString(string: String): String {
val parser = NaturalKronParser()
return parser.parse(string).toString()
}

/**
* Checks if a string is a valid cron expression
*/
fun isValid(string: String): Boolean {
if (string == "@reboot") {
return true
}
return try {
fromString(string)
true
} catch (e: ParseException) {
false
}
}
}


Loading

0 comments on commit c9c1a27

Please sign in to comment.