diff --git a/build.gradle b/build.gradle index 8d9f712..c3d39c3 100644 --- a/build.gradle +++ b/build.gradle @@ -90,6 +90,7 @@ dependencies { implementation group: 'com.softwaremill.sttp.client3', name: scala('okhttp-backend'), version: lib_sttp_v implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: lib_okhttp_v implementation group: 'com.google.code.gson', name: 'gson', version: lib_gson_v + implementation group: 'com.cronutils', name: 'cron-utils', version: lib_cron_utils_v testImplementation group: 'org.scalatest', name: scala('scalatest'), version: lib_scalatest_v testImplementation group: 'org.scalatest', name: scala('scalatest-freespec'), version: lib_scalatest_v diff --git a/gradle.properties b/gradle.properties index 082346f..9dcc505 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.3.0-dev1 +VERSION = 1.3.0-dev2 USE_DELTA = false VERSION_DELTA = @@ -25,5 +25,6 @@ lib_javatelegramapi_v = 6.2.0 lib_sttp_v = 3.9.0 lib_okhttp_v = 4.11.0 lib_gson_v = 2.10.1 +lib_cron_utils_v = 9.2.0 lib_scalatest_v = 3.2.17 diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala index 516499e..2b37510 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala @@ -4,7 +4,6 @@ import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener} import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.event.OnQuestionMarkReply.isAllMessageMark import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec -import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage import scala.language.postfixOps @@ -33,7 +32,9 @@ class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener { object OnQuestionMarkReply { - private val QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓') + // todo: due to the limitation of Java char, the ⁉️ character (actually not a + // single character) is not supported yet. + private val QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '⸘', '❔', '❓') def isAllMessageMark (using text: String): Boolean = { boundary[Boolean] { diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala index 522e6ab..1171379 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala @@ -7,11 +7,14 @@ import cc.sukazyo.cono.morny.util.schedule.RoutineTask import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import cc.sukazyo.cono.morny.util.CommonFormat import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis +import com.cronutils.builder.CronBuilder +import com.cronutils.model.definition.{CronDefinition, CronDefinitionBuilder} +import com.cronutils.model.time.ExecutionTime import com.pengrad.telegrambot.model.{Message, MessageEntity} import com.pengrad.telegrambot.request.{EditMessageText, SendMessage} import com.pengrad.telegrambot.response.SendResponse -import java.time.{LocalDateTime, ZoneOffset} +import java.time.{Instant, ZonedDateTime, ZoneOffset} import scala.collection.mutable.ArrayBuffer import scala.language.implicitConversions @@ -85,18 +88,24 @@ class MedicationTimer (using coeur: MornyCoeur) { object MedicationTimer { + //noinspection ScalaWeakerAccess + val cronDef: CronDefinition = CronDefinitionBuilder.defineCron + .withHours.and + .instance + @throws[IllegalArgumentException] def calcNextRoutineTimestamp (baseTimeMillis: EpochMillis, zone: ZoneOffset, notifyAt: Set[Int]): EpochMillis = { if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set") - var time = LocalDateTime.ofEpochSecond( - baseTimeMillis / 1000, ((baseTimeMillis % 1000) * 1000 * 1000) toInt, - zone - ).withMinute(0).withSecond(0).withNano(0) - time = time plusHours 1 - while (!(notifyAt contains(time getHour))) { - time = time plusHours 1 - } - (time toInstant zone) toEpochMilli + import com.cronutils.model.field.expression.FieldExpressionFactory.* + ExecutionTime.forCron(CronBuilder.cron(cronDef) + .withHour(and({ + import scala.jdk.CollectionConverters.* + (for (i <- notifyAt) yield on(i)).toList.asJava + })) + .instance + ).nextExecution( + ZonedDateTime ofInstant (Instant ofEpochMilli baseTimeMillis, zone.normalized) + ).get.toInstant.toEpochMilli } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/schedule/CronTask.scala b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/CronTask.scala new file mode 100644 index 0000000..5a8de3d --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/schedule/CronTask.scala @@ -0,0 +1,46 @@ +package cc.sukazyo.cono.morny.util.schedule + +import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis +import com.cronutils.model.time.ExecutionTime +import com.cronutils.model.Cron + +import java.time.{Instant, ZonedDateTime, ZoneId} +import scala.jdk.OptionConverters.* + +trait CronTask extends RoutineTask { + + private transparent inline def cronCalc = ExecutionTime.forCron(cron) + + def cron: Cron + + def zone: ZoneId + + override def firstRoutineTimeMillis: EpochMillis = + cronCalc.nextExecution( + ZonedDateTime.ofInstant( + Instant.now, zone + ) + ).get.toInstant.toEpochMilli + + override def nextRoutineTimeMillis (previousRoutineScheduledTimeMillis: EpochMillis): EpochMillis | Null = + cronCalc.nextExecution( + ZonedDateTime.ofInstant( + Instant.ofEpochMilli(previousRoutineScheduledTimeMillis), + zone + ) + ).toScala match + case Some(time) => time.toInstant.toEpochMilli + case None => null + +} + +object CronTask { + + def apply (_name: String, _cron: Cron, _zone: ZoneId, _main: =>Unit): CronTask = + new CronTask: + override def name: String = _name + override def cron: Cron = _cron + override def zone: ZoneId = _zone + override def main: Unit = _main + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/bot/event/OnQuestionMarkReplyTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/bot/event/OnQuestionMarkReplyTest.scala index 1c8de88..38ee81d 100644 --- a/src/test/scala/cc/sukazyo/cono/morny/test/bot/event/OnQuestionMarkReplyTest.scala +++ b/src/test/scala/cc/sukazyo/cono/morny/test/bot/event/OnQuestionMarkReplyTest.scala @@ -16,10 +16,19 @@ class OnQuestionMarkReplyTest extends MornyTests with TableDrivenPropertyChecks ("为什么?", false), ("?这不合理", false), ("??尊嘟假嘟", false), + (":¿", false), ("?????", true), + ("¿", true), + ("⁈??", true), + ("?!??", false), + ("⁇", true), + ("‽", true), + ("?⸘?", true), ("?", true), - ("?", true), - ("??❔", true), + ("??", true), + ("❔", true), + ("❓❓❓", true), +// ("⁉️", true) ) forAll(examples) { (text, is) => diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/CronTaskTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/CronTaskTest.scala new file mode 100644 index 0000000..4ace71c --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/CronTaskTest.scala @@ -0,0 +1,51 @@ +package cc.sukazyo.cono.morny.test.utils.schedule + +import cc.sukazyo.cono.morny.test.MornyTests +import cc.sukazyo.cono.morny.util.schedule.{CronTask, Scheduler} +import cc.sukazyo.cono.morny.util.CommonFormat.formatDate +import com.cronutils.builder.CronBuilder +import com.cronutils.model.definition.CronDefinitionBuilder +import com.cronutils.model.field.expression.FieldExpressionFactory as C +import com.cronutils.model.time.ExecutionTime +import org.scalatest.tagobjects.Slow + +import java.lang.System.currentTimeMillis +import java.time.{ZonedDateTime, ZoneOffset} +import java.time.temporal.ChronoUnit + +class CronTaskTest extends MornyTests { + + "cron task works fine" taggedAs Slow in { + + val scheduler = Scheduler() + val cronSecondly = + CronBuilder.cron( + CronDefinitionBuilder.defineCron + .withSeconds.and + .instance + ).withSecond(C.every(1)).instance + Thread.sleep( + ExecutionTime.forCron(cronSecondly) + .timeToNextExecution(ZonedDateTime.now) + .get.get(ChronoUnit.NANOS)/1000 + ) // aligned current time to millisecond 000 + note(s"CronTask test time aligned to ${formatDate(currentTimeMillis, 0)}") + + var times = 0 + val task = CronTask("cron-task", cronSecondly, ZoneOffset.ofHours(0).normalized, { + times = times + 1 + note(s"CronTask executed at ${formatDate(currentTimeMillis, 0)}") + }) + scheduler ++ task + Thread.sleep(10300) + + // it should be at 300ms position to 10 seconds + + scheduler % task + scheduler.stop() + note(s"CronTasks done at ${formatDate(currentTimeMillis, 0)}") + times shouldEqual 10 + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/IntervalsTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/IntervalsTest.scala index c0d1d81..da49874 100644 --- a/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/IntervalsTest.scala +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/IntervalsTest.scala @@ -17,7 +17,7 @@ class IntervalsTest extends MornyTests { val timeUsed = System.currentTimeMillis() - startTime times shouldEqual 10 timeUsed should (be <= 2100L and be >= 1900L) - info(s"interval 200ms for 10 times used time ${timeUsed}ms") + info(s"Interval Task with interval 200ms for 10 times used time ${timeUsed}ms") } } diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/SchedulerTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/SchedulerTest.scala index bb9271e..c0383b0 100644 --- a/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/SchedulerTest.scala +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/schedule/SchedulerTest.scala @@ -12,13 +12,15 @@ class SchedulerTest extends MornyTests { "Task with scheduleTime smaller than current time should be executed immediately" in { val scheduler = Scheduler() - var time = System.currentTimeMillis + val time = System.currentTimeMillis + var doneTime: Option[Long] = None scheduler ++ Task("task", 0, { - time = System.currentTimeMillis - time + doneTime = Some(System.currentTimeMillis) }) - scheduler.waitForStopAtAllDone() - time should be <= 10L - info(s"Immediately Task done with time $time") + Thread.sleep(10) + scheduler.stop() + doneTime shouldBe defined + info(s"Immediately Task done in ${doneTime.get - time}ms") } "Task's running thread name should be task name" in {