diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/Observables.kt b/Toolkt/src/main/java/com/reco1l/toolkt/Observables.kt index c6ca3cb..a008aeb 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/Observables.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/Observables.kt @@ -3,8 +3,7 @@ package com.reco1l.toolkt /** * Intended to be used in manager classes where many events happens at once that must be listened. */ -interface IObservable -{ +interface IObservable { /** * The list of observers @@ -16,8 +15,7 @@ interface IObservable * @param index The index where the observer should be bind, by default last index. * @param observer The observer to be bind. */ - fun bindObserver(index: Int = observers.size, observer: T): Boolean - { + fun bindObserver(index: Int = observers.size, observer: T): Boolean { observers.add(index, observer) return index == observers.indexOf(observer) } @@ -31,8 +29,7 @@ interface IObservable /** * Iterate over all observers. */ -inline fun IObservable.forEachObserver(action: (T) -> Unit) -{ +inline fun IObservable.forEachObserver(action: (T) -> Unit) { for (i in 0.. IObservable.forEachObserver(action: (T) -> Unit) /** * Intended to be used in manager classes where many events happens at once that must be listened. */ -interface IMapObservable -{ +interface IMapObservable { /** * The list of observers @@ -54,8 +50,7 @@ interface IMapObservable * @param index The index where the observer should be bind, by default last index. * @param observer The observer to be bind. */ - fun bindObserver(key: K, index: Int = observers[key]?.size ?: 0, observer: T): Boolean - { + fun bindObserver(key: K, index: Int = observers[key]?.size ?: 0, observer: T): Boolean { val list = observers.getOrPut(key) { mutableListOf() } list.add(index, observer) return index == list.indexOf(observer) @@ -69,8 +64,7 @@ interface IMapObservable /** * Unbind and existent observer. */ - fun unbindObserver(key: K, observer: T): Boolean - { + fun unbindObserver(key: K, observer: T): Boolean { val list = observers[key] ?: return false val result = list.remove(observer) @@ -84,10 +78,10 @@ interface IMapObservable /** * Iterate over all observers for a key. */ -fun IMapObservable.forEachObserver(key: K, action: (T) -> Unit) -{ +fun IMapObservable.forEachObserver(key: K, action: (T) -> Unit) { val list = observers[key] ?: return - for (i in 0.. 7.5625f * it.pow(2) - - it < 2f / 2.75f -> 7.5625f * (it - 1.5f / 2.75f).pow(2) + 0.75f - - it < 2.5f / 2.75f -> 7.5625f * (it - 2.25f / 2.75f).pow(2) + 0.9375f - - else -> 7.5625f * (it - 2.625f / 2.75f).pow(2) + 0.984375f - } - } - - val BOUNCE_IN = EasingFunction { 1f - BOUNCE_OUT.getInterpolation(1f - it) } - - // Sine - - val SINE_IN = EasingFunction { -cos(it * MathF.PI.half) + 1f } - - val SINE_OUT = EasingFunction { sin(it * MathF.PI.half) } - - - // Acceleration - - val DECELERATE = EasingFunction { 1.0f - (1.0f - it).pow(2) } - - val ACCELERATE = EasingFunction { it.pow(2) } - -} \ No newline at end of file diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/animation/ValueAnimations.kt b/Toolkt/src/main/java/com/reco1l/toolkt/animation/ValueAnimations.kt index 4a5f089..b755522 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/animation/ValueAnimations.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/animation/ValueAnimations.kt @@ -2,6 +2,7 @@ package com.reco1l.toolkt.animation import android.animation.TimeInterpolator import android.animation.ValueAnimator +import android.view.animation.LinearInterpolator import kotlin.reflect.KFunction1 import kotlin.reflect.KMutableProperty0 @@ -21,7 +22,10 @@ private fun animateFloat( duration = end startDelay = delay - interpolator = ease ?: TimeEasing.LINEAR + + if (ease != null) { + interpolator = ease + } addUpdateListener { onApply(it.animatedValue as Float) } @@ -43,7 +47,11 @@ private fun animateInt( duration = end startDelay = delay - interpolator = ease ?: TimeEasing.LINEAR + + if (ease != null) { + interpolator = ease + } + addUpdateListener { onApply(it.animatedValue as Int) } diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/animation/ViewAnimations.kt b/Toolkt/src/main/java/com/reco1l/toolkt/animation/ViewAnimations.kt index f1b1273..3c92d0d 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/animation/ViewAnimations.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/animation/ViewAnimations.kt @@ -1,5 +1,6 @@ package com.reco1l.toolkt.animation +import android.animation.TimeInterpolator import android.view.View import android.view.ViewPropertyAnimator @@ -10,7 +11,7 @@ private fun View.animateTo( value: T, time: Long, delay: Long, - timeInterpolator: EasingFunction?, + timeInterpolator: TimeInterpolator?, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -24,7 +25,10 @@ private fun View.animateTo( duration = time startDelay = delay - interpolator = timeInterpolator + + if (timeInterpolator != null) { + interpolator = timeInterpolator + } start() } @@ -44,7 +48,7 @@ fun View.toAlpha( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -55,7 +59,7 @@ fun View.toScaleX( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -66,7 +70,7 @@ fun View.toScaleY( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -77,7 +81,7 @@ fun View.toTranslationX( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -88,7 +92,7 @@ fun View.toTranslationY( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -99,7 +103,7 @@ fun View.toX( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -110,7 +114,7 @@ fun View.toY( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -121,7 +125,7 @@ fun View.toRotation( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -132,7 +136,7 @@ fun View.toRotationX( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null @@ -143,7 +147,7 @@ fun View.toRotationY( value: Float, time: Long = 0L, delay: Long = 0L, - ease: EasingFunction? = null, + ease: TimeInterpolator? = null, onStart: (() -> Unit)? = null, onEnd: (() -> Unit)? = null diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/data/IO.kt b/Toolkt/src/main/java/com/reco1l/toolkt/data/IO.kt index a1d3c69..cc530ac 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/data/IO.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/data/IO.kt @@ -34,10 +34,11 @@ fun File.isExtension(extension: String) = isFile && extension.equals(extension, /** * Create a new sub directory. */ -fun File.subDirectory(name: String): File -{ - if (!isDirectory) +fun File.subDirectory(name: String): File { + + if (!isDirectory) { throw IOException("This isn't a directory.") + } return File(this, name).apply { @@ -49,10 +50,11 @@ fun File.subDirectory(name: String): File /** * Create a new sub file. */ -fun File.subFile(name: String): File -{ - if (!isDirectory) +fun File.subFile(name: String): File { + + if (!isDirectory) { throw IOException("This isn't a directory.") + } return File(this, name).apply { @@ -64,8 +66,7 @@ fun File.subFile(name: String): File /** * Similarly to [File.listFiles]. */ -fun File.getFiles(vararg extensions: String? = emptyArray()): Array? -{ +fun File.getFiles(vararg extensions: String? = emptyArray()): Array? { return listFiles { file -> extensions.isEmpty() || extensions.any { it.equals(file.extension, true) } } @@ -74,42 +75,36 @@ fun File.getFiles(vararg extensions: String? = emptyArray()): Array? /** * Iterate all over the files inside the directory. */ -fun File.forEach( - vararg extensions: String? = emptyArray(), - selector: ((File) -> Int)? = null, - block: (File) -> Unit -) -{ +fun File.forEach(vararg extensions: String? = emptyArray(), selector: ((File) -> Int)? = null, block: (File) -> Unit) { + getFiles(*extensions)?.also { - if (selector != null) + if (selector != null) { it.sortedBy(selector).forEach(block) - else + } else { it.forEach(block) + } } } /** * Iterate all over files and sub-directories. */ -fun File.forEachRecursive( - vararg extensions: String? = emptyArray(), - selector: ((File) -> Int)? = null, - block: (File) -> Unit -) -{ - val action = { file: File -> +fun File.forEachRecursive(vararg extensions: String? = emptyArray(), selector: ((File) -> Int)? = null, block: (File) -> Unit) { - if (file.isDirectory) + val action = { file: File -> + if (file.isDirectory) { file.forEachRecursive(*extensions, selector = selector, block = block) - else + } else { block(file) + } } getFiles(*extensions)?.also { - if (selector != null) + if (selector != null) { it.sortedBy(selector).forEach(action) - else + } else { it.forEach(action) + } } } @@ -133,8 +128,7 @@ operator fun List.get(name: String, ignoreCase: Boolean = true) = find { /** * If the input stream belongs to a file we write it to the destination file. */ -fun InputStream.writeToFile(destination: File) -{ +fun InputStream.writeToFile(destination: File) { destination.outputStream().use { copyTo(it) } } @@ -149,27 +143,25 @@ fun InputStream.writeToFile(destination: File) * If a [File] object is not necessary consider using [InputStream] instead. Also consider using [Uri.toFile] * instead if you sure that the URI scheme is [SCHEME_FILE]. */ -fun Uri.toFile(parent: File, resolver: ContentResolver): File -{ - if (scheme == SCHEME_FILE) +fun Uri.toFile(parent: File, resolver: ContentResolver): File { + + if (scheme == SCHEME_FILE) { return toFile() + } val file = File(parent, resolveFilename(resolver)) - - resolver.openInputStream(this)?.use { it.writeToFile(file) } - ?: - throw IOException("Failed to create InputStream from given URI.") - + resolver.openInputStream(this)?.use { it.writeToFile(file) } ?: throw IOException("Failed to create InputStream from given URI.") return file } /** * Resolves the filename given by the URI. */ -fun Uri.resolveFilename(resolver: ContentResolver): String -{ - if (scheme != SCHEME_CONTENT) +fun Uri.resolveFilename(resolver: ContentResolver): String { + + if (scheme != SCHEME_CONTENT) { throw UnsupportedOperationException("The URI scheme does not equal \"content\".") + } return resolver.query(this, null, null, null, null)?.use { diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/data/Json.kt b/Toolkt/src/main/java/com/reco1l/toolkt/data/Json.kt index 0132d3d..26438be 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/data/Json.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/data/Json.kt @@ -57,8 +57,7 @@ inline fun JSONArray.forEach(action: (JSONObject) -> Unit) { * @throws Exception Advise to catch any conversion error. */ @Experimental -inline fun JSONObject.mapInto(): T? -{ +inline fun JSONObject.mapInto(): T? { val constructor = T::class.primaryConstructor ?: return null val parameters = constructor.parameters @@ -74,12 +73,9 @@ inline fun JSONObject.mapInto(): T? * @throws Exception Advise to catch any conversion error. */ @Experimental -inline fun JSONArray.mapIntoListOf(): MutableList? -{ +inline fun JSONArray.mapIntoListOf(): MutableList? { val list = mutableListOf() - forEach { list.add(it.mapInto() ?: return null) } - return list.takeUnless { it.isEmpty() } } @@ -87,14 +83,13 @@ inline fun JSONArray.mapIntoListOf(): MutableList? /** * Iterator for [JSONArray] */ -operator fun JSONArray.iterator(): Iterator = object : Iterator -{ +operator fun JSONArray.iterator(): Iterator = object : Iterator { + private var index = 0 override fun hasNext() = index < length() - override fun next(): Any - { + override fun next(): Any { val element = get(index) index++ return element diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/data/Okio.kt b/Toolkt/src/main/java/com/reco1l/toolkt/data/Okio.kt index 63628e0..2066a94 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/data/Okio.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/data/Okio.kt @@ -30,8 +30,7 @@ fun buildUrl(base: String, block: HttpUrl.Builder.() -> Unit) = base.toHttpUrl() /** * Reads all UTF8 encoded lines until there's nothing left to read. */ -inline fun BufferedSource.readUTF8Lines(block: (String) -> Unit) -{ +inline fun BufferedSource.readUTF8Lines(block: (String) -> Unit) { while (readUtf8Line()?.also(block) != null) Unit close() } @@ -40,7 +39,6 @@ inline fun BufferedSource.readUTF8Lines(block: (String) -> Unit) * Reads all UTF8 encoded lines until there's nothing left to read or the predicate isn't accomplished * anymore. */ -inline fun BufferedSource.readUTF8LinesUntil(predicate: (String) -> Boolean, block: (String) -> Unit) -{ +inline fun BufferedSource.readUTF8LinesUntil(predicate: (String) -> Boolean, block: (String) -> Unit) { while (readUtf8Line()?.takeUnless(predicate)?.also(block) != null) Unit } \ No newline at end of file diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Anchors.kt b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Anchors.kt index 6541a41..c04248c 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Anchors.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Anchors.kt @@ -20,10 +20,8 @@ import androidx.annotation.IntDef Anchor.BOTTOM_CENTER, ] ) -annotation class Anchor -{ - companion object - { +annotation class Anchor { + companion object { const val CENTER = 0 const val LEFT = 1 diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Axis.kt b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Axis.kt index 5d5968c..19d4b75 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Axis.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Axis.kt @@ -2,15 +2,15 @@ package com.reco1l.toolkt.graphics import androidx.annotation.IntDef -@IntDef(value = [ - Axis.VERTICAL, - Axis.HORIZONTAL, - Axis.BOTH -]) -annotation class Axis -{ - companion object - { +@IntDef( + value = [ + Axis.VERTICAL, + Axis.HORIZONTAL, + Axis.BOTH + ] +) +annotation class Axis { + companion object { const val VERTICAL = 0 const val HORIZONTAL = 1 const val BOTH = 2 diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Bitmaps.kt b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Bitmaps.kt index 7b4ec58..930deed 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Bitmaps.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Bitmaps.kt @@ -14,8 +14,7 @@ import java.io.File * * @param file The file to get the dimensions from. */ -fun Options.calculateDimensions(file: File): Options -{ +fun Options.calculateDimensions(file: File): Options { // This is required to be true otherwise the bitmap will be loaded into memory when we only // want to know its dimensions. inJustDecodeBounds = true @@ -32,8 +31,7 @@ fun Options.calculateDimensions(file: File): Options * * @see BitmapFactory.Options.inSampleSize */ -fun Options.approximateSampleSize(targetWidth: Int, targetHeight: Int): Options -{ +fun Options.approximateSampleSize(targetWidth: Int, targetHeight: Int): Options { if (outWidth <= targetWidth && outHeight <= targetHeight) return this @@ -50,7 +48,7 @@ fun Options.approximateSampleSize(targetWidth: Int, targetHeight: Int): Options /** * Decodes a file into a bitmap with this as options. */ -fun Options.createBitmap(file: File) = BitmapFactory.decodeFile(file.absolutePath, this) +fun Options.createBitmap(file: File): Bitmap = BitmapFactory.decodeFile(file.absolutePath, this) // Bitmap @@ -58,8 +56,7 @@ fun Options.createBitmap(file: File) = BitmapFactory.decodeFile(file.absolutePat /** * Crops the bitmap in the center to the given dimensions. */ -fun Bitmap.cropInCenter(targetWidth: Int, targetHeight: Int): Bitmap -{ +fun Bitmap.cropInCenter(targetWidth: Int, targetHeight: Int): Bitmap { val newX = (width - targetWidth) / 2 val newY = (height - targetHeight) / 2 diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Canvas.kt b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Canvas.kt index 761060a..b0ef7dd 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Canvas.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Canvas.kt @@ -24,12 +24,10 @@ val RectF.rangeY operator fun RectF.contains(event: MotionEvent) = event.x in rangeX && event.y in rangeY -fun Path.addRoundRect(rectF: RectF, radius: Float, direction: Direction) -{ +fun Path.addRoundRect(rectF: RectF, radius: Float, direction: Direction) { addRoundRect(rectF, radius, radius, direction) } -fun Canvas.drawRoundRect(rectF: RectF, radius: Float, paint: Paint) -{ +fun Canvas.drawRoundRect(rectF: RectF, radius: Float, paint: Paint) { drawRoundRect(rectF, radius, radius, paint) } \ No newline at end of file diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Drawables.kt b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Drawables.kt index ecbe35a..770f827 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Drawables.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/graphics/Drawables.kt @@ -15,10 +15,7 @@ import com.reco1l.toolkt.kotlin.runSafe /** * Wrap a drawable inside a [ClipDrawable]. */ -fun Drawable.clip( - gravity: Int = Gravity.LEFT, - orientation: Int = ClipDrawable.HORIZONTAL -) = ClipDrawable(this, gravity, orientation) +fun Drawable.clip(gravity: Int = Gravity.LEFT, orientation: Int = ClipDrawable.HORIZONTAL) = ClipDrawable(this, gravity, orientation) // LayerDrawable @@ -26,10 +23,10 @@ fun Drawable.clip( /** * Iterate over all layers. */ -fun LayerDrawable.forEach(block: (Drawable) -> Unit) -{ - for (i in 0 until numberOfLayers) +fun LayerDrawable.forEach(block: (Drawable) -> Unit) { + for (i in 0 until numberOfLayers) { block(getDrawable(i)) + } } /** @@ -46,19 +43,16 @@ fun LayerDrawable(vararg layers: Drawable) = LayerDrawable(layers) * @param anchor The anchor to apply the radius, if `null` is passed the radius will be applied * to all anchors. */ -fun GradientDrawable.setRadius(radius: Float, @CornerAnchor anchor: Int? = null) -{ +fun GradientDrawable.setRadius(radius: Float, @CornerAnchor anchor: Int? = null) { val radii = FloatArray(8) - if (anchor == null) - { + if (anchor == null) { radii.fill(radius) cornerRadii = radii return } - when (anchor) - { + when (anchor) { Anchor.TOP_LEFT -> radii.fill(radius, 0, 2) Anchor.TOP_RIGHT -> radii.fill(radius, 2, 4) Anchor.BOTTOM_LEFT -> radii.fill(radius, 4, 6) diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Arrays.kt b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Arrays.kt new file mode 100644 index 0000000..9e37941 --- /dev/null +++ b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Arrays.kt @@ -0,0 +1,177 @@ +package com.reco1l.toolkt.kotlin + +import kotlin.math.max +import kotlin.math.min + + +// Checks + +/** + * Safe check if an element is in a nullable array. If the array is null then the result is `false`. + */ +infix operator fun T.contains(array: Array?): Boolean = array != null && this in array + +/** + * Finds if there's any coincidence between two lists. + */ +fun Array.anyCoincidence(other: Array): Boolean { + for (i in indices) { + if (this[i] in other) { + return true + } + } + return false +} + +/** + * Finds if there's any difference between two lists. + */ +fun Array.anyDifference(other: Array): Boolean { + for (i in indices) { + if (this[i] !in other) { + return true + } + } + return false +} + + +// Peeking + +/** + * Returns the next element in the list from the passed element. + * + * @param element The element to start from. + * @param circular If `true` it'll loop back to the first element once the last is reached, + * otherwise it'll return the element passed as [fallback]. + * @param fallback The element to return if the bound is reached and [circular] is `false`, by default + * it's the same as [element]. + */ +@JvmOverloads +fun Array.nextTo(element: T, circular: Boolean = false, fallback: T = element): T? { + + var index = indexOf(element) + + if (index == -1) { + throw IllegalArgumentException("The passed element isn't present in the list.") + } + + index++ + + if (circular && index > lastIndex) { + index = 0 + } + + return getOrNull(index) ?: fallback +} + +/** + * Returns the previous element in the list from the passed element. + * + * @param element The element to start from. + * @param circular If `true` it'll loop back to the last element once the first is reached, + * otherwise it'll return the element passed as [fallback]. + * @param fallback The element to return if the bound is reached and [circular] is `false`, by default + * it's the same as [element]. + */ +@JvmOverloads +fun Array.previousTo(element: T, circular: Boolean = false, fallback: T = element): T? { + + var index = indexOf(element) + + if (index == -1) { + throw IllegalArgumentException("The passed element isn't present in the list.") + } + + index-- + + if (circular && index < 0) { + index = size - 1 + } + + return getOrNull(index) ?: fallback +} + + +// Loops + +/** + * Iterates all over the list. + * + * Despite [forEach] this doesn't use a [Iterator] internally which can lead to a performance penalty, if + * the list is modified while iterating it can throw an [ArrayIndexOutOfBoundsException] rather than a + * [ConcurrentModificationException]. + */ +inline fun Array.fastForEach(block: (T) -> Unit) { + for (i in indices) { + block(this[i]) + } +} + +/** + * Same as [fastForEach] but with indices. + */ +inline fun Array.fastForEachIndexed(block: (index: Int, element: T) -> Unit) { + for (i in indices) { + block(i, this[i]) + } +} + + +// Mappings + +/** + * Iterates all over the list transforming the result and returning it at the end. + */ +inline fun Array.forEachLet(block: (element: T, result: R?) -> R?): R? +{ + var result = block(this[0], null) + for (i in indices) { + result = block(this[i], result) + } + return result +} + +/** + * Returns the sum of all values produced by [selector] function applied to each element in the array. + * + * Note: This is a missing function in Kotlin standard library that can be implemented later. + */ +inline fun Array.sumOf(selector: (T) -> Float): Float { + var sum = 0f + for (i in indices) { + sum += selector(this[i]) + } + return sum +} + + +/** + * Returns the maximum value produced by [selector] function applied to each element in the array. + * + * Despite [maxOf] this doesn't use a [Iterator] internally which can lead to a performance penalty, if + * the list is modified while iterating it can throw an [ArrayIndexOutOfBoundsException] rather than a + * [ConcurrentModificationException]. + */ +inline fun Array.fastMaxOf(selector: (T) -> Float): Float { + var result = selector(this[0]) + for (i in indices) { + result = max(result, selector(this[i])) + } + return result +} + +/** + * Returns the minimum value produced by [selector] function applied to each element in the array. + * + * Despite [minOf] this doesn't use a [Iterator] internally which can lead to a performance penalty, if + * the list is modified while iterating it can throw an [ArrayIndexOutOfBoundsException] rather than a + * [ConcurrentModificationException]. + */ +inline fun Array.fastMinOf(selector: (T) -> Float): Float { + var result = selector(this[0]) + for (i in indices) { + result = min(result, selector(this[i])) + } + return result +} \ No newline at end of file diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Collections.kt b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Collections.kt index f21b137..bb9c1f3 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Collections.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Collections.kt @@ -1,7 +1,7 @@ - package com.reco1l.toolkt.kotlin -import com.reco1l.toolkt.kotlin.BoundConflict.* +import kotlin.math.max +import kotlin.math.min import kotlin.reflect.KClass @@ -10,18 +10,16 @@ import kotlin.reflect.KClass /** * Safe check if an element is in a nullable array. If the array is null then the result is `false`. */ -infix fun T.safeIn(array: Array?): Boolean = array != null && this in array - +infix operator fun T.contains(collection: Collection?): Boolean = collection != null && this in collection /** * Finds if there's any coincidence between two lists. */ -fun List.anyCoincidence(other: Collection): Boolean -{ - for (i in indices) - { - if (get(i) in other) +fun List.anyCoincidence(other: Collection): Boolean { + for (i in indices) { + if (this[i] in other) { return true + } } return false } @@ -29,101 +27,70 @@ fun List.anyCoincidence(other: Collection): Boolean /** * Finds if there's any difference between two lists. */ -fun List.anyDifference(other: Collection): Boolean -{ - for (i in indices) - { - if (get(i) !in other) +fun List.anyDifference(other: Collection): Boolean { + for (i in indices) { + if (this[i] !in other) { return true + } } return false } -// Addition - -/** - * Add an element if the passed element isn't null. - */ -fun MutableCollection.addIfNotNull(element: T?) = element?.let { add(it) } ?: false - - // Peeking -/** - * Defines the behavior when reaching the bound. - */ -enum class BoundConflict -{ - /** - * Once the bound is reached it'll return `null`. - */ - NULL, - /** - * Once the bound is reached it'll return the same object. - */ - CLAMP, - /** - * Once the bound is reached it'll return the first or last element. - */ - START_END -} - - /** * Returns the next element in the list from the passed element. - * @param boundConflict The behavior when reaching the bound, see [BoundConflict] for more info. + * + * @param element The element to start from. + * @param circular If `true` it'll loop back to the first element once the last is reached, + * otherwise it'll return the element passed as [fallback]. + * @param fallback The element to return if the bound is reached and [circular] is `false`, by default + * it's the same as [element]. */ -fun List.nextOf( - element: T, - boundConflict: BoundConflict = NULL -): T? -{ +@JvmOverloads +fun List.nextTo(element: T, circular: Boolean = false, fallback: T = element): T? { + var index = indexOf(element) - if (index == -1) + if (index == -1) { throw IllegalArgumentException("The passed element isn't present in the list.") + } index++ - if (index > lastIndex) - { - return when (boundConflict) - { - NULL -> null - CLAMP -> element - START_END -> get(0) - } + if (circular && index > lastIndex) { + index = 0 } - return get(index) + + return getOrNull(index) ?: fallback } /** * Returns the previous element in the list from the passed element. - * @param boundConflict The behavior when reaching the bound, see [BoundConflict] for more info. + * + * @param element The element to start from. + * @param circular If `true` it'll loop back to the last element once the first is reached, + * otherwise it'll return the element passed as [fallback]. + * @param fallback The element to return if the bound is reached and [circular] is `false`, by default + * it's the same as [element]. */ -fun List.previousOf( - element: T, - boundConflict: BoundConflict = NULL -): T? -{ +@JvmOverloads +fun List.previousTo(element: T, circular: Boolean = false, fallback: T = element): T? { + var index = indexOf(element) - if (index == -1) + if (index == -1) { throw IllegalArgumentException("The passed element isn't present in the list.") + } index-- - if (index < 0) - { - return when(boundConflict) - { - NULL -> null - CLAMP -> element - START_END -> get(lastIndex) - } + if (circular && index < 0) { + index = size - 1 } - return get(index) + + return getOrNull(index) ?: fallback } @@ -132,90 +99,101 @@ fun List.previousOf( /** * Iterates all over the list. * - * Kotlin's standard [forEach][Iterable.forEach] uses an [Iterator] internally which can lead to a - * performance penalty in some cases due to allocation. - * - * This should be used with precaution, when a list is modified while iterating it can throw an - * [ArrayIndexOutOfBoundsException] rather than a [ConcurrentModificationException]. + * Despite [forEach] this doesn't use a [Iterator] internally which can lead to a performance penalty, if + * the list is modified while iterating it can throw an [ArrayIndexOutOfBoundsException] rather than a + * [ConcurrentModificationException]. */ -inline fun List.fastForEach(block: (T) -> Unit) -{ - val size = size - - var i = 0 - while (i < size) - { - block(get(i)) - ++i +inline fun List.fastForEach(block: (T) -> Unit) { + for (i in indices) { + block(this[i]) } } /** - * Same as [fastForEach] but with indexes. + * Same as [fastForEach] but with indices. */ -inline fun List.fastForEachIndexed(block: (index: Int, element: T) -> Unit) -{ - var i = 0 - while (i < size) - { - block(i, get(i)) - ++i +inline fun List.fastForEachIndexed(block: (index: Int, element: T) -> Unit) { + for (i in indices) { + block(i, this[i]) } } +// Modification + /** * Iterates all over the list removing the elements from start or from the end depending of [reversed] * parameter. */ -inline fun MutableList.forEachTrim(reversed: Boolean = false, block: (T) -> Unit) -{ - while (isNotEmpty()) +inline fun MutableList.forEachTrim(reversed: Boolean = false, block: (T) -> Unit) { + while (isNotEmpty()) { block(if (reversed) removeLast() else removeFirst()) + } } + +// Mappings + /** * Iterates all over the list transforming the result and returning it at the end. */ -inline fun Array.forEachLet(block: (element: T, result: R?) -> R?): R? +inline fun List.forEachLet(block: (element: T, result: R?) -> R?): R? { - var result: R? = null - forEach { result = block(it, result) } + var result = block(this[0], null) + for (i in indices) { + result = block(this[i], result) + } return result } +/** + * Returns the sum of all values produced by [selector] function applied to each element in the array. + * + * Note: This is a missing function in Kotlin standard library may be implemented later. + */ +inline fun List.sumOf(selector: (T) -> Float): Float { + var sum = 0f + for (i in indices) { + sum += selector(this[i]) + } + return sum +} -// Mappings /** - * Covers the same functions as [associateWith] with indices. + * Returns the maximum value produced by [selector] function applied to each element in the array. + * + * Despite [maxOf] this doesn't use a [Iterator] internally which can lead to a performance penalty, if + * the list is modified while iterating it can throw an [ArrayIndexOutOfBoundsException] rather than a + * [ConcurrentModificationException]. */ -inline fun Iterable.associateWithIndexed(valueSelector: (K, Int) -> V): Map -{ - var index = 0 - return associateWith { - val value = valueSelector(it, index) - index++ - value +inline fun List.fastMaxOf(selector: (T) -> Float): Float { + var result = selector(this[0]) + for (i in indices) { + result = max(result, selector(this[i])) } + return result } /** - * Returns the sum of all values produced by [selector] function applied to each element in the array. + * Returns the minimum value produced by [selector] function applied to each element in the array. * - * Note: This is a missing function in Kotlin standard library may be implemented later. + * Despite [minOf] this doesn't use a [Iterator] internally which can lead to a performance penalty, if + * the list is modified while iterating it can throw an [ArrayIndexOutOfBoundsException] rather than a + * [ConcurrentModificationException]. */ -inline fun Array.sumOf(selector: (T) -> Float): Float { - var sum = 0f - for (element in this) - sum += selector(element) - return sum +inline fun List.fastMinOf(selector: (T) -> Float): Float { + var result = selector(this[0]) + for (i in indices) { + result = min(result, selector(this[i])) + } + return result } -// Instances +// Constructors /** * Store instances from a class inheritors as singletons. */ -fun instanceMapOf() = HashMap, T>() +fun instanceMapOf() = HashMap, T>() diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Execution.kt b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Execution.kt index 7256555..ab83f05 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Execution.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Execution.kt @@ -28,9 +28,9 @@ fun delayed(time: Long, block: () -> Unit) = GlobalScope.launch { /** * Executes the block if the boolean equals `true`. */ -fun Boolean.then(block: () -> Unit): Boolean -{ - if (this) +fun Boolean.then(block: () -> Unit): Boolean { + if (this) { block() + } return this } \ No newline at end of file diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Reflection.kt b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Reflection.kt index 82df054..9cbb55b 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Reflection.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Reflection.kt @@ -1,78 +1,90 @@ package com.reco1l.toolkt.kotlin import kotlin.reflect.KClass +import kotlin.reflect.KParameter import kotlin.reflect.KProperty0 import kotlin.reflect.full.createInstance + +// Instantiation + /** * Create a new instance with the given parameters. */ -fun KClass.createInstance(vararg parameters: Any?): T -{ - if (parameters.isEmpty()) +fun KClass.createInstance(vararg parameterValues: Any?): T { + + if (parameterValues.isEmpty()) { return createInstance() + } - val constructor = constructors.find { it.parameters.size == parameters.size } - ?: + val constructor = constructors.find { it.parameters.size == parameterValues.size } + if (constructor == null) { throw IllegalArgumentException("No constructor matches the given parameters for class: $this") + } + + val parameters = constructor.parameters + val associativeMap = mutableMapOf() - return constructor.callBy(constructor.parameters.associateWithIndexed { _, i -> parameters[i] }) + for (i in parameterValues.indices) { + associativeMap[parameters[i]] = parameterValues[i] + } + + return constructor.callBy(associativeMap) } -/** - * Returns `true` if the property is lazy initialized. - */ -val KProperty0<*>.isLazyInit: Boolean - get() = getDelegate() is Lazy<*> + +// Properties /** - * Returns `true` if the lazy property was initialized or if it's not a lazy property. + * Whether the property is a lazy initialized and it's already initialized. */ val KProperty0<*>.isLazyInitialized: Boolean get() = (getDelegate() as? Lazy<*>)?.isInitialized() ?: true -// Properties - -inline fun KClass.setField( - instance: Any, - name: String, - value: V -) -{ - java.getDeclaredField(name).apply { - - if (!isAccessible) - isAccessible = true - - set(instance, value) +/** + * Sets the value of a property by its name. + * + * If the property is not accessible it'll be made it accessible forcefully. + * @see [Class.getDeclaredField] + */ +inline fun T.setField(name: String, value: V) { + val field = javaClass.getDeclaredField(name) + if (!field.isAccessible) { + field.isAccessible = true } + field.set(this, value) } -inline fun KClass.getField(instance: Any, name: String): V -{ - return java.getDeclaredField(name).let { - - if (!it.isAccessible) - it.isAccessible = true - - it.get(instance) - } as V +/** + * Gets the value of a property by its name. + * + * If the property is not accessible it'll be made it accessible forcefully. + * @see [Class.getDeclaredField] + */ +inline fun T.getField(name: String): V { + val field = javaClass.getDeclaredField(name) + if (!field.isAccessible) { + field.isAccessible = true + } + return field.get(this) as V } -fun KClass.invokeMethod( - instance: Any, - name: String, - vararg parameters: Any -) -{ - java.getDeclaredMethod(name, *parameters.map { it.javaClass }.toTypedArray()).apply { - if (!isAccessible) - isAccessible = true +// Methods - invoke(instance, *parameters) +/** + * Invokes a method by its name. + * + * If the method is not accessible it'll be made it accessible forcefully. + * @see [Class.getDeclaredMethod] + */ +fun T.invokeMethod(name: String, vararg parameters: Any) { + val method = javaClass.getDeclaredMethod(name, *parameters.map { it.javaClass }.toTypedArray()) + if (!method.isAccessible) { + method.isAccessible = true } + method.invoke(this, *parameters) } diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Standard.kt b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Standard.kt index 511421f..2da282a 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Standard.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Standard.kt @@ -1,18 +1,44 @@ package com.reco1l.toolkt.kotlin - // Cast /** - * This converts the string to boolean allowing numeric booleans (`1` for `true` and `0` for `false`). + * This converts the string to boolean allowing numeric booleans. + * + * Allowed values: + * * `0` -> `false` + * * `1` -> `true` + * * `0.0` -> `false` + * * `1.0` -> `true` + * * `true` -> `true` + * * `false` -> `false` + * * `TRUE` -> `true` + * * `FALSE` -> `false` */ -fun String.toBooleanOrNull(): Boolean? -{ - if (length == 1 && (get(0) == '0' || get(0) == '1')) - return get(0) == '1' +fun String?.toBooleanOrNull(): Boolean? { + + if (isNullOrEmpty()) { + return null + } + + // Avoiding to run equals() comparison again. + val isTrue = equals("true", true) + if (isTrue || equals("false", true)) { + return isTrue + } + + val long = toLongOrNull() + if (long == 1L || long == 0L) { + return long == 1L + } + + val double = toDoubleOrNull() + if (double == 1.0 || double == 0.0) { + return double == 1.0 + } - return try { toBoolean() } catch (_: Exception) { null } + return null } diff --git a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Strings.kt b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Strings.kt index 6e23e65..aa6f877 100644 --- a/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Strings.kt +++ b/Toolkt/src/main/java/com/reco1l/toolkt/kotlin/Strings.kt @@ -33,13 +33,14 @@ infix fun String.isBetween(pair: Pair) = startsWith(pair.first) && e * Returns a substring cropped between the specified characters or `null` if it doesn't contains * those characters or the part between them is empty. */ -fun String.between(first: Char, last: Char): String? -{ +fun String.between(first: Char, last: Char): String? { + val firstIndex = indexOf(first) val secondIndex = lastIndexOf(last) - if (firstIndex == -1 || firstIndex + 1 >= secondIndex) + if (firstIndex == -1 || firstIndex + 1 >= secondIndex) { return null + } return substring(firstIndex + 1, secondIndex).takeUnless { it.isEmpty() } } @@ -47,11 +48,11 @@ fun String.between(first: Char, last: Char): String? /** * Multiply the characters and create a sequence */ -operator fun Char.times(times: Int): CharSequence -{ +operator fun Char.times(times: Int): CharSequence { var result = "" - for (i in 0 until abs(times)) + for (i in 0 until abs(times)) { result += this + } return result } @@ -67,8 +68,7 @@ fun String.takeIfMatches(regex: Regex) = takeIf { regex.matches(it) } /** * Pack of constants with defaults regular expressions. */ -object Regexs -{ +object Regexs { /** * Regex for integer numbers. @@ -128,10 +128,11 @@ fun String.replaceAlphanumeric(with: String = "_") = replace("[^a-zA-Z0-9.\\-]". // Escapes -fun String.withTranslatedEscapes(ignoreInvalidSequences: Boolean = true): String -{ - if (isEmpty()) +fun String.withTranslatedEscapes(ignoreInvalidSequences: Boolean = true): String { + + if (isEmpty()) { return "" + } val sequence = toCharArray() val length = sequence.size @@ -139,16 +140,13 @@ fun String.withTranslatedEscapes(ignoreInvalidSequences: Boolean = true): String var from = 0 var to = 0 - while (from < length) - { + while (from < length) { var ch = sequence[from++] - if (ch == '\\') - { + if (ch == '\\') { ch = if (from < length) sequence[from++] else '\u0000' - when (ch) - { + when (ch) { 'b' -> ch = '\b' 'f' -> ch = '\u000c' 'n' -> ch = '\n' @@ -157,13 +155,11 @@ fun String.withTranslatedEscapes(ignoreInvalidSequences: Boolean = true): String 't' -> ch = '\t' '\'', '\"', '\\' -> Unit - '0', '1', '2', '3', '4', '5', '6', '7' -> - { + '0', '1', '2', '3', '4', '5', '6', '7' -> { val limit = min(from + if (ch <= '3') 2 else 1, length) var code = ch.code - '0'.code - while (from < limit) - { + while (from < limit) { ch = sequence[from] if (ch < '0' || '7' < ch) @@ -176,18 +172,18 @@ fun String.withTranslatedEscapes(ignoreInvalidSequences: Boolean = true): String } '\n' -> continue - '\r' -> - { - if (from < length && sequence[from] == '\n') + '\r' -> { + if (from < length && sequence[from] == '\n') { from++ + } continue } - else -> - { - if (!ignoreInvalidSequences) + else -> { + if (!ignoreInvalidSequences) { throw IllegalArgumentException("Invalid escape sequence: \\%c \\\\u%04X".format(ch, ch.code)) + } } } } @@ -222,19 +218,16 @@ fun String.fromDate(date: Date = Date()) = DateFormat.format(this, date).toStrin * * `HH:mm:ss` gets converted to `12:00:00` format. * * @param ms The time to be converted. + * @param sdf The [SimpleDateFormat] instance to be used. * @see SimpleDateFormat */ -fun String.formatTimeMilliseconds(ms: Long): String -{ - @SuppressLint("SimpleDateFormat") - val sdf = SimpleDateFormat(this) +fun String.formatTimeMilliseconds(ms: Long, @SuppressLint("SimpleDateFormat") sdf: SimpleDateFormat = SimpleDateFormat()): String { + sdf.applyPattern(this) sdf.timeZone = TimeZone.getTimeZone("GTM+0") - return sdf.format(ms)!! } - /** * Decodes an URL encoded string according to the specified charset, by default UTF 8. * @@ -264,34 +257,24 @@ fun String.decodeAsURL(charset: Charset = Charset.defaultCharset()) = URLDecoder * @param lowercaseWords Whether the words (excluding the first one) should be lowercased. * @param capitalize Whether the first word should be capitalized. */ -fun String.splitCamelCase( - separator: String = " ", - capitalize: Boolean = true, - lowercaseWords: Boolean = true -): String -{ +fun String.splitCamelCase(separator: String = " ", capitalize: Boolean = true, lowercaseWords: Boolean = true): String { + var r = "" - for (i in indices) - { + for (i in indices) { val char = get(i) val p = if (i > 0) get(i - 1) else ' ' val n = if (i < length - 1) get(i + 1) else ' ' - if (char.isLowerCase()) - r += if (i == 0 && capitalize) - char.uppercaseChar() - else - char - else - r += if (p.isUpperCase() && n.isUpperCase() || n == ' ') - char - else - separator + if (lowercaseWords) - char.lowercaseChar() - else - char + if (char.isLowerCase()) { + r += (if (i == 0 && capitalize) char.uppercaseChar() else char) + continue + } + + r += if (p.isUpperCase() && n.isUpperCase() || n == ' ') char else { + separator + (if (lowercaseWords) char.lowercaseChar() else char) + } } return r