diff --git a/README.md b/README.md index 0dbf7f5..9c5dcc4 100644 --- a/README.md +++ b/README.md @@ -56,18 +56,38 @@ and equivalents for `getQuantityString()` and `getQuantityText()` provided by QuantityStringResource(R.plurals.res_id, 1, "arg") QuantityTextResource(R.plurals.res_id, 2) -While `AppId` or `AppVersion` just provide data from the `Context` itself. +While `AppId` or `AppVersion` just provide the data from the `Context` itself. ### `AString` Transformers -While `AString` represent any generic `CharSequence`, it might be useful to -transform the value before its use. +Since the `AString` represent any generic `CharSequence`, it might be useful +to transform the value before its use. aString.format("arg", AppId) + aString.defaultIfNull("value") aString.nullIfBlank() - aString.string() + aString.mapToString() aString.trim() +### `AString` Reducers + +If multiple instances of `AString` are provided, it is also possible to reduce +these to a single and single `AString`. + + aStrings.firstNonBlank() + aStrings.joinNonNull(',') + +The provided values of the `Iterable` are provided lazily. + +### `@InefficientAStringApi AString` usages + +Generic `AString.Provider`, `AString.Transformer` and `AString.Reducer` are +supported but do not provide comparability or readable string representations. + + AString { it.packageName } + aString.map { it?.toString() } + aStrings.reduce { it.singleOrNull() } + ### _Jetpack_ Compose extension functions [![API][compose-shield]][compose] diff --git a/astring/src/androidTest/java/xyz/tynn/astring/AStringReducerAndroidTest.java b/astring/src/androidTest/java/xyz/tynn/astring/AStringReducerAndroidTest.java new file mode 100644 index 0000000..fa8d115 --- /dev/null +++ b/astring/src/androidTest/java/xyz/tynn/astring/AStringReducerAndroidTest.java @@ -0,0 +1,120 @@ +// Copyright 2023 Christian Schmitz +// SPDX-License-Identifier: Apache-2.0 + +package xyz.tynn.astring; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static xyz.tynn.astring.AStringFactory.createFromCharSequence; +import static xyz.tynn.astring.test.AStringAssert.assertParcelableAStringInvocation; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.junit.Test; + +import java.util.List; + +public class AStringReducerAndroidTest { + + private final Iterable aStrings = List.of(AString.Null); + + @Test + public void delegate_should_implement_parcelable() { + String value = "value"; + assertParcelableAStringInvocation(Delegate.wrap($ -> value, aStrings)); + assertParcelableAStringInvocation(Delegate.wrap($ -> value, + AString.Null, createFromCharSequence("value"))); + } + + @Test + @SuppressWarnings("Convert2Lambda") + public void interface_should_not_be_efficient() { + assertNotEquals(AStringFactory.reduce(aStrings, new AString.Reducer() { + @Override + public CharSequence invoke(@NonNull Iterable values) { + return ""; + } + }), AStringFactory.reduce(aStrings, new AString.Reducer() { + @Override + public CharSequence invoke(@NonNull Iterable values) { + return ""; + } + })); + } + + @Test + @SuppressWarnings("Convert2Lambda") + public void interface_ref_should_be_efficient() { + AString.Reducer function = new AString.Reducer() { + @Override + public CharSequence invoke(@NonNull Iterable values) { + return ""; + } + }; + assertEquals(AStringFactory.reduce(aStrings, function), + AStringFactory.reduce(aStrings, function)); + } + + @Test + public void instance_should_be_efficient() { + assertEquals(AStringFactory.reduce(aStrings, new Reducer()), + AStringFactory.reduce(aStrings, new Reducer())); + } + + @Test + public void instance_ref_should_be_efficient() { + AString.Reducer function = new Reducer(); + assertEquals(AStringFactory.reduce(aStrings, function), + AStringFactory.reduce(aStrings, function)); + } + + @Test + public void function_should_not_be_efficient() { + assertNotEquals(AStringFactory.reduce(aStrings, this::function), + AStringFactory.reduce(aStrings, this::function)); + } + + @Test + public void function_ref_should_be_efficient() { + AString.Reducer function = this::function; + assertEquals(AStringFactory.reduce(aStrings, function), + AStringFactory.reduce(aStrings, function)); + } + + @Test + public void lambda_should_not_be_efficient() { + assertNotEquals(AStringFactory.reduce(aStrings, values -> ""), + AStringFactory.reduce(aStrings, values -> "")); + } + + @Test + public void lambda_ref_should_be_efficient() { + AString.Reducer function = values -> ""; + assertEquals(AStringFactory.reduce(aStrings, function), + AStringFactory.reduce(aStrings, function)); + } + + private CharSequence function(Iterable values) { + return ""; + } + + private final static class Reducer implements AString.Reducer { + + @Nullable + @Override + public CharSequence invoke(@NonNull Iterable values) { + return null; + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof Reducer; + } + + @Override + public int hashCode() { + return 0; + } + } +} diff --git a/astring/src/androidTest/java/xyz/tynn/astring/ParcelableAStringTest.java b/astring/src/androidTest/java/xyz/tynn/astring/ParcelableAStringTest.java index 1b1bf8b..8459d1e 100644 --- a/astring/src/androidTest/java/xyz/tynn/astring/ParcelableAStringTest.java +++ b/astring/src/androidTest/java/xyz/tynn/astring/ParcelableAStringTest.java @@ -65,18 +65,18 @@ public void Delegate_should_implement_parcelable() { assertParcelableAStringEquality(Delegate.wrap(Provider.AppVersion)); assertParcelableAStringInvocation(Delegate.wrap(Provider.AppVersion)); assertParcelableAStringInvocation(Delegate.wrap(Object::toString)); - assertParcelableAStringIdentity(Delegate.wrap(null, null)); - assertParcelableAStringInvocation(Delegate.wrap(null, null)); - assertParcelableAStringEquality(Delegate.wrap(Predicate.NonBlank, FORMAT)); - assertParcelableAStringInvocation(Delegate.wrap(Predicate.NonBlank, FORMAT)); - assertParcelableAStringEquality(Delegate.wrap(Predicate.NonEmpty, FORMAT)); - assertParcelableAStringInvocation(Delegate.wrap(Predicate.NonEmpty, FORMAT)); - assertParcelableAStringEquality(Delegate.wrap(Predicate.NonNull, FORMAT)); - assertParcelableAStringInvocation(Delegate.wrap(Predicate.NonNull, FORMAT)); - assertParcelableAStringEquality(Delegate.wrap(Transformer.ToString, FORMAT)); - assertParcelableAStringInvocation(Delegate.wrap(Transformer.ToString, FORMAT)); - assertParcelableAStringEquality(Delegate.wrap(Transformer.Trim, FORMAT)); - assertParcelableAStringInvocation(Delegate.wrap(Transformer.Trim, FORMAT)); + assertParcelableAStringIdentity(Delegate.wrap((AString) null, null)); + assertParcelableAStringInvocation(Delegate.wrap((AString) null, null)); + assertParcelableAStringEquality(Delegate.wrap(FORMAT, Predicate.NonBlank)); + assertParcelableAStringInvocation(Delegate.wrap(FORMAT, Predicate.NonBlank)); + assertParcelableAStringEquality(Delegate.wrap(FORMAT, Predicate.NonEmpty)); + assertParcelableAStringInvocation(Delegate.wrap(FORMAT, Predicate.NonEmpty)); + assertParcelableAStringEquality(Delegate.wrap(FORMAT, Predicate.NonNull)); + assertParcelableAStringInvocation(Delegate.wrap(FORMAT, Predicate.NonNull)); + assertParcelableAStringEquality(Delegate.wrap(FORMAT, Transformer.ToString)); + assertParcelableAStringInvocation(Delegate.wrap(FORMAT, Transformer.ToString)); + assertParcelableAStringEquality(Delegate.wrap(FORMAT, Transformer.Trim)); + assertParcelableAStringInvocation(Delegate.wrap(FORMAT, Transformer.Trim)); } private static class FormatAString implements AString { diff --git a/astring/src/androidTest/kotlin/xyz/tynn/astring/AStringReducerKtAndroidTest.kt b/astring/src/androidTest/kotlin/xyz/tynn/astring/AStringReducerKtAndroidTest.kt new file mode 100644 index 0000000..0b320d3 --- /dev/null +++ b/astring/src/androidTest/kotlin/xyz/tynn/astring/AStringReducerKtAndroidTest.kt @@ -0,0 +1,230 @@ +// Copyright 2023 Christian Schmitz +// SPDX-License-Identifier: Apache-2.0 + +package xyz.tynn.astring + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import xyz.tynn.astring.test.AStringAssert.assertParcelableAStringEquality +import xyz.tynn.astring.test.AStringAssert.assertParcelableAStringInvocation + +@InefficientAStringApi +internal class AStringReducerKtAndroidTest { + + private val aString1 = AString.Null + private val aString2 = "+".asAString() + private val aStrings = listOf(aString1, aString2) + private val aStringIterable = sequenceOf(aString1, aString2).asIterable() + + @Test + fun firstNonBlank_should_be_parcelable() { + assertParcelableAStringEquality(firstNonBlank(aString1, aString2)) + assertParcelableAStringInvocation(firstNonBlank(aString1, aString2)) + assertParcelableAStringEquality(aStrings.firstNonBlank()) + assertParcelableAStringInvocation(aStrings.firstNonBlank()) + assertParcelableAStringEquality(aStringIterable.firstNonBlank()) + assertParcelableAStringInvocation(aStringIterable.firstNonBlank()) + } + + @Test + fun firstNonEmpty_should_be_parcelable() { + assertParcelableAStringEquality(firstNonEmpty(aString1, aString2)) + assertParcelableAStringInvocation(firstNonEmpty(aString1, aString2)) + assertParcelableAStringEquality(aStrings.firstNonEmpty()) + assertParcelableAStringInvocation(aStrings.firstNonEmpty()) + assertParcelableAStringEquality(aStringIterable.firstNonEmpty()) + assertParcelableAStringInvocation(aStringIterable.firstNonEmpty()) + } + + @Test + fun firstNonNull_should_be_parcelable() { + assertParcelableAStringEquality(firstNonNull(aString1, aString2)) + assertParcelableAStringInvocation(firstNonNull(aString1, aString2)) + assertParcelableAStringEquality(aStrings.firstNonNull()) + assertParcelableAStringInvocation(aStrings.firstNonNull()) + assertParcelableAStringEquality(aStringIterable.firstNonNull()) + assertParcelableAStringInvocation(aStringIterable.firstNonNull()) + } + + @Test + fun join_should_be_parcelable() { + assertParcelableAStringEquality(join(aString1, aString2, separator = "-")) + assertParcelableAStringInvocation(join(aString1, aString2, separator = "-")) + assertParcelableAStringEquality(aStrings.join(separator = "-")) + assertParcelableAStringInvocation(aStrings.join(separator = "-")) + assertParcelableAStringEquality(aStringIterable.join(separator = "-")) + assertParcelableAStringInvocation(aStringIterable.join(separator = "-")) + } + + @Test + fun joinNonBlank_should_be_parcelable() { + assertParcelableAStringEquality(joinNonBlank(aString1, aString2, separator = "-")) + assertParcelableAStringInvocation(joinNonBlank(aString1, aString2, separator = "-")) + assertParcelableAStringEquality(aStrings.joinNonBlank(separator = "-")) + assertParcelableAStringInvocation(aStrings.joinNonBlank(separator = "-")) + assertParcelableAStringEquality(aStringIterable.joinNonBlank(separator = "-")) + assertParcelableAStringInvocation(aStringIterable.joinNonBlank(separator = "-")) + } + + @Test + fun joinNonEmpty_should_be_parcelable() { + assertParcelableAStringEquality(joinNonEmpty(aString1, aString2, separator = "-")) + assertParcelableAStringInvocation(joinNonEmpty(aString1, aString2, separator = "-")) + assertParcelableAStringEquality(aStrings.joinNonEmpty(separator = "-")) + assertParcelableAStringInvocation(aStrings.joinNonEmpty(separator = "-")) + assertParcelableAStringEquality(aStringIterable.joinNonEmpty(separator = "-")) + assertParcelableAStringInvocation(aStringIterable.joinNonEmpty(separator = "-")) + } + + @Test + fun joinNonNull_should_be_parcelable() { + assertParcelableAStringEquality(joinNonNull(aString1, aString2, separator = "-")) + assertParcelableAStringInvocation(joinNonNull(aString1, aString2, separator = "-")) + assertParcelableAStringEquality(aStrings.joinNonNull(separator = "-")) + assertParcelableAStringInvocation(aStrings.joinNonNull(separator = "-")) + assertParcelableAStringEquality(aStringIterable.joinNonNull(separator = "-")) + assertParcelableAStringInvocation(aStringIterable.joinNonNull(separator = "-")) + } + + @Test + @Suppress("RedundantSamConstructor") + fun interface_should_not_be_efficient() { + assertNotEquals( + reduce(aString1, aString2, reducer = AString.Reducer { "" }), + reduce(aString1, aString2, reducer = AString.Reducer { "" }), + ) + assertNotEquals( + reduce(aString1, aString2, reducer = AString.Reducer { "" }), + aStrings.reduce(reducer = AString.Reducer { "" }), + ) + assertNotEquals( + reduce(aString1, aString2, reducer = AString.Reducer { "" }), + aStringIterable.reduce(reducer = AString.Reducer { "" }), + ) + } + + @Test + fun interface_val_should_be_efficient() { + val function = AString.Reducer { "" } + assertEquals( + reduce(aString1, aString2, reducer = function), + reduce(aString1, aString2, reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + aStrings.reduce(reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + aStringIterable.reduce(reducer = function), + ) + } + + @Test + fun instance_should_be_efficient() { + assertEquals( + reduce(aString1, aString2, reducer = Reducer()), + reduce(aString1, aString2, reducer = Reducer()), + ) + assertEquals( + reduce(aString1, aString2, reducer = Reducer()), + aStrings.reduce(reducer = Reducer()), + ) + assertEquals( + reduce(aString1, aString2, reducer = Reducer()), + aStringIterable.reduce(reducer = Reducer()), + ) + } + + @Test + fun instance_val_should_be_efficient() { + val function = Reducer() + assertEquals( + reduce(aString1, aString2, reducer = function), + reduce(aString1, aString2, reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + aStrings.reduce(reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + aStringIterable.reduce(reducer = function), + ) + } + + @Test + fun function_reference_should_be_efficient() { + assertEquals( + reduce(aString1, aString2, reducer = ::function), + reduce(aString1, aString2, reducer = ::function), + ) + assertEquals( + reduce(aString1, aString2, reducer = ::function), + aStrings.reduce(reducer = ::function), + ) + assertEquals( + reduce(aString1, aString2, reducer = ::function), + aStringIterable.reduce(reducer = ::function), + ) + } + + @Test + fun function_reference_val_should_be_efficient() { + val function = ::function + assertEquals( + reduce(aString1, aString2, reducer = function), + reduce(aString1, aString2, reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + aStrings.reduce(reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + aStringIterable.reduce(reducer = function), + ) + } + + @Test + fun lambda_should_not_be_efficient() { + assertNotEquals( + reduce(aString1, aString2) { it.toString() }, + reduce(aString1, aString2) { it.toString() }, + ) + assertNotEquals( + reduce(aString1, aString2) { it.toString() }, + aStrings.reduce { it.toString() }, + ) + assertNotEquals( + reduce(aString1, aString2) { it.toString() }, + aStringIterable.reduce { it.toString() }, + ) + } + + @Test + fun lambda_val_should_be_efficient() { + val function = { _: Iterable -> "" } + assertEquals( + reduce(aString1, aString2, reducer = function), + reduce(aString1, aString2, reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + aStrings.reduce(reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + aStringIterable.reduce(reducer = function), + ) + } + + private fun function(values: Iterable) = values.first() + + private class Reducer : AString.Reducer { + override fun invoke(values: Iterable) = "" + override fun equals(other: Any?) = other is Reducer + override fun hashCode() = 0 + } +} diff --git a/astring/src/main/java/xyz/tynn/astring/Delegate.java b/astring/src/main/java/xyz/tynn/astring/Delegate.java index 386de60..43d76a6 100644 --- a/astring/src/main/java/xyz/tynn/astring/Delegate.java +++ b/astring/src/main/java/xyz/tynn/astring/Delegate.java @@ -15,7 +15,10 @@ import androidx.annotation.Nullable; import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; import java.util.Objects; final class Delegate implements AString { @@ -39,7 +42,22 @@ static AString wrap(AString.Provider provider) { } @InefficientAStringApi - static AString wrap(AString.Transformer transformer, AString aString) { + static AString wrap(AString.Reducer reducer, AString... aStrings) { + if (reducer == null || aStrings == null || aStrings.length == 0) return Null; + return new Delegate(new Serializer.Reducer(reducer), aStrings); + } + + @InefficientAStringApi + static AString wrap(AString.Reducer reducer, Iterable aStrings) { + if (reducer == null || aStrings == null) return Null; + Collection list = aStrings instanceof Collection + ? (Collection) aStrings : new ArrayList<>(); + if (list != aStrings) for (AString aString : aStrings) list.add(aString); + return wrap(reducer, list.toArray(EMPTY)); + } + + @InefficientAStringApi + static AString wrap(AString aString, Transformer transformer) { if (transformer == null) return Null; if (aString == null || aString == Null) return Wrapper.wrap(transformer.invoke(null)); if (aString instanceof Wrapper) return ((Wrapper) aString).map(transformer); @@ -124,6 +142,65 @@ public String toString() { } } + @InefficientAStringApi + static final class Reducer extends Serializer { + + Reducer(AString.Reducer delegate) { + super(delegate); + } + + @Nullable + @Override + public CharSequence invoke(@NonNull Context context, @NonNull AString[] aStrings) { + try (LazyIterable iterable = new LazyIterable(context, aStrings)) { + return delegate.invoke(iterable); + } + } + + @NonNull + @Override + public String toString() { + return "Reduce(" + delegate + ')'; + } + + private static final class LazyIterable implements AutoCloseable, Iterable { + + Context context; + AString[] aStrings; + + LazyIterable(Context context, AString[] aStrings) { + this.context = context; + this.aStrings = aStrings; + } + + @Override + public void close() { + context = null; + aStrings = null; + } + + @NonNull + @Override + public Iterator iterator() { + return new Iterator<>() { + private int i = 0; + + @Override + public boolean hasNext() { + return i < aStrings.length; + } + + @Override + public CharSequence next() { + if (context == null || aStrings == null) + throw new AssertionError("accessed values after invocation"); + return aStrings[i++].invoke(context); + } + }; + } + } + } + @InefficientAStringApi static final class Transformer extends Serializer { diff --git a/astring/src/main/java/xyz/tynn/astring/Joiner.java b/astring/src/main/java/xyz/tynn/astring/Joiner.java new file mode 100644 index 0000000..16f7ff9 --- /dev/null +++ b/astring/src/main/java/xyz/tynn/astring/Joiner.java @@ -0,0 +1,59 @@ +// Copyright 2023 Christian Schmitz +// SPDX-License-Identifier: Apache-2.0 + +package xyz.tynn.astring; + +import android.annotation.SuppressLint; +import android.text.SpannableStringBuilder; +import android.text.Spanned; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +@SuppressLint("UnsafeOptInUsageError") +class Joiner implements AString.Reducer { + + private final String separator; + private final Predicate predicate; + + Joiner(String separator, Predicate predicate) { + this.separator = separator; + this.predicate = predicate; + } + + @Nullable + @Override + public CharSequence invoke(@NonNull Iterable values) { + boolean isSpanned = false; + SpannableStringBuilder sb = new SpannableStringBuilder(); + int count = 0; + for (CharSequence value : values) + if (predicate.test(value)) { + isSpanned |= value instanceof Spanned; + if (count++ > 0) sb.append(separator); + sb.append(value == null ? "null" : value); + } + return isSpanned ? sb : sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Joiner)) return false; + Joiner that = (Joiner) o; + return predicate == that.predicate && separator.equals(that.separator); + } + + @Override + public int hashCode() { + return Objects.hash(separator, predicate); + } + + @NonNull + @Override + public String toString() { + return "Join('" + separator + "'," + predicate + ')'; + } +} diff --git a/astring/src/main/java/xyz/tynn/astring/Predicate.java b/astring/src/main/java/xyz/tynn/astring/Predicate.java index 37d0d61..de7741d 100644 --- a/astring/src/main/java/xyz/tynn/astring/Predicate.java +++ b/astring/src/main/java/xyz/tynn/astring/Predicate.java @@ -12,7 +12,20 @@ import androidx.annotation.Nullable; @SuppressLint("UnsafeOptInUsageError") -enum Predicate implements AString.Transformer { +enum Predicate implements AString.Reducer, AString.Transformer { + + AnyValue(null) { + @Override + boolean test(CharSequence value) { + return true; + } + + @Override + @androidx.annotation.NonNull + public String toString() { + return "Any"; + } + }, NonBlank(null) { @Override @@ -71,4 +84,11 @@ public String toString() { public CharSequence invoke(@Nullable CharSequence value) { return test(value) ? value : defaultValue; } + + @Nullable + @Override + public CharSequence invoke(@androidx.annotation.NonNull Iterable values) { + for (CharSequence value : values) if (test(value)) return value; + return null; + } } diff --git a/astring/src/main/kotlin/xyz/tynn/astring/AString.kt b/astring/src/main/kotlin/xyz/tynn/astring/AString.kt index 3693b7e..9402d05 100644 --- a/astring/src/main/kotlin/xyz/tynn/astring/AString.kt +++ b/astring/src/main/kotlin/xyz/tynn/astring/AString.kt @@ -54,6 +54,14 @@ public interface AString : Parcelable { public operator fun invoke(context: Context): CharSequence? } + /** + * Functional interface reducing [CharSequence] provided by a lazy [Iterable] of [AString] + */ + @InefficientAStringApi + public fun interface Reducer : Serializable { + public operator fun invoke(values: Iterable): CharSequence? + } + /** * Functional interface transforming a [CharSequence] provided by an [AString] */ diff --git a/astring/src/main/kotlin/xyz/tynn/astring/AStringImpl.kt b/astring/src/main/kotlin/xyz/tynn/astring/AStringImpl.kt index 444f728..77b8a5e 100644 --- a/astring/src/main/kotlin/xyz/tynn/astring/AStringImpl.kt +++ b/astring/src/main/kotlin/xyz/tynn/astring/AStringImpl.kt @@ -46,7 +46,7 @@ public inline fun AString?.asAString(): AString = AString( * aString ?: AString.NULL * ``` */ -@JvmName("ensureAStringNull") +@JvmName("ensureNonNull") public fun AString( aString: AString?, ): AString = aString ?: AString.Null diff --git a/astring/src/main/kotlin/xyz/tynn/astring/AStringProvider.kt b/astring/src/main/kotlin/xyz/tynn/astring/AStringProvider.kt index 7350a06..d4f0e46 100644 --- a/astring/src/main/kotlin/xyz/tynn/astring/AStringProvider.kt +++ b/astring/src/main/kotlin/xyz/tynn/astring/AStringProvider.kt @@ -11,10 +11,10 @@ import android.os.Parcelable import java.io.Serializable /** - * Creates an [AString] by wrapping the [AStringProvider] + * Creates an [AString] by wrapping the [AString.Provider] * * **Note**: While [AString] implements [Parcelable], - * the [AStringProvider] only implements [Serializable] + * the [AString.Provider] only implements [Serializable] * and thus can be implemented with a single lambda */ @[InefficientAStringApi JvmName("createAString")] diff --git a/astring/src/main/kotlin/xyz/tynn/astring/AStringReducer.kt b/astring/src/main/kotlin/xyz/tynn/astring/AStringReducer.kt new file mode 100644 index 0000000..2293944 --- /dev/null +++ b/astring/src/main/kotlin/xyz/tynn/astring/AStringReducer.kt @@ -0,0 +1,210 @@ +// Copyright 2023 Christian Schmitz +// SPDX-License-Identifier: Apache-2.0 + +@file:[JvmName("AStringFactory") JvmMultifileClass Suppress("FunctionName")] + +package xyz.tynn.astring + +import xyz.tynn.astring.Predicate.AnyValue +import xyz.tynn.astring.Predicate.NonBlank +import xyz.tynn.astring.Predicate.NonEmpty +import xyz.tynn.astring.Predicate.NonNull + +/** + * Reduces an [Array] of [AString] to a single [CharSequence] + */ +@InefficientAStringApi +public fun reduce( + vararg aStrings: AString, + reducer: AString.Reducer, +): AString = Delegate.wrap( + reducer, + *aStrings, +) + +/** + * Reduces an [Iterable] of [AString] to a single [CharSequence] + */ +@InefficientAStringApi +public fun Iterable.reduce( + reducer: AString.Reducer, +): AString = Delegate.wrap( + reducer, + this, +) + +/** + * Reduces all [aStrings] by returning the first non-blank [CharSequence] value + */ +@OptIn(InefficientAStringApi::class) +public fun firstNonBlank( + vararg aStrings: AString, +): AString = Delegate.wrap( + NonBlank, + *aStrings, +) + +/** + * Reduces all items by returning the first non-blank [CharSequence] value + */ +@OptIn(InefficientAStringApi::class) +public fun Iterable.firstNonBlank(): AString = Delegate.wrap( + NonBlank, + this, +) + +/** + * Reduces all [aStrings] by returning the first non-empty [CharSequence] value + */ +@OptIn(InefficientAStringApi::class) +public fun firstNonEmpty( + vararg aStrings: AString, +): AString = Delegate.wrap( + NonEmpty, + *aStrings, +) + +/** + * Reduces all items by returning the first non-empty [CharSequence] value + */ +@OptIn(InefficientAStringApi::class) +public fun Iterable.firstNonEmpty(): AString = Delegate.wrap( + NonEmpty, + this, +) + +/** + * Reduces all [aStrings] by returning the first non-null [CharSequence] value + */ +@OptIn(InefficientAStringApi::class) +public fun firstNonNull( + vararg aStrings: AString, +): AString = Delegate.wrap( + NonNull, + *aStrings, +) + +/** + * Reduces all items by returning the first non-null [CharSequence] value + */ +@OptIn(InefficientAStringApi::class) +public fun Iterable.firstNonNull(): AString = Delegate.wrap( + NonNull, + this, +) + +/** + * Reduces all [aStrings] by joining all nullable [CharSequence] values + */ +@OptIn(InefficientAStringApi::class) +public fun join( + vararg aStrings: AString, + separator: String, +): AString = Delegate.wrap( + Joiner( + separator, + AnyValue, + ), + *aStrings, +) + +/** + * Reduces all items by joining all nullable [CharSequence] values + */ +@OptIn(InefficientAStringApi::class) +public fun Iterable.join( + separator: String, +): AString = Delegate.wrap( + Joiner( + separator, + AnyValue, + ), + this, +) + +/** + * Reduces all [aStrings] by joining all non-null [CharSequence] values + */ +@OptIn(InefficientAStringApi::class) +public fun joinNonBlank( + vararg aStrings: AString, + separator: String, +): AString = Delegate.wrap( + Joiner( + separator, + NonBlank, + ), + *aStrings, +) + +/** + * Reduces all items by joining all non-null [CharSequence] values + */ +@OptIn(InefficientAStringApi::class) +public fun Iterable.joinNonBlank( + separator: String, +): AString = Delegate.wrap( + Joiner( + separator, + NonBlank, + ), + this, +) + +/** + * Reduces all [aStrings] by joining all non-null [CharSequence] values + */ +@OptIn(InefficientAStringApi::class) +public fun joinNonEmpty( + vararg aStrings: AString, + separator: String, +): AString = Delegate.wrap( + Joiner( + separator, + NonEmpty, + ), + *aStrings, +) + +/** + * Reduces all items by joining all non-null [CharSequence] values + */ +@OptIn(InefficientAStringApi::class) +public fun Iterable.joinNonEmpty( + separator: String, +): AString = Delegate.wrap( + Joiner( + separator, + NonEmpty, + ), + this, +) + +/** + * Reduces all [aStrings] by joining all non-null [CharSequence] values + */ +@OptIn(InefficientAStringApi::class) +public fun joinNonNull( + vararg aStrings: AString, + separator: String, +): AString = Delegate.wrap( + Joiner( + separator, + NonNull, + ), + *aStrings, +) + +/** + * Reduces all items by joining all non-null [CharSequence] values + */ +@OptIn(InefficientAStringApi::class) +public fun Iterable.joinNonNull( + separator: String, +): AString = Delegate.wrap( + Joiner( + separator, + NonNull, + ), + this, +) diff --git a/astring/src/main/kotlin/xyz/tynn/astring/AStringTransformer.kt b/astring/src/main/kotlin/xyz/tynn/astring/AStringTransformer.kt index e85979b..55f2893 100644 --- a/astring/src/main/kotlin/xyz/tynn/astring/AStringTransformer.kt +++ b/astring/src/main/kotlin/xyz/tynn/astring/AStringTransformer.kt @@ -19,8 +19,8 @@ import java.util.Locale public fun AString.map( transformer: AString.Transformer, ): AString = Delegate.wrap( - transformer, this, + transformer, ) /** @@ -63,6 +63,78 @@ public fun AString?.format( formatArgs, ) +/** + * Maps a blank `CharSequence` to [defaultValue] + */ +@[JvmName("mapBlankToDefault") OptIn(InefficientAStringApi::class)] +public fun AString?.defaultIfBlank( + defaultValue: CharSequence, +): AString = Delegate.wrap( + NonBlank, + this, + AString(defaultValue), +) + +/** + * Maps a blank `CharSequence` to [defaultValue] + */ +@[JvmName("mapBlankToDefault") OptIn(InefficientAStringApi::class)] +public fun AString?.defaultIfBlank( + defaultValue: AString, +): AString = Delegate.wrap( + NonBlank, + this, + defaultValue, +) + +/** + * Maps a empty `CharSequence` to [defaultValue] + */ +@[JvmName("mapEmptyToDefault") OptIn(InefficientAStringApi::class)] +public fun AString?.defaultIfEmpty( + defaultValue: CharSequence, +): AString = Delegate.wrap( + NonEmpty, + this, + AString(defaultValue), +) + +/** + * Maps a empty `CharSequence` to [defaultValue] + */ +@[JvmName("mapEmptyToDefault") OptIn(InefficientAStringApi::class)] +public fun AString?.defaultIfEmpty( + defaultValue: AString, +): AString = Delegate.wrap( + NonEmpty, + this, + defaultValue, +) + +/** + * Maps a null `CharSequence` to [defaultValue] + */ +@[JvmName("mapNullToDefault") OptIn(InefficientAStringApi::class)] +public fun AString?.defaultIfNull( + defaultValue: CharSequence, +): AString = Delegate.wrap( + NonNull, + this, + AString(defaultValue), +) + +/** + * Maps a null `CharSequence` to [defaultValue] + */ +@[JvmName("mapNullToDefault") OptIn(InefficientAStringApi::class)] +public fun AString?.defaultIfNull( + defaultValue: AString, +): AString = Delegate.wrap( + NonNull, + this, + defaultValue, +) + /** * Maps a null [AString] `CharSequence` to an empty `String` * @@ -70,8 +142,8 @@ public fun AString?.format( */ @[JvmName("mapNullToEmpty") OptIn(InefficientAStringApi::class)] public fun AString?.emptyIfNull(): AString = Delegate.wrap( - NonNull, this, + NonNull, ) /** @@ -81,8 +153,8 @@ public fun AString?.emptyIfNull(): AString = Delegate.wrap( */ @[JvmName("mapBlankToNull") OptIn(InefficientAStringApi::class)] public fun AString?.nullIfBlank(): AString = Delegate.wrap( - NonBlank, this, + NonBlank, ) /** @@ -92,8 +164,8 @@ public fun AString?.nullIfBlank(): AString = Delegate.wrap( */ @[JvmName("mapEmptyToNull") OptIn(InefficientAStringApi::class)] public fun AString?.nullIfEmpty(): AString = Delegate.wrap( - NonEmpty, this, + NonEmpty, ) /** @@ -102,7 +174,7 @@ public fun AString?.nullIfEmpty(): AString = Delegate.wrap( * Returns [AString.Null] if this [AString] is `null` or [AString.Null] */ @JvmName("mapCharSequenceToString") -public fun AString?.string(): AString = Format.wrap( +public fun AString?.mapToString(): AString = Format.wrap( this, null, null, @@ -115,6 +187,6 @@ public fun AString?.string(): AString = Format.wrap( */ @[JvmName("trimCharSequence") OptIn(InefficientAStringApi::class)] public fun AString?.trim(): AString = Delegate.wrap( - Trim, this, + Trim, ) diff --git a/astring/src/test/java/xyz/tynn/astring/AStringFactoryTest.java b/astring/src/test/java/xyz/tynn/astring/AStringFactoryTest.java index 757680b..c442a81 100644 --- a/astring/src/test/java/xyz/tynn/astring/AStringFactoryTest.java +++ b/astring/src/test/java/xyz/tynn/astring/AStringFactoryTest.java @@ -4,6 +4,7 @@ package xyz.tynn.astring; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import org.junit.Test; @@ -29,8 +30,8 @@ public void mapAString_should_return_NULL() { } @Test - public void ensureAStringNull_should_return_NULL() { - assertSame(AString.Null, AStringFactory.ensureAStringNull(null)); + public void ensureNonNull_should_return_NULL() { + assertSame(AString.Null, AStringFactory.ensureNonNull(null)); } @Test @@ -111,11 +112,32 @@ public void mapEmptyToNull_should_be_NULL_on_null() { } @Test - public void mapNullToEmpty_should_be_NULL_on_null() { + public void mapNullToEmpty_should_be_empty_on_null() { assertEquals(AStringFactory.createFromCharSequence(""), AStringFactory.mapNullToEmpty(null)); } + @Test + @SuppressWarnings("ConstantConditions") + public void mapBlankToDefault_should_not_be_null_on_null() { + assertNotNull(AStringFactory.mapBlankToDefault(null, (CharSequence) null)); + assertNotNull(AStringFactory.mapBlankToDefault(null, (AString) null)); + } + + @Test + @SuppressWarnings("ConstantConditions") + public void mapEmptyToDefault_should_not_be_null_on_null() { + assertNotNull(AStringFactory.mapEmptyToDefault(null, (CharSequence) null)); + assertNotNull(AStringFactory.mapEmptyToDefault(null, (AString) null)); + } + + @Test + @SuppressWarnings("ConstantConditions") + public void mapNullToDefault_should_not_be_null_on_null() { + assertNotNull(AStringFactory.mapNullToDefault(null, (CharSequence) null)); + assertNotNull(AStringFactory.mapNullToDefault(null, (AString) null)); + } + @Test public void AppId_should_return_AppId_provider_wrap() { assertEquals(Delegate.wrap(Provider.AppId), AStringFactory.getAppId()); diff --git a/astring/src/test/java/xyz/tynn/astring/AStringReducerTest.java b/astring/src/test/java/xyz/tynn/astring/AStringReducerTest.java new file mode 100644 index 0000000..af66e39 --- /dev/null +++ b/astring/src/test/java/xyz/tynn/astring/AStringReducerTest.java @@ -0,0 +1,121 @@ +// Copyright 2023 Christian Schmitz +// SPDX-License-Identifier: Apache-2.0 + +package xyz.tynn.astring; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class AStringReducerTest { + + private final Iterable aStrings = List.of( + AStringFactory.getAppId(), + AStringFactory.getAppVersion() + ); + + @Test + public void delegate_should_wrap_null_AString() { + AString.Reducer function = values -> "value"; + Assert.assertEquals(Delegate.wrap(function, AString.Null, AString.Null), + Delegate.wrap(function, null, null)); + } + + @Test + @SuppressWarnings("Convert2Lambda") + public void interface_should_not_be_efficient() { + assertNotEquals(AStringFactory.reduce(aStrings, new AString.Reducer() { + @Override + public CharSequence invoke(@NonNull Iterable values) { + return ""; + } + }), AStringFactory.reduce(aStrings, new AString.Reducer() { + @Override + public CharSequence invoke(@NonNull Iterable values) { + return ""; + } + })); + } + + @Test + @SuppressWarnings("Convert2Lambda") + public void interface_ref_should_be_efficient() { + AString.Reducer function = new AString.Reducer() { + @Override + public CharSequence invoke(@NonNull Iterable values) { + return ""; + } + }; + assertEquals(AStringFactory.reduce(aStrings, function), + AStringFactory.reduce(aStrings, function)); + } + + @Test + public void instance_should_be_efficient() { + assertEquals(AStringFactory.reduce(aStrings, new Reducer()), + AStringFactory.reduce(aStrings, new Reducer())); + } + + @Test + public void instance_ref_should_be_efficient() { + AString.Reducer function = new Reducer(); + assertEquals(AStringFactory.reduce(aStrings, function), + AStringFactory.reduce(aStrings, function)); + } + + @Test + public void function_should_not_be_efficient() { + assertNotEquals(AStringFactory.reduce(aStrings, this::function), + AStringFactory.reduce(aStrings, this::function)); + } + + @Test + public void function_ref_should_be_efficient() { + AString.Reducer function = this::function; + assertEquals(AStringFactory.reduce(aStrings, function), + AStringFactory.reduce(aStrings, function)); + } + + @Test + public void lambda_should_not_be_efficient() { + assertNotEquals(AStringFactory.reduce(aStrings, values -> ""), + AStringFactory.reduce(aStrings, values -> "")); + } + + @Test + public void lambda_ref_should_be_efficient() { + AString.Reducer function = values -> ""; + assertEquals(AStringFactory.reduce(aStrings, function), + AStringFactory.reduce(aStrings, function)); + } + + private CharSequence function(Iterable values) { + return ""; + } + + private final static class Reducer implements AString.Reducer { + + @Nullable + @Override + public CharSequence invoke(@NonNull Iterable values) { + return null; + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof Reducer; + } + + @Override + public int hashCode() { + return 0; + } + } +} diff --git a/astring/src/test/java/xyz/tynn/astring/AStringTransformerTest.java b/astring/src/test/java/xyz/tynn/astring/AStringTransformerTest.java index 962302b..97c7f1a 100644 --- a/astring/src/test/java/xyz/tynn/astring/AStringTransformerTest.java +++ b/astring/src/test/java/xyz/tynn/astring/AStringTransformerTest.java @@ -19,8 +19,8 @@ public class AStringTransformerTest { @Test public void delegate_should_wrap_null_AString() { AString.Transformer function = value -> value; - assertEquals(Delegate.wrap(function, AString.Null), - Delegate.wrap(function, null)); + assertEquals(Delegate.wrap(AString.Null, function), + Delegate.wrap(null, function)); } @Test @@ -28,7 +28,7 @@ public void delegate_should_wrap_null_AString() { public void map_should_wrap_null_AString() { AString.Transformer function = value -> value; assertEquals( - Delegate.wrap(function, AString.Null), + Delegate.wrap(AString.Null, function), mapAString(null, function) ); } @@ -96,7 +96,7 @@ public void lambda_ref_should_be_efficient() { @Test public void delegate_should_return_Null_on_null_transformer() { - assertEquals(AString.Null, Delegate.wrap(null, null)); + assertEquals(AString.Null, Delegate.wrap((AString) null, null)); } private CharSequence function(CharSequence value) { diff --git a/astring/src/test/kotlin/xyz/tynn/astring/AStringFactoryKtTest.kt b/astring/src/test/kotlin/xyz/tynn/astring/AStringFactoryKtTest.kt index 1fd1f84..95d49fc 100644 --- a/astring/src/test/kotlin/xyz/tynn/astring/AStringFactoryKtTest.kt +++ b/astring/src/test/kotlin/xyz/tynn/astring/AStringFactoryKtTest.kt @@ -261,15 +261,15 @@ internal class AStringFactoryKtTest { fun `format should return Format with format args for Format`() { assertEquals( Format.wrap(TextResource(1), null, arrayOf(1, "2")), - TextResource(1).string().format(1, "2"), + TextResource(1).mapToString().format(1, "2"), ) assertEquals( Format.wrap(TextResource(1), Locale.GERMAN, arrayOf(1, "2")), - TextResource(1).string().format(Locale.GERMAN, 1, "2"), + TextResource(1).mapToString().format(Locale.GERMAN, 1, "2"), ) assertEquals( Format.wrap(TextResource(1), null, arrayOf(1, "2")), - TextResource(1).string().format(locale = null, 1, "2"), + TextResource(1).mapToString().format(locale = null, 1, "2"), ) } @@ -290,53 +290,53 @@ internal class AStringFactoryKtTest { } @Test - fun `string should return unformatted Format`() { + fun `mapToString should return unformatted Format`() { assertEquals( Format.wrap(TextResource(1), null, null), - TextResource(1).string(), + TextResource(1).mapToString(), ) } @Test - fun `string should return identity for Format`() { + fun `mapToString should return identity for Format`() { val format = "format".asAString().format(1) assertSame( format, - format.string(), + format.mapToString(), ) - val toString = TextResource(1).string() + val toString = TextResource(1).mapToString() assertSame( toString, - toString.string(), + toString.mapToString(), ) } @Test - fun `string should return NULL for null`() { + fun `mapToString should return NULL for null`() { assertSame( AString.Null, - null.string(), + null.mapToString(), ) assertSame( AString.Null, - null.asAString().string(), + null.asAString().mapToString(), ) } @Test - fun `string should return mapped Wrapper`() { + fun `mapToString should return mapped Wrapper`() { assertEquals( "format".asAString(), - StringBuilder("format").asAString().string(), + StringBuilder("format").asAString().mapToString(), ) } @Test - fun `string should return identity for Wrapper of String`() { + fun `mapToString should return identity for Wrapper of String`() { val wrapper = "format".asAString() assertSame( wrapper, - wrapper.string(), + wrapper.mapToString(), ) } @@ -374,8 +374,8 @@ internal class AStringFactoryKtTest { val aString = mockk() assertEquals( Delegate.wrap( - Predicate.NonBlank, aString, + Predicate.NonBlank, ), aString.nullIfBlank(), ) @@ -415,8 +415,8 @@ internal class AStringFactoryKtTest { val aString = mockk() assertEquals( Delegate.wrap( - Predicate.NonEmpty, aString, + Predicate.NonEmpty, ), aString.nullIfEmpty(), ) @@ -452,8 +452,8 @@ internal class AStringFactoryKtTest { val aString = mockk() assertEquals( Delegate.wrap( - Predicate.NonNull, aString, + Predicate.NonNull, ), aString.emptyIfNull(), ) @@ -484,8 +484,8 @@ internal class AStringFactoryKtTest { val aString = mockk() assertEquals( Delegate.wrap( - Transformer.Trim, aString, + Transformer.Trim, ), aString.trim(), ) diff --git a/astring/src/test/kotlin/xyz/tynn/astring/AStringProviderKtTest.kt b/astring/src/test/kotlin/xyz/tynn/astring/AStringProviderKtTest.kt index 7bea434..a7f2a1a 100644 --- a/astring/src/test/kotlin/xyz/tynn/astring/AStringProviderKtTest.kt +++ b/astring/src/test/kotlin/xyz/tynn/astring/AStringProviderKtTest.kt @@ -33,29 +33,30 @@ internal class AStringProviderKtTest { @Test fun `equals should be false for non matching provider`() { assertFalse { - Delegate.wrap(mockk()) == Delegate.wrap(mockk()) + Delegate.wrap(mockk()) == Delegate.wrap(mockk()) } } @Test fun `equals should be false for non provider Delegate`() { assertFalse { - Delegate.wrap(mockk()).equals("foo") + Delegate.wrap(mockk()).equals("foo") } assertFalse { - Delegate.wrap(mockk()) == mockk() + Delegate.wrap(mockk()) == mockk() } assertFalse { - Delegate.wrap(mockk()).equals(mockk()) + Delegate.wrap(mockk()).equals(mockk()) } assertFalse { - Delegate.wrap(mockk()).equals(mockk()) + Delegate.wrap(mockk()).equals(mockk()) } assertFalse { - Delegate.wrap(mockk()).equals(mockk()) + Delegate.wrap(mockk()).equals(mockk()) } assertFalse { - Delegate.wrap(mockk()) == Delegate.wrap(mockk(), mockk()) + Delegate.wrap(mockk()) == + Delegate.wrap(mockk(), mockk()) } } @@ -63,7 +64,9 @@ internal class AStringProviderKtTest { fun `hashCode should return delegate to provider`() { assertEquals( 4775, - Delegate.wrap(mockk { every { this@mockk.hashCode() } returns 123 }).hashCode(), + Delegate.wrap( + mockk { every { this@mockk.hashCode() } returns 123 }, + ).hashCode(), ) } diff --git a/astring/src/test/kotlin/xyz/tynn/astring/AStringReducerKtTest.kt b/astring/src/test/kotlin/xyz/tynn/astring/AStringReducerKtTest.kt new file mode 100644 index 0000000..087de57 --- /dev/null +++ b/astring/src/test/kotlin/xyz/tynn/astring/AStringReducerKtTest.kt @@ -0,0 +1,582 @@ +// Copyright 2023 Christian Schmitz +// SPDX-License-Identifier: Apache-2.0 + +package xyz.tynn.astring + +import android.content.Context +import android.text.SpannableStringBuilder +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkConstructor +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +@InefficientAStringApi +internal class AStringReducerKtTest { + + private val context = mockk() + private val aString1 = mockk() + private val aString2 = mockk() + + @Test + fun `equals should be true for matching reducer and receiver`() { + assertTrue { + Delegate.wrap(Reducer(), AppId) == Delegate.wrap(Reducer(), AppId) + } + assertTrue { + Delegate.wrap(Reducer(), AppVersion) == Delegate.wrap(Reducer(), AppVersion) + } + assertTrue { + Delegate.wrap(Predicate.AnyValue, AppId) == Delegate.wrap(Predicate.AnyValue, AppId) + } + assertTrue { + Delegate.wrap(Predicate.NonBlank, AppId) == Delegate.wrap(Predicate.NonBlank, AppId) + } + assertTrue { + Delegate.wrap(Predicate.NonEmpty, AppId) == Delegate.wrap(Predicate.NonEmpty, AppId) + } + assertTrue { + Delegate.wrap(Predicate.NonNull, AppId) == Delegate.wrap(Predicate.NonNull, AppId) + } + } + + @Test + fun `equals should be false for non matching reducer`() { + assertFalse { + Delegate.wrap(mockk(), AppId) == + Delegate.wrap(mockk(), AppId) + } + } + + @Test + fun `equals should be false for non matching receiver`() { + assertFalse { + Delegate.wrap(Reducer(), mockk()) == Delegate.wrap(Reducer(), mockk()) + } + } + + @Test + fun `equals should be false for non reducer Delegate`() { + assertFalse { + Delegate.wrap(mockk(), mockk()).equals("foo") + } + assertFalse { + Delegate.wrap(mockk(), mockk()) == mockk() + } + assertFalse { + Delegate.wrap(mockk(), mockk()).equals(mockk()) + } + assertFalse { + Delegate.wrap(mockk(), mockk()).equals(mockk()) + } + assertFalse { + Delegate.wrap(mockk(), mockk()).equals(mockk()) + } + assertFalse { + Delegate.wrap(mockk(), mockk()) == Delegate.wrap(mockk()) + } + } + + @Test + fun `hashCode should return delegate to reducer and receiver`() { + assertEquals( + 17108, + Delegate.wrap( + mockk { every { this@mockk.hashCode() } returns 123 }, + mockk { every { this@mockk.hashCode() } returns 345 }, + mockk { every { this@mockk.hashCode() } returns 678 }, + ).hashCode(), + ) + } + + @Test + fun `toString should delegate to reducer and receiver`() { + val reducer = mockk { + every { this@mockk.toString() } returns "to-string" + } + assertEquals( + "AString(Reduce(to-string),$AppId)", + reduce(AppId, reducer = reducer).toString(), + ) + } + + @Test + fun `firstNonBlank should provide first non blank value`() { + assertEquals( + "value", + context.aString( + firstNonBlank( + AString(null), + AString(""), + AString(" "), + AString("\n \t"), + AString("value"), + AString("invalid"), + ), + ), + ) + assertEquals( + "value", + context.aString( + listOf( + AString(null), + AString(""), + AString(" "), + AString("\n \t"), + AString("value"), + AString("invalid"), + ).firstNonBlank(), + ), + ) + assertEquals( + "value", + context.aString( + sequenceOf( + AString(null), + AString(""), + AString(" "), + AString("\n \t"), + AString("value"), + AString("invalid"), + ).asIterable().firstNonBlank(), + ), + ) + } + + @Test + fun `firstNonEmpty should provide first non empty value`() { + assertEquals( + " ", + context.aString( + firstNonEmpty( + AString(null), + AString(""), + AString(" "), + AString("invalid"), + ), + ), + ) + assertEquals( + " ", + context.aString( + listOf( + AString(null), + AString(""), + AString(" "), + AString("invalid"), + ).firstNonEmpty(), + ), + ) + assertEquals( + " ", + context.aString( + sequenceOf( + AString(null), + AString(""), + AString(" "), + AString("invalid"), + ).asIterable().firstNonEmpty(), + ), + ) + } + + @Test + fun `firstNonNull should provide first non null value`() { + assertEquals( + "", + context.aString( + firstNonNull( + AString(null), + AString(""), + AString("invalid"), + ), + ), + ) + assertEquals( + "", + context.aString( + listOf( + AString(null), + AString(""), + AString("invalid"), + ).firstNonNull(), + ), + ) + assertEquals( + "", + context.aString( + sequenceOf( + AString(null), + AString(""), + AString("invalid"), + ).asIterable().firstNonNull(), + ), + ) + } + + @Test + fun `join should join all values with separator`() { + withSpannableStringBuilder { + assertEquals( + "null--null- -value", + context.aString( + join( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + separator = "-", + ), + ), + ) + } + withSpannableStringBuilder { + assertEquals( + "null--null- -value", + context.aString( + listOf( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + ).join( + separator = "-", + ), + ), + ) + } + withSpannableStringBuilder { + assertEquals( + "null--null- -value", + context.aString( + sequenceOf( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + ).asIterable().join( + separator = "-", + ), + ), + ) + } + } + + @Test + fun `joinNonBlank should join all non blank values with separator`() { + withSpannableStringBuilder { + assertEquals( + "null-value", + context.aString( + joinNonBlank( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + separator = "-", + ), + ), + ) + } + withSpannableStringBuilder { + assertEquals( + "null-value", + context.aString( + listOf( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + ).joinNonBlank( + separator = "-", + ), + ), + ) + } + withSpannableStringBuilder { + assertEquals( + "null-value", + context.aString( + sequenceOf( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + ).asIterable().joinNonBlank( + separator = "-", + ), + ), + ) + } + } + + @Test + fun `joinNonEmpty should join all non empty values with separator`() { + withSpannableStringBuilder { + assertEquals( + "null- -value", + context.aString( + joinNonEmpty( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + separator = "-", + ), + ), + ) + } + withSpannableStringBuilder { + assertEquals( + "null- -value", + context.aString( + listOf( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + ).joinNonEmpty( + separator = "-", + ), + ), + ) + } + withSpannableStringBuilder { + assertEquals( + "null- -value", + context.aString( + sequenceOf( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + ).asIterable().joinNonEmpty( + separator = "-", + ), + ), + ) + } + } + + @Test + fun `joinNonNull should join all non null values with separator`() { + withSpannableStringBuilder { + assertEquals( + "-null- -value", + context.aString( + joinNonNull( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + separator = "-", + ), + ), + ) + } + withSpannableStringBuilder { + assertEquals( + "-null- -value", + context.aString( + listOf( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + ).joinNonNull( + separator = "-", + ), + ), + ) + } + withSpannableStringBuilder { + assertEquals( + "-null- -value", + context.aString( + sequenceOf( + AString(null), + AString(""), + AString("null"), + AString(" "), + AString("value"), + ).asIterable().joinNonNull( + separator = "-", + ), + ), + ) + } + } + + @Test + @Suppress("RedundantSamConstructor") + fun `interface should not be efficient`() { + assertNotEquals( + reduce(aString1, aString2, reducer = AString.Reducer { "" }), + reduce(aString1, aString2, reducer = AString.Reducer { "" }), + ) + assertNotEquals( + reduce(aString1, aString2, reducer = AString.Reducer { "" }), + listOf(aString1, aString2).reduce(reducer = AString.Reducer { "" }), + ) + assertNotEquals( + reduce(aString1, aString2, reducer = AString.Reducer { "" }), + sequenceOf(aString1, aString2).asIterable().reduce(reducer = AString.Reducer { "" }), + ) + } + + @Test + fun `interface val should be efficient`() { + val function = AString.Reducer { "" } + assertEquals( + reduce(aString1, aString2, reducer = function), + reduce(aString1, aString2, reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + listOf(aString1, aString2).reduce(reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + sequenceOf(aString1, aString2).asIterable().reduce(reducer = function), + ) + } + + @Test + fun `instance should be efficient`() { + assertEquals( + reduce(aString1, aString2, reducer = Reducer()), + reduce(aString1, aString2, reducer = Reducer()), + ) + assertEquals( + reduce(aString1, aString2, reducer = Reducer()), + listOf(aString1, aString2).reduce(reducer = Reducer()), + ) + assertEquals( + reduce(aString1, aString2, reducer = Reducer()), + sequenceOf(aString1, aString2).asIterable().reduce(reducer = Reducer()), + ) + } + + @Test + fun `instance val should be efficient`() { + val function = Reducer() + assertEquals( + reduce(aString1, aString2, reducer = function), + reduce(aString1, aString2, reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + listOf(aString1, aString2).reduce(reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + sequenceOf(aString1, aString2).asIterable().reduce(reducer = function), + ) + } + + @Test + fun `function reference should be efficient`() { + assertEquals( + reduce(aString1, aString2, reducer = ::function), + reduce(aString1, aString2, reducer = ::function), + ) + assertEquals( + reduce(aString1, aString2, reducer = ::function), + listOf(aString1, aString2).reduce(reducer = ::function), + ) + assertEquals( + reduce(aString1, aString2, reducer = ::function), + sequenceOf(aString1, aString2).asIterable().reduce(reducer = ::function), + ) + } + + @Test + fun `function reference val should be efficient`() { + val function = ::function + assertEquals( + reduce(aString1, aString2, reducer = function), + reduce(aString1, aString2, reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + listOf(aString1, aString2).reduce(reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + sequenceOf(aString1, aString2).asIterable().reduce(reducer = function), + ) + } + + @Test + fun `lambda should not be efficient`() { + assertNotEquals( + reduce(aString1, aString2) { it.toString() }, + reduce(aString1, aString2) { it.toString() }, + ) + assertNotEquals( + reduce(aString1, aString2) { it.toString() }, + listOf(aString1, aString2).reduce { it.toString() }, + ) + assertNotEquals( + reduce(aString1, aString2) { it.toString() }, + sequenceOf(aString1, aString2).asIterable().reduce { it.toString() }, + ) + } + + @Test + fun `lambda val should be efficient`() { + val function = { _: Iterable -> "" } + assertEquals( + reduce(aString1, aString2, reducer = function), + reduce(aString1, aString2, reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + listOf(aString1, aString2).reduce(reducer = function), + ) + assertEquals( + reduce(aString1, aString2, reducer = function), + sequenceOf(aString1, aString2).asIterable().reduce(reducer = function), + ) + } + + private fun function(values: Iterable) = values.first() + + private class Reducer : AString.Reducer { + override fun invoke(values: Iterable) = "" + override fun equals(other: Any?) = other is Reducer + override fun hashCode() = 0 + } + + private fun withSpannableStringBuilder(block: () -> Unit) { + mockkConstructor(SpannableStringBuilder::class) { + val sb = StringBuilder() + every { + anyConstructed().toString() + } answers { + sb.toString() + } + every { + anyConstructed().append(any()) + } answers { + sb.append(firstArg()) + self as SpannableStringBuilder + } + block() + } + } +} diff --git a/astring/src/test/kotlin/xyz/tynn/astring/AStringTransformerKtTest.kt b/astring/src/test/kotlin/xyz/tynn/astring/AStringTransformerKtTest.kt index 4c23416..4767a45 100644 --- a/astring/src/test/kotlin/xyz/tynn/astring/AStringTransformerKtTest.kt +++ b/astring/src/test/kotlin/xyz/tynn/astring/AStringTransformerKtTest.kt @@ -22,55 +22,55 @@ internal class AStringTransformerKtTest { @Test fun `equals should be true for matching transformer and receiver`() { assertTrue { - Delegate.wrap(Transformer(), AppId) == Delegate.wrap(Transformer(), AppId) + Delegate.wrap(AppId, Transformer()) == Delegate.wrap(AppId, Transformer()) } assertTrue { - Delegate.wrap(Transformer(), AppVersion) == Delegate.wrap(Transformer(), AppVersion) + Delegate.wrap(AppVersion, Transformer()) == Delegate.wrap(AppVersion, Transformer()) } assertTrue { - Delegate.wrap(Predicate.NonBlank, AppId) == Delegate.wrap(Predicate.NonBlank, AppId) + Delegate.wrap(AppId, Predicate.NonBlank) == Delegate.wrap(AppId, Predicate.NonBlank) } assertTrue { - Delegate.wrap(Predicate.NonEmpty, AppId) == Delegate.wrap(Predicate.NonEmpty, AppId) + Delegate.wrap(AppId, Predicate.NonEmpty) == Delegate.wrap(AppId, Predicate.NonEmpty) } assertTrue { - Delegate.wrap(Predicate.NonNull, AppId) == Delegate.wrap(Predicate.NonNull, AppId) + Delegate.wrap(AppId, Predicate.NonNull) == Delegate.wrap(AppId, Predicate.NonNull) } } @Test fun `equals should be false for non matching transformer`() { assertFalse { - Delegate.wrap(mockk(), AppId) == Delegate.wrap(mockk(), AppId) + Delegate.wrap(AppId, mockk()) == Delegate.wrap(AppId, mockk()) } } @Test fun `equals should be false for non matching receiver`() { assertFalse { - Delegate.wrap(Transformer(), mockk()) == Delegate.wrap(Transformer(), mockk()) + Delegate.wrap(mockk(), Transformer()) == Delegate.wrap(mockk(), Transformer()) } } @Test fun `equals should be false for non transformer Delegate`() { assertFalse { - Delegate.wrap(mockk()).equals("foo") + Delegate.wrap(mockk(), mockk()).equals("foo") } assertFalse { - Delegate.wrap(mockk()) == mockk() + Delegate.wrap(mockk(), mockk()) == mockk() } assertFalse { - Delegate.wrap(mockk()).equals(mockk()) + Delegate.wrap(mockk(), mockk()).equals(mockk()) } assertFalse { - Delegate.wrap(mockk()).equals(mockk()) + Delegate.wrap(mockk(), mockk()).equals(mockk()) } assertFalse { - Delegate.wrap(mockk()).equals(mockk()) + Delegate.wrap(mockk(), mockk()).equals(mockk()) } assertFalse { - Delegate.wrap(mockk(), mockk()) == Delegate.wrap(mockk()) + Delegate.wrap(mockk(), mockk()) == Delegate.wrap(mockk()) } } @@ -79,14 +79,14 @@ internal class AStringTransformerKtTest { assertEquals( 5150, Delegate.wrap( + mockk { every { this@mockk.hashCode() } returns 345 }, mockk { every { this@mockk.hashCode() } returns 123 }, - mockk { every { this@mockk.hashCode() } returns 345 }, ).hashCode(), ) } @Test - fun `toString should delegate to provider and receiver`() { + fun `toString should delegate to transformer and receiver`() { val transformer = mockk { every { this@mockk.toString() } returns "to-string" } @@ -162,6 +162,294 @@ internal class AStringTransformerKtTest { ) } + @Test + fun `defaultIfBlank should return original if non blank`() { + assertEquals( + "value", + context.aString( + AString( + value = "value", + ).defaultIfBlank( + defaultValue = "", + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = "value", + ).defaultIfBlank( + defaultValue = AString( + value = "", + ), + ), + ), + ) + } + + @Test + fun `defaultIfBlank should return defaultValue if null`() { + assertEquals( + "value", + context.aString( + AString( + value = null, + ).defaultIfBlank( + defaultValue = "value", + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = "", + ).defaultIfBlank( + defaultValue = "value", + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = " ", + ).defaultIfBlank( + defaultValue = "value", + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = null, + ).defaultIfBlank( + defaultValue = AString( + value = "value", + ) + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = "", + ).defaultIfBlank( + defaultValue = AString( + value = "value", + ) + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = " ", + ).defaultIfBlank( + defaultValue = AString( + value = "value", + ) + ), + ), + ) + } + + @Test + fun `defaultIfEmpty should return original if non null`() { + assertEquals( + " ", + context.aString( + AString( + value = " ", + ).defaultIfEmpty( + defaultValue = "", + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = "value", + ).defaultIfEmpty( + defaultValue = "", + ), + ), + ) + assertEquals( + " ", + context.aString( + AString( + value = " ", + ).defaultIfEmpty( + defaultValue = AString( + value = "", + ), + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = "value", + ).defaultIfEmpty( + defaultValue = AString( + value = "", + ), + ), + ), + ) + } + + @Test + fun `defaultIfEmpty should return defaultValue if null`() { + assertEquals( + "value", + context.aString( + AString( + value = null, + ).defaultIfEmpty( + defaultValue = "value", + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = "", + ).defaultIfEmpty( + defaultValue = "value", + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = null, + ).defaultIfEmpty( + defaultValue = AString( + value = "value", + ) + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = "", + ).defaultIfEmpty( + defaultValue = AString( + value = "value", + ) + ), + ), + ) + } + + @Test + fun `defaultIfNull should return original if non null`() { + assertEquals( + "", + context.aString( + AString( + value = "", + ).defaultIfNull( + defaultValue = " ", + ), + ), + ) + assertEquals( + " ", + context.aString( + AString( + value = " ", + ).defaultIfNull( + defaultValue = "", + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = "value", + ).defaultIfNull( + defaultValue = "", + ), + ), + ) + assertEquals( + "", + context.aString( + AString( + value = "", + ).defaultIfNull( + defaultValue = AString( + value = " ", + ), + ), + ), + ) + assertEquals( + " ", + context.aString( + AString( + value = " ", + ).defaultIfNull( + defaultValue = AString( + value = "", + ), + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = "value", + ).defaultIfNull( + defaultValue = AString( + value = "", + ), + ), + ), + ) + } + + @Test + fun `defaultIfNull should return defaultValue if null`() { + assertEquals( + "value", + context.aString( + AString( + value = null, + ).defaultIfNull( + defaultValue = "value", + ), + ), + ) + assertEquals( + "value", + context.aString( + AString( + value = null, + ).defaultIfNull( + defaultValue = AString( + value = "value", + ) + ), + ), + ) + } + @Test @Suppress("RedundantSamConstructor") fun `interface should not be efficient`() { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b73586e..e1609d1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] android = "8.2.0" -compose-compiler = "1.5.6" -kotlin = "1.9.21" +compose-compiler = "1.5.7" +kotlin = "1.9.22" [plugins] android = { id = "com.android.library", version.ref = "android" } @@ -27,7 +27,7 @@ kotlin-stdlib.module = "org.jetbrains.kotlin:kotlin-stdlib" kotlin-test.module = "org.jetbrains.kotlin:kotlin-test-junit" mockk = "io.mockk:mockk:1.13.8" robolectric = "org.robolectric:robolectric:4.11.1" -slf4j = "org.slf4j:slf4j-nop:2.0.9" +slf4j = "org.slf4j:slf4j-nop:2.0.10" [bundles] testing = ["kotlin-test", "junit", "mockk", "slf4j"]