Skip to content

Commit

Permalink
Merge pull request #2 from teogor/feature/hashcode-builder
Browse files Browse the repository at this point in the history
Introduce HashCodeBuilder Utility and Lazy HashCode Functions
  • Loading branch information
teogor authored Sep 2, 2024
2 parents 1516564 + bd34152 commit 7d8db41
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 19 deletions.
10 changes: 10 additions & 0 deletions composeApp/src/commonMain/kotlin/dev/teogor/crosslens/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(),
Expand Down
15 changes: 15 additions & 0 deletions crosslens-core/api/android/crosslens-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
public fun <init> (II)V
public synthetic fun <init> (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 <init> ()V
public synthetic fun create (Landroid/content/Context;)Ljava/lang/Object;
Expand Down
15 changes: 15 additions & 0 deletions crosslens-core/api/jvm/crosslens-core.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
public final class dev/teogor/crosslens/core/HashCodeBuilder {
public fun <init> ()V
public fun <init> (II)V
public synthetic fun <init> (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;
}

3 changes: 3 additions & 0 deletions crosslens-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ kotlin {
}
commonMain.dependencies {
}
commonTest.dependencies {
implementation(libs.jetbrains.kotlin.test)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Int> =
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<Int> =
lazyHashCode { HashCodeBuilder().apply(initializer).build() }

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down

0 comments on commit 7d8db41

Please sign in to comment.