Make unit converter parsing more lenient and add units of volume

Close #1323
This commit is contained in:
MM20
2025-10-05 22:36:20 +02:00
parent f508ac65b5
commit 9205025761
8 changed files with 322 additions and 22 deletions

View File

@@ -39,6 +39,7 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.icons.Cube
import de.mm20.launcher2.search.data.CurrencyUnitConverter
import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.ui.R
@@ -220,7 +221,7 @@ fun getDimensionIcon(dimension: Dimension): ImageVector {
Dimension.Mass -> Icons.Rounded.FitnessCenter
Dimension.Length -> Icons.Rounded.Straighten
Dimension.Velocity -> Icons.Rounded.Speed
Dimension.Volume -> TODO()
Dimension.Volume -> Icons.Rounded.Cube
Dimension.Area -> Icons.Rounded.SquareFoot
Dimension.Currency -> Icons.Rounded.Toll
Dimension.Data -> Icons.Rounded.Storage

View File

@@ -714,7 +714,7 @@ fun CustomizeCustomIntent(viewModel: EditSearchActionSheetVM) {
supportingText = {
Text(
if (action.queryKey == null) {
"The URI template that is used to construct the intent\\'s data URI. Use \${1} as a placeholder for the actual search term, e.g. geo:0,0?q=,\${1}"
"The URI template that is used to construct the intent\\'s data URI. Use \${1} as a placeholder for the actual search term"
} else {
"The template that is used to construct the string that is passed to the intent as a string extra. Use \${1} as a placeholder for the actual search term"
}

View File

@@ -2305,3 +2305,47 @@ private val _Climbing = materialIcon("Icons.Rounded.Climbing") {
val Icons.Rounded.Climbing
get() = _Climbing
private val _Cube = materialIcon("Icons.Rounded.Cube") {
materialPath {
moveTo(11f, 19.425f)
verticalLineToRelative(-6.85f)
lineTo(5f, 9.1f)
verticalLineToRelative(6.85f)
close()
moveToRelative(2f, 0f)
lineToRelative(6f, -3.475f)
verticalLineTo(9.1f)
lineToRelative(-6f, 3.475f)
close()
moveTo(12f, 10.85f)
lineTo(17.925f, 7.425f)
lineTo(12f, 4f)
lineTo(6.075f, 7.425f)
close()
moveTo(4f, 17.7f)
quadTo(3.525f, 17.425f, 3.2625f, 16.975f)
quadTo(3f, 16.525f, 3f, 15.975f)
verticalLineToRelative(-7.95f)
quadToRelative(0f, -0.55f, 0.2625f, -1f)
quadTo(3.525f, 6.575f, 4f, 6.3f)
lineTo(11f, 2.275f)
quadTo(11.475f, 2f, 12f, 2f)
quadTo(12.525f, 2f, 13f, 2.275f)
lineTo(20f, 6.3f)
quadTo(20.475f, 6.575f, 20.7375f, 7.025f)
quadTo(21f, 7.475f, 21f, 8.025f)
verticalLineToRelative(7.95f)
quadToRelative(0f, 0.55f, -0.2625f, 1f)
quadTo(20.475f, 17.425f, 20f, 17.7f)
lineToRelative(-7f, 4.025f)
quadTo(12.525f, 22f, 12f, 22f)
quadTo(11.475f, 22f, 11f, 21.725f)
close()
moveTo(12f, 12f)
close()
}
}
val Icons.Rounded.Cube
get() = _Cube

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="unit_gallon_imp_symbol">gal</string>
<plurals name="unit_gallon_imp">
<item quantity="one">gallon</item>
<item quantity="other">gallons</item>
</plurals>
<string name="unit_pint_imp_symbol">pt</string>
<plurals name="unit_pint_imp">
<item quantity="one">pint</item>
<item quantity="other">pints</item>
</plurals>
<string name="unit_fluid_ounce_imp_symbol">fl oz</string>
<plurals name="unit_fluid_ounce_imp">
<item quantity="one">fluid ounce</item>
<item quantity="other">fluid ounces</item>
</plurals>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="unit_gallon_us_symbol">gal</string>
<plurals name="unit_gallon_us">
<item quantity="one">gallon</item>
<item quantity="other">gallons</item>
</plurals>
<string name="unit_pint_us_symbol">pt</string>
<plurals name="unit_pint_us">
<item quantity="one">pint</item>
<item quantity="other">pints</item>
</plurals>
<string name="unit_fluid_ounce_us_symbol">fl oz</string>
<plurals name="unit_fluid_ounce_us">
<item quantity="one">fluid ounce</item>
<item quantity="other">fluid ounces</item>
</plurals>
</resources>

View File

@@ -294,4 +294,92 @@
<item quantity="one">kelvin</item>
<item quantity="other">kelvins</item>
</plurals>
<!-- UNITS OF VOLUME -->
<string name="unit_liter_symbol">l</string>
<plurals name="unit_liter">
<item quantity="one">liter</item>
<item quantity="other">liters</item>
</plurals>
<string name="unit_milliliter_symbol">ml</string>
<plurals name="unit_milliliter">
<item quantity="one">milliliter</item>
<item quantity="other">milliliters</item>
</plurals>
<string name="unit_cubic_meter_symbol"></string>
<plurals name="unit_cubic_meter">
<item quantity="one">cubic meter</item>
<item quantity="other">cubic meters</item>
</plurals>
<string name="unit_cubic_centimeter_symbol">cm³</string>
<plurals name="unit_cubic_centimeter">
<item quantity="one">cubic centimeter</item>
<item quantity="other">cubic centimeters</item>
</plurals>
<string name="unit_cubic_millimeter_symbol">mm³</string>
<plurals name="unit_cubic_millimeter">
<item quantity="one">cubic millimeter</item>
<item quantity="other">cubic millimeters</item>
</plurals>
<string name="unit_cubic_inch_symbol">cuin</string>
<plurals name="unit_cubic_inch">
<item quantity="one">cubic inch</item>
<item quantity="other">cubic inches</item>
</plurals>
<string name="unit_cubic_foot_symbol">cuft</string>
<plurals name="unit_cubic_foot">
<item quantity="one">cubic foot</item>
<item quantity="other">cubic feet</item>
</plurals>
<string name="unit_cubic_yard_symbol">cuyd</string>
<plurals name="unit_cubic_yard">
<item quantity="one">cubic yard</item>
<item quantity="other">cubic yards</item>
</plurals>
<string name="unit_gallon_us_symbol">gal US</string>
<plurals name="unit_gallon_us">
<item quantity="one">gallon (US)</item>
<item quantity="other">gallons (US)</item>
</plurals>
<string name="unit_gallon_imp_symbol">gal imp</string>
<plurals name="unit_gallon_imp">
<item quantity="one">gallon (imperial)</item>
<item quantity="other">gallons (imperial)</item>
</plurals>
<string name="unit_pint_us_symbol">pt US</string>
<plurals name="unit_pint_us">
<item quantity="one">pint (US)</item>
<item quantity="other">pints (US)</item>
</plurals>
<string name="unit_pint_imp_symbol">pt imp</string>
<plurals name="unit_pint_imp">
<item quantity="one">pint (imperial)</item>
<item quantity="other">pints (imperial)</item>
</plurals>
<string name="unit_fluid_ounce_us_symbol">fl.oz.US</string>
<plurals name="unit_fluid_ounce_us">
<item quantity="one">US fluid ounce</item>
<item quantity="other">US fluid ounces</item>
</plurals>
<string name="unit_fluid_ounce_imp_symbol">fl.oz.imp</string>
<plurals name="unit_fluid_ounce_imp">
<item quantity="one">fluid ounce (imperial)</item>
<item quantity="other">fluid ounces (imperial)</item>
</plurals>
<string name="unit_cup_symbol">cup</string>
<plurals name="unit_cup">
<item quantity="one">cup (US)</item>
<item quantity="other">cups (US)</item>
</plurals>
<string name="unit_tablespoon_symbol">tbsp</string>
<plurals name="unit_tablespoon">
<item quantity="one">tablespoon (US)</item>
<item quantity="other">tablespoons (US)</item>
</plurals>
<string name="unit_teaspoon_symbol">tsp</string>
<plurals name="unit_teaspoon">
<item quantity="one">teaspoon (US)</item>
<item quantity="other">teaspoons (US)</item>
</plurals>
</resources>

View File

@@ -13,13 +13,13 @@ import de.mm20.launcher2.unitconverter.converters.MassConverter
import de.mm20.launcher2.unitconverter.converters.TemperatureConverter
import de.mm20.launcher2.unitconverter.converters.TimeConverter
import de.mm20.launcher2.unitconverter.converters.VelocityConverter
import de.mm20.launcher2.unitconverter.converters.VolumeConverter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -27,7 +27,7 @@ import org.koin.core.component.KoinComponent
interface UnitConverterRepository {
fun search(query: String): Flow<UnitConverter?>
suspend fun getAvailableConverters(includeCurrencies: Boolean) : List<Converter>
suspend fun getAvailableConverters(includeCurrencies: Boolean): List<Converter>
}
internal class UnitConverterRepositoryImpl(
@@ -56,7 +56,7 @@ internal class UnitConverterRepositoryImpl(
}
}
override suspend fun getAvailableConverters(includeCurrencies: Boolean) : List<Converter> {
override suspend fun getAvailableConverters(includeCurrencies: Boolean): List<Converter> {
val converters = mutableListOf(
MassConverter(context),
LengthConverter(context),
@@ -64,7 +64,8 @@ internal class UnitConverterRepositoryImpl(
TimeConverter(context),
VelocityConverter(context),
AreaConverter(context),
TemperatureConverter(context)
TemperatureConverter(context),
VolumeConverter(context),
)
if (includeCurrencies) converters.add(CurrencyConverter(currencyRepository))
@@ -75,28 +76,51 @@ internal class UnitConverterRepositoryImpl(
query: String,
includeCurrencies: Boolean
): UnitConverter? {
if (!query.matches(Regex("[0-9,.:]+ [^\\s]+")) &&
!query.matches(Regex("[0-9,.:]+ [^\\s]+ >> [^\\s]+")) &&
!query.matches(Regex("[0-9,.:]+ [^\\s]+ > [^\\s]+")) &&
!query.matches(Regex("[0-9,.:]+ [^\\s]+ - [^\\s]+"))) return null
val valueStr: String
val unitStr: String
val targetUnitStr: String?
val regex = Regex("""([+\-]?[\d+\-e,.]+|[^\d>\-]+)""")
query.split(" ").also {
valueStr = it.get(0)
unitStr = it.get(1)
targetUnitStr = it.getOrNull(3)
val matches = regex.findAll(query)
var inputStr: String? = null
var inputValue: Double? = null
var inputUnit: String? = null
var outputUnit: String? = null
for ((i, match) in matches.withIndex()) {
when (i) {
0 -> {
inputStr = match.value.trim()
inputValue = inputStr.toDoubleOrNull()
?: inputStr.replace(',', '.').toDoubleOrNull()
?: return null
}
1 -> inputUnit = match.value.trim()
2 -> {
if (!match.value.contains("-") && !match.value.contains(">")) {
outputUnit = match.value.trim()
}
}
3 -> {
if (outputUnit == null) {
outputUnit = match.value.trim()
break
} else {
return null
}
}
else -> return null
}
}
if (inputValue == null || inputUnit == null) {
return null
}
val value = valueStr.toDoubleOrNull() ?: valueStr.replace(',', '.').toDoubleOrNull()
?: return null
val converters = getAvailableConverters(includeCurrencies)
for (converter in converters) {
if (!converter.isValidUnit(unitStr)) continue
if (targetUnitStr != null && !converter.isValidUnit(targetUnitStr)) continue
return converter.convert(context, unitStr, value, targetUnitStr)
if (!converter.isValidUnit(inputUnit)) continue
if (outputUnit != null && !converter.isValidUnit(outputUnit)) continue
return converter.convert(context, inputUnit, inputValue, outputUnit)
}
return null
}

View File

@@ -0,0 +1,107 @@
package de.mm20.launcher2.unitconverter.converters
import android.content.Context
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.R
internal class VolumeConverter(context: Context) : SimpleFactorConverter() {
override val dimension: Dimension = Dimension.Volume
override val standardUnits = listOf(
// SI units
MeasureUnitWithFactor(
factor = 1.0,
symbol = context.getString(R.string.unit_cubic_meter_symbol),
nameResource = R.plurals.unit_cubic_meter
),
MeasureUnitWithFactor(
factor = 1_000_000.0,
symbol = context.getString(R.string.unit_cubic_centimeter_symbol),
nameResource = R.plurals.unit_cubic_centimeter
),
MeasureUnitWithFactor(
factor = 1_000_000_000.0,
symbol = context.getString(R.string.unit_cubic_millimeter_symbol),
nameResource = R.plurals.unit_cubic_millimeter
),
// Other metric units
MeasureUnitWithFactor(
factor = 1000.0,
symbol = context.getString(R.string.unit_liter_symbol),
nameResource = R.plurals.unit_liter
),
MeasureUnitWithFactor(
factor = 1_000_000.0,
symbol = context.getString(R.string.unit_milliliter_symbol),
nameResource = R.plurals.unit_milliliter
),
// Imperial geometric units
MeasureUnitWithFactor(
factor = 61023.74409473228,
symbol = context.getString(R.string.unit_cubic_inch_symbol),
nameResource = R.plurals.unit_cubic_inch
),
MeasureUnitWithFactor(
factor = 61023.74409473228 / 1728.0,
symbol = context.getString(R.string.unit_cubic_foot_symbol),
nameResource = R.plurals.unit_cubic_foot
),
MeasureUnitWithFactor(
factor = 61023.74409473228 / (1728.0 * 27.0),
symbol = context.getString(R.string.unit_cubic_yard_symbol),
nameResource = R.plurals.unit_cubic_yard
),
// US fluid units
MeasureUnitWithFactor(
factor = 264.1720523581484,
symbol = context.getString(R.string.unit_gallon_us_symbol),
nameResource = R.plurals.unit_gallon_us
),
MeasureUnitWithFactor(
factor = 264.1720523581484 * 8,
symbol = context.getString(R.string.unit_pint_us_symbol),
nameResource = R.plurals.unit_pint_us
),
MeasureUnitWithFactor(
factor = 264.1720523581484 * 16,
symbol = context.getString(R.string.unit_cup_symbol),
nameResource = R.plurals.unit_cup
),
MeasureUnitWithFactor(
factor = 264.1720523581484 * 128,
symbol = context.getString(R.string.unit_fluid_ounce_us_symbol),
nameResource = R.plurals.unit_fluid_ounce_us
),
MeasureUnitWithFactor(
factor = 264.1720523581484 * 256,
symbol = context.getString(R.string.unit_tablespoon_symbol),
nameResource = R.plurals.unit_tablespoon
),
MeasureUnitWithFactor(
factor = 264.1720523581484 * 768,
symbol = context.getString(R.string.unit_teaspoon_symbol),
nameResource = R.plurals.unit_teaspoon
),
// Imperial fluid units
MeasureUnitWithFactor(
factor = 219.9692482990878,
symbol = context.getString(R.string.unit_gallon_imp_symbol),
nameResource = R.plurals.unit_gallon_imp
),
MeasureUnitWithFactor(
factor = 219.969248299087 * 8,
symbol = context.getString(R.string.unit_pint_imp_symbol),
nameResource = R.plurals.unit_pint_imp
),
MeasureUnitWithFactor(
factor = 219.969248299087 * 128,
symbol = context.getString(R.string.unit_fluid_ounce_imp_symbol),
nameResource = R.plurals.unit_fluid_ounce_imp
),
)
}