mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 00:35:26 +02:00
Merge branch 'refactor/ui'
This commit is contained in:
commit
84b428e620
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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()
|
||||
}
|
42
app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt
Normal file
42
app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T1, T2, T3, T4, T5, T6, R> combine(
|
||||
flow: Flow<T1>,
|
||||
flow2: Flow<T2>,
|
||||
flow3: Flow<T3>,
|
||||
flow4: Flow<T4>,
|
||||
flow5: Flow<T5>,
|
||||
flow6: Flow<T6>,
|
||||
transform: suspend (T1, T2, T3, T4, T5, T6) -> R
|
||||
): Flow<R> =
|
||||
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,
|
||||
)
|
||||
}
|
@ -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<AbstractUnit>.sortByLev(stringA: String): Sequence<AbstractUnit> {
|
||||
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<Pair<AbstractUnit, Int>>()
|
||||
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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
116
app/src/main/java/com/sadellie/unitto/screens/Formatter.kt
Normal file
116
app/src/main/java/com/sadellie/unitto/screens/Formatter.kt
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String> = 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
|
||||
}
|
||||
}
|
60
app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt
Normal file
60
app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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]
|
||||
}
|
30
app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt
Normal file
30
app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)))
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String> = 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<AbstractUnit>.sortByLev(stringA: String): Sequence<AbstractUnit> {
|
||||
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<Pair<AbstractUnit, Int>>()
|
||||
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 <T1, T2, T3, T4, T5, T6, R> combine(
|
||||
flow: Flow<T1>,
|
||||
flow2: Flow<T2>,
|
||||
flow3: Flow<T3>,
|
||||
flow4: Flow<T4>,
|
||||
flow5: Flow<T5>,
|
||||
flow6: Flow<T6>,
|
||||
transform: suspend (T1, T2, T3, T4, T5, T6,) -> R
|
||||
): Flow<R> = 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()
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user