Merge branch 'master' into feat/time-zone-converter

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
#	core/base/src/main/res/values/strings.xml
#	settings.gradle.kts
This commit is contained in:
sadellie 2023-07-21 23:15:32 +03:00
commit bba2f24c7c
183 changed files with 8550 additions and 4326 deletions

View File

@ -1,9 +0,0 @@
<p align="middle">
<img src="./content/donate.png" width="99%" />
</p>
I don't need your money, just help Unitto gain **more users**:
- Tell your relatives
- Tell your friends
- Tell strangers on the streets
- Spread the word, take over the world... or something like that

View File

@ -11,8 +11,7 @@
## 📲 Download
<a href="https://play.google.com/store/apps/details?id=com.sadellie.unitto"><img alt="Google Play" src="./content/googlePlay.png" width="32%"/></a>
<a href="https://f-droid.org/packages/com.sadellie.unitto"><img alt="F-Droid" src="./content/fDroid.png" width="32%"/></a>
<a href="https://apps.rustore.ru/app/com.sadellie.unitto"><img alt="Rustore" src="./content/ruStore.png" width="32%"/></a>
<a href="https://f-droid.org/packages/com.sadellie.unitto"><img alt="F-Droid" src="./content/fDroid.png" width="27%"/></a>
## 😎 Features
- **Instant** expression evaluation
@ -29,25 +28,21 @@
- Customizable number **formatter**
- **SI Standard**
## 👅 Translate
<a href="https://poeditor.com/join/project/T4zjmoq8dx" target="_blank"><img src="./content/poeditor.png" alt="Unitto - Calculate&#0032;and&#0032;convert&#0044;&#0032;but&#0032;better&#0046; | POEditor" style="width: 180px; height: 60px;" width="180" height="60" /></a>
## 👅 [Translate](https://poeditor.com/join/project/T4zjmoq8dx)
Join on **POEditor** to help.
## 🤑 Donate
Visit [FUNDING.md](./FUNDING.md)
## 💡 [Open issues](https://github.com/sadellie/unitto/issues/new)
Report bugs or request improvements. I may close your issue as not planned and reopen it later (things change).
## 🎤 [Start discussions](https://github.com/sadellie/unitto/discussions/new/choose)
If you think that your question will not fit in "Issues", start a discussion.
## 👩‍💻 ~~Contribute code~~
Code contributions are **not** welcomed. If you really want to, **ask me** first.
Hard forks and alterations of Unitto are **not** welcomed. Use a "Fork" button so that commits' author is not lost.
## 🔎 Additional
<p align="middle">
<a href="https://trello.com/b/cxAbRlvu/unitto" target="_blank">
<img src="./content/progress.png" width="99%"/>
</a>
</p>
Terms and Conditions: https://sadellie.github.io/unitto/terms
<a href="https://www.producthunt.com/posts/unitto?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-unitto" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=372851&theme=light" alt="Unitto - Calculate&#0032;and&#0032;convert&#0044;&#0032;but&#0032;better&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
Terms and Conditions, Privacy Policy, Press Kit and contact links:
https://sadellie.github.io/unitto/
## 🤓 Technical details
- App is written in Compose
- Multi-module architecture
- Convention plugins for modules
Privacy Policy: https://sadellie.github.io/unitto/privacy

View File

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("UnstableApiUsage")
plugins {
// Basic stuff
id("com.android.application")
@ -26,14 +28,14 @@ plugins {
android {
namespace = "com.sadellie.unitto"
compileSdk = 33
compileSdk = 34
defaultConfig {
applicationId = "com.sadellie.unitto"
minSdk = 21
targetSdk = 33
versionCode = 20
versionName = "Kobicha"
targetSdk = 34
versionCode = 22
versionName = "Lilac Luster"
}
buildTypes {
@ -74,19 +76,19 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
packagingOptions {
packaging {
resources {
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
}
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi"
)
@ -97,6 +99,12 @@ android {
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
}
dependencies {
implementation(libs.androidx.core)
coreLibraryDesugaring(libs.android.desugarJdkLibs)
@ -112,9 +120,8 @@ dependencies {
implementation(project(mapOf("path" to ":feature:calculator")))
implementation(project(mapOf("path" to ":feature:settings")))
implementation(project(mapOf("path" to ":feature:unitslist")))
implementation(project(mapOf("path" to ":feature:epoch")))
implementation(project(mapOf("path" to ":feature:datedifference")))
implementation(project(mapOf("path" to ":feature:timezone")))
implementation(project(mapOf("path" to ":data:units")))
implementation(project(mapOf("path" to ":data:model")))
implementation(project(mapOf("path" to ":data:userprefs")))
implementation(project(mapOf("path" to ":core:ui")))

View File

@ -41,10 +41,10 @@ internal class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
val userPrefs = userPrefsRepository.userPreferencesFlow
val uiPrefs = userPrefsRepository.uiPreferencesFlow
.collectAsStateWithLifecycle(null).value
if (userPrefs != null) UnittoApp(userPrefs)
if (uiPrefs != null) UnittoApp(uiPrefs)
}
}

View File

@ -18,11 +18,10 @@
package com.sadellie.unitto
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.tween
import androidx.compose.material3.DrawerValue
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@ -40,35 +39,44 @@ import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.core.ui.common.UnittoDrawerSheet
import com.sadellie.unitto.core.ui.common.UnittoModalNavigationDrawer
import com.sadellie.unitto.core.ui.common.close
import com.sadellie.unitto.core.ui.common.isOpen
import com.sadellie.unitto.core.ui.common.open
import com.sadellie.unitto.core.ui.common.rememberUnittoDrawerState
import com.sadellie.unitto.core.ui.model.DrawerItems
import com.sadellie.unitto.core.ui.theme.AppTypography
import com.sadellie.unitto.core.ui.theme.DarkThemeColors
import com.sadellie.unitto.core.ui.theme.LightThemeColors
import com.sadellie.unitto.data.userprefs.UserPreferences
import com.sadellie.unitto.data.userprefs.UIPreferences
import io.github.sadellie.themmo.Themmo
import io.github.sadellie.themmo.rememberThemmoController
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun UnittoApp(userPrefs: UserPreferences) {
internal fun UnittoApp(uiPrefs: UIPreferences) {
val themmoController = rememberThemmoController(
lightColorScheme = LightThemeColors,
darkColorScheme = DarkThemeColors,
// Anything below will not be called if theming mode is still loading from DataStore
themingMode = userPrefs.themingMode,
dynamicThemeEnabled = userPrefs.enableDynamicTheme,
amoledThemeEnabled = userPrefs.enableAmoledTheme,
customColor = userPrefs.customColor,
monetMode = userPrefs.monetMode
themingMode = uiPrefs.themingMode,
dynamicThemeEnabled = uiPrefs.enableDynamicTheme,
amoledThemeEnabled = uiPrefs.enableAmoledTheme,
customColor = uiPrefs.customColor,
monetMode = uiPrefs.monetMode
)
val navController = rememberNavController()
val sysUiController = rememberSystemUiController()
// Navigation drawer stuff
val drawerState = rememberDrawerState(DrawerValue.Closed)
val drawerState = rememberUnittoDrawerState()
val drawerScope = rememberCoroutineScope()
val mainTabs = listOf(DrawerItems.Calculator, DrawerItems.Converter)
val mainTabs = listOf(
DrawerItems.Calculator,
DrawerItems.Converter,
DrawerItems.DateDifference
)
val additionalTabs = listOf(DrawerItems.Settings)
val navBackStackEntry by navController.currentBackStackEntryAsState()
@ -84,14 +92,6 @@ internal fun UnittoApp(userPrefs: UserPreferences) {
}
}
}
val gesturesEnabled: Boolean by remember(navBackStackEntry?.destination) {
derivedStateOf {
// Will be true for routes like
// [null, calculator_route, settings_graph, settings_route, themes_route]
// We disable drawer drag gesture when we are too deep
navController.backQueue.size <= 4
}
}
Themmo(
themmoController = themmoController,
@ -103,10 +103,8 @@ internal fun UnittoApp(userPrefs: UserPreferences) {
mutableStateOf(backgroundColor.luminance() > 0.5f)
}
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = gesturesEnabled,
drawerContent = {
UnittoModalNavigationDrawer(
drawer = {
UnittoDrawerSheet(
modifier = Modifier,
mainTabs = mainTabs,
@ -122,15 +120,24 @@ internal fun UnittoApp(userPrefs: UserPreferences) {
restoreState = true
}
}
}
) {
},
modifier = Modifier,
state = drawerState,
gesturesEnabled = true,
scope = drawerScope,
content = {
UnittoNavigation(
navController = navController,
themmoController = it,
startDestination = userPrefs.startingScreen,
startDestination = uiPrefs.startingScreen,
openDrawer = { drawerScope.launch { drawerState.open() } }
)
}
)
BackHandler(drawerState.isOpen) {
drawerScope.launch { drawerState.close() }
}
LaunchedEffect(useDarkIcons) {
sysUiController.setNavigationBarColor(Color.Transparent, useDarkIcons)

View File

@ -23,14 +23,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import com.sadellie.unitto.feature.calculator.navigation.calculatorScreen
import com.sadellie.unitto.feature.converter.ConverterViewModel
import com.sadellie.unitto.feature.converter.navigation.converterScreen
import com.sadellie.unitto.feature.epoch.navigation.epochScreen
import com.sadellie.unitto.feature.settings.SettingsViewModel
import com.sadellie.unitto.feature.datedifference.navigation.dateDifferenceScreen
import com.sadellie.unitto.feature.settings.navigation.navigateToSettings
import com.sadellie.unitto.feature.settings.navigation.navigateToUnitGroups
import com.sadellie.unitto.feature.settings.navigation.settingGraph
@ -51,27 +49,16 @@ internal fun UnittoNavigation(
) {
val converterViewModel: ConverterViewModel = hiltViewModel()
val unitsListViewModel: UnitsListViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
NavHost(
navController = navController,
startDestination = startDestination,
modifier = Modifier.background(MaterialTheme.colorScheme.background)
) {
fun navigateToSettings() {
navController.navigateToSettings {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
converterScreen(
navigateToLeftScreen = navController::navigateToLeftSide,
navigateToRightScreen = navController::navigateToRightSide,
navigateToSettings = ::navigateToSettings,
navigateToSettings = navController::navigateToSettings,
navigateToMenu = openDrawer,
viewModel = converterViewModel
)
@ -91,7 +78,6 @@ internal fun UnittoNavigation(
)
settingGraph(
settingsViewModel = settingsViewModel,
themmoController = themmoController,
navController = navController,
menuButtonClick = openDrawer
@ -99,14 +85,17 @@ internal fun UnittoNavigation(
calculatorScreen(
navigateToMenu = openDrawer,
navigateToSettings = ::navigateToSettings
navigateToSettings = navController::navigateToSettings
)
epochScreen(navigateToMenu = openDrawer)
dateDifferenceScreen(
navigateToMenu = openDrawer,
navigateToSettings = navController::navigateToSettings
)
timeZoneScreen(
navigateToMenu = openDrawer,
navigateToSettings = ::navigateToSettings
navigateToSettings = navController::navigateToSettings
)
}
}

View File

@ -27,6 +27,12 @@ java {
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
}
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)

View File

@ -22,6 +22,7 @@ import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
@Suppress("UNUSED")
class UnittoHiltPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {

View File

@ -25,6 +25,7 @@ import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
@Suppress("UNUSED")
class UnittoLibraryComposePlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {

View File

@ -22,6 +22,7 @@ import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
@Suppress("UNUSED")
class UnittoLibraryFeaturePlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {

View File

@ -25,6 +25,7 @@ import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
@Suppress("UNUSED")
class UnittoLibraryPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {

View File

@ -23,6 +23,7 @@ import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
@Suppress("UnstableApiUsage")
internal fun Project.configureCompose(
commonExtension: CommonExtension<*, *, *, *>,
) {

View File

@ -25,13 +25,16 @@ import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.plugins.ExtensionAware
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@Suppress("UnstableApiUsage")
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *>,
) {
commonExtension.apply {
compileSdk = 33
compileSdk = 34
defaultConfig {
minSdk = 21
@ -45,8 +48,8 @@ internal fun Project.configureKotlinAndroid(
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
@ -59,7 +62,7 @@ internal fun Project.configureKotlinAndroid(
resValues = false
}
packagingOptions {
packaging {
resources {
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
}
@ -73,7 +76,14 @@ internal fun Project.configureKotlinAndroid(
"-opt-in=androidx.compose.ui.unit.ExperimentalUnitApi",
"-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi"
)
jvmTarget = JavaVersion.VERSION_1_8.toString()
jvmTarget = JavaVersion.VERSION_11.toString()
}
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
// Set JVM target to 11
jvmTarget = JavaVersion.VERSION_11.toString()
}
}

Binary file not shown.

View File

@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositories {
google()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("UnstableApiUsage")
plugins {
id("unitto.library")
}

View File

@ -29,14 +29,3 @@ object OutputFormat {
// App will try it's best to use engineering notation
const val FORCE_ENGINEERING = 2
}
/**
* Available formats. Used in settings
*/
val OUTPUT_FORMAT: Map<Int, Int> by lazy {
mapOf(
OutputFormat.PLAIN to R.string.plain,
OutputFormat.ALLOW_ENGINEERING to R.string.allow_engineering,
OutputFormat.FORCE_ENGINEERING to R.string.force_engineering,
)
}

View File

@ -22,28 +22,3 @@ package com.sadellie.unitto.core.base
* Current maximum scale that will be used in app. Used in various place in code
*/
const val MAX_PRECISION: Int = 1_000
/**
* Currently available scale options
*/
val PRECISIONS: Map<Int, Int> by lazy {
mapOf(
0 to R.string.precision_zero,
1 to R.string.precision_one,
2 to R.string.precision_two,
3 to R.string.precision_three,
4 to R.string.precision_four,
5 to R.string.precision_five,
6 to R.string.precision_six,
7 to R.string.precision_seven,
8 to R.string.precision_eight,
9 to R.string.precision_nine,
10 to R.string.precision_ten,
11 to R.string.precision_eleven,
12 to R.string.precision_twelve,
13 to R.string.precision_thirteen,
14 to R.string.precision_fourteen,
15 to R.string.precision_fifteen,
MAX_PRECISION to R.string.max_precision
)
}

View File

@ -22,18 +22,7 @@ package com.sadellie.unitto.core.base
* Separators mean symbols that separate fractional part
*/
object Separator {
const val SPACES = 0
const val SPACE = 0
const val PERIOD = 1
const val COMMA = 2
}
/**
* Map of separators that is used in settings
*/
val SEPARATORS: Map<Int, Int> by lazy {
mapOf(
Separator.SPACES to R.string.spaces,
Separator.PERIOD to R.string.period,
Separator.COMMA to R.string.comma
)
}

View File

@ -19,6 +19,7 @@
package com.sadellie.unitto.core.base
object Token {
object Digit {
const val _1 = "1"
const val _2 = "2"
const val _3 = "3"
@ -29,93 +30,115 @@ object Token {
const val _8 = "8"
const val _9 = "9"
const val _0 = "0"
const val baseA = "A"
const val baseB = "B"
const val baseC = "C"
const val baseD = "D"
const val baseE = "E"
const val baseF = "F"
const val dot = "."
const val comma = ","
const val E = "E"
val all by lazy {
listOf(_1, _2, _3, _4, _5, _6, _7, _8, _9, _0)
}
val allWithDot by lazy { all + dot }
}
object Letter {
const val _A = "A"
const val _B = "B"
const val _C = "C"
const val _D = "D"
const val _E = "E"
const val _F = "F"
val all by lazy {
listOf(_A, _B, _C, _D, _E, _F)
}
}
object Operator {
const val plus = "+"
const val minus = "-"
const val minusDisplay = ""
const val divide = "/"
const val divideDisplay = "÷"
const val multiply = "*"
const val multiplyDisplay = "×"
const val minus = ""
const val multiply = "×"
const val divide = "÷"
const val leftBracket = "("
const val rightBracket = ")"
const val exponent = "^"
const val sqrt = ""
const val pi = "π"
const val power = "^"
const val factorial = "!"
const val sin = "sin("
const val arSin = "arsin("
const val cos = "cos("
const val arCos = "arcos("
const val tan = "tan("
const val acTan = "actan("
const val e = "e"
const val exp = "exp("
const val modulo = "#"
const val ln = "ln("
const val log = "log("
const val percent = "%"
const val sqrt = ""
val operators by lazy {
val all by lazy {
listOf(
plus,
minus,
minusDisplay,
multiply,
multiplyDisplay,
divide,
divideDisplay,
sqrt,
exponent,
)
}
val digits by lazy {
listOf(
_1,
_2,
_3,
_4,
_5,
_6,
_7,
_8,
_9,
_0,
)
}
val internalToDisplay: Map<String, String> = hashMapOf(
minus to minusDisplay,
multiply to multiplyDisplay,
divide to divideDisplay
)
val knownSymbols: List<String> by lazy {
listOf(
arSin, arCos, acTan, exp,
sin, cos, tan, ln, log,
plus, minus, multiply, divide,
leftBracket, rightBracket,
exponent, sqrt, factorial,
modulo, e, percent, pi,
multiply, multiplyDisplay,
plus, minus, minusDisplay, divide, divideDisplay,
baseA, baseB, baseC, baseD, baseE, baseF,
_1, _2, _3, _4, _5, _6, _7, _8, _9, _0
power, factorial, modulo, percent, sqrt,
)
}
}
object Func {
const val sin = "sin"
const val sinBracket = "$sin("
const val cos = "cos"
const val cosBracket = "$cos("
const val tan = "tan"
const val tanBracket = "$tan("
const val arsin = "sin⁻¹"
const val arsinBracket = "$arsin("
const val arcos = "cos⁻¹"
const val arcosBracket = "$arcos("
const val actan = "tan⁻¹"
const val actanBracket = "$actan("
const val ln = "ln"
const val lnBracket = "$ln("
const val log = "log"
const val logBracket = "$log("
const val exp = "exp"
const val expBracket = "$exp("
val all by lazy {
listOf(
arsin, arcos, actan, sin, cos, tan, log, exp, ln
).sortedByDescending { it.length }
}
val allWithOpeningBracket by lazy {
listOf(
arsinBracket, arcosBracket, actanBracket, sinBracket, cosBracket, tanBracket,
logBracket, expBracket, lnBracket
)
}
}
object Const {
const val pi = "π"
const val e = "e"
val all by lazy {
listOf(pi, e)
}
}
// Used only in formatter, don't use internally
object DisplayOnly {
const val comma = ","
const val engineeringE = "E"
const val minus = ""
}
val expressionTokens by lazy {
Digit.allWithDot + Operator.all + Func.all + Const.all
}
val numberBaseTokens by lazy {
Digit.all + Letter.all
}
val sexyToUgly by lazy {
mapOf(
Operator.minus to listOf("-", "", "", ""),
Operator.divide to listOf("/"),
Operator.multiply to listOf("*", ""),
Func.arsin to listOf("arsin"),
Func.arcos to listOf("arcos"),
Func.actan to listOf("actan")
)
}
}

View File

@ -34,9 +34,9 @@ sealed class TopLevelDestinations(
name = R.string.calculator
)
object Epoch : TopLevelDestinations(
route = "epoch_route",
name = R.string.epoch_converter
object DateDifference : TopLevelDestinations(
route = "date_difference_route",
name = R.string.date_difference
)
object TimeZone : TopLevelDestinations(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -377,7 +377,7 @@
<string name="attopascal_short">aPa</string>
<string name="femtopascal">Femtopascal</string>
<string name="femtopascal_short">fPa</string>
<string name="picopascal">Picopascal</string>
<string name="picopascal">Pikopascal</string>
<string name="picopascal_short">pPa</string>
<string name="nanopascal">Nanopascal</string>
<string name="nanopascal_short">nPa</string>
@ -421,7 +421,7 @@
<string name="attometer_per_square_second_short">am/s^2</string>
<string name="femtometer_per_square_second">Femtometer pro Quadratsekunde</string>
<string name="femtometer_per_square_second_short">fm/s^2</string>
<string name="picometer_per_square_second">Picometer pro Quadratsekunde</string>
<string name="picometer_per_square_second">Pikometer pro Quadratsekunde</string>
<string name="picometer_per_square_second_short">pm/s^2</string>
<string name="nanometer_per_square_second">Nanometer pro Quadratsekunde</string>
<string name="nanometer_per_square_second_short">mm/s^2</string>
@ -659,7 +659,9 @@
<string name="theme_setting">Farbthemen</string>
<string name="precision_setting">Präzision</string>
<string name="separator_setting">Trennzeichen</string>
<string name="output_format_setting">Ausgabeformate</string>
<!-- Fuzzy -->
<string name="exponential_notation_setting">Exponentielle Notation</string>
<string name="unit_groups_setting">Einheitengruppen</string>
<string name="currency_rates_note_setting">Falsche Wechselkurse</string>
<string name="currency_rates_note_title">Hinweis </string>
@ -668,29 +670,29 @@
<string name="privacy_policy">Datenschutz-Bestimmungen </string>
<string name="third_party_licenses">Richtlinien von Drittanbietern </string>
<string name="rate_this_app">Diese App bewerten</string>
<string name="formatting_setting">Formatierung</string>
<string name="additional_settings_group">Zusätzliches</string>
<!-- Precision -->
<string name="precision_setting_support">Anzahl der Dezimalstellen</string>
<string name="precision_setting_info">Umgerechnete Werte können eine höhere Präzision als eingestellt haben.</string>
<string name="max_precision">1 000 (Maximum)</string>
<string name="max_precision">%1$s (Maximum)</string>
<!-- Separator -->
<string name="separator_setting_support">Gruppentrennzeichen</string>
<string name="period">Punkt (42.069,12)</string>
<string name="comma">Komma (42,069.12)</string>
<string name="spaces">Leerzeichen (42 069.12)</string>
<string name="period">Punkt</string>
<string name="comma">Komma</string>
<!-- Fuzzy -->
<string name="space">Leerzeichen</string>
<!-- Output format -->
<string name="output_format_setting_support">Ergebnisformatierung</string>
<string name="output_format_setting_info">Wissenschaftliche Notation sieht wie 1E-21 aus</string>
<string name="plain">Standard</string>
<string name="allow_engineering">Wissenschaftliche Notation erlauben</string>
<string name="force_engineering">Wissenschaftliche Notation erzwingen</string>
<!-- Fuzzy -->
<string name="exponential_notation_setting_support">Einen Teil der Nummer durch E ersetzen</string>
<!-- Theme -->
<string name="theme_setting_support">App Aussehen</string>
<string name="force_auto_mode">Automatisch</string>
<string name="auto_label">Automatisch</string>
<string name="force_light_mode">Hell</string>
<string name="force_dark_mode">Dunkel</string>
<string name="color_theme">Farbthema</string>
@ -726,4 +728,415 @@
<string name="reorder_unit_group_description">Einheitengruppe umordnen</string>
<string name="disable_unit_group_description">Einheitengruppe deaktivieren</string>
<string name="app_version_name_setting">Versionsname</string>
<string name="about_unitto">Über Unitto</string>
<string name="about_unitto_support">Mehr über die App erfahren</string>
<string name="unit_groups_support">Einheiten deaktivieren und anordnen</string>
<string name="cent">Cent</string>
<string name="cent_short">cent</string>
<!-- Flux -->
<string name="maxwell">Maxwell</string>
<string name="maxwell_short">Mx</string>
<string name="weber">Weber</string>
<string name="weber_short">Wb</string>
<string name="milliweber">Milliweber</string>
<string name="milliweber_short">mWb</string>
<string name="microweber">Microweber</string>
<string name="microweber_short">μWb</string>
<string name="kiloweber">Kiloweber</string>
<string name="kiloweber_short">kWb</string>
<string name="megaweber">Megaweber</string>
<string name="megaweber_short">MWb</string>
<string name="gigaweber">Gigaweber</string>
<string name="gigaweber_short">GWb</string>
<string name="flux">Flux</string>
<string name="open_source">Quellcode anzeigen</string>
<string name="translate_app">Diese App übersetzen</string>
<string name="translate_app_support">Dem POEditor-Projekt beitreten und helfen</string>
<!-- Number base -->
<string name="binary">Binär</string>
<string name="binary_short">base2</string>
<string name="ternary">Ternär</string>
<string name="ternary_short">base3</string>
<string name="quaternary">Quartär</string>
<string name="quaternary_short">base4</string>
<string name="quinary">Quinär</string>
<string name="quinary_short">base5</string>
<string name="senary">Senär</string>
<string name="senary_short">base6</string>
<string name="septenary">Septenär</string>
<string name="septenary_short">base7</string>
<string name="octal">Oktal</string>
<string name="octal_short">base8</string>
<string name="nonary">Nonär</string>
<string name="nonary_short">base9</string>
<string name="decimal">Dezimal</string>
<string name="decimal_short">base10</string>
<string name="undecimal">Undezimal</string>
<string name="undecimal_short">base11</string>
<string name="duodecimal">Duodezimal</string>
<string name="duodecimal_short">base12</string>
<string name="tridecimal">Tridezimal</string>
<string name="tridecimal_short">base13</string>
<string name="tetradecimal">Tetradezimal</string>
<string name="tetradecimal_short">base14</string>
<string name="pentadecimal">Pentadezimal</string>
<string name="pentadecimal_short">base15</string>
<string name="hexadecimal">Hexadezimal</string>
<string name="hexadecimal_short">base16</string>
<string name="number_base">Basis</string>
<string name="enable_vibrations">Vibrationen</string>
<string name="enable_vibrations_support">Haptisches Feedback für Tastaturtasten</string>
<string name="millibar">Millibar</string>
<string name="millibar_short">mbar</string>
<string name="kilopascal">Kilopascal</string>
<string name="kilopascal_short">kPa</string>
<string name="micron_of_mercury">Mikrometer Quecksilber</string>
<string name="micron_of_mercury_short">μmHg</string>
<!-- Tools -->
<!-- Fuzzy -->
<string name="epoch_converter">Epochkonverter</string>
<!-- Calculator -->
<string name="calculator">Taschenrechner</string>
<!-- Epoch -->
<string name="year_short">y</string>
<string name="month_short">m</string>
<string name="nautical_mile">Nautische Meile</string>
<string name="nautical_mile_short">M</string>
<string name="starting_screen_setting">Startbildschirm</string>
<string name="starting_screen_setting_support">Wählen Sie, welcher Bildschirm beim Starten der App angezeigt wird</string>
<!-- Tools -->
<string name="unit_converter">Einheitenumrechner</string>
<string name="calculator_clear_history_label">Leeren</string>
<string name="calculator_clear_history_title">Verlauf leeren</string>
<string name="calculator_clear_history_support">Alle Ausdrücke aus dem Verlauf werden für immer gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!</string>
<string name="calculator_no_history">Kein Verlauf</string>
<string name="open_menu_description">Menü öffnen</string>
<string name="microgram">Mikrogramm</string>
<string name="microgram_short">µg</string>
<!-- ELECTROSTATIC CAPACITANCE -->
<string name="attofarad">Attofarad</string>
<string name="attofarad_short">aF</string>
<string name="statfarad">Statfarad</string>
<string name="statfarad_short">stF</string>
<string name="farad">Farad</string>
<string name="farad_short">F</string>
<string name="exafarad">Exafarad</string>
<string name="exafarad_short">EF</string>
<string name="picofarad">Pikofarad</string>
<string name="picofarad_short">pF</string>
<string name="nanofarad">Nanofarad</string>
<string name="nanofarad_short">nF</string>
<string name="microfarad">Microfarad</string>
<string name="microfarad_short">µF</string>
<string name="millifarad">Millifarad</string>
<string name="millifarad_short">mF</string>
<string name="kilofarad">Kilofarad</string>
<string name="kilofarad_short">kF</string>
<string name="megafarad">Megafarad</string>
<string name="megafarad_short">MF</string>
<string name="gigafarad">Gigafarad</string>
<string name="gigafarad_short">GF</string>
<string name="petafarad">Petafarad</string>
<string name="petafarad_short">PF</string>
<!-- Prefixes -->
<string name="prefix_quetta">Quetta</string>
<string name="prefix_quetta_short">Q</string>
<string name="prefix_ronna">Ronna</string>
<string name="prefix_ronna_short">R</string>
<string name="prefix_yotta">Yotta</string>
<string name="prefix_yotta_short">Y</string>
<string name="prefix_zetta">Zetta</string>
<string name="prefix_zetta_short">Z</string>
<string name="prefix_exa">Exa</string>
<string name="prefix_exa_short">E</string>
<string name="prefix_peta">Peta</string>
<string name="prefix_peta_short">P</string>
<string name="prefix_tera">Tera</string>
<string name="prefix_tera_short">T</string>
<string name="prefix_giga">Giga</string>
<string name="prefix_giga_short">G</string>
<string name="prefix_mega">Mega</string>
<string name="prefix_mega_short">M</string>
<string name="prefix_kilo">Kilo</string>
<string name="prefix_kilo_short">k</string>
<string name="prefix_hecto">Hekto</string>
<string name="prefix_hecto_short">h</string>
<string name="prefix_deca">Deka</string>
<string name="prefix_deca_short">da</string>
<string name="prefix_base">Basis</string>
<string name="prefix_base_short">Basis</string>
<string name="prefix_deci">Dezi</string>
<string name="prefix_deci_short">d</string>
<string name="prefix_centi">Zenti</string>
<string name="prefix_centi_short">c</string>
<string name="prefix_milli">Milli</string>
<string name="prefix_milli_short">m</string>
<string name="prefix_micro">Micro</string>
<string name="prefix_micro_short">μ</string>
<string name="prefix_nano">Nano</string>
<string name="prefix_nano_short">n</string>
<string name="prefix_pico">Piko</string>
<string name="prefix_pico_short">P</string>
<string name="prefix_femto">Femto</string>
<string name="prefix_femto_short">f</string>
<string name="prefix_atto">Atto</string>
<string name="prefix_atto_short">a</string>
<string name="prefix_zepto">Zepto</string>
<string name="prefix_zepto_short">z</string>
<string name="prefix_yocto">Yokto</string>
<string name="prefix_yocto_short">y</string>
<string name="prefix_ronto">Ronto</string>
<string name="prefix_ronto_short">r</string>
<string name="prefix_quecto">Quekto</string>
<string name="prefix_quecto_short">q</string>
<!-- Force -->
<string name="newton">Newton</string>
<string name="newton_short">N</string>
<string name="kilonewton">Kilonewton</string>
<string name="kilonewton_short">kN</string>
<!-- Fuzzy -->
<string name="gram_force">Gramm-Kraft</string>
<string name="gram_force_short">gf</string>
<!-- Fuzzy -->
<string name="kilogram_force">Kilogramm-Kraft</string>
<string name="kilogram_force_short">kgf</string>
<string name="ton_force">Tonnen-Kraft</string>
<string name="ton_force_short">tf</string>
<string name="millinewton">Millinewton</string>
<string name="millinewton_short">mN</string>
<string name="attonewton">Attonewton</string>
<string name="attonewton_short">aN</string>
<string name="dyne">Dyn</string>
<string name="dyne_short">dyn</string>
<string name="joule_per_meter">Joule/Meter</string>
<string name="joule_per_meter_short">J/m</string>
<string name="joule_per_centimeter">Joule/Zentimeter</string>
<string name="joule_per_centimeter_short">J/cm</string>
<string name="kilopound_force">Kilopfund-Kraft</string>
<string name="kilopound_force_short">kipf</string>
<string name="pound_force">Pfund-Kraft</string>
<string name="pound_force_short">lbf</string>
<string name="ounce_force">Unzen-Kraft</string>
<string name="ounce_force_short">ozf</string>
<string name="pond">Pond</string>
<string name="pond_short">p</string>
<!-- Fuzzy -->
<string name="kilopond">Kilopond</string>
<string name="kilopond_short">kp</string>
<!-- Torque -->
<string name="newton_meter">Newtonmeter</string>
<string name="newton_meter_short">N*m</string>
<string name="newton_centimeter">Newtonzentimeter</string>
<string name="newton_centimeter_short">N*cm</string>
<string name="newton_millimeter">Newtonmillimeter</string>
<string name="newton_millimeter_short">N*mm</string>
<string name="kilonewton_meter">Kilonewtonmeter</string>
<string name="kilonewton_meter_short">kN*m</string>
<string name="dyne_meter">Dynmeter</string>
<string name="dyne_meter_short">dyn*m</string>
<!-- Fuzzy -->
<string name="dyne_centimeter">Dynzentimeter</string>
<string name="dyne_centimeter_short">dyn*cm</string>
<!-- Fuzzy -->
<string name="dyne_millimeter">Dynmillimeter</string>
<string name="dyne_millimeter_short">dyn*mm</string>
<!-- Fuzzy -->
<string name="kilogram_force_meter">Kilogramm-Kraft-Meter</string>
<string name="kilogram_force_meter_short">kgf*m</string>
<!-- Fuzzy -->
<string name="kilogram_force_centimeter">Kilogramm-Kraft-Zentimeter</string>
<string name="kilogram_force_centimeter_short">kgf*cm</string>
<!-- Fuzzy -->
<string name="kilogram_force_millimeter">Kilogramm-Kraft-Millimeter</string>
<string name="kilogram_force_millimeter_short">kgf*mm</string>
<!-- Fuzzy -->
<string name="gram_force_meter">Gramm-Kraft Meter</string>
<string name="gram_force_meter_short">gf*m</string>
<!-- Fuzzy -->
<string name="gram_force_centimeter">Gramm-Kraft Zentimeter</string>
<string name="gram_force_centimeter_short">gf*cm</string>
<!-- Fuzzy -->
<string name="gram_force_millimeter">Gramm-Kraft Millimeter</string>
<string name="gram_force_millimeter_short">gf*mm</string>
<!-- Fuzzy -->
<string name="ounce_force_foot">Unzen-Kraft-Fuß</string>
<string name="ounce_force_foot_short">ozf*ft</string>
<!-- Fuzzy -->
<string name="ounce_force_inch">Unze-Kraft-Zoll</string>
<string name="ounce_force_inch_short">ozf*in</string>
<string name="pound_force_foot">Pfund-Kraft-Fuß</string>
<string name="pound_force_foot_short">lbf*ft</string>
<!-- Fuzzy -->
<string name="pound_force_inch">Pfund-Kraft-Zoll</string>
<string name="pound_force_inch_short">lbf*in</string>
<!-- Flow rate -->
<string name="liter_per_hour">Liter/Stunde</string>
<string name="liter_per_hour_short">L/h</string>
<string name="liter_per_minute">Liter/Minute</string>
<string name="liter_per_minute_short">L/m</string>
<string name="liter_per_second">Liter/Sekunde</string>
<string name="liter_per_second_short">L/s</string>
<string name="milliliter_per_hour">Milliliter/Stunde</string>
<string name="milliliter_per_hour_short">mL/h</string>
<string name="milliliter_per_minute">Milliliter/Minute</string>
<string name="milliliter_per_minute_short">mL/m</string>
<string name="milliliter_per_second">Milliliter/Sekunde</string>
<string name="milliliter_per_second_short">mL/s</string>
<string name="cubic_meter_per_hour">Kubikmeter/Stunde</string>
<string name="cubic_meter_per_hour_short">m3/h</string>
<string name="cubic_meter_per_minute">Kubikmeter/Minute</string>
<string name="cubic_meter_per_minute_short">m3/m</string>
<string name="cubic_meter_per_second">Kubikmeter/Sekunde</string>
<string name="cubic_meter_per_second_short">m3/s</string>
<string name="cubic_millimeter_per_hour">Kubikmillimeter/Stunde</string>
<string name="cubic_millimeter_per_hour_short">mm3/h</string>
<string name="cubic_millimeter_per_minute">Kubikmillimeter/Minute</string>
<string name="cubic_millimeter_per_minute_short">mm3/m</string>
<string name="cubic_millimeter_per_second">Kubikmillimeter/Sekunde</string>
<string name="cubic_millimeter_per_second_short">mm3/s</string>
<string name="cubic_foot_per_hour">Kubikfuß/Stunde</string>
<string name="cubic_foot_per_hour_short">ft3/h</string>
<string name="cubic_foot_per_minute">Kubikfuß/Minute</string>
<string name="cubic_foot_per_minute_short">ft3/m</string>
<string name="cubic_foot_per_second">Kubikfuß/Sekunde</string>
<string name="cubic_foot_per_second_short">ft3/s</string>
<string name="gallon_per_hour_us">Gallone/Stunde (U.S.)</string>
<string name="gallon_per_hour_us_short">gal/h</string>
<string name="gallon_per_minute_us">Gallone/Minute(U.S.)</string>
<string name="gallon_per_minute_us_short">gal/m</string>
<string name="gallon_per_second_us">Gallone/Sekunde(U.S.)</string>
<string name="gallon_per_second_us_short">gal/s</string>
<string name="gallon_per_hour_imperial">Gallone/Stunde (Imperial)</string>
<string name="gallon_per_hour_imperial_short">gal/h</string>
<string name="gallon_per_minute_imperial">Gallone/Minute(Imperial)</string>
<string name="gallon_per_minute_imperial_short">gal/m</string>
<string name="gallon_per_second_imperial">Gallone/Sekunde(Imperial)</string>
<string name="gallon_per_second_imperial_short">gal/s</string>
<!-- Luminance -->
<string name="candela_per_square_meter">Candela/Quadratmeter</string>
<string name="candela_per_square_meter_short">cd/m^2</string>
<string name="candela_per_square_centimeter">Candela/Quadratzentimeter</string>
<string name="candela_per_square_centimeter_short">cd/cm^2</string>
<string name="candela_per_square_foot">Candela/Quadratfuß</string>
<string name="candela_per_square_foot_short">cd/ft^2</string>
<string name="candela_per_square_inch">Candela/Quadratzoll</string>
<string name="candela_per_square_inch_short">cd/in^2</string>
<string name="kilocandela_per_square_meter">Kilocandela/Quadratmeter</string>
<string name="kilocandela_per_square_meter_short">kcd</string>
<string name="stilb">Stilb</string>
<string name="stilb_short">sb</string>
<string name="lumen_per_square_meter_per_steradian">Lumen/Quadratmeter/Steradiant</string>
<string name="lumen_per_square_meter_per_steradian_short">lm/m^2/sr</string>
<string name="lumen_per_square_centimeter_per_steradian">Lumen/Quadratzentimeter/Steradian</string>
<string name="lumen_per_square_centimeter_per_steradian_short">lm/cm^2/sr</string>
<string name="lumen_per_square_foot_per_steradian">Lumen/Quadratfuß/Steradian</string>
<string name="lumen_per_square_foot_per_steradian_short">lm/ft^2/sr</string>
<string name="watt_per_square_centimeter_per_steradian">Watt/Quadratzentimeter/Steradian</string>
<string name="watt_per_square_centimeter_per_steradian_short">W/cm^2/sr</string>
<string name="nit">Nit</string>
<string name="nit_short">nt</string>
<string name="millinit">Millinit</string>
<string name="millinit_short">mnt</string>
<string name="lambert">Lambert</string>
<string name="lambert_short">L</string>
<string name="millilambert">Millilambert</string>
<string name="millilambert_short">mL</string>
<string name="foot_lambert">Fuß-Lambert</string>
<string name="foot_lambert_short">fL</string>
<string name="apostilb">Apostilb</string>
<string name="apostilb_short">asb</string>
<string name="blondel">Blondel</string>
<string name="blondel_short">blondel</string>
<string name="bril">Bril</string>
<string name="bril_short">bril</string>
<string name="skot">Skot</string>
<string name="skot_short">sk</string>
<string name="electrostatic_capacitance">Kapazität</string>
<string name="prefix">Präfix</string>
<string name="force">Kraft</string>
<string name="torque">Drehmoment</string>
<!-- Fuzzy -->
<string name="flow_rate">Fluss</string>
<string name="luminance">Leuchtdichte</string>
<!-- Fuzzy -->
<string name="format_time">Zeit formatieren</string>
<string name="format_time_support">Beispiel: 130 Minuten als 2h 10m anzeigen</string>
<string name="units_sorting">Einheitenlistensortierung</string>
<string name="units_sorting_support">Einheitenreihenfolge ändern</string>
<!-- Units list sorting -->
<string name="sort_by_usage">Benutzung</string>
<string name="sort_by_alphabetical">Alphabetisch</string>
<!-- Fuzzy -->
<string name="sort_by_scale_desc">Skala (Abst.)</string>
<!-- Fuzzy -->
<string name="sort_by_scale_asc">Skala (Aufst.)</string>
<string name="color_theme_support">Farbthema auswählen</string>
<string name="color_scheme">Farbschema</string>
<string name="selected_color">Ausgewählte Farbe</string>
<string name="monet_mode">Stil auswählen</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/Gj9ZCNPXIfKddDQgccvboVFz.jpg
https://s3.eu-west-1.amazonaws.com/po-pub/i/prtM85P6x1fMuLg1I0zbkceo.png
Maybe this can be labeled better? Let me know. It should be something that can describe content of the Formatting screen. -->
<string name="formatting_setting_support">Genauigkeit und Zahlendarstellung</string>
<string name="divide_by_zero_error">Kann nicht durch 0 teilen</string>
<string name="date_difference">Datumsunterschied</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/uWOHJmIq9riqsq7PO82ZQp3a.png -->
<string name="select_time">Zeit auswählen</string>
<string name="date_difference_start">Anfang</string>
<string name="date_difference_end">Ende</string>
<string name="date_difference_result">Unterschied</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_years">Jahre</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_months">Monate</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_days">Tage</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_hours">Stunden</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_minutes">Minuten</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/9lSfdkfKShwyQFEF4nvbVaIb.jpg
Used in this dialog window. Should be short -->
<string name="next_label">Weiter</string>
</resources>

View File

@ -228,11 +228,11 @@
<string name="exabyte_per_second_short">EB/s</string>
<!-- Volume -->
<string name="attoliter">Attoliter</string>
<string name="attoliter">Attolitre</string>
<string name="attoliter_short">aL</string>
<string name="milliliter">Milliliter</string>
<string name="milliliter">Millilitre</string>
<string name="milliliter_short">mL</string>
<string name="liter">Liter</string>
<string name="liter">Litre</string>
<string name="liter_short">L</string>
<string name="us_liquid_gallon">US liquid gallon</string>
<string name="us_liquid_gallon_short">gal (US)</string>
@ -659,10 +659,8 @@
<string name="theme_setting">Themes</string>
<string name="precision_setting">Precision</string>
<string name="separator_setting">Separator</string>
<string name="output_format_setting">Output format</string>
<string name="exponential_notation_setting">Exponential notation</string>
<string name="unit_groups_setting">Unit groups</string>
<string name="enable_vibrations">Vibrations</string>
<string name="enable_vibrations_support">Haptic feedback when clicking keyboard buttons</string>
<string name="currency_rates_note_setting">Wrong currency rates?</string>
<string name="currency_rates_note_title">Note</string>
<string name="currency_rates_note_text">Currency rates are updated daily. There\'s no real-time market monitoring in the app</string>
@ -670,30 +668,26 @@
<string name="privacy_policy">Privacy Policy</string>
<string name="third_party_licenses">Third party licenses</string>
<string name="rate_this_app">Rate this app</string>
<string name="formatting_settings_group">Formatting</string>
<string name="formatting_setting">Formatting</string>
<string name="additional_settings_group">Additional</string>
<!-- Precision -->
<string name="precision_setting_support">Number of decimal places</string>
<string name="precision_setting_info">Converted values may have a precision higher than the preferred one.</string>
<string name="max_precision">1 000 (Max)</string>
<string name="max_precision">%1$s (Max)</string>
<!-- Separator -->
<string name="separator_setting_support">Group separator symbol</string>
<string name="period">Period (42.069,12)</string>
<string name="comma">Comma (42,069.12)</string>
<string name="spaces">Spaces (42 069.12)</string>
<string name="period">Period</string>
<string name="comma">Comma</string>
<string name="space">Space</string>
<!-- Output format -->
<string name="output_format_setting_support">Result value formatting</string>
<string name="output_format_setting_info">Engineering strings look like 1E-21</string>
<string name="plain">Default</string>
<string name="allow_engineering">Allow engineering</string>
<string name="force_engineering">Force engineering</string>
<string name="exponential_notation_setting_support">Replace part of the number with E</string>
<!-- Theme -->
<string name="theme_setting_support">App look and feel</string>
<string name="force_auto_mode">Auto</string>
<string name="auto_label">Auto</string>
<string name="force_light_mode">Light</string>
<string name="force_dark_mode">Dark</string>
<string name="color_theme">Colour theme</string>
@ -787,4 +781,321 @@
<string name="hexadecimal">Hexadecimal</string>
<string name="hexadecimal_short">base16</string>
<string name="number_base">Base</string>
<string name="enable_vibrations">Vibrations</string>
<string name="enable_vibrations_support">Haptic feedback when clicking keyboard buttons</string>
<string name="millibar">Millibar</string>
<string name="millibar_short">mbar</string>
<string name="kilopascal">Kilopascal</string>
<string name="kilopascal_short">kPa</string>
<string name="micron_of_mercury">Micron of mercury</string>
<string name="micron_of_mercury_short">μmHg</string>
<!-- Tools -->
<string name="epoch_converter">Epoch converter</string>
<!-- Calculator -->
<string name="calculator">Calculator</string>
<!-- Epoch -->
<string name="year_short">y</string>
<string name="month_short">m</string>
<string name="nautical_mile">Nautical mile</string>
<string name="nautical_mile_short">M</string>
<string name="starting_screen_setting">Starting screen</string>
<string name="starting_screen_setting_support">Choose which screen is shown when you launch the app</string>
<!-- Tools -->
<string name="unit_converter">Unit converter</string>
<string name="calculator_clear_history_label">Clear</string>
<string name="calculator_clear_history_title">Clear history</string>
<string name="calculator_clear_history_support">All expressions from history will be deleted forever. This action can\'t be undone!</string>
<string name="calculator_no_history">No history</string>
<string name="open_menu_description">Open menu</string>
<string name="microgram">Microgram</string>
<string name="microgram_short">µg</string>
<!-- ELECTROSTATIC CAPACITANCE -->
<string name="attofarad">Attofarad</string>
<string name="attofarad_short">aF</string>
<string name="statfarad">Statfarad</string>
<string name="statfarad_short">stF</string>
<string name="farad">Farad</string>
<string name="farad_short">F</string>
<string name="exafarad">Exafarad</string>
<string name="exafarad_short">EF</string>
<string name="picofarad">Picofarad</string>
<string name="picofarad_short">pF</string>
<string name="nanofarad">Nanofarad</string>
<string name="nanofarad_short">nF</string>
<string name="microfarad">Microfarad</string>
<string name="microfarad_short">µF</string>
<string name="millifarad">Millifarad</string>
<string name="millifarad_short">mF</string>
<string name="kilofarad">Kilofarad</string>
<string name="kilofarad_short">kF</string>
<string name="megafarad">Megafarad</string>
<string name="megafarad_short">MF</string>
<string name="gigafarad">Gigafarad</string>
<string name="gigafarad_short">GF</string>
<string name="petafarad">Petafarad</string>
<string name="petafarad_short">PF</string>
<!-- Prefixes -->
<string name="prefix_quetta">Quetta</string>
<string name="prefix_quetta_short">Q</string>
<string name="prefix_ronna">Ronna</string>
<string name="prefix_ronna_short">R</string>
<string name="prefix_yotta">Yotta</string>
<string name="prefix_yotta_short">Y</string>
<string name="prefix_zetta">Zetta</string>
<string name="prefix_zetta_short">Z</string>
<string name="prefix_exa">Exa</string>
<string name="prefix_exa_short">E</string>
<string name="prefix_peta">Peta</string>
<string name="prefix_peta_short">P</string>
<string name="prefix_tera">Tera</string>
<string name="prefix_tera_short">T</string>
<string name="prefix_giga">Giga</string>
<string name="prefix_giga_short">G</string>
<string name="prefix_mega">Mega</string>
<string name="prefix_mega_short">M</string>
<string name="prefix_kilo">Kilo</string>
<string name="prefix_kilo_short">k</string>
<string name="prefix_hecto">Hecto</string>
<string name="prefix_hecto_short">h</string>
<string name="prefix_deca">Deca</string>
<string name="prefix_deca_short">da</string>
<string name="prefix_base">Base</string>
<string name="prefix_base_short">Base</string>
<string name="prefix_deci">Deci</string>
<string name="prefix_deci_short">d</string>
<string name="prefix_centi">Centi</string>
<string name="prefix_centi_short">c</string>
<string name="prefix_milli">Milli</string>
<string name="prefix_milli_short">m</string>
<string name="prefix_micro">Micro</string>
<string name="prefix_micro_short">μ</string>
<string name="prefix_nano">Nano</string>
<string name="prefix_nano_short">n</string>
<string name="prefix_pico">Pico</string>
<string name="prefix_pico_short">p</string>
<string name="prefix_femto">Femto</string>
<string name="prefix_femto_short">f</string>
<string name="prefix_atto">Atto</string>
<string name="prefix_atto_short">a</string>
<string name="prefix_zepto">Zepto</string>
<string name="prefix_zepto_short">z</string>
<string name="prefix_yocto">Yocto</string>
<string name="prefix_yocto_short">y</string>
<string name="prefix_ronto">Ronto</string>
<string name="prefix_ronto_short">r</string>
<string name="prefix_quecto">Quecto</string>
<string name="prefix_quecto_short">q</string>
<!-- Force -->
<string name="newton">Newton</string>
<string name="newton_short">N</string>
<string name="kilonewton">Kilonewton</string>
<string name="kilonewton_short">kN</string>
<string name="gram_force">Gram-force</string>
<string name="gram_force_short">gf</string>
<string name="kilogram_force">Kilogram-force</string>
<string name="kilogram_force_short">kgf</string>
<string name="ton_force">Ton-force</string>
<string name="ton_force_short">tf</string>
<string name="millinewton">Millinewton</string>
<string name="millinewton_short">mN</string>
<string name="attonewton">Attonewton</string>
<string name="attonewton_short">aN</string>
<string name="dyne">Dyne</string>
<string name="dyne_short">dyn</string>
<string name="joule_per_meter">Joule/metre</string>
<string name="joule_per_meter_short">J/m</string>
<string name="joule_per_centimeter">Joule/centimetre</string>
<string name="joule_per_centimeter_short">J/cm</string>
<string name="kilopound_force">Kilopound-force</string>
<string name="kilopound_force_short">kipf</string>
<string name="pound_force">Pound-force</string>
<string name="pound_force_short">lbf</string>
<string name="ounce_force">Ounce-force</string>
<string name="ounce_force_short">ozf</string>
<string name="pond">Pond</string>
<string name="pond_short">p</string>
<string name="kilopond">Kilopond</string>
<string name="kilopond_short">kp</string>
<!-- Torque -->
<string name="newton_meter">Newton metre</string>
<string name="newton_meter_short">N*m</string>
<string name="newton_centimeter">Newton centimetre</string>
<string name="newton_centimeter_short">N*cm</string>
<string name="newton_millimeter">Newton millimetre</string>
<string name="newton_millimeter_short">N*mm</string>
<string name="kilonewton_meter">Kilonewton metre</string>
<string name="kilonewton_meter_short">kN*m</string>
<string name="dyne_meter">Dyne metre</string>
<string name="dyne_meter_short">dyn*m</string>
<string name="dyne_centimeter">Dyne centimetre</string>
<string name="dyne_centimeter_short">dyn*cm</string>
<string name="dyne_millimeter">Dyne millimetre</string>
<string name="dyne_millimeter_short">dyn*mm</string>
<string name="kilogram_force_meter">Kilogram-force metre</string>
<string name="kilogram_force_meter_short">kgf*m</string>
<string name="kilogram_force_centimeter">Kilogram-force centimetre</string>
<string name="kilogram_force_centimeter_short">kgf*cm</string>
<string name="kilogram_force_millimeter">Kilogram-force millimetre</string>
<string name="kilogram_force_millimeter_short">kgf*mm</string>
<string name="gram_force_meter">Gram-force metre</string>
<string name="gram_force_meter_short">gf*m</string>
<string name="gram_force_centimeter">Gram-force centimetre</string>
<string name="gram_force_centimeter_short">gf*cm</string>
<string name="gram_force_millimeter">Gram-force millimetre</string>
<string name="gram_force_millimeter_short">gf*mm</string>
<string name="ounce_force_foot">Ounce-force foot</string>
<string name="ounce_force_foot_short">ozf*ft</string>
<string name="ounce_force_inch">Ounce-force inch</string>
<string name="ounce_force_inch_short">ozf*in</string>
<string name="pound_force_foot">Pound-force foot</string>
<string name="pound_force_foot_short">lbf*ft</string>
<string name="pound_force_inch">Pound-force inch</string>
<string name="pound_force_inch_short">lbf*in</string>
<!-- Flow rate -->
<string name="liter_per_hour">Litre/hour</string>
<string name="liter_per_hour_short">L/h</string>
<string name="liter_per_minute">Litre/minute</string>
<string name="liter_per_minute_short">L/m</string>
<string name="liter_per_second">Litre/second</string>
<string name="liter_per_second_short">L/s</string>
<string name="milliliter_per_hour">Millilitre/hour</string>
<string name="milliliter_per_hour_short">mL/h</string>
<string name="milliliter_per_minute">Millilitre/minute</string>
<string name="milliliter_per_minute_short">mL/m</string>
<string name="milliliter_per_second">Millilitre/second</string>
<string name="milliliter_per_second_short">mL/s</string>
<string name="cubic_meter_per_hour">Cubic Metre/hour</string>
<string name="cubic_meter_per_hour_short">m3/h</string>
<string name="cubic_meter_per_minute">Cubic Metre/minute</string>
<string name="cubic_meter_per_minute_short">m3/m</string>
<string name="cubic_meter_per_second">Cubic Metre/second</string>
<string name="cubic_meter_per_second_short">m3/s</string>
<string name="cubic_millimeter_per_hour">Cubic Millimetre/hour</string>
<string name="cubic_millimeter_per_hour_short">mm3/h</string>
<string name="cubic_millimeter_per_minute">Cubic Millimetre/minute</string>
<string name="cubic_millimeter_per_minute_short">mm3/m</string>
<string name="cubic_millimeter_per_second">Cubic Millimetre/second</string>
<string name="cubic_millimeter_per_second_short">mm3/s</string>
<string name="cubic_foot_per_hour">Cubic Foot/hour</string>
<string name="cubic_foot_per_hour_short">ft3/h</string>
<string name="cubic_foot_per_minute">Cubic Foot/minute</string>
<string name="cubic_foot_per_minute_short">ft3/m</string>
<string name="cubic_foot_per_second">Cubic Foot/second</string>
<string name="cubic_foot_per_second_short">ft3/s</string>
<string name="gallon_per_hour_us">Gallon/hour (U.S.)</string>
<string name="gallon_per_hour_us_short">gal/h</string>
<string name="gallon_per_minute_us">Gallon/minute (U.S.)</string>
<string name="gallon_per_minute_us_short">gal/m</string>
<string name="gallon_per_second_us">Gallon/second (U.S.)</string>
<string name="gallon_per_second_us_short">gal/s</string>
<string name="gallon_per_hour_imperial">Gallon/hour (Imperial)</string>
<string name="gallon_per_hour_imperial_short">gal/h</string>
<string name="gallon_per_minute_imperial">Gallon/minute (Imperial)</string>
<string name="gallon_per_minute_imperial_short">gal/m</string>
<string name="gallon_per_second_imperial">Gallon/second (Imperial)</string>
<string name="gallon_per_second_imperial_short">gal/s</string>
<!-- Luminance -->
<string name="candela_per_square_meter">Candela/square metre</string>
<string name="candela_per_square_meter_short">cd/m^2</string>
<string name="candela_per_square_centimeter">Candela/square centimetre</string>
<string name="candela_per_square_centimeter_short">cd/cm^2</string>
<string name="candela_per_square_foot">Candela/square foot</string>
<string name="candela_per_square_foot_short">cd/ft^2</string>
<string name="candela_per_square_inch">Candela/square inch</string>
<string name="candela_per_square_inch_short">cd/in^2</string>
<string name="kilocandela_per_square_meter">Kilocandela/square metre</string>
<string name="kilocandela_per_square_meter_short">kcd</string>
<string name="stilb">Stilb</string>
<string name="stilb_short">sb</string>
<string name="lumen_per_square_meter_per_steradian">Lumen/square metre/steradian</string>
<string name="lumen_per_square_meter_per_steradian_short">lm/m^2/sr</string>
<string name="lumen_per_square_centimeter_per_steradian">Lumen/square centimetre/steradian</string>
<string name="lumen_per_square_centimeter_per_steradian_short">lm/cm^2/sr</string>
<string name="lumen_per_square_foot_per_steradian">Lumen/square foot/steradian</string>
<string name="lumen_per_square_foot_per_steradian_short">lm/ft^2/sr</string>
<string name="watt_per_square_centimeter_per_steradian">Watt/square centimetre/steradian</string>
<string name="watt_per_square_centimeter_per_steradian_short">W/cm^2/sr</string>
<string name="nit">Nit</string>
<string name="nit_short">nt</string>
<string name="millinit">Millinit</string>
<string name="millinit_short">mnt</string>
<string name="lambert">Lambert</string>
<string name="lambert_short">L</string>
<string name="millilambert">Millilambert</string>
<string name="millilambert_short">mL</string>
<string name="foot_lambert">Foot-lambert</string>
<string name="foot_lambert_short">fL</string>
<string name="apostilb">Apostilb</string>
<string name="apostilb_short">asb</string>
<string name="blondel">Blondel</string>
<string name="blondel_short">blondel</string>
<string name="bril">Bril</string>
<string name="bril_short">bril</string>
<string name="skot">Skot</string>
<string name="skot_short">sk</string>
<string name="electrostatic_capacitance">Capacitance</string>
<string name="prefix">Prefix</string>
<string name="force">Force</string>
<string name="torque">Torque</string>
<string name="flow_rate">Flow</string>
<string name="luminance">Luminance</string>
<string name="format_time">Format time</string>
<string name="format_time_support">Example: Show 130 minutes as 2h 10m</string>
<string name="units_sorting">Units list sorting</string>
<string name="units_sorting_support">Change units order</string>
<!-- Units list sorting -->
<string name="sort_by_usage">Usage</string>
<string name="sort_by_alphabetical">Alphabetical</string>
<string name="sort_by_scale_desc">Scale (Desc.)</string>
<string name="sort_by_scale_asc">Scale (Asc.)</string>
<string name="color_theme_support">Pick a theming mode</string>
<string name="color_scheme">Colour scheme</string>
<string name="selected_color">Selected colour</string>
<string name="monet_mode">Selected style</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/Gj9ZCNPXIfKddDQgccvboVFz.jpg
https://s3.eu-west-1.amazonaws.com/po-pub/i/prtM85P6x1fMuLg1I0zbkceo.png
Maybe this can be labeled better? Let me know. It should be something that can describe content of the Formatting screen. -->
<string name="formatting_setting_support">Precision and numbers appearance</string>
<string name="divide_by_zero_error">Can\'t divide by 0</string>
<string name="date_difference">Date difference</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/uWOHJmIq9riqsq7PO82ZQp3a.png -->
<string name="select_time">Select time</string>
<string name="date_difference_start">Start</string>
<string name="date_difference_end">End</string>
<string name="date_difference_result">Difference</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_years">Years</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_months">Months</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_days">Days</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_hours">Hours</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_minutes">Minutes</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/9lSfdkfKShwyQFEF4nvbVaIb.jpg
Used in this dialog window. Should be short -->
<string name="next_label">Next</string>
<string name="formatting_setting_preview_box_label">Preview (click to switch)</string>
</resources>

View File

@ -659,7 +659,9 @@
<string name="theme_setting">Thèmes</string>
<string name="precision_setting">Précision</string>
<string name="separator_setting">Séparateur</string>
<string name="output_format_setting">Format de sortie</string>
<!-- Fuzzy -->
<string name="exponential_notation_setting">Notation exponentielle</string>
<string name="unit_groups_setting">Groupes d\'unités</string>
<string name="currency_rates_note_title">Note</string>
<string name="currency_rates_note_text">Les taux de change sont mis à jour quotidiennement. L\'application ne permet pas de suivre le marché en temps réel.</string>
@ -667,21 +669,22 @@
<string name="privacy_policy">Politique de confidentialité</string>
<string name="third_party_licenses">Licences tierces</string>
<string name="rate_this_app">Évaluer l\'application</string>
<string name="formatting_settings_group">Formattage</string>
<string name="formatting_setting">Formattage</string>
<string name="additional_settings_group">Additional</string>
<!-- Precision -->
<string name="precision_setting_support">Nombre de décimales</string>
<string name="precision_setting_info">Les valeurs converties peuvent avoir une précision supérieure à la précision préférée.</string>
<string name="max_precision">1 000 (Max)</string>
<string name="max_precision">%1$s (Max)</string>
<!-- Separator -->
<string name="separator_setting_support">Symbole de séparation de groupe</string>
<string name="period">Période (42.069,12)</string>
<string name="comma">Virgule (42,069.12)</string>
<string name="spaces">Espaces (42 069.12)</string>
<string name="plain">Défaut</string>
<string name="force_auto_mode">Auto</string>
<string name="period">Période</string>
<string name="comma">Virgule</string>
<!-- Fuzzy -->
<string name="space">Espaces</string>
<string name="auto_label">Auto</string>
<string name="force_light_mode">Clair</string>
<string name="force_dark_mode">Sombre</string>
<string name="force_amoled_mode">AMOLED Noir</string>

File diff suppressed because it is too large Load Diff

View File

@ -659,7 +659,7 @@
<string name="theme_setting">Темы</string>
<string name="precision_setting">Точность</string>
<string name="separator_setting">Разделитель</string>
<string name="output_format_setting">Формат вывода</string>
<string name="exponential_notation_setting">Экспоненциальная нотация</string>
<string name="unit_groups_setting">Группы величин</string>
<string name="currency_rates_note_setting">Неправильные курсы валют?</string>
<string name="currency_rates_note_title">Внимание</string>
@ -668,35 +668,31 @@
<string name="privacy_policy">Политика конфиденциальности</string>
<string name="third_party_licenses">Лицензии третьих лиц</string>
<string name="rate_this_app">Оценить приложение</string>
<string name="formatting_settings_group">Форматирование</string>
<string name="formatting_setting">Форматирование</string>
<string name="additional_settings_group">Дополнительное</string>
<!-- Precision -->
<string name="precision_setting_support">Количество десятичных знаков</string>
<string name="precision_setting_info">Переводимые значения могут иметь точность выше предпочтительной.</string>
<string name="max_precision">1 000 (Максимум)</string>
<string name="max_precision">%1$s (Максимум)</string>
<!-- Separator -->
<string name="separator_setting_support">Символ разделителя</string>
<string name="period">Точка (42.069,12)</string>
<string name="comma">Запятая (42,069.12)</string>
<string name="spaces">Пробел (42 069.12)</string>
<string name="period">Точка</string>
<string name="comma">Запятая</string>
<string name="space">Пробел</string>
<!-- Output format -->
<string name="output_format_setting_support">Формат результата перевода</string>
<string name="output_format_setting_info">Инженерный формат выглядит как 1E-21</string>
<string name="plain">По умолчанию</string>
<string name="allow_engineering">Разрешить инженерный</string>
<string name="force_engineering">Преимущественно инженерный</string>
<string name="exponential_notation_setting_support">Замените часть числа на E</string>
<!-- Theme -->
<string name="theme_setting_support">Внешний вид приложения</string>
<string name="force_auto_mode">Автоматическая</string>
<string name="auto_label">Авто</string>
<string name="force_light_mode">Светлая</string>
<string name="force_dark_mode">Темная</string>
<string name="force_dark_mode">Тёмная</string>
<string name="color_theme">Цветовая тема</string>
<string name="force_amoled_mode">Темная AMOLED</string>
<string name="force_amoled_mode_support">Использовать черный фон в темных темах</string>
<string name="force_amoled_mode">Чёрная AMOLED</string>
<string name="force_amoled_mode_support">Использовать чёрный фон в тёмной теме</string>
<string name="enable_dynamic_colors">Динамичные цвета</string>
<string name="enable_dynamic_colors_support">Использовать цвета обоев</string>
@ -1048,7 +1044,7 @@
<string name="skot">Скот</string>
<string name="skot_short">ск</string>
<string name="electrostatic_capacitance">Емкость</string>
<string name="prefix">Префиск</string>
<string name="prefix">Префикс</string>
<string name="force">Сила</string>
<string name="torque">Момент</string>
<string name="flow_rate">Течение</string>
@ -1067,4 +1063,39 @@
<string name="color_scheme">Цветовая схема</string>
<string name="selected_color">Выбранный цвет</string>
<string name="monet_mode">Выбранный стиль</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/Gj9ZCNPXIfKddDQgccvboVFz.jpg
https://s3.eu-west-1.amazonaws.com/po-pub/i/prtM85P6x1fMuLg1I0zbkceo.png
Maybe this can be labeled better? Let me know. It should be something that can describe content of the Formatting screen. -->
<string name="formatting_setting_support">Точность и представление чисел</string>
<string name="divide_by_zero_error">Нельзя делить на 0</string>
<string name="date_difference">Разница между датами</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/uWOHJmIq9riqsq7PO82ZQp3a.png -->
<string name="select_time">Выберите время</string>
<string name="date_difference_start">Начало</string>
<string name="date_difference_end">Конец</string>
<string name="date_difference_result">Разница</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_years">Лет</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_months">Месяцев</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_days">Дней</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_hours">Часов</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
<string name="date_difference_minutes">Минут</string>
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/9lSfdkfKShwyQFEF4nvbVaIb.jpg
Used in this dialog window. Should be short -->
<string name="next_label">Далее</string>
<string name="formatting_setting_preview_box_label">Предпросмотр (нажмите для переключения)</string>
</resources>

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("UnstableApiUsage")
plugins {
id("unitto.library")
id("unitto.library.compose")
@ -35,7 +37,6 @@ android {
dependencies {
testImplementation(libs.junit)
testImplementation(libs.org.robolectric)
testImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.test.manifest)

View File

@ -1,251 +0,0 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022-2023 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.core.ui
import android.content.Context
import com.sadellie.unitto.core.base.Separator
import com.sadellie.unitto.core.base.Token
import java.math.BigDecimal
import java.math.RoundingMode
// Legacy, LOL. Will change later
object Formatter : UnittoFormatter()
open class UnittoFormatter {
/**
* This regex will catch things like "123.456", "123", ".456"
*/
private val numbersRegex = Regex("[\\d.]+")
private val SPACE = " "
private val PERIOD = "."
private val COMMA = ","
/**
* Grouping separator.
*/
var grouping: String = SPACE
/**
* Fractional part separator.
*/
var fractional = Token.comma
private val timeDivisions by lazy {
mapOf(
R.string.day_short to BigDecimal("86400000000000000000000"),
R.string.hour_short to BigDecimal("3600000000000000000000"),
R.string.minute_short to BigDecimal("60000000000000000000"),
R.string.second_short to BigDecimal("1000000000000000000"),
R.string.millisecond_short to BigDecimal("1000000000000000"),
R.string.microsecond_short to BigDecimal("1000000000000"),
R.string.nanosecond_short to BigDecimal("1000000000"),
R.string.attosecond_short to BigDecimal("1"),
)
}
/**
* 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) Token.comma else Token.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(Token.E)) return input.replace(Token.dot, fractional)
var output = input
val allNumbers: List<String> = input.getOnlyNumbers()
allNumbers.forEach {
output = output.replace(it, formatNumber(it))
}
Token.internalToDisplay.forEach {
output = output.replace(it.key, it.value)
}
return output
}
/**
* Reapply formatting. Reverses [format] and applies [format] again.
*/
fun reFormat(input: String): String {
// We get 123.45,6789
// We need 12.345,6789
// 123.45,6789
// Remove grouping
// 12345,6789
// Replace fractional with "." because formatter accepts only numbers where fractional is a dot
return format(
input
.replace(grouping, "")
.replace(fractional, Token.dot)
)
}
/**
* Helper method to change formatting from [input] with a specified [separator] to the one that
* is set for this [UnittoFormatter].
*/
fun fromSeparator(input: String, separator: Int): String {
val sGrouping = when (separator) {
Separator.PERIOD -> PERIOD
Separator.COMMA -> COMMA
else -> SPACE
}
.also { if (it == grouping) return input }
val sFractional = if (separator == Separator.PERIOD) Token.comma else Token.dot
return input
.replace(sGrouping, "\t")
.replace(sFractional, fractional)
.replace("\t", grouping)
}
fun toSeparator(input: String, separator: Int): String {
val output = filterUnknownSymbols(input).replace(fractional, Token.dot)
val sGrouping = when (separator) {
Separator.PERIOD -> PERIOD
Separator.COMMA -> COMMA
else -> SPACE
}
val sFractional = if (separator == Separator.PERIOD) Token.comma else Token.dot
return format(output)
.replace(grouping, "\t")
.replace(fractional, sFractional)
.replace("\t", sGrouping)
}
fun removeGrouping(input: String): String = input.replace(grouping, "")
/**
* Takes [input] and [basicUnit] of the unit to format it to be more human readable.
*
* @return String like "1d 12h 12s".
*/
fun formatTime(context: Context, input: String, basicUnit: BigDecimal?): String {
if (basicUnit == null) return Token._0
try {
// Don't need magic if the input is zero
if (BigDecimal(input).compareTo(BigDecimal.ZERO) == 0) return Token._0
} catch (e: NumberFormatException) {
// For case such as "10-" and "("
return Token._0
}
// Attoseconds don't need "magic"
if (basicUnit.compareTo(BigDecimal.ONE) == 0) return formatNumber(input)
var result = if (input.startsWith(Token.minus)) Token.minus else ""
var remainingSeconds = BigDecimal(input)
.abs()
.multiply(basicUnit)
.setScale(0, RoundingMode.HALF_EVEN)
if (remainingSeconds.compareTo(BigDecimal.ZERO) == 0) return Token._0
timeDivisions.forEach { (timeStr, divider) ->
val division = remainingSeconds.divideAndRemainder(divider)
val time = division.component1()
remainingSeconds = division.component2()
if (time.compareTo(BigDecimal.ZERO) == 1) {
result += "${formatNumber(time.toPlainString())}${context.getString(timeStr)} "
}
}
return result.trimEnd()
}
/**
* Format given [input].
*
* Input must be a number with dot!!!. Will replace grouping separators and fractional part (dot)
* separators.
*
* @see grouping
* @see fractional
*/
private fun formatNumber(input: String): String {
if (input.any { it.isLetter() }) return input
var firstPart = input.takeWhile { it != '.' }
val remainingPart = input.removePrefix(firstPart)
// Number of empty symbols (spaces) we need to add to correctly split into chunks.
val offset = 3 - firstPart.length.mod(3)
val output = if (offset != 3) {
// We add some spaces at the beginning so that last chunk has 3 symbols
firstPart = " ".repeat(offset) + firstPart
firstPart.chunked(3).joinToString(grouping).drop(offset)
} else {
firstPart.chunked(3).joinToString(grouping)
}
return (output + remainingPart.replace(".", fractional))
}
/**
* @receiver Must be a string with a dot (".") used as a fractional.
*/
private fun String.getOnlyNumbers(): List<String> =
numbersRegex.findAll(this).map(MatchResult::value).toList()
fun filterUnknownSymbols(input: String): String {
var clearStr = input.replace(" ", "")
var garbage = clearStr
// String with unknown symbols
Token.knownSymbols.plus(fractional).forEach {
garbage = garbage.replace(it, " ")
}
// Remove unknown symbols from input
garbage.split(" ").forEach {
clearStr = clearStr.replace(it, "")
}
clearStr = clearStr
.replace(Token.divide, Token.divideDisplay)
.replace(Token.multiply, Token.multiplyDisplay)
.replace(Token.minus, Token.minusDisplay)
return clearStr
}
}

View File

@ -0,0 +1,284 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common
import android.text.format.DateFormat
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerLayoutType
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.sadellie.unitto.core.base.R
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
import kotlin.math.max
@Composable
fun TimePickerDialog(
modifier: Modifier = Modifier,
localDateTime: LocalDateTime,
confirmLabel: String = stringResource(R.string.ok_label),
dismissLabel: String = stringResource(R.string.cancel_label),
onDismiss: () -> Unit = {},
onConfirm: (LocalDateTime) -> Unit,
vertical: Boolean
) {
val pickerState = rememberTimePickerState(
localDateTime.hour,
localDateTime.minute,
DateFormat.is24HourFormat(LocalContext.current)
)
AlertDialog(
onDismissRequest = onDismiss,
modifier = modifier.wrapContentHeight(),
properties = DialogProperties(usePlatformDefaultWidth = vertical)
) {
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.extraLarge,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 6.dp,
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = stringResource(R.string.select_time),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.align(Alignment.Start)
)
TimePicker(
state = pickerState,
modifier = Modifier.padding(top = 20.dp),
layoutType = if (vertical) TimePickerLayoutType.Vertical else TimePickerLayoutType.Horizontal
)
Row(
modifier = Modifier.align(Alignment.End),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(
onClick = onDismiss
) {
Text(text = dismissLabel)
}
TextButton(
onClick = {
onConfirm(
localDateTime
.withHour(pickerState.hour)
.withMinute(pickerState.minute)
)
}
) {
Text(text = confirmLabel)
}
}
}
}
}
}
@Composable
fun DatePickerDialog(
modifier: Modifier = Modifier,
localDateTime: LocalDateTime,
confirmLabel: String = stringResource(R.string.ok_label),
dismissLabel: String = stringResource(R.string.cancel_label),
onDismiss: () -> Unit = {},
onConfirm: (LocalDateTime) -> Unit,
) {
val pickerState = rememberDatePickerState(localDateTime.toEpochSecond(ZoneOffset.UTC) * 1000)
AlertDialog(
onDismissRequest = onDismiss,
modifier = modifier.wrapContentHeight(),
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(
modifier = modifier
.requiredWidth(360.dp)
.heightIn(max = 568.dp),
shape = DatePickerDefaults.shape,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 6.dp,
) {
Column(verticalArrangement = Arrangement.SpaceBetween) {
DatePicker(state = pickerState)
Box(modifier = Modifier
.align(Alignment.End)
.padding(DialogButtonsPadding)) {
AlertDialogFlowRow(
mainAxisSpacing = DialogButtonsMainAxisSpacing,
crossAxisSpacing = DialogButtonsCrossAxisSpacing
) {
TextButton(
onClick = onDismiss
) {
Text(text = dismissLabel)
}
TextButton(
onClick = {
val millis = pickerState.selectedDateMillis ?: return@TextButton
val date = LocalDateTime.ofInstant(
Instant.ofEpochMilli(millis), ZoneId.systemDefault()
)
onConfirm(
localDateTime
.withYear(date.year)
.withMonth(date.monthValue)
.withDayOfMonth(date.dayOfMonth)
)
}
) {
Text(text = confirmLabel)
}
}
}
}
}
}
}
// From androidx/compose/material3/AlertDialog.kt
@Composable
private fun AlertDialogFlowRow(
mainAxisSpacing: Dp,
crossAxisSpacing: Dp,
content: @Composable () -> Unit
) {
Layout(content) { measurables, constraints ->
val sequences = mutableListOf<List<Placeable>>()
val crossAxisSizes = mutableListOf<Int>()
val crossAxisPositions = mutableListOf<Int>()
var mainAxisSpace = 0
var crossAxisSpace = 0
val currentSequence = mutableListOf<Placeable>()
var currentMainAxisSize = 0
var currentCrossAxisSize = 0
// Return whether the placeable can be added to the current sequence.
fun canAddToCurrentSequence(placeable: Placeable) =
currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() +
placeable.width <= constraints.maxWidth
// Store current sequence information and start a new sequence.
fun startNewSequence() {
if (sequences.isNotEmpty()) {
crossAxisSpace += crossAxisSpacing.roundToPx()
}
sequences += currentSequence.toList()
crossAxisSizes += currentCrossAxisSize
crossAxisPositions += crossAxisSpace
crossAxisSpace += currentCrossAxisSize
mainAxisSpace = max(mainAxisSpace, currentMainAxisSize)
currentSequence.clear()
currentMainAxisSize = 0
currentCrossAxisSize = 0
}
for (measurable in measurables) {
// Ask the child for its preferred size.
val placeable = measurable.measure(constraints)
// Start a new sequence if there is not enough space.
if (!canAddToCurrentSequence(placeable)) startNewSequence()
// Add the child to the current sequence.
if (currentSequence.isNotEmpty()) {
currentMainAxisSize += mainAxisSpacing.roundToPx()
}
currentSequence.add(placeable)
currentMainAxisSize += placeable.width
currentCrossAxisSize = max(currentCrossAxisSize, placeable.height)
}
if (currentSequence.isNotEmpty()) startNewSequence()
val mainAxisLayoutSize = max(mainAxisSpace, constraints.minWidth)
val crossAxisLayoutSize = max(crossAxisSpace, constraints.minHeight)
layout(mainAxisLayoutSize, crossAxisLayoutSize) {
sequences.forEachIndexed { i, placeables ->
val childrenMainAxisSizes = IntArray(placeables.size) { j ->
placeables[j].width +
if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
}
val arrangement = Arrangement.End
val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 }
with(arrangement) {
arrange(
mainAxisLayoutSize, childrenMainAxisSizes,
layoutDirection, mainAxisPositions
)
}
placeables.forEachIndexed { j, placeable ->
placeable.place(
x = mainAxisPositions[j],
y = crossAxisPositions[i]
)
}
}
}
}
}
private val DialogButtonsPadding by lazy { PaddingValues(bottom = 8.dp, end = 6.dp) }
private val DialogButtonsMainAxisSpacing by lazy { 8.dp }
private val DialogButtonsCrossAxisSpacing by lazy { 12.dp }

View File

@ -20,25 +20,18 @@ package com.sadellie.unitto.core.ui.common
import android.content.res.Configuration
import android.view.HapticFeedbackConstants
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalView
import kotlinx.coroutines.launch
@Composable
fun BasicKeyboardButton(
@ -52,21 +45,21 @@ fun BasicKeyboardButton(
contentHeight: Float
) {
val view = LocalView.current
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val cornerRadius: Int by animateIntAsState(
targetValue = if (isPressed) 30 else 50,
animationSpec = tween(easing = FastOutSlowInEasing),
)
val coroutineScope = rememberCoroutineScope()
UnittoButton(
modifier = modifier,
onClick = onClick,
onClick = {
onClick()
if (allowVibration) {
coroutineScope.launch {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
}
},
onLongClick = onLongClick,
shape = RoundedCornerShape(cornerRadius),
containerColor = containerColor,
contentPadding = PaddingValues(),
interactionSource = interactionSource
contentPadding = PaddingValues()
) {
Icon(
imageVector = icon,
@ -75,10 +68,6 @@ fun BasicKeyboardButton(
tint = iconColor
)
}
LaunchedEffect(key1 = isPressed) {
if (isPressed and allowVibration) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
}
@Composable

View File

@ -24,7 +24,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.sadellie.unitto.core.ui.R
import com.sadellie.unitto.core.base.R
/**
* Button that is used in Top bars

View File

@ -0,0 +1,87 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
fun Modifier.squashable(
onClick: () -> Unit = {},
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
interactionSource: MutableInteractionSource,
cornerRadiusRange: IntRange,
role: Role = Role.Button,
) = composed {
val isPressed by interactionSource.collectIsPressedAsState()
val cornerRadius: Int by animateIntAsState(
targetValue = if (isPressed) cornerRadiusRange.first else cornerRadiusRange.last,
animationSpec = tween(easing = FastOutSlowInEasing),
)
Modifier
.clip(RoundedCornerShape(cornerRadius))
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
indication = rememberRipple(),
role = role,
enabled = enabled
)
}
fun Modifier.squashable(
onClick: () -> Unit = {},
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
interactionSource: MutableInteractionSource,
cornerRadiusRange: ClosedRange<Dp>,
role: Role = Role.Button,
) = composed {
val isPressed by interactionSource.collectIsPressedAsState()
val cornerRadius: Dp by animateDpAsState(
targetValue = if (isPressed) cornerRadiusRange.start else cornerRadiusRange.endInclusive,
animationSpec = tween(easing = FastOutSlowInEasing),
)
Modifier
.clip(RoundedCornerShape(cornerRadius))
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
indication = rememberRipple(),
role = role,
enabled = enabled
)
}

View File

@ -24,7 +24,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.sadellie.unitto.core.ui.R
import com.sadellie.unitto.core.base.R
/**
* Button that is used in Top bars

View File

@ -66,7 +66,7 @@ fun RowScope.SegmentedButton(
label: String,
onClick: () -> Unit,
selected: Boolean,
icon: ImageVector
icon: ImageVector? = null
) {
val containerColor =
if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface
@ -81,6 +81,7 @@ fun RowScope.SegmentedButton(
),
contentPadding = PaddingValues(horizontal = 12.dp)
) {
if (icon != null) {
Crossfade(targetState = selected) {
if (it) {
Icon(Icons.Default.Check, null, Modifier.size(18.dp))
@ -89,6 +90,7 @@ fun RowScope.SegmentedButton(
}
}
Spacer(Modifier.width(8.dp))
}
Text(label)
}
}

View File

@ -0,0 +1,34 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.sadellie.unitto.core.base.R
@Composable
fun SettingsButton(onClick: () -> Unit) {
IconButton(onClick) {
Icon(Icons.Outlined.Settings, stringResource(R.string.open_settings_description))
}
}

View File

@ -19,7 +19,6 @@
package com.sadellie.unitto.core.ui.common
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@ -27,8 +26,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
@ -40,17 +37,14 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.semantics.Role
@Composable
fun UnittoButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
shape: Shape = RoundedCornerShape(100),
enabled: Boolean = true,
containerColor: Color,
contentColor: Color = contentColorFor(containerColor),
border: BorderStroke? = null,
@ -59,16 +53,16 @@ fun UnittoButton(
content: @Composable RowScope.() -> Unit
) {
Surface(
modifier = modifier.clip(shape).combinedClickable(
modifier = modifier.squashable(
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
indication = rememberRipple(),
role = Role.Button,
cornerRadiusRange = 30..50,
enabled = enabled
),
color = containerColor,
contentColor = contentColor,
border = border
border = border,
) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {

View File

@ -31,8 +31,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.core.ui.R
import com.sadellie.unitto.core.ui.model.DrawerItems
@Composable

View File

@ -46,7 +46,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.R
import com.sadellie.unitto.core.base.R
/**
* Represents one item in list on Settings screen.

View File

@ -0,0 +1,195 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.model.DrawerItems
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
// Why do I have to do it myself?
@Composable
fun UnittoModalNavigationDrawer(
drawer: @Composable () -> Unit,
modifier: Modifier,
state: AnchoredDraggableState<UnittoDrawerState>,
gesturesEnabled: Boolean,
scope: CoroutineScope,
content: @Composable () -> Unit,
) {
Box(modifier.fillMaxSize()) {
content()
Scrim(
open = state.isOpen,
onClose = { if (gesturesEnabled) scope.launch { state.close() } },
fraction = {
fraction(state.anchors.minAnchor(), state.anchors.maxAnchor(), state.offset)
},
color = DrawerDefaults.scrimColor
)
// Drawer
Box(Modifier
.offset {
IntOffset(
x = state
.requireOffset()
.roundToInt(), y = 0
)
}
.anchoredDraggable(
state = state,
orientation = Orientation.Horizontal,
enabled = gesturesEnabled or state.isOpen,
)
.padding(end = 18.dp) // Draggable when closed
) {
drawer()
}
}
}
@Composable
private fun Scrim(
open: Boolean,
onClose: () -> Unit,
fraction: () -> Float,
color: Color,
) {
val dismissDrawer = if (open) {
Modifier.pointerInput(onClose) { detectTapGestures { onClose() } }
} else {
Modifier
}
Canvas(
Modifier
.fillMaxSize()
.then(dismissDrawer)
) {
drawRect(color, alpha = fraction())
}
}
enum class UnittoDrawerState { OPEN, CLOSED }
@Composable
fun rememberUnittoDrawerState(
initialValue: UnittoDrawerState = UnittoDrawerState.CLOSED,
): AnchoredDraggableState<UnittoDrawerState> {
val minValue = -with(LocalDensity.current) { 360.dp.toPx() }
val positionalThreshold = -minValue * 0.5f
val velocityThreshold = with(LocalDensity.current) { 400.dp.toPx() }
return remember {
AnchoredDraggableState(
initialValue = initialValue,
anchors = DraggableAnchors {
UnittoDrawerState.OPEN at 0F
UnittoDrawerState.CLOSED at minValue
},
positionalThreshold = { positionalThreshold },
velocityThreshold = { velocityThreshold },
animationSpec = tween()
)
}
}
val AnchoredDraggableState<UnittoDrawerState>.isOpen
get() = this.currentValue == UnittoDrawerState.OPEN
suspend fun AnchoredDraggableState<UnittoDrawerState>.close() {
this.animateTo(UnittoDrawerState.CLOSED)
}
suspend fun AnchoredDraggableState<UnittoDrawerState>.open() {
this.animateTo(UnittoDrawerState.OPEN)
}
private fun fraction(a: Float, b: Float, pos: Float) =
((pos - a) / (b - a)).coerceIn(0f, 1f)
@Preview(backgroundColor = 0xFFC8F7D4, showBackground = true, showSystemUi = true)
@Composable
private fun PreviewUnittoModalNavigationDrawer() {
val drawerState = rememberUnittoDrawerState(initialValue = UnittoDrawerState.OPEN)
val corScope = rememberCoroutineScope()
UnittoModalNavigationDrawer(
drawer = {
UnittoDrawerSheet(
modifier = Modifier,
mainTabs = listOf(
DrawerItems.Calculator,
DrawerItems.Calculator,
DrawerItems.Calculator,
),
additionalTabs = listOf(
DrawerItems.Calculator,
DrawerItems.Calculator,
DrawerItems.Calculator,
),
currentDestination = DrawerItems.Calculator.destination,
onItemClick = {}
)
},
modifier = Modifier,
state = drawerState,
gesturesEnabled = true,
scope = corScope,
content = {
Column {
Text(text = "Content")
Button(
onClick = { corScope.launch { drawerState.open() } }
) {
Text(text = "BUTTON")
}
}
}
)
}

View File

@ -0,0 +1,158 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.ceil
import kotlin.math.roundToInt
@Composable
fun UnittoSlider(
modifier: Modifier = Modifier,
value: Float,
valueRange: ClosedFloatingPointRange<Float>,
onValueChange: (Float) -> Unit,
onValueChangeFinished: (Float) -> Unit = {}
) {
val animated = animateFloatAsState(targetValue = value)
Slider(
value = animated.value,
onValueChange = onValueChange,
modifier = modifier,
valueRange = valueRange,
onValueChangeFinished = { onValueChangeFinished(animated.value) },
track = { sliderPosition -> SquigglyTrack(sliderPosition) },
steps = valueRange.endInclusive.roundToInt(),
)
}
@Composable
private fun SquigglyTrack(
sliderState: SliderState,
eachWaveWidth: Float = 80f,
strokeWidth: Float = 15f,
filledColor: Color = MaterialTheme.colorScheme.primary,
unfilledColor: Color = MaterialTheme.colorScheme.surfaceVariant
) {
val coroutineScope = rememberCoroutineScope()
var direct by remember { mutableFloatStateOf(0.72f) }
val animatedDirect = animateFloatAsState(direct, spring(stiffness = Spring.StiffnessLow))
val slider = sliderState.valueRange.endInclusive
LaunchedEffect(sliderState.valueRange.endInclusive) {
coroutineScope.launch {
delay(200L)
direct *= -1
}
}
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(20.dp)
) {
val width = size.width
val height = size.height
val path = Path().apply {
moveTo(
x = strokeWidth / 2,
y = height.times(0.5f)
)
val amount = ceil(width.div(eachWaveWidth))
repeat(amount.toInt()) {
val peek = if (it % 2 == 0) animatedDirect.value else -animatedDirect.value
relativeQuadraticBezierTo(
dx1 = eachWaveWidth * 0.5f,
// 0.75, because 1.0 was clipping out of bound for some reason
dy1 = height.times(peek),
dx2 = eachWaveWidth,
dy2 = 0f
)
}
}
clipRect(
top = 0f,
left = 0f,
right = width.times(slider),
bottom = height,
clipOp = ClipOp.Intersect
) {
drawPath(
path = path,
color = filledColor,
style = Stroke(strokeWidth, cap = StrokeCap.Round)
)
}
drawLine(
color = unfilledColor,
start = Offset(width.times(slider), height.times(0.5f)),
end = Offset(width, height.times(0.5f)),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
}
}
@Preview(device = "spec:width=411dp,height=891dp")
@Preview(device = "spec:width=673.5dp,height=841dp,dpi=480")
@Preview(device = "spec:width=1280dp,height=800dp,dpi=480")
@Preview(device = "spec:width=1920dp,height=1080dp,dpi=480")
@Composable
private fun PreviewNewSlider() {
var currentValue by remember { mutableFloatStateOf(0.9f) }
UnittoSlider(
value = currentValue,
valueRange = 0f..1f,
onValueChange = { currentValue = it }
)
}

View File

@ -0,0 +1,73 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common.textfield
import com.sadellie.unitto.core.base.Token
import kotlin.math.abs
fun String.fixCursor(pos: Int, grouping: String): Int {
if (isEmpty()) return pos
// Best position if we move cursor left
var leftCursor = pos
while (this.isPlacedIllegallyAt(leftCursor, grouping)) leftCursor--
// Best position if we move cursor right
var rightCursor = pos
while (this.isPlacedIllegallyAt(rightCursor, grouping)) rightCursor++
return listOf(leftCursor, rightCursor).minBy { abs(it - pos) }
}
fun String.tokenLengthAhead(pos: Int): Int {
Token.Func.allWithOpeningBracket.forEach {
if (pos.isAfterToken(this, it)) return it.length
}
return 1
}
private fun String.isPlacedIllegallyAt(pos: Int, grouping: String): Boolean {
// For things like "123,|456" - this is illegal
if (pos.isAfterToken(this, grouping)) return true
// For things like "123,456+c|os(8)" - this is illegal
Token.Func.allWithOpeningBracket.forEach {
if (pos.isAtToken(this, it)) return true
}
return false
}
private fun Int.isAtToken(str: String, token: String): Boolean {
val checkBound = (token.length - 1).coerceAtLeast(1)
return str
.substring(
startIndex = (this - checkBound).coerceAtLeast(0),
endIndex = (this + checkBound).coerceAtMost(str.length)
)
.contains(token)
}
private fun Int.isAfterToken(str: String, token: String): Boolean {
return str
.substring((this - token.length).coerceAtLeast(0), this)
.contains(token)
}

View File

@ -0,0 +1,82 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common.textfield
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
class ExpressionTransformer(private val formatterSymbols: FormatterSymbols) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val formatted = text.text.formatExpression(formatterSymbols)
return TransformedText(
text = AnnotatedString(formatted),
offsetMapping = ExpressionMapping(text.text, formatted)
)
}
inner class ExpressionMapping(
private val original: String,
private val transformed: String
) : OffsetMapping {
// Called when entering text (on each text change)
// Basically moves cursor to the right position
//
// original input is "1000" and cursor is placed at the end "1000|"
// the transformed is "1,000" where cursor should be? - "1,000|"
override fun originalToTransformed(offset: Int): Int {
if (offset <= 0) return 0
if (offset >= original.length) return transformed.length
val unformattedSubstr = original.take(offset)
var buffer = ""
var groupings = 0
run {
transformed.forEach {
when (it) {
formatterSymbols.grouping.first() -> groupings++
formatterSymbols.fractional.first() -> buffer += "."
else -> buffer += it
}
if (buffer == unformattedSubstr) return@run
}
}
return transformed.fixCursor(buffer.length + groupings, formatterSymbols.grouping)
}
// Called when clicking transformed text
// Snaps cursor to the right position
//
// the transformed is "1,000" and cursor is placed at the end "1,000|"
// original input is "1000" where cursor should be? - "1000|"
override fun transformedToOriginal(offset: Int): Int {
if (offset <= 0) return 0
if (offset >= transformed.length) return original.length
val grouping = formatterSymbols.grouping.first()
val fixedCursor = transformed.fixCursor(offset, formatterSymbols.grouping)
val addedSymbols = transformed.take(fixedCursor).count { it == grouping }
return fixedCursor - addedSymbols
}
}
}

View File

@ -0,0 +1,193 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common.textfield
import android.content.Context
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.base.Token
import java.math.BigDecimal
import java.math.RoundingMode
private val numbersRegex by lazy { Regex("[\\d.]+") }
private val timeDivisions by lazy {
mapOf(
R.string.day_short to BigDecimal("86400000000000000000000"),
R.string.hour_short to BigDecimal("3600000000000000000000"),
R.string.minute_short to BigDecimal("60000000000000000000"),
R.string.second_short to BigDecimal("1000000000000000000"),
R.string.millisecond_short to BigDecimal("1000000000000000"),
R.string.microsecond_short to BigDecimal("1000000000000"),
R.string.nanosecond_short to BigDecimal("1000000000"),
R.string.attosecond_short to BigDecimal("1"),
)
}
fun String.clearAndFilterExpression(formatterSymbols: FormatterSymbols): String {
var clean = this
.replace(formatterSymbols.grouping, "")
.replace(formatterSymbols.fractional, Token.Digit.dot)
.replace(" ", "")
Token.sexyToUgly.forEach { (token, ugliness) ->
ugliness.forEach {
clean = clean.replace(it, token)
}
}
return clean.cleanIt(Token.expressionTokens)
}
internal fun String.clearAndFilterNumberBase(): String {
return uppercase().cleanIt(Token.numberBaseTokens)
}
/**
* Format string time conversion result into a more readable format.
*
* @param basicUnit Basic unit of the unit we convert to
* @return String like "1d 12h 12s".
*/
fun String.formatTime(
context: Context,
basicUnit: BigDecimal?,
formatterSymbols: FormatterSymbols
): String {
// We get ugly version of input (non-fancy minus)
val input = this
if (basicUnit == null) return Token.Digit._0
try {
// Don't need magic if the input is zero
if (BigDecimal(input).compareTo(BigDecimal.ZERO) == 0) return Token.Digit._0
} catch (e: NumberFormatException) {
// For case such as "10-" and "("
return Token.Digit._0
}
// Attoseconds don't need "magic"
if (basicUnit.compareTo(BigDecimal.ONE) == 0) return input.formatExpression(formatterSymbols)
var result = if (input.startsWith("-")) Token.Operator.minus else ""
var remainingSeconds = BigDecimal(input)
.abs()
.multiply(basicUnit)
.setScale(0, RoundingMode.HALF_EVEN)
if (remainingSeconds.compareTo(BigDecimal.ZERO) == 0) return Token.Digit._0
timeDivisions.forEach { (timeStr, divider) ->
val division = remainingSeconds.divideAndRemainder(divider)
val time = division.component1()
remainingSeconds = division.component2()
if (time.compareTo(BigDecimal.ZERO) != 0) {
result += "${time.toPlainString().formatExpression(formatterSymbols)}${context.getString(timeStr)} "
}
}
return result.trimEnd()
}
fun String.formatExpression(
formatterSymbols: FormatterSymbols
): String {
var input = this
// Don't do anything to engineering string.
if (input.contains(Token.DisplayOnly.engineeringE)) {
return input.replace(Token.Digit.dot, formatterSymbols.fractional)
}
numbersRegex
.findAll(input)
.map(MatchResult::value)
.forEach {
input = input.replace(it, it.formatNumber(formatterSymbols))
}
Token.sexyToUgly.forEach { (token, ugliness) ->
ugliness.forEach { uglySymbol ->
input = input.replace(uglySymbol, token)
}
}
return input
}
private fun String.formatNumber(
formatterSymbols: FormatterSymbols
): String {
val input = this
if (input.any { it.isLetter() }) return input
var firstPart = input.takeWhile { it != '.' }
val remainingPart = input.removePrefix(firstPart)
// Number of empty symbols (spaces) we need to add to correctly split into chunks.
val offset = 3 - firstPart.length.mod(3)
val output = if (offset != 3) {
// We add some spaces at the beginning so that last chunk has 3 symbols
firstPart = " ".repeat(offset) + firstPart
firstPart.chunked(3).joinToString(formatterSymbols.grouping).drop(offset)
} else {
firstPart.chunked(3).joinToString(formatterSymbols.grouping)
}
return output.plus(remainingPart.replace(".", formatterSymbols.fractional))
}
private fun String.cleanIt(legalTokens: List<String>): String {
val streamOfTokens = this
fun peekTokenAfter(cursor: Int): String? {
legalTokens.forEach { token ->
val subs = streamOfTokens
.substring(
cursor,
(cursor + token.length).coerceAtMost(streamOfTokens.length)
)
if (subs == token) {
// Got a digit, see if there are other digits coming after
if (token in Token.Digit.allWithDot) {
return streamOfTokens
.substring(cursor)
.takeWhile { Token.Digit.allWithDot.contains(it.toString()) }
}
return token
}
}
return null
}
var cursor = 0
var tokens = ""
while (cursor != streamOfTokens.length) {
val nextToken = peekTokenAfter(cursor)
if (nextToken != null) {
tokens += nextToken
cursor += nextToken.length
} else {
// Didn't find any token, move left slowly (by 1 symbol)
cursor++
}
}
return tokens
}

View File

@ -0,0 +1,46 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common.textfield
import com.sadellie.unitto.core.base.Separator
sealed class FormatterSymbols(val grouping: String, val fractional: String) {
object Spaces : FormatterSymbols(" ", ".")
object Period : FormatterSymbols(".", ",")
object Comma : FormatterSymbols(",", ".")
}
object AllFormatterSymbols {
private val allFormatterSymbols by lazy {
hashMapOf(
Separator.SPACE to FormatterSymbols.Spaces,
Separator.PERIOD to FormatterSymbols.Period,
Separator.COMMA to FormatterSymbols.Comma
)
}
/**
* Defaults to [FormatterSymbols.Spaces] if not found.
*
* @see Separator
*/
fun getById(separator: Int): FormatterSymbols {
return allFormatterSymbols.getOrElse(separator) { FormatterSymbols.Spaces }
}
}

View File

@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@ -54,105 +55,124 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
import com.sadellie.unitto.core.base.Separator
import com.sadellie.unitto.core.ui.Formatter
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge
import kotlin.math.ceil
import kotlin.math.roundToInt
@Composable
fun InputTextField(
fun ExpressionTextField(
modifier: Modifier,
value: TextFieldValue,
textStyle: TextStyle = NumbersTextStyleDisplayLarge,
minRatio: Float = 1f,
cutCallback: () -> Unit,
pasteCallback: (String) -> Unit,
onCursorChange: (IntRange) -> Unit,
cutCallback: () -> Unit = {},
pasteCallback: (String) -> Unit = {},
onCursorChange: (TextRange) -> Unit,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
formatterSymbols: FormatterSymbols,
readOnly: Boolean = false,
placeholder: String? = null,
) {
val clipboardManager = LocalClipboardManager.current
fun copyCallback() = clipboardManager.copyWithoutGrouping(value)
fun copyCallback() {
clipboardManager.copyWithoutGrouping(value, formatterSymbols)
onCursorChange(TextRange(value.selection.end))
}
val textToolbar = UnittoTextToolbar(
val textToolbar: UnittoTextToolbar = if (readOnly) {
UnittoTextToolbar(
view = LocalView.current,
copyCallback = ::copyCallback,
)
} else {
UnittoTextToolbar(
view = LocalView.current,
copyCallback = ::copyCallback,
pasteCallback = {
pasteCallback(
Formatter.toSeparator(
clipboardManager.getText()?.text ?: "", Separator.COMMA
)
)
pasteCallback(clipboardManager.getText()?.text?.clearAndFilterExpression(formatterSymbols) ?: "")
},
cutCallback = {
copyCallback()
clipboardManager.copyWithoutGrouping(value, formatterSymbols)
cutCallback()
onCursorChange(value.selection.end..value.selection.end)
}
)
}
CompositionLocalProvider(
LocalTextInputService provides null,
LocalTextToolbar provides textToolbar
) {
AutoSizableTextField(
modifier = modifier,
value = value,
textStyle = textStyle.copy(color = textColor),
formattedValue = value.text.formatExpression(formatterSymbols),
textStyle = NumbersTextStyleDisplayLarge.copy(color = textColor),
minRatio = minRatio,
onValueChange = {
onCursorChange(it.selection.start..it.selection.end)
},
onValueChange = { onCursorChange(it.selection) },
readOnly = readOnly,
showToolbar = textToolbar::showMenu,
hideToolbar = textToolbar::hide
hideToolbar = textToolbar::hide,
visualTransformation = ExpressionTransformer(formatterSymbols),
placeholder = placeholder,
textToolbar = textToolbar
)
}
}
@Composable
fun InputTextField(
modifier: Modifier = Modifier,
value: String,
textStyle: TextStyle = NumbersTextStyleDisplayLarge,
fun UnformattedTextField(
modifier: Modifier,
value: TextFieldValue,
minRatio: Float = 1f,
cutCallback: () -> Unit = {},
pasteCallback: (String) -> Unit = {},
onCursorChange: (TextRange) -> Unit,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
readOnly: Boolean = false,
placeholder: String? = null,
) {
var textFieldValue by remember(value) {
mutableStateOf(TextFieldValue(value, selection = TextRange(value.length)))
}
val clipboardManager = LocalClipboardManager.current
fun copyCallback() {
clipboardManager.copyWithoutGrouping(textFieldValue)
textFieldValue = textFieldValue.copy(selection = TextRange(textFieldValue.selection.end))
clipboardManager.copy(value)
onCursorChange(TextRange(value.selection.end))
}
CompositionLocalProvider(
LocalTextInputService provides null,
LocalTextToolbar provides UnittoTextToolbar(
val textToolbar: UnittoTextToolbar = if (readOnly) {
UnittoTextToolbar(
view = LocalView.current,
copyCallback = ::copyCallback,
)
) {
AutoSizableTextField(
modifier = modifier,
value = textFieldValue,
onValueChange = { textFieldValue = it },
textStyle = textStyle.copy(color = textColor),
minRatio = minRatio,
readOnly = true,
interactionSource = interactionSource
} else {
UnittoTextToolbar(
view = LocalView.current,
copyCallback = ::copyCallback,
pasteCallback = {
pasteCallback(clipboardManager.getText()?.text?.clearAndFilterNumberBase() ?: "")
},
cutCallback = {
clipboardManager.copy(value)
cutCallback()
}
)
}
AutoSizableTextField(
modifier = modifier,
value = value,
textStyle = NumbersTextStyleDisplayLarge.copy(color = textColor),
minRatio = minRatio,
onValueChange = { onCursorChange(it.selection) },
readOnly = readOnly,
showToolbar = textToolbar::showMenu,
hideToolbar = textToolbar::hide,
placeholder = placeholder,
textToolbar = textToolbar
)
}
@Composable
private fun AutoSizableTextField(
modifier: Modifier = Modifier,
value: TextFieldValue,
formattedValue: String = value.text,
textStyle: TextStyle = TextStyle(),
scaleFactor: Float = 0.95f,
minRatio: Float = 1f,
@ -160,11 +180,14 @@ private fun AutoSizableTextField(
readOnly: Boolean = false,
showToolbar: (rect: Rect) -> Unit = {},
hideToolbar: () -> Unit = {},
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
visualTransformation: VisualTransformation = VisualTransformation.None,
placeholder: String? = null,
textToolbar: UnittoTextToolbar
) {
val focusRequester = remember { FocusRequester() }
val density = LocalDensity.current
val textValue = value.copy(value.text.take(2000))
var nFontSize: TextUnit by remember { mutableStateOf(0.sp) }
var minFontSize: TextUnit
@ -174,14 +197,14 @@ private fun AutoSizableTextField(
) {
with(density) {
// Cursor handle is not visible without this, 0.836f is the minimum required factor here
nFontSize = maxHeight.toSp() * 0.836f
nFontSize = maxHeight.toSp() * 0.835f
minFontSize = nFontSize * minRatio
}
// Modified: https://blog.canopas.com/autosizing-textfield-in-jetpack-compose-7a80f0270853
val calculateParagraph = @Composable {
Paragraph(
text = value.text,
text = formattedValue,
style = textStyle.copy(fontSize = nFontSize),
constraints = Constraints(
maxWidth = ceil(with(density) { maxWidth.toPx() }).toInt()
@ -210,8 +233,12 @@ private fun AutoSizableTextField(
)
var offset = Offset.Zero
CompositionLocalProvider(
LocalTextInputService provides null,
LocalTextToolbar provides textToolbar
) {
BasicTextField(
value = value,
value = textValue,
onValueChange = {
showToolbar(Rect(offset, 0f))
hideToolbar()
@ -242,14 +269,35 @@ private fun AutoSizableTextField(
)
}
}
.onGloballyPositioned { layoutCoords -> offset = layoutCoords.positionInWindow() },
.onGloballyPositioned { layoutCoords ->
offset = layoutCoords.positionInWindow()
},
textStyle = nTextStyle,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
singleLine = true,
readOnly = readOnly,
interactionSource = interactionSource
visualTransformation = visualTransformation,
decorationBox = { innerTextField ->
if (textValue.text.isEmpty() and !placeholder.isNullOrEmpty()) {
Text(
text = placeholder!!, // It's not null, i swear
style = nTextStyle,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(x = -placeable.width, y = 0)
}
}
)
}
innerTextField()
}
)
}
}
}
/**
@ -260,8 +308,22 @@ private fun AutoSizableTextField(
*
* @param value Formatted value that has grouping symbols.
*/
fun ClipboardManager.copyWithoutGrouping(value: TextFieldValue) = this.setText(
fun ClipboardManager.copyWithoutGrouping(
value: TextFieldValue,
formatterSymbols: FormatterSymbols
) = this.setText(
AnnotatedString(
Formatter.removeGrouping(value.annotatedString.subSequence(value.selection).text)
value.annotatedString
.subSequence(value.selection)
.text
.replace(formatterSymbols.grouping, "")
)
)
fun ClipboardManager.copy(value: TextFieldValue) = this.setText(
AnnotatedString(
value.annotatedString
.subSequence(value.selection)
.text
)
)

View File

@ -0,0 +1,59 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common.textfield
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
fun TextFieldValue.addTokens(tokens: String): TextFieldValue {
return this.copy(
text = text.replaceRange(selection.start, selection.end, tokens),
selection = TextRange(selection.start + tokens.length)
)
}
fun TextFieldValue.deleteTokens(): TextFieldValue {
val distanceFromEnd = text.length - selection.end
val deleteRangeStart = when (selection.end) {
// Don't delete if at the start of the text field
0 -> return this
// We don't have anything selected (cursor in one position)
// like this 1234|56 => after deleting will be like this 123|56
// Cursor moved one symbol left
selection.start -> {
// We default to 1 here. It means that cursor is not placed after illegal token
// Just a number or a binary operator or something else, can delete by one symbol
val symbolsToDelete = text.tokenLengthAhead(selection.end)
selection.start - symbolsToDelete
}
// We have multiple symbols selected
// like this 123[45]6 => after deleting will be like this 123|6
// Cursor will be placed where selection start was
else -> selection.start
}
val newText = text.removeRange(deleteRangeStart, selection.end)
return this.copy(
text = newText,
selection = TextRange((newText.length - distanceFromEnd).coerceAtLeast(0))
)
}

View File

@ -20,9 +20,11 @@ package com.sadellie.unitto.core.ui.model
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Calculate
import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material.icons.outlined.Calculate
import androidx.compose.material.icons.outlined.Event
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.ui.graphics.vector.ImageVector
@ -45,6 +47,12 @@ sealed class DrawerItems(
defaultIcon = Icons.Outlined.SwapHoriz
)
object DateDifference : DrawerItems(
destination = TopLevelDestinations.DateDifference,
selectedIcon = Icons.Filled.Event,
defaultIcon = Icons.Outlined.Event
)
object Settings : DrawerItems(
destination = TopLevelDestinations.Settings,
selectedIcon = Icons.Filled.Settings,

View File

@ -25,7 +25,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import com.sadellie.unitto.core.ui.R
import com.sadellie.unitto.core.base.R
private val Montserrat = FontFamily(
Font(R.font.montserrat_light, weight = FontWeight.Light),

View File

@ -0,0 +1,68 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui
import com.sadellie.unitto.core.ui.common.textfield.ExpressionTransformer
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class ExpressionTransformerTest {
private val expr = ExpressionTransformer(FormatterSymbols.Comma)
private fun origToTrans(orig: String, trans: String, offset: Int): Int =
expr.ExpressionMapping(orig, trans).originalToTransformed(offset)
private fun transToOrig(trans: String, orig: String, offset: Int): Int =
expr.ExpressionMapping(orig, trans).transformedToOriginal(offset)
@Test
fun `test 1234`() {
// at the start
assertEquals(0, origToTrans("1,234", "1234", 0))
assertEquals(0, transToOrig("1,234", "1234", 0))
// somewhere in inside, no offset needed
assertEquals(1, origToTrans("1234", "1,234", 1))
assertEquals(1, transToOrig("1,234", "1234", 1))
// somewhere in inside, offset needed
assertEquals(1, transToOrig("1,234", "1234", 2))
// at the end
assertEquals(5, origToTrans("1234", "1,234", 4))
assertEquals(4, transToOrig("1,234", "1234", 5))
}
@Test
fun `test 123`() {
// at the start
assertEquals(0, origToTrans("123", "123", 0))
assertEquals(0, transToOrig("123", "123", 0))
// somewhere in inside
assertEquals(1, origToTrans("123", "123", 1))
assertEquals(1, transToOrig("123", "123", 1))
// at the end
assertEquals(3, origToTrans("123", "123", 3))
assertEquals(3, transToOrig("123", "123", 3))
}
}

View File

@ -20,7 +20,9 @@ package com.sadellie.unitto.core.ui
import android.content.Context
import androidx.compose.ui.test.junit4.createComposeRule
import com.sadellie.unitto.core.base.Separator
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import com.sadellie.unitto.core.ui.common.textfield.formatExpression
import com.sadellie.unitto.core.ui.common.textfield.formatTime
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@ -29,16 +31,14 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import java.math.BigDecimal
private val formatter = Formatter
private const val ENG_VALUE = "123E+21"
private const val ENG_VALUE_FRACTIONAL = "123.3E+21"
private const val COMPLETE_VALUE = "123456.789"
private const val INCOMPLETE_VALUE = "123456."
private const val NO_FRACTIONAL_VALUE = "123456"
private const val INCOMPLETE_EXPR = "50+123456÷8×0.812+"
private const val COMPLETE_EXPR = "50+123456÷8×0.812+0-√9*4^9+2×(9+8×7)"
private const val LONG_HALF_COMPLETE_EXPR = "50+123456÷89078..9×0.812+0-√9*4^9+2×(9+8×7)×sin(13sin123cos"
private const val INCOMPLETE_EXPR = "50+123456÷8×0.8-12+"
private const val COMPLETE_EXPR = "50+123456÷8×0.8-12+0-√9×4^9+2×(9+8×7)"
private const val LONG_HALF_COMPLETE_EXPR = "50+123456÷89078..9×0.8-12+0-√9×4^9+2×(9+8×7)×sin(13sin123cos"
private const val SOME_BRACKETS = "(((((((("
@RunWith(RobolectricTestRunner::class)
@ -49,167 +49,143 @@ class FormatterTest {
@Test
fun setSeparatorSpaces() {
formatter.setSeparator(Separator.SPACES)
assertEquals(".", formatter.fractional)
assertEquals("123E+21", formatter.format(ENG_VALUE))
assertEquals("123.3E+21", formatter.format(ENG_VALUE_FRACTIONAL))
assertEquals("123 456.789", formatter.format(COMPLETE_VALUE))
assertEquals("123 456.", formatter.format(INCOMPLETE_VALUE))
assertEquals("123 456", formatter.format(NO_FRACTIONAL_VALUE))
assertEquals("50+123 456÷8×0.812+", formatter.format(INCOMPLETE_EXPR))
assertEquals("50+123 456÷8×0.812+0√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
assertEquals("50+123 456÷89 078..9×0.812+0√9×4^9+2×(9+8×7)×sin(13sin123cos", formatter.format(LONG_HALF_COMPLETE_EXPR))
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
fun String.format(): String = formatExpression(FormatterSymbols.Spaces)
assertEquals("123E+21", ENG_VALUE.format())
assertEquals("123.3E+21", ENG_VALUE_FRACTIONAL.format())
assertEquals("123 456.789", COMPLETE_VALUE.format())
assertEquals("123 456.", INCOMPLETE_VALUE.format())
assertEquals("123 456", NO_FRACTIONAL_VALUE.format())
assertEquals("50+123 456÷8×0.812+", INCOMPLETE_EXPR.format())
assertEquals("50+123 456÷8×0.812+0√9×4^9+2×(9+8×7)", COMPLETE_EXPR.format())
assertEquals("50+123 456÷89 078..9×0.812+0√9×4^9+2×(9+8×7)×sin(13sin123cos", LONG_HALF_COMPLETE_EXPR.format())
assertEquals("((((((((", SOME_BRACKETS.format())
}
@Test
fun setSeparatorComma() {
formatter.setSeparator(Separator.COMMA)
assertEquals(".", formatter.fractional)
assertEquals("123E+21", formatter.format(ENG_VALUE))
assertEquals("123.3E+21", formatter.format(ENG_VALUE_FRACTIONAL))
assertEquals("123,456.789", formatter.format(COMPLETE_VALUE))
assertEquals("123,456.", formatter.format(INCOMPLETE_VALUE))
assertEquals("123,456", formatter.format(NO_FRACTIONAL_VALUE))
assertEquals("50+123,456÷8×0.812+", formatter.format(INCOMPLETE_EXPR))
assertEquals("50+123,456÷8×0.812+0√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
assertEquals("50+123,456÷89,078..9×0.812+0√9×4^9+2×(9+8×7)×sin(13sin123cos", formatter.format(LONG_HALF_COMPLETE_EXPR))
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
fun String.format(): String = formatExpression(FormatterSymbols.Comma)
assertEquals("123E+21", ENG_VALUE.format())
assertEquals("123.3E+21", ENG_VALUE_FRACTIONAL.format())
assertEquals("123,456.789", COMPLETE_VALUE.format())
assertEquals("123,456.", INCOMPLETE_VALUE.format())
assertEquals("123,456", NO_FRACTIONAL_VALUE.format())
assertEquals("50+123,456÷8×0.812+", INCOMPLETE_EXPR.format())
assertEquals("50+123,456÷8×0.812+0√9×4^9+2×(9+8×7)", COMPLETE_EXPR.format())
assertEquals("50+123,456÷89,078..9×0.812+0√9×4^9+2×(9+8×7)×sin(13sin123cos", LONG_HALF_COMPLETE_EXPR.format())
assertEquals("((((((((", SOME_BRACKETS.format())
}
@Test
fun setSeparatorPeriod() {
formatter.setSeparator(Separator.PERIOD)
assertEquals(",", formatter.fractional)
assertEquals("123E+21", formatter.format(ENG_VALUE))
assertEquals("123,3E+21", formatter.format(ENG_VALUE_FRACTIONAL))
assertEquals("123.456,789", formatter.format(COMPLETE_VALUE))
assertEquals("123.456,", formatter.format(INCOMPLETE_VALUE))
assertEquals("123.456", formatter.format(NO_FRACTIONAL_VALUE))
assertEquals("50+123.456÷8×0,812+", formatter.format(INCOMPLETE_EXPR))
assertEquals("50+123.456÷8×0,812+0√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
assertEquals("50+123.456÷89.078,,9×0,812+0√9×4^9+2×(9+8×7)×sin(13sin123cos", formatter.format(LONG_HALF_COMPLETE_EXPR))
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
fun String.format(): String = formatExpression(FormatterSymbols.Period)
assertEquals("123E+21", ENG_VALUE.format())
assertEquals("123,3E+21", ENG_VALUE_FRACTIONAL.format())
assertEquals("123.456,789", COMPLETE_VALUE.format())
assertEquals("123.456,", INCOMPLETE_VALUE.format())
assertEquals("123.456", NO_FRACTIONAL_VALUE.format())
assertEquals("50+123.456÷8×0,812+", INCOMPLETE_EXPR.format())
assertEquals("50+123.456÷8×0,812+0√9×4^9+2×(9+8×7)", COMPLETE_EXPR.format())
assertEquals("50+123.456÷89.078,,9×0,812+0√9×4^9+2×(9+8×7)×sin(13sin123cos", LONG_HALF_COMPLETE_EXPR.format())
assertEquals("((((((((", SOME_BRACKETS.format())
}
@Test
fun formatTimeTest() {
formatter.setSeparator(Separator.SPACES)
val formatterSymbols = FormatterSymbols.Spaces
var basicValue = BigDecimal.valueOf(1)
val mContext: Context = RuntimeEnvironment.getApplication().applicationContext
assertEquals("-28", formatter.formatTime(mContext, "-28", basicValue))
assertEquals("-0.05", formatter.formatTime(mContext, "-0.05", basicValue))
assertEquals("0", formatter.formatTime(mContext, "0", basicValue))
assertEquals("0", formatter.formatTime(mContext, "-0", basicValue))
fun String.formatTime() = this.formatTime(mContext, basicValue, formatterSymbols)
assertEquals("28", "-28".formatTime())
assertEquals("0.05", "-0.05".formatTime())
assertEquals("0", "0".formatTime())
assertEquals("0", "0".formatTime())
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
assertEquals("-28d", formatter.formatTime(mContext, "-28", basicValue))
assertEquals("-1h 12m", formatter.formatTime(mContext, "-0.05", basicValue))
assertEquals("0", formatter.formatTime(mContext, "0", basicValue))
assertEquals("0", formatter.formatTime(mContext, "-0", basicValue))
assertEquals("28d", "-28".formatTime())
assertEquals("1h 12m", "-0.05".formatTime())
assertEquals("0", "0".formatTime())
assertEquals("0", "-0".formatTime())
// DAYS
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
assertEquals("12h", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("1h 12m", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("7m 12s", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28d", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90d", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90d 12h", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90d 7m 12s", formatter.formatTime(mContext, "90.005", basicValue))
assertEquals("12h","0.5".formatTime())
assertEquals("1h 12m","0.05".formatTime())
assertEquals("7m 12s","0.005".formatTime())
assertEquals("28d","28".formatTime())
assertEquals("90d","90".formatTime())
assertEquals("90d 12h","90.5".formatTime())
assertEquals("90d 7m 12s","90.005".formatTime())
// HOURS
basicValue = BigDecimal.valueOf(3_600_000_000_000_000_000_000.0)
assertEquals("30m", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("3m", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("18s", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("1d 4h", formatter.formatTime(mContext, "28", basicValue))
assertEquals("3d 18h", formatter.formatTime(mContext, "90", basicValue))
assertEquals("3d 18h 30m", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("3d 18h 18s", formatter.formatTime(mContext, "90.005", basicValue))
assertEquals("30m", "0.5".formatTime())
assertEquals("3m", "0.05".formatTime())
assertEquals("18s", "0.005".formatTime())
assertEquals("1d 4h", "28".formatTime())
assertEquals("3d 18h", "90".formatTime())
assertEquals("3d 18h 30m", "90.5".formatTime())
assertEquals("3d 18h 18s", "90.005".formatTime())
// MINUTES
basicValue = BigDecimal.valueOf(60_000_000_000_000_000_000.0)
assertEquals("30s", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("3s", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("300ms", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28m", formatter.formatTime(mContext, "28", basicValue))
assertEquals("1h 30m", formatter.formatTime(mContext, "90", basicValue))
assertEquals("1h 30m 30s", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("1h 30m 300ms", formatter.formatTime(mContext, "90.005", basicValue))
assertEquals("30s", "0.5".formatTime())
assertEquals("3s", "0.05".formatTime())
assertEquals("300ms", "0.005".formatTime())
assertEquals("28m", "28".formatTime())
assertEquals("1h 30m", "90".formatTime())
assertEquals("1h 30m 30s", "90.5".formatTime())
assertEquals("1h 30m 300ms", "90.005".formatTime())
// SECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000_000_000)
assertEquals("500ms", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("50ms", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("5ms", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28s", formatter.formatTime(mContext, "28", basicValue))
assertEquals("1m 30s", formatter.formatTime(mContext, "90", basicValue))
assertEquals("1m 30s 500ms", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("1m 30s 5ms", formatter.formatTime(mContext, "90.005", basicValue))
assertEquals("500ms", "0.5".formatTime())
assertEquals("50ms", "0.05".formatTime())
assertEquals("5ms", "0.005".formatTime())
assertEquals("28s", "28".formatTime())
assertEquals("1m 30s", "90".formatTime())
assertEquals("1m 30s 500ms", "90.5".formatTime())
assertEquals("1m 30s 5ms", "90.005".formatTime())
// MILLISECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000_000)
assertEquals("500µs", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("50µs", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("5µs", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28ms", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90ms", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90ms 500µs", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90ms 5µs", formatter.formatTime(mContext, "90.005", basicValue))
assertEquals("500µs", "0.5".formatTime())
assertEquals("50µs", "0.05".formatTime())
assertEquals("5µs", "0.005".formatTime())
assertEquals("28ms", "28".formatTime())
assertEquals("90ms", "90".formatTime())
assertEquals("90ms 500µs", "90.5".formatTime())
assertEquals("90ms 5µs", "90.005".formatTime())
// MICROSECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000)
assertEquals("500ns", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("50ns", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("5ns", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28µs", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90µs", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90µs 500ns", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90µs 5ns", formatter.formatTime(mContext, "90.005", basicValue))
assertEquals("500ns", "0.5".formatTime())
assertEquals("50ns", "0.05".formatTime())
assertEquals("5ns", "0.005".formatTime())
assertEquals("28µs", "28".formatTime())
assertEquals("90µs", "90".formatTime())
assertEquals("90µs 500ns", "90.5".formatTime())
assertEquals("90µs 5ns", "90.005".formatTime())
// NANOSECONDS
basicValue = BigDecimal.valueOf(1_000_000_000)
assertEquals("500 000 000as", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("50 000 000as", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("5 000 000as", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28ns", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90ns", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90ns 500 000 000as", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90ns 5 000 000as", formatter.formatTime(mContext, "90.005", basicValue))
assertEquals("500 000 000as", "0.5".formatTime())
assertEquals("50 000 000as", "0.05".formatTime())
assertEquals("5 000 000as", "0.005".formatTime())
assertEquals("28ns", "28".formatTime())
assertEquals("90ns", "90".formatTime())
assertEquals("90ns 500 000 000as", "90.5".formatTime())
assertEquals("90ns 5 000 000as", "90.005".formatTime())
// ATTOSECONDS
basicValue = BigDecimal.valueOf(1)
assertEquals("0.5", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("0.05", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("0.005", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90.5", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90.005", formatter.formatTime(mContext, "90.005", basicValue))
}
@Test
fun fromSeparatorToSpacesTest() {
formatter.setSeparator(Separator.SPACES)
assertEquals("123 456.789", formatter.fromSeparator("123,456.789", Separator.COMMA))
assertEquals("123 456.789", formatter.fromSeparator("123 456.789", Separator.SPACES))
assertEquals("123 456.789", formatter.fromSeparator("123.456,789", Separator.PERIOD))
}
@Test
fun fromSeparatorToPeriodTest() {
formatter.setSeparator(Separator.PERIOD)
assertEquals("123.456,789", formatter.fromSeparator("123,456.789", Separator.COMMA))
assertEquals("123.456,789", formatter.fromSeparator("123 456.789", Separator.SPACES))
assertEquals("123.456,789", formatter.fromSeparator("123.456,789", Separator.PERIOD))
}
@Test
fun fromSeparatorToCommaTest() {
formatter.setSeparator(Separator.COMMA)
assertEquals("123,456.789", formatter.fromSeparator("123,456.789", Separator.COMMA))
assertEquals("123,456.789", formatter.fromSeparator("123 456.789", Separator.SPACES))
assertEquals("123,456.789", formatter.fromSeparator("123.456,789", Separator.PERIOD))
assertEquals("0.5", "0.5".formatTime())
assertEquals("0.05", "0.05".formatTime())
assertEquals("0.005", "0.005".formatTime())
assertEquals("28", "28".formatTime())
assertEquals("90", "90".formatTime())
assertEquals("90.5", "90.5".formatTime())
assertEquals("90.005", "90.005".formatTime())
}
}

View File

@ -59,6 +59,7 @@ class CalculatorHistoryRepository @Inject constructor(
private fun List<CalculatorHistoryEntity>.toHistoryItemList(): List<HistoryItem> {
return this.map {
HistoryItem(
id = it.entityId,
date = Date(it.timestamp),
expression = it.expression,
result = it.result

View File

@ -26,4 +26,5 @@ android {
dependencies {
implementation(project(mapOf("path" to ":core:base")))
testImplementation(libs.junit)
}

View File

@ -64,11 +64,7 @@ fun BigDecimal.setMinimumRequiredScale(prefScale: Int): BigDecimal {
/**
* 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()
}

View File

@ -18,6 +18,8 @@
package com.sadellie.unitto.data.common
import com.sadellie.unitto.core.base.Token
/**
* Compute Levenshtein Distance between this string and [secondString]. Doesn't matter which string is
* first.
@ -58,3 +60,18 @@ fun String.lev(secondString: String): Int {
return cost[this.length]
}
fun String.isExpression(): Boolean {
if (isEmpty()) return false
// Positive numbers and zero
if (all { it.toString() in Token.Digit.allWithDot }) return false
// Negative numbers
// Needs to start with an negative
if (this.first().toString() != Token.Operator.minus) return true
// Rest of the string must be just like positive
return this.drop(1).isExpression()
}

View File

@ -0,0 +1,49 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.common
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class IsExpressionText {
@Test
fun `empty string`() = assertEquals(false, "".isExpression())
@Test
fun `positive real number`() = assertEquals(false, "123".isExpression())
@Test
fun `positive float`() = assertEquals(false, "123.123".isExpression())
@Test
fun `negative real`() = assertEquals(false, "123".isExpression())
@Test
fun `negative float`() = assertEquals(false, "123.123".isExpression())
@Test
fun `super negative float`() = assertEquals(false, "123.123".isExpression())
@Test
fun expression1() = assertEquals(true, "123.123+456".isExpression())
@Test
fun expression2() = assertEquals(true, "123.123+456".isExpression())
}

View File

@ -18,8 +18,8 @@
package com.sadellie.unitto.data.epoch
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class DateToEpochTest {

View File

@ -18,17 +18,14 @@
plugins {
id("unitto.library")
id("unitto.android.hilt")
}
android {
namespace = "com.sadellie.unitto.data.unitgroups"
// Different namespace. Possible promotion to a separate project.
namespace = "io.github.sadellie.evaluatto"
}
dependencies {
testImplementation(libs.junit)
implementation(libs.org.burnoutcrew.composereorderable)
implementation(project(mapOf("path" to ":core:base")))
implementation(project(mapOf("path" to ":data:model")))
testImplementation(libs.junit)
}

View File

@ -0,0 +1,309 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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 io.github.sadellie.evaluatto
import com.sadellie.unitto.core.base.MAX_PRECISION
import com.sadellie.unitto.core.base.Token
import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.tan
import kotlin.math.acos
import kotlin.math.asin
import kotlin.math.atan
import kotlin.math.ln
import kotlin.math.log
import kotlin.math.exp
import kotlin.math.pow
sealed class ExpressionException(override val message: String): Exception(message) {
class DivideByZero : ExpressionException("Can't divide by zero")
class FactorialCalculation : ExpressionException("Can calculate factorial of non-negative real numbers only")
class BadExpression : ExpressionException("Invalid expression. Probably some operator lacks argument")
class TooBig : ExpressionException("Value is too big")
}
class Expression(input: String, private val radianMode: Boolean = true) {
private val tokens = Tokenizer(input).tokenize()
private var cursorPosition = 0
/**
* Expression := [ "-" ] Term { ("+" | "-") Term }
*
* Term := Factor { ( "*" | "/" ) Factor }
*
* Factor := RealNumber | "(" Expression ")"
*
* RealNumber := Digit{Digit} | [ Digit ] "." {Digit}
*
* Digit := "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
*/
fun calculate(): BigDecimal {
try {
return parseExpression()
} catch (e: UninitializedPropertyAccessException) {
throw ExpressionException.BadExpression()
}
}
// Null when at the end of expression
private fun peek() = tokens.getOrNull(cursorPosition) ?: ""
private fun moveIfMatched(token: String): Boolean {
if (peek() == token) {
// Move cursor
cursorPosition++
return true
}
return false
}
// Expression := [ "-" ] Term { ("+" | "-") Term }
private fun parseExpression(): BigDecimal {
var expression = parseTerm()
while (peek() in listOf(Token.Operator.plus, Token.Operator.minus)) {
when {
moveIfMatched(Token.Operator.plus) -> expression += parseTerm()
moveIfMatched(Token.Operator.minus) -> expression -= parseTerm()
}
}
return expression
}
// Term := Factor { ( "*" | "/" ) Factor }
private fun parseTerm(): BigDecimal {
var expression = parseFactor()
while (peek() in listOf(Token.Operator.multiply, Token.Operator.divide)) {
when {
moveIfMatched(Token.Operator.multiply) -> expression =
expression.multiply(parseFactor())
moveIfMatched(Token.Operator.divide) -> {
val divisor = parseFactor()
if (divisor.compareTo(BigDecimal.ZERO) == 0) throw ExpressionException.DivideByZero()
expression = expression.divide(divisor, RoundingMode.HALF_EVEN)
}
}
}
return expression
}
// Factor := RealNumber | "(" Expression ")"
private fun parseFactor(negative: Boolean = false): BigDecimal {
// This will throw Exception if some function lacks argument, for example: "cos()" or "600^"
lateinit var expr: BigDecimal
fun parseFuncParentheses(): BigDecimal {
return if (moveIfMatched(Token.Operator.leftBracket)) {
// Parse in parentheses
val res = parseExpression()
// Check if parentheses is closed
if (!moveIfMatched(Token.Operator.rightBracket)) throw Exception("Closing bracket is missing")
res
} else {
parseFactor()
}
}
// Unary plus
if (moveIfMatched(Token.Operator.plus)) return parseFactor()
// Unary minus
if (moveIfMatched(Token.Operator.minus)) {
return -parseFactor(true)
}
// Parentheses
if (moveIfMatched(Token.Operator.leftBracket)) {
// Parse in parentheses
expr = parseExpression()
// Check if parentheses is closed
if (!moveIfMatched(Token.Operator.rightBracket)) throw Exception("Closing bracket is missing")
}
// Numbers
val possibleNumber = peek()
// We know that if next token starts with a digit or dot, it can be converted into BigDecimal
// Ugly
if (possibleNumber.isNotEmpty()) {
if (Token.Digit.allWithDot.contains(possibleNumber.first().toString())) {
expr = BigDecimal(possibleNumber).setScale(MAX_PRECISION)
cursorPosition++
}
}
// PI
if (moveIfMatched(Token.Const.pi)) {
expr = BigDecimal.valueOf(Math.PI)
}
// e
if (moveIfMatched(Token.Const.e)) {
expr = BigDecimal.valueOf(Math.E)
}
// sqrt
if (moveIfMatched(Token.Operator.sqrt)) {
expr = parseFuncParentheses().pow(BigDecimal(0.5))
}
// sin
if (moveIfMatched(Token.Func.sin)) {
expr = parseFuncParentheses().sin(radianMode)
}
// cos
if (moveIfMatched(Token.Func.cos)) {
expr = parseFuncParentheses().cos(radianMode)
}
// tan
if (moveIfMatched(Token.Func.tan)) {
expr = parseFuncParentheses().tan(radianMode)
}
// arsin
if (moveIfMatched(Token.Func.arsin)) {
expr = parseFuncParentheses().arsin(radianMode)
}
// arcos
if (moveIfMatched(Token.Func.arcos)) {
expr = parseFuncParentheses().arcos(radianMode)
}
// actan
if (moveIfMatched(Token.Func.actan)) {
expr = parseFuncParentheses().artan(radianMode)
}
// ln
if (moveIfMatched(Token.Func.ln)) {
expr = parseFuncParentheses().ln()
}
// log
if (moveIfMatched(Token.Func.log)) {
expr = parseFuncParentheses().log()
}
// exp
if (moveIfMatched(Token.Func.exp)) {
expr = parseFuncParentheses().exp()
}
// Power
if (moveIfMatched(Token.Operator.power)) {
expr = expr.pow(parseFactor())
}
// Modulo
if (moveIfMatched(Token.Operator.modulo)) {
expr = expr.remainder(parseFactor())
}
// Factorial
if (moveIfMatched(Token.Operator.factorial)) {
if (negative) throw ExpressionException.FactorialCalculation()
expr = expr.factorial()
}
return expr
}
}
private fun BigDecimal.sin(radianMode: Boolean): BigDecimal {
val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
return sin(angle).toBigDecimal()
}
private fun BigDecimal.arsin(radianMode: Boolean): BigDecimal {
val angle: Double = asin(this.toDouble())
return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
}
private fun BigDecimal.cos(radianMode: Boolean): BigDecimal {
val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
return cos(angle).toBigDecimal()
}
private fun BigDecimal.arcos(radianMode: Boolean): BigDecimal {
val angle: Double = acos(this.toDouble())
return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
}
private fun BigDecimal.tan(radianMode: Boolean): BigDecimal {
val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
return tan(angle).toBigDecimal()
}
private fun BigDecimal.artan(radianMode: Boolean): BigDecimal {
val angle: Double = atan(this.toDouble())
return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
}
private fun BigDecimal.ln(): BigDecimal {
return ln(this.toDouble()).toBigDecimal()
}
private fun BigDecimal.log(): BigDecimal {
return log(this.toDouble(), 10.0).toBigDecimal()
}
private fun BigDecimal.exp(): BigDecimal {
return exp(this.toDouble()).toBigDecimal()
}
private fun BigDecimal.pow(n: BigDecimal): BigDecimal {
val mathContext: MathContext = MathContext.DECIMAL64
var right = n
val signOfRight = right.signum()
right = right.multiply(signOfRight.toBigDecimal())
val remainderOfRight = right.remainder(BigDecimal.ONE)
val n2IntPart = right.subtract(remainderOfRight)
val intPow = pow(n2IntPart.intValueExact(), mathContext)
val doublePow = BigDecimal(
toDouble().pow(remainderOfRight.toDouble())
)
var result = intPow.multiply(doublePow, mathContext)
if (signOfRight == -1) result =
BigDecimal.ONE.divide(result, mathContext.precision, RoundingMode.HALF_UP)
return result
}
private fun BigDecimal.factorial(): BigDecimal {
if (this.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) != 0) throw ExpressionException.FactorialCalculation()
if (this < BigDecimal.ZERO) throw ExpressionException.FactorialCalculation()
var expr = this
for (i in 1 until this.toInt()) {
expr *= BigDecimal(i)
}
return expr
}

View File

@ -0,0 +1,244 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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 io.github.sadellie.evaluatto
import com.sadellie.unitto.core.base.Token
sealed class TokenizerException(override val message: String) : Exception(message) {
class BadNumber : TokenizerException("Number has multiple commas in it")
}
class Tokenizer(private val streamOfTokens: String) {
// Don't create object at all?
fun tokenize(): List<String> {
var cursor = 0
val tokens: MutableList<String> = mutableListOf()
while (cursor != streamOfTokens.length) {
val nextToken = peekTokenAfter(cursor)
if (nextToken != null) {
tokens.add(nextToken)
cursor += nextToken.length
} else {
// Didn't find any token, move left slowly (by 1 symbol)
cursor++
}
}
return tokens.repairLexicon()
}
private fun peekTokenAfter(cursor: Int): String? {
Token.expressionTokens.forEach { token ->
val subs = streamOfTokens
.substring(
cursor,
(cursor + token.length).coerceAtMost(streamOfTokens.length)
)
if (subs == token) {
// Got a digit, see if there are other digits coming after
if (token in Token.Digit.allWithDot) {
val number = streamOfTokens
.substring(cursor)
.takeWhile { Token.Digit.allWithDot.contains(it.toString()) }
if (number.count { it.toString() == Token.Digit.dot } > 1) {
throw TokenizerException.BadNumber()
}
return number
}
return token
}
}
return null
}
private fun List<String>.repairLexicon(): List<String> {
return this
.missingClosingBrackets()
.missingMultiply()
.unpackAlPercents()
// input like 80%80% should be treated as 80%-80%.
// After unpacking we get (80/100)(80/100), the multiply is missing
// No, we can't unpack before fixing missing multiply.
// Ideally we we need to add missing multiply for 80%80%
// In that case unpackAlPercents gets input with all operators 80%*80% in this case
// Can't be done right now since missingMultiply checks for tokens in front only
.missingMultiply()
}
private fun List<String>.missingClosingBrackets(): List<String> {
val leftBracket = this.count { it == Token.Operator.leftBracket }
val rightBrackets = this.count { it == Token.Operator.rightBracket }
val neededBrackets = leftBracket - rightBrackets
if (neededBrackets <= 0) return this
var fixed = this
repeat(neededBrackets) {
fixed = fixed + Token.Operator.rightBracket
}
return fixed
}
private fun List<String>.missingMultiply(): List<String> {
val results = this.toMutableList()
val insertIndexes = mutableListOf<Int>()
// Records the index if it needs a multiply symbol
fun needsMultiply(index: Int) {
val tokenInFront = results.getOrNull(index - 1) ?: return
when {
tokenInFront.first().toString() in Token.Digit.allWithDot ||
tokenInFront == Token.Operator.rightBracket ||
tokenInFront in Token.Const.all -> {
// Can't add token now, it will modify tokens list (we are looping over it)
insertIndexes.add(index + insertIndexes.size)
}
}
}
results.forEachIndexed { index, s ->
when (s) {
Token.Operator.leftBracket,
Token.Operator.sqrt,
in Token.Const.all,
in Token.Func.all -> needsMultiply(index)
}
}
insertIndexes.forEach {
results.add(it, Token.Operator.multiply)
}
return results
}
private fun List<String>.unpackAlPercents(): List<String> {
var result = this
while (result.contains(Token.Operator.percent)) {
val percIndex = result.indexOf(Token.Operator.percent)
result = result.unpackPercentAt(percIndex)
}
return result
}
private fun List<String>.unpackPercentAt(percentIndex: Int): List<String> {
var cursor = percentIndex
// get whatever is the percentage
val percentage = this.getNumberOrExpressionBefore(percentIndex)
// Move cursor
cursor -= percentage.size
// get the operator in front
cursor -= 1
val operator = this.getOrNull(cursor)
// Don't go further
if ((operator == null) or (operator !in listOf(Token.Operator.plus, Token.Operator.minus))) {
val mutList = this.toMutableList()
// Remove percentage
mutList.removeAt(percentIndex)
//Add opening bracket before percentage
mutList.add(percentIndex - percentage.size, Token.Operator.leftBracket)
// Add "/ 100" and closing bracket
mutList.addAll(percentIndex + 1, listOf(Token.Operator.divide, "100", Token.Operator.rightBracket))
return mutList
}
// Get the base
val base = this.getBaseBefore(cursor)
val mutList = this.toMutableList()
// Remove percentage
mutList.removeAt(percentIndex)
//Add opening bracket before percentage
mutList.add(percentIndex - percentage.size, Token.Operator.leftBracket)
// Add "/ 100" and other stuff
mutList.addAll(
percentIndex + 1,
listOf(
Token.Operator.divide,
"100",
Token.Operator.multiply,
Token.Operator.leftBracket,
*base.toTypedArray(),
Token.Operator.rightBracket,
Token.Operator.rightBracket
)
)
return mutList
}
private fun List<String>.getNumberOrExpressionBefore(pos: Int): List<String> {
val digits = Token.Digit.allWithDot.map { it[0] }
val tokenInFront = this[pos - 1]
// Just number
if (tokenInFront.all { it in digits }) return listOf(tokenInFront)
// Not just a number. Probably expression in brackets.
if (tokenInFront != Token.Operator.rightBracket) throw Exception("Unexpected token before percentage")
// Start walking left until we get balanced brackets
var cursor = pos - 1
var leftBrackets = 0
var rightBrackets = 1 // We set 1 because we start with closing bracket
while (leftBrackets != rightBrackets) {
cursor--
val currentToken = this[cursor]
if (currentToken == Token.Operator.leftBracket) leftBrackets++
if (currentToken == Token.Operator.rightBracket) rightBrackets++
}
return this.subList(cursor, pos)
}
private fun List<String>.getBaseBefore(pos: Int): List<String> {
var cursor = pos
var leftBrackets = 0
var rightBrackets = 0
while ((--cursor >= 0)) {
val currentToken = this[cursor]
if (currentToken == Token.Operator.leftBracket) leftBrackets++
if (currentToken == Token.Operator.rightBracket) rightBrackets++
if (leftBrackets > rightBrackets) break
}
// Return cursor back to last token
cursor += 1
return this.subList(cursor, pos)
}
}

View File

@ -0,0 +1,60 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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 io.github.sadellie.evaluatto
import org.junit.jupiter.api.Test
class ExpressionComplexTest {
@Test
fun expression1() = assertExpr("94×π×89×cos(0.5)3!÷9^(2)×√8", "23064.9104578494")
@Test
fun expression2() = assertExpr("√(25)×2+10÷2", "15")
@Test
fun expression3() = assertExpr("(3+4)×(52)", "21")
@Test
fun expression4() = assertExpr("8÷4+2×3", "8")
@Test
fun expression5() = assertExpr("2^3+4^25×6", "-6")
@Test
fun expression6() = assertExpr("(102)^2÷8+3×2", "14")
@Test
fun expression7() = assertExpr("7!÷3!5!÷2!", "780")
@Test
fun expression8() = assertExpr("(2^2+3^3)÷5√(16)×2", "-1.8")
@Test
fun expression9() = assertExpr("10×log(100)+2^43^2", "27")
@Test
fun expression10() = assertExpr("sin(π÷3)×cos(π÷6)+tan(π÷4)√3", "0.017949192431123")
@Test
fun expression11() = assertExpr("2^62^5+2^42^3+2^2^1+2^0", "41.25")
@Test
fun expression12() = assertExpr("2×(3+4)×(52)÷6", "7")
}

View File

@ -0,0 +1,42 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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 io.github.sadellie.evaluatto
import org.junit.jupiter.api.Test
class ExpressionExceptionsTest {
@Test
fun `divide by zero`() = assertExprFail(ExpressionException.DivideByZero::class.java, "2÷0")
@Test
fun `factorial of float`() = assertExprFail(ExpressionException.FactorialCalculation::class.java, "3.2!")
@Test
fun `factorial of negative`() = assertExprFail(ExpressionException.FactorialCalculation::class.java, "5!")
@Test
fun `factorial of negative2`() = assertExprFail(ExpressionException.FactorialCalculation::class.java, "(5)!")
@Test
fun `ugly ahh expression`() = assertExprFail(ExpressionException.BadExpression::class.java, "100+cos()")
@Test
fun `ugly ahh expression2`() = assertExprFail(TokenizerException.BadNumber::class.java, "...")
}

View File

@ -0,0 +1,111 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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 io.github.sadellie.evaluatto
import org.junit.jupiter.api.Test
class ExpressionSimpleTest {
@Test
fun expression1() = assertExpr("789", "789")
@Test
fun expression2() = assertExpr("0.1+0.2", "0.3")
@Test
fun expression3() = assertExpr(".1+.2", "0.3")
@Test
fun expression4() = assertExpr("789+200", "989")
@Test
fun expression5() = assertExpr("600×7.89", "4734")
@Test
fun expression6() = assertExpr("600÷7", "85.7142857143")
@Test
fun expression7() = assertExpr("(200+200)×200", "80000")
@Test
fun expression8() = assertExpr("99^5", "9509900499")
@Test
fun expression9() = assertExpr("12!", "479001600")
@Test
fun expression10() = assertExpr("12#5", "2")
@Test
fun `125 plus 9 percent`() = assertExpr("125+9%", "136.25")
@Test
fun expression11() = assertExpr("12×√5", "26.8328157300")
@Test
fun expression12() = assertExpr("sin(42)", "-0.9165215479")
@Test
fun expression13() = assertExpr("sin(42)", "0.6691306064", radianMode = false)
@Test
fun expression14() = assertExpr("cos(42)", "-0.3999853150")
@Test
fun expression15() = assertExpr("cos(42)", "0.7431448255", radianMode = false)
@Test
fun expression16() = assertExpr("tan(42)", "2.2913879924")
@Test
fun expression17() = assertExpr("tan(42)", "0.9004040443", radianMode = false)
@Test
fun expression18() = assertExpr("sin⁻¹(.69)", "0.7614890527")
@Test
fun expression19() = assertExpr("sin⁻¹(.69)", "43.6301088679", radianMode = false)
@Test
fun expression20() = assertExpr("cos⁻¹(.69)", "0.8093072740")
@Test
fun expression21() = assertExpr("cos⁻¹(.69)", "46.3698911321", radianMode = false)
@Test
fun expression22() = assertExpr("tan⁻¹(.69)", "0.6039829783")
@Test
fun expression23() = assertExpr("tan⁻¹(.69)", "34.6056755516", radianMode = false)
@Test
fun expression24() = assertExpr("ln(.69)", "-0.3710636814")
@Test
fun expression25() = assertExpr("log(.69)", "-0.1611509093")
@Test
fun expression26() = assertExpr("exp(3)", "20.0855369232")
@Test
fun expression27() = assertExpr("π", "3.1415926536")
@Test
fun expression28() = assertExpr("e", "2.7182818285")
}

View File

@ -0,0 +1,129 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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 io.github.sadellie.evaluatto
import org.junit.jupiter.api.Test
class FixLexiconTest {
@Test
fun `missing multiply`() {
assertLex(
"2×(69420)", "2(69420)"
)
assertLex(
"0.×(69420)", "0.(69420)"
)
assertLex(
".0×(69420)", ".0(69420)"
)
assertLex(
".×(69420)", ".(69420)"
)
assertLex(
"2×(69420)×(234)×cos(9)×tan((sin⁻¹(.9)))",
"2(69420)(234)cos(9)tan((sin⁻¹(.9)))"
)
assertLex(
"e×e+π", "ee+π"
)
}
@Test
fun `balanced brackets`() {
assertLex(
"123×(12+4)", "123(12+4"
)
assertLex(
"12312+4", "12312+4"
)
assertLex(
"123)))12+4", "123)))12+4"
)
assertLex(
"sin(cos(tan(3)))", "sin(cos(tan(3"
)
assertLex(
"sin(cos(tan(3)))", "sin(cos(tan(3)"
)
assertLex(
"sin(cos(tan(3)))", "sin(cos(tan(3))"
)
}
@Test
fun `unpack percentage`() {
// 132.5+14% > 132.5+132.5*0.14
assertLex(
"132.5+(14÷100×(132.5))", "132.5+14%"
)
// 132.5+(14)% > 132.5+(14)/100*132.5
assertLex(
"132.5+((14)÷100×(132.5))" , "132.5+(14)%"
)
// 132.5+(15+4)% > 132.5+(15+4)*132.5/100
assertLex(
"132.5+((15+4)÷100×(132.5))", "132.5+(15+4)%"
)
// (132.5+12%)+(15+4)% > (132.5+12/100*132.5)+(15+4)/100*(132.5+12/100*132.5)
assertLex(
"(132.5+(12÷100×(132.5)))+((15+4)÷100×((132.5+(12÷100×(132.5)))))", "(132.5+12%)+(15+4)%"
)
// 2% > 2/100
assertLex(
"(2÷100)", "2%"
)
assertLex(
"((2)÷100)", "(2)%"
)
assertLex(
"(132.5+5)+(90÷100×((132.5+5)))", "(132.5+5)+90%"
)
assertLex(
"((90÷100)+(90÷100×((90÷100))))", "(90%+90%)"
)
assertLex(
"((90÷100)÷(90÷100))+((90÷100)(90÷100×((90÷100))))", "(90%÷90%)+(90%90%)"
)
assertLex("(80÷100)×(80÷100)", "80%80%")
assertLex("10+(2.0÷100×(10))", "10+2.0%")
assertLex("10+(2.÷100×(10))", "10+2.%")
}
}

View File

@ -0,0 +1,46 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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 io.github.sadellie.evaluatto
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import java.math.BigDecimal
import java.math.RoundingMode
fun assertExpr(expr: String, result: String, radianMode: Boolean = true) =
assertEquals(
BigDecimal(result).setScale(10, RoundingMode.HALF_EVEN),
Expression(expr, radianMode).calculate().setScale(10, RoundingMode.HALF_EVEN)
)
fun <T : Throwable?> assertExprFail(
expectedThrowable: Class<T>?,
expr: String,
radianMode: Boolean = true
) {
assertThrows(expectedThrowable) {
Expression(expr, radianMode = radianMode).calculate()
}
}
fun assertLex(expected: List<String>, actual: String) =
assertEquals(expected, Tokenizer(actual).tokenize())
fun assertLex(expected: String, actual: String) =
assertEquals(expected, Tokenizer(actual).tokenize().joinToString(""))

View File

@ -0,0 +1,51 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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 io.github.sadellie.evaluatto
import org.junit.jupiter.api.Test
class TokenizerTest {
@Test
fun tokens1() = assertLex(listOf("789"), "789")
@Test
fun tokens2() = assertLex(listOf("789", "+", "200"), "789+200")
@Test
fun tokens3() = assertLex(listOf("0.1", "+", "0.2"), "0.1+0.2")
@Test
fun tokens4() = assertLex(listOf(".1", "+", ".2"), ".1+.2")
@Test
fun tokens5() = assertLex(listOf(".1", "+", ".2"), ".1+.2")
@Test
fun tokens6() = assertLex(listOf("789", "+", "200", "+", "cos", "(", "456", ")"), "789+200+cos(456)")
@Test
fun tokens8() = assertLex(emptyList(), "")
@Test
fun tokens9() = assertLex(listOf("e"), "something") // Tokenizer knows "e"
@Test
fun tokens10() = assertLex(emptyList(), "funnyword")
}

View File

@ -28,23 +28,6 @@ data class AppLibrary(
val ALL_LIBRARIES by lazy {
listOf(
AppLibrary(
name = "MathParser.org-mXparser",
dev = "Mariusz Gromada",
website = "https://github.com/mariuszgromada/MathParser.org-mXparser/",
license = "Non-Commercial license",
description = "Math Parser Java Android C# .NET/MONO (.NET Framework, .NET Core, .NET " +
"Standard, .NET PCL, Xamarin.Android, Xamarin.iOS) CLS Library - a super easy, rich" +
" and flexible mathematical expression parser (expression evaluator, expression " +
"provided as plain text / strings) for JAVA and C#."
),
AppLibrary(
name = "ExprK",
dev = "Keelar",
website = "https://github.com/Keelar/ExprK",
license = "MIT license",
description = "A simple mathematical expression evaluator for Kotlin and Java, written in Kotlin."
),
AppLibrary(
name = "currency-api",
dev = "Fawaz Ahmed (fawazahmed0)",
@ -122,20 +105,6 @@ val ALL_LIBRARIES by lazy {
license = "Apache-2.0",
description = "Utilities for Jetpack Compose"
),
AppLibrary(
name = "firebase-analytics-ktx",
dev = "Google",
website = "https://developer.android.com/studio/terms.html",
license = "ASDKL",
description = "Library to collect and send usage statistics"
),
AppLibrary(
name = "firebase-crashlytics-ktx",
dev = "Google",
website = "https://developer.android.com/studio/terms.html",
license = "Apache-2.0",
description = "Library to collect and send crash logs"
),
AppLibrary(
name = "Compose Tooling API",
dev = "The Android Open Source Project",

View File

@ -37,7 +37,6 @@ import java.math.BigDecimal
* @property renderedShortName Used as cache. Stores short name string for this specific device. Need for
* search functionality.
* @property isFavorite Whether this unit is favorite.
* @property isEnabled Whether we need to show this unit or not
* @property pairedUnit Latest paired unit on the right
* @property counter The amount of time this unit was chosen
*/
@ -50,7 +49,6 @@ abstract class AbstractUnit(
var renderedName: String = String(),
var renderedShortName: String = String(),
var isFavorite: Boolean = false,
var isEnabled: Boolean = true,
var pairedUnit: String? = null,
var counter: Int = 0
) {

View File

@ -50,6 +50,9 @@ class DefaultUnit(
value: BigDecimal,
scale: Int
): BigDecimal {
// Avoid division by zero
if (unitTo.basicUnit.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO
return this
.basicUnit
.setScale(MAX_PRECISION)

View File

@ -18,9 +18,10 @@
package com.sadellie.unitto.data.model
import java.util.*
import java.util.Date
data class HistoryItem(
val id: Int,
val date: Date,
val expression: String,
val result: String

View File

@ -19,6 +19,7 @@
package com.sadellie.unitto.data.model
import androidx.annotation.StringRes
import com.sadellie.unitto.core.base.R
val ALL_UNIT_GROUPS: List<UnitGroup> by lazy {
UnitGroup.values().toList()

View File

@ -1,5 +1,15 @@
-repackageclasses
# https://github.com/square/retrofit/issues/3751#issuecomment-1192043644
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-keepclassmembers class ** {
@com.squareup.moshi.FromJson *;
@com.squareup.moshi.ToJson *;

View File

@ -48,6 +48,9 @@ import com.sadellie.unitto.data.units.collections.temperatureCollection
import com.sadellie.unitto.data.units.collections.timeCollection
import com.sadellie.unitto.data.units.collections.torqueCollection
import com.sadellie.unitto.data.units.collections.volumeCollection
import com.sadellie.unitto.data.units.remote.CurrencyApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import javax.inject.Inject
import javax.inject.Singleton
@ -123,8 +126,8 @@ class AllUnitsRepository @Inject constructor() {
/**
* Filter [AllUnitsRepository.allUnits] and group them.
*
* @param hideBrokenCurrencies When set to True will remove [AbstractUnit]s that have
* [AbstractUnit.isEnabled] set to False, which means that [AbstractUnit] can not be used.
* @param hideBrokenUnits When set to True will remove [AbstractUnit]s that have
* [AbstractUnit.basicUnit] set to [BigDecimal.ZERO] (comes from currencies API).
* @param chosenUnitGroup If provided will scope list to a specific [UnitGroup].
* @param favoritesOnly When True will filter only [AbstractUnit]s with [AbstractUnit.isFavorite]
* set to True.
@ -135,7 +138,7 @@ class AllUnitsRepository @Inject constructor() {
* @return Grouped by [UnitGroup] list of [AbstractUnit]s.
*/
fun filterUnits(
hideBrokenCurrencies: Boolean,
hideBrokenUnits: Boolean,
chosenUnitGroup: UnitGroup?,
favoritesOnly: Boolean,
searchQuery: String,
@ -153,8 +156,8 @@ class AllUnitsRepository @Inject constructor() {
if (favoritesOnly) {
units = units.filter { it.isFavorite }
}
if (hideBrokenCurrencies) {
units = units.filter { it.isEnabled }
if (hideBrokenUnits) {
units = units.filter { it.basicUnit > BigDecimal.ZERO }
}
units = when (sorting) {
@ -198,22 +201,20 @@ class AllUnitsRepository @Inject constructor() {
/**
* Update [AbstractUnit.basicUnit] properties for currencies from [currencyCollection].
*
* @param conversions Map: [AbstractUnit.unitId] and [BigDecimal] that will replace current
* [AbstractUnit.basicUnit].
* @param unitFrom Base unit
*/
fun updateBasicUnitsForCurrencies(
conversions: Map<String, BigDecimal>
) {
suspend fun updateBasicUnitsForCurrencies(
unitFrom: AbstractUnit
) = withContext(Dispatchers.IO) {
val conversions: Map<String, BigDecimal> = CurrencyApi.retrofitService.getCurrencyPairs(unitFrom.unitId).currency
getCollectionByGroup(UnitGroup.CURRENCY).forEach {
// Getting rates from map. We set ZERO as default so that it can be skipped
val rate = conversions.getOrElse(it.unitId) { BigDecimal.ZERO }
// We make sure that we don't divide by zero
if (rate > BigDecimal.ZERO) {
it.isEnabled = true
it.basicUnit = BigDecimal.ONE.setScale(MAX_PRECISION).div(rate)
} else {
// Hiding broken currencies
it.isEnabled = false
it.basicUnit = BigDecimal.ZERO
}
}
}

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val accelerationCollection: List<AbstractUnit> by lazy {

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val angleCollection: List<AbstractUnit> by lazy {

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val areaCollection: List<AbstractUnit> by lazy {

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val electrostaticCapacitance: List<AbstractUnit> by lazy {

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val currencyCollection: List<AbstractUnit> by lazy {

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val dataCollection: List<AbstractUnit> by lazy {

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val dataTransferCollection: List<AbstractUnit> by lazy {

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val energyCollection: List<AbstractUnit> by lazy {

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.FlowRateUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
val flowRateCollection: List<AbstractUnit> by lazy {

View File

@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val fluxCollection: List<AbstractUnit> by lazy {

Some files were not shown because too many files have changed in this diff Show More