diff --git a/composeApp/src/commonMain/kotlin/dev/teogor/crosslens/App.kt b/composeApp/src/commonMain/kotlin/dev/teogor/crosslens/App.kt index 4a72cce..dea5942 100644 --- a/composeApp/src/commonMain/kotlin/dev/teogor/crosslens/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/teogor/crosslens/App.kt @@ -25,9 +25,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import dev.teogor.crosslens.core.buildHashCode import dev.teogor.crosslens.ui.rememberVisibilityState import org.jetbrains.compose.ui.tooling.preview.Preview @@ -47,6 +49,14 @@ public fun App() { visibility.show() visibility.toggle() } + remember { + buildHashCode { + append(visibility.isVisible) + append(visibility.scope) + }.let { + println("HashCode: $it") + } + } MaterialTheme { Surface( modifier = Modifier.fillMaxSize(), diff --git a/crosslens-core/api/android/crosslens-core.api b/crosslens-core/api/android/crosslens-core.api index 088804b..7bda753 100644 --- a/crosslens-core/api/android/crosslens-core.api +++ b/crosslens-core/api/android/crosslens-core.api @@ -3,6 +3,21 @@ public final class dev/teogor/crosslens/core/ContextUtilsKt { public static final fun getCurrentActivity ()Landroid/app/Activity; } +public final class dev/teogor/crosslens/core/HashCodeBuilder { + public fun ()V + public fun (II)V + public synthetic fun (IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun append (Ljava/lang/Object;)Ldev/teogor/crosslens/core/HashCodeBuilder; + public final fun build ()I +} + +public final class dev/teogor/crosslens/core/HashCodeBuilderKt { + public static final fun buildHashCode (IILkotlin/jvm/functions/Function1;)I + public static synthetic fun buildHashCode$default (IILkotlin/jvm/functions/Function1;ILjava/lang/Object;)I + public static final fun buildLazyHashCode (Lkotlin/jvm/functions/Function1;)Lkotlin/Lazy; + public static final fun lazyHashCode (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy; +} + public final class dev/teogor/crosslens/core/startup/ActivityInitializer : androidx/startup/Initializer { public fun ()V public synthetic fun create (Landroid/content/Context;)Ljava/lang/Object; diff --git a/crosslens-core/api/jvm/crosslens-core.api b/crosslens-core/api/jvm/crosslens-core.api index e69de29..dbcb68b 100644 --- a/crosslens-core/api/jvm/crosslens-core.api +++ b/crosslens-core/api/jvm/crosslens-core.api @@ -0,0 +1,15 @@ +public final class dev/teogor/crosslens/core/HashCodeBuilder { + public fun ()V + public fun (II)V + public synthetic fun (IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun append (Ljava/lang/Object;)Ldev/teogor/crosslens/core/HashCodeBuilder; + public final fun build ()I +} + +public final class dev/teogor/crosslens/core/HashCodeBuilderKt { + public static final fun buildHashCode (IILkotlin/jvm/functions/Function1;)I + public static synthetic fun buildHashCode$default (IILkotlin/jvm/functions/Function1;ILjava/lang/Object;)I + public static final fun buildLazyHashCode (Lkotlin/jvm/functions/Function1;)Lkotlin/Lazy; + public static final fun lazyHashCode (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy; +} + diff --git a/crosslens-core/build.gradle.kts b/crosslens-core/build.gradle.kts index d6fbd08..5651110 100644 --- a/crosslens-core/build.gradle.kts +++ b/crosslens-core/build.gradle.kts @@ -87,6 +87,9 @@ kotlin { } commonMain.dependencies { } + commonTest.dependencies { + implementation(libs.jetbrains.kotlin.test) + } } } diff --git a/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/HashCodeBuilder.kt b/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/HashCodeBuilder.kt new file mode 100644 index 0000000..636bfc5 --- /dev/null +++ b/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/HashCodeBuilder.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2024 Teogor (Teodor Grigor) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.teogor.crosslens.core + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * A utility class for building hash codes using a custom algorithm. + * + * @property initialValue The initial value for the hash code computation. Defaults to `31`. + * @property multiplier The multiplier used to calculate the hash code. Defaults to `31`. + * + * This class allows you to append values to a hash code and compute the final hash code using + * a specified multiplier and initial value. + * + * @see [buildHashCode] for creating a hash code using a builder action. + */ +public class HashCodeBuilder( + private val initialValue: Int = 31, + private val multiplier: Int = 31, +) { + private var hashCode = initialValue + + /** + * Appends the hash code of the given value to the current hash code. + * + * @param value The value to append. If the value is an array, its deep hash code is used. + * Null values are treated as `0`. If the hash code of the value is `0`, it is + * not included. + * @return The current instance of [HashCodeBuilder], for chaining. + * + * @see [buildHashCode] for an example of how to use this method. + */ + public fun append(value: Any?): HashCodeBuilder = + apply { + val currentHashCode = + when (value) { + is Array<*> -> value.contentDeepHashCode() + else -> value?.hashCode() ?: 0 + } + if (currentHashCode != 0) { + hashCode = multiplier * hashCode + currentHashCode + } + } + + /** + * Computes and returns the final hash code. + * + * @return The final hash code computed by the builder. + * + * @see [buildHashCode] for an example of how to use this method. + */ + public fun build(): Int = hashCode +} + +/** + * Creates a hash code using a custom builder action. + * + * @param initialValue The initial value for the hash code computation. Defaults to `31`. + * @param multiplier The multiplier used in hash code calculation. Defaults to `31`. + * @param builderAction A lambda function to build the hash code using [HashCodeBuilder]. + * + * @return The computed hash code. + * + * @see [HashCodeBuilder] for details on how hash codes are computed. + */ +@OptIn(ExperimentalContracts::class) +public inline fun buildHashCode( + initialValue: Int = 31, + multiplier: Int = 31, + builderAction: HashCodeBuilder.() -> Unit, +): Int { + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + return HashCodeBuilder(initialValue, multiplier).apply(builderAction).build() +} + +/** + * Creates a lazy hash code using an initializer function. + * + * @param initializer A function that returns the hash code value. + * + * @return A [Lazy] instance that computes the hash code when accessed. + * + * @see [buildLazyHashCode] for creating a lazy hash code using a [HashCodeBuilder]. + */ +public fun lazyHashCode(initializer: () -> Int): Lazy = + lazy(LazyThreadSafetyMode.PUBLICATION) { initializer() } + +/** + * Creates a lazy hash code using a builder initializer function. + * + * @param initializer A lambda function to build the hash code using [HashCodeBuilder]. + * + * @return A [Lazy] instance that computes the hash code when accessed. + * + * @see [HashCodeBuilder] for details on hash code computation. + */ +public inline fun buildLazyHashCode(crossinline initializer: HashCodeBuilder.() -> Unit): Lazy = + lazyHashCode { HashCodeBuilder().apply(initializer).build() } diff --git a/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/root.kt b/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/root.kt deleted file mode 100644 index 1d55bb1..0000000 --- a/crosslens-core/src/commonMain/kotlin/dev/teogor/crosslens/core/root.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2024 Teogor (Teodor Grigor) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package dev.teogor.crosslens.core diff --git a/crosslens-core/src/commonTest/kotlin/dev/teogor/crosslens/core/HashCodeBuilderTest.kt b/crosslens-core/src/commonTest/kotlin/dev/teogor/crosslens/core/HashCodeBuilderTest.kt new file mode 100644 index 0000000..2455651 --- /dev/null +++ b/crosslens-core/src/commonTest/kotlin/dev/teogor/crosslens/core/HashCodeBuilderTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2024 Teogor (Teodor Grigor) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.teogor.crosslens.core + +import kotlin.test.Test +import kotlin.test.assertEquals + +class HashCodeBuilderTest { + @Test + fun initialHashCode_shouldBe31() { + val builder = + HashCodeBuilder() + .build() + assertEquals(31, builder) + } + + @Test + fun appendNullValue_shouldResultInInitialHashCode() { + val hashCodeActual = + buildHashCode { + append(null) + } + val hashCodeExpected = 31 + assertEquals( + expected = hashCodeExpected, + actual = hashCodeActual, + message = "Null value should result in initial hash code (31)", + ) + } + + @Test + fun appendSingleValue_shouldCalculateHashCodeCorrectly() { + val hashCodeActual = + buildHashCode { + append("test") + } + val hashCodeExpected = 31 * 31 + "test".hashCode() + assertEquals( + expected = hashCodeExpected, + actual = hashCodeActual, + message = "Hash code should be calculated using initial value and string's hash code", + ) + } + + @Test + fun appendMultipleValues_shouldCalculateHashCodeCorrectly() { + val hashCodeActual = + buildHashCode { + append("test") + append(123) + append(45.67) + } + val hashCodeExpected = + ( + (31 * 31 + "test".hashCode()) * 31 + 123.hashCode() + ) * 31 + 45.67.hashCode() + assertEquals( + expected = hashCodeExpected, + actual = hashCodeActual, + ) + } + + @Test + fun appendSameValueMultipleTimes_shouldCalculateHashCodeCorrectly() { + val hashCodeActual = + buildHashCode { + append("same") + append("same") + append("same") + } + val hashCodeExpected = + ( + (31 * 31 + "same".hashCode()) * 31 + "same".hashCode() + ) * 31 + "same".hashCode() + assertEquals( + expected = hashCodeExpected, + actual = hashCodeActual, + ) + } + + @Test + fun hashCodeConsistency_shouldBeMaintained() { + val builder = + HashCodeBuilder() + .append("consistent") + .append("value") + val hashCode1 = builder.build() + val hashCode2 = builder.build() + assertEquals(hashCode1, hashCode2) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3de8ed..c3a8b4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,8 +25,8 @@ androidx-startup = "1.1.1" [libraries] androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" } -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +jetbrains-kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +jetbrains-kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }