Compare commits

...

174 Commits

Author SHA1 Message Date
65d01b1fb3 add klock module 2023-12-17 23:37:45 +06:00
6230accb68 start 0.20.23 2023-12-17 22:30:25 +06:00
10e03bb951 allow to create own Diff with constructor 2023-12-14 23:56:56 +06:00
aa4f392948 start 0.20.22 2023-12-14 23:52:28 +06:00
f51b59ec02 Update libs.versions.toml 2023-12-14 13:47:34 +06:00
8c76834ae4 Merge pull request #359 from InsanusMokrassar/0.20.21
0.20.21
2023-12-12 23:49:26 +06:00
4a454f3d67 add opportunity to use translation with default locale 2023-12-12 23:45:22 +06:00
151aa1863d update gradle wrapper 2023-12-12 21:41:53 +06:00
3bf6896296 get back translations with android and java resources packages 2023-12-12 21:41:09 +06:00
0d01561476 optimizations and improvements in resources 2023-12-12 21:39:58 +06:00
f6ded92251 init resources module 2023-12-12 20:58:08 +06:00
d01b735cc6 start 0.20.21 2023-12-12 20:21:37 +06:00
6c12001080 Merge pull request #358 from InsanusMokrassar/0.20.20
0.20.20
2023-12-12 00:12:34 +06:00
1afbf03606 improvements in AbstractExposedCRUDRepo 2023-12-12 00:01:58 +06:00
f6ef5c61c5 start 0.20.20 2023-12-11 23:54:25 +06:00
c18fee8107 update gradle publishing scripts
one more potential ix of publishing scripts

drop new configs of publish scripts

revert publish scripts and update gradle properties
2023-12-09 20:38:44 +06:00
d9df7a4384 Revert gradle wrapper version 2023-12-09 01:38:45 +06:00
87c2230e8e revert xmx 2500 2023-12-08 23:42:26 +06:00
da7eb6de0a change xmx 2023-12-08 21:34:05 +06:00
ed3815118f update jvmargs of gradle 2023-12-08 19:34:47 +06:00
be726f42bd Merge pull request #356 from InsanusMokrassar/0.20.19
update publishing scripts
2023-12-08 16:06:22 +06:00
a91006132f update publishing scripts 2023-12-08 16:06:07 +06:00
9a9f741a0b Merge pull request #355 from InsanusMokrassar/0.20.19
0.20.19
2023-12-08 16:03:05 +06:00
5028f130e9 update dependencies 2023-12-08 15:58:16 +06:00
77fa019651 start 0.20.19 2023-12-08 15:56:44 +06:00
9715da9384 Merge pull request #353 from InsanusMokrassar/0.20.18
0.20.18
2023-12-04 16:13:33 +06:00
f6d5035c1a small refactor in SpecialMutableStateFlow 2023-12-04 15:44:06 +06:00
43e782ab6f SpecialMutableStateFlow : MutableStateFlow 2023-12-04 15:37:02 +06:00
f3f9920bfb deprecate FlowState 2023-12-04 15:08:52 +06:00
2bfd615812 start 0.20.18 2023-12-04 15:01:42 +06:00
ebfacb3659 Merge pull request #352 from InsanusMokrassar/0.20.17
0.20.17
2023-12-01 02:51:00 +06:00
c71d557eec update dependencies 2023-12-01 02:46:15 +06:00
e0398cef21 start 0.10.17 2023-12-01 02:44:44 +06:00
f91599e9c6 Merge pull request #348 from InsanusMokrassar/0.20.16
0.20.16
2023-11-30 01:54:31 +06:00
f8f9f93c97 update changelog, rename SpecialMutableStateFlow file and update gradle wrapper 2023-11-30 01:54:04 +06:00
a8a5340d8b kdocs 2023-11-30 01:27:14 +06:00
871b27f37d initialization fixes 2023-11-30 01:22:43 +06:00
6f174cae1d fixes 2023-11-30 01:05:01 +06:00
22d7ac3e22 FlowState 2023-11-29 23:54:27 +06:00
9b30c3a155 small matrix improvements 2023-11-29 20:33:06 +06:00
915bac64b1 small fixes in SpecialStateFlow 2023-11-29 20:26:23 +06:00
9d2b50e55d SpecialStateFlow 2023-11-29 17:13:21 +06:00
bde100f63d start 0.20.16 2023-11-29 17:12:37 +06:00
05b035a13d Merge pull request #345 from InsanusMokrassar/0.20.15
0.20.15
2023-11-24 23:47:02 +06:00
eefb56bed7 Update libs.versions.toml 2023-11-24 20:14:32 +06:00
fcc0dc4189 update dependencies 2023-11-24 16:58:50 +06:00
47ff20317f start 0.20.15 2023-11-24 16:46:22 +06:00
1558b9103d Merge pull request #343 from InsanusMokrassar/0.20.14
0.20.14
2023-11-18 23:27:06 +06:00
7a78742162 update kslog 2023-11-18 18:11:07 +06:00
c01e240f66 update dependencies 2023-11-18 15:40:12 +06:00
fef4fcbac6 start 0.20.14 2023-11-18 15:37:19 +06:00
5ab18bce4b Merge pull request #335 from InsanusMokrassar/0.20.13
0.20.13
2023-11-11 16:37:11 +06:00
24aec7271a update dependencies and changelog 2023-11-11 16:30:59 +06:00
9b19a2cb95 Update libs.versions.toml 2023-11-10 04:29:38 +06:00
efdd7b8a57 Update libs.versions.toml 2023-11-10 04:28:54 +06:00
6df4cc9c3b start 0.20.13 2023-11-06 21:12:28 +06:00
b9d93db0f5 Merge pull request #334 from InsanusMokrassar/0.20.12
0.20.12
2023-11-02 19:16:56 +06:00
d7ee45ca64 update github workflows 2023-11-02 19:01:00 +06:00
d7c31b1b22 update and fix build 2023-11-02 19:00:35 +06:00
7d6794a358 start 0.20.12 2023-11-02 18:26:15 +06:00
473eb87346 Merge pull request #330 from InsanusMokrassar/0.20.11
0.20.11
2023-11-01 12:38:44 +06:00
8b18b07790 Update CHANGELOG.md 2023-11-01 12:37:30 +06:00
ab3c80a5ec update dependencies 2023-10-31 17:53:56 +06:00
075b93ecd6 SmartRWLocker now will wait first unlock of write mutex for acquiring read 2023-10-26 12:28:24 +06:00
f6d0f72e49 start 0.20.11 2023-10-26 12:28:04 +06:00
fcda3af862 Merge pull request #329 from InsanusMokrassar/0.20.10
0.20.10
2023-10-26 10:51:45 +06:00
d5c7a589b1 update dependencies 2023-10-26 10:48:36 +06:00
86e099ed25 start 0.20.10 2023-10-26 10:37:46 +06:00
ebd7befe73 Merge pull request #324 from InsanusMokrassar/0.20.9
0.20.9
2023-10-24 16:25:05 +06:00
b8c7e581a1 add support of linuxArm64 target 2023-10-20 21:53:27 +06:00
8281259179 start 0.20.9 2023-10-17 23:34:53 +06:00
d3e06b07df Update libs.versions.toml 2023-10-14 22:53:02 +06:00
4967018418 Merge pull request #322 from InsanusMokrassar/0.20.8
0.20.8
2023-10-14 21:01:45 +06:00
537a3c38fa update dependencies 2023-10-14 16:22:00 +06:00
0124957833 start 0.20.8 2023-10-14 16:08:51 +06:00
f0420e2d61 Merge pull request #312 from InsanusMokrassar/0.20.7
0.20.7
2023-10-09 18:16:58 +06:00
7090566041 fill changelog 2023-10-09 13:58:17 +06:00
f0d5035cd0 update dependencies 2023-10-09 13:53:44 +06:00
0fb9b8dc30 update compose version 2023-09-27 17:59:09 +06:00
eef6e81134 Update libs.versions.toml 2023-09-26 01:28:36 +06:00
1593159a3f Update libs.versions.toml 2023-09-26 01:27:20 +06:00
3da9eb9dbe temporal update 2023-09-22 14:14:23 +06:00
f17613f3fb update dependencies 2023-09-12 14:11:12 +06:00
14337ccb46 start 0.20.7 2023-09-10 17:09:08 +06:00
1a3913b09c Merge pull request #311 from InsanusMokrassar/0.20.6
0.20.6
2023-09-08 04:20:58 +06:00
039aed2747 improvements in ExposedReadKeyValuesRepo 2023-09-08 02:59:53 +06:00
173991e3cb start 0.20.6 2023-09-08 02:55:25 +06:00
8b3f8cab01 Merge pull request #310 from InsanusMokrassar/0.20.5
0.20.5
2023-09-07 20:09:58 +06:00
2a20d24589 fixes in SmartRWLocker 2023-09-07 16:57:59 +06:00
53c2d552ec start 0.20.5 2023-09-07 16:57:08 +06:00
af11c1a83d Merge pull request #307 from InsanusMokrassar/0.20.4
0.20.4
2023-09-06 19:17:17 +06:00
a65cf1481c fixes in build 2023-09-06 19:15:09 +06:00
0318716236 update kslog version 2023-09-06 19:08:32 +06:00
88eb4b3342 update dependencies 2023-09-06 18:58:10 +06:00
4810d1ef6a start 0.20.4 2023-09-06 18:55:48 +06:00
f2a9514d89 Merge pull request #306 from InsanusMokrassar/renovate/jb.dokka
Update dependency org.jetbrains.dokka:dokka-gradle-plugin to v1.9.0
2023-09-02 04:15:10 +06:00
925ba6ac24 Update build.gradle 2023-09-02 04:04:04 +06:00
renovate[bot]
ef407268a2 Update dependency org.jetbrains.dokka:dokka-gradle-plugin to v1.9.0 2023-09-01 21:04:25 +00:00
cd6c4bbe38 Merge pull request #304 from InsanusMokrassar/0.20.3
0.20.3
2023-09-02 03:03:57 +06:00
c058e18408 update smart rw locker tests 2023-09-01 20:11:04 +06:00
6d3ca565ca update ktor 2023-09-01 19:41:49 +06:00
236a7b4fd2 Update libs.versions.toml 2023-09-01 04:33:36 +06:00
e1f387dbf7 update dependencies and make a lot of fixes in repos and smart lockers/semaphors 2023-08-31 01:37:35 +06:00
3d113dd31e fixes in locks of caches repos 2023-08-29 15:06:55 +06:00
e0e57f0336 start 0.20.3 2023-08-29 14:59:57 +06:00
e775b58d41 Merge pull request #298 from InsanusMokrassar/0.20.2
0.20.2
2023-08-24 20:10:06 +06:00
5b070a8478 add small kdoc for map repos 2023-08-24 17:15:29 +06:00
8a61193500 update korlibs 2023-08-24 14:29:15 +06:00
fad522b8fe Update CHANGELOG.md 2023-08-24 03:00:08 +06:00
0acac205af fix build 2023-08-24 02:17:27 +06:00
069d4c61b7 update android target 2023-08-23 14:52:09 +06:00
7c113f5700 Update libs.versions.toml 2023-08-23 13:27:23 +06:00
dfdaf4225b start 0.20.2 and all main repos uses 'SmartRWLocker' 2023-08-23 03:26:54 +06:00
bd39ab2467 Merge pull request #295 from InsanusMokrassar/0.20.1
0.20.1
2023-08-12 22:47:03 +06:00
6ce1eb3f2d add tests for smartrwlocker 2023-08-12 22:37:35 +06:00
ce7d4fe9a2 fix of #293 2023-08-12 22:08:12 +06:00
a65bb2f419 add number picker, set picker and small text field 2023-08-11 01:51:40 +06:00
cbc868448b start 0.20.1 2023-08-11 00:58:09 +06:00
9c336a0b56 Update CHANGELOG.md 2023-08-09 02:43:53 +06:00
0f0d09399e Update CHANGELOG.md 2023-08-09 02:42:28 +06:00
e13a1162a9 Merge pull request #294 from InsanusMokrassar/0.20.0
0.20.0
2023-08-09 00:32:03 +06:00
57ebed903f done migration 2023-08-08 18:45:36 +06:00
4478193d8a update dependencies 2023-08-05 13:49:45 +06:00
ee948395e3 start 0.20.0 2023-08-02 13:03:40 +06:00
0616b051ae Merge pull request #291 from InsanusMokrassar/0.19.9
0.19.9
2023-07-29 17:37:38 +06:00
4d155d0505 update koin 2023-07-29 16:37:25 +06:00
a169e733d9 fill changelog 2023-07-29 16:31:50 +06:00
f081e237c8 improvements in startup plugin 2023-07-29 16:30:17 +06:00
f412d387fa start 0.19.9 2023-07-29 16:05:24 +06:00
67354b43e2 Merge pull request #287 from InsanusMokrassar/0.19.8
0.19.8
2023-07-27 01:33:49 +06:00
39135a4000 skipStartInvalidate 2023-07-27 00:35:01 +06:00
eaa014cebd update dependencies 2023-07-27 00:30:26 +06:00
856e657f81 fixes in KeyValueRepo.clear 2023-07-23 13:47:20 +06:00
3a609e5b66 start 0.19.8 2023-07-23 13:26:39 +06:00
d0022dd599 Merge pull request #281 from InsanusMokrassar/0.19.7
0.19.7
2023-06-30 14:37:36 +06:00
7ba6eed453 Update CHANGELOG.md 2023-06-30 12:04:42 +06:00
beeb6ecc0a Update CHANGELOG.md 2023-06-30 12:03:17 +06:00
7cdc17a714 Update libs.versions.toml 2023-06-30 12:02:20 +06:00
4765a950a9 start 0.19.7 2023-06-30 12:01:21 +06:00
65e8137e08 Merge pull request #279 from InsanusMokrassar/0.19.6
0.19.6
2023-06-29 20:27:19 +06:00
ae546dd9ad update dependencies 2023-06-29 18:38:58 +06:00
8110c42be0 update compose 2023-06-27 16:53:10 +06:00
bd2b5ae5fc add versions plugin 2023-06-27 16:46:45 +06:00
2ddfffa6a9 start 0.19.6 2023-06-27 16:46:45 +06:00
a4b54e861d Merge pull request #277 from InsanusMokrassar/0.19.5
0.19.5
2023-06-20 20:22:33 +06:00
c6785f1a4f fixes in types generation 2023-06-20 20:20:47 +06:00
83fe621c56 start 0.19.5 2023-06-20 20:16:24 +06:00
b3a93e17eb Merge pull request #275 from InsanusMokrassar/0.19.4
0.19.4
2023-06-19 16:25:43 +06:00
546a391af3 dependencies update 2023-06-19 16:19:14 +06:00
786cf9bd8b fix in koin generator 2023-06-19 13:52:41 +06:00
dfd6fe062d start 0.19.4 2023-06-19 13:45:29 +06:00
b6ef818613 Merge pull request #274 from InsanusMokrassar/0.19.3
0.19.3
2023-06-19 08:45:45 +06:00
b0f9e9c30a generic-oriented koin definitions 2023-06-19 00:32:45 +06:00
7e5c88ddc3 koin generator improvements 2023-06-18 23:38:36 +06:00
9824c3e00f start 0.19.3 2023-06-18 23:11:50 +06:00
9171d5ed11 update dokka version and return js kdocs 2023-06-09 12:01:11 +06:00
a1830ebb82 Merge pull request #269 from InsanusMokrassar/0.19.2
0.19.2
2023-06-02 13:02:38 +06:00
22d2a3d9bf Update CHANGELOG.md 2023-06-02 12:47:45 +06:00
c9b97fc965 Update libs.versions.toml 2023-06-02 12:46:56 +06:00
51f85becd5 Merge pull request #267 from InsanusMokrassar/0.19.2
0.19.2
2023-06-02 12:39:27 +06:00
a8a281cfb4 downgrade kotlin-poet version 2023-06-02 12:19:02 +06:00
ddd1304949 update dependencies 2023-06-02 12:17:21 +06:00
86d70b6c02 start 0.19.2 2023-06-02 12:11:49 +06:00
a22bdb39e7 Merge pull request #261 from InsanusMokrassar/0.19.1
update kotlin poet (0.19.1)
2023-05-29 17:35:29 +06:00
7ae4d5ef95 update kotlin poet 2023-05-29 17:31:40 +06:00
a2038cbefa Merge pull request #259 from InsanusMokrassar/0.19.1
0.19.1
2023-05-29 17:28:19 +06:00
992091eade Update CHANGELOG.md 2023-05-29 17:27:57 +06:00
e3bfead0c5 Update libs.versions.toml 2023-05-29 10:15:19 +06:00
0de96141fd start 0.19.1 2023-05-29 10:14:35 +06:00
fa18d15c3c Merge pull request #257 from InsanusMokrassar/0.19.0
0.19.0
2023-05-26 20:17:30 +06:00
ea9dbf2371 Koin.get and Scope.get with opportunity to get dependency by its type and definition 2023-05-25 21:12:21 +06:00
d34e3ec7a9 update dependencies 2023-05-25 21:08:12 +06:00
c8833a36af start 0.19.0 2023-05-25 20:58:05 +06:00
a067cb0c0f Merge pull request #256 from InsanusMokrassar/0.18.4
0.18.4
2023-05-14 16:51:57 +06:00
329 changed files with 11163 additions and 681 deletions

View File

@@ -8,7 +8,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
java-version: 17
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"

View File

@@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
java-version: 17
- name: Build
run: ./gradlew build && ./gradlew dokkaHtml
- name: Publish KDocs

View File

@@ -1,5 +1,255 @@
# Changelog
## 0.20.23
* `Klock`:
* Inited as copypaste from [korlibs/korge](https://github.com/korlibs/korge) and [korlibs/korlibs4](https://github.com/korlibs/korlibs4)
## 0.20.22
* `Common`:
* Add opportunity to create own `Diff` with base constructor
## 0.20.21
* `Resources`:
* Inited
## 0.20.20
* `Repos`:
* `Exposed`:
* Add opportunity for setup flows in `AbstractExposedCRUDRepo`
## 0.20.19
* `Versions`:
* `Ktor`: `2.3.6` -> `2.3.7`
## 0.20.18
* `Coroutines`:
* `SpecialMutableStateFlow` now extends `MutableStateFlow`
* `Compose`:
* Deprecate `FlowState` due to its complexity in fixes
## 0.20.17
* `Versions`:
* `Serialization`: `1.6.1` -> `1.6.2`
## 0.20.16
* `Versions`:
* `Exposed`: `0.44.1` -> `0.45.0`
* `Coroutines`:
* Add `SpecialMutableStateFlow`
* `Compose`:
* Add `FlowState`
## 0.20.15
* `Versions`:
* `Kotlin`: `1.9.20` -> `1.9.21`
* `KSLog`: `1.3.0` -> `1.3.1`
* `Compose`: `1.5.10` -> `1.5.11`
## 0.20.14
* `Versions`:
* `Serialization`: `1.6.0` -> `1.6.1`
* `KSLog`: `1.2.4` -> `1.3.0`
## 0.20.13
* `Versions`:
* `Ktor`: `2.3.5` -> `2.3.6`
* `UUID`: `0.8.1` -> `0.8.2`
## 0.20.12
**It is experimental migration onto new gradle version. Be careful in use of this version**
**This update have JDK 17 in `compatibility` and `target` versions**
## 0.20.11
* `Versions`:
* `Kotlin`: `1.9.20-RC2` -> `1.9.20`
* `Exposed`: `0.44.0` -> `0.44.1`
* `Compose`: `1.5.10-rc02` -> `1.5.10`
* `Coroutines`:
* `SmartRWLocker` now will wait first unlock of write mutex for acquiring read
## 0.20.10
* `Versions`:
* `Kotlin`: `1.9.20-RC` -> `1.9.20-RC1`
* `KSLog`: `1.2.1` -> `1.2.2`
* `Compose`: `1.5.10-rc01` -> `1.5.10-rc02`
* `RecyclerView`: `1.3.1` -> `1.3.2`
## 0.20.9
* Most of common modules now supports `linuxArm64` target
## 0.20.8
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO RC VERSIONS. USE WITH CAUTION**
* `Versions`:
* `Kotlin`: `1.9.20-Beta2` -> `1.9.20-RC`
* `Compose`: `1.5.10-beta02` -> `1.5.10-rc01`
## 0.20.7
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO BETA VERSIONS. USE WITH CAUTION**
* `Versions`:
* `Kotlin`: `1.9.10` -> `1.9.20-Beta2`
* `Compose`: `1.5.1` -> `1.5.10-beta02`
* `Exposed`: `0.43.0` -> `0.44.0`
* `Ktor`: `2.3.4` -> `2.3.5`
* `Koin`: `3.4.3` -> `3.5.0`
* `Okio`: `3.5.0` -> `3.6.0`
* `Android Core`: `1.10.1` -> `1.12.0`
* `Android Compose Material`: `1.1.1` -> `1.1.2`
## 0.20.6
* `Repos`:
* `Exposed`
* Fixes in exposed key-values repos
## 0.20.5
* `Coroutines`:
* Fixes in `SmartRWLocker`
## 0.20.4
* `Versions`:
* `Kotlin`: `1.9.0` -> `1.9.10`
* `KSLog`: `1.2.0` -> `1.2.1`
* `Compose`: `1.5.0` -> `1.5.1`
* `UUID`: `0.8.0` -> `0.8.1`
## 0.20.3
* `Versions`:
* `Compose`: `1.4.3` -> `1.5.0`
* `Exposed`: `0.42.1` -> `0.43.0`
* `Ktor`: `2.3.3` -> `2.3.4`
* `Repos`:
* `Cache`:
* Fixes in locks of caches
## 0.20.2
* All main repos uses `SmartRWLocker`
* `Versions`:
* `Serialization`: `1.5.1` -> `1.6.0`
* `Exposed`: `0.42.0` -> `0.42.1`
* `Korlibs`: `4.0.9` -> `4.0.10`
* `Androis SDK`: `33` -> `34`
## 0.20.1
* `SmallTextField`:
* Module is initialized
* `Pickers`:
* Module is initialized
* `Coroutines`:
* Add `SmartSemaphore`
* Add `SmartRWLocker`
## 0.20.0
* `Versions`:
* `Kotlin`: `1.8.22` -> `1.9.0`
* `KSLog`: `1.1.1` -> `1.2.0`
* `Exposed`: `0.41.1` -> `0.42.0`
* `UUID`: `0.7.1` -> `0.8.0`
* `Korlibs`: `4.0.3` -> `4.0.9`
* `Ktor`: `2.3.2` -> `2.3.3`
* `Okio`: `3.4.0` -> `3.5.0`
## 0.19.9
* `Versions`:
* `Koin`: `3.4.2` -> `3.4.3`
* `Startup`:
* Now it is possible to start application in synchronous way
## 0.19.8
* `Versions`:
* `Coroutines`: `1.7.2` -> `1.7.3`
* `Kotlin`: `1.8.20` -> `1.8.22`
* `Compose`: `1.4.1` -> `1.4.3`
* `Okio`: `3.3.0` -> `3.4.0`
* `RecyclerView`: `1.3.0` -> `1.3.1`
* `Fragment`: `1.6.0` -> `1.6.1`
* `Repos`:
* Fixes In `KeyValueRepo.clear()` of almost all inheritors of `KeyValueRepo`
* `Cache`:
* All full caches got `skipStartInvalidate` property. By default, this property is `false` and fully caching repos
will be automatically invalidated on start of their work
## 0.19.7
* `Versions`:
* `Coroutines`: `1.7.1` -> `1.7.2`
## 0.19.6
* `Versions`:
* `Coroutines`: `1.6.4` -> `1.7.1`
* `Ktor`: `2.3.1` -> `2.3.2`
* `Compose`: `1.4.0` -> `1.4.1`
## 0.19.5
* `Repos`:
* `Generator`:
* Fixes in new type generation
## 0.19.4
* `Versions`:
* `Koin`: `3.4.1` -> `3.4.2`
* `Android Fragments`: `1.5.7` -> `1.6.0`
* `Koin`
* `Generator`
* Fixes in new generic generator part
## 0.19.3
* `Koin`
* `Generator`
* New getter methods now available with opportunity to use parameters
* Old notation `*Single` and `*Factory` is deprecated since this release. With old
will be generated new `single*` and `factory*` notations for new generations
* Add opportunity to use generic-oriented koin definitions
## 0.19.2
* `Versions`:
* `Ktor`: `2.3.0` -> `2.3.1`
* `Koin`: `3.4.0` -> `3.4.1`
* `Uuid`: `0.7.0` -> `0.7.1`
## 0.19.1
* `Versions`:
* `Korlibs`: `4.0.1` -> `4.0.3`
* `Kotlin Poet`: `1.13.2` -> `1.14.0`
## 0.19.0
* `Versions`:
* `Korlibs`: `3.4.0` -> `4.0.1`
## 0.18.4
* `Koin`:

View File

@@ -1,5 +1,11 @@
# MicroUtils
---
**`Klock` module initial commit based on [korlibs/korge](https://github.com/korlibs/korge) and [korlibs/korlibs4](https://github.com/korlibs/korlibs4)**
---
This is a library with collection of tools for working in Kotlin environment. First of all, this library collection is oriented to use next technologies:
* [`Kotlin Coroutines`](https://github.com/Kotlin/kotlinx.coroutines)

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -1 +0,0 @@
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -1 +0,0 @@
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>

View File

@@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
kotlin {
sourceSets {
androidMain {
dependencies {
api project(":micro_utils.android.smalltextfield")
}
}
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,27 @@
package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.*
internal suspend fun Animatable<Float, AnimationVector1D>.fling(
initialVelocity: Float,
animationSpec: DecayAnimationSpec<Float>,
adjustTarget: ((Float) -> Float)?,
block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
): AnimationResult<Float, AnimationVector1D> {
val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
val adjustedTarget = adjustTarget?.invoke(targetValue)
return if (adjustedTarget != null) {
animateTo(
targetValue = adjustedTarget,
initialVelocity = initialVelocity,
block = block
)
} else {
animateDecay(
initialVelocity = initialVelocity,
animationSpec = animationSpec,
block = block,
)
}
}

View File

@@ -0,0 +1,222 @@
package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ContentAlpha
import androidx.compose.material.IconButton
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.center
import androidx.compose.ui.unit.dp
import dev.inmo.micro_utils.android.smalltextfield.SmallTextField
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2)
}
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun NumberPicker(
number: Int,
modifier: Modifier = Modifier,
range: IntRange? = null,
textStyle: TextStyle = LocalTextStyle.current,
arrowsColor: Color = MaterialTheme.colorScheme.primary,
allowUseManualInput: Boolean = true,
onStateChanged: (Int) -> Unit = {},
) {
val coroutineScope = rememberCoroutineScope()
val numbersColumnHeight = 36.dp
val halvedNumbersColumnHeight = numbersColumnHeight / 2
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt()
val animatedOffset = remember { Animatable(0f) }.apply {
if (range != null) {
val offsetRange = remember(number, range) {
val value = number
val first = -(range.last - value) * halvedNumbersColumnHeightPx
val last = -(range.first - value) * halvedNumbersColumnHeightPx
first..last
}
updateBounds(offsetRange.start, offsetRange.endInclusive)
}
}
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
val animatedStateValue = animatedStateValue(animatedOffset.value)
val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled)
val inputFieldShown = if (allowUseManualInput) {
remember { mutableStateOf(false) }
} else {
null
}
Column(
modifier = modifier
.wrapContentSize()
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { deltaY ->
if (inputFieldShown ?.value != true) {
coroutineScope.launch {
animatedOffset.snapTo(animatedOffset.value + deltaY)
}
}
},
onDragStopped = { velocity ->
if (inputFieldShown ?.value != true) {
coroutineScope.launch {
val endValue = animatedOffset.fling(
initialVelocity = velocity,
animationSpec = exponentialDecay(frictionMultiplier = 20f),
adjustTarget = { target ->
val coercedTarget = target % halvedNumbersColumnHeightPx
val coercedAnchors =
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
val base =
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
coercedPoint + base
}
).endState.value
onStateChanged(animatedStateValue(endValue))
animatedOffset.snapTo(0f)
}
}
}
),
horizontalAlignment = Alignment.CenterHorizontally
) {
val spacing = 4.dp
val upEnabled = range == null || range.first < number
IconButton(
{
onStateChanged(number - 1)
inputFieldShown ?.value = false
},
enabled = upEnabled
) {
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
}
Spacer(modifier = Modifier.height(spacing))
Box(
modifier = Modifier
.offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) },
contentAlignment = Alignment.Center
) {
val baseLabelModifier = Modifier.align(Alignment.Center)
ProvideTextStyle(textStyle) {
Text(
text = (animatedStateValue - 1).toString(),
modifier = baseLabelModifier
.offset(y = -halvedNumbersColumnHeight)
.alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx)
)
if (inputFieldShown ?.value == true) {
val currentValue = remember { mutableStateOf(number.toString()) }
val focusRequester = remember { FocusRequester() }
SmallTextField(
currentValue.value,
{
val asDigit = it.toIntOrNull()
when {
(asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString()
(asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it
else -> { /* do nothing */ }
}
},
baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) {
detectTapGestures {
if (!checkContains(it)) {
currentValue.value.toIntOrNull() ?.let(onStateChanged)
inputFieldShown.value = false
}
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
keyboardActions = KeyboardActions {
currentValue.value.toIntOrNull() ?.let(onStateChanged)
inputFieldShown.value = false
},
singleLine = true,
textStyle = textStyle
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
} else {
Text(
text = animatedStateValue.toString(),
modifier = baseLabelModifier
.alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
.clickable {
if (inputFieldShown ?.value == false) {
inputFieldShown.value = true
}
}
)
}
Text(
text = (animatedStateValue + 1).toString(),
modifier = baseLabelModifier
.offset(y = halvedNumbersColumnHeight)
.alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx)
)
}
}
Spacer(modifier = Modifier.height(spacing))
val downEnabled = range == null || range.last > number
IconButton(
{
onStateChanged(number + 1)
inputFieldShown ?.value = false
},
enabled = downEnabled
) {
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
}
}
}

View File

@@ -0,0 +1,156 @@
package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlin.math.*
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun <T> SetPicker(
current: T,
dataList: List<T>,
modifier: Modifier = Modifier,
textStyle: TextStyle = LocalTextStyle.current,
arrowsColor: Color = MaterialTheme.colorScheme.primary,
dataToString: @Composable (T) -> String = { it.toString() },
onStateChanged: (T) -> Unit = {},
) {
val coroutineScope = rememberCoroutineScope()
val numbersColumnHeight = 8.dp + with(LocalDensity.current) {
textStyle.lineHeight.toDp()
}
val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() }
val halvedNumbersColumnHeight = numbersColumnHeight / 2
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current)
val lastIndex = dataList.size - 1
fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt()
val animatedOffset = remember { Animatable(0f) }.apply {
val offsetRange = remember(index, lastIndex) {
val value = index
val first = -(lastIndex - value) * halvedNumbersColumnHeightPx
val last = value * halvedNumbersColumnHeightPx
first..last
}
updateBounds(offsetRange.start, offsetRange.endInclusive)
}
val indexAnimatedOffset = if (animatedOffset.value > 0) {
(index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
} else {
(index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
}
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
val boxOffset = (indexAnimatedOffset * halvedNumbersColumnHeightPx) - coercedAnimatedOffset
val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled)
val scrollState = rememberScrollState()
Column(
modifier = modifier
.wrapContentSize()
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { deltaY ->
coroutineScope.launch {
animatedOffset.snapTo(animatedOffset.value + deltaY)
}
},
onDragStopped = { velocity ->
coroutineScope.launch {
val endValue = animatedOffset.fling(
initialVelocity = velocity,
animationSpec = exponentialDecay(frictionMultiplier = 20f),
adjustTarget = { target ->
val coercedTarget = target % halvedNumbersColumnHeightPx
val coercedAnchors =
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
val base =
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
coercedPoint + base
}
).endState.value
onStateChanged(dataList.elementAt(animatedStateValue(endValue)))
animatedOffset.snapTo(0f)
}
}
),
horizontalAlignment = Alignment.CenterHorizontally
) {
val spacing = 4.dp
val upEnabled = index > 0
IconButton(
{
onStateChanged(dataList.elementAt(index - 1))
},
enabled = upEnabled
) {
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
}
Spacer(modifier = Modifier.height(spacing))
Box(
modifier = Modifier,
contentAlignment = Alignment.Center
) {
ProvideTextStyle(textStyle) {
dataList.forEachIndexed { i, t ->
val alpha = when {
i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx
i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx
else -> return@forEachIndexed
}
val offset = when {
i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx
i == indexAnimatedOffset -> coercedAnimatedOffset
i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx
else -> return@forEachIndexed
}
Text(
text = dataToString(t),
modifier = Modifier
.alpha(alpha)
.offset(y = with(LocalDensity.current) { offset.toDp() })
)
}
}
}
Spacer(modifier = Modifier.height(spacing))
val downEnabled = index < lastIndex
IconButton(
{
onStateChanged(dataList.elementAt(index + 1))
},
enabled = downEnabled
) {
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
}
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

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

View File

@@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
kotlin {
sourceSets {
androidMain {
dependencies {
api libs.android.compose.material3
}
}
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,66 @@
package dev.inmo.micro_utils.android.smalltextfield
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SmallTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
textColor: Color = textStyle.color.takeOrElse {
LocalContentColor.current
},
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
BasicTextField(
value = value,
modifier = modifier,
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = textStyle.copy(
color = textColor
),
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
cursorBrush = SolidColor(
textStyle.color.takeOrElse {
LocalContentColor.current
}
)
)
}

View File

@@ -17,13 +17,17 @@ buildscript {
}
}
plugins {
alias(libs.plugins.versions)
}
allprojects {
repositories {
mavenLocal()
mavenCentral()
google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
}
// temporal crutch until legacy tests will be stabled or legacy target will be removed
@@ -38,3 +42,4 @@ allprojects {
apply from: "./extensions.gradle"
apply from: "./github_release.gradle"
apply from: "./versions_plugin_setup.gradle"

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
kotlin {
sourceSets {
@@ -31,5 +31,10 @@ kotlin {
api libs.okio
}
}
linuxArm64Main {
dependencies {
api libs.okio
}
}
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

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

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -27,7 +27,7 @@ private inline fun <T> getObject(
* @see calculateDiff
*/
@Serializable
data class Diff<T> internal constructor(
data class Diff<T> @Warning(warning) constructor(
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
/**
* Old-New values pairs
@@ -36,6 +36,10 @@ data class Diff<T> internal constructor(
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
) {
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
companion object {
private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing"
}
}
fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList())

View File

@@ -0,0 +1,36 @@
package dev.inmo.micro_utils.common
import okio.FileSystem
import okio.Path
import okio.use
actual typealias MPPFile = Path
/**
* @suppress
*/
actual val MPPFile.filename: FileName
get() = FileName(toString())
/**
* @suppress
*/
actual val MPPFile.filesize: Long
get() = FileSystem.SYSTEM.openReadOnly(this).use {
it.size()
}
/**
* @suppress
*/
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = {
FileSystem.SYSTEM.read(this) {
readByteArray()
}
}
/**
* @suppress
*/
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = {
bytesAllocatorSync()
}

View File

@@ -0,0 +1,25 @@
package dev.inmo.micro_utils.common
import kotlinx.cinterop.*
import platform.posix.snprintf
import platform.posix.sprintf
@OptIn(ExperimentalForeignApi::class)
actual fun Float.fixed(signs: Int): Float {
return memScoped {
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
sprintf(buff, "%.${signs}f", this@fixed)
buff.toKString().toFloat()
}
}
@OptIn(ExperimentalForeignApi::class)
actual fun Double.fixed(signs: Int): Double {
return memScoped {
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
sprintf(buff, "%.${signs}f", this@fixed)
buff.toKString().toDouble()
}
}

View File

@@ -1,12 +1,10 @@
package dev.inmo.micro_utils.common
import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.allocArray
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.toKString
import kotlinx.cinterop.*
import platform.posix.snprintf
import platform.posix.sprintf
@OptIn(ExperimentalForeignApi::class)
actual fun Float.fixed(signs: Int): Float {
return memScoped {
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
@@ -16,6 +14,7 @@ actual fun Float.fixed(signs: Int): Float {
}
}
@OptIn(ExperimentalForeignApi::class)
actual fun Double.fixed(signs: Int): Double {
return memScoped {
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)

View File

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

View File

@@ -1,12 +1,10 @@
package dev.inmo.micro_utils.common
import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.allocArray
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.toKString
import kotlinx.cinterop.*
import platform.posix.snprintf
import platform.posix.sprintf
@OptIn(ExperimentalForeignApi::class)
actual fun Float.fixed(signs: Int): Float {
return memScoped {
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
@@ -16,6 +14,7 @@ actual fun Float.fixed(signs: Int): Float {
}
}
@OptIn(ExperimentalForeignApi::class)
actual fun Double.fixed(signs: Int): Double {
return memScoped {
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
kotlin {
sourceSets {

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,46 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.MutableState
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
/**
* This type works like [MutableState], [kotlinx.coroutines.flow.StateFlow] and [kotlinx.coroutines.flow.MutableSharedFlow].
* Based on [SpecialMutableStateFlow]
*/
@Deprecated("Will be removed soon")
class FlowState<T>(
initial: T,
internalScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : MutableState<T>,
SpecialMutableStateFlow<T>(initial, internalScope) {
private var internalValue: T = initial
override var value: T
get() = internalValue
set(value) {
internalValue = value
tryEmit(value)
}
override fun onChangeWithoutSync(value: T) {
internalValue = value
super.onChangeWithoutSync(value)
}
override fun component1(): T = value
override fun component2(): (T) -> Unit = { tryEmit(it) }
override fun tryEmit(value: T): Boolean {
internalValue = value
return super.tryEmit(value)
}
override suspend fun emit(value: T) {
internalValue = value
super.emit(value)
}
}
//fun <T> MutableState<T>.asFlowState(scope: CoroutineScope = CoroutineScope(Dispatchers.Main)) = FlowState(this, scope)

View File

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

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,105 @@
package dev.inmo.micro_utils.coroutines
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Composite mutex which works with next rules:
*
* * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore]
* * [releaseRead] will just free up one permit in [readSemaphore]
* * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed
* * [unlockWrite] will just unlock [writeMutex]
*/
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0)
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
val writeMutex: SmartMutex.Immutable = _writeMutex.immutable()
/**
* Do lock in [readSemaphore] inside of [writeMutex] locking
*/
suspend fun acquireRead() {
_writeMutex.waitUnlock()
_readSemaphore.acquire()
}
/**
* Release one read permit in [readSemaphore]
*/
suspend fun releaseRead(): Boolean {
return _readSemaphore.release()
}
/**
* Locking [writeMutex] and wait while all [readSemaphore] permits will be freed
*/
suspend fun lockWrite() {
_writeMutex.lock()
_readSemaphore.acquire(readPermits)
}
/**
* Unlock [writeMutex]
*/
suspend fun unlockWrite(): Boolean {
return _writeMutex.unlock().also {
if (it) {
_readSemaphore.release(readPermits)
}
}
}
}
/**
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartRWLocker.withReadAcquire(action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
acquireRead()
try {
return action()
} finally {
releaseRead()
}
}
/**
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
*
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
* the fact that some other parties may lock it again
*/
suspend fun SmartRWLocker.waitReadRelease(permits: Int = 1) = readSemaphore.waitRelease(permits)
/**
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartRWLocker.withWriteLock(action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
lockWrite()
try {
return action()
} finally {
unlockWrite()
}
}
/**
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
*
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
* that some other parties may lock it again
*/
suspend fun SmartRWLocker.waitWriteUnlock() = writeMutex.waitUnlock()

View File

@@ -0,0 +1,168 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* It is interface which will work like classic [Semaphore], but in difference have [permitsStateFlow] for listening of the
* [SmartSemaphore] state.
*
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
* [Mutable] [SmartSemaphore]. Otherwise, [Immutable].
*
* Any [Mutable] [SmartSemaphore] may produce its [Immutable] variant which will contains [permitsStateFlow] equal to its
* [Mutable] creator
*/
sealed interface SmartSemaphore {
val permitsStateFlow: StateFlow<Int>
/**
* * True - locked
* * False - unlocked
*/
val freePermits: Int
get() = permitsStateFlow.value
/**
* Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow]
*/
class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore
/**
* Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create
* [Immutable] variant of [this] instance with [immutable] factory
*
* @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow]
*/
class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
private val _freePermitsStateFlow = MutableStateFlow<Int>(permits - acquiredPermits)
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
private val internalChangesMutex = Mutex(false)
fun immutable() = Immutable(permitsStateFlow)
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits)
/**
* Holds call until this [SmartSemaphore] will be re-locked. That means that current method will
*/
suspend fun acquire(permits: Int = 1) {
var acquiredPermits = 0
val checkedPermits = checkedPermits(permits)
try {
do {
val shouldContinue = internalChangesMutex.withLock {
val requiredPermits = checkedPermits - acquiredPermits
val acquiring = minOf(freePermits, requiredPermits).takeIf { it > 0 } ?: return@withLock true
acquiredPermits += acquiring
_freePermitsStateFlow.value -= acquiring
acquiredPermits != checkedPermits
}
if (shouldContinue) {
waitRelease()
}
} while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) {
release(acquiredPermits)
throw e
}
}
/**
* Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will
* wait for [freePermits] == false and then try to lock
*/
suspend fun acquireByOne(permits: Int = 1) {
val checkedPermits = checkedPermits(permits)
do {
waitRelease(checkedPermits)
val shouldContinue = internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < checkedPermits) {
true
} else {
_freePermitsStateFlow.value -= checkedPermits
false
}
}
} while (shouldContinue && currentCoroutineContext().isActive)
}
/**
* Will try to lock this [SmartSemaphore] immediataly
*
* @return True if lock was successful. False otherwise
*/
suspend fun tryAcquire(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits)
return if (_freePermitsStateFlow.value < checkedPermits) {
internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < checkedPermits) {
_freePermitsStateFlow.value -= checkedPermits
true
} else {
false
}
}
} else {
false
}
}
/**
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this
* [SmartSemaphore] - false
*/
suspend fun release(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits)
return if (_freePermitsStateFlow.value < this.permits) {
internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < this.permits) {
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits)
true
} else {
false
}
}
} else {
false
}
}
}
}
/**
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
acquire(permits)
try {
return action()
} finally {
release(permits)
}
}
/**
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
*
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
* the fact that some other parties may lock it again
*/
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits }

View File

@@ -0,0 +1,86 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized
/**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber
*/
open class SpecialMutableStateFlow<T>(
initialValue: T,
internalScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@OptIn(InternalCoroutinesApi::class)
private val syncObject = SynchronizedObject()
protected val internalSharedFlow: MutableSharedFlow<T> = MutableSharedFlow(
replay = 0,
extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
protected val publicSharedFlow: MutableSharedFlow<T> = MutableSharedFlow(
replay = 1,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
protected var _value: T = initialValue
override var value: T
get() = _value
set(value) {
doOnChangeAction(value)
}
protected val job = internalSharedFlow.subscribe(internalScope) {
doOnChangeAction(it)
}
override val replayCache: List<T>
get() = publicSharedFlow.replayCache
override val subscriptionCount: StateFlow<Int>
get() = publicSharedFlow.subscriptionCount
@OptIn(InternalCoroutinesApi::class)
override fun compareAndSet(expect: T, update: T): Boolean {
return synchronized(syncObject) {
if (expect == _value && update != _value) {
doOnChangeAction(update)
}
expect == _value
}
}
protected open fun onChangeWithoutSync(value: T) {
_value = value
publicSharedFlow.tryEmit(value)
}
@OptIn(InternalCoroutinesApi::class)
protected open fun doOnChangeAction(value: T) {
synchronized(syncObject) {
if (_value != value) {
onChangeWithoutSync(value)
}
}
}
@ExperimentalCoroutinesApi
override fun resetReplayCache() = publicSharedFlow.resetReplayCache()
override fun tryEmit(value: T): Boolean {
return internalSharedFlow.tryEmit(value)
}
override suspend fun emit(value: T) {
internalSharedFlow.emit(value)
}
override suspend fun collect(collector: FlowCollector<T>) = publicSharedFlow.collect(collector)
}

View File

@@ -0,0 +1,151 @@
import dev.inmo.micro_utils.coroutines.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SmartRWLockerTests {
@Test
fun compositeTest() {
val locker = SmartRWLocker()
val readAndWriteWorkers = 10
runTest {
var started = 0
var done = 0
val doneMutex = Mutex()
val readWorkers = (0 until readAndWriteWorkers).map {
launch(start = CoroutineStart.LAZY) {
locker.withReadAcquire {
doneMutex.withLock {
started++
}
delay(100L)
doneMutex.withLock {
done++
}
}
}
}
var doneWrites = 0
val writeWorkers = (0 until readAndWriteWorkers).map {
launch(start = CoroutineStart.LAZY) {
locker.withWriteLock {
assertTrue(done == readAndWriteWorkers || started == 0)
delay(10L)
doneWrites++
}
}
}
readWorkers.forEach { it.start() }
writeWorkers.forEach { it.start() }
readWorkers.joinAll()
writeWorkers.joinAll()
assertEquals(expected = readAndWriteWorkers, actual = done)
assertEquals(expected = readAndWriteWorkers, actual = doneWrites)
}
}
@Test
fun simpleWithWriteLockTest() {
val locker = SmartRWLocker()
runTest {
locker.withWriteLock {
assertEquals(0, locker.readSemaphore.freePermits)
assertEquals(true, locker.writeMutex.isLocked)
}
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
}
@Test
fun failureWithWriteLockTest() {
val locker = SmartRWLocker()
val exception = IllegalArgumentException()
try {
runTest {
val subscope = kotlinx.coroutines.CoroutineScope(this.coroutineContext)
var happenException: Throwable? = null
try {
locker.withWriteLock {
val checkFunction = fun (): Deferred<Unit> {
return subscope.async {
assertEquals(0, locker.readSemaphore.freePermits)
assertEquals(true, locker.writeMutex.isLocked)
throw exception
}
}
doInDefault {
assertEquals(0, locker.readSemaphore.freePermits)
assertEquals(true, locker.writeMutex.isLocked)
checkFunction().await()
}
}
} catch (e: Exception) {
happenException = e
}
if (exception != happenException) {
assertEquals(exception, happenException ?.cause)
}
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
} catch (e: Exception) {
assertEquals(exception, e)
}
}
@Test
fun simpleWithReadAcquireTest() {
val locker = SmartRWLocker()
runTest {
locker.withReadAcquire {
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
locker.withReadAcquire {
assertEquals(Int.MAX_VALUE - 2, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
}
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
}
@Test
fun simple2WithWriteLockTest() {
val locker = SmartRWLocker()
val unlockDelay = 1000L // 1 sec
var unlocked: Boolean = false
runTest {
launch {
locker.withReadAcquire {
delay(unlockDelay)
}
unlocked = true
}
locker.readSemaphore.permitsStateFlow.first { it == Int.MAX_VALUE - 1 }
assertEquals(false, unlocked)
locker.withWriteLock {
assertEquals(true, unlocked)
assertEquals(0, locker.readSemaphore.freePermits)
assertEquals(true, locker.writeMutex.isLocked)
}
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
}
}

View File

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

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
kotlin {
sourceSets {

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -1,6 +1,6 @@
package dev.inmo.micro_utils.crypto
import com.soywiz.krypto.md5
import korlibs.crypto.md5
typealias MD5 = String

View File

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

View File

@@ -9,6 +9,7 @@ android {
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
versionCode "${android_code_version}".toInteger()
versionName "$version"
namespace "${project.group}.${project.name}"
}
buildTypes {
release {
@@ -26,11 +27,7 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}

View File

@@ -13,10 +13,10 @@ repositories {
kotlin {
jvm()
// js(IR) {
// browser()
// nodejs()
// }
js(IR) {
browser()
nodejs()
}
android {}
sourceSets {
@@ -26,44 +26,44 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
) {
api it
}
}
}
}
// jsMain {
// dependencies {
// implementation kotlin('stdlib')
jsMain {
dependencies {
implementation kotlin('stdlib')
// project.parent.subprojects.forEach {
// if (
// it != project
// && it.hasProperty("kotlin")
// && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
// ) {
// api it
// }
// }
// }
// }
project.parent.subprojects.forEach {
if (
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
) {
api it
}
}
}
}
jvmMain {
dependencies {
implementation kotlin('stdlib')
project.parent.subprojects.forEach {
if (
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
) {
api it
}
@@ -76,10 +76,10 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
) {
api it
}
@@ -106,7 +106,7 @@ tasks.dokkaHtml {
skipDeprecated.set(true)
sourceLink {
localDirectory.set(file("./"))
localDirectory.set(file("../"))
remoteUrl.set(new URL("https://github.com/InsanusMokrassar/MicroUtils/blob/master/"))
remoteLineSuffix.set("#L")
}
@@ -116,9 +116,9 @@ tasks.dokkaHtml {
sourceRoots.setFrom(findSourcesWithName("commonMain"))
}
// named("jsMain") {
// sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
// }
named("jsMain") {
sourceRoots.setFrom(findSourcesWithName("jsMain"))
}
named("jvmMain") {
sourceRoots.setFrom(findSourcesWithName("jvmMain"))

View File

@@ -0,0 +1 @@
<manifest/>

View File

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

View File

@@ -19,11 +19,19 @@ allprojects {
}
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
// String compilerPluginVersionFromProperties = (String) project.properties["compose.kotlinCompilerPluginVersion"]
// String compilerPluginVersionFromLibrariesVersions = libs.versions.compose.kotlin.get()
// composePluginKotlinVersion = compilerPluginVersionFromProperties
// if (compilerPluginVersionFromProperties == null) {
// composePluginKotlinVersion = compilerPluginVersionFromLibrariesVersions
// }
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidProject.gradle"
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
mppJvmJsLinuxMingwProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwProject.gradle"
mppJvmJsLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwLinuxArm64Project.gradle"
mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle"
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
kotlin {
sourceSets {

View File

@@ -0,0 +1 @@
<manifest/>

View File

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

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
kotlin {
sourceSets {

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -1 +0,0 @@
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>

View File

@@ -3,9 +3,10 @@ org.gradle.parallel=true
kotlin.js.generate.externals=true
kotlin.incremental=true
kotlin.incremental.js=true
#kotlin.experimental.tryK2=true
android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
# JS NPM
@@ -14,5 +15,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.18.4
android_code_version=195
version=0.20.23
android_code_version=229

View File

@@ -1,42 +1,45 @@
[versions]
kt = "1.8.20"
kt-serialization = "1.5.1"
kt-coroutines = "1.6.4"
kt = "1.9.21"
kt-serialization = "1.6.2"
kt-coroutines = "1.7.3"
kslog = "1.1.1"
kslog = "1.3.1"
jb-compose = "1.4.0"
jb-exposed = "0.41.1"
jb-dokka = "1.8.10"
jb-compose = "1.5.11"
jb-exposed = "0.45.0"
jb-dokka = "1.9.10"
korlibs = "3.4.0"
uuid = "0.7.0"
korlibs = "4.0.10"
uuid = "0.8.2"
ktor = "2.3.0"
ktor = "2.3.7"
gh-release = "2.4.1"
koin = "3.4.0"
koin = "3.5.0"
okio = "3.3.0"
okio = "3.6.0"
ksp = "1.8.20-1.0.11"
kotlin-poet = "1.13.2"
ksp = "1.9.21-1.0.16"
kotlin-poet = "1.15.3"
android-gradle = "7.4.2"
versions = "0.50.0"
android-gradle = "8.2.0"
dexcount = "4.0.0"
android-coreKtx = "1.10.1"
android-recyclerView = "1.3.0"
android-coreKtx = "1.12.0"
android-recyclerView = "1.3.2"
android-appCompat = "1.6.1"
android-fragment = "1.5.7"
android-fragment = "1.6.2"
android-espresso = "3.5.1"
android-test = "1.1.5"
android-compose-material3 = "1.1.2"
android-props-minSdk = "21"
android-props-compileSdk = "33"
android-props-buildTools = "33.0.2"
android-props-compileSdk = "34"
android-props-buildTools = "34.0.0"
[libraries]
@@ -81,6 +84,7 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
android-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "android-compose-material3" }
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
@@ -109,3 +113,5 @@ buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gra
[plugins]
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -2,11 +2,11 @@ apply plugin: 'maven-publish'
task javadocJar(type: Jar) {
from javadoc
classifier = 'javadoc'
archiveClassifier = 'javadoc'
}
task sourcesJar(type: Jar) {
from sourceSets.main.allSource
classifier = 'sources'
archiveClassifier = 'sources'
}
publishing {
@@ -68,18 +68,14 @@ publishing {
}
}
if (project.hasProperty('GITEA_TOKEN') || System.getenv('GITEA_TOKEN') != null) {
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
maven {
name = "Gitea"
url = uri("https://git.inmo.dev/api/packages/InsanusMokrassar/maven")
name = "InmoNexus"
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = project.hasProperty('GITEA_TOKEN') ? project.property('GITEA_TOKEN') : System.getenv('GITEA_TOKEN')
}
authentication {
header(HttpHeaderAuthentication)
credentials {
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
}
}
@@ -115,4 +111,27 @@ if (project.hasProperty("signing.gnupg.keyName")) {
dependsOn(it)
}
}
// Workaround to make android sign operations depend on signing tasks
project.getTasks().withType(AbstractPublishToMaven.class).configureEach {
def signingTasks = project.getTasks().withType(Sign.class)
mustRunAfter(signingTasks)
}
// Workaround to make test tasks use sign
project.getTasks().withType(Sign.class).configureEach { signTask ->
def withoutSign = (signTask.name.startsWith("sign") ? signTask.name.minus("sign") : signTask.name)
def pubName = withoutSign.endsWith("Publication") ? withoutSign.substring(0, withoutSign.length() - "Publication".length()) : withoutSign
// These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets
// Task ':linkDebugTest<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def debugTestTask = tasks.findByName("linkDebugTest$pubName")
if (debugTestTask != null) {
signTask.mustRunAfter(debugTestTask)
}
// Task ':compileTestKotlin<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def testTask = tasks.findByName("compileTestKotlin$pubName")
if (testTask != null) {
signTask.mustRunAfter(testTask)
}
}
}

View File

@@ -1 +1 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"Gitea","url":"https://git.inmo.dev/api/packages/InsanusMokrassar/maven","credsType":{"type":"dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository.CredentialsType.HttpHeaderCredentials","headerName":"Authorization","headerValueProperty":"GITEA_TOKEN"}},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"}
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"}

47
klock/LICENSE Normal file
View File

@@ -0,0 +1,47 @@
MIT License
Copyright (c) 2023 Ovsiannikov Aleksei
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------
MIT License
Copyright (c) 2017-2019 Carlos Ballesteros Velasco and contributors
* https://github.com/korlibs/korge/graphs/contributors
* https://github.com/korlibs-archive/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7
klock/build.gradle Normal file
View File

@@ -0,0 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"

View File

@@ -0,0 +1,37 @@
@file:Suppress("PackageDirectoryMismatch")
package korlibs.time.internal
import korlibs.time.*
import java.util.*
internal actual object KlockInternal {
actual val currentTime: Double get() = CurrentKlockInternalJvm.currentTime
actual val now: TimeSpan get() = CurrentKlockInternalJvm.hrNow
actual fun localTimezoneOffsetMinutes(time: DateTime): TimeSpan = CurrentKlockInternalJvm.localTimezoneOffsetMinutes(time)
actual fun sleep(time: TimeSpan) {
val nanos = time.nanoseconds.toLong()
Thread.sleep(nanos / 1_000_000, (nanos % 1_000_000).toInt())
}
}
inline fun <T> TemporalKlockInternalJvm(impl: KlockInternalJvm, callback: () -> T): T {
val old = CurrentKlockInternalJvm
CurrentKlockInternalJvm = impl
try {
return callback()
} finally {
CurrentKlockInternalJvm = old
}
}
var CurrentKlockInternalJvm = object : KlockInternalJvm {
}
interface KlockInternalJvm {
val currentTime: Double get() = (System.currentTimeMillis()).toDouble()
val microClock: Double get() = hrNow.microseconds
val hrNow: TimeSpan get() = TimeSpan.fromNanoseconds(System.nanoTime().toDouble())
fun localTimezoneOffsetMinutes(time: DateTime): TimeSpan = TimeZone.getDefault().getOffset(time.unixMillisLong).milliseconds
}
actual typealias Serializable = java.io.Serializable

View File

@@ -0,0 +1,8 @@
@file:Suppress("PackageDirectoryMismatch")
package korlibs.time.internal
import korlibs.time.*
import java.util.Date
fun Date.toDateTime() = DateTime(this.time)
fun DateTime.toDate() = Date(this.unixMillisLong)

View File

@@ -0,0 +1,72 @@
package korlibs.time
import korlibs.time.internal.Serializable
import kotlin.jvm.JvmInline
import kotlin.math.abs
/**
* Represents a triple of [year], [month] and [day].
*
* It is packed in a value class wrapping an Int to prevent allocations.
*/
@JvmInline
value class Date(val encoded: Int) : Comparable<Date>, Serializable {
companion object {
@Suppress("MayBeConstant", "unused")
private const val serialVersionUID = 1L
/** Constructs a new [Date] from the [year], [month] and [day] components. */
operator fun invoke(year: Int, month: Int, day: Int) = Date((year shl 16) or (month shl 8) or (day shl 0))
/** Constructs a new [Date] from the [year], [month] and [day] components. */
operator fun invoke(year: Int, month: Month, day: Int) = Date(year, month.index1, day)
/** Constructs a new [Date] from the [year], [month] and [day] components. */
operator fun invoke(year: Year, month: Month, day: Int) = Date(year.year, month.index1, day)
/** Constructs a new [Date] from the [yearMonth] and [day] components. */
operator fun invoke(yearMonth: YearMonth, day: Int) = Date(yearMonth.yearInt, yearMonth.month1, day)
}
/** The [year] part as [Int]. */
val year: Int get() = encoded shr 16
/** The [month] part as [Int] where [Month.January] is 1. */
val month1: Int get() = (encoded ushr 8) and 0xFF
/** The [month] part. */
val month: Month get() = Month[month1]
/** The [day] part. */
val day: Int get() = (encoded ushr 0) and 0xFF
/** The [year] part as [Year]. */
val yearYear: Year get() = Year(year)
/** A [DateTime] instance representing this date and time from the beginning of the [day]. */
val dateTimeDayStart get() = DateTime(year, month, day)
/** The [dayOfYear] part. */
val dayOfYear get() = dateTimeDayStart.dayOfYear
/** The [dayOfWeek] part. */
val dayOfWeek get() = dateTimeDayStart.dayOfWeek
/** The [dayOfWeek] part as [Int]. */
val dayOfWeekInt get() = dateTimeDayStart.dayOfWeekInt
/** Converts this date to String using [format] for representing it. */
fun format(format: String) = dateTimeDayStart.format(format)
/** Converts this date to String using [format] for representing it. */
fun format(format: DateFormat) = dateTimeDayStart.format(format)
/** Converts this date to String formatting it like "2020-01-01", "2020-12-31" or "-2020-12-31" if the [year] is negative */
override fun toString(): String = "${if (year < 0) "-" else ""}${abs(year).toString()}-${abs(month1).toString().padStart(2, '0')}-${abs(day).toString().padStart(2, '0')}"
override fun compareTo(other: Date): Int = this.encoded.compareTo(other.encoded)
}
operator fun Date.plus(time: TimeSpan) = (this.dateTimeDayStart + time).date
operator fun Date.plus(time: MonthSpan) = (this.dateTimeDayStart + time).date
operator fun Date.plus(time: DateTimeSpan) = (this.dateTimeDayStart + time).date
operator fun Date.plus(time: Time) = DateTime.createAdjusted(year, month1, day, time.hour, time.minute, time.second, time.millisecond)
operator fun Date.minus(time: TimeSpan) = (this.dateTimeDayStart - time).date
operator fun Date.minus(time: MonthSpan) = (this.dateTimeDayStart - time).date
operator fun Date.minus(time: DateTimeSpan) = (this.dateTimeDayStart - time).date
operator fun Date.minus(time: Time) = DateTime.createAdjusted(year, month1, day, -time.hour, -time.minute, -time.second, -time.millisecond)
fun Date.inThisWeek(dayOfWeek: DayOfWeekWithLocale): Date =
this + (dayOfWeek.index0 - this.dayOfWeek.withLocale(dayOfWeek.locale).index0).days
fun Date.inThisWeek(dayOfWeek: DayOfWeek, locale: KlockLocale = KlockLocale.default): Date = inThisWeek(dayOfWeek.withLocale(locale))

View File

@@ -0,0 +1,6 @@
package korlibs.time
/**
* An exception for Date operations.
*/
class DateException(msg: String) : RuntimeException(msg)

View File

@@ -0,0 +1,43 @@
package korlibs.time
/** Allows to [format] and [parse] instances of [Date], [DateTime] and [DateTimeTz] */
interface DateFormat {
fun format(dd: DateTimeTz): String
fun tryParse(str: String, doThrow: Boolean = false, doAdjust: Boolean = true): DateTimeTz?
companion object {
val DEFAULT_FORMAT = DateFormat("EEE, dd MMM yyyy HH:mm:ss z")
val FORMAT1 = DateFormat("yyyy-MM-dd'T'HH:mm:ssXXX")
val FORMAT2 = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
val FORMAT_DATE = DateFormat("yyyy-MM-dd")
val FORMATS = listOf(DEFAULT_FORMAT, FORMAT1, FORMAT2, FORMAT_DATE)
fun parse(date: String): DateTimeTz {
var lastError: Throwable? = null
for (format in FORMATS) {
try {
return format.parse(date)
} catch (e: Throwable) {
lastError = e
}
}
throw lastError!!
}
operator fun invoke(pattern: String) = PatternDateFormat(pattern)
}
}
fun DateFormat.parse(str: String, doAdjust: Boolean = true): DateTimeTz =
tryParse(str, doThrow = true, doAdjust = doAdjust) ?: throw DateException("Not a valid format: '$str' for '$this'")
fun DateFormat.parseDate(str: String): Date = parse(str).local.date
fun DateFormat.parseUtc(str: String): DateTime = parse(str).utc
fun DateFormat.parseLocal(str: String): DateTime = parse(str).local
fun DateFormat.format(date: Double): String = format(DateTime.fromUnixMillis(date))
fun DateFormat.format(date: Long): String = format(DateTime.fromUnixMillis(date))
fun DateFormat.format(dd: DateTime): String = format(dd.toOffsetUnadjusted(0.minutes))
fun DateFormat.format(dd: Date): String = format(dd.dateTimeDayStart)

View File

@@ -0,0 +1,458 @@
package korlibs.time
import korlibs.time.DateTime.Companion.EPOCH
import korlibs.time.internal.*
import kotlin.jvm.JvmInline
import kotlin.math.*
/**
* Represents a Date in UTC (GMT+00) with millisecond precision.
*
* It is internally represented as an inlined double, thus doesn't allocate in any target including JS.
* It can represent without loss dates between (-(2 ** 52) and (2 ** 52)):
* - Thu Aug 10 -140744 07:15:45 GMT-0014 (Central European Summer Time)
* - Wed May 23 144683 18:29:30 GMT+0200 (Central European Summer Time)
*/
@JvmInline
value class DateTime(
/** Number of milliseconds since UNIX [EPOCH] */
val unixMillis: Double
) : Comparable<DateTime>, Serializable {
companion object {
@Suppress("MayBeConstant", "unused")
private const val serialVersionUID = 1L
/** It is a [DateTime] instance representing 00:00:00 UTC, Thursday, 1 January 1970. */
val EPOCH = DateTime(0.0)
/**
* Constructs a new [DateTime] from date and time information.
*
* This might throw a [DateException] on invalid dates.
*/
operator fun invoke(
year: Year,
month: Month,
day: Int,
hour: Int = 0,
minute: Int = 0,
second: Int = 0,
milliseconds: Int = 0
): DateTime = DateTime(
DateTime.dateToMillis(year.year, month.index1, day) + DateTime.timeToMillis(
hour,
minute,
second
) + milliseconds
)
/**
* Constructs a new [DateTime] from date and time information.
*
* This might throw a [DateException] on invalid dates.
*/
operator fun invoke(
date: Date,
time: Time = Time(0.milliseconds)
): DateTime = DateTime(
date.year, date.month1, date.day,
time.hour, time.minute, time.second, time.millisecond
)
/**
* Constructs a new [DateTime] from date and time information.
*
* This might throw a [DateException] on invalid dates.
*/
operator fun invoke(
year: Int,
month: Month,
day: Int,
hour: Int = 0,
minute: Int = 0,
second: Int = 0,
milliseconds: Int = 0
): DateTime = DateTime(
DateTime.dateToMillis(year, month.index1, day) + DateTime.timeToMillis(
hour,
minute,
second
) + milliseconds
)
/**
* Constructs a new [DateTime] from date and time information.
*
* This might throw a [DateException] on invalid dates.
*/
operator fun invoke(
year: Int,
month: Int,
day: Int,
hour: Int = 0,
minute: Int = 0,
second: Int = 0,
milliseconds: Int = 0
): DateTime = DateTime(
DateTime.dateToMillis(year, month, day) + DateTime.timeToMillis(
hour,
minute,
second
) + milliseconds
)
/**
* Constructs a new [DateTime] from date and time information.
*
* On invalid dates, this function will try to adjust the specified invalid date to a valid one by clamping components.
*/
fun createClamped(
year: Int,
month: Int,
day: Int,
hour: Int = 0,
minute: Int = 0,
second: Int = 0,
milliseconds: Int = 0
): DateTime {
val clampedMonth = month.coerceIn(1, 12)
return createUnchecked(
year = year,
month = clampedMonth,
day = day.coerceIn(1, Month(month).days(year)),
hour = hour.coerceIn(0, 23),
minute = minute.coerceIn(0, 59),
second = second.coerceIn(0, 59),
milliseconds = milliseconds
)
}
/**
* Constructs a new [DateTime] from date and time information.
*
* On invalid dates, this function will try to adjust the specified invalid date to a valid one by adjusting other components.
*/
fun createAdjusted(
year: Int,
month: Int,
day: Int,
hour: Int = 0,
minute: Int = 0,
second: Int = 0,
milliseconds: Int = 0
): DateTime {
var dy = year
var dm = month
var dd = day
var th = hour
var tm = minute
var ts = second
tm += ts.cycleSteps(0, 59); ts = ts.cycle(0, 59) // Adjust seconds, adding minutes
th += tm.cycleSteps(0, 59); tm = tm.cycle(0, 59) // Adjust minutes, adding hours
dd += th.cycleSteps(0, 23); th = th.cycle(0, 23) // Adjust hours, adding days
while (true) {
val dup = Month(dm).days(dy)
dm += dd.cycleSteps(1, dup); dd = dd.cycle(1, dup) // Adjust days, adding months
dy += dm.cycleSteps(1, 12); dm = dm.cycle(1, 12) // Adjust months, adding years
// We have already found a day that is valid for the adjusted month!
if (dd.cycle(1, Month(dm).days(dy)) == dd) {
break
}
}
return createUnchecked(dy, dm, dd, th, tm, ts, milliseconds)
}
/**
* Constructs a new [DateTime] from date and time information.
*
* On invalid dates, this function will have an undefined behaviour.
*/
fun createUnchecked(
year: Int,
month: Int,
day: Int,
hour: Int = 0,
minute: Int = 0,
second: Int = 0,
milliseconds: Int = 0
): DateTime {
return DateTime(
DateTime.dateToMillisUnchecked(year, month, day) + DateTime.timeToMillisUnchecked(
hour,
minute,
second
) + milliseconds
)
}
/** Constructs a new [DateTime] from a [unix] timestamp in milliseconds. */
operator fun invoke(unix: Long) = fromUnixMillis(unix)
/** Constructs a new [DateTime] from a [unix] timestamp in milliseconds. */
operator fun invoke(unix: Double) = fromUnixMillis(unix)
/** Constructs a new [DateTime] from a [unix] timestamp in milliseconds. */
fun fromUnixMillis(unix: Double): DateTime = DateTime(unix)
/** Constructs a new [DateTime] from a [unix] timestamp in milliseconds. */
fun fromUnixMillis(unix: Long): DateTime = fromUnixMillis(unix.toDouble())
/** Constructs a new [DateTime] by parsing the [str] using standard date formats. */
fun fromString(str: String) = DateFormat.parse(str)
/** Constructs a new [DateTime] by parsing the [str] using standard date formats. */
fun parse(str: String) = DateFormat.parse(str)
/** Returns the current time as [DateTime]. Note that since [DateTime] is inline, this property doesn't allocate on JavaScript. */
fun now(): DateTime = DateTime(KlockInternal.currentTime)
/** Returns the current local time as [DateTimeTz]. */
fun nowLocal(): DateTimeTz = DateTimeTz.nowLocal()
/** Returns the total milliseconds since unix epoch. The same as [nowUnixMillisLong] but as double. To prevent allocation on targets without Long support. */
fun nowUnixMillis(): Double = KlockInternal.currentTime
/** Returns the total milliseconds since unix epoch. */
fun nowUnixMillisLong(): Long = KlockInternal.currentTime.toLong()
internal const val EPOCH_INTERNAL_MILLIS =
62135596800000.0 // Millis since 00-00-0000 00:00 UTC to UNIX EPOCH
internal enum class DatePart { Year, DayOfYear, Month, Day }
internal fun dateToMillisUnchecked(year: Int, month: Int, day: Int): Double =
(Year(year).daysSinceOne + Month(month).daysToStart(year) + day - 1) * MILLIS_PER_DAY.toDouble() - EPOCH_INTERNAL_MILLIS
private fun timeToMillisUnchecked(hour: Int, minute: Int, second: Int): Double =
hour.toDouble() * MILLIS_PER_HOUR + minute.toDouble() * MILLIS_PER_MINUTE + second.toDouble() * MILLIS_PER_SECOND
private fun dateToMillis(year: Int, month: Int, day: Int): Double {
//Year.checked(year)
Month.checked(month)
if (day !in 1..Month(month).days(year)) throw DateException("Day $day not valid for year=$year and month=$month")
return dateToMillisUnchecked(year, month, day)
}
private fun timeToMillis(hour: Int, minute: Int, second: Int): Double {
if (hour !in 0..23) throw DateException("Hour $hour not in 0..23")
if (minute !in 0..59) throw DateException("Minute $minute not in 0..59")
if (second !in 0..59) throw DateException("Second $second not in 0..59")
return timeToMillisUnchecked(hour, minute, second)
}
// millis are 00-00-0000 based.
internal fun getDatePart(millis: Double, part: DatePart): Int {
val totalDays = (millis / MILLIS_PER_DAY).toInt2()
// Year
val year = Year.fromDays(totalDays)
if (part == DatePart.Year) return year.year
// Day of Year
val isLeap = year.isLeap
val startYearDays = year.daysSinceOne
val dayOfYear = 1 + ((totalDays - startYearDays) umod year.days)
if (part == DatePart.DayOfYear) return dayOfYear
// Month
val month = Month.fromDayOfYear(dayOfYear, isLeap)
?: error("Invalid dayOfYear=$dayOfYear, isLeap=$isLeap")
if (part == DatePart.Month) return month.index1
// Day
val dayOfMonth = dayOfYear - month.daysToStart(isLeap)
if (part == DatePart.Day) return dayOfMonth
error("Invalid DATE_PART")
}
}
/** Number of milliseconds since the 00:00:00 UTC, Monday, 1 January 1 */
val yearOneMillis: Double get() = EPOCH_INTERNAL_MILLIS + unixMillis
/** The local offset for this date for the timezone of the device */
val localOffset: TimezoneOffset get() = TimezoneOffset.local(DateTime(unixMillisDouble))
/** Number of milliseconds since UNIX [EPOCH] as [Double] */
val unixMillisDouble: Double get() = unixMillis
/** Number of milliseconds since UNIX [EPOCH] as [Long] */
val unixMillisLong: Long get() = unixMillisDouble.toLong()
/** The [Year] part */
val year: Year get() = Year(yearInt)
/** The [Year] part as [Int] */
val yearInt: Int get() = getDatePart(yearOneMillis, DatePart.Year)
/** The [Month] part */
val month: Month get() = Month[month1]
/** The [Month] part as [Int] where January is represented as 0 */
val month0: Int get() = month1 - 1
/** The [Month] part as [Int] where January is represented as 1 */
val month1: Int get() = getDatePart(yearOneMillis, DatePart.Month)
/** Represents a couple of [Year] and [Month] that has leap information and thus allows to get the number of days of that month */
val yearMonth: YearMonth get() = YearMonth(year, month)
/** The [dayOfMonth] part */
val dayOfMonth: Int get() = getDatePart(yearOneMillis, DatePart.Day)
/** The [dayOfWeek] part */
val dayOfWeek: DayOfWeek get() = DayOfWeek[dayOfWeekInt]
/** The [dayOfWeek] part as [Int] */
val dayOfWeekInt: Int get() = (yearOneMillis / MILLIS_PER_DAY + 1).toIntMod(7)
/** The [dayOfYear] part */
val dayOfYear: Int get() = getDatePart(yearOneMillis, DatePart.DayOfYear)
/** The [hours] part */
val hours: Int get() = (yearOneMillis / MILLIS_PER_HOUR).toIntMod(24)
/** The [minutes] part */
val minutes: Int get() = (yearOneMillis / MILLIS_PER_MINUTE).toIntMod(60)
/** The [seconds] part */
val seconds: Int get() = (yearOneMillis / MILLIS_PER_SECOND).toIntMod(60)
/** The [milliseconds] part */
val milliseconds: Int get() = (yearOneMillis).toIntMod(1000)
/** Returns a new local date that will match these components. */
val localUnadjusted: DateTimeTz get() = DateTimeTz.local(this, localOffset)
/** Returns a new local date that will match these components but with a different [offset]. */
fun toOffsetUnadjusted(offset: TimeSpan) = toOffsetUnadjusted(offset.offset)
/** Returns a new local date that will match these components but with a different [offset]. */
fun toOffsetUnadjusted(offset: TimezoneOffset) = DateTimeTz.local(this, offset)
/** Returns this date with the local offset of this device. Components might change because of the offset. */
val local: DateTimeTz get() = DateTimeTz.utc(this, localOffset)
/** Returns this date with a local offset. Components might change because of the [offset]. */
fun toOffset(offset: TimeSpan) = toOffset(offset.offset)
/** Returns this date with a local offset. Components might change because of the [offset]. */
fun toOffset(offset: TimezoneOffset) = DateTimeTz.utc(this, offset)
/** Returns this date with a local offset. Components might change because of the [timeZone]. */
fun toTimezone(timeZone: Timezone) = toOffset(timeZone.offset)
/** Returns this date with a 0 offset. Components are equal. */
val utc: DateTimeTz get() = DateTimeTz.utc(this, TimezoneOffset(0.minutes))
/** Returns a [DateTime] of [this] day with the hour at 00:00:00 */
val dateDayStart get() = DateTime(year, month, dayOfMonth, 0, 0, 0, 0)
/** Returns a [DateTime] of [this] day with the hour at 23:59:59.999 */
val dateDayEnd get() = DateTime(year, month, dayOfMonth, 23, 59, 59, 999)
/** Returns the quarter 1, 2, 3 or 4 */
val quarter get() = (month0 / 3) + 1
// startOf
val startOfYear get() = DateTime(year, Month.January, 1)
val startOfMonth get() = DateTime(year, month, 1)
val startOfQuarter get() = DateTime(year, Month[(quarter - 1) * 3 + 1], 1)
fun startOfDayOfWeek(day: DayOfWeek): DateTime {
for (n in 0 until 7) {
val date = (this - n.days)
if (date.dayOfWeek == day) return date.startOfDay
}
error("Shouldn't happen")
}
val startOfWeek: DateTime get() = startOfDayOfWeek(DayOfWeek.Sunday)
val startOfIsoWeek: DateTime get() = startOfDayOfWeek(DayOfWeek.Monday)
val startOfDay get() = DateTime(year, month, dayOfMonth)
val startOfHour get() = DateTime(year, month, dayOfMonth, hours)
val startOfMinute get() = DateTime(year, month, dayOfMonth, hours, minutes)
val startOfSecond get() = DateTime(year, month, dayOfMonth, hours, minutes, seconds)
// endOf
val endOfYear get() = DateTime(year, Month.December, 31, 23, 59, 59, 999)
val endOfMonth get() = DateTime(year, month, month.days(year), 23, 59, 59, 999)
val endOfQuarter get() = DateTime(year, Month[(quarter - 1) * 3 + 3], month.days(year), 23, 59, 59, 999)
fun endOfDayOfWeek(day: DayOfWeek): DateTime {
for (n in 0 until 7) {
val date = (this + n.days)
if (date.dayOfWeek == day) return date.endOfDay
}
error("Shouldn't happen")
}
val endOfWeek: DateTime get() = endOfDayOfWeek(DayOfWeek.Monday)
val endOfIsoWeek: DateTime get() = endOfDayOfWeek(DayOfWeek.Sunday)
val endOfDay get() = DateTime(year, month, dayOfMonth, 23, 59, 59, 999)
val endOfHour get() = DateTime(year, month, dayOfMonth, hours, 59, 59, 999)
val endOfMinute get() = DateTime(year, month, dayOfMonth, hours, minutes, 59, 999)
val endOfSecond get() = DateTime(year, month, dayOfMonth, hours, minutes, seconds, 999)
val date get() = Date(yearInt, month1, dayOfMonth)
val time get() = Time(hours, minutes, seconds, milliseconds)
operator fun plus(delta: MonthSpan): DateTime = this.add(delta.totalMonths, 0.0)
operator fun plus(delta: DateTimeSpan): DateTime = this.add(delta.totalMonths, delta.totalMilliseconds)
operator fun plus(delta: TimeSpan): DateTime = add(0, delta.milliseconds)
operator fun minus(delta: MonthSpan): DateTime = this + -delta
operator fun minus(delta: DateTimeSpan): DateTime = this + -delta
operator fun minus(delta: TimeSpan): DateTime = this + (-delta)
operator fun minus(other: DateTime): TimeSpan = (this.unixMillisDouble - other.unixMillisDouble).milliseconds
override fun compareTo(other: DateTime): Int = this.unixMillis.compareTo(other.unixMillis)
/** Constructs a new [DateTime] after adding [deltaMonths] and [deltaMilliseconds] */
fun add(deltaMonths: Int, deltaMilliseconds: Double): DateTime = when {
deltaMonths == 0 && deltaMilliseconds == 0.0 -> this
deltaMonths == 0 -> DateTime(this.unixMillis + deltaMilliseconds)
else -> {
var year = this.year
var month = this.month.index1
var day = this.dayOfMonth
val i = month - 1 + deltaMonths
if (i >= 0) {
month = i % Month.Count + 1
year += i / Month.Count
} else {
month = Month.Count + (i + 1) % Month.Count
year += (i - (Month.Count - 1)) / Month.Count
}
//Year.checked(year)
val days = Month(month).days(year)
if (day > days) day = days
DateTime(dateToMillisUnchecked(year.year, month, day) + (yearOneMillis % MILLIS_PER_DAY) + deltaMilliseconds)
}
}
/** Constructs a new [DateTime] after adding [dateSpan] and [timeSpan] */
fun add(dateSpan: MonthSpan, timeSpan: TimeSpan): DateTime = add(dateSpan.totalMonths, timeSpan.milliseconds)
fun copyDayOfMonth(
year: Year = this.year,
month: Month = this.month,
dayOfMonth: Int = this.dayOfMonth,
hours: Int = this.hours,
minutes: Int = this.minutes,
seconds: Int = this.seconds,
milliseconds: Int = this.milliseconds
) = DateTime(year, month, dayOfMonth, hours, minutes, seconds, milliseconds)
/** Converts this date to String using [format] for representing it */
fun format(format: DateFormat): String = format.format(this)
/** Converts this date to String using [format] for representing it */
fun format(format: String): String = DateFormat(format).format(this)
/** Converts this date to String using [format] for representing it */
fun toString(format: String): String = DateFormat(format).format(this)
/** Converts this date to String using [format] for representing it */
fun toString(format: DateFormat): String = format.format(this)
/** Converts this date to String using the [DateFormat.DEFAULT_FORMAT] for representing it */
fun toStringDefault(): String = DateFormat.DEFAULT_FORMAT.format(this)
//override fun toString(): String = DateFormat.DEFAULT_FORMAT.format(this)
override fun toString(): String = "DateTime($unixMillisLong)"
}
fun max(a: DateTime, b: DateTime): DateTime =
DateTime.fromUnixMillis(max(a.unixMillis, b.unixMillis))
fun min(a: DateTime, b: DateTime): DateTime =
DateTime.fromUnixMillis(min(a.unixMillis, b.unixMillis))
fun DateTime.clamp(min: DateTime, max: DateTime): DateTime = when {
this < min -> min
this > max -> max
else -> this
}

View File

@@ -0,0 +1,154 @@
package korlibs.time
import korlibs.time.internal.Serializable
/**
* Represents a right-opened range between two dates.
*/
data class DateTimeRange(val from: DateTime, val to: DateTime) : Comparable<DateTime>, Serializable {
val valid get() = from <= to
companion object {
@Suppress("MayBeConstant", "unused")
private const val serialVersionUID = 1L
operator fun invoke(base: Date, from: Time, to: Time): DateTimeRange = DateTimeRange(base + from, base + to)
}
val size: TimeSpan get() = to - from
val min get() = from
val max get() = to
/**
* Duration [TimeSpan] without having into account actual months/years.
*/
val duration: TimeSpan get() = to - from
/**
* [DateTimeSpan] distance between two dates, month and year aware.
*/
val span: DateTimeSpan by lazy {
val reverse = to < from
val rfrom = if (!reverse) from else to
val rto = if (!reverse) to else from
var years = 0
var months = 0
var pivot = rfrom
// Compute years
val diffYears = (rto.year - pivot.year)
pivot += diffYears.years
years += diffYears
if (pivot > rto) {
pivot -= 1.years
years--
}
// Compute months (at most an iteration of 12)
while (true) {
val t = pivot + 1.months
if (t <= rto) {
months++
pivot = t
} else {
break
}
}
val out = DateTimeSpan(years.years + months.months, rto - pivot)
if (reverse) -out else out
}
/**
* Checks if a date is contained in this range.
*/
operator fun contains(date: DateTime): Boolean {
val unix = date.unixMillisDouble
val from = from.unixMillisDouble
val to = to.unixMillisDouble
return if (unix < from) false else unix < to
}
operator fun contains(other: DateTimeRange): Boolean {
return other.min >= this.min && other.max <= this.max
}
private inline fun <T> _intersectionWith(that: DateTimeRange, rightOpen: Boolean, handler: (from: DateTime, to: DateTime, matches: Boolean) -> T): T {
val from = max(this.from, that.from)
val to = min(this.to, that.to)
return handler(from, to, if (rightOpen) from < to else from <= to)
}
/**
* Returns new [DateTimeRange] or null - the result of intersection of this and [that] DateTimeRanges.
*/
fun intersectionWith(that: DateTimeRange, rightOpen: Boolean = true): DateTimeRange? {
return _intersectionWith(that, rightOpen) { from, to, matches ->
when {
matches -> DateTimeRange(from, to)
else -> null
}
}
}
/**
* Returns true if this and [that] DateTimeRanges have intersection otherwise false.
*/
fun intersectsWith(that: DateTimeRange, rightOpen: Boolean = true): Boolean = _intersectionWith(that, rightOpen) { _, _, matches -> matches }
/**
* Returns true if this and [that] DateTimeRanges have intersection or at least a common end otherwise false.
*/
fun intersectsOrInContactWith(that: DateTimeRange): Boolean = intersectsWith(that, rightOpen = false)
/**
* Returns new [DateTimeRange] or null - the result of merging this and [that] DateTimeRanges if they have intersection.
*/
fun mergeOnContactOrNull(that: DateTimeRange): DateTimeRange? {
if (!intersectsOrInContactWith(that)) return null
val min = min(this.min, that.min)
val max = max(this.max, that.max)
return DateTimeRange(min, max)
}
/**
* Returns a [List] of 0, 1 or 2 [DateTimeRange]s - the result of removing [that] DateTimeRange from this one
*/
fun without(that: DateTimeRange): List<DateTimeRange> = when {
// Full remove
(that.min <= this.min) && (that.max >= this.max) -> listOf()
// To the right or left, nothing to remove
(that.min >= this.max) || (that.max <= this.min) -> listOf(this)
// In the middle
else -> {
val p0 = this.min
val p1 = that.min
val p2 = that.max
val p3 = this.max
val c1 = if (p0 < p1) DateTimeRange(p0, p1) else null
val c2 = if (p2 < p3) DateTimeRange(p2, p3) else null
listOfNotNull(c1, c2)
}
}
fun toString(format: DateFormat): String = "${min.toString(format)}..${max.toString(format)}"
fun toStringLongs(): String = "${min.unixMillisLong}..${max.unixMillisLong}"
fun toStringDefault(): String = toString(DateFormat.FORMAT1)
//override fun toString(): String = toString(DateFormat.FORMAT1)
override fun toString(): String = "$min..$max"
override fun compareTo(other: DateTime): Int {
if (this.max <= other) return -1
if (this.min > other) return +1
return 0
}
}
fun List<DateTimeRange>.toStringLongs() = this.map { it.toStringLongs() }.toString()
/**
* Generates a right-opened range between two [DateTime]s
*/
infix fun DateTime.until(other: DateTime) = DateTimeRange(this, other)

View File

@@ -0,0 +1,270 @@
package korlibs.time
import korlibs.time.internal.BSearchResult
import korlibs.time.internal.Serializable
import korlibs.time.internal.fastForEach
import korlibs.time.internal.genericBinarySearch
// Properties:
// - ranges are sorted
// - ranges do not overlap/intersect between each other (they are merged and normalized)
// These properties allows to do some tricks and optimizations like binary search and a lot of O(n) operations.
data class DateTimeRangeSet private constructor(val dummy: Boolean, val ranges: List<DateTimeRange>) : Serializable {
/** [DateTimeRange] from the beginning of the first element to the end of the last one. */
val bounds = DateTimeRange(
ranges.firstOrNull()?.from ?: DateTime.EPOCH,
ranges.lastOrNull()?.to ?: DateTime.EPOCH
)
/** Total time of all [ranges]. */
val size: TimeSpan by lazy {
var out = 0.seconds
ranges.fastForEach { out += it.size }
out
}
constructor(ranges: List<DateTimeRange>) : this(false, Fast.combine(ranges))
constructor(range: DateTimeRange) : this(listOf(range))
constructor(vararg ranges: DateTimeRange) : this(ranges.toList())
operator fun plus(range: DateTimeRange): DateTimeRangeSet = this + DateTimeRangeSet(range)
operator fun plus(right: DateTimeRangeSet): DateTimeRangeSet = DateTimeRangeSet(this.ranges + right.ranges)
operator fun minus(range: DateTimeRange): DateTimeRangeSet = this - DateTimeRangeSet(range)
operator fun minus(right: DateTimeRangeSet): DateTimeRangeSet = Fast.minus(this, right)
operator fun contains(time: DateTime): Boolean = Fast.contains(time, this)
operator fun contains(time: DateTimeRange): Boolean = Fast.contains(time, this)
fun intersection(range: DateTimeRange): DateTimeRangeSet = this.intersection(DateTimeRangeSet(range))
fun intersection(vararg range: DateTimeRange): DateTimeRangeSet = this.intersection(DateTimeRangeSet(*range))
fun intersection(right: DateTimeRangeSet): DateTimeRangeSet = Fast.intersection(this, right)
companion object {
@Suppress("MayBeConstant", "unused")
private const val serialVersionUID = 1L
fun toStringLongs(ranges: List<DateTimeRange>): String = "${ranges.map { it.toStringLongs() }}"
}
object Fast {
internal fun combine(ranges: List<DateTimeRange>): List<DateTimeRange> {
if (ranges.isEmpty()) return ranges
val sorted = ranges.sortedBy { it.from.unixMillis }
val out = arrayListOf<DateTimeRange>()
var pivot = sorted.first()
for (n in 1 until sorted.size) {
val current = sorted[n]
val result = pivot.mergeOnContactOrNull(current)
pivot = if (result != null) {
result
} else {
out.add(pivot)
current
}
}
return out + listOf(pivot)
}
internal fun minus(left: DateTimeRangeSet, right: DateTimeRangeSet): DateTimeRangeSet {
if (left.ranges.isEmpty() || right.ranges.isEmpty()) return left
val ll = left.ranges
val rr = right.ranges.filter { it.intersectsWith(left.bounds) }
var lpos = 0
var rpos = 0
var l = ll.getOrNull(lpos++)
var r = rr.getOrNull(rpos++)
val out = arrayListOf<DateTimeRange>()
//debug { "-----------------" }
//debug { "Minus:" }
//debug { " - ll=${toStringLongs(ll)}" }
//debug { " - rr=${toStringLongs(rr)}" }
while (l != null && r != null) {
val result = l.without(r)
//debug { "Minus ${l!!.toStringLongs()} with ${r!!.toStringLongs()} -- ${toStringLongs(result)}" }
when (result.size) {
0 -> {
//debug { " - Full remove" }
l = ll.getOrNull(lpos++)
}
1 -> {
//debug { " - Result 1" }
when {
r.from >= l.to -> {
//debug { " - Move left. Emit ${result[0].toStringLongs()}" }
out.add(result[0])
l = ll.getOrNull(lpos++)
}
l == result[0] -> {
//debug { " - Move right. Change l from ${l!!.toStringLongs()} to ${result[0].toStringLongs()}" }
r = rr.getOrNull(rpos++)
}
else -> {
//debug { " - Use this l=${result[0].toStringLongs()} from ${l!!.toStringLongs()}" }
l = result[0]
}
}
}
else -> {
//debug { " - One chunk removed: ${result.map { it.toStringLongs() }}" }
//debug { " - Emit: ${result[0].toStringLongs()}" }
//debug { " - Keep: ${result[1].toStringLongs()}" }
out.add(result[0])
l = result[1]
}
}
}
if (l != null) {
out.add(l)
}
while (lpos < ll.size) out.add(ll[lpos++])
//debug { toStringLongs(out) }
return DateTimeRangeSet(out)
}
fun intersection(left: DateTimeRangeSet, right: DateTimeRangeSet): DateTimeRangeSet {
if (left.ranges.isEmpty() || right.ranges.isEmpty()) return DateTimeRangeSet(listOf())
val ll = left.ranges.filter { it.intersectsWith(right.bounds) }
val rr = right.ranges.filter { it.intersectsWith(left.bounds) }
val out = arrayListOf<DateTimeRange>()
//debug { "-----------------" }
//debug { "Intersection:" }
//debug { " - ll=${toStringLongs(ll)}" }
//debug { " - rr=${toStringLongs(rr)}" }
var rpos = 0
for (l in ll) {
rpos = 0
// We should be able to do this because the time ranges doesn't intersect each other
//while (rpos > 0) {
// val r = rr.getOrNull(rpos) ?: break
// if ((r.from < l.from) && (r.to < l.from)) break // End since we are already
// rpos--
//}
while (rpos < rr.size) {
val r = rr.getOrNull(rpos) ?: break
if (r.min > l.max) break // End since the rest are going to be farther
val res = l.intersectionWith(r)
if (res != null) {
out.add(res)
}
rpos++
}
}
//debug { toStringLongs(out) }
return DateTimeRangeSet(out)
}
fun contains(time: DateTime, rangeSet: DateTimeRangeSet): Boolean {
if (time !in rangeSet.bounds) return false // Early guard clause
val ranges = rangeSet.ranges
val result = BSearchResult(genericBinarySearch(0, ranges.size) { index -> ranges[index].compareTo(time) })
return result.found
}
fun contains(time: DateTimeRange, rangeSet: DateTimeRangeSet): Boolean {
if (time !in rangeSet.bounds) return false // Early guard clause
val ranges = rangeSet.ranges
val result = BSearchResult(genericBinarySearch(0, ranges.size) { index ->
val range = ranges[index]
when {
time in range -> 0
time.min < range.min -> +1
else -> -1
}
})
return result.found
}
//private inline fun debug(gen: () -> String) { println(gen()) }
}
object Slow {
// @TODO: Optimize
internal fun minus(l: DateTimeRangeSet, r: DateTimeRangeSet): DateTimeRangeSet {
val rightList = r.ranges
var out = l.ranges.toMutableList()
restart@ while (true) {
for ((leftIndex, left) in out.withIndex()) {
for (right in rightList) {
val result = left.without(right)
if (result.size != 1 || result[0] != left) {
out = (out.slice(0 until leftIndex) + result + out.slice(leftIndex + 1 until out.size)).toMutableList()
continue@restart
}
}
}
break
}
return DateTimeRangeSet(out)
}
internal fun combine(ranges: List<DateTimeRange>): List<DateTimeRange> {
// @TODO: Improve performance and verify fast combiner
val ranges = ranges.toMutableList()
restart@ while (true) {
for (i in ranges.indices) {
for (j in ranges.indices) {
if (i == j) continue
val ri = ranges[i]
val rj = ranges[j]
val concat = ri.mergeOnContactOrNull(rj)
if (concat != null) {
//println("Combining $ri and $rj : $concat")
ranges.remove(rj)
ranges[i] = concat
continue@restart
}
}
}
break
}
return ranges
}
fun intersection(left: DateTimeRangeSet, right: DateTimeRangeSet): DateTimeRangeSet {
val leftList = left.ranges
val rightList = right.ranges
val out = arrayListOf<DateTimeRange>()
for (l in leftList) {
for (r in rightList) {
if (r.min > l.max) break
val result = l.intersectionWith(r)
if (result != null) {
out.add(result)
}
}
//val chunks = rightList.mapNotNull { r -> l.intersectionWith(r) }
//out.addAll(DateTimeRangeSet(chunks).ranges)
}
return DateTimeRangeSet(out)
}
fun contains(time: DateTime, rangeSet: DateTimeRangeSet): Boolean {
if (time !in rangeSet.bounds) return false // Early guard clause
// @TODO: Fast binary search, since the ranges doesn't intersect each other
rangeSet.ranges.fastForEach { range ->
if (time in range) return true
}
return false
}
fun contains(time: DateTimeRange, rangeSet: DateTimeRangeSet): Boolean {
if (time !in rangeSet.bounds) return false // Early guard clause
// @TODO: Fast binary search, since the ranges doesn't intersect each other
rangeSet.ranges.fastForEach { range ->
if (time in range) return true
}
return false
}
}
fun toStringLongs(): String = "${ranges.map { it.toStringLongs() }}"
override fun toString(): String = "$ranges"
}
fun Iterable<DateTimeRange>.toRangeSet() = DateTimeRangeSet(this.toList())

View File

@@ -0,0 +1,143 @@
package korlibs.time
import korlibs.time.internal.MILLIS_PER_DAY
import korlibs.time.internal.MILLIS_PER_HOUR
import korlibs.time.internal.MILLIS_PER_MINUTE
import korlibs.time.internal.MILLIS_PER_SECOND
import korlibs.time.internal.MILLIS_PER_WEEK
import korlibs.time.internal.Moduler
import korlibs.time.internal.Serializable
/**
* Immutable structure representing a set of a [monthSpan] and a [timeSpan].
* This structure loses information about which months are included, that makes it impossible to generate a real [TimeSpan] including months.
* You can use [DateTimeRange.duration] to get this information from two real [DateTime].
*/
data class DateTimeSpan(
/** The [MonthSpan] part */
val monthSpan: MonthSpan,
/** The [TimeSpan] part */
val timeSpan: TimeSpan
) : Comparable<DateTimeSpan>, Serializable {
companion object {
@Suppress("MayBeConstant", "unused")
private const val serialVersionUID = 1L
}
constructor(
years: Int = 0,
months: Int = 0,
weeks: Int = 0,
days: Int = 0,
hours: Int = 0,
minutes: Int = 0,
seconds: Int = 0,
milliseconds: Double = 0.0
) : this(
years.years + months.months,
weeks.weeks + days.days + hours.hours + minutes.minutes + seconds.seconds + milliseconds.milliseconds
)
operator fun unaryMinus() = DateTimeSpan(-monthSpan, -timeSpan)
operator fun unaryPlus() = DateTimeSpan(+monthSpan, +timeSpan)
operator fun plus(other: TimeSpan) = DateTimeSpan(monthSpan, timeSpan + other)
operator fun plus(other: MonthSpan) = DateTimeSpan(monthSpan + other, timeSpan)
operator fun plus(other: DateTimeSpan) = DateTimeSpan(monthSpan + other.monthSpan, timeSpan + other.timeSpan)
operator fun minus(other: TimeSpan) = this + -other
operator fun minus(other: MonthSpan) = this + -other
operator fun minus(other: DateTimeSpan) = this + -other
operator fun times(times: Double) = DateTimeSpan((monthSpan * times), (timeSpan * times))
operator fun times(times: Int) = this * times.toDouble()
operator fun times(times: Float) = this * times.toDouble()
operator fun div(times: Double) = times(1.0 / times)
operator fun div(times: Int) = this / times.toDouble()
operator fun div(times: Float) = this / times.toDouble()
/** From the date part, all months represented as a [totalYears] [Double] */
val totalYears: Double get() = monthSpan.totalYears
/** From the date part, all months including months and years */
val totalMonths: Int get() = monthSpan.totalMonths
/** From the time part, all the milliseconds including milliseconds, seconds, minutes, hours, days and weeks */
val totalMilliseconds: Double get() = timeSpan.milliseconds
/** The [years] part as an integer. */
val years: Int get() = monthSpan.years
/** The [months] part as an integer. */
val months: Int get() = monthSpan.months
/** The [weeks] part as an integer. */
val weeks: Int get() = computed.weeks
val daysNotIncludingWeeks: Int get() = days
/** The [daysIncludingWeeks] part as an integer including days and weeks. */
val daysIncludingWeeks: Int get() = computed.days + (computed.weeks * DayOfWeek.Count)
/** The [days] part as an integer. */
val days: Int get() = computed.days
/** The [hours] part as an integer. */
val hours: Int get() = computed.hours
/** The [minutes] part as an integer. */
val minutes: Int get() = computed.minutes
/** The [seconds] part as an integer. */
val seconds: Int get() = computed.seconds
/** The [milliseconds] part as a double. */
val milliseconds: Double get() = computed.milliseconds
/** The [secondsIncludingMilliseconds] part as a doble including seconds and milliseconds. */
val secondsIncludingMilliseconds: Double get() = computed.seconds + computed.milliseconds / MILLIS_PER_SECOND
/**
* Note that if milliseconds overflow months this could not be exactly true. But probably will work in most cases.
* This structure doesn't have information about which months are counted. So some months could have 28-31 days and thus can't be done.
* You can use [DateTimeRange.duration] to compare this with real precision using a range between two [DateTime].
*/
override fun compareTo(other: DateTimeSpan): Int {
if (this.totalMonths != other.totalMonths) return this.monthSpan.compareTo(other.monthSpan)
return this.timeSpan.compareTo(other.timeSpan)
}
/**
* Represents this [DateTimeSpan] as a string like `50Y 10M 3W 6DH 30m 15s`.
* Parts that are zero, won't be included. You can omit weeks and represent them
* as days by adjusting the [includeWeeks] parameter.
*/
fun toString(includeWeeks: Boolean): String = arrayListOf<String>().apply {
if (years != 0) add("${years}Y")
if (months != 0) add("${months}M")
if (includeWeeks && weeks != 0) add("${weeks}W")
if (days != 0 || (!includeWeeks && weeks != 0)) add("${if (includeWeeks) days else daysIncludingWeeks}D")
if (hours != 0) add("${hours}H")
if (minutes != 0) add("${minutes}m")
if (seconds != 0 || milliseconds != 0.0) add("${secondsIncludingMilliseconds}s")
if (monthSpan == 0.years && ((timeSpan == 0.seconds) || (timeSpan == (-0).seconds))) add("0s")
}.joinToString(" ")
override fun toString(): String = toString(includeWeeks = true)
private class ComputedTime(val weeks: Int, val days: Int, val hours: Int, val minutes: Int, val seconds: Int, val milliseconds: Double) {
companion object {
operator fun invoke(time: TimeSpan): ComputedTime = Moduler(time.milliseconds).run {
val weeks = int(MILLIS_PER_WEEK)
val days = int(MILLIS_PER_DAY)
val hours = int(MILLIS_PER_HOUR)
val minutes = int(MILLIS_PER_MINUTE)
val seconds = int(MILLIS_PER_SECOND)
val milliseconds = double(1)
return ComputedTime(weeks, days, hours, minutes, seconds, milliseconds)
}
}
}
private val computed by lazy { ComputedTime(timeSpan) }
}

View File

@@ -0,0 +1,12 @@
package korlibs.time
interface DateTimeSpanFormat {
fun format(dd: DateTimeSpan): String
fun tryParse(str: String, doThrow: Boolean): DateTimeSpan?
}
fun DateTimeSpanFormat.format(dd: TimeSpan): String = format(dd + 0.months)
fun DateTimeSpanFormat.format(dd: MonthSpan): String = format(dd + 0.seconds)
fun DateTimeSpanFormat.parse(str: String): DateTimeSpan =
tryParse(str, doThrow = true) ?: throw DateException("Not a valid format: '$str' for '$this'")

View File

@@ -0,0 +1,125 @@
package korlibs.time
import korlibs.time.internal.Serializable
/** [DateTime] with an associated [TimezoneOffset] */
class DateTimeTz private constructor(
/** The [adjusted] part */
private val adjusted: DateTime,
/** The [offset] part */
val offset: TimezoneOffset
) : Comparable<DateTimeTz>, Serializable {
companion object {
@Suppress("MayBeConstant", "unused")
private const val serialVersionUID = 1L
/** Creates a new [DateTimeTz] with the [utc] date and an [offset]. The [utc] components will be the same as this independently on the [offset]. */
fun local(local: DateTime, offset: TimezoneOffset) = DateTimeTz(local, offset)
/** Creates a new [DateTimeTz] with the [utc] date and an [offset]. The [utc] components might be different depending on the [offset]. */
fun utc(utc: DateTime, offset: TimezoneOffset) = DateTimeTz(utc + offset.time, offset)
/** Creates a new local [DateTimeTz] from a [unix] time */
fun fromUnixLocal(unix: Long): DateTimeTz = DateTime(unix).localUnadjusted
/** Creates a new local [DateTimeTz] from a [unix] time applied*/
fun fromUnix(unix: Long): DateTimeTz {
val unixDateTime = DateTime(unix)
return utc(unixDateTime, TimezoneOffset.local(unixDateTime))
}
/** Returns the current local [DateTimeTz] */
fun nowLocal(): DateTimeTz = DateTime.now().local
}
/** Returns a new UTC date that will match these components without being the same time */
val local: DateTime get() = adjusted
/** Returns a new UTC date that might not match these components, but it is the same time as UTC */
val utc: DateTime get() = (adjusted - offset.time)
/** The [Year] part */
val year: Year get() = adjusted.year
/** The [Year] part as [Int] */
val yearInt: Int get() = adjusted.yearInt
/** The [Month] part */
val month: Month get() = adjusted.month
/** The [Month] part as [Int] where January is represented as 0 */
val month0: Int get() = adjusted.month0
/** The [Month] part as [Int] where January is represented as 1 */
val month1: Int get() = adjusted.month1
/** Represents a couple of [Year] and [Month] that has leap information and thus allows to get the number of days of that month */
val yearMonth: YearMonth get() = adjusted.yearMonth
/** The [dayOfMonth] part */
val dayOfMonth: Int get() = adjusted.dayOfMonth
/** The [dayOfWeek] part */
val dayOfWeek: DayOfWeek get() = adjusted.dayOfWeek
/** The [dayOfWeek] part as [Int] */
val dayOfWeekInt: Int get() = adjusted.dayOfWeekInt
/** The [dayOfYear] part */
val dayOfYear: Int get() = adjusted.dayOfYear
/** The [hours] part */
val hours: Int get() = adjusted.hours
/** The [minutes] part */
val minutes: Int get() = adjusted.minutes
/** The [seconds] part */
val seconds: Int get() = adjusted.seconds
/** The [milliseconds] part */
val milliseconds: Int get() = adjusted.milliseconds
/** Constructs this local date with a new [offset] without changing its components */
fun toOffsetUnadjusted(offset: TimeSpan) = toOffsetUnadjusted(offset.offset)
/** Constructs this local date with a new [offset] without changing its components */
fun toOffsetUnadjusted(offset: TimezoneOffset) = DateTimeTz.local(this.local, offset)
/** Constructs this local date by adding an additional [offset] without changing its components */
fun addOffsetUnadjusted(offset: TimeSpan) = addOffsetUnadjusted(offset.offset)
/** Constructs this local date by adding an additional [offset] without changing its components */
fun addOffsetUnadjusted(offset: TimezoneOffset) = DateTimeTz.local(this.local, (this.offset.time + offset.time).offset)
/** Constructs the UTC part of this date with a new [offset] */
fun toOffset(offset: TimeSpan) = toOffset(offset.offset)
/** Constructs the UTC part of this date with a new [offset] */
fun toOffset(offset: TimezoneOffset) = DateTimeTz.utc(this.utc, offset)
/** Constructs the UTC part of this date by adding an additional [offset] */
fun addOffset(offset: TimeSpan) = addOffset(offset.offset)
/** Constructs the UTC part of this date by adding an additional [offset] */
fun addOffset(offset: TimezoneOffset) = DateTimeTz.utc(this.utc, (this.offset.time + offset.time).offset)
/** Constructs a new [DateTimeTz] after adding [dateSpan] and [timeSpan] */
fun add(dateSpan: MonthSpan, timeSpan: TimeSpan): DateTimeTz = DateTimeTz(adjusted.add(dateSpan, timeSpan), offset)
operator fun plus(delta: MonthSpan) = add(delta, 0.milliseconds)
operator fun plus(delta: DateTimeSpan) = add(delta.monthSpan, delta.timeSpan)
operator fun plus(delta: TimeSpan) = add(0.months, delta)
operator fun minus(delta: MonthSpan) = this + (-delta)
operator fun minus(delta: DateTimeSpan) = this + (-delta)
operator fun minus(delta: TimeSpan) = this + (-delta)
operator fun minus(other: DateTimeTz) = (this.utc.unixMillisDouble - other.utc.unixMillisDouble).milliseconds
override fun hashCode(): Int = this.local.hashCode() + offset.totalMinutesInt
override fun equals(other: Any?): Boolean = other is DateTimeTz && this.utc.unixMillisDouble == other.utc.unixMillisDouble
override fun compareTo(other: DateTimeTz): Int = this.utc.unixMillis.compareTo(other.utc.unixMillis)
/** Converts this date to String using [format] for representing it */
fun format(format: DateFormat): String = format.format(this)
/** Converts this date to String using [format] for representing it */
fun format(format: String): String = DateFormat(format).format(this)
/** Converts this date to String using [format] for representing it */
fun toString(format: DateFormat): String = format.format(this)
/** Converts this date to String using [format] for representing it */
fun toString(format: String): String = DateFormat(format).format(this)
/** Converts this date to String using the [DateFormat.DEFAULT_FORMAT] for representing it */
fun toStringDefault(): String = DateFormat.DEFAULT_FORMAT.format(this)
override fun toString(): String = "DateTimeTz($adjusted, $offset)"
}

View File

@@ -0,0 +1,95 @@
package korlibs.time
import korlibs.time.DayOfWeek.Friday
import korlibs.time.DayOfWeek.Monday
import korlibs.time.DayOfWeek.Saturday
import korlibs.time.DayOfWeek.Sunday
import korlibs.time.DayOfWeek.Thursday
import korlibs.time.DayOfWeek.Tuesday
import korlibs.time.DayOfWeek.Wednesday
import korlibs.time.internal.*
/** Represents the day of the week. [Sunday], [Monday], [Tuesday], [Wednesday], [Thursday], [Friday], [Saturday]. */
enum class DayOfWeek(
/** 0: [Sunday], 1: [Monday], 2: [Tuesday], 3: [Wednesday], 4: [Thursday], 5: [Friday], 6: [Saturday] */
val index0: Int
) : Serializable {
Sunday(0),
Monday(1),
Tuesday(2),
Wednesday(3),
Thursday(4),
Friday(5),
Saturday(6);
/**
* 1: [Sunday], 2: [Monday], 3: [Tuesday], 4: [Wednesday], 5: [Thursday], 6: [Friday], 7: [Saturday]
*/
val index1 get() = index0 + 1
val index0Sunday get() = index0
val index1Sunday get() = index1
/** 0: [Monday], 1: [Tuesday], 2: [Wednesday], 3: [Thursday], 4: [Friday], 5: [Saturday], 6: [Sunday] */
val index0Monday get() = (index0 - 1) umod 7
/** 1: [Monday], 2: [Tuesday], 3: [Wednesday], 4: [Thursday], 5: [Friday], 6: [Saturday], 7: [Sunday] */
val index1Monday get() = index0Monday + 1
fun index0Locale(locale: KlockLocale): Int = (index0 - locale.firstDayOfWeek.index0) umod 7
fun index1Locale(locale: KlockLocale): Int = index0Locale(locale) + 1
/** Returns if this day of the week is weekend for a specific [locale] */
fun isWeekend(locale: KlockLocale = KlockLocale.default) = locale.isWeekend(this)
val localName get() = localName(KlockLocale.default)
fun localName(locale: KlockLocale) = locale.daysOfWeek[index0]
val localShortName get() = localShortName(KlockLocale.default)
fun localShortName(locale: KlockLocale) = locale.daysOfWeekShort[index0]
val prev get() = DayOfWeek[index0 - 1]
val next get() = DayOfWeek[index0 + 1]
fun prev(offset: Int = 1) = DayOfWeek[index0 - offset]
fun next(offset: Int = 1) = DayOfWeek[index0 + offset]
companion object {
@Suppress("MayBeConstant", "unused")
private const val serialVersionUID = 1L
/**
* Number of days in a wekk.
*/
const val Count = 7
private val BY_INDEX0 = values()
/**
* 0: [Sunday], 1: [Monday], 2: [Tuesday], 3: [Wednesday], 4: [Thursday], 5: [Friday], 6: [Saturday]
*/
operator fun get(index0: Int) = BY_INDEX0[index0 umod 7]
fun get0(index0: Int, locale: KlockLocale = KlockLocale.default): DayOfWeek = DayOfWeek[index0 + locale.firstDayOfWeek.index0]
fun get1(index1: Int, locale: KlockLocale = KlockLocale.default): DayOfWeek = get0((index1 - 1) umod 7, locale)
/**
* Returns the first day of the week for a specific [locale].
*/
fun firstDayOfWeek(locale: KlockLocale = KlockLocale.default) = locale.firstDayOfWeek
fun comparator(locale: KlockLocale = KlockLocale.default) = locale.daysOfWeekComparator
}
}
fun DayOfWeek.withLocale(locale: KlockLocale) = locale.localizedDayOfWeek(this)
data class DayOfWeekWithLocale(val dayOfWeek: DayOfWeek, val locale: KlockLocale) : Comparable<DayOfWeekWithLocale> {
val index0: Int get() = dayOfWeek.index0Locale(locale)
val index1: Int get() = dayOfWeek.index1Locale(locale)
override fun compareTo(other: DayOfWeekWithLocale): Int {
if (other.locale != this.locale) error("Can't compare two day of weeks with different locales")
return locale.daysOfWeekComparator.compare(dayOfWeek, other.dayOfWeek)
}
}

View File

@@ -0,0 +1,42 @@
package korlibs.time
import korlibs.time.internal.*
import kotlin.jvm.*
val TimeSpan.hz: Frequency get() = timesPerSecond
val Int.hz: Frequency get() = timesPerSecond
val Double.hz: Frequency get() = timesPerSecond
fun TimeSpan.toFrequency(): Frequency = timesPerSecond
val TimeSpan.timesPerSecond get() = Frequency(1.0 / this.seconds)
val Int.timesPerSecond get() = Frequency(this.toDouble())
val Double.timesPerSecond get() = Frequency(this)
@JvmInline
value class Frequency(val hertz: Double) : Comparable<Frequency>, Serializable {
companion object {
fun from(timeSpan: TimeSpan) = timeSpan.toFrequency()
}
override fun compareTo(other: Frequency): Int = this.hertz.compareTo(other.hertz)
operator fun unaryMinus() = Frequency(-this.hertz)
operator fun unaryPlus() = this
operator fun plus(other: Frequency): Frequency = Frequency(this.hertz + other.hertz)
operator fun minus(other: Frequency): Frequency = Frequency(this.hertz - other.hertz)
operator fun times(scale: Int): Frequency = Frequency(this.hertz * scale)
operator fun times(scale: Float): Frequency = Frequency(this.hertz * scale)
operator fun times(scale: Double): Frequency = Frequency(this.hertz * scale)
operator fun div(scale: Int): Frequency = Frequency(this.hertz / scale)
operator fun div(scale: Float): Frequency = Frequency(this.hertz / scale)
operator fun div(scale: Double): Frequency = Frequency(this.hertz / scale)
operator fun rem(other: Frequency): Frequency = Frequency(this.hertz % other.hertz)
infix fun umod(other: Frequency): Frequency = Frequency(this.hertz umod other.hertz)
val timeSpan get() = (1.0 / this.hertz).seconds
}

View File

@@ -0,0 +1,446 @@
package korlibs.time
import korlibs.time.internal.MicroStrReader
import korlibs.time.internal.fastForEach
import korlibs.time.internal.padded
import korlibs.time.internal.readTimeZoneOffset
import kotlin.math.absoluteValue
// https://en.wikipedia.org/wiki/ISO_8601
object ISO8601 {
data class BaseIsoTimeFormat(val format: String) : TimeFormat {
companion object {
private val ref = DateTime(1900, 1, 1)
}
private val dateTimeFormat = BaseIsoDateTimeFormat(format)
override fun format(dd: TimeSpan): String = dateTimeFormat.format(ref + dd)
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? =
dateTimeFormat.tryParse(str, doThrow, doAdjust)?.let { it.utc - ref }
}
data class BaseIsoDateTimeFormat(val format: String, val twoDigitBaseYear: Int = 1900) : DateFormat {
override fun format(dd: DateTimeTz): String = buildString {
val d = dd.local
val s = d.copyDayOfMonth(hours = 0, minutes = 0, seconds = 0, milliseconds = 0)
val time = d - s
val fmtReader = MicroStrReader(format)
while (fmtReader.hasMore) {
when {
fmtReader.tryRead("Z") -> {
//if (dd.offset != TimezoneOffset.UTC) {
if (dd.offset != TimezoneOffset.UTC) {
dd.offset.deltaHoursAbs
append(if (dd.offset.positive) "+" else "-")
append(dd.offset.deltaHoursAbs.padded(2))
append(":")
append(dd.offset.deltaMinutesAbs.padded(2))
} else {
append("Z")
}
}
fmtReader.tryRead("YYYYYY") -> append(d.yearInt.absoluteValue.padded(6))
fmtReader.tryRead("YYYY") -> append(d.yearInt.absoluteValue.padded(4))
fmtReader.tryRead("YY") -> append((d.yearInt.absoluteValue % 100).padded(2))
fmtReader.tryRead("MM") -> append(d.month1.padded(2))
fmtReader.tryRead("DD") -> append(d.dayOfMonth.padded(2))
fmtReader.tryRead("DDD") -> append(d.dayOfWeekInt.padded(3))
fmtReader.tryRead("ww") -> append(d.weekOfYear1.padded(2))
fmtReader.tryRead("D") -> append(d.dayOfWeek.index1Monday)
fmtReader.tryRead("hh") -> {
val nextComma = fmtReader.tryRead(',')
val result = if (nextComma || fmtReader.tryRead('.')) {
var decCount = 0
while (fmtReader.tryRead('h')) decCount++
time.hours.padded(2, decCount)
} else {
d.hours.padded(2)
}
append(if (nextComma) result.replace('.', ',') else result)
}
fmtReader.tryRead("mm") -> {
val nextComma = fmtReader.tryRead(',')
val result = if (nextComma || fmtReader.tryRead('.')) {
var decCount = 0
while (fmtReader.tryRead('m')) decCount++
(time.minutes % 60.0).padded(2, decCount)
} else {
d.minutes.padded(2)
}
append(if (nextComma) result.replace('.', ',') else result)
}
fmtReader.tryRead("ss") -> {
val nextComma = fmtReader.tryRead(',')
val result = if (nextComma || fmtReader.tryRead('.')) {
var decCount = 0
while (fmtReader.tryRead('s')) decCount++
(time.seconds % 60.0).padded(2, decCount)
} else {
d.seconds.padded(2)
}
append(if (nextComma) result.replace('.', ',') else result)
}
fmtReader.tryRead("±") -> append(if (d.yearInt < 0) "-" else "+")
else -> append(fmtReader.readChar())
}
}
}
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? {
return _tryParse(str, doAdjust).also {
if (doThrow && it == null) throw DateException("Can't parse $str with $format")
}
}
private fun reportParse(reason: String): DateTimeTz? {
//println("reason: $reason")
return null
}
private fun _tryParse(str: String, doAdjust: Boolean): DateTimeTz? {
var sign = +1
var tzOffset: TimeSpan? = null
var year = twoDigitBaseYear
var month = 1
var dayOfMonth = 1
var dayOfWeek = -1
var dayOfYear = -1
var weekOfYear = -1
var hours = 0.0
var minutes = 0.0
var seconds = 0.0
val reader = MicroStrReader(str)
val fmtReader = MicroStrReader(format)
while (fmtReader.hasMore) {
when {
fmtReader.tryRead("Z") -> tzOffset = reader.readTimeZoneOffset()
fmtReader.tryRead("YYYYYY") -> year = reader.tryReadInt(6) ?: return reportParse("YYYYYY")
fmtReader.tryRead("YYYY") -> year = reader.tryReadInt(4) ?: return reportParse("YYYY")
//fmtReader.tryRead("YY") -> year = twoDigitBaseYear + (reader.tryReadInt(2) ?: return null) // @TODO: Kotlin compiler BUG?
fmtReader.tryRead("YY") -> {
val base = reader.tryReadInt(2) ?: return reportParse("YY")
year = twoDigitBaseYear + base
}
fmtReader.tryRead("MM") -> month = reader.tryReadInt(2) ?: return reportParse("MM")
fmtReader.tryRead("DD") -> dayOfMonth = reader.tryReadInt(2) ?: return reportParse("DD")
fmtReader.tryRead("DDD") -> dayOfYear = reader.tryReadInt(3) ?: return reportParse("DDD")
fmtReader.tryRead("ww") -> weekOfYear = reader.tryReadInt(2) ?: return reportParse("ww")
fmtReader.tryRead("D") -> dayOfWeek = reader.tryReadInt(1) ?: return reportParse("D")
fmtReader.tryRead("hh") -> {
val nextComma = fmtReader.tryRead(',')
hours = if (nextComma || fmtReader.tryRead('.')) {
var count = 3
while (fmtReader.tryRead('h')) count++
reader.tryReadDouble(count) ?: return reportParse("incorrect hours")
} else {
reader.tryReadDouble(2) ?: return reportParse("incorrect hours")
}
}
fmtReader.tryRead("mm") -> {
val nextComma = fmtReader.tryRead(',')
minutes = if (nextComma || fmtReader.tryRead('.')) {
var count = 3
while (fmtReader.tryRead('m')) count++
reader.tryReadDouble(count) ?: return reportParse("incorrect minutes")
} else {
reader.tryReadDouble(2) ?: return reportParse("incorrect seconds")
}
}
fmtReader.tryRead("ss") -> {
val nextComma = fmtReader.tryRead(',')
seconds = if (nextComma || fmtReader.tryRead('.')) {
var count = 3
while (fmtReader.tryRead('s')) count++
reader.tryReadDouble(count) ?: return reportParse("incorrect seconds")
} else {
reader.tryReadDouble(2) ?: return reportParse("incorrect seconds")
}
}
fmtReader.tryRead("±") -> {
sign = when (reader.readChar()) {
'+' -> +1
'-' -> -1
else -> return reportParse("±")
}
}
else -> if (fmtReader.readChar() != reader.readChar()) return reportParse("separator")
}
}
if (reader.hasMore) return reportParse("uncomplete")
val dateTime = when {
dayOfYear >= 0 -> DateTime(year, 1, 1) + (dayOfYear - 1).days
weekOfYear >= 0 -> {
val reference = Year(year).first(DayOfWeek.Thursday) - 3.days
val days = ((weekOfYear - 1) * 7 + (dayOfWeek - 1))
reference + days.days
}
else -> DateTime(year, month, dayOfMonth)
}
val baseDateTime = dateTime + hours.hours + minutes.minutes + seconds.seconds
return if (tzOffset != null) DateTimeTz.local(baseDateTime, TimezoneOffset(tzOffset)) else baseDateTime.local
}
fun withTwoDigitBaseYear(twoDigitBaseYear: Int = 1900) = BaseIsoDateTimeFormat(format, twoDigitBaseYear)
}
class IsoIntervalFormat(val format: String) : DateTimeSpanFormat {
override fun format(dd: DateTimeSpan): String = buildString {
val fmtReader = MicroStrReader(format)
var time = false
while (fmtReader.hasMore) {
when {
fmtReader.tryRead("T") -> append('T').also { time = true }
fmtReader.tryRead("nnY") -> append(dd.years).append('Y')
fmtReader.tryRead("nnM") -> append(if (time) dd.minutes else dd.months).append('M')
fmtReader.tryRead("nnD") -> append(dd.daysIncludingWeeks).append('D')
fmtReader.tryRead("nnH") -> append(dd.hours).append('H')
fmtReader.tryRead("nnS") -> append(dd.seconds).append('S')
else -> append(fmtReader.readChar())
}
}
}
override fun tryParse(str: String, doThrow: Boolean): DateTimeSpan? {
var time = false
var years = 0.0
var months = 0.0
var days = 0.0
var hours = 0.0
var minutes = 0.0
var seconds = 0.0
val reader = MicroStrReader(str)
val fmtReader = MicroStrReader(format)
while (fmtReader.hasMore) {
when {
fmtReader.tryRead("nn,nnY") || fmtReader.tryRead("nnY") -> {
years = reader.tryReadDouble() ?: return null
if (!reader.tryRead("Y")) return null
}
fmtReader.tryRead("nn,nnM") || fmtReader.tryRead("nnM") -> {
if (time) {
minutes = reader.tryReadDouble() ?: return null
} else {
months = reader.tryReadDouble() ?: return null
}
if (!reader.tryRead("M")) return null
}
fmtReader.tryRead("nn,nnD") || fmtReader.tryRead("nnD") -> {
days = reader.tryReadDouble() ?: return null
if (!reader.tryRead("D")) return null
}
fmtReader.tryRead("nn,nnH") || fmtReader.tryRead("nnH") -> {
hours = reader.tryReadDouble() ?: return null
if (!reader.tryRead("H")) return null
}
fmtReader.tryRead("nn,nnS") || fmtReader.tryRead("nnS") -> {
seconds = reader.tryReadDouble() ?: return null
if (!reader.tryRead("S")) return null
}
else -> {
val char = fmtReader.readChar()
if (char != reader.readChar()) return null
if (char == 'T') time = true
}
}
}
return ((years * 12) + months).toInt().months + (days.days + hours.hours + minutes.minutes + seconds.seconds)
}
}
data class IsoTimeFormat(val basicFormat: String?, val extendedFormat: String?) : TimeFormat {
val basic = BaseIsoTimeFormat(basicFormat ?: extendedFormat ?: TODO())
val extended = BaseIsoTimeFormat(extendedFormat ?: basicFormat ?: TODO())
override fun format(dd: TimeSpan): String = extended.format(dd)
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? =
basic.tryParse(str, false, doAdjust) ?: extended.tryParse(str, false, doAdjust)
?: (if (doThrow) throw DateException("Invalid format $str") else null)
}
data class IsoDateTimeFormat(val basicFormat: String?, val extendedFormat: String?) : DateFormat {
val basic = BaseIsoDateTimeFormat(basicFormat ?: extendedFormat ?: TODO())
val extended = BaseIsoDateTimeFormat(extendedFormat ?: basicFormat ?: TODO())
override fun format(dd: DateTimeTz): String = extended.format(dd)
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? = null
?: basic.tryParse(str, false, doAdjust)
?: extended.tryParse(str, false, doAdjust)
?: (if (doThrow) throw DateException("Invalid format $str") else null)
}
// Date Calendar Variants
val DATE_CALENDAR_COMPLETE = IsoDateTimeFormat("YYYYMMDD", "YYYY-MM-DD")
val DATE_CALENDAR_REDUCED0 = IsoDateTimeFormat(null, "YYYY-MM")
val DATE_CALENDAR_REDUCED1 = IsoDateTimeFormat("YYYY", null)
val DATE_CALENDAR_REDUCED2 = IsoDateTimeFormat("YY", null)
val DATE_CALENDAR_EXPANDED0 = IsoDateTimeFormat("±YYYYYYMMDD", "±YYYYYY-MM-DD")
val DATE_CALENDAR_EXPANDED1 = IsoDateTimeFormat("±YYYYYYMM", "±YYYYYY-MM")
val DATE_CALENDAR_EXPANDED2 = IsoDateTimeFormat("±YYYYYY", null)
val DATE_CALENDAR_EXPANDED3 = IsoDateTimeFormat("±YYY", null)
// Date Ordinal Variants
val DATE_ORDINAL_COMPLETE = IsoDateTimeFormat("YYYYDDD", "YYYY-DDD")
val DATE_ORDINAL_EXPANDED = IsoDateTimeFormat("±YYYYYYDDD", "±YYYYYY-DDD")
// Date Week Variants
val DATE_WEEK_COMPLETE = IsoDateTimeFormat("YYYYWwwD", "YYYY-Www-D")
val DATE_WEEK_REDUCED = IsoDateTimeFormat("YYYYWww", "YYYY-Www")
val DATE_WEEK_EXPANDED0 = IsoDateTimeFormat("±YYYYYYWwwD", "±YYYYYY-Www-D")
val DATE_WEEK_EXPANDED1 = IsoDateTimeFormat("±YYYYYYWww", "±YYYYYY-Www")
val DATE_ALL = listOf(
DATE_CALENDAR_COMPLETE, DATE_CALENDAR_REDUCED0, DATE_CALENDAR_REDUCED1, DATE_CALENDAR_REDUCED2,
DATE_CALENDAR_EXPANDED0, DATE_CALENDAR_EXPANDED1, DATE_CALENDAR_EXPANDED2, DATE_CALENDAR_EXPANDED3,
DATE_ORDINAL_COMPLETE, DATE_ORDINAL_EXPANDED,
DATE_WEEK_COMPLETE, DATE_WEEK_REDUCED, DATE_WEEK_EXPANDED0, DATE_WEEK_EXPANDED1
)
// Time Variants
val TIME_LOCAL_COMPLETE = IsoTimeFormat("hhmmss", "hh:mm:ss")
val TIME_LOCAL_REDUCED0 = IsoTimeFormat("hhmm", "hh:mm")
val TIME_LOCAL_REDUCED1 = IsoTimeFormat("hh", null)
val TIME_LOCAL_FRACTION0 = IsoTimeFormat("hhmmss,ss", "hh:mm:ss,ss")
val TIME_LOCAL_FRACTION1 = IsoTimeFormat("hhmm,mm", "hh:mm,mm")
val TIME_LOCAL_FRACTION2 = IsoTimeFormat("hh,hh", null)
// Time UTC Variants
val TIME_UTC_COMPLETE = IsoTimeFormat("hhmmssZ", "hh:mm:ssZ")
val TIME_UTC_REDUCED0 = IsoTimeFormat("hhmmZ", "hh:mmZ")
val TIME_UTC_REDUCED1 = IsoTimeFormat("hhZ", null)
val TIME_UTC_FRACTION0 = IsoTimeFormat("hhmmss,ssZ", "hh:mm:ss,ssZ")
val TIME_UTC_FRACTION1 = IsoTimeFormat("hhmm,mmZ", "hh:mm,mmZ")
val TIME_UTC_FRACTION2 = IsoTimeFormat("hh,hhZ", null)
// Time Relative Variants
val TIME_RELATIVE0 = IsoTimeFormat("±hhmm", "±hh:mm")
val TIME_RELATIVE1 = IsoTimeFormat("±hh", null)
val TIME_ALL = listOf(
TIME_LOCAL_COMPLETE,
TIME_LOCAL_REDUCED0,
TIME_LOCAL_REDUCED1,
TIME_LOCAL_FRACTION0,
TIME_LOCAL_FRACTION1,
TIME_LOCAL_FRACTION2,
TIME_UTC_COMPLETE,
TIME_UTC_REDUCED0,
TIME_UTC_REDUCED1,
TIME_UTC_FRACTION0,
TIME_UTC_FRACTION1,
TIME_UTC_FRACTION2,
TIME_RELATIVE0,
TIME_RELATIVE1
)
// Date + Time Variants
val DATETIME_COMPLETE = IsoDateTimeFormat("YYYYMMDDThhmmss", "YYYY-MM-DDThh:mm:ss")
val DATETIME_UTC_COMPLETE = IsoDateTimeFormat("YYYYMMDDThhmmssZ", "YYYY-MM-DDThh:mm:ssZ")
val DATETIME_UTC_COMPLETE_FRACTION = IsoDateTimeFormat("YYYYMMDDThhmmss.sssZ", "YYYY-MM-DDThh:mm:ss.sssZ")
// Interval Variants
val INTERVAL_COMPLETE0 = IsoIntervalFormat("PnnYnnMnnDTnnHnnMnnS")
val INTERVAL_COMPLETE1 = IsoIntervalFormat("PnnYnnW")
val INTERVAL_REDUCED0 = IsoIntervalFormat("PnnYnnMnnDTnnHnnM")
val INTERVAL_REDUCED1 = IsoIntervalFormat("PnnYnnMnnDTnnH")
val INTERVAL_REDUCED2 = IsoIntervalFormat("PnnYnnMnnD")
val INTERVAL_REDUCED3 = IsoIntervalFormat("PnnYnnM")
val INTERVAL_REDUCED4 = IsoIntervalFormat("PnnY")
val INTERVAL_DECIMAL0 = IsoIntervalFormat("PnnYnnMnnDTnnHnnMnn,nnS")
val INTERVAL_DECIMAL1 = IsoIntervalFormat("PnnYnnMnnDTnnHnn,nnM")
val INTERVAL_DECIMAL2 = IsoIntervalFormat("PnnYnnMnnDTnn,nnH")
val INTERVAL_DECIMAL3 = IsoIntervalFormat("PnnYnnMnn,nnD")
val INTERVAL_DECIMAL4 = IsoIntervalFormat("PnnYnn,nnM")
val INTERVAL_DECIMAL5 = IsoIntervalFormat("PnnYnn,nnW")
val INTERVAL_DECIMAL6 = IsoIntervalFormat("PnnY")
val INTERVAL_ZERO_OMIT0 = IsoIntervalFormat("PnnYnnDTnnHnnMnnS")
val INTERVAL_ZERO_OMIT1 = IsoIntervalFormat("PnnYnnDTnnHnnM")
val INTERVAL_ZERO_OMIT2 = IsoIntervalFormat("PnnYnnDTnnH")
val INTERVAL_ZERO_OMIT3 = IsoIntervalFormat("PnnYnnD")
val INTERVAL_ALL = listOf(
INTERVAL_COMPLETE0, INTERVAL_COMPLETE1,
INTERVAL_REDUCED0, INTERVAL_REDUCED1, INTERVAL_REDUCED2, INTERVAL_REDUCED3, INTERVAL_REDUCED4,
INTERVAL_DECIMAL0, INTERVAL_DECIMAL1, INTERVAL_DECIMAL2, INTERVAL_DECIMAL3, INTERVAL_DECIMAL4,
INTERVAL_DECIMAL5, INTERVAL_DECIMAL6,
INTERVAL_ZERO_OMIT0, INTERVAL_ZERO_OMIT1, INTERVAL_ZERO_OMIT2, INTERVAL_ZERO_OMIT3
)
// Detects and parses all the variants
val DATE = object : DateFormat {
override fun format(dd: DateTimeTz): String = DATE_CALENDAR_COMPLETE.format(dd)
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): DateTimeTz? {
DATE_ALL.fastForEach { format ->
val result = format.extended.tryParse(str, false, doAdjust)
if (result != null) return result
}
DATE_ALL.fastForEach { format ->
val result = format.basic.tryParse(str, false, doAdjust)
if (result != null) return result
}
return if (doThrow) throw DateException("Invalid format") else null
}
}
val TIME = object : TimeFormat {
override fun format(dd: TimeSpan): String = TIME_LOCAL_FRACTION0.format(dd)
override fun tryParse(str: String, doThrow: Boolean, doAdjust: Boolean): TimeSpan? {
TIME_ALL.fastForEach { format ->
val result = format.extended.tryParse(str, false, doAdjust)
if (result != null) return result
}
TIME_ALL.fastForEach { format ->
val result = format.basic.tryParse(str, false, doAdjust)
if (result != null) return result
}
return if (doThrow) throw DateException("Invalid format") else null
}
}
val INTERVAL = object : DateTimeSpanFormat {
override fun format(dd: DateTimeSpan): String = INTERVAL_DECIMAL0.format(dd)
override fun tryParse(str: String, doThrow: Boolean): DateTimeSpan? {
INTERVAL_ALL.fastForEach { format ->
val result = format.tryParse(str, false)
if (result != null) return result
}
return if (doThrow) throw DateException("Invalid format") else null
}
}
}
// ISO 8601 (first week is the one after 1 containing a thursday)
fun Year.first(dayOfWeek: DayOfWeek): DateTime {
val start = DateTime(this.year, 1, 1)
var n = 0
while (true) {
val time = (start + n.days)
if (time.dayOfWeek == dayOfWeek) return time
n++
}
}
val DateTime.weekOfYear0: Int
get() {
val firstThursday = year.first(DayOfWeek.Thursday)
val offset = firstThursday.dayOfMonth - 3
return (dayOfYear - offset) / 7
}
val DateTime.weekOfYear1: Int get() = weekOfYear0 + 1
val DateTimeTz.weekOfYear0: Int get() = local.weekOfYear0
val DateTimeTz.weekOfYear1: Int get() = local.weekOfYear1

View File

@@ -0,0 +1,122 @@
package korlibs.time
import korlibs.time.internal.substr
import kotlin.native.concurrent.ThreadLocal
private var KlockLocale_default: KlockLocale? = null
abstract class KlockLocale {
abstract val ISO639_1: String
abstract val daysOfWeek: List<String>
abstract val months: List<String>
abstract val firstDayOfWeek: DayOfWeek
open val monthsShort: List<String> get() = months.map { it.substr(0, 3) }
open val daysOfWeekShort: List<String> get() = daysOfWeek.map { it.substr(0, 3) }
//private val daysOfWeekWithLocaleList: Array<DayOfWeekWithLocale> = Array(7) { DayOfWeekWithLocale(DayOfWeek[it], this) }
//fun localizedDayOfWeek(dayOfWeek: DayOfWeek) = daysOfWeekWithLocaleList[dayOfWeek.index0]
fun localizedDayOfWeek(dayOfWeek: DayOfWeek) = DayOfWeekWithLocale(DayOfWeek[dayOfWeek.index0], this)
val daysOfWeekComparator get() = Comparator<DayOfWeek> { a, b ->
a.index0Locale(this).compareTo(b.index0Locale(this))
}
open val ordinals get() = Array(32) {
if (it in 11..13) {
"${it}th"
} else {
when (it % 10) {
1 -> "${it}st"
2 -> "${it}nd"
3 -> "${it}rd"
else -> "${it}th"
}
}
}
open fun getOrdinalByDay(day: Int, context: KlockLocaleContext = KlockLocaleContext.Default): String = ordinals[day]
open fun getDayByOrdinal(ordinal: String): Int = ordinals.indexOf(ordinal)
//open val monthsShort: List<String> by klockAtomicLazy { months.map { it.substr(0, 3) } }
//open val daysOfWeekShort: List<String> by klockAtomicLazy { daysOfWeek.map { it.substr(0, 3) } }
/*
private val _lock = KlockLock()
private val _monthsShort = KlockAtomicRef<List<String>?>(null)
private val _daysOfWeekShort = KlockAtomicRef<List<String>?>(null)
//open val monthsShort by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { months.map { it.substr(0, 3) } }
open val monthsShort: List<String> get() = _lock {
if (_monthsShort.value == null) {
_monthsShort.value = months.map { it.substr(0, 3) }
}
_monthsShort.value!!
}
open val daysOfWeekShort: List<String> get() = _lock {
if (_daysOfWeekShort.value == null) {
_daysOfWeekShort.value = daysOfWeek.map { it.substr(0, 3) }
}
_daysOfWeekShort.value!!
}
*/
open val h12Marker: List<String> get() = listOf("am", "pm")
// This might be required for some languages like chinese?
open fun intToString(value: Int) = "$value"
open fun isWeekend(dayOfWeek: DayOfWeek): Boolean = dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday
protected fun format(str: String) = PatternDateFormat(str, this)
open val formatDateTimeMedium get() = format("MMM d, y h:mm:ss a")
open val formatDateTimeShort get() = format("M/d/yy h:mm a")
open val formatDateFull get() = format("EEEE, MMMM d, y")
open val formatDateLong get() = format("MMMM d, y")
open val formatDateMedium get() = format("MMM d, y")
open val formatDateShort get() = format("M/d/yy")
open val formatTimeMedium get() = format("HH:mm:ss")
open val formatTimeShort get() = format("HH:mm")
companion object {
val english get() = English
var default: KlockLocale
set(value) { KlockLocale_default = value }
get() = KlockLocale_default ?: English
inline fun <R> setTemporarily(locale: KlockLocale, callback: () -> R): R {
val old = default
default = locale
try {
return callback()
} finally {
default = old
}
}
}
open class English : KlockLocale() {
companion object : English()
override val ISO639_1 get() = "en"
override val firstDayOfWeek: DayOfWeek get() = DayOfWeek.Sunday
override val daysOfWeek: List<String> get() = listOf(
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
)
override val months: List<String> get() = listOf(
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
)
override val formatTimeMedium get() = format("h:mm:ss a")
override val formatTimeShort get() = format("h:mm a")
}
}
fun DateTime.format(format: String, locale: KlockLocale): String = DateFormat(format).withLocale(locale).format(this)
fun DateTimeTz.format(format: String, locale: KlockLocale): String = DateFormat(format).withLocale(locale).format(this)

View File

@@ -0,0 +1,14 @@
package korlibs.time
data class KlockLocaleContext(val gender: KlockLocaleGender = KlockLocaleGender.Neuter) {
companion object {
val Default = KlockLocaleContext()
}
}
enum class KlockLocaleGender {
Neuter,
Masculine,
}

View File

@@ -0,0 +1,37 @@
package korlibs.time
/**
* Executes a [callback] and measure the time it takes to complete.
*/
inline fun measureTime(callback: () -> Unit): TimeSpan {
val start = PerformanceCounter.microseconds
callback()
val end = PerformanceCounter.microseconds
return (end - start).microseconds
}
inline fun <T> measureTime(callback: () -> T, handleTime: (TimeSpan) -> Unit): T {
val start = PerformanceCounter.microseconds
val result = callback()
val end = PerformanceCounter.microseconds
val elapsed = (end - start).microseconds
handleTime(elapsed)
return result
}
/**
* Executes the [callback] measuring the time it takes to complete.
* Returns a [TimedResult] with the time and the return value of the callback.
*/
inline fun <T> measureTimeWithResult(callback: () -> T): TimedResult<T> {
val start = PerformanceCounter.microseconds
val result = callback()
val end = PerformanceCounter.microseconds
val elapsed = (end - start).microseconds
return TimedResult(result, elapsed)
}
/**
* Represents a [result] associated to a [time].
*/
data class TimedResult<T>(val result: T, val time: TimeSpan)

View File

@@ -0,0 +1,131 @@
package korlibs.time
import korlibs.time.Month.*
import korlibs.time.internal.*
import kotlin.math.*
/** Represents one of the twelve months of the year. */
enum class Month(
/** 1: [January], 2: [February], 3: [March], 4: [April], 5: [May], 6: [June], 7: [July], 8: [August], 9: [September], 10: [October], 11: [November], 12: [December] */
val index1: Int,
/** Number of days of this month in a common year */
val daysCommon: Int,
/** Number of days of this month in a leap year */
val daysLeap: Int = daysCommon
) : Serializable {
January(1, daysCommon = 31),
February(2, daysCommon = 28, daysLeap = 29),
March(3, daysCommon = 31),
April(4, daysCommon = 30),
May(5, daysCommon = 31),
June(6, daysCommon = 30),
July(7, daysCommon = 31),
August(8, daysCommon = 31),
September(9, daysCommon = 30),
October(10, daysCommon = 31),
November(11, daysCommon = 30),
December(12, daysCommon = 31);
/** 0: [January], 1: [February], 2: [March], 3: [April], 4: [May], 5: [June], 6: [July], 7: [August], 8: [September], 9: [October], 10: [November], 11: [December] */
val index0: Int get() = index1 - 1
/** Number of days in a specific month (28-31) depending whether the year is [leap] or not. */
fun days(leap: Boolean): Int = if (leap) daysLeap else daysCommon
/** Number of days in a specific month (28-31) depending whether the [year] or not. */
fun days(year: Int): Int = days(Year(year).isLeap)
/** Number of days in a specific month (28-31) depending whether the [year] or not. */
fun days(year: Year): Int = days(year.isLeap)
/** Number of days since the start of the [leap] year to reach this month. */
fun daysToStart(leap: Boolean): Int = YEAR_DAYS(leap)[index0]
/** Number of days since the start of the [year] to reach this month. */
fun daysToStart(year: Int): Int = daysToStart(Year(year).isLeap)
/** Number of days since the start of the [year] to reach this month. */
fun daysToStart(year: Year): Int = daysToStart(year.isLeap)
/** Number of days since the start of the [leap] year to reach next month. */
fun daysToEnd(leap: Boolean): Int = YEAR_DAYS(leap)[index1]
/** Number of days since the start of the [year] to reach next month. */
fun daysToEnd(year: Int): Int = daysToEnd(Year(year).isLeap)
/** Number of days since the start of the [year] to reach next month. */
fun daysToEnd(year: Year): Int = daysToEnd(year.isLeap)
/** Previous [Month]. */
val previous: Month get() = this - 1
/** Next [Month]. */
val next: Month get() = this + 1
operator fun plus(delta: Int): Month = Month[index1 + delta]
operator fun minus(delta: Int): Month = Month[index1 - delta]
operator fun minus(other: Month): Int = abs(this.index0 - other.index0)
val localName get() = localName(KlockLocale.default)
fun localName(locale: KlockLocale) = locale.months[index0]
val localShortName get() = localShortName(KlockLocale.default)
fun localShortName(locale: KlockLocale) = locale.monthsShort[index0]
companion object {
@Suppress("MayBeConstant", "unused")
private const val serialVersionUID = 1L
/**
* Number of months in a year (12).
*/
const val Count = 12
/** 1: [January], 2: [February], 3: [March], 4: [April], 5: [May], 6: [June], 7: [July], 8: [August], 9: [September], 10: [October], 11: [November], 12: [December] */
operator fun invoke(index1: Int) = adjusted(index1)
/** 1: [January], 2: [February], 3: [March], 4: [April], 5: [May], 6: [June], 7: [July], 8: [August], 9: [September], 10: [October], 11: [November], 12: [December] */
operator fun get(index1: Int) = adjusted(index1)
/**
* Gets the [Month] from a month index where [January]=1 wrapping the index to valid values.
*
* For example 0 and 12=[December], 1 and 13=[January], -1 and 11=[November].
*/
fun adjusted(index1: Int) = BY_INDEX0[(index1 - 1) umod 12]
/**
* Gets the [Month] from a month index where [January]=1 checking that the provided [index1] is valid between 1..12.
*/
fun checked(index1: Int) = BY_INDEX0[index1.also { if (index1 !in 1..12) throw DateException("Month $index1 not in 1..12") } - 1]
/**
* Gets the [Month] of a [dayOfYear] in a [leap] year.
*
* Returns null if the year doesn't contain that [dayOfYear].
*/
fun fromDayOfYear(dayOfYear: Int, leap: Boolean): Month? {
val days = YEAR_DAYS(leap)
val day0 = dayOfYear - 1
val guess = day0 / 32
if (guess in 0..11 && day0 in days[guess] until days[guess + 1]) return Month[guess + 1]
if (guess in 0..10 && day0 in days[guess + 1] until days[guess + 2]) return Month[guess + 2]
return null
}
/**
* Gets the [Month] of a [dayOfYear] in the specified [year].
*
* Returns null if the year doesn't contain that [dayOfYear].
*/
fun fromDayOfYear(dayOfYear: Int, year: Year): Month? = fromDayOfYear(dayOfYear, year.isLeap)
private val BY_INDEX0 = values()
private fun YEAR_DAYS(isLeap: Boolean): IntArray = if (isLeap) YEAR_DAYS_LEAP else YEAR_DAYS_COMMON
private val YEAR_DAYS_LEAP = generateDaysToStart(leap = true)
private val YEAR_DAYS_COMMON = generateDaysToStart(leap = false)
private fun generateDaysToStart(leap: Boolean): IntArray {
var total = 0
return IntArray(13) {
total += if (it == 0) 0 else BY_INDEX0[it - 1].days(leap)
total
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More