/*
* 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 android.util.Log
import com.sadellie.unitto.FirebaseHelper
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.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 java.text.NumberFormat
import java.util.*
import kotlin.math.floor
import kotlin.math.log10
import kotlin.math.max
object Formatter {
private var nf: NumberFormat = NumberFormat.getInstance(Locale.GERMANY)
/**
* Currently used symbol to separate fractional part
*/
var fractional = KEY_COMMA
/**
* Change current separator
*
* @param separator [Separator] to change to
*/
fun setSeparator(separator: Int) {
nf = when (separator) {
Separator.PERIOD -> NumberFormat.getInstance(Locale.GERMANY)
Separator.COMMA -> NumberFormat.getInstance(Locale.US)
// SPACE BASICALLY
else -> NumberFormat.getInstance(Locale.FRANCE)
}
fractional = if (separator == Separator.PERIOD) KEY_COMMA else KEY_DOT
}
/**
* Custom formatter function which work with big decimals and with strings ending with a dot.
* Also doesn't lose any precision
* @param[input] The string we want to format. Will be split with dot symbol
*/
fun format(input: String): String {
// NOTE: We receive input like 1234 or 1234. or 1234.5
// NOTICE DOTS, not COMMAS
// For engineering string we only replace decimal separator
if (input.contains(KEY_E)) return input.replace(KEY_DOT, fractional)
// Stupid Huawei catching impossible bugs, stupid workaround
return try {
var result = String()
// Formatting everything before fractional part
result += nf.format(input.substringBefore(KEY_DOT).toBigInteger())
// Now we add the part after dot
if (input.contains(KEY_DOT)) {
result += fractional + input.substringAfter(KEY_DOT)
}
result
} catch (e: Exception) {
Log.e("FormatterError", e.toString())
FirebaseHelper().recordException(e)
input
}
}
}
/**
* 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. Doesn't really matter which string goes first
*
* @param stringToCompare Second string
* @return The amount of changes that are needed to transform one string into another
*/
fun String.lev(stringToCompare: String): Int {
val stringA = this
val stringB = stringToCompare
// 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 stringToCompare 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,
flow7: Flow,
flow8: Flow,
transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R
): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { 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,
args[6] as T7,
args[7] as T8
)
}