diff --git a/app/src/main/java/com/sadellie/unitto/data/BigDecimalUtils.kt b/app/src/main/java/com/sadellie/unitto/data/BigDecimalUtils.kt
new file mode 100644
index 00000000..b7fca45e
--- /dev/null
+++ b/app/src/main/java/com/sadellie/unitto/data/BigDecimalUtils.kt
@@ -0,0 +1,74 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2022 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.data
+
+import com.sadellie.unitto.data.preferences.OutputFormat
+import java.math.BigDecimal
+import java.math.RoundingMode
+import kotlin.math.floor
+import kotlin.math.log10
+
+/**
+ * Shorthand function to use correct `toString` method according to [outputFormat].
+ */
+fun BigDecimal.toStringWith(outputFormat: Int): String {
+ // Setting result value using a specified OutputFormat
+ return when (outputFormat) {
+ OutputFormat.ALLOW_ENGINEERING -> this.toString()
+ OutputFormat.FORCE_ENGINEERING -> this.toEngineeringString()
+ else -> this.toPlainString()
+ }
+}
+
+/**
+ * Sets the minimum scale that is required to get first non zero value in fractional part
+ *
+ * @param[prefScale] Is the preferred scale, the one which will be compared against
+ */
+fun BigDecimal.setMinimumRequiredScale(prefScale: Int): BigDecimal {
+ /**
+ * Here we are getting the amount of zeros in fractional part before non zero value
+ * For example, for 0.00000123456 we need the length of 00000
+ * Next we add one to get the position of the first non zero value
+ * Also, this block is only for VERY small numbers
+ */
+ return this.setScale(
+ kotlin.math.max(
+ prefScale,
+ if (this.abs() < BigDecimal.ONE) {
+ // https://stackoverflow.com/a/46136593
+ -floor(log10(this.abs().remainder(BigDecimal.ONE).toDouble())).toInt()
+ } else {
+ 0
+ }
+ ),
+ RoundingMode.HALF_EVEN
+ )
+}
+
+/**
+ * Removes all trailing zeroes.
+ *
+ * @throws NumberFormatException if value is bigger than [Double.MAX_VALUE] to avoid memory overflow.
+ */
+fun BigDecimal.trimZeros(): BigDecimal {
+ if (this.abs() > BigDecimal.valueOf(Double.MAX_VALUE)) throw NumberFormatException()
+
+ return if (this.compareTo(BigDecimal.ZERO) == 0) BigDecimal.ZERO else this.stripTrailingZeros()
+}
diff --git a/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt b/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt
new file mode 100644
index 00000000..77b2533c
--- /dev/null
+++ b/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt
@@ -0,0 +1,42 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2022 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.data
+
+import kotlinx.coroutines.flow.Flow
+
+@Suppress("UNCHECKED_CAST")
+fun combine(
+ flow: Flow,
+ flow2: Flow,
+ flow3: Flow,
+ flow4: Flow,
+ flow5: Flow,
+ flow6: Flow,
+ transform: suspend (T1, T2, T3, T4, T5, T6) -> R
+): Flow =
+ kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> ->
+ transform(
+ args[0] as T1,
+ args[1] as T2,
+ args[2] as T3,
+ args[3] as T4,
+ args[4] as T5,
+ args[5] as T6,
+ )
+ }
diff --git a/app/src/main/java/com/sadellie/unitto/data/units/AbstractUnit.kt b/app/src/main/java/com/sadellie/unitto/data/units/AbstractUnit.kt
index 8f058ce6..bcc2fb79 100644
--- a/app/src/main/java/com/sadellie/unitto/data/units/AbstractUnit.kt
+++ b/app/src/main/java/com/sadellie/unitto/data/units/AbstractUnit.kt
@@ -19,6 +19,7 @@
package com.sadellie.unitto.data.units
import androidx.annotation.StringRes
+import com.sadellie.unitto.screens.lev
import java.math.BigDecimal
/**
@@ -64,3 +65,64 @@ abstract class AbstractUnit(
scale: Int
): BigDecimal
}
+
+/**
+ * Sorts sequence of units by Levenshtein distance
+ *
+ * @param stringA String for Levenshtein distance
+ * @return Sorted sequence of units. Units with lower Levenshtein distance are higher
+ */
+fun Sequence.sortByLev(stringA: String): Sequence {
+ val stringToCompare = stringA.lowercase()
+ // We don't need units where name is too different, half of the symbols is wrong in this situation
+ val threshold: Int = stringToCompare.length / 2
+ // List of pair: Unit and it's levDist
+ val unitsWithDist = mutableListOf>()
+ this.forEach { unit ->
+ val unitName: String = unit.renderedName.lowercase()
+
+ /**
+ * There is chance that unit name doesn't need any edits (contains part of the query)
+ * So computing levDist is a waste of resources
+ */
+ when {
+ // It's the best possible match if it start with
+ unitName.startsWith(stringToCompare) -> {
+ unitsWithDist.add(Pair(unit, 0))
+ return@forEach
+ }
+ // It's a little bit worse when it just contains part of the query
+ unitName.contains(stringToCompare) -> {
+ unitsWithDist.add(Pair(unit, 1))
+ return@forEach
+ }
+ }
+
+ /**
+ * Levenshtein Distance for this specific name of this unit
+ *
+ * We use substring so that we compare not the whole unit name, but only part of it.
+ * It's required because without it levDist will be too high for units with longer
+ * names than the search query
+ *
+ * For example:
+ * Search query is 'Kelometer' and unit name is 'Kilometer per hour'
+ * Without substring levDist will be 9 which means that this unit will be skipped
+ *
+ * With substring levDist will be 3 so unit will be included
+ */
+ val levDist = unitName
+ .substring(0, minOf(stringToCompare.length, unitName.length))
+ .lev(stringToCompare)
+
+ // Threshold
+ if (levDist < threshold) {
+ unitsWithDist.add(Pair(unit, levDist))
+ }
+ }
+ // Sorting by levDist and getting units
+ return unitsWithDist
+ .sortedBy { it.second }
+ .map { it.first }
+ .asSequence()
+}
diff --git a/app/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt b/app/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt
index 063252cf..8c778d4b 100644
--- a/app/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt
+++ b/app/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt
@@ -21,10 +21,9 @@ package com.sadellie.unitto.data.units
import android.content.Context
import com.sadellie.unitto.R
import com.sadellie.unitto.data.preferences.MAX_PRECISION
+import com.sadellie.unitto.data.setMinimumRequiredScale
+import com.sadellie.unitto.data.trimZeros
import com.sadellie.unitto.data.units.database.MyBasedUnit
-import com.sadellie.unitto.screens.setMinimumRequiredScale
-import com.sadellie.unitto.screens.sortByLev
-import com.sadellie.unitto.screens.trimZeros
import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject
diff --git a/app/src/main/java/com/sadellie/unitto/data/units/MyUnit.kt b/app/src/main/java/com/sadellie/unitto/data/units/MyUnit.kt
index ce6c2696..5beb2373 100644
--- a/app/src/main/java/com/sadellie/unitto/data/units/MyUnit.kt
+++ b/app/src/main/java/com/sadellie/unitto/data/units/MyUnit.kt
@@ -20,8 +20,8 @@ package com.sadellie.unitto.data.units
import androidx.annotation.StringRes
import com.sadellie.unitto.data.preferences.MAX_PRECISION
-import com.sadellie.unitto.screens.setMinimumRequiredScale
-import com.sadellie.unitto.screens.trimZeros
+import com.sadellie.unitto.data.setMinimumRequiredScale
+import com.sadellie.unitto.data.trimZeros
import java.math.BigDecimal
/**
diff --git a/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt b/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt
new file mode 100644
index 00000000..c94b1a63
--- /dev/null
+++ b/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt
@@ -0,0 +1,116 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2022 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.screens
+
+import com.sadellie.unitto.data.KEY_COMMA
+import com.sadellie.unitto.data.KEY_DOT
+import com.sadellie.unitto.data.KEY_E
+import com.sadellie.unitto.data.KEY_LEFT_BRACKET
+import com.sadellie.unitto.data.KEY_RIGHT_BRACKET
+import com.sadellie.unitto.data.OPERATORS
+import com.sadellie.unitto.data.preferences.Separator
+
+object Formatter {
+ private const val SPACE = " "
+ private const val PERIOD = "."
+ private const val COMMA = ","
+
+ /**
+ * Grouping separator.
+ */
+ private var grouping: String = SPACE
+
+ /**
+ * Fractional part separator.
+ */
+ var fractional = KEY_COMMA
+
+ /**
+ * Change current separator to another [separator].
+ *
+ * @see [Separator]
+ */
+ fun setSeparator(separator: Int) {
+ grouping = when (separator) {
+ Separator.PERIOD -> PERIOD
+ Separator.COMMA -> COMMA
+ else -> SPACE
+ }
+ fractional = if (separator == Separator.PERIOD) KEY_COMMA else KEY_DOT
+ }
+
+ /**
+ * Format [input].
+ *
+ * This will replace operators to their more appealing variants: divide, multiply and minus.
+ * Plus operator remains unchanged.
+ *
+ * Numbers will also be formatted.
+ *
+ * @see [formatNumber]
+ */
+ fun format(input: String): String {
+ // Don't do anything to engineering string.
+ if (input.contains(KEY_E)) return input.replace(KEY_DOT, fractional)
+
+ var output = input
+
+ // We may receive expressions
+ // Find all numbers in that expression
+ val allNumbers: List = input.split(
+ *OPERATORS.toTypedArray(), KEY_LEFT_BRACKET, KEY_RIGHT_BRACKET
+ )
+
+ allNumbers.forEach {
+ output = output.replace(it, formatNumber(it))
+ }
+
+ return output
+ }
+
+ /**
+ * Format given [input].
+ *
+ * Input must be a number. Will replace grouping separators and fractional part separators.
+ *
+ * @see grouping
+ * @see fractional
+ */
+ private fun formatNumber(input: String): String {
+ val splitInput = input.split(".")
+ var firstPart = splitInput[0]
+
+ // Number of empty symbols (spaces) we need to add to correctly split into chunks.
+ val offset = 3 - firstPart.length.mod(3)
+ var output = if (offset != 3) {
+ // We add some spaces at the begging so that last chunk has 3 symbols
+ firstPart = " ".repeat(offset) + firstPart
+ firstPart.chunked(3).joinToString(this.grouping).drop(offset)
+ } else {
+ firstPart.chunked(3).joinToString(this.grouping)
+ }
+
+ // Handling fractional part
+ if (input.contains(".")) {
+ output = output + fractional + splitInput.getOrElse(1) { "" }
+ }
+
+ return output
+ }
+}
diff --git a/app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt b/app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt
new file mode 100644
index 00000000..a83e9df0
--- /dev/null
+++ b/app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt
@@ -0,0 +1,60 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2022-2022 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.screens
+
+/**
+ * Compute Levenshtein Distance between this string and [secondString]. Doesn't matter which string is
+ * first.
+ *
+ * @return The amount of changes that are needed to transform one string into another
+ */
+fun String.lev(secondString: String): Int {
+ val stringA = this.lowercase()
+ val stringB = secondString.lowercase()
+
+ // Skipping computation for this cases
+ if (stringA == stringB) return 0
+ if (stringA.isEmpty()) return stringB.length
+ // This case is basically unreal in this app, because secondString is a unit name and they are never empty
+ if (stringB.isEmpty()) return stringA.length
+
+ var cost = IntArray(stringA.length + 1) { it }
+ var newCost = IntArray(stringA.length + 1)
+
+ for (i in 1..stringB.length) {
+ // basically shifting this to the right by 1 each time
+ newCost[0] = i
+
+ for (j in 1..stringA.length) {
+ newCost[j] = minOf(
+ // Adding 1 if they don't match, i.e. need to replace
+ cost[j - 1] + if (stringA[j - 1] == stringB[i - 1]) 0 else 1,
+ // Insert
+ cost[j] + 1,
+ // Delete
+ newCost[j - 1] + 1
+ )
+ }
+
+ // Swapping costs
+ cost = newCost.also { newCost = cost }
+ }
+
+ return cost[this.length]
+}
diff --git a/app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt b/app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt
new file mode 100644
index 00000000..291774b3
--- /dev/null
+++ b/app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt
@@ -0,0 +1,30 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2022 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.screens
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+
+/**
+ * Open given link in browser
+ */
+fun openLink(mContext: Context, url: String) {
+ mContext.startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(url)))
+}
diff --git a/app/src/main/java/com/sadellie/unitto/screens/Utils.kt b/app/src/main/java/com/sadellie/unitto/screens/Utils.kt
deleted file mode 100644
index d9583431..00000000
--- a/app/src/main/java/com/sadellie/unitto/screens/Utils.kt
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * Unitto is a unit converter for Android
- * Copyright (c) 2022-2022 Elshan Agaev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.sadellie.unitto.screens
-
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import com.sadellie.unitto.data.KEY_COMMA
-import com.sadellie.unitto.data.KEY_DOT
-import com.sadellie.unitto.data.KEY_E
-import com.sadellie.unitto.data.KEY_LEFT_BRACKET
-import com.sadellie.unitto.data.KEY_RIGHT_BRACKET
-import com.sadellie.unitto.data.OPERATORS
-import com.sadellie.unitto.data.preferences.OutputFormat
-import com.sadellie.unitto.data.preferences.Separator
-import com.sadellie.unitto.data.units.AbstractUnit
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import java.math.BigDecimal
-import java.math.RoundingMode
-import kotlin.math.floor
-import kotlin.math.log10
-import kotlin.math.max
-
-object Formatter {
- private const val SPACE = " "
- private const val PERIOD = "."
- private const val COMMA = ","
-
- /**
- * Grouping separator.
- */
- private var grouping: String = SPACE
-
- /**
- * Fractional part separator.
- */
- var fractional = KEY_COMMA
-
- /**
- * Change current separator to another [separator].
- *
- * @see [Separator]
- */
- fun setSeparator(separator: Int) {
- grouping = when (separator) {
- Separator.PERIOD -> PERIOD
- Separator.COMMA -> COMMA
- else -> SPACE
- }
- fractional = if (separator == Separator.PERIOD) KEY_COMMA else KEY_DOT
- }
-
-
- /**
- * Format [input].
- *
- * This will replace operators to their more appealing variants: divide, multiply and minus.
- * Plus operator remains unchanged.
- *
- * Numbers will also be formatted.
- *
- * @see [formatNumber]
- */
- fun format(input: String): String {
- // Don't do anything to engineering string.
- if (input.contains(KEY_E)) return input.replace(KEY_DOT, fractional)
-
- var output = input
-
- // We may receive expressions
- // Find all numbers in that expression
- val allNumbers: List = input.split(
- *OPERATORS.toTypedArray(), KEY_LEFT_BRACKET, KEY_RIGHT_BRACKET
- )
-
- allNumbers.forEach {
- output = output.replace(it, formatNumber(it))
- }
-
- return output
- }
-
- /**
- * Format given [input].
- *
- * Input must be a number. Will replace grouping separators and fractional part separators.
- *
- * @see grouping
- * @see fractional
- */
- private fun formatNumber(input: String): String {
- val splitInput = input.split(".")
- var firstPart = splitInput[0]
-
- // Number of empty symbols (spaces) we need to add to correctly split into chunks.
- val offset = 3 - firstPart.length.mod(3)
- var output = if (offset != 3) {
- // We add some spaces at the begging so that last chunk has 3 symbols
- firstPart = " ".repeat(offset) + firstPart
- firstPart.chunked(3).joinToString(this.grouping).drop(offset)
- } else {
- firstPart.chunked(3).joinToString(this.grouping)
- }
-
- // Handling fractional part
- if (input.contains(".")) {
- output = output + fractional + splitInput.getOrElse(1) { "" }
- }
-
- return output
- }
-
-}
-
-/**
- * Shorthand function to use correct `toString` method according to [outputFormat].
- */
-fun BigDecimal.toStringWith(outputFormat: Int): String {
- // Setting result value using a specified OutputFormat
- return when (outputFormat) {
- OutputFormat.ALLOW_ENGINEERING -> this.toString()
- OutputFormat.FORCE_ENGINEERING -> this.toEngineeringString()
- else -> this.toPlainString()
- }
-}
-
-/**
- * Sets the minimum scale that is required to get first non zero value in fractional part
- *
- * @param[prefScale] Is the preferred scale, the one which will be compared against
- */
-fun BigDecimal.setMinimumRequiredScale(prefScale: Int): BigDecimal {
- /**
- * Here we are getting the amount of zeros in fractional part before non zero value
- * For example, for 0.00000123456 we need the length of 00000
- * Next we add one to get the position of the first non zero value
- * Also, this block is only for VERY small numbers
- */
- return this.setScale(
- max(
- prefScale,
- if (this.abs() < BigDecimal.ONE) {
- // https://stackoverflow.com/a/46136593
- -floor(log10(this.abs().remainder(BigDecimal.ONE).toDouble())).toInt()
- } else {
- 0
- }
- ),
- RoundingMode.HALF_EVEN
- )
-}
-
-/**
- * Open given link in browser
- */
-fun openLink(mContext: Context, url: String) {
- mContext.startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(url)))
-}
-
-/**
- * Compute Levenshtein Distance between this string and [secondString]. Doesn't matter which string is
- * first.
- *
- * @return The amount of changes that are needed to transform one string into another
- */
-fun String.lev(secondString: String): Int {
- val stringA = this.lowercase()
- val stringB = secondString.lowercase()
-
- // Skipping computation for this cases
- if (stringA == stringB) return 0
- if (stringA.isEmpty()) return stringB.length
- // This case is basically unreal in this app, because secondString is a unit name and they are never empty
- if (stringB.isEmpty()) return stringA.length
-
- var cost = IntArray(stringA.length + 1) { it }
- var newCost = IntArray(stringA.length + 1)
-
- for (i in 1..stringB.length) {
- // basically shifting this to the right by 1 each time
- newCost[0] = i
-
- for (j in 1..stringA.length) {
- newCost[j] = minOf(
- // Adding 1 if they don't match, i.e. need to replace
- cost[j - 1] + if (stringA[j - 1] == stringB[i - 1]) 0 else 1,
- // Insert
- cost[j] + 1,
- // Delete
- newCost[j - 1] + 1
- )
- }
-
- // Swapping costs
- cost = newCost.also { newCost = cost }
- }
-
- return cost[this.length]
-}
-
-/**
- * Sorts sequence of units by Levenshtein distance
- *
- * @param stringA String for Levenshtein distance
- * @return Sorted sequence of units. Units with lower Levenshtein distance are higher
- */
-fun Sequence.sortByLev(stringA: String): Sequence {
- val stringToCompare = stringA.lowercase()
- // We don't need units where name is too different, half of the symbols is wrong in this situation
- val threshold: Int = stringToCompare.length / 2
- // List of pair: Unit and it's levDist
- val unitsWithDist = mutableListOf>()
- this.forEach { unit ->
- val unitName: String = unit.renderedName.lowercase()
-
- /**
- * There is chance that unit name doesn't need any edits (contains part of the query)
- * So computing levDist is a waste of resources
- */
- when {
- // It's the best possible match if it start with
- unitName.startsWith(stringToCompare) -> {
- unitsWithDist.add(Pair(unit, 0))
- return@forEach
- }
- // It's a little bit worse when it just contains part of the query
- unitName.contains(stringToCompare) -> {
- unitsWithDist.add(Pair(unit, 1))
- return@forEach
- }
- }
-
- /**
- * Levenshtein Distance for this specific name of this unit
- *
- * We use substring so that we compare not the whole unit name, but only part of it.
- * It's required because without it levDist will be too high for units with longer
- * names than the search query
- *
- * For example:
- * Search query is 'Kelometer' and unit name is 'Kilometer per hour'
- * Without substring levDist will be 9 which means that this unit will be skipped
- *
- * With substring levDist will be 3 so unit will be included
- */
- val levDist = unitName
- .substring(0, minOf(stringToCompare.length, unitName.length))
- .lev(stringToCompare)
-
- // Threshold
- if (levDist < threshold) {
- unitsWithDist.add(Pair(unit, levDist))
- }
- }
- // Sorting by levDist and getting units
- return unitsWithDist
- .sortedBy { it.second }
- .map { it.first }
- .asSequence()
-}
-
-@Suppress("UNCHECKED_CAST")
-fun combine(
- flow: Flow,
- flow2: Flow,
- flow3: Flow,
- flow4: Flow,
- flow5: Flow,
- flow6: Flow,
- transform: suspend (T1, T2, T3, T4, T5, T6,) -> R
-): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6,) { args: Array<*> ->
- transform(
- args[0] as T1,
- args[1] as T2,
- args[2] as T3,
- args[3] as T4,
- args[4] as T5,
- args[5] as T6,
- )
-}
-
-/**
- * Removes all trailing zeroes.
- *
- * @throws NumberFormatException if value is bigger than [Double.MAX_VALUE] to avoid memory overflow.
- */
-fun BigDecimal.trimZeros(): BigDecimal {
- if (this.abs() > BigDecimal.valueOf(Double.MAX_VALUE)) throw NumberFormatException()
-
- return if (this.compareTo(BigDecimal.ZERO) == 0) BigDecimal.ZERO else this.stripTrailingZeros()
-}
diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt
index 3ce3cc98..5137a6be 100644
--- a/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt
+++ b/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt
@@ -18,12 +18,6 @@
package com.sadellie.unitto.screens.main
-import android.content.res.Configuration
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.MoreVert
@@ -40,9 +34,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.sadellie.unitto.R
@@ -59,7 +51,6 @@ fun MainScreen(
viewModel: MainViewModel = viewModel()
) {
var launched: Boolean by rememberSaveable { mutableStateOf(false) }
-
val mainScreenUIState = viewModel.mainFlow.collectAsStateWithLifecycle()
Scaffold(
@@ -82,11 +73,10 @@ fun MainScreen(
)
},
content = { padding ->
- PortraitMainScreenContent(
+ MainScreenContent(
modifier = Modifier.padding(padding),
unitFrom = viewModel.unitFrom,
unitTo = viewModel.unitTo,
- portrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT,
mainScreenUIState = mainScreenUIState.value,
navControllerAction = { navControllerAction(it) },
swapMeasurements = { viewModel.swapUnits() },
@@ -108,11 +98,10 @@ fun MainScreen(
}
@Composable
-private fun PortraitMainScreenContent(
+private fun MainScreenContent(
modifier: Modifier,
unitFrom: AbstractUnit,
unitTo: AbstractUnit,
- portrait: Boolean = true,
mainScreenUIState: MainScreenUIState = MainScreenUIState(),
navControllerAction: (String) -> Unit = {},
swapMeasurements: () -> Unit = {},
@@ -120,14 +109,11 @@ private fun PortraitMainScreenContent(
deleteDigit: () -> Unit = {},
clearInput: () -> Unit = {},
) {
- if (portrait) {
- Column(
- modifier
- .fillMaxSize(),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
+ PortraitLandscape(
+ modifier = modifier,
+ content1 = {
TopScreenPart(
- modifier = Modifier,
+ modifier = it,
inputValue = mainScreenUIState.inputValue,
calculatedValue = mainScreenUIState.calculatedValue,
outputValue = mainScreenUIState.resultValue,
@@ -139,48 +125,14 @@ private fun PortraitMainScreenContent(
onUnitSelectionClick = navControllerAction,
swapUnits = swapMeasurements
)
- // Keyboard which takes half the screen
+ },
+ content2 = {
Keyboard(
- Modifier
- .fillMaxSize()
- .padding(horizontal = 8.dp),
+ modifier = it,
addDigit = processInput,
deleteDigit = deleteDigit,
clearInput = clearInput,
)
}
- } else {
- Row(
- modifier
- .fillMaxSize(),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- TopScreenPart(
- modifier = Modifier
- .weight(1f)
- .fillMaxHeight(),
- inputValue = mainScreenUIState.inputValue,
- calculatedValue = mainScreenUIState.calculatedValue,
- outputValue = mainScreenUIState.resultValue,
- unitFrom = unitFrom,
- unitTo = unitTo,
- loadingDatabase = mainScreenUIState.isLoadingDatabase,
- loadingNetwork = mainScreenUIState.isLoadingNetwork,
- networkError = mainScreenUIState.showError,
- onUnitSelectionClick = navControllerAction,
- swapUnits = swapMeasurements
- )
-
- // Keyboard which takes half the screen
- Keyboard(
- Modifier
- .weight(1f)
- .fillMaxSize()
- .padding(horizontal = 8.dp),
- addDigit = processInput,
- deleteDigit = deleteDigit,
- clearInput = clearInput,
- )
- }
- }
+ )
}
diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt b/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt
index 097614d8..e77ea5b7 100644
--- a/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt
+++ b/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt
@@ -59,9 +59,9 @@ import com.sadellie.unitto.data.units.database.MyBasedUnit
import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository
import com.sadellie.unitto.data.units.remote.CurrencyApi
import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse
-import com.sadellie.unitto.screens.combine
-import com.sadellie.unitto.screens.toStringWith
-import com.sadellie.unitto.screens.trimZeros
+import com.sadellie.unitto.data.combine
+import com.sadellie.unitto.data.toStringWith
+import com.sadellie.unitto.data.trimZeros
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/PortraitLandscape.kt b/app/src/main/java/com/sadellie/unitto/screens/main/PortraitLandscape.kt
new file mode 100644
index 00000000..a9964392
--- /dev/null
+++ b/app/src/main/java/com/sadellie/unitto/screens/main/PortraitLandscape.kt
@@ -0,0 +1,74 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2022 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.screens.main
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.dp
+
+/**
+ * When Portrait mode will place [content1] and [content2] in a [Column].
+ *
+ * When Landscape mode will place [content1] and [content2] in a [Row].
+ */
+@Composable
+fun PortraitLandscape(
+ modifier: Modifier,
+ content1: @Composable (Modifier) -> Unit,
+ content2: @Composable (Modifier) -> Unit,
+) {
+ if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ content1(Modifier)
+ content2(
+ Modifier
+ .fillMaxSize()
+ .padding(horizontal = 8.dp)
+ )
+ }
+ } else {
+ Row(
+ modifier = modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ content1(
+ Modifier
+ .weight(1f)
+ .fillMaxHeight()
+ )
+ content2(
+ Modifier
+ .weight(1f)
+ .fillMaxSize()
+ .padding(horizontal = 8.dp)
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/components/MyTextField.kt b/app/src/main/java/com/sadellie/unitto/screens/main/components/MyTextField.kt
index a4609b3a..991bbd1d 100644
--- a/app/src/main/java/com/sadellie/unitto/screens/main/components/MyTextField.kt
+++ b/app/src/main/java/com/sadellie/unitto/screens/main/components/MyTextField.kt
@@ -51,51 +51,45 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.R
-import com.sadellie.unitto.screens.Formatter
import com.sadellie.unitto.ui.theme.NumbersTextStyleDisplayLarge
import com.sadellie.unitto.ui.theme.NumbersTextStyleDisplayMedium
/**
* Component for input and output
*
- * @param modifier Modifier that is applied to [LazyRow]
+ * @param modifier Modifier that is applied to [LazyRow].
* @param primaryText Primary text to show (input/output).
* @param secondaryText Secondary text to show (input, calculated result).
- * @param helperText Helper text below current text (short unit name)
- * @param showLoading Show "Loading" text
- * @param showError Show "Error" text
+ * @param helperText Helper text below current text (short unit name).
+ * @param textToCopy Text that will be copied to clipboard when long-clicking.
*/
@Composable
fun MyTextField(
- modifier: Modifier = Modifier,
- primaryText: String = String(),
- secondaryText: String? = null,
- helperText: String = String(),
- showLoading: Boolean = false,
- showError: Boolean = false
+ modifier: Modifier,
+ primaryText: @Composable () -> String,
+ secondaryText: String?,
+ helperText: String,
+ textToCopy: String,
) {
val clipboardManager = LocalClipboardManager.current
val mc = LocalContext.current
- val textToShow = when {
- showError -> stringResource(R.string.error_label)
- showLoading -> stringResource(R.string.loading_label)
- else -> Formatter.format(primaryText)
- }
+ val textToShow: String = primaryText()
val copiedText: String =
- stringResource(R.string.copied, secondaryText?.let { Formatter.format(it) } ?: textToShow)
+ stringResource(R.string.copied, textToCopy)
- Column(modifier = Modifier
- .combinedClickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = rememberRipple(),
- onClick = {},
- onLongClick = {
- clipboardManager.setText(AnnotatedString(secondaryText ?: textToShow))
- Toast
- .makeText(mc, copiedText, Toast.LENGTH_SHORT)
- .show()
- }
- )
+ Column(
+ modifier = Modifier
+ .combinedClickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(),
+ onClick = {},
+ onLongClick = {
+ clipboardManager.setText(AnnotatedString(secondaryText ?: textToShow))
+ Toast
+ .makeText(mc, copiedText, Toast.LENGTH_SHORT)
+ .show()
+ }
+ )
) {
LazyRow(
modifier = modifier
@@ -153,7 +147,7 @@ fun MyTextField(
Text(
modifier = Modifier,
// Quick fix to prevent the UI from crashing
- text = Formatter.format(it?.take(1000) ?: ""),
+ text = it?.take(1000) ?: "",
textAlign = TextAlign.End,
softWrap = false,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt
index ea69eb4b..bee0e89e 100644
--- a/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt
+++ b/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt
@@ -44,6 +44,7 @@ import com.sadellie.unitto.R
import com.sadellie.unitto.data.NavRoutes.LEFT_LIST_SCREEN
import com.sadellie.unitto.data.NavRoutes.RIGHT_LIST_SCREEN
import com.sadellie.unitto.data.units.AbstractUnit
+import com.sadellie.unitto.screens.Formatter
/**
* Top of the main screen. Contains input and output TextFields, and unit selection row of buttons.
@@ -88,20 +89,30 @@ fun TopScreenPart(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MyTextField(
- Modifier.fillMaxWidth(),
- inputValue,
- calculatedValue,
- stringResource(if (loadingDatabase) R.string.loading_label else unitFrom.shortName),
- loadingNetwork,
- networkError
+ modifier = Modifier.fillMaxWidth(),
+ primaryText = {
+ when {
+ loadingDatabase || loadingNetwork -> stringResource(R.string.loading_label)
+ networkError -> stringResource(R.string.error_label)
+ else -> Formatter.format(inputValue)
+ }
+ },
+ secondaryText = calculatedValue?.let { Formatter.format(it) },
+ helperText = stringResource(if (loadingDatabase) R.string.loading_label else unitFrom.shortName),
+ textToCopy = calculatedValue ?: inputValue,
)
MyTextField(
- Modifier.fillMaxWidth(),
- outputValue,
- null,
- stringResource(if (loadingDatabase) R.string.loading_label else unitTo.shortName),
- loadingNetwork,
- networkError
+ modifier = Modifier.fillMaxWidth(),
+ primaryText = {
+ when {
+ loadingDatabase || loadingNetwork -> stringResource(R.string.loading_label)
+ networkError -> stringResource(R.string.error_label)
+ else -> Formatter.format(outputValue)
+ }
+ },
+ secondaryText = null,
+ helperText = stringResource(if (loadingDatabase) R.string.loading_label else unitTo.shortName),
+ textToCopy = outputValue,
)
// Unit selection buttons
Row(
diff --git a/app/src/test/java/com/sadellie/unitto/screens/LevenshteinFilterAndSortTest.kt b/app/src/test/java/com/sadellie/unitto/screens/LevenshteinFilterAndSortTest.kt
index ce19fd40..54c7a72b 100644
--- a/app/src/test/java/com/sadellie/unitto/screens/LevenshteinFilterAndSortTest.kt
+++ b/app/src/test/java/com/sadellie/unitto/screens/LevenshteinFilterAndSortTest.kt
@@ -21,6 +21,7 @@ package com.sadellie.unitto.screens
import com.sadellie.unitto.data.units.AbstractUnit
import com.sadellie.unitto.data.units.MyUnit
import com.sadellie.unitto.data.units.UnitGroup
+import com.sadellie.unitto.data.units.sortByLev
import org.junit.Assert.assertEquals
import org.junit.Test
import java.math.BigDecimal
diff --git a/app/src/test/java/com/sadellie/unitto/screens/MinimumRequiredScaleTest.kt b/app/src/test/java/com/sadellie/unitto/screens/MinimumRequiredScaleTest.kt
index 4facd9b7..3982b0b5 100644
--- a/app/src/test/java/com/sadellie/unitto/screens/MinimumRequiredScaleTest.kt
+++ b/app/src/test/java/com/sadellie/unitto/screens/MinimumRequiredScaleTest.kt
@@ -18,6 +18,7 @@
package com.sadellie.unitto.screens
+import com.sadellie.unitto.data.setMinimumRequiredScale
import org.junit.Assert.assertEquals
import org.junit.Test
import java.math.BigDecimal