Compare commits

..

152 Commits
0.2.1 ... 0.6.2

Author SHA1 Message Date
a8be548a72 Update build.gradle 2021-07-29 19:35:07 +06:00
6ec77b4e6f Remove bintray from repos 2021-07-29 19:33:35 +06:00
0ecb34709b update workflows 2021-07-28 14:41:27 +06:00
0f973adb9f fill changelog, remove legacy compiler for js 2021-07-28 14:31:30 +06:00
abc7a63d2e Update gradle.properties 2021-07-28 11:37:43 +06:00
57fa3a30a6 Update dependencies 2021-07-28 11:27:17 +06:00
f50564abe2 start 0.6.2 2021-07-28 11:24:38 +06:00
8276ebc7ec Update README.md 2021-07-28 11:23:55 +06:00
5a6084c573 Merge pull request #18 from InsanusMokrassar/0.6.1
0.6.1
2021-06-03 13:40:06 +06:00
437206e915 remove old comments 2021-06-03 12:55:16 +06:00
0ecc6f0028 upfill of changelog 2021-06-03 12:52:39 +06:00
fec17f949f fixes in tests 2021-06-03 12:37:13 +06:00
c3ed1d024c fix tests 2021-06-03 12:16:39 +06:00
b4790b892a partial fix 2021-05-30 18:53:54 +06:00
28f84d4e3a rewrite mechanism of calculating of near time 2021-05-30 18:53:54 +06:00
22ef00fe8f Update publishing_packages.yml 2021-05-30 09:00:57 +06:00
c509d35b00 update klock 2021-05-29 17:30:28 +06:00
3de66a7274 start 0.6.1 2021-05-29 17:25:40 +06:00
1f0387363d Merge pull request #17 from InsanusMokrassar/0.6.0
0.6.0
2021-05-25 15:48:02 +06:00
e816ad88bc Update CHANGELOG.md 2021-05-25 15:47:47 +06:00
404ea5e188 Update gradle.properties 2021-05-25 15:21:45 +06:00
63e110dbc8 Update CHANGELOG.md 2021-05-15 01:35:10 +06:00
39323f3cd1 Update gradle.properties 2021-05-15 01:34:46 +06:00
15e3430a55 update dependencies 2021-05-07 13:33:35 +06:00
fde5d91375 start 0.6.0 2021-05-07 12:59:13 +06:00
ba548969e1 Merge pull request #16 from InsanusMokrassar/0.5.2
0.5.2
2021-04-24 18:46:21 +06:00
c6bfc5c8f8 optimize imports before merge 2021-04-24 18:33:59 +06:00
301afb1ee0 updates and fixes 2021-04-24 18:33:37 +06:00
7e58ec0873 fixes in flow according to time zones 2021-04-24 16:58:25 +06:00
db1af7818e Update CHANGELOG.md 2021-04-24 01:53:57 +06:00
635a2d6a44 Update CHANGELOG.md 2021-04-24 01:47:29 +06:00
da88d18d4d add note about offsets 2021-04-22 12:44:22 +06:00
20f12f3f2d update readme to include week days info 2021-04-22 12:32:45 +06:00
86cdda51a1 tests and fixes in week work 2021-04-22 12:24:26 +06:00
bfa546ad1f add support of week days 2021-04-22 11:58:19 +06:00
0c7ceb03ee temporarily add nodejs crutch 2021-04-20 17:24:33 +06:00
b0889d0677 remove redundant changes 2021-04-10 15:38:55 +06:00
3408d58cb7 fixes for Tz 2021-04-10 15:38:55 +06:00
5c81eede9c add CronDateTimeSchedulerTz variant in CollectionKronScheduler 2021-04-10 00:33:05 +06:00
5844859369 rewrite system with offsets 2021-04-10 00:28:08 +06:00
a07027399c add offset parameter in strings 2021-03-30 14:07:39 +06:00
93246cada7 fix of next with DateTimeTz 2021-03-29 22:29:10 +06:00
5311e651f4 add work with time zones 2021-03-29 22:14:49 +06:00
68c49faa1d update kotlin 2021-03-29 21:32:39 +06:00
8ce351ecfe start 0.5.2 2021-03-29 21:30:24 +06:00
b43bbea341 Delete .travis.yml 2021-03-29 21:07:52 +06:00
fff010b890 Merge pull request #13 from InsanusMokrassar/0.5.1
0.5.1
2021-03-13 22:19:34 +06:00
3a0870144b Update publishing_packages.yml 2021-03-13 22:03:38 +06:00
40adf80d6f Update publishing_packages.yml 2021-03-13 21:49:08 +06:00
0d1e382182 Update publishing_packages.yml 2021-03-13 21:34:56 +06:00
a15b4162fb Update publishing_packages.yml 2021-03-13 21:17:42 +06:00
d52435a014 include publishing to packages 2021-03-13 20:31:50 +06:00
9ce69efad5 update publishing scripts 2021-03-13 20:30:02 +06:00
092fc6bc9b update gradle wrapper 2021-03-13 20:23:15 +06:00
ec85fd77f9 update dependencies 2021-03-13 20:22:55 +06:00
496d4cf100 start 0.5.1 2021-03-13 20:19:03 +06:00
dce0fd87a1 Update CHANGELOG.md 2021-01-03 20:01:25 +06:00
fd842b17fa Merge pull request #12 from InsanusMokrassar/0.5.0
0.5.0
2021-01-03 13:32:04 +06:00
726824fb4f disable jetifier 2021-01-03 12:52:04 +06:00
e04d14ccb1 fixed issues 2021-01-03 12:45:38 +06:00
3f132da62d add KronSchedulerWork 2021-01-02 23:56:31 +06:00
3cce44664f fix tests issue 2021-01-02 22:56:35 +06:00
0376eb7910 add android target 2021-01-02 22:43:32 +06:00
bbb4f41723 fill changelog 2021-01-02 21:55:06 +06:00
365adb1016 add years, KrontabWrapper, new function to create scheduler from template and update readme 2021-01-02 21:35:08 +06:00
476239faa6 CronDateTimeScheduler now is internal (again :( ) 2021-01-02 20:13:47 +06:00
0a2b5ef6a0 start 0.5.0 2021-01-02 19:42:10 +06:00
aedb49f6f8 Merge pull request #11 from InsanusMokrassar/0.4.2
0.4.2
2021-01-01 21:11:16 +06:00
e8071fa15c deprecation messages fix in CronDateTimeScheduler 2021-01-01 21:09:31 +06:00
1cc8ff5519 small update of readme 2021-01-01 21:00:11 +06:00
aebd32cb5e update dependencies 2021-01-01 20:58:45 +06:00
d11f106f19 deprecation of CronDateTImeScheduler 2021-01-01 20:51:09 +06:00
36f316357f start 0.4.2 2021-01-01 20:50:31 +06:00
5fcbe50e39 update publishing 2020-12-14 19:22:06 +06:00
86552d427c update module name in dokka 2020-12-07 14:24:41 +06:00
92d902c027 update dokka 2020-12-07 14:08:59 +06:00
9d8cb20d10 Merge pull request #10 from InsanusMokrassar/0.4.1
0.4.1
2020-12-06 02:14:08 +06:00
ca248a25ad compile fixes 2020-12-06 01:48:00 +06:00
4995a34c1a CronDateTimeScheduler adding 2020-12-06 01:44:24 +06:00
3162780447 update kdocs 2020-12-06 01:40:29 +06:00
05af4d1f67 update dependencies 2020-12-06 01:03:24 +06:00
3c0818cabf CollectionKronScheduler 2020-12-06 01:00:57 +06:00
a38c233bbf downgrade version to 0.4.1 2020-12-06 00:01:02 +06:00
346672b32b start 0.5.0 2020-12-04 22:59:30 +06:00
7458e4880d Update README.md 2020-12-04 17:56:44 +06:00
46f227f9a3 Merge pull request #9 from InsanusMokrassar/0.4.0
0.4.0
2020-11-21 15:29:23 +06:00
35f5531d26 Update gradle-wrapper.properties 2020-11-21 15:20:04 +06:00
1e7acb2f4b update changelog 2020-11-21 15:08:17 +06:00
50e6868fba update scripts 2020-11-21 15:04:34 +06:00
e0ece2de33 package upfix 2020-11-21 15:00:30 +06:00
104e9b1c87 change package 2020-11-21 14:58:19 +06:00
c1d912f170 start 0.4.0 2020-11-21 14:48:57 +06:00
f4ace9ac37 Merge pull request #8 from InsanusMokrassar/0.3.3
0.3.3
2020-11-09 23:48:48 +06:00
6374911902 update coroutines 2020-11-09 23:22:34 +06:00
7b370ef3e9 reorganize changelog 2020-11-09 23:15:04 +06:00
07f2d7b9cd Merge branch 'master' into 0.3.3 2020-10-21 21:59:09 +06:00
95244dc18f Update README.md 2020-10-13 16:49:15 +06:00
2a8267f0c9 start 0.3.3 2020-10-10 22:53:18 +06:00
753dcae747 Merge pull request #7 from InsanusMokrassar/0.3.2
0.3.2
2020-10-10 22:25:43 +06:00
1431c0cda2 fix for parser 2020-10-10 22:17:48 +06:00
66e75b4315 add "each" 2020-10-10 21:39:00 +06:00
5a13437c17 add last and first 2020-10-10 21:12:32 +06:00
6d612ce95d start 0.3.2 2020-10-10 20:03:52 +06:00
92f1ec03dd fix in github_release 2020-10-08 18:17:46 +06:00
f23740a6a5 fix of github_release files 2020-10-08 18:04:27 +06:00
16d8850ca7 fix of github_release files 2020-10-08 17:53:24 +06:00
f278361470 Merge pull request #6 from InsanusMokrassar/0.3.1
0.3.1
2020-10-08 17:13:28 +06:00
47d84751e3 update publication scripts 2020-10-08 17:09:08 +06:00
3aa658168d add changelog and github release tools 2020-10-08 15:26:07 +06:00
60458999b4 update version, dependencies 2020-10-08 15:26:07 +06:00
94573ecd43 update gradle files 2020-08-29 11:48:54 +06:00
241fb2ee51 KrontabTemplate typealias 2020-08-22 21:42:47 +06:00
cdeea96c54 update dependencies versions 2020-08-22 21:36:55 +06:00
4b310071d7 start 0.3.0 2020-08-22 21:22:42 +06:00
5ccebc5033 Merge pull request #2 from InsanusMokrassar/0.2.4
0.2.4
2020-07-26 00:12:15 +06:00
65477bb0bc Update gradle wrapper version 2020-07-24 23:52:46 +06:00
990b0d1011 fixes in docs and refactor 2020-07-24 15:23:44 +06:00
5cc9c8278e removing of unnecessary code 2020-07-24 14:25:30 +06:00
12d2d82c71 Update CHANGELOG.md 2020-07-24 14:18:57 +06:00
0d19d80d48 buildSchedule hotfix 2020-07-24 14:18:18 +06:00
5358bfdb47 add support of ranges in strings 2020-07-24 13:16:56 +06:00
5c452d58b7 update libraries 2020-07-24 11:50:17 +06:00
a36fd1a30a start 0.2.4 2020-07-24 11:43:45 +06:00
6dc85d7e18 Merge pull request #1 from InsanusMokrassar/0.2.3
0.2.3
2020-06-03 22:16:03 +06:00
07e6baba3d Revert "fix of travis build script"
This reverts commit eb11235360.
2020-06-03 22:11:44 +06:00
d8ed07da98 new buildSchedule function and tests for string parsing 2020-06-03 22:04:05 +06:00
c9243d7968 optimization and docs of executes 2020-06-03 21:53:55 +06:00
eb11235360 fix of travis build script 2020-06-03 21:42:30 +06:00
565aff4538 fix for EverySecondScheduler 2020-06-03 21:39:52 +06:00
686716e20f TimeBuilder docs 2020-06-03 21:36:01 +06:00
6077384a17 fill documentation and optimize SchedulerBuilder#build and createSimpleScheduler functions 2020-06-03 21:21:52 +06:00
408349be04 docs 2020-06-03 21:07:10 +06:00
237deff0e6 fill docs of SchedulerShortcuts 2020-06-03 20:41:54 +06:00
59cd600d60 update CronDateTime documentation 2020-06-03 20:19:57 +06:00
be33a130d3 add data docs 2020-06-03 20:17:00 +06:00
1f971117bd refactor of CronDateTime 2020-06-03 20:11:42 +06:00
0178ac140a add dokka config 2020-06-03 20:10:06 +06:00
69e2e10d29 small optimization of Executes 2020-06-03 20:03:04 +06:00
25901b7ab6 add actions configs 2020-06-03 19:02:59 +06:00
7892a6897e start 0.2.3 and update dependencies 2020-06-03 18:59:58 +06:00
ef436892aa optimize imports 2020-03-22 18:35:25 +06:00
85cb87c459 version 0.2.2 2020-03-22 18:35:06 +06:00
75f6e06552 include build status from travis badge 2020-02-19 15:12:53 +06:00
e600d5fec9 update travis config to temporaly avoid of scanning 2020-02-19 15:09:54 +06:00
f480873cf0 add travis build script 2020-02-18 19:56:08 +06:00
363aeed732 Add maven link badge 2020-02-07 11:26:43 +00:00
18e0af4592 Add '_config.yml' 2020-02-07 11:21:05 +00:00
5c0e02220f update publish script 2020-01-15 21:04:03 +06:00
d516c26381 update publish script 2020-01-15 20:39:39 +06:00
10ee1672bc fix README 2020-01-14 14:53:36 +06:00
b564c36359 update publish gradle 2020-01-13 23:23:13 +06:00
394522db6b Merge branch '0.2.1' of InsanusMokrassar/krontab into master 2020-01-13 16:02:27 +00:00
61 changed files with 2242 additions and 614 deletions

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# These are supported funding model platforms
patreon: InsanusMokrassar
custom: ['https://paypal.me/InsanusMokrassar?locale.x=ru_RU']

3
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
code: "**/*.kt"
gradle: "**/*.gradle"
markdown: "**/*.md"

13
.github/workflows/greetings.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Greetings
on: [pull_request, issues]
jobs:
greeting:
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: 'Welcome with your first issue'
pr-message: 'Welcome with your first Pull Request'

18
.github/workflows/label.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
# This workflow will triage pull requests and apply a label based on the
# paths that are modified in the pull request.
#
# To use this workflow, you will need to set up a .github/labeler.yml
# file with configuration. For more information, see:
# https://github.com/actions/labeler/blob/master/README.md
name: "Pull Request Labeler"
on:
- pull_request
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -0,0 +1,21 @@
name: Publish package to GitHub Packages
on: [push]
jobs:
publishing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Update version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
sed -i -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" gradle.properties
- name: prebuild
run: ./gradlew clean build
- name: Publish package
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidReleasePublication
env:
GITHUBPACKAGES_USER: ${{ github.actor }}
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

12
.github/workflows/regular_build.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Regular build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: build
run: ./gradlew clean build

View File

@@ -1,12 +1,157 @@
# Changelog
# 0.2.0
## 0.6.2
**LEGACY COMPILER VARIANT FOR JS IS NOT AVAILABLE SINCE THIS UPDATE**
* Versions
* `Kotlin`: `1.5.21`
* `Coroutines`: `1.5.1`
* `Klock`: `2.3.1`
## 0.6.1
* Versions
* `Klock`: `2.1.0` -> `2.1.2`
* Rewriting of default mechanism of `KronScheduler`s
## 0.6.0
* Versions
* `Kotlin`: `1.4.32` -> `1.5.10`
* `Coroutines`: `1.4.3` -> `1.5.0`
* `Klock`: `2.0.7` -> `2.1.0`
## 0.5.2
* Versions
* `Kotlin`: `1.4.31` -> `1.4.32`
* Supporting of weekdays
* Supporting of timezones
* Any `KronScheduler` now can be used for calling `next` with `DateTimeTz`
* New type `KronSchedulerTz`
* `SchedulerFlow` has been deprecated
* New extension `asTzFlow` and small changes in `asFlow` logic
* `merge` extensions now return `CollectionKronScheduler` instead of just `KronScheduler`
## 0.5.1
* Versions
* `Kotlin`: `1.4.21` -> `1.4.31`
* `Coroutines`: `1.4.2` -> `1.4.3`
* `Klock`: `2.0.3` -> `2.0.7`
* `Androidx Work`: `2.4.0` -> `2.5.0`
## 0.5.0 Years
**BREAKING CHANGES**
* `CronDateTimeScheduler` has been marked as `internal` and no longer accessible outside of internal functions
* Old methods `merge` and `plus` related to `CronDateTimeScheduler` has been marked as `deprecated` and changed their
parameters types - it is `KronScheduler` now
* New methods `merge` has been added
* **`KronScheduler#next` method now is nullable. Use `nextOrRelative`/`nextOrNow` to get next time certainly**
* **Years was added as optional part of krontab template and opportunity in `SchedulerBuilder`**
* New builder `YearsBuilder`
* `SchedulerFlow#collectSafely` will be normally (without exceptions) finish when `next` of scheduler will return
null
* `KronScheduler#doOnce` will run code immediately in case when `next` is returning null value
* `KrontabTemplateWrapper` has been added
* New extension `KrontabTemplate#toKronScheduler` (works as `toSchedule`)
* **Fixed issue related to the fact that `toNearDateTime` of `CronDateTime` incorrectly handled months**
* **Android target has been added**
## 0.4.2
* Versions
* `Kotlin`: `1.4.20` -> `1.4.21`
* `Klock`: `2.0.1` -> `2.0.3`
* `CronDateTimeScheduler` now is deprecated and will be set up as `internal` in future
## 0.4.1
* Versions:
* `Coroutines`: `1.4.1` -> `1.4.2`
* `Klock`: `2.0.0` -> `2.0.1`
* `CronDateTimeScheduler` now is public
* New functions for `CronDateTimeScheduler`
* Add `CollectionKronScheduler`. It will give opportunity to unite several schedulers in one
## 0.4.0
**BREAKING CHANGES**
Package of project has been changed. Migration:
* Replace in your dependencies `com.insanusmokrassar:krontab` by `dev.inmo:krontab`
* Replace in your project all imports `com.insanusmokrassar.krontab` by `dev.inmo.krontab`
* Versions:
* `Kotlin`: `1.4.10` -> `1.4.20`
* `Klock`: `1.12.1` -> `2.0.0`
## 0.3.3
* Versions:
* `Coroutines`: `1.3.9` -> `1.4.1`
## 0.3.2
* Function `TimeBuilder#each` was added (works as `at`)
* Add opportunity to use `first` shortcuts:
* Value property `TimeBuilder#first` for including via functions like `TimeBuilder#at`
* Shortcut for kron string format `f` or `F`
* Add opportunity to use `last` shortcuts:
* Value property `TimeBuilder#last` for including via functions like `TimeBuilder#at`
* Shortcut for kron string format `l` or `L`
## 0.3.1
* Versions:
* `Kotlin`: `1.4.0` -> `1.4.10`
* `Klock`: `1.12.0` -> `1.12.1`
## 0.3.0
* Versions:
* `Kotlin`: `1.3.72` -> `1.4.0`
* `Coroutines`: `1.3.8` -> `1.3.9`
* `Klock`: `1.11.14` -> `1.12.0`
* Typealias `KrontabTemplate` was added
* Extension `KrontabTemplate#toSchedule` was added
### 0.2.4
* Updates in libraries:
* Klock `1.11.3` -> `1.11.14`
* Coroutines `1.3.7` -> `1.3.8`
* Ranges support were included. Now it is possible to correctly use syntax `0-5` in strings schedules
## 0.2.3
* Updates in libraries:
* Kotlin `1.3.70` -> `1.3.72`
* Coroutines `1.3.5` -> `1.3.7`
* Klock `1.10.0` -> `1.11.3`
* A lot of KDocs added and fixed
* `EverySecondScheduler` changed its building logic - now it is lazy with builder using
* `KronScheduler#doOnce` was optimized: now it will be explicitly called once and return result of its calculations
* `KronScheduler#doWhile` was rewritten to use `KronScheduler#doOnce` for calculations of `block` result
* New `buildSchedule(String)` function as a shortcut for `createSimpleScheduler(String)`
## 0.2.2
* Updates in libraries:
* Kotlin `1.3.61` -> `1.3.70`
* Coroutines `1.3.3` -> `1.3.5`
* Klock `1.8.6` -> `1.10.0`
## 0.2.1
* Added support of flows: now any `KronScheduler` can be convert to `Flow<DateTime>` using `asFlow` extension
## 0.2.0
* Updated way of publishing (for more info look at the [git](https://git.insanusmokrassar.com/InsanusMokrassar/krontab))
* Updates in libraries:
* Coroutines `1.3.2` -> `1.3.3`
* Klock `1.7.3` -> `1.8.6`
## 0.2.1
* Added support of flows: now any `KronScheduler` can be convert to `Flow<DateTime>` using `asFlow` extension

View File

@@ -1,6 +1,7 @@
# krontab
[ ![Download](https://api.bintray.com/packages/insanusmokrassar/InsanusMokrassar/krontab/images/download.svg) ](https://bintray.com/insanusmokrassar/InsanusMokrassar/krontab/_latestVersion)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/krontab/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/krontab)
[![Build Status](https://travis-ci.com/InsanusMokrassar/krontab.svg?branch=master)](https://travis-ci.com/InsanusMokrassar/krontab)
Library was created to give oppotunity to launch some things from time to time according to some schedule in
runtime of applications.
@@ -12,6 +13,8 @@ runtime of applications.
| [ How to use: Config from string ](#config-from-string) |
| [ How to use: Config via builder (DSL preview) ](#config-via-builder) |
| [ How to use: KronScheduler as a Flow ](#KronScheduler-as-a-Flow) |
| [ How to use: Offsets ](#Offsets) |
| [ How to use: Note about week days ](#Note-about-week-days) |
## How to use
@@ -37,12 +40,12 @@ If you want to include `krontab` in your project, just add next line to your
dependencies part:
```groovy
implementation "com.insanusmokrassar:krontab:$krontab_version"
implementation "dev.inmo:krontab:$krontab_version"
```
Next version is the latest currently for the library:
[ ![Download](https://api.bintray.com/packages/insanusmokrassar/InsanusMokrassar/krontab/images/download.svg) ](https://bintray.com/insanusmokrassar/InsanusMokrassar/krontab/_latestVersion)
[ ![Download](https://api.bintray.com/packages/insanusmokrassar/InsanusMokrassar/krontab-mpp/images/download.svg) ](https://bintray.com/insanusmokrassar/InsanusMokrassar/krontab-mpp/_latestVersion)
For old version of Gradle, instead of `implementation` word developers must use `compile`.
@@ -50,19 +53,22 @@ For old version of Gradle, instead of `implementation` word developers must use
Developers can use more simple way to configure repeat times is string. String configuring
like a `crontab`, but with a little bit different meanings:
```
/-------- Seconds
| /------ Minutes
| | /---- Hours
| | | /-- Days of months
| | | | / Months
| | | | |
* * * * *
/--------------- Seconds
| /------------- Minutes
| | /----------- Hours
| | | /--------- Days of months
| | | | /------- Months
| | | | | /----- (optional) Year
| | | | | | /--- (optional) Timezone offset
| | | | | | | / (optional) Week days
* * * * * * 0o *w
```
It is different with original `crontab` syntax for the reason, that expected that in practice developers
will use seconds and minutes with more probability than months (for example). In fact, developers will use something
like:
will use seconds and minutes with more probability than months (for example) or even years. In fact, developers will use
something like:
```kotlin
doWhile("/5 * * * *") {
@@ -71,7 +77,7 @@ doWhile("/5 * * * *") {
}
```
Or more version:
An other version:
```kotlin
doInfinity("/5 * * * *") {
@@ -83,7 +89,7 @@ Both of examples will print `Called` message every five seconds.
### Config via builder
Also this library currently supports DSL for creating the same goals:
Also, this library currently supports DSL for creating the same goals:
```kotlin
val kronScheduler = buildSchedule {
@@ -140,8 +146,8 @@ val kronScheduler = buildSchedule {
val flow = kronScheduler.asFlow()
```
So, in this case any operations related to flow are available and it is expected tt th will work correctly. For example,
it is possible to use this flow with `takeWhile`:
So, in this case any operations related to flow are available and it is expected that they will work correctly. For
example, it is possible to use this flow with `takeWhile`:
```kotlin
flow.takeWhile {
@@ -150,3 +156,27 @@ flow.takeWhile {
action()
}
```
### Offsets
Offsets in this library works via passing parameter ending with `o` in any place after `month` config. Currently
there is only one format supported for offsets: minutes of offsets. To use time zones you will need to call `next`
method with `DateTimeTz` argument or `nextTimeZoned` method with any `KronScheduler` instance, but in case if this
scheduler is not instance of `KronSchedulerTz` it will works like you passed just `DateTime`.
Besides, in case you wish to use time zones explicitly, you will need to get `KronSchedulerTz`. It is possible by:
* Using `createSimpleScheduler`/`buildSchedule`/`KrontabTemplate#toSchedule`/`KrontabTemplate#toKronScheduler` methods
with passing `defaultOffset` parameter
* Using `SchedulerBuilder#build`/`createSimpleScheduler`/`buildSchedule`/`KrontabTemplate#toSchedule`/`KrontabTemplate#toKronScheduler`
methods with casting to `KronSchedulerTz` in case you are pretty sure that it is timezoned `KronScheduler`
* Creating your own implementation of `KronSchedulerTz`
### Note about week days
Unlike original CRON, here week days:
* Works as `AND`: cron date time will search first day which will pass requirement according all parameters including
week days
* You may use any related to numbers syntax with week days: `0-3w`, `0,1,2,3w`, etc.
* Week days (like years and offsets) are optional and can be placed anywhere after `month`

1
_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-cayman

View File

@@ -1,70 +1,134 @@
buildscript {
repositories {
mavenLocal()
jcenter()
// jcenter()
mavenCentral()
google()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version"
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
classpath 'com.android.tools.build:gradle:4.1.3'
}
}
plugins {
id "org.jetbrains.kotlin.multiplatform" version "$kotlin_version"
id "org.jetbrains.kotlin.plugin.serialization" version "$kotlin_version"
id "org.jetbrains.dokka" version "$dokka_version"
}
project.version = "0.2.1"
project.group = "com.insanusmokrassar"
// temporal crutch until legacy tests will be stabled or legacy target will be removed
allprojects {
if (it != rootProject.findProject("docs")) {
tasks.whenTaskAdded { task ->
if(task.name == "jsLegacyBrowserTest" || task.name == "jsLegacyNodeTest") {
task.enabled = false
}
}
}
}
apply plugin: "com.android.library"
project.version = "$version"
project.group = "dev.inmo"
apply from: "publish.gradle"
apply from: "github_release.gradle"
repositories {
mavenLocal()
jcenter()
// jcenter()
mavenCentral()
maven { url "https://kotlin.bintray.com/kotlinx" }
// maven { url "https://kotlin.bintray.com/kotlinx" }
google()
}
apply from: './dokka.gradle'
kotlin {
jvm()
js()
js(IR) {
browser()
nodejs()
}
android {
publishAllLibraryVariants()
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$kotlin_coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
api "com.soywiz.korlibs.klock:klock:$klockVersion"
}
}
androidMain {
dependencies {
api "androidx.work:work-runtime-ktx:$androidx_work_version"
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutines_version"
}
}
jvmMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
}
}
jsMain {
jsTest {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$kotlin_coroutines_version"
implementation kotlin('test-js')
}
}
androidTest {
dependencies {
implementation kotlin('test-junit')
}
}
}
}
apply plugin: 'com.getkeepsafe.dexcount'
android {
compileSdkVersion "$android_compileSdkVersion".toInteger()
buildToolsVersion "$android_buildToolsVersion"
defaultConfig {
minSdkVersion "$android_minSdkVersion".toInteger()
targetSdkVersion "$android_compileSdkVersion".toInteger()
versionCode "${android_code_version}".toInteger()
versionName "$version"
}
buildTypes {
release {
minifyEnabled false
}
debug {
debuggable true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test {
java.srcDir file("src/jvmTest")
}
}
}

24
changelog_parser.sh Normal file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
function parse() {
version=$1
while IFS= read -r line && [ -z "`echo $line | grep -e "^#\+ $version"`" ]
do
: # do nothing
done
while IFS= read -r line && [ -z "`echo $line | grep -e "^#\+"`" ]
do
echo "$line"
done
}
version=$1
file=$2
if [ -n "$file" ]; then
parse $version < "$file"
else
parse $version
fi

45
dokka.gradle Normal file
View File

@@ -0,0 +1,45 @@
dokkaHtml {
String dokkaOutputDirectoryPath = null
switch (true) {
case project.hasProperty("DOKKA_PATH"):
dokkaOutputDirectoryPath = project.property("DOKKA_PATH").toString()
break
case System.getenv("DOKKA_PATH") != null:
dokkaOutputDirectoryPath = System.getenv("DOKKA_PATH")
break
}
if (dokkaOutputDirectoryPath != null) {
File outputDirectoryFile = new File(dokkaOutputDirectoryPath)
outputDirectoryFile.mkdirs()
outputDirectory = outputDirectoryFile
}
moduleName.set("kdocs")
dokkaSourceSets {
configureEach {
skipDeprecated.set(true)
includeNonPublic.set(true)
reportUndocumented.set(true)
sourceLink {
localDirectory.set(file("./"))
remoteUrl.set(new URL("https://github.com/InsanusMokrassar/krontab/blob/master/"))
remoteLineSuffix.set("#L")
}
}
named("commonMain") {
sourceRoot { path = "src/commonMain" }
}
//
// named("jsMain") {
// sourceRoot { path = "src/jsMain" }
// }
//
// named("jvmMain") {
// sourceRoot { path = "src/jvmMain" }
// }
}
}

30
github_release.gradle Normal file
View File

@@ -0,0 +1,30 @@
private String getCurrentVersionChangelog(String version) {
OutputStream changelogDataOS = new ByteArrayOutputStream()
exec {
commandLine 'chmod', "+x", './changelog_parser.sh'
}
exec {
standardOutput = changelogDataOS
commandLine './changelog_parser.sh', "$version", 'CHANGELOG.md'
}
return changelogDataOS.toString().trim()
}
if (new File(projectDir, "secret.gradle").exists()) {
apply from: './secret.gradle'
apply plugin: "com.github.breadmoirai.github-release"
githubRelease {
token "${project.property('GITHUB_RELEASE_TOKEN')}"
owner "InsanusMokrassar"
repo "${rootProject.name}"
tagName "${project.version}"
releaseName "${project.version}"
targetCommitish "${project.version}"
body getCurrentVersionChangelog("${project.version}")
}
}

View File

@@ -1,9 +1,37 @@
kotlin.code.style=official
kotlin_version=1.3.61
kotlin_coroutines_version=1.3.3
gradle_bintray_plugin_version=1.8.4
klockVersion=1.8.6
org.gradle.parallel=true
kotlin.js.generate.externals=true
kotlin.incremental=true
kotlin.incremental.js=true
kotlin.incremental.multiplatform=true
android.useAndroidX=true
android.enableJetifier=false
kotlin_version=1.5.21
kotlin_coroutines_version=1.5.1
dokka_version=1.4.32
klockVersion=2.3.1
## Github reease
github_release_plugin_version=2.2.12
## Android
android_minSdkVersion=19
android_compileSdkVersion=30
android_buildToolsVersion=30.0.3
dexcount_version=2.0.0
junit_version=4.12
test_ext_junit_version=1.1.2
espresso_core=3.3.0
androidx_work_version=2.5.0
## Common
version=0.6.2
android_code_version=6

View File

@@ -1,6 +1,6 @@
# Tue May 21 17:58:54 HKT 2019
# Fri Jul 24 23:52:00 +06 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip

View File

@@ -1,57 +0,0 @@
apply plugin: 'maven-publish'
task javadocsJar(type: Jar) {
classifier = 'javadoc'
}
afterEvaluate {
project.publishing.publications.all {
// rename artifacts
groupId "${project.group}"
if (it.name.contains('kotlinMultiplatform')) {
artifactId = "${project.name}"
} else {
artifactId = "${project.name}-$name"
}
}
}
publishing {
publications.all {
artifact javadocsJar
pom.withXml {
asNode().children().last() + {
resolveStrategy = Closure.DELEGATE_FIRST
description "It is an analog of crontab util for Kotlin Coroutines"
name "Krontab"
url "https://git.insanusmokrassar.com/InsanusMokrassar/krontab"
scm {
developerConnection "scm:git:[fetch=]https://git.insanusmokrassar.com/InsanusMokrassar/krontab.git[push=]https://git.insanusmokrassar.com/InsanusMokrassar/krontab.git"
url "https://git.insanusmokrassar.com/InsanusMokrassar/krontab.git"
}
developers {
developer {
id "InsanusMokrassar"
name "Ovsiannikov Aleksei"
email "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
license {
name "Apache Software License 2.0"
url "https://git.insanusmokrassar.com/InsanusMokrassar/krontab/src/master/LICENSE"
}
}
}
}
}
}

View File

@@ -1 +0,0 @@
{"bintrayConfig":{"repo":"InsanusMokrassar","packageName":"${project.name}","packageVcs":"https://git.insanusmokrassar.com/InsanusMokrassar/krontab"},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://git.insanusmokrassar.com/InsanusMokrassar/krontab/src/master/LICENSE"}],"mavenConfig":{"name":"Krontab","description":"It is an analog of crontab util for Kotlin Coroutines","url":"https://git.insanusmokrassar.com/InsanusMokrassar/krontab","vcsUrl":"https://git.insanusmokrassar.com/InsanusMokrassar/krontab.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]}}

1
mpp_config.kpsb Normal file
View File

@@ -0,0 +1 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/krontab/blob/master/LICENSE"}],"mavenConfig":{"name":"Krontab","description":"It is an analog of crontab util for Kotlin Coroutines","url":"https://github.com/InsanusMokrassar/krontab","vcsUrl":"https://github.com/InsanusMokrassar/krontab.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/krontab"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}}

View File

@@ -1,48 +1,69 @@
apply plugin: 'com.jfrog.bintray'
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply from: "maven.publish.gradle"
task javadocsJar(type: Jar) {
classifier = 'javadoc'
}
bintray {
user = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
key = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
filesSpec {
from "${buildDir}/publications/"
eachFile {
if (it.getName() == "module.json") {
File file = it.getFile()
String directorySubname = file.parentFile.name
if (directorySubname == "kotlinMultiplatform") {
it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.module")
} else {
it.setPath("${project.name}-${directorySubname}/${project.version}/${project.name}-${directorySubname}-${project.version}.module")
publishing {
publications.all {
artifact javadocsJar
pom {
description = "It is an analog of crontab util for Kotlin Coroutines"
name = "Krontab"
url = "https://github.com/InsanusMokrassar/krontab"
scm {
developerConnection = "scm:git:[fetch=]https://github.com/InsanusMokrassar/krontab.git[push=]https://github.com/InsanusMokrassar/krontab.git"
url = "https://github.com/InsanusMokrassar/krontab.git"
}
developers {
developer {
id = "InsanusMokrassar"
name = "Ovsiannikov Aleksei"
email = "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
license {
name = "Apache Software License 2.0"
url = "https://github.com/InsanusMokrassar/krontab/blob/master/LICENSE"
}
}
}
repositories {
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/krontab")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
} else {
it.exclude()
}
}
into "${project.group}".replace(".", "/")
}
pkg {
repo = "InsanusMokrassar"
name = "${project.name}"
vcsUrl = "https://git.insanusmokrassar.com/InsanusMokrassar/krontab"
licenses = ["Apache-2.0"]
version {
name = "${project.version}"
released = new Date()
vcsTag = "${project.version}"
gpg {
sign = true
passphrase = project.hasProperty('signing.gnupg.passphrase') ? project.property('signing.gnupg.passphrase') : System.getenv('signing.gnupg.passphrase')
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
}
}
}
}
bintrayUpload.doFirst {
publications = publishing.publications.collect {
it.name
}
signing {
useGpgCmd()
sign publishing.publications
}
bintrayUpload.dependsOn publishToMavenLocal

View File

@@ -1,32 +0,0 @@
package com.insanusmokrassar.krontab
import com.soywiz.klock.DateTime
import kotlinx.coroutines.delay
suspend inline fun KronScheduler.doWhile(noinline block: suspend () -> Boolean) {
do {
delay(next().unixMillisLong - DateTime.now().unixMillisLong)
} while (block())
}
suspend inline fun doWhile(
scheduleConfig: String,
noinline block: suspend () -> Boolean
) = createSimpleScheduler(scheduleConfig).doWhile(block)
suspend inline fun KronScheduler.doInfinity(noinline block: suspend () -> Unit) = doWhile {
block()
true
}
suspend inline fun doInfinity(
scheduleConfig: String,
noinline block: suspend () -> Unit
) = createSimpleScheduler(scheduleConfig).doInfinity(block)
suspend inline fun KronScheduler.doOnce(noinline block: suspend () -> Unit) = doWhile {
block()
false
}
suspend inline fun doOnce(
scheduleConfig: String,
noinline block: suspend () -> Unit
) = createSimpleScheduler(scheduleConfig).doOnce(block)

View File

@@ -1,7 +0,0 @@
package com.insanusmokrassar.krontab
import com.soywiz.klock.DateTime
interface KronScheduler {
suspend fun next(relatively: DateTime = DateTime.now()): DateTime
}

View File

@@ -1,29 +0,0 @@
package com.insanusmokrassar.krontab
import com.insanusmokrassar.krontab.builder.buildSchedule
import com.insanusmokrassar.krontab.internal.CronDateTime
internal val anyCronDateTime by lazy {
CronDateTime()
}
val AnyTimeScheduler: KronScheduler by lazy {
CronDateTimeScheduler(listOf(anyCronDateTime))
}
val EverySecondScheduler: KronScheduler
get() = AnyTimeScheduler
val EveryMinuteScheduler: KronScheduler by lazy {
buildSchedule { minutes { 0 every 1 } }
}
val EveryHourScheduler: KronScheduler by lazy {
buildSchedule { hours { 0 every 1 } }
}
val EveryDayOfMonthScheduler: KronScheduler by lazy {
buildSchedule { dayOfMonth { 0 every 1 } }
}
val EveryMonthScheduler: KronScheduler by lazy {
buildSchedule { months { 0 every 1 } }
}

View File

@@ -1,68 +0,0 @@
package com.insanusmokrassar.krontab
import com.insanusmokrassar.krontab.internal.*
import com.insanusmokrassar.krontab.internal.CronDateTime
import com.insanusmokrassar.krontab.internal.parseDaysOfMonth
import com.insanusmokrassar.krontab.internal.parseHours
import com.insanusmokrassar.krontab.internal.parseMinutes
import com.insanusmokrassar.krontab.internal.parseMonths
import com.insanusmokrassar.krontab.internal.parseSeconds
/**
* Parse [incoming] string and adapt according to next format: "* * * * *" where order of things:
*
* seconds
* minutes
* hours
* dayOfMonth
* month
*
* And each one have next format:
*
* {number},{number},...
*
* and {number} here is one of {int}-{int} OR {int}/{int} OR *\/{int} OR {int}.
*
* Seconds ranges can be found in [com.insanusmokrassar.krontab.internal.secondsRange].
* Minutes ranges can be found in [com.insanusmokrassar.krontab.internal.minutesRange].
* Hours ranges can be found in [com.insanusmokrassar.krontab.internal.hoursRange].
* Days of month ranges can be found in [com.insanusmokrassar.krontab.internal.dayOfMonthRange].
* Months ranges can be found in [com.insanusmokrassar.krontab.internal.monthRange].
*
* @sample "0/5 * * * *" for every five seconds triggering
* @sample "0/15 30 * * *" for every 15th seconds in a half of each hour
* @sample "1 2 3 4 5" for triggering in near first second of second minute of third hour of fourth day of may
*/
fun createSimpleScheduler(incoming: String): KronScheduler {
val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ")
val secondsParsed = parseSeconds(secondsSource)
val minutesParsed = parseMinutes(minutesSource)
val hoursParsed = parseHours(hoursSource)
val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource)
val monthParsed = parseMonths(monthSource)
val resultCronDateTimes = mutableListOf(CronDateTime())
secondsParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(seconds = currentTime)
}
minutesParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(minutes = currentTime)
}
hoursParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(hours = currentTime)
}
dayOfMonthParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(dayOfMonth = currentTime)
}
monthParsed ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(month = currentTime)
}
return CronDateTimeScheduler(resultCronDateTimes.toList())
}

View File

@@ -1,104 +0,0 @@
package com.insanusmokrassar.krontab.builder
import com.insanusmokrassar.krontab.CronDateTimeScheduler
import com.insanusmokrassar.krontab.KronScheduler
import com.insanusmokrassar.krontab.internal.CronDateTime
import com.insanusmokrassar.krontab.internal.fillWith
fun buildSchedule(settingsBlock: SchedulerBuilder.() -> Unit): KronScheduler {
val builder = SchedulerBuilder()
builder.settingsBlock()
return builder.build()
}
class SchedulerBuilder(
private var seconds: Array<Byte>? = null,
private var minutes: Array<Byte>? = null,
private var hours: Array<Byte>? = null,
private var dayOfMonth: Array<Byte>? = null,
private var month: Array<Byte>? = null
) {
private fun <T : TimeBuilder> callAndReturn(
initial: Array<Byte>?,
builder: T,
block: T.() -> Unit
): Array<Byte>? {
builder.block()
val builderValue = builder.build()
return initial ?.let {
builderValue ?.let { _ ->
(it + builderValue).distinct().toTypedArray()
} ?: builderValue
} ?: builderValue
}
fun seconds(block: SecondsBuilder.() -> Unit) {
seconds = callAndReturn(
seconds,
SecondsBuilder(),
block
)
}
fun minutes(block: MinutesBuilder.() -> Unit) {
minutes = callAndReturn(
minutes,
MinutesBuilder(),
block
)
}
fun hours(block: HoursBuilder.() -> Unit) {
hours = callAndReturn(
hours,
HoursBuilder(),
block
)
}
fun dayOfMonth(block: DaysOfMonthBuilder.() -> Unit) {
dayOfMonth = callAndReturn(
dayOfMonth,
DaysOfMonthBuilder(),
block
)
}
fun months(block: MonthsBuilder.() -> Unit) {
month = callAndReturn(
month,
MonthsBuilder(),
block
)
}
fun build(): KronScheduler {
val resultCronDateTimes = mutableListOf(CronDateTime())
seconds ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(seconds = currentTime)
}
minutes ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(minutes = currentTime)
}
hours ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(hours = currentTime)
}
dayOfMonth ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(dayOfMonth = currentTime)
}
month ?.fillWith(resultCronDateTimes) { previousCronDateTime: CronDateTime, currentTime: Byte ->
previousCronDateTime.copy(month = currentTime)
}
return CronDateTimeScheduler(resultCronDateTimes.toList())
}
}

View File

@@ -1,53 +0,0 @@
package com.insanusmokrassar.krontab.builder
import com.insanusmokrassar.krontab.internal.*
import com.insanusmokrassar.krontab.utils.clamp
sealed class TimeBuilder (
private val restrictionsRange: IntRange
) {
private var result: Set<Int>? = null
fun allowAll() {
result = null
}
infix fun include(array: Array<Int>) {
val clamped = array.map { it.clamp(restrictionsRange) } + (result ?: emptySet())
result = clamped.toSet()
}
infix fun at(value: Int) {
result = (result ?: emptySet()) + value.clamp(restrictionsRange)
}
inline fun from(value: Int) = value
infix fun Int.every(delay: Int): Array<Int> {
val progression = clamp(restrictionsRange) .. restrictionsRange.last step delay
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
return result
}
infix fun every(delay: Int): Array<Int> = 0 every delay
infix fun Int.upTo(endIncluding: Int): Array<Int> {
val progression = clamp(restrictionsRange) .. endIncluding.clamp(restrictionsRange)
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
return result
}
infix fun upTo(endIncluding: Int): Array<Int> = 0 upTo endIncluding
internal fun build() = result ?.map { it.toByte() } ?.toTypedArray()
}
class SecondsBuilder : TimeBuilder(secondsRange)
class MinutesBuilder : TimeBuilder(minutesRange)
class HoursBuilder : TimeBuilder(com.insanusmokrassar.krontab.internal.hoursRange)
class DaysOfMonthBuilder : TimeBuilder(com.insanusmokrassar.krontab.internal.dayOfMonthRange)
class MonthsBuilder : TimeBuilder(monthRange)

View File

@@ -1,77 +0,0 @@
package com.insanusmokrassar.krontab.internal
import com.insanusmokrassar.krontab.utils.clamp
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeSpan
/**
* [month] 0-11
* [dayOfMonth] 0-31
* [hours] 0-23
* [minutes] 0-59
* [seconds] 0-59
*/
internal data class CronDateTime(
val month: Byte? = null,
val dayOfMonth: Byte? = null,
val hours: Byte? = null,
val minutes: Byte? = null,
val seconds: Byte? = null
) {
init {
check(month ?.let { it in com.insanusmokrassar.krontab.internal.monthRange } ?: true)
check(dayOfMonth ?.let { it in com.insanusmokrassar.krontab.internal.dayOfMonthRange } ?: true)
check(hours?.let { it in com.insanusmokrassar.krontab.internal.hoursRange } ?: true)
check(minutes?.let { it in com.insanusmokrassar.krontab.internal.minutesRange } ?: true)
check(seconds?.let { it in com.insanusmokrassar.krontab.internal.secondsRange } ?: true)
}
internal val klockDayOfMonth = dayOfMonth ?.plus(1)
companion object {
fun create(
month: Int? = null,
dayOfMonth: Int? = null,
hours: Int? = null,
minutes: Int? = null,
seconds: Int? = null
) = CronDateTime(
month ?.clamp(com.insanusmokrassar.krontab.internal.monthRange) ?.toByte(),
dayOfMonth ?.clamp(com.insanusmokrassar.krontab.internal.dayOfMonthRange) ?.toByte(),
hours ?.clamp(com.insanusmokrassar.krontab.internal.hoursRange) ?.toByte(),
minutes ?.clamp(com.insanusmokrassar.krontab.internal.minutesRange) ?.toByte(),
seconds ?.clamp(com.insanusmokrassar.krontab.internal.secondsRange) ?.toByte()
)
}
}
internal fun CronDateTime.toNearDateTime(relativelyTo: DateTime = DateTime.now()): DateTime {
var current = relativelyTo
seconds?.let {
val left = it - current.seconds
current += DateTimeSpan(minutes = if (left <= 0) 1 else 0, seconds = left)
}
minutes?.let {
val left = it - current.minutes
current += DateTimeSpan(hours = if (left < 0) 1 else 0, minutes = left)
}
hours?.let {
val left = it - current.hours
current += DateTimeSpan(days = if (left < 0) 1 else 0, hours = left)
}
klockDayOfMonth ?.let {
val left = it - current.dayOfMonth
current += DateTimeSpan(months = if (left < 0) 1 else 0, days = left)
}
month ?.let {
val left = it - current.month0
current += DateTimeSpan(months = if (left < 0) 1 else 0, days = left)
}
return current
}

View File

@@ -1,13 +0,0 @@
package com.insanusmokrassar.krontab
import com.insanusmokrassar.krontab.internal.*
import com.soywiz.klock.DateTime
internal data class CronDateTimeScheduler internal constructor(
internal val cronDateTimes: List<CronDateTime>
) : KronScheduler {
override suspend fun next(relatively: DateTime): DateTime {
return cronDateTimes.map { it.toNearDateTime(relatively) }.min() ?: anyCronDateTime.toNearDateTime(relatively)
}
}

View File

@@ -1,48 +0,0 @@
package com.insanusmokrassar.krontab.internal
import com.insanusmokrassar.krontab.utils.clamp
private fun createSimpleScheduler(from: String, dataRange: IntRange): Array<Byte>? {
val things = from.split(",")
val results = things.flatMap {
when {
it.contains("/") -> {
val (start, step) = it.split("/")
val startNum = (if (start.isEmpty() || start == "*") {
0
} else {
start.toInt()
}).clamp(dataRange)
val stepNum = step.toInt().clamp(dataRange)
(startNum .. dataRange.last step stepNum).map { it }
}
it == "*" -> return null
else -> listOf(it.toInt().clamp(dataRange))
}
}
return results.map { it.toByte() }.toTypedArray()
}
internal fun parseMonths(from: String) = createSimpleScheduler(from, monthRange)
internal fun parseDaysOfMonth(from: String) = createSimpleScheduler(from, dayOfMonthRange)
internal fun parseHours(from: String) = createSimpleScheduler(from, hoursRange)
internal fun parseMinutes(from: String) = createSimpleScheduler(from, minutesRange)
internal fun parseSeconds(from: String) = createSimpleScheduler(from, secondsRange)
internal fun Array<Byte>.fillWith(
whereToPut: MutableList<CronDateTime>,
createFactory: (CronDateTime, Byte) -> CronDateTime
) {
val previousValues = whereToPut.toList()
whereToPut.clear()
previousValues.forEach { previousValue ->
forEach {
whereToPut.add(createFactory(previousValue, it))
}
}
}

View File

@@ -1,4 +0,0 @@
package com.insanusmokrassar.krontab.utils
internal fun Int.clamp(min: Int, max: Int): Int = if (this < min) min else if (this > max) max else this
internal fun Int.clamp(range: IntRange): Int = clamp(range.first, range.last)

View File

@@ -1,25 +0,0 @@
package com.insanusmokrassar.krontab.utils
import com.insanusmokrassar.krontab.KronScheduler
import com.soywiz.klock.DateTime
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@FlowPreview
fun KronScheduler.asFlow(): Flow<DateTime> = SchedulerFlow(this)
@FlowPreview
class SchedulerFlow(
private val scheduler: KronScheduler
) : AbstractFlow<DateTime>() {
@FlowPreview
override suspend fun collectSafely(collector: FlowCollector<DateTime>) {
while (true) {
val now = DateTime.now()
val nextTime = scheduler.next(now)
val sleepDelay = (nextTime - now).millisecondsLong
delay(sleepDelay)
collector.emit(nextTime)
}
}
}

View File

@@ -0,0 +1,61 @@
package dev.inmo.krontab
import com.soywiz.klock.DateTime
import kotlinx.coroutines.delay
/**
* Execute [block] once at the [KronScheduler.next] time and return result of [block] calculation.
*
* WARNING!!! If you want to launch it in parallel, you must do this explicitly.
*
* WARNING!!! In case if [KronScheduler.next] of [this] instance will return null, [block] will be called immediately
*/
suspend inline fun <T> KronScheduler.doOnce(noinline block: suspend () -> T): T {
next() ?.let {
delay((it - DateTime.now()).millisecondsLong)
}
return block()
}
/**
* Will [buildSchedule] using [scheduleConfig] and call [doOnce] on it
* @see buildSchedule
*/
suspend inline fun <T> doOnce(
scheduleConfig: String,
noinline block: suspend () -> T
) = buildSchedule(scheduleConfig).doOnce(block)
/**
* Will execute [block] while it will return true as a result of its calculation
*/
suspend inline fun KronScheduler.doWhile(noinline block: suspend () -> Boolean) {
do { val doNext = doOnce(block) } while (doNext)
}
/**
* Will [buildSchedule] using [scheduleConfig] and call [doWhile] with [block]
*
* @see buildSchedule
*/
suspend inline fun doWhile(
scheduleConfig: String,
noinline block: suspend () -> Boolean
) = buildSchedule(scheduleConfig).doWhile(block)
/**
* Will execute [block] without any checking of result
*/
suspend inline fun KronScheduler.doInfinity(noinline block: suspend () -> Unit) = doWhile {
block()
true
}
/**
* Will [buildSchedule] using [scheduleConfig] and call [doInfinity] with [block]
*
* @see buildSchedule
*/
suspend inline fun doInfinity(
scheduleConfig: String,
noinline block: suspend () -> Unit
) = buildSchedule(scheduleConfig).doInfinity(block)

View File

@@ -0,0 +1,26 @@
package dev.inmo.krontab
import com.soywiz.klock.DateTime
/**
* This interface was created for abstraction of [next] operation. Currently, there is only
* [dev.inmo.krontab.internal.CronDateTimeScheduler] realisation of this interface inside of this library,
* but you it is possible to create your own realisation of this interface for scheduling, for example, depending of
* users activity or something like this
*
* @see dev.inmo.krontab.internal.CronDateTimeScheduler
*/
interface KronScheduler {
/**
* @return Next [DateTime] when some action must be triggered according to settings of this instance
*
* @see dev.inmo.krontab.internal.CronDateTimeScheduler.next
*/
suspend fun next(relatively: DateTime = DateTime.now()): DateTime?
}
suspend fun KronScheduler.nextOrRelative(relatively: DateTime = DateTime.now()): DateTime = next(relatively) ?: getAnyNext(relatively)
suspend fun KronScheduler.nextOrNow(): DateTime = DateTime.now().let {
next(it) ?: getAnyNext(it)
}

View File

@@ -0,0 +1,34 @@
package dev.inmo.krontab
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
/**
* This interface extending [KronScheduler] to use [DateTimeTz] with taking into account offset of incoming time for
* [next] operation.
*
* @see dev.inmo.krontab.internal.CronDateTimeScheduler
* @see dev.inmo.krontab.KronScheduler
*/
interface KronSchedulerTz : KronScheduler {
suspend fun next(relatively: DateTimeTz): DateTimeTz?
override suspend fun next(relatively: DateTime): DateTime? = next(relatively.localUnadjusted) ?.local
}
suspend fun KronSchedulerTz.nextOrRelative(relatively: DateTimeTz): DateTimeTz = next(relatively) ?: getAnyNext(
relatively.local
).toOffsetUnadjusted(relatively.offset)
suspend fun KronSchedulerTz.nextOrNowWithOffset(): DateTimeTz = DateTimeTz.nowLocal().let {
next(it) ?: getAnyNext(
it.local
).toOffsetUnadjusted(it.offset)
}
suspend fun KronScheduler.next(relatively: DateTimeTz) = if (this is KronSchedulerTz) {
this.next(relatively)
} else {
this.next(relatively.local) ?.toOffsetUnadjusted(relatively.offset)
}
suspend fun KronScheduler.nextTimeZoned() = next(DateTime.now().local)

View File

@@ -0,0 +1,46 @@
package dev.inmo.krontab
import dev.inmo.krontab.collection.CollectionKronScheduler
import dev.inmo.krontab.collection.includeAll
import dev.inmo.krontab.internal.*
/**
* Create new one [CollectionKronScheduler] to include all [KronScheduler]s of [this] [Iterator]
*
* @see CollectionKronScheduler
* @see CollectionKronScheduler.include
*/
fun Iterator<KronScheduler>.merge(): CollectionKronScheduler {
val cronDateTimes = mutableListOf<CronDateTime>()
val timezonedCronDateTimes = mutableListOf<CronDateTimeSchedulerTz>()
val collectionScheduler = CollectionKronScheduler()
forEach {
when (it) {
is CronDateTimeScheduler -> cronDateTimes.add(it.cronDateTime)
is CronDateTimeSchedulerTz -> timezonedCronDateTimes.add(it)
else -> collectionScheduler.include(it)
}
}
if (cronDateTimes.isNotEmpty()) {
collectionScheduler.include(CronDateTimeScheduler(cronDateTimes.merge()))
}
if (timezonedCronDateTimes.isNotEmpty()) {
collectionScheduler.includeAll(mergeCronDateTimeSchedulers(timezonedCronDateTimes))
}
return collectionScheduler
}
/**
* Create new one [CollectionKronScheduler] to include all [KronScheduler]s of [this] [Iterator]
*
* @see CollectionKronScheduler
* @see CollectionKronScheduler.include
*/
@Suppress("NOTHING_TO_INLINE")
inline fun Iterable<KronScheduler>.merge(): CollectionKronScheduler = iterator().merge()
/**
* @return Vararg shortcut for [merge]
*/
@Suppress("NOTHING_TO_INLINE")
inline fun merge(vararg kronDateTimeSchedulers: KronScheduler): CollectionKronScheduler = kronDateTimeSchedulers.iterator().merge()

View File

@@ -0,0 +1,18 @@
package dev.inmo.krontab
/**
* This class contains [template] and can be simply serialized/deserialized. In fact that class will work as
* [dev.inmo.krontab.internal.CronDateTimeScheduler] due to the fact that [toKronScheduler] will return it under the
* hood
*/
data class KrontabTemplateWrapper(
val template: KrontabTemplate
) : KronScheduler by template.toKronScheduler()
/**
* Will create [KrontabTemplateWrapper] from [this] [KrontabTemplate]
*
* @see [toKronScheduler]
* @see [KrontabTemplateWrapper]
*/
fun KrontabTemplate.wrapAsKronScheduler() = KrontabTemplateWrapper(this)

View File

@@ -0,0 +1,59 @@
package dev.inmo.krontab
import com.soywiz.klock.DateTime
import dev.inmo.krontab.builder.buildSchedule
import dev.inmo.krontab.internal.*
internal val anyCronDateTime by lazy {
CronDateTime()
}
internal fun getAnyNext(relatively: DateTime) = anyCronDateTime.toNearDateTime(relatively)!!
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now]
*/
val AnyTimeScheduler: KronScheduler by lazy {
CronDateTimeScheduler(anyCronDateTime)
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one second
*/
val EverySecondScheduler: KronScheduler by lazy {
buildSchedule { seconds { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one minute
*/
val EveryMinuteScheduler: KronScheduler by lazy {
buildSchedule { minutes { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one hour
*/
val EveryHourScheduler: KronScheduler by lazy {
buildSchedule { hours { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one day
*/
val EveryDayOfMonthScheduler: KronScheduler by lazy {
buildSchedule { dayOfMonth { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one month
*/
val EveryMonthScheduler: KronScheduler by lazy {
buildSchedule { months { 0 every 1 } }
}
/**
* [KronScheduler.next] will always return [com.soywiz.klock.DateTime.now] + one year
*/
val EveryYearScheduler: KronScheduler by lazy {
buildSchedule { years { 0 every 1 } }
}

View File

@@ -0,0 +1,150 @@
package dev.inmo.krontab
import com.soywiz.klock.TimezoneOffset
import com.soywiz.klock.minutes
import dev.inmo.krontab.internal.*
import dev.inmo.krontab.utils.Minutes
/**
* @see createSimpleScheduler
* @see buildSchedule
*/
typealias KrontabTemplate = String
/**
* Parse [incoming] string and adapt according to next format: "* * * * *" where order of things:
*
* * seconds
* * minutes
* * hours
* * dayOfMonth
* * month
* * (optional) year
* * (optional) (can be placed anywhere after month) (must be marked with `o` at the end, for example: 60o == +01:00) offset
* * (optional) (can be placed anywhere after month) dayOfWeek
*
* And each one (except of offsets) have next format:
*
* `{number}[,{number},...]` or `*`
*
* and {number} here is one of
*
* * {int}-{int}
* * {int}/{int}
* * *&#47;{int}
* * {int}
* * F
* * L
*
* Week days must be marked with `w` at the end, and starts with 0 which means Sunday. For example, 0w == Sunday. With
* weeks you can use syntax like with any number like seconds, for example: 0-2w means Sunday-Tuesday
*
* Additional info about ranges can be found in follow accordance:
*
* * Seconds ranges can be found in [secondsRange]
* * Minutes ranges can be found in [minutesRange]
* * Hours ranges can be found in [hoursRange]
* * Days of month ranges can be found in [dayOfMonthRange]
* * Months ranges can be found in [monthRange]
* * Years ranges can be found in [yearRange] (in fact - any [Int])
* * WeekDay (timezone) ranges can be found in [dayOfWeekRange]
*
* Examples:
*
* * "0/5 * * * *" for every five seconds triggering
* * "0/5,L * * * *" for every five seconds triggering and on 59 second
* * "0/15 30 * * *" for every 15th seconds in a half of each hour
* * "1 2 3 F,4,L 5" for triggering in near first second of second minute of third hour of fourth day of may
* * "1 2 3 F,4,L 5 60o" for triggering in near first second of second minute of third hour of fourth day of may with timezone UTC+01:00
* * "1 2 3 F,4,L 5 60o 0-2w" for triggering in near first second of second minute of third hour of fourth day of may in case if it will be in Sunday-Tuesday week days with timezone UTC+01:00
* * "1 2 3 F,4,L 5 2021" for triggering in near first second of second minute of third hour of fourth day of may of 2021st year
* * "1 2 3 F,4,L 5 2021 60o" for triggering in near first second of second minute of third hour of fourth day of may of 2021st year with timezone UTC+01:00
* * "1 2 3 F,4,L 5 2021 60o 0-2w" for triggering in near first second of second minute of third hour of fourth day of may of 2021st year if it will be in Sunday-Tuesday week days with timezone UTC+01:00
*
* @return In case when offset parameter is absent in [incoming] will be used [createSimpleScheduler] method and
* returned [CronDateTimeScheduler]. In case when offset parameter there is in [incoming] [KrontabTemplate] will be used
* [createKronSchedulerWithOffset] and returned [CronDateTimeSchedulerTz]
*
* @see dev.inmo.krontab.internal.createKronScheduler
*/
fun createSimpleScheduler(
incoming: KrontabTemplate
): KronScheduler {
var offsetParsed: Int? = null
var dayOfWeekParsed: Array<Byte>? = null
var yearParsed: Array<Int>? = null
val (secondsSource, minutesSource, hoursSource, dayOfMonthSource, monthSource) = incoming.split(" ").also {
listOfNotNull(
it.getOrNull(5),
it.getOrNull(6),
it.getOrNull(7)
).forEach {
val offsetFromString = parseOffset(it)
val dayOfWeekFromString = parseWeekDay(it)
offsetParsed = offsetParsed ?: offsetFromString
dayOfWeekParsed = dayOfWeekParsed ?: dayOfWeekFromString
when {
dayOfWeekFromString != null || offsetFromString != null -> return@forEach
yearParsed == null -> {
yearParsed = parseYears(it)
}
}
}
}
val secondsParsed = parseSeconds(secondsSource)
val minutesParsed = parseMinutes(minutesSource)
val hoursParsed = parseHours(hoursSource)
val dayOfMonthParsed = parseDaysOfMonth(dayOfMonthSource)
val monthParsed = parseMonths(monthSource)
return offsetParsed ?.let { offset ->
createKronSchedulerWithOffset(
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed, dayOfWeekParsed, TimezoneOffset(offset.minutes)
)
} ?: createKronScheduler(
secondsParsed, minutesParsed, hoursParsed, dayOfMonthParsed, monthParsed, yearParsed, dayOfWeekParsed
)
}
fun createSimpleScheduler(
incoming: KrontabTemplate,
defaultOffset: Minutes
): KronSchedulerTz {
val scheduler = createSimpleScheduler(incoming)
return if (scheduler is KronSchedulerTz) {
scheduler
} else {
CronDateTimeSchedulerTz(
(scheduler as CronDateTimeScheduler).cronDateTime,
TimezoneOffset(defaultOffset.minutes)
)
}
}
/**
* Shortcut for [createSimpleScheduler]
*/
fun buildSchedule(incoming: KrontabTemplate): KronScheduler = createSimpleScheduler(incoming)
/**
* Shortcut for [createSimpleScheduler]
*/
fun buildSchedule(incoming: KrontabTemplate, defaultOffset: Minutes): KronSchedulerTz = createSimpleScheduler(incoming, defaultOffset)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toSchedule(): KronScheduler = buildSchedule(this)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toSchedule(defaultOffset: Minutes): KronSchedulerTz = buildSchedule(this, defaultOffset)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toKronScheduler(): KronScheduler = buildSchedule(this)
/**
* Shortcut for [buildSchedule]
*/
fun KrontabTemplate.toKronScheduler(defaultOffset: Minutes): KronSchedulerTz = buildSchedule(this, defaultOffset)

View File

@@ -0,0 +1,152 @@
package dev.inmo.krontab.builder
import com.soywiz.klock.TimezoneOffset
import com.soywiz.klock.minutes
import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.KronSchedulerTz
import dev.inmo.krontab.internal.createKronScheduler
import dev.inmo.krontab.internal.createKronSchedulerWithOffset
import dev.inmo.krontab.utils.Minutes
/**
* Will help to create an instance of [KronScheduler]
*
* @see dev.inmo.krontab.createSimpleScheduler
*/
fun buildSchedule(settingsBlock: SchedulerBuilder.() -> Unit): KronScheduler {
val builder = SchedulerBuilder()
builder.settingsBlock()
return builder.build()
}
/**
* Will help to create an instance of [KronScheduler]
*
* @see dev.inmo.krontab.createSimpleScheduler
*/
fun buildSchedule(
offset: Minutes,
settingsBlock: SchedulerBuilder.() -> Unit
): KronSchedulerTz {
val builder = SchedulerBuilder(offset = offset)
builder.settingsBlock()
return builder.build() as KronSchedulerTz
}
class SchedulerBuilder(
private var seconds: Array<Byte>? = null,
private var minutes: Array<Byte>? = null,
private var hours: Array<Byte>? = null,
private var dayOfMonth: Array<Byte>? = null,
private var month: Array<Byte>? = null,
private var year: Array<Int>? = null,
private var dayOfWeek: Array<Byte>? = null,
private val offset: Minutes? = null
) {
private fun <I, T : TimeBuilder<I>> callAndReturn(
initial: Array<I>?,
builder: T,
block: T.() -> Unit
): List<I>? {
builder.block()
val builderValue = builder.build()
return initial ?.let {
builderValue ?.let { _ ->
(it + builderValue).distinct()
} ?: builderValue
} ?: builderValue
}
/**
* Starts an seconds block
*/
fun seconds(block: SecondsBuilder.() -> Unit) {
seconds = callAndReturn(
seconds,
SecondsBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an minutes block
*/
fun minutes(block: MinutesBuilder.() -> Unit) {
minutes = callAndReturn(
minutes,
MinutesBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an hours block
*/
fun hours(block: HoursBuilder.() -> Unit) {
hours = callAndReturn(
hours,
HoursBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an days of month block
*/
fun dayOfMonth(block: DaysOfMonthBuilder.() -> Unit) {
dayOfMonth = callAndReturn(
dayOfMonth,
DaysOfMonthBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an hours block
*/
fun dayOfWeek(block: WeekDaysBuilder.() -> Unit) {
dayOfWeek = callAndReturn(
dayOfWeek,
WeekDaysBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an months block
*/
fun months(block: MonthsBuilder.() -> Unit) {
month = callAndReturn(
month,
MonthsBuilder(),
block
) ?.toTypedArray()
}
/**
* Starts an year block
*/
fun years(block: YearsBuilder.() -> Unit) {
year = callAndReturn(
year,
YearsBuilder(),
block
) ?.toTypedArray()
}
/**
* @return Completely built and independent [KronScheduler]
*
* @see dev.inmo.krontab.createSimpleScheduler
* @see dev.inmo.krontab.internal.createKronScheduler
*/
fun build(): KronScheduler = offset ?.let {
createKronSchedulerWithOffset(seconds, minutes, hours, dayOfMonth, month, year, dayOfWeek, TimezoneOffset(it.minutes))
} ?: createKronScheduler(seconds, minutes, hours, dayOfMonth, month, year, dayOfWeek)
}

View File

@@ -0,0 +1,131 @@
package dev.inmo.krontab.builder
import dev.inmo.krontab.internal.*
/**
* This class was created for incapsulation of builder work with specified [restrictionsRange]. For example,
* [include] function of [TimeBuilder] will always [coerceIn] incoming data using its [restrictionsRange]
*/
sealed class TimeBuilder<T : Number> (
private val restrictionsRange: IntRange,
private val converter: Converter<T>
) {
private var result: Set<Int>? = null
/**
* The first possible value of builder
*/
val first
get() = restrictionsRange.first
/**
* The last possible value of builder. Using of this variable equal to using "L" in strings
*/
val last
get() = restrictionsRange.last
/**
* After calling of this function this builder will allow any value of current time
*/
@Suppress("unused")
fun allowAll() {
result = null
}
/**
* Will include all variations from this array inside of this timeline
*/
@Suppress("MemberVisibilityCanBePrivate")
infix fun include(array: Array<Int>) {
val clamped = array.map { it.coerceIn(restrictionsRange) } + (result ?: emptySet())
result = clamped.toSet()
}
/**
* Add one [value] to current timeline
*/
@Suppress("unused")
infix fun at(value: Int) {
result = (result ?: emptySet()) + value.coerceIn(restrictionsRange)
}
/**
* Shortcut for [at]. In fact will
*/
@Suppress("unused", "NOTHING_TO_INLINE")
inline infix fun each(value: Int) = at(value)
/**
* Just wrapper for more obvious writing something like "[from] 2 [every] 5". For example, for [SecondsBuilder] it
* will mean "[from] second second [every] 5 seconds", or "2, 7, 13, ..."
*/
@Suppress("NOTHING_TO_INLINE")
inline infix fun from(value: Int) = value
/**
* Will create an sequence of times starting [from] [this] [every] [delay] times. For example, for [SecondsBuilder] it
* will mean "[from] second second [every] 5 seconds", or "2, 7, 13, ..."
*
* @see [from]
*/
infix fun Int.every(delay: Int): Array<Int> {
val progression = coerceIn(restrictionsRange) .. restrictionsRange.last step delay
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
return result
}
/**
* Shortcut for "[from] 0 [every] [delay]"
*/
infix fun every(delay: Int): Array<Int> = this from 0 every delay
/**
* Will fill up this timeline from [this] up to [endIncluding]
*/
@Suppress("MemberVisibilityCanBePrivate")
infix fun Int.upTo(endIncluding: Int): Array<Int> {
val progression = coerceIn(restrictionsRange) .. endIncluding.coerceIn(restrictionsRange)
val result = progression.toSet().toTypedArray()
this@TimeBuilder include result
return result
}
/**
* Shortcut for "[from] 0 [upTo] [endIncluding]"
*/
@Suppress("unused")
infix fun upTo(endIncluding: Int): Array<Int> = this from 0 upTo endIncluding
/**
* Will fill up this timeline from [this] up to [endIncluding]
*/
@Suppress("MemberVisibilityCanBePrivate")
infix operator fun Int.rangeTo(endIncluding: Int) = upTo(endIncluding)
/**
* Shortcut for "[from] 0 [rangeTo] [endIncluding]"
*/
@Suppress("MemberVisibilityCanBePrivate")
infix operator fun rangeTo(endIncluding: Int) = (this from 0) rangeTo endIncluding
/**
* Will include the last possible value
*/
fun includeLast() = at(restrictionsRange.last)
/**
* Will include the first possible value
*/
fun includeFirst() = at(restrictionsRange.first)
internal fun build() = result ?.map(converter)
}
class SecondsBuilder : TimeBuilder<Byte>(secondsRange, intToByteConverter)
class MinutesBuilder : TimeBuilder<Byte>(minutesRange, intToByteConverter)
class HoursBuilder : TimeBuilder<Byte>(hoursRange, intToByteConverter)
class DaysOfMonthBuilder : TimeBuilder<Byte>(dayOfMonthRange, intToByteConverter)
class MonthsBuilder : TimeBuilder<Byte>(monthRange, intToByteConverter)
class YearsBuilder : TimeBuilder<Int>(yearRange, intToIntConverter)
class WeekDaysBuilder : TimeBuilder<Byte>(dayOfWeekRange, intToByteConverter)

View File

@@ -0,0 +1,25 @@
package dev.inmo.krontab.collection
import dev.inmo.krontab.KronScheduler
@Suppress("NOTHING_TO_INLINE")
inline fun CollectionKronScheduler.includeAll(kronSchedulers: List<KronScheduler>) {
kronSchedulers.forEach {
include(it)
}
}
@Suppress("NOTHING_TO_INLINE")
inline fun CollectionKronScheduler.includeAll(vararg kronSchedulers: KronScheduler) {
includeAll(kronSchedulers.toList())
}
operator fun KronScheduler.plus(kronScheduler: KronScheduler): CollectionKronScheduler {
return CollectionKronScheduler().apply {
includeAll(this, kronScheduler)
}
}
operator fun CollectionKronScheduler.plusAssign(kronScheduler: KronScheduler) {
include(kronScheduler)
}

View File

@@ -0,0 +1,66 @@
package dev.inmo.krontab.collection
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.*
import dev.inmo.krontab.internal.*
/**
* This scheduler will be useful in case you want to unite several different [KronScheduler]s
*/
data class CollectionKronScheduler internal constructor(
internal val schedulers: MutableList<KronScheduler>
) : KronSchedulerTz {
internal constructor() : this(mutableListOf())
/**
* Add [kronScheduler] into its [schedulers] list
*
* * When [kronScheduler] is [CronDateTimeScheduler] it will merge all [CronDateTimeScheduler]s from [schedulers] list
* and this [kronScheduler] using [mergeCronDateTimeSchedulers] function
* * When [kronScheduler] is [CollectionKronScheduler] it this instance will include all [kronScheduler]
* [schedulers]
* * Otherwise [kronScheduler] will be added to [schedulers] list
*/
fun include(kronScheduler: KronScheduler) {
when (kronScheduler) {
is CronDateTimeScheduler -> {
val resultCronDateTimes = mutableListOf(kronScheduler)
schedulers.removeAll {
if (it is CronDateTimeScheduler) {
resultCronDateTimes.add(it)
} else {
false
}
}
schedulers.add(
mergeCronDateTimeSchedulers(resultCronDateTimes)
)
}
is CronDateTimeSchedulerTz -> {
val newCronDateTimes = mutableListOf(kronScheduler.cronDateTime)
schedulers.removeAll {
if (it is CronDateTimeSchedulerTz && it.offset == kronScheduler.offset) {
newCronDateTimes.add(it.cronDateTime)
true
} else {
false
}
}
schedulers.add(CronDateTimeSchedulerTz(newCronDateTimes.merge(), kronScheduler.offset))
}
is CollectionKronScheduler -> kronScheduler.schedulers.forEach {
include(it)
}
else -> schedulers.add(kronScheduler)
}
}
override suspend fun next(relatively: DateTime): DateTime {
return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively)
}
override suspend fun next(relatively: DateTimeTz): DateTimeTz {
return schedulers.mapNotNull { it.next(relatively) }.minOrNull() ?: getAnyNext(relatively.local).toOffsetUnadjusted(relatively.offset)
}
}

View File

@@ -0,0 +1,107 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.*
import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.utils.copy
/**
* @param daysOfWeek 0-6
* @param years any int
* @param months 0-11
* @param daysOfMonth 0-31
* @param hours 0-23
* @param minutes 0-59
* @param seconds 0-59
*/
internal data class CronDateTime(
val daysOfWeek: Array<Byte>? = null,
val years: Array<Int>? = null,
val months: Array<Byte>? = null,
val daysOfMonth: Array<Byte>? = null,
val hours: Array<Byte>? = null,
val minutes: Array<Byte>? = null,
val seconds: Array<Byte>? = null
) {
init {
check(daysOfWeek ?.all { it in dayOfWeekRange } ?: true)
check(years?.all { it in yearRange } ?: true)
check(months?.all { it in monthRange } ?: true)
check(daysOfMonth ?.all { it in dayOfMonthRange } ?: true)
check(hours?.all { it in hoursRange } ?: true)
check(minutes?.all { it in minutesRange } ?: true)
check(seconds?.all { it in secondsRange } ?: true)
}
internal val calculators = listOf(
years ?.let { NearDateTimeCalculatorYears(it) },
daysOfWeek ?.let { NearDateTimeCalculatorWeekDays(it) },
NearDateTimeCalculatorMillis(arrayOf(0)),
seconds ?.let { NearDateTimeCalculatorSeconds(it) },
minutes ?.let { NearDateTimeCalculatorMinutes(it) },
hours ?.let { NearDateTimeCalculatorHours(it) },
daysOfMonth ?.let { NearDateTimeCalculatorDays(it) },
months ?.let { NearDateTimeCalculatorMonths(it) },
)
internal fun toNearDateTime(relativelyTo: DateTime = DateTime.now()): DateTime? {
var current = relativelyTo
whileLoop@while (true) {
for (calculator in calculators) {
val (calculated, requireRecalculation) = (calculator ?: continue).calculateNearTime(current) ?: return null
current = calculated
if (requireRecalculation) {
continue@whileLoop
}
}
return current
}
}
}
internal fun createCronDateTime(
seconds: Array<Byte>? = null,
minutes: Array<Byte>? = null,
hours: Array<Byte>? = null,
dayOfMonth: Array<Byte>? = null,
month: Array<Byte>? = null,
years: Array<Int>? = null,
weekDays: Array<Byte>? = null
): CronDateTime {
return CronDateTime(weekDays, years, month, dayOfMonth, hours, minutes, seconds)
}
/**
* @return [KronScheduler] (in fact [CronDateTimeScheduler]) based on incoming data
*/
internal fun createKronScheduler(
seconds: Array<Byte>? = null,
minutes: Array<Byte>? = null,
hours: Array<Byte>? = null,
dayOfMonth: Array<Byte>? = null,
month: Array<Byte>? = null,
years: Array<Int>? = null,
weekDays: Array<Byte>? = null
): KronScheduler = CronDateTimeScheduler(createCronDateTime(seconds, minutes, hours, dayOfMonth, month, years, weekDays))
/**
* @return [KronScheduler] (in fact [CronDateTimeScheduler]) based on incoming data
*/
internal fun createKronSchedulerWithOffset(
seconds: Array<Byte>? = null,
minutes: Array<Byte>? = null,
hours: Array<Byte>? = null,
dayOfMonth: Array<Byte>? = null,
month: Array<Byte>? = null,
years: Array<Int>? = null,
weekDays: Array<Byte>? = null,
offset: TimezoneOffset
): KronScheduler = CronDateTimeSchedulerTz(createCronDateTime(seconds, minutes, hours, dayOfMonth, month, years, weekDays), offset)
internal fun List<CronDateTime>.merge() = CronDateTime(
flatMap { it.daysOfWeek ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.years ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.months ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.daysOfMonth ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.hours ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.minutes ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
flatMap { it.seconds ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
)

View File

@@ -0,0 +1,58 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.DateTime
import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.collection.plus
/**
* Cron-oriented realisation of [KronScheduler]
*
* @see dev.inmo.krontab.AnyTimeScheduler
* @see dev.inmo.krontab.EverySecondScheduler
* @see dev.inmo.krontab.EveryMinuteScheduler
* @see dev.inmo.krontab.EveryHourScheduler
* @see dev.inmo.krontab.EveryDayOfMonthScheduler
* @see dev.inmo.krontab.EveryMonthScheduler
* @see dev.inmo.krontab.EveryYearScheduler
*
* @see dev.inmo.krontab.builder.buildSchedule
* @see dev.inmo.krontab.builder.SchedulerBuilder
*/
internal data class CronDateTimeScheduler internal constructor(
internal val cronDateTime: CronDateTime
) : KronScheduler {
/**
* @return Near date using [cronDateTimes] list and getting the [Iterable.minByOrNull] one
*
* @see toNearDateTime
*/
override suspend fun next(relatively: DateTime): DateTime? {
return cronDateTime.toNearDateTime(relatively)
}
}
internal fun mergeCronDateTimeSchedulers(
schedulers: List<CronDateTimeScheduler>
): CronDateTimeScheduler = CronDateTimeScheduler(
schedulers.map { it.cronDateTime }.merge()
)
/**
* @return New instance of [CronDateTimeScheduler] with all unique [CronDateTimeScheduler.cronDateTimes] of
* [kronSchedulers] included
*/
@Deprecated("Will be removed in next major release", ReplaceWith("merge", "dev.inmo.krontab"))
fun merge(kronSchedulers: List<KronScheduler>) = kronSchedulers.apply { dev.inmo.krontab.merge() }
/**
* @return Vararg shortcut for [dev.inmo.krontab.merge]
*/
@Suppress("NOTHING_TO_INLINE")
@Deprecated("Will be removed in next major release", ReplaceWith("merge", "dev.inmo.krontab"))
inline fun merge(vararg kronDateTimeSchedulers: KronScheduler) = kronDateTimeSchedulers.apply { dev.inmo.krontab.merge() }
/**
* @return Vararg shortcut for [dev.inmo.krontab.merge]
*/
@Suppress("NOTHING_TO_INLINE")
@Deprecated("Will be removed in next major release", ReplaceWith("merge", "dev.inmo.krontab"))
inline fun KronScheduler.plus(other: KronScheduler) = this + other

View File

@@ -0,0 +1,32 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.DateTimeTz
import com.soywiz.klock.TimezoneOffset
import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.KronSchedulerTz
/**
* Cron-oriented realisation of [KronScheduler] with taking into account [offset] for list of [cronDateTimes]
*
* @see CronDateTime
*/
internal data class CronDateTimeSchedulerTz internal constructor(
internal val cronDateTime: CronDateTime,
internal val offset: TimezoneOffset
) : KronSchedulerTz {
override suspend fun next(relatively: DateTimeTz): DateTimeTz? {
val dateTimeWithActualOffset = relatively.toOffset(offset).local
return cronDateTime.toNearDateTime(dateTimeWithActualOffset) ?.toOffsetUnadjusted(offset) ?.toOffset(relatively.offset)
}
}
internal fun mergeCronDateTimeSchedulers(
schedulers: List<CronDateTimeSchedulerTz>
) = schedulers.groupBy {
it.offset
}.map { (offset, schedulers) ->
CronDateTimeSchedulerTz(
schedulers.map { it.cronDateTime }.merge(),
offset
)
}

View File

@@ -0,0 +1,183 @@
package dev.inmo.krontab.internal
import com.soywiz.klock.*
import dev.inmo.krontab.utils.copy
import kotlin.math.min
fun interface NearDateTimeCalculator {
/**
* @return pair of near [DateTime] for this checker and [Boolean] flag that all previous calculations must be
* recalculated
*/
fun calculateNearTime(
relativelyTo: DateTime
): Pair<DateTime, Boolean>?
}
internal class CommonNearDateTimeCalculator<T>(
private val times: Array<T>,
private val partGetter: (DateTime) -> T,
private val partSetter: (DateTime, T) -> DateTime?
) : NearDateTimeCalculator where T : Comparable<T>, T : Number {
/**
* @return pair of near [DateTime] for this checker and [Boolean] flag that all previous calculations must be
* recalculated
*/
override fun calculateNearTime(
relativelyTo: DateTime
): Pair<DateTime, Boolean>? {
val currentData = partGetter(relativelyTo)
val greaterOrEquals = times.firstOrNull { it >= currentData }
val newDateTime = when (greaterOrEquals) {
null -> partSetter(relativelyTo, times.first()) ?: return null
currentData -> relativelyTo
else -> partSetter(relativelyTo, greaterOrEquals) ?: return null
}
return if (newDateTime == relativelyTo) {
relativelyTo to false
} else {
newDateTime to true
}
}
}
internal fun NearDateTimeCalculatorMillis(
times: Array<Short>
) = CommonNearDateTimeCalculator(
times,
{ it.milliseconds.toShort() },
{ dateTime, newOne ->
(if (newOne < dateTime.milliseconds) {
dateTime.plus(1.seconds)
} else {
dateTime
}).copy(milliseconds = newOne.toInt())
}
)
internal fun NearDateTimeCalculatorSeconds(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.seconds.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.seconds) {
dateTime.plus(1.minutes)
} else {
dateTime
}).copy(second = newOne.toInt(), milliseconds = 0)
}
)
internal fun NearDateTimeCalculatorMinutes(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.minutes.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.minutes) {
dateTime.plus(1.hours)
} else {
dateTime
}).copy(minute = newOne.toInt(), second = 0, milliseconds = 0)
}
)
internal fun NearDateTimeCalculatorHours(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.hours.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.hours) {
dateTime.plus(1.days)
} else {
dateTime
}).copy(hour = newOne.toInt(), minute = 0, second = 0, milliseconds = 0)
}
)
internal fun NearDateTimeCalculatorDays(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.dayOfMonth.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.dayOfMonth) {
dateTime.plus(1.months)
} else {
dateTime
}).copy(
dayOfMonth = min(dateTime.month.days(dateTime.year), newOne.toInt() + 1), // index1
hour = 0,
minute = 0,
second = 0,
milliseconds = 0
)
}
)
internal fun NearDateTimeCalculatorMonths(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.dayOfMonth.toByte() },
{ dateTime, newOne ->
(if (newOne < dateTime.month0) {
dateTime.plus(1.years)
} else {
dateTime
}).copy(
month = newOne.toInt() + 1, // index1
dayOfMonth = 1, // index1
hour = 0,
minute = 0,
second = 0,
milliseconds = 0
)
}
)
internal fun NearDateTimeCalculatorWeekDays(
times: Array<Byte>
) = CommonNearDateTimeCalculator(
times,
{ it.dayOfWeek.index0.toByte() },
{ dateTime, newOne ->
val currentDayOfWeek = dateTime.dayOfWeek.index0
if (newOne.toInt() == currentDayOfWeek) return@CommonNearDateTimeCalculator dateTime
(if (newOne < currentDayOfWeek) {
dateTime.plus(7.days - (currentDayOfWeek - newOne).days)
} else {
dateTime.plus(newOne.toInt().days - currentDayOfWeek.days)
}).copy(
hour = 0,
minute = 0,
second = 0,
milliseconds = 0
)
}
)
internal fun NearDateTimeCalculatorYears(
times: Array<Int>
) = CommonNearDateTimeCalculator(
times,
{ it.yearInt },
{ dateTime, newOne ->
val currentYear = dateTime.yearInt
if (newOne == currentYear) return@CommonNearDateTimeCalculator dateTime
(if (newOne < currentYear) {
null
} else {
dateTime.plus(newOne.years - currentYear.years)
}) ?.copy(
month = 1, // index1
dayOfMonth = 1, // index1
hour = 0,
minute = 0,
second = 0,
milliseconds = 0
)
}
)

View File

@@ -0,0 +1,75 @@
package dev.inmo.krontab.internal
typealias Converter<T> = (Int) -> T
internal val intToByteConverter: Converter<Byte> = { it: Int -> it.toByte() }
internal val intToIntConverter: Converter<Int> = { it: Int -> it }
private fun <T> createSimpleScheduler(from: String, dataRange: IntRange, dataConverter: Converter<T>): List<T>? {
val things = from.split(",")
val results = things.flatMap {
val currentToken = it.lowercase().replace(
"f", dataRange.first.toString()
).replace(
"l", dataRange.last.toString()
)
when {
currentToken.contains("-") -> {
val splitted = currentToken.split("-")
(splitted.first().toInt().coerceIn(dataRange) .. splitted[1].toInt().coerceIn(dataRange)).toList()
}
currentToken.contains("/") -> {
val (start, step) = currentToken.split("/")
val startNum = (if (start.isEmpty() || start == "*") {
0
} else {
start.toInt()
}).coerceIn(dataRange)
val stepNum = step.toInt().coerceIn(dataRange)
(startNum .. dataRange.last step stepNum).map { it }
}
currentToken == "*" -> return null
else -> listOf(currentToken.toInt().coerceIn(dataRange))
}
}
return results.map(dataConverter)
}
internal fun parseWeekDay(from: String?) = from ?.let { if (it.endsWith("w")) createSimpleScheduler(it.removeSuffix("w"), dayOfWeekRange, intToByteConverter) ?.toTypedArray() else null }
internal fun parseOffset(from: String?) = from ?.let { if (it.endsWith("o")) it.removeSuffix("o").toIntOrNull() else null }
internal fun parseYears(from: String?) = from ?.let { createSimpleScheduler(from, yearRange, intToIntConverter) ?.toTypedArray() }
internal fun parseMonths(from: String) = createSimpleScheduler(from, monthRange, intToByteConverter) ?.toTypedArray()
internal fun parseDaysOfMonth(from: String) = createSimpleScheduler(from, dayOfMonthRange, intToByteConverter) ?.toTypedArray()
internal fun parseHours(from: String) = createSimpleScheduler(from, hoursRange, intToByteConverter) ?.toTypedArray()
internal fun parseMinutes(from: String) = createSimpleScheduler(from, minutesRange, intToByteConverter) ?.toTypedArray()
internal fun parseSeconds(from: String) = createSimpleScheduler(from, secondsRange, intToByteConverter) ?.toTypedArray()
internal fun <T> Array<T>.fillWith(
whereToPut: MutableList<CronDateTime>,
createFactory: (CronDateTime, T) -> CronDateTime
) {
val previousValues = whereToPut.toList()
whereToPut.clear()
previousValues.forEach { previousValue ->
forEach {
whereToPut.add(createFactory(previousValue, it))
}
}
}
internal fun <T> T.fillWith(
whereToPut: MutableList<CronDateTime>,
createFactory: (CronDateTime, T) -> CronDateTime
) {
val previousValues = whereToPut.toList()
whereToPut.clear()
previousValues.forEach { previousValue ->
whereToPut.add(createFactory(previousValue, this))
}
}

View File

@@ -1,5 +1,7 @@
package com.insanusmokrassar.krontab.internal
package dev.inmo.krontab.internal
internal val dayOfWeekRange = 0 .. 6
internal val yearRange = Int.MIN_VALUE .. Int.MAX_VALUE
internal val monthRange = 0 .. 11
internal val dayOfMonthRange = 0 .. 30
internal val hoursRange = 0 .. 23

View File

@@ -0,0 +1,22 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.*
import kotlin.math.min
fun DateTime.copy(
year: Int = yearInt,
month: Int = month1,
dayOfMonth: Int = this.dayOfMonth,
hour: Int = hours,
minute: Int = minutes,
second: Int = seconds,
milliseconds: Int = this.milliseconds
) = DateTime(
year,
month,
min(Month(month).days(yearInt), dayOfMonth),
hour,
minute,
second,
milliseconds
)

View File

@@ -0,0 +1,58 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import dev.inmo.krontab.KronScheduler
import dev.inmo.krontab.next
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* This [Flow] will trigger emitting each near time which will be returned from [this] [KronScheduler] with attention to
* time zones
*
* @see channelFlow
*/
@FlowPreview
fun KronScheduler.asTzFlow(): Flow<DateTimeTz> = channelFlow {
var previousTime = DateTime.nowLocal()
while (isActive) {
val now = DateTime.nowLocal()
val nextTime = next(now) ?: break
if (previousTime == nextTime) {
delay(1L) // skip 1ms
continue
} else {
previousTime = nextTime
}
val sleepDelay = (nextTime - DateTime.now().local).millisecondsLong
delay(sleepDelay)
send(nextTime)
}
}
/**
* This method is a map for [asTzFlow] and will works the same but return flow with [DateTime]s
*/
@FlowPreview
fun KronScheduler.asFlow(): Flow<DateTime> = asTzFlow().map { it.local }
@Deprecated(
"It is not recommended to use this class in future. This functionality will be removed soon",
ReplaceWith("asFlow", "dev.inmo.krontab.utils.asFlow")
)
@FlowPreview
class SchedulerFlow(
private val scheduler: KronScheduler
) : AbstractFlow<DateTime>() {
@FlowPreview
override suspend fun collectSafely(collector: FlowCollector<DateTime>) {
while (true) {
val now = DateTime.now()
val nextTime = scheduler.next(now) ?: break
val sleepDelay = (nextTime - now).millisecondsLong
delay(sleepDelay)
collector.emit(nextTime)
}
}
}

View File

@@ -0,0 +1,3 @@
package dev.inmo.krontab.utils
typealias Minutes = Int

View File

@@ -1,4 +1,4 @@
package com.insanusmokrassar.krontab.utils
package dev.inmo.krontab.utils
import kotlinx.coroutines.*

View File

@@ -1,4 +1,4 @@
package com.insanusmokrassar.krontab.utils
package dev.inmo.krontab.utils
import kotlinx.coroutines.CoroutineScope

View File

@@ -1,7 +1,6 @@
package com.insanusmokrassar.krontab.utils
package dev.inmo.krontab.utils
import com.insanusmokrassar.krontab.builder.buildSchedule
import com.soywiz.klock.DateTime
import dev.inmo.krontab.builder.buildSchedule
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.takeWhile

View File

@@ -0,0 +1,96 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.*
import dev.inmo.krontab.KronSchedulerTz
import dev.inmo.krontab.buildSchedule
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.takeWhile
import kotlin.test.Test
import kotlin.test.assertEquals
@ExperimentalCoroutinesApi
@FlowPreview
class StringParseTest {
@Test
fun testThatFlowIsCorrectlyWorkEverySecondBuiltOnString() {
val kronScheduler = buildSchedule("*/1 * * * *")
val flow = kronScheduler.asFlow()
runTest {
val mustBeCollected = 10
var collected = 0
flow.takeWhile {
collected < mustBeCollected
}.collect {
collected++
}
assertEquals(mustBeCollected, collected)
}
}
@Test
fun testThatFlowIsCorrectlyWorkEverySecondWithMuchOfEmittersBuiltOnString() {
val kronScheduler = buildSchedule("*/1 * * * *")
val flow = kronScheduler.asFlow()
runTest {
val testsCount = 10
val failJob = it.createFailJob((testsCount * 2) * 1000L)
val mustBeCollected = 10
val answers = (0 until testsCount).map { _ ->
it.async {
var collected = 0
flow.takeWhile {
collected < mustBeCollected
}.collect {
collected++
}
collected
}
}.awaitAll()
failJob.cancel()
answers.forEach {
assertEquals(mustBeCollected, it)
}
}
}
@Test
fun testThatFlowIsCorrectlyWorkEverySeveralSecondsRangeBuiltOnString() {
val rangesEnds = listOf(0 to 5, 30 to 35)
val kronScheduler = buildSchedule("${rangesEnds.joinToString(",") { "${it.first}-${it.second}" }} * * * *")
val flow = kronScheduler.asFlow()
runTest {
val ranges = rangesEnds.map { it.first .. it.second }.flatten().distinct().toMutableList()
val expectedCollects = ranges.size
var collected = 0
flow.takeWhile { ranges.isNotEmpty() }.collect {
ranges.remove(it.seconds)
collected++
}
assertEquals(expectedCollects, collected)
}
}
@Test
fun testThatTimezoneCorrectlyDeserialized() {
val now = DateTime.now().copy(milliseconds = 0).local
runTest {
for (i in 0 .. 1339) {
val expectedInCurrentOffset = now.toOffset(TimezoneOffset(i.minutes)) + 1.hours
val kronScheduler = buildSchedule(
"${expectedInCurrentOffset.seconds} ${expectedInCurrentOffset.minutes} ${expectedInCurrentOffset.hours} * * ${i}o"
) as KronSchedulerTz
val next = kronScheduler.next(now)
assertEquals(expectedInCurrentOffset.toOffset(now.offset), next)
}
}
}
}

View File

@@ -0,0 +1,28 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.*
import dev.inmo.krontab.builder.buildSchedule
import dev.inmo.krontab.next
import kotlin.test.Test
import kotlin.test.assertEquals
class TimeZoneTest {
@Test
fun testDifferentTimeZonesReturnsDifferentTimes() {
val scheduler = buildSchedule { seconds { every(1) } }
val additionalMilliseconds = 100.milliseconds
val baseDate = DateTime.now().startOfWeek.copy(milliseconds = additionalMilliseconds.millisecondsInt)
runTest {
for (i in 0 until 7) {
val now = baseDate + i.days
for (j in 0 .. 24) {
val nowTz = now.toOffset(j.hours)
val next = scheduler.next(nowTz)!!
assertEquals(
(nowTz + 1.seconds - additionalMilliseconds).utc.unixMillisLong, next.utc.unixMillisLong
)
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
package dev.inmo.krontab.utils
import com.soywiz.klock.*
import dev.inmo.krontab.builder.buildSchedule
import kotlin.math.ceil
import kotlin.test.*
class WeekDaysTest {
@Test
fun testThatWeekDaysSchedulingWorks() {
val startDateTime = DateTime.now().startOfDay
val weekDay = startDateTime.dayOfWeek.index0
val testDays = 400
val scheduler = buildSchedule {
dayOfWeek {
at(weekDay)
}
years {
at(startDateTime.yearInt)
}
}
runTest {
for (day in 0 until testDays) {
val currentDateTime = startDateTime + day.days
val next = scheduler.next(currentDateTime)
val expected = when {
day % 7 == 0 -> currentDateTime
else -> startDateTime + ceil(day.toFloat() / 7).weeks
}
if (expected.yearInt != startDateTime.yearInt) {
assertNull(next)
} else {
assertEquals(expected, next)
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package com.insanusmokrassar.krontab.utils
package dev.inmo.krontab.utils
import kotlinx.coroutines.*

View File

@@ -1,4 +1,4 @@
package com.insanusmokrassar.krontab.utils
package dev.inmo.krontab.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.krontab"/>

View File

@@ -0,0 +1,147 @@
package dev.inmo.krontab
import android.content.Context
import androidx.work.*
import com.soywiz.klock.DateTime
import java.util.concurrent.TimeUnit
/**
* This method will enqueue [OneTimeWorkRequest] with [workName] and [existingWorkPolicy]. Use [setUpRequest] callback
* in case you need some additional actions to do before request will be enqueued
*/
suspend fun <T : KronSchedulerWork> Context.enqueueKronSchedulerWork(
workName: String,
delayMillis: Long,
workClass: Class<T>,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
setUpRequest: suspend OneTimeWorkRequest.Builder.() -> Unit = {}
) = WorkManager.getInstance(applicationContext).enqueueUniqueWork(
workName,
existingWorkPolicy,
OneTimeWorkRequest.Builder(workClass).apply {
setInitialDelay(delayMillis, TimeUnit.MILLISECONDS)
setUpRequest()
}.build()
)
/**
* This method is shortcut for [enqueueKronSchedulerWork] with reified [T] parameter
*/
suspend inline fun <reified T : KronSchedulerWork> Context.enqueueKronSchedulerWork(
workName: String,
delayMillis: Long,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
noinline setUpRequest: suspend OneTimeWorkRequest.Builder.() -> Unit = {}
) = enqueueKronSchedulerWork(workName, delayMillis, T::class.java, existingWorkPolicy, setUpRequest)
/**
* This method is shortcut for [enqueueKronSchedulerWork] with [initialScheduler]. It will try to calculate delay by
* itself. In case if [KronScheduler.next] of [initialScheduler] will return null, work WILL NOT be enqueued
*
* @return null in case if [KronScheduler.next] of [initialScheduler] has returned null and work has not been enqueued
*/
suspend fun <T : KronSchedulerWork> Context.enqueueKronSchedulerWork(
workName: String,
initialScheduler: KronScheduler,
workClass: Class<T>,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
setUpRequest: suspend OneTimeWorkRequest.Builder.() -> Unit = {}
): Operation? {
val now = DateTime.now()
val nextTriggerTime = initialScheduler.next(now)
val delayMillis = nextTriggerTime ?.minus(now) ?.millisecondsLong ?: return null
return enqueueKronSchedulerWork(workName, delayMillis, workClass, existingWorkPolicy, setUpRequest)
}
/**
* This method is shortcut for [enqueueKronSchedulerWork] with reified [T]
*/
suspend inline fun <reified T : KronSchedulerWork> Context.enqueueKronSchedulerWork(
workName: String,
initialScheduler: KronScheduler,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
noinline setUpRequest: suspend OneTimeWorkRequest.Builder.() -> Unit = {}
) = enqueueKronSchedulerWork(workName, initialScheduler, T::class.java, existingWorkPolicy, setUpRequest)
/**
* Use this class as a super class in case you wish to implement krontab-based enqueuing of works
*
* @see enqueueKronSchedulerWork
* @see KrontabTemplateSchedulerWork
*/
abstract class KronSchedulerWork(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(
context,
workerParams
) {
/**
* This variable will be used to reorder new work after that one is happen
*
* @see prolongOnException
* @see prolongOnFailure
* @see prolongOnSuccess
*/
protected abstract val workName: String
/**
* Set this to false in case when this work must not be enqueued after successful complete
*/
protected open val prolongOnSuccess: Boolean = true
/**
* Set this to false in case when this work must not be enqueued after failure complete
*/
protected open val prolongOnFailure
get() = prolongOnSuccess
/**
* Set this to false in case when this work must not be enqueued after exception happen
*/
protected open val prolongOnException = false
/**
* [KronScheduler] of this method will be used to [prolong] this worker
*/
protected abstract suspend fun kronScheduler(): KronScheduler?
/**
* This method is replacement of [doWork]. It is required to wrap work with [prolong]ing and handling of complete
* state
*/
protected abstract suspend fun onWork(): Result
/**
* Override this method in case you have some additional settings for future [OneTimeWorkRequest]
*/
protected open suspend fun OneTimeWorkRequest.Builder.setUpRequest() {}
/**
* This method will [enqueueKronSchedulerWork] using [workName], [kronScheduler] and default
* [ExistingWorkPolicy.REPLACE]. You can call this method in case you want to enqueue work by yourself, but you must
* be sure that you set up to false [prolongOnSuccess], [prolongOnFailure] and [prolongOnException]
*/
protected suspend fun prolong() {
applicationContext.enqueueKronSchedulerWork(
workName,
kronScheduler() ?: return,
this::class.java
) {
setUpRequest()
}
}
override suspend fun doWork(): Result {
val result = try {
onWork()
} catch (e: Throwable) {
if (prolongOnException) {
prolong()
}
throw e
}
when (result) {
is Result.Failure -> if (prolongOnFailure) prolong()
is Result.Success -> if (prolongOnSuccess) prolong()
}
return result
}
}

View File

@@ -0,0 +1,68 @@
package dev.inmo.krontab
import android.content.Context
import androidx.work.*
const val krontabTemplateWorkField = "krontabTemplate"
/**
* Will [enqueueKronSchedulerWork] with [KronScheduler] from [krontabTemplate] and call [setUpRequest] on setting up
* [OneTimeWorkRequest.Builder] with [Data] which will be used to [OneTimeWorkRequest.Builder.setInputData] after
* [setUpRequest] completed
*/
suspend inline fun <reified T : KrontabTemplateSchedulerWork> Context.enqueueKrontabTemplateSchedulerWork(
workName: String,
krontabTemplate: KrontabTemplate,
existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE,
noinline setUpRequest: suspend OneTimeWorkRequest.Builder.(
data: Data
) -> Unit = {}
) = enqueueKronSchedulerWork(workName, krontabTemplate.toKronScheduler(), T::class.java, existingWorkPolicy) {
val data = workDataOf(
krontabTemplateWorkField to krontabTemplate
)
setUpRequest(data)
setInputData(data)
}
/**
* Extend this class in case you wish to base on [KrontabTemplate]. It will automatically handle request of
* [kronScheduler] and put it in [setUpRequest]
*/
abstract class KrontabTemplateSchedulerWork(
context: Context,
workerParams: WorkerParameters
) : KronSchedulerWork(context, workerParams) {
/**
* Will try to get [KrontabTemplate] from [getInputData] by key [krontabTemplateWorkField]
*
* @see setUpRequest
*/
protected val krontabTemplate: KrontabTemplate?
get() = inputData.getString(krontabTemplateWorkField)
/**
* Override this methods instead of old [setUpRequest] in case you wish to set up some work request parameters
*
* @param data This parameter will be used to put data inside of [OneTimeWorkRequest.Builder] after this method
* will be completed
*/
protected open suspend fun OneTimeWorkRequest.Builder.setUpRequest(data: Data) {}
/**
* Will automatically put [krontabTemplate] into work data, call [setUpRequest] with future [Data] object and then
* call [OneTimeWorkRequest.Builder.setInputData] with that [Data] object
*/
override suspend fun OneTimeWorkRequest.Builder.setUpRequest() {
val data = workDataOf(
krontabTemplateWorkField to krontabTemplate,
)
setUpRequest(data)
setInputData(data)
}
/**
* Will return [KronScheduler] in case if [krontabTemplate] was not null
*/
override suspend fun kronScheduler(): KronScheduler? = krontabTemplate ?.toKronScheduler()
}