mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-12-28 08:59:36 +00:00
Compare commits
125 Commits
v0.19.9
...
65d01b1fb3
| Author | SHA1 | Date | |
|---|---|---|---|
| 65d01b1fb3 | |||
| 6230accb68 | |||
| 10e03bb951 | |||
| aa4f392948 | |||
| f51b59ec02 | |||
| 8c76834ae4 | |||
| 4a454f3d67 | |||
| 151aa1863d | |||
| 3bf6896296 | |||
| 0d01561476 | |||
| f6ded92251 | |||
| d01b735cc6 | |||
| 6c12001080 | |||
| 1afbf03606 | |||
| f6ef5c61c5 | |||
| c18fee8107 | |||
| d9df7a4384 | |||
| 87c2230e8e | |||
| da7eb6de0a | |||
| ed3815118f | |||
| be726f42bd | |||
| a91006132f | |||
| 9a9f741a0b | |||
| 5028f130e9 | |||
| 77fa019651 | |||
| 9715da9384 | |||
| f6d5035c1a | |||
| 43e782ab6f | |||
| f3f9920bfb | |||
| 2bfd615812 | |||
| ebfacb3659 | |||
| c71d557eec | |||
| e0398cef21 | |||
| f91599e9c6 | |||
| f8f9f93c97 | |||
| a8a5340d8b | |||
| 871b27f37d | |||
| 6f174cae1d | |||
| 22d7ac3e22 | |||
| 9b30c3a155 | |||
| 915bac64b1 | |||
| 9d2b50e55d | |||
| bde100f63d | |||
| 05b035a13d | |||
| eefb56bed7 | |||
| fcc0dc4189 | |||
| 47ff20317f | |||
| 1558b9103d | |||
| 7a78742162 | |||
| c01e240f66 | |||
| fef4fcbac6 | |||
| 5ab18bce4b | |||
| 24aec7271a | |||
| 9b19a2cb95 | |||
| efdd7b8a57 | |||
| 6df4cc9c3b | |||
| b9d93db0f5 | |||
| d7ee45ca64 | |||
| d7c31b1b22 | |||
| 7d6794a358 | |||
| 473eb87346 | |||
| 8b18b07790 | |||
| ab3c80a5ec | |||
| 075b93ecd6 | |||
| f6d0f72e49 | |||
| fcda3af862 | |||
| d5c7a589b1 | |||
| 86e099ed25 | |||
| ebd7befe73 | |||
| b8c7e581a1 | |||
| 8281259179 | |||
| d3e06b07df | |||
| 4967018418 | |||
| 537a3c38fa | |||
| 0124957833 | |||
| f0420e2d61 | |||
| 7090566041 | |||
| f0d5035cd0 | |||
| 0fb9b8dc30 | |||
| eef6e81134 | |||
| 1593159a3f | |||
| 3da9eb9dbe | |||
| f17613f3fb | |||
| 14337ccb46 | |||
| 1a3913b09c | |||
| 039aed2747 | |||
| 173991e3cb | |||
| 8b3f8cab01 | |||
| 2a20d24589 | |||
| 53c2d552ec | |||
| af11c1a83d | |||
| a65cf1481c | |||
| 0318716236 | |||
| 88eb4b3342 | |||
| 4810d1ef6a | |||
| f2a9514d89 | |||
| 925ba6ac24 | |||
|
|
ef407268a2 | ||
| cd6c4bbe38 | |||
| c058e18408 | |||
| 6d3ca565ca | |||
| 236a7b4fd2 | |||
| e1f387dbf7 | |||
| 3d113dd31e | |||
| e0e57f0336 | |||
| e775b58d41 | |||
| 5b070a8478 | |||
| 8a61193500 | |||
| fad522b8fe | |||
| 0acac205af | |||
| 069d4c61b7 | |||
| 7c113f5700 | |||
| dfdaf4225b | |||
| bd39ab2467 | |||
| 6ce1eb3f2d | |||
| ce7d4fe9a2 | |||
| a65bb2f419 | |||
| cbc868448b | |||
| 9c336a0b56 | |||
| 0f0d09399e | |||
| e13a1162a9 | |||
| 57ebed903f | |||
| 4478193d8a | |||
| ee948395e3 | |||
| 0616b051ae |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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 "[^/]*$"`"
|
||||
|
||||
2
.github/workflows/dokka_push.yml
vendored
2
.github/workflows/dokka_push.yml
vendored
@@ -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
|
||||
|
||||
174
CHANGELOG.md
174
CHANGELOG.md
@@ -1,5 +1,179 @@
|
||||
# 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`:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>
|
||||
18
android/pickers/build.gradle
Normal file
18
android/pickers/build.gradle
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
android/pickers/src/androidMain/AndroidManifest.xml
Normal file
1
android/pickers/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
27
android/pickers/src/androidMain/kotlin/Fling.kt
Normal file
27
android/pickers/src/androidMain/kotlin/Fling.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
222
android/pickers/src/androidMain/kotlin/NumberPicker.kt
Normal file
222
android/pickers/src/androidMain/kotlin/NumberPicker.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
156
android/pickers/src/androidMain/kotlin/SetPicker.kt
Normal file
156
android/pickers/src/androidMain/kotlin/SetPicker.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
1
android/recyclerview/src/androidMain/AndroidManifest.xml
Normal file
1
android/recyclerview/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.recyclerview"/>
|
||||
18
android/smalltextfield/build.gradle
Normal file
18
android/smalltextfield/build.gradle
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ allprojects {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
common/compose/src/androidMain/AndroidManifest.xml
Normal file
1
common/compose/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
||||
1
common/src/androidMain/AndroidManifest.xml
Normal file
1
common/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -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())
|
||||
|
||||
36
common/src/linuxArm64Main/kotlin/ActualMPPFile.kt
Normal file
36
common/src/linuxArm64Main/kotlin/ActualMPPFile.kt
Normal 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()
|
||||
}
|
||||
25
common/src/linuxArm64Main/kotlin/fixed.kt
Normal file
25
common/src/linuxArm64Main/kotlin/fixed.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.common"/>
|
||||
@@ -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)
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
1
coroutines/compose/src/androidMain/AndroidManifest.xml
Normal file
1
coroutines/compose/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
|
||||
1
coroutines/src/androidMain/AndroidManifest.xml
Normal file
1
coroutines/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -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()
|
||||
@@ -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 }
|
||||
@@ -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)
|
||||
}
|
||||
151
coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
Normal file
151
coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines"/>
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
1
crypto/src/androidMain/AndroidManifest.xml
Normal file
1
crypto/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.crypto"/>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
1
dokka/src/androidMain/AndroidManifest.xml
Normal file
1
dokka/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.dokka"/>
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
1
fsm/common/src/androidMain/AndroidManifest.xml
Normal file
1
fsm/common/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.fsm.common"/>
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
1
fsm/repos/common/src/androidMain/AndroidManifest.xml
Normal file
1
fsm/repos/common/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>
|
||||
@@ -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.19.9
|
||||
android_code_version=205
|
||||
version=0.20.23
|
||||
android_code_version=229
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
[versions]
|
||||
|
||||
kt = "1.8.22"
|
||||
kt-serialization = "1.5.1"
|
||||
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.3"
|
||||
jb-exposed = "0.41.1"
|
||||
jb-dokka = "1.8.20"
|
||||
jb-compose = "1.5.11"
|
||||
jb-exposed = "0.45.0"
|
||||
jb-dokka = "1.9.10"
|
||||
|
||||
korlibs = "4.0.3"
|
||||
uuid = "0.7.1"
|
||||
korlibs = "4.0.10"
|
||||
uuid = "0.8.2"
|
||||
|
||||
ktor = "2.3.2"
|
||||
ktor = "2.3.7"
|
||||
|
||||
gh-release = "2.4.1"
|
||||
|
||||
koin = "3.4.3"
|
||||
koin = "3.5.0"
|
||||
|
||||
okio = "3.4.0"
|
||||
okio = "3.6.0"
|
||||
|
||||
ksp = "1.8.22-1.0.11"
|
||||
kotlin-poet = "1.14.2"
|
||||
ksp = "1.9.21-1.0.16"
|
||||
kotlin-poet = "1.15.3"
|
||||
|
||||
versions = "0.47.0"
|
||||
versions = "0.50.0"
|
||||
|
||||
android-gradle = "7.4.2"
|
||||
android-gradle = "8.2.0"
|
||||
dexcount = "4.0.0"
|
||||
|
||||
android-coreKtx = "1.10.1"
|
||||
android-recyclerView = "1.3.1"
|
||||
android-coreKtx = "1.12.0"
|
||||
android-recyclerView = "1.3.2"
|
||||
android-appCompat = "1.6.1"
|
||||
android-fragment = "1.6.1"
|
||||
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]
|
||||
|
||||
@@ -83,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" }
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
47
klock/LICENSE
Normal 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
7
klock/build.gradle
Normal file
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
@@ -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
|
||||
8
klock/src/androidMain/kotlin/korlibs/time/Time.jvm.kt
Normal file
8
klock/src/androidMain/kotlin/korlibs/time/Time.jvm.kt
Normal 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)
|
||||
72
klock/src/commonMain/kotlin/korlibs/time/Date.kt
Normal file
72
klock/src/commonMain/kotlin/korlibs/time/Date.kt
Normal 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))
|
||||
@@ -0,0 +1,6 @@
|
||||
package korlibs.time
|
||||
|
||||
/**
|
||||
* An exception for Date operations.
|
||||
*/
|
||||
class DateException(msg: String) : RuntimeException(msg)
|
||||
43
klock/src/commonMain/kotlin/korlibs/time/DateFormat.kt
Normal file
43
klock/src/commonMain/kotlin/korlibs/time/DateFormat.kt
Normal 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)
|
||||
458
klock/src/commonMain/kotlin/korlibs/time/DateTime.kt
Normal file
458
klock/src/commonMain/kotlin/korlibs/time/DateTime.kt
Normal 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
|
||||
}
|
||||
154
klock/src/commonMain/kotlin/korlibs/time/DateTimeRange.kt
Normal file
154
klock/src/commonMain/kotlin/korlibs/time/DateTimeRange.kt
Normal 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)
|
||||
270
klock/src/commonMain/kotlin/korlibs/time/DateTimeRangeSet.kt
Normal file
270
klock/src/commonMain/kotlin/korlibs/time/DateTimeRangeSet.kt
Normal 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())
|
||||
143
klock/src/commonMain/kotlin/korlibs/time/DateTimeSpan.kt
Normal file
143
klock/src/commonMain/kotlin/korlibs/time/DateTimeSpan.kt
Normal 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) }
|
||||
}
|
||||
@@ -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'")
|
||||
125
klock/src/commonMain/kotlin/korlibs/time/DateTimeTz.kt
Normal file
125
klock/src/commonMain/kotlin/korlibs/time/DateTimeTz.kt
Normal 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)"
|
||||
}
|
||||
95
klock/src/commonMain/kotlin/korlibs/time/DayOfWeek.kt
Normal file
95
klock/src/commonMain/kotlin/korlibs/time/DayOfWeek.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
42
klock/src/commonMain/kotlin/korlibs/time/Frequency.kt
Normal file
42
klock/src/commonMain/kotlin/korlibs/time/Frequency.kt
Normal 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
|
||||
}
|
||||
446
klock/src/commonMain/kotlin/korlibs/time/ISO8601.kt
Normal file
446
klock/src/commonMain/kotlin/korlibs/time/ISO8601.kt
Normal 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
|
||||
122
klock/src/commonMain/kotlin/korlibs/time/KlockLocale.kt
Normal file
122
klock/src/commonMain/kotlin/korlibs/time/KlockLocale.kt
Normal 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)
|
||||
@@ -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,
|
||||
}
|
||||
37
klock/src/commonMain/kotlin/korlibs/time/Measure.kt
Normal file
37
klock/src/commonMain/kotlin/korlibs/time/Measure.kt
Normal 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)
|
||||
131
klock/src/commonMain/kotlin/korlibs/time/Month.kt
Normal file
131
klock/src/commonMain/kotlin/korlibs/time/Month.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
klock/src/commonMain/kotlin/korlibs/time/MonthSpan.kt
Normal file
66
klock/src/commonMain/kotlin/korlibs/time/MonthSpan.kt
Normal file
@@ -0,0 +1,66 @@
|
||||
package korlibs.time
|
||||
|
||||
import korlibs.time.internal.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
/**
|
||||
* Creates a [MonthSpan] representing these years.
|
||||
*/
|
||||
inline val Int.years get() = MonthSpan(12 * this)
|
||||
|
||||
/**
|
||||
* Creates a [MonthSpan] representing these months.
|
||||
*/
|
||||
inline val Int.months get() = MonthSpan(this)
|
||||
|
||||
/**
|
||||
* Represents a number of years and months temporal distance.
|
||||
*/
|
||||
@JvmInline
|
||||
value class MonthSpan(
|
||||
/** Total months of this [MonthSpan] as integer */
|
||||
val totalMonths: Int
|
||||
) : Comparable<MonthSpan>, Serializable {
|
||||
companion object {
|
||||
@Suppress("MayBeConstant", "unused")
|
||||
private const val serialVersionUID = 1L
|
||||
}
|
||||
|
||||
operator fun unaryMinus() = MonthSpan(-totalMonths)
|
||||
operator fun unaryPlus() = MonthSpan(+totalMonths)
|
||||
|
||||
operator fun plus(other: TimeSpan) = DateTimeSpan(this, other)
|
||||
operator fun plus(other: MonthSpan) = MonthSpan(totalMonths + other.totalMonths)
|
||||
operator fun plus(other: DateTimeSpan) = DateTimeSpan(other.monthSpan + this, 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) = MonthSpan((totalMonths * times).toInt())
|
||||
operator fun times(times: Int) = this * times.toDouble()
|
||||
operator fun times(times: Float) = this * times.toDouble()
|
||||
|
||||
operator fun div(times: Double) = MonthSpan((totalMonths / times).toInt())
|
||||
operator fun div(times: Int) = this / times.toDouble()
|
||||
operator fun div(times: Float) = this / times.toDouble()
|
||||
|
||||
override fun compareTo(other: MonthSpan): Int = this.totalMonths.compareTo(other.totalMonths)
|
||||
|
||||
/** Converts this time to String formatting it like "20Y", "20Y 1M", "1M" or "0M". */
|
||||
override fun toString(): String {
|
||||
val list = arrayListOf<String>()
|
||||
if (years != 0) list.add("${years}Y")
|
||||
if (months != 0 || years == 0) list.add("${months}M")
|
||||
return list.joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
/** Total years of this [MonthSpan] as double (might contain decimals) */
|
||||
val MonthSpan.totalYears: Double get() = totalMonths.toDouble() / 12.0
|
||||
|
||||
/** Years part of this [MonthSpan] as integer */
|
||||
val MonthSpan.years: Int get() = totalMonths / 12
|
||||
|
||||
/** Months part of this [MonthSpan] as integer */
|
||||
val MonthSpan.months: Int get() = totalMonths % 12
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user