From e1dd564063f5c57b7dabc0a54c6ac0827a0402e8 Mon Sep 17 00:00:00 2001 From: Zakir Ahmad Sheikh Date: Mon, 29 Aug 2022 18:58:56 +0530 Subject: [PATCH 1/2] * Updated Libraries --- app/build.gradle | 8 +- app/src/main/java/com/prime/toolz2/App.kt | 36 - .../java/com/prime/toolz2/MainActivity.kt | 335 ---- app/src/main/java/com/prime/toolz2/Theme.kt | 252 --- .../main/java/com/prime/toolz2/common/Util.kt | 25 - .../prime/toolz2/common/billing/Advertiser.kt | 4 - .../toolz2/common/billing/BillingManager.kt | 378 ---- .../prime/toolz2/common/billing/Security.java | 126 -- .../prime/toolz2/common/compose/Compose.kt | 13 - .../prime/toolz2/common/compose/Snackbar.kt | 172 -- .../com/prime/toolz2/common/compose/Util.kt | 268 --- .../prime/toolz2/common/compose/WindowSize.kt | 84 - .../com/prime/toolz2/core/converter/Angle.kt | 47 - .../com/prime/toolz2/core/converter/Area.kt | 120 -- .../prime/toolz2/core/converter/Converter.kt | 227 --- .../com/prime/toolz2/core/converter/Data.kt | 13 - .../com/prime/toolz2/core/converter/Energy.kt | 88 - .../com/prime/toolz2/core/converter/Length.kt | 148 -- .../com/prime/toolz2/core/converter/Mass.kt | 158 -- .../com/prime/toolz2/core/converter/Power.kt | 76 - .../prime/toolz2/core/converter/Pressure.kt | 94 - .../com/prime/toolz2/core/converter/Speed.kt | 104 -- .../toolz2/core/converter/Temperature.kt | 143 -- .../com/prime/toolz2/core/converter/Time.kt | 129 -- .../com/prime/toolz2/core/converter/Volume.kt | 13 - .../toolz2/core/math/BoundedRational.java | 564 ------ .../prime/toolz2/core/math/StringUtils.java | 94 - .../prime/toolz2/core/math/UnifiedReal.java | 1287 ------------- .../java/com/prime/toolz2/core/math/Util.java | 162 -- .../com/prime/toolz2/core/math/creals/CR.java | 1646 ----------------- .../core/math/creals/StringFloatRep.java | 73 - .../core/math/creals/UnaryCRFunction.java | 667 ------- .../java/com/prime/toolz2/core/math/more.kt | 207 --- .../com/prime/toolz2/settings/FontFamily.kt | 21 - .../com/prime/toolz2/settings/GlobalKeys.kt | 87 - .../com/prime/toolz2/settings/NightMode.kt | 49 - .../com/prime/toolz2/settings/Settings.kt | 313 ---- .../toolz2/settings/SettingsViewModel.kt | 173 -- app/src/main/java/com/prime/toolz2/ui/Home.kt | 97 - .../toolz2/ui/converter/UnitConverter.kt | 706 ------- .../ui/converter/UnitConverterViewModel.kt | 282 --- 41 files changed, 4 insertions(+), 9485 deletions(-) delete mode 100644 app/src/main/java/com/prime/toolz2/App.kt delete mode 100644 app/src/main/java/com/prime/toolz2/MainActivity.kt delete mode 100644 app/src/main/java/com/prime/toolz2/Theme.kt delete mode 100644 app/src/main/java/com/prime/toolz2/common/Util.kt delete mode 100644 app/src/main/java/com/prime/toolz2/common/billing/Advertiser.kt delete mode 100644 app/src/main/java/com/prime/toolz2/common/billing/BillingManager.kt delete mode 100644 app/src/main/java/com/prime/toolz2/common/billing/Security.java delete mode 100644 app/src/main/java/com/prime/toolz2/common/compose/Compose.kt delete mode 100644 app/src/main/java/com/prime/toolz2/common/compose/Snackbar.kt delete mode 100644 app/src/main/java/com/prime/toolz2/common/compose/Util.kt delete mode 100644 app/src/main/java/com/prime/toolz2/common/compose/WindowSize.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Angle.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Area.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Converter.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Data.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Energy.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Length.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Mass.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Power.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Pressure.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Speed.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Temperature.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Time.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Volume.kt delete mode 100644 app/src/main/java/com/prime/toolz2/core/math/BoundedRational.java delete mode 100644 app/src/main/java/com/prime/toolz2/core/math/StringUtils.java delete mode 100644 app/src/main/java/com/prime/toolz2/core/math/UnifiedReal.java delete mode 100644 app/src/main/java/com/prime/toolz2/core/math/Util.java delete mode 100644 app/src/main/java/com/prime/toolz2/core/math/creals/CR.java delete mode 100644 app/src/main/java/com/prime/toolz2/core/math/creals/StringFloatRep.java delete mode 100644 app/src/main/java/com/prime/toolz2/core/math/creals/UnaryCRFunction.java delete mode 100644 app/src/main/java/com/prime/toolz2/core/math/more.kt delete mode 100644 app/src/main/java/com/prime/toolz2/settings/FontFamily.kt delete mode 100644 app/src/main/java/com/prime/toolz2/settings/GlobalKeys.kt delete mode 100644 app/src/main/java/com/prime/toolz2/settings/NightMode.kt delete mode 100644 app/src/main/java/com/prime/toolz2/settings/Settings.kt delete mode 100644 app/src/main/java/com/prime/toolz2/settings/SettingsViewModel.kt delete mode 100644 app/src/main/java/com/prime/toolz2/ui/Home.kt delete mode 100644 app/src/main/java/com/prime/toolz2/ui/converter/UnitConverter.kt delete mode 100644 app/src/main/java/com/prime/toolz2/ui/converter/UnitConverterViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index aa393bf..7ec4c98 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.8.0' //implementation 'com.google.android.material:material:1.6.1' - def compose_version = '1.3.0-alpha03' + def compose_version = '1.3.0-beta01' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" @@ -115,8 +115,8 @@ dependencies { implementation "com.airbnb.android:lottie-compose:$lottieVersion" //Preferences and other widgets - def accomplice_version = '1.0.0-alpha07' - implementation "com.github.prime-zs:accomplice:$accomplice_version" + def support_version = '1.0.0-alpha09' + implementation "com.github.prime-zs:support:$support_version" // Splash Screen API implementation 'androidx.core:core-splashscreen:1.0.0' @@ -128,7 +128,7 @@ dependencies { implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version" //kotlin-serialization - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0' //hilt implementation "com.google.dagger:hilt-android:$hilt_version" diff --git a/app/src/main/java/com/prime/toolz2/App.kt b/app/src/main/java/com/prime/toolz2/App.kt deleted file mode 100644 index acd421b..0000000 --- a/app/src/main/java/com/prime/toolz2/App.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.prime.toolz2 - -import android.app.Application -import androidx.compose.material.MaterialTheme -import dagger.hilt.android.HiltAndroidApp -import android.content.Context -import androidx.compose.runtime.compositionLocalOf -import com.google.android.gms.tasks.Task -import com.google.android.play.core.appupdate.AppUpdateInfo -import com.google.android.play.core.appupdate.AppUpdateManager -import com.google.android.play.core.appupdate.AppUpdateManagerFactory -import com.google.android.play.core.install.model.AppUpdateType -import com.google.android.play.core.install.model.UpdateAvailability -import com.primex.preferences.Preferences -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@HiltAndroidApp -class App : Application() - - - -@Module -@InstallIn(SingletonComponent::class) -object AppModule { - /** - * Provides the Singleton Implementation of Preferences DataStore. - */ - @Provides - @Singleton - fun preferences(@ApplicationContext context: Context) = Preferences(context) -} diff --git a/app/src/main/java/com/prime/toolz2/MainActivity.kt b/app/src/main/java/com/prime/toolz2/MainActivity.kt deleted file mode 100644 index ce903f2..0000000 --- a/app/src/main/java/com/prime/toolz2/MainActivity.kt +++ /dev/null @@ -1,335 +0,0 @@ -package com.prime.toolz2 - -import LocalWindowSizeClass -import android.animation.ObjectAnimator -import android.app.Activity -import android.os.Bundle -import android.util.Log -import android.view.View -import android.view.animation.AnticipateInterpolator -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.dp -import androidx.core.animation.doOnEnd -import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat -import androidx.lifecycle.lifecycleScope -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.google.android.play.core.appupdate.AppUpdateManagerFactory -import com.google.android.play.core.ktx.AppUpdateResult -import com.google.android.play.core.ktx.requestAppUpdateInfo -import com.google.android.play.core.ktx.requestReview -import com.google.android.play.core.ktx.requestUpdateFlow -import com.google.android.play.core.review.ReviewManagerFactory -import com.google.firebase.analytics.FirebaseAnalytics -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase -import com.prime.toolz2.common.billing.BillingManager -import com.prime.toolz2.common.compose.* -import com.prime.toolz2.settings.GlobalKeys -import com.prime.toolz2.settings.NightMode -import com.prime.toolz2.ui.Home -import com.primex.core.rememberState -import com.primex.preferences.LocalPreferenceStore -import com.primex.preferences.Preferences -import com.primex.preferences.longPreferenceKey -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch -import rememberWindowSizeClass -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -private const val TAG = "MainActivity" - -private const val RESULT_CODE_APP_UPDATE = 1000 - -@AndroidEntryPoint -class MainActivity : ComponentActivity() { - lateinit var fAnalytics: FirebaseAnalytics - - @Inject - lateinit var preferences: Preferences - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // The app has started from scratch if savedInstanceState is null. - val isColdStart = savedInstanceState == null //why? - // Obtain the FirebaseAnalytics instance. - fAnalytics = Firebase.analytics - // show splash screen - initSplashScreen( - isColdStart - ) - //init - val channel = SnackDataChannel() - if (isColdStart) { - val counter = - with(preferences) { preferences[GlobalKeys.KEY_LAUNCH_COUNTER].obtain() } ?: 0 - // update launch counter if - // cold start. - preferences[GlobalKeys.KEY_LAUNCH_COUNTER] = counter + 1 - // check for updates on startup - // don't report - // check silently - launchUpdateFlow(channel) - // TODO: Try to reconcile if it is any good to ask for reviews here. - // launchReviewFlow() - } - WindowCompat.setDecorFitsSystemWindows(window, false) - // setup billing manager - - val billingManager = - BillingManager( - context = this, - products = arrayOf( - BillingTokens.DISABLE_ASD_IN_APP_PRODUCT - ) - ) - lifecycle.addObserver(billingManager) - - setContent { - val sWindow = rememberWindowSizeClass() - - // observe the change to density - val density = LocalDensity.current - val fontScale by with(preferences) { get(GlobalKeys.FONT_SCALE).observeAsState() } - val modified = Density(density = density.density, fontScale = fontScale) - // The state of the Snackbar - val snackbar = remember(::SnackbarHostState) - // observe the channel - // emit the updates - val resource = LocalContext.current.resources - LaunchedEffect(key1 = channel) { - channel.receiveAsFlow().collect { (label, message, duration, action) -> - // dismantle the given snack and use the corresponding components - val result = snackbar.showSnackbar( - message = resource.stringResource(message).text, - actionLabel = resource.stringResource(label)?.text - ?: resource.getString(R.string.dismiss), - duration = duration - ) - // action based on - when (result) { - SnackbarResult.ActionPerformed -> action?.invoke() - SnackbarResult.Dismissed -> { /*do nothing*/ - } - } - } - } - var windowPadding by rememberState(initial = PaddingValues(0.dp)) - CompositionLocalProvider( - LocalWindowPadding provides windowPadding, - LocalElevationOverlay provides null, - LocalWindowSizeClass provides sWindow, - LocalPreferenceStore provides preferences, - LocalSystemUiController provides rememberSystemUiController(), - LocalDensity provides modified, - LocalSnackDataChannel provides channel, - LocalBillingManager provides billingManager - ) { - Material(isDark = resolveAppThemeState()) { - val state = rememberScaffoldState(snackbarHostState = snackbar) - // scaffold - // FixMe: Re-design the SnackBar Api. - // Introduce: SideBar and Bottom Bar. - Scaffold(scaffoldState = state) { inner -> - windowPadding = inner - Home() - } - } - } - } - } -} - -/** - * Manages SplashScreen - */ -private fun MainActivity.initSplashScreen(isColdStart: Boolean) { - // Install Splash Screen and Play animation when cold start. - installSplashScreen() - .let { splashScreen -> - // Animate entry of content - // if cold start - if (isColdStart) - splashScreen.setOnExitAnimationListener { splashScreenViewProvider -> - val splashScreenView = splashScreenViewProvider.view - // Create your custom animation. - val alpha = ObjectAnimator.ofFloat( - splashScreenView, - View.ALPHA, - 1f, - 0f - ) - alpha.interpolator = AnticipateInterpolator() - alpha.duration = 700L - - // Call SplashScreenView.remove at the end of your custom animation. - alpha.doOnEnd { splashScreenViewProvider.remove() } - - // Run your animation. - alpha.start() - } - } -} - -@Composable -private fun resolveAppThemeState(): Boolean { - val preferences = LocalPreferenceStore.current - val mode by with(preferences) { - preferences[GlobalKeys.NIGHT_MODE].observeAsState() - } - return when (mode) { - NightMode.YES -> true - else -> false - } -} - -private val KEY_LAST_REVIEW_TIME = - longPreferenceKey( - TAG + "_last_review_time" - ) - -private const val MIN_LAUNCH_COUNT = 20 -private val MAX_DAYS_BEFORE_FIRST_REVIEW = TimeUnit.DAYS.toMillis(7) -private val MAX_DAY_AFTER_FIRST_REVIEW = TimeUnit.DAYS.toMillis(10) - -/** - * A convince method for launching an in-app review. - * The review API is guarded by some conditions which are - * * The first review will be asked when launchCount is > [MIN_LAUNCH_COUNT] and daysPassed >=[MAX_DAYS_BEFORE_FIRST_REVIEW] - * * After asking first review then after each [MAX_DAY_AFTER_FIRST_REVIEW] a review dialog will be showed. - * Note: The review should not be asked after every coldBoot. - */ -fun Activity.launchReviewFlow() { - require(this is MainActivity) - val count = - with(preferences) { preferences[GlobalKeys.KEY_LAUNCH_COUNTER].obtain() } ?: 0 - - // the time when lastly asked for review - val lastAskedTime = - with(preferences) { preferences[KEY_LAST_REVIEW_TIME].obtain() } - - val firstInstallTime = - com.primex.core.runCatching(TAG + "_review") { - packageManager.getPackageInfo(packageName, 0).firstInstallTime - } - - val currentTime = System.currentTimeMillis() - - // Only first time we should not ask immediately - // however other than this whenever we do some thing of appreciation. - // we should ask for review. - val askFirstReview = - lastAskedTime == null && - firstInstallTime != null && - count >= MIN_LAUNCH_COUNT && - currentTime - firstInstallTime >= MAX_DAYS_BEFORE_FIRST_REVIEW - - val askNormalOne = - lastAskedTime != null && - count >= MIN_LAUNCH_COUNT && - currentTime - lastAskedTime >= MAX_DAY_AFTER_FIRST_REVIEW - - // The flow has finished. The API does not indicate whether the user - // reviewed or not, or even whether the review dialog was shown. Thus, no - // matter the result, we continue our app flow. - lifecycleScope.launch { - if (askFirstReview || askNormalOne) { - val reviewManager = ReviewManagerFactory.create(this@launchReviewFlow) - com.primex.core.runCatching(TAG) { - // update the last asking - preferences[KEY_LAST_REVIEW_TIME] = System.currentTimeMillis() - val info = reviewManager.requestReview() - reviewManager.launchReviewFlow(this@launchReviewFlow, info) - //host.fAnalytics. - } - } - } -} - -private const val FLEXIBLE_UPDATE_MAX_STALENESS_DAYS = 2 - -/** - * A utility method to check for updates. - * @param channel [SnackDataChannel] to report errors, inform users about the availability of update. - * @param report simple messages. - */ -fun Activity.launchUpdateFlow( - channel: SnackDataChannel, - report: Boolean = false, -) { - require(this is MainActivity) - lifecycleScope.launch { - val manager = AppUpdateManagerFactory.create(this@launchUpdateFlow) - with(manager) { - val result = - kotlin.runCatching { - requestUpdateFlow() - .collect { result -> - when (result) { - AppUpdateResult.NotAvailable -> if (report) - channel.send("The app is already updated to the latest version.") - is AppUpdateResult.InProgress -> { - //FixMe: Publish progress - val state = result.installState - val progress = - state.bytesDownloaded() / (state.totalBytesToDownload() + 0.1) * 100 - Log.i(TAG, "check: $progress") - // currently don't show any message - // future version find ways to show progress. - } - is AppUpdateResult.Downloaded -> { - val info = requestAppUpdateInfo() - //when update first becomes available - //don't force it. - // make it required when staleness days overcome allowed limit - val isFlexible = - (info.clientVersionStalenessDays() ?: -1) <= - FLEXIBLE_UPDATE_MAX_STALENESS_DAYS - - // forcefully update; if it's flexible - if (!isFlexible) - completeUpdate() - else - // ask gracefully - channel.send( - message = "An update has just been downloaded.", - label = "RESTART", - action = this::completeUpdate, - duration = SnackbarDuration.Indefinite - ) - // no message needs to be shown - } - is AppUpdateResult.Available -> { - // if user choose to skip the update handle that case also. - val isFlexible = - (result.updateInfo.clientVersionStalenessDays() ?: -1) <= - FLEXIBLE_UPDATE_MAX_STALENESS_DAYS - if (isFlexible) - result.startFlexibleUpdate( - activity = this@launchUpdateFlow, - RESULT_CODE_APP_UPDATE - ) - else - result.startImmediateUpdate( - activity = this@launchUpdateFlow, - RESULT_CODE_APP_UPDATE - ) - // no message needs to be shown - } - } - } - } - Log.d(TAG, "launchUpdateFlow() returned: $result") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/Theme.kt b/app/src/main/java/com/prime/toolz2/Theme.kt deleted file mode 100644 index c7b9af3..0000000 --- a/app/src/main/java/com/prime/toolz2/Theme.kt +++ /dev/null @@ -1,252 +0,0 @@ -package com.prime.toolz2 - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.material.MaterialTheme.colors -import androidx.compose.runtime.* -import androidx.compose.ui.graphics.* -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.prime.toolz2.common.compose.LongDurationMills -import com.prime.toolz2.settings.FontFamily -import com.prime.toolz2.settings.GlobalKeys -import com.primex.core.hsl -import com.primex.preferences.LocalPreferenceStore -import com.primex.ui.* -import kotlinx.coroutines.flow.map -import androidx.compose.ui.text.font.FontFamily as AndroidFontFamily - -private const val TAG = "Theme" - -typealias Material = MaterialTheme - -/** - * An Extra font family. - */ -private val ProvidedFontFamily = - AndroidFontFamily( - //light - Font(R.font.lato_light, FontWeight.Light), - //normal - Font(R.font.lato_regular, FontWeight.Normal), - //bold - Font(R.font.lato_bold, FontWeight.Bold), - ) - -/** - * Constructs the typography with the [fontFamily] provided with support for capitalizing. - */ -private fun Typography(fontFamily: AndroidFontFamily): Typography { - return Typography( - defaultFontFamily = fontFamily, - button = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - letterSpacing = 1.25.sp, - // a workaround for capitalizing - fontFeatureSettings = "c2sc, smcp" - ), - overline = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 10.sp, - letterSpacing = 1.5.sp, - // a workaround for capitalizing - fontFeatureSettings = "c2sc, smcp" - ) - ) -} - -/** - * A variant of caption. - */ -private val caption2 = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 10.sp, - letterSpacing = 0.4.sp -) - -/** - * A variant of caption - */ -val Typography.caption2 get() = com.prime.toolz2.caption2 - -/** - * The alpha of the container colors. - */ -val MaterialTheme.CONTAINER_COLOR_ALPHA get() = 0.15f - -/** - * checks If [GlobalKeys.FORCE_COLORIZE] - */ -val MaterialTheme.forceColorize - @Composable inline get() = LocalPreferenceStore.current.run { - get(GlobalKeys.FORCE_COLORIZE).observeAsState().value - } - -private val small2 = RoundedCornerShape(8.dp) - -/** - * A variant of MaterialTheme shape with coroner's 8 dp - */ -val Shapes.small2 get() = com.prime.toolz2.small2 - -/** - * returns [primary] if [requires] is met else [elze]. - * @param requires The condition for primary to return. default value is [requiresAccent] - * @param elze The color to return if [requires] is not met The default value is [surface] - */ -@Composable -fun Colors.primary(requires: Boolean = MaterialTheme.forceColorize, elze: Color = colors.surface) = - if (requires) MaterialTheme.colors.primary else elze - -/** - * returns [onPrimary] if [requires] is met else [otherwise]. - * @param requires The condition for onPrimary to return. default value is [requiresAccent] - * @param otherwise The color to return if [requires] is not met The default value is [onSurface] - */ -@Composable -fun Colors.onPrimary( - requires: Boolean = MaterialTheme.forceColorize, - elze: Color = colors.onSurface -) = - if (requires) MaterialTheme.colors.onPrimary else elze - -/** - * @see primary() - */ -@Composable -fun Colors.secondary( - requires: Boolean = MaterialTheme.forceColorize, - elze: Color = colors.surface -) = - if (requires) colors.secondary else elze - -/** - * @see onPrimary() - */ -@Composable -fun Colors.onSecondary( - requires: Boolean = MaterialTheme.forceColorize, - elze: Color = colors.onSurface -) = - if (requires) MaterialTheme.colors.onSecondary else elze - -val Colors.surfaceVariant - @Composable inline get() = colors.surface.hsl(lightness = if (isLight) 0.94f else 0.06f) - -/** - * Primary container is applied to elements needing less emphasis than primary - */ -val Colors.primaryContainer - @Composable inline get() = colors.primary.copy(MaterialTheme.CONTAINER_COLOR_ALPHA) - -/** - * On-primary container is applied to content (icons, text, etc.) that sits on top of primary container - */ -val Colors.onPrimaryContainer @Composable inline get() = colors.primary - -val Colors.secondaryContainer - @Composable inline get() = colors.secondary.copy(MaterialTheme.CONTAINER_COLOR_ALPHA) - -val Colors.onSecondaryContainer @Composable inline get() = colors.secondary - -val Colors.errorContainer - @Composable inline get() = colors.error.copy(MaterialTheme.CONTAINER_COLOR_ALPHA) - -val Colors.onErrorContainer @Composable inline get() = colors.error - -/** - * Observes the coloring [GlobalKeys.COLOR_STATUS_BAR] of status Bar. - */ -val MaterialTheme.colorStatusBar - @Composable inline get() = LocalPreferenceStore.current.run { - get(GlobalKeys.COLOR_STATUS_BAR).observeAsState().value - } - -inline val Colors.overlay - get() = (if (isLight) Color.Black else Color.White).copy(0.04f) - -inline val Colors.outline - get() = (if (isLight) Color.Black else Color.White).copy(0.12f) - -val Colors.onOverlay - @Composable inline get() = - (colors.onBackground).copy(alpha = ContentAlpha.medium) - -val Colors.lightShadowColor - inline get() = - if (isLight) Color.White else Color.White.copy(0.025f) - -val Colors.darkShadowColor - inline get() = - if (isLight) Color(0xFFAEAEC0).copy(0.7f) else Color.Black.copy(0.6f) - - -private val defaultPrimaryColor = Color(0xFF5600E8) -private val defaultSecondaryColor = Color.MetroGreen - -private val defaultThemeShapes = - Shapes( - small = RoundedCornerShape(4.dp), - medium = RoundedCornerShape(4.dp), - large = RoundedCornerShape(0.dp) - ) - -@Composable -fun Material(isDark: Boolean, content: @Composable () -> Unit) { - - val preferences = LocalPreferenceStore.current - - val background by animateColorAsState( - targetValue = if (isDark) Color(0xFF0E0E0F) else Color(0xFFF5F5FA), - animationSpec = tween(AnimationConstants.LongDurationMills) - ) - - val surface by animateColorAsState( - targetValue = if (isDark) Color.TrafficBlack else Color.White, - animationSpec = tween(AnimationConstants.LongDurationMills) - ) - - val primary = defaultPrimaryColor - val secondary = defaultSecondaryColor - - val colors = Colors( - primary = primary, - secondary = secondary, - background = background, - surface = surface, - primaryVariant = primary.blend(Color.Black, 0.2f), - secondaryVariant = secondary.blend(Color.Black, 0.2f), - onPrimary = Color.SignalWhite, - onSurface = if (isDark) Color.SignalWhite else Color.UmbraGrey, - onBackground = if (isDark) Color.SignalWhite else Color.Black, - error = Color.OrientRed, - onSecondary = Color.SignalWhite, - onError = Color.SignalWhite, - isLight = !isDark - ) - - val fontFamily by with(preferences) { - preferences[GlobalKeys.FONT_FAMILY].map { font -> - when (font) { - FontFamily.SYSTEM_DEFAULT -> AndroidFontFamily.Default - FontFamily.PROVIDED -> ProvidedFontFamily - FontFamily.SAN_SERIF -> AndroidFontFamily.SansSerif - FontFamily.SARIF -> AndroidFontFamily.Serif - FontFamily.CURSIVE -> AndroidFontFamily.Cursive - } - }.observeAsState() - } - - MaterialTheme( - colors = colors, - typography = Typography(fontFamily), - shapes = defaultThemeShapes, - content = content - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/common/Util.kt b/app/src/main/java/com/prime/toolz2/common/Util.kt deleted file mode 100644 index 53fc088..0000000 --- a/app/src/main/java/com/prime/toolz2/common/Util.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.prime.toolz2.common - -import android.app.Activity -import android.util.Log -import androidx.compose.material.SnackbarDuration -import androidx.lifecycle.lifecycleScope -import com.google.android.play.core.appupdate.AppUpdateManager -import com.google.android.play.core.appupdate.AppUpdateManagerFactory -import com.google.android.play.core.ktx.AppUpdateResult -import com.google.android.play.core.ktx.requestAppUpdateInfo -import com.google.android.play.core.ktx.requestReview -import com.google.android.play.core.ktx.requestUpdateFlow -import com.google.android.play.core.review.ReviewManagerFactory -import com.prime.toolz2.MainActivity -import com.prime.toolz2.common.compose.SnackDataChannel -import com.prime.toolz2.common.compose.send -import com.prime.toolz2.settings.GlobalKeys -import com.primex.core.activity -import com.primex.core.runCatching -import com.primex.preferences.longPreferenceKey -import kotlinx.coroutines.launch -import java.util.concurrent.TimeUnit - -private const val TAG = "Util" - diff --git a/app/src/main/java/com/prime/toolz2/common/billing/Advertiser.kt b/app/src/main/java/com/prime/toolz2/common/billing/Advertiser.kt deleted file mode 100644 index f0e19f7..0000000 --- a/app/src/main/java/com/prime/toolz2/common/billing/Advertiser.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.prime.toolz2.common.billing - -interface Advertiser { -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/common/billing/BillingManager.kt b/app/src/main/java/com/prime/toolz2/common/billing/BillingManager.kt deleted file mode 100644 index 95cf02c..0000000 --- a/app/src/main/java/com/prime/toolz2/common/billing/BillingManager.kt +++ /dev/null @@ -1,378 +0,0 @@ -package com.prime.toolz2.common.billing - -import android.app.Activity -import android.content.Context -import android.util.Log -import androidx.compose.runtime.* -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import com.android.billingclient.api.* -import com.prime.toolz2.BuildConfig -import kotlinx.coroutines.* -import java.lang.Long.min - -private const val TAG = "BillingManager" - - -private const val RECONNECT_TIMER_START_MILLISECONDS = 1L * 1000L -private const val RECONNECT_TIMER_MAX_TIME_MILLISECONDS = 1000L * 60L * 15L // 15 minutes -private const val SKU_DETAILS_REQUERY_TIME = 1000L * 60L * 60L * 4L // 4 hours - -/** - * A one stop solution for monetization and Billing. - * @param products The list of in-app product ids. - * @param subscriptions The list of in-app subscription Ids. - * - * @author Zakir Ahmad Sheikh - * @since 18-08-2022 - */ -class BillingManager( - context: Context, - products: Array? = null, - // TODO: Implement in future version of BillingManger - subscriptions: Array? = null, -) : PurchasesUpdatedListener, Advertiser, BillingClientStateListener, DefaultLifecycleObserver { - - private val mBillingClient = - BillingClient.newBuilder(context) - .setListener(this) - .enablePendingPurchases() - .build() - - // how long before the data source tries to reconnect to Google play - private var delayReconnectMills = - RECONNECT_TIMER_START_MILLISECONDS - - private val products = - products?.map { product -> - QueryProductDetailsParams.Product.newBuilder() - .setProductId(product) - .setProductType(BillingClient.ProductType.INAPP) - .build() - } - - private val subscriptions = - products?.map { product -> - QueryProductDetailsParams.Product.newBuilder() - .setProductId(product) - .setProductType(BillingClient.ProductType.SUBS) - .build() - } - - init { - // requires not everything to be null or empty. - require(!products.isNullOrEmpty() || !subscriptions.isNullOrEmpty()) - mBillingClient.startConnection(this) - - } - - /** - * The list of items that the user have purchased. - */ - private val _purchases = - mutableStateOf>(emptyList()) - - /** - * @see _purchases - */ - val purchases: State> get() = _purchases - - /** - * An item is purchased if its state [Purchase.PurchaseState.PURCHASED] && [Purchase.isAcknowledged] - * @param id the product id. - */ - @Composable - fun isPurchased(id: String) = - derivedStateOf { - val purchase = _purchases.value.find { it.products.contains(id) } - purchase != null && purchase.purchaseState == Purchase.PurchaseState.PURCHASED - && if (BuildConfig.DEBUG) true else purchase.isAcknowledged - } - - /** - * The [ProductDetails] mapped with their product Ids. - */ - private val _details = - mutableStateOf>(emptyMap()) - - /** - * @see _details - */ - val details: State> get() = _details - - /** - * A [CoroutineScope] to handle async tasks. - */ - private val billingManagerScope = - CoroutineScope(Dispatchers.IO) - - override fun onPurchasesUpdated( - result: BillingResult, - purchases: MutableList? - ) { - when (result.responseCode) { - BillingClient.BillingResponseCode.OK -> { - if (purchases.isNullOrEmpty()) { - Log.d(TAG, "Null Purchase List Returned from OK response!") - return - } - // process the purchases. - process(purchases) - } - BillingClient.BillingResponseCode.USER_CANCELED -> - Log.i(TAG, "onPurchasesUpdated: User canceled the purchase") - BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> - Log.i(TAG, "onPurchasesUpdated: The user already owns this item") - BillingClient.BillingResponseCode.DEVELOPER_ERROR -> Log.e( - TAG, - "onPurchasesUpdated: Developer error means that Google Play " + - "does not recognize the configuration. If you are just getting started, " + - "make sure you have configured the application correctly in the " + - "Google Play Console. The SKU product ID must match and the APK you " + - "are using must be signed with release keys." - ) - else -> - Log.d( - TAG, "BillingResult [" + result.responseCode + "]: " + result.debugMessage - ) - } - } - - /** - * This is a pretty unusual occurrence. It happens primarily if the Google Play Store - * self-upgrades or is force closed. - */ - override fun onBillingServiceDisconnected() = reconnect() - - override fun onBillingSetupFinished(result: BillingResult) { - if (result.responseCode != BillingClient.BillingResponseCode.OK) { - Log.i(TAG, result.debugMessage) - reconnect() - return - } - // The billing client is ready. You can query purchases here. - // This doesn't mean that your app is set up correctly in the console -- it just - // means that you have a connection to the Billing service. - billingManagerScope - .launch { - val purchases = async { query(BillingClient.ProductType.INAPP) } - val response = purchases.await() - // process them. - process(response) - _purchases.value = response - - // details - val details = async { query(products) } - _details.value = details.await().associateBy { it.productId } - } - } - - /** - * It's recommended to requery purchases during onResume. - */ - override fun onResume(owner: LifecycleOwner) { - super.onResume(owner) - Log.d(TAG, "ON_RESUME") - // this just avoids an extra purchase refresh after we finish a billing flow - if (mBillingClient.isReady) { - billingManagerScope.launch { - // refresh. - val purchases = query(BillingClient.ProductType.INAPP) - process(purchases) - _purchases.value = purchases - } - } - } - - override fun onDestroy(owner: LifecycleOwner) { - Log.i(TAG, "Terminating connection") - mBillingClient.endConnection() - billingManagerScope.cancel("destroying BillingManager") - super.onDestroy(owner) - } - - - /** - * Simple Utility function does is process the [purchases] and acknowledges them - */ - private fun process(purchases: List) { - billingManagerScope.launch { - for (purchase in purchases) { - // Global check to make sure all purchases are signed correctly. - // This check is best performed on your server. - val state = purchase.purchaseState - if (state == Purchase.PurchaseState.PURCHASED) { - if (!isSignatureValid(purchase)) { - Log.e(TAG, "Invalid signature. Make sure your public key is correct.") - continue - } - } - if (!purchase.isAcknowledged) { - acknowledge(purchase) - } - } - } - } - - /** - * A simple query method. - * *Note - Handles all error cases.* - * @param : The list of products to query. - * @return empty in case error else the products. - */ - private suspend fun query( - products: List? - ): List { - - //leave early if null or empty - Log.i(TAG, "query: ") - if (products.isNullOrEmpty()) { - Log.i(TAG, "query: Null $products id list passed.") - return emptyList() - } - - // construct params - val prams = - QueryProductDetailsParams - .newBuilder() - .setProductList(products) - .build() - - val (result, list) = mBillingClient.queryProductDetails(prams) - if (result.responseCode != BillingClient.BillingResponseCode.OK) { - Log.i(TAG, "query: ${result}}") - return emptyList() - } - if (list.isNullOrEmpty()) { - Log.e( - TAG, - "onProductDetailsResponse: " + - "Found null or empty ProductDetails. " + - "Check to see if the Products you requested are correctly " + - "published in the Google Play Console." - ) - return emptyList() - } - return list - } - - /** - * A convince query method. It just returns [_purchases] and handles error cases and nothing more. - * @param type the type of purchases to fetch E.g., [BillingClient.ProductType.INAPP]] - * @return empty list in case of error else purchases. - */ - private suspend fun query( - type: String = BillingClient.ProductType.INAPP - ): List { - if (!mBillingClient.isReady) { - Log.e(TAG, "queryPurchases: BillingClient is not ready") - reconnect() - } - - val params = - QueryPurchasesParams - .newBuilder() - .setProductType(type) - .build() - - val response = mBillingClient.queryPurchasesAsync(params = params) - val result = response.billingResult - - when (result.responseCode) { - BillingClient.BillingResponseCode.OK -> { - if (response.purchasesList.isEmpty()) { - Log.e(TAG, "Null|Empty Purchase List Returned from OK response!") - return emptyList() - } - return response.purchasesList - } - else -> Log.d(TAG, "query [" + result.responseCode + "]: " + result.debugMessage) - } - return emptyList() - } - - - /** - * Ideally your implementation will comprise a secure server, rendering this check - * unnecessary. @see [Security] - */ - private fun isSignatureValid(purchase: Purchase): Boolean = - Security.verifyPurchase(purchase.originalJson, purchase.signature) - - /** - * Acknowledges the [purchase] in case it is not. - * @param purchase the purchase to acknowledge. - * @return true if acknowledged else false. Note if [purchase] is already acknowledged it will return false. - */ - private suspend fun acknowledge( - purchase: Purchase - ): Boolean { - // don't acknowledge in debug app. - if (BuildConfig.DEBUG) return true - if (purchase.isAcknowledged) return false - val params = - AcknowledgePurchaseParams - .newBuilder() - .setPurchaseToken(purchase.purchaseToken) - .build() - val result = mBillingClient.acknowledgePurchase(params) - Log.i(TAG, "acknowledge: $result") - return result.responseCode == BillingClient.BillingResponseCode.OK - && purchase.purchaseState == Purchase.PurchaseState.PURCHASED - - } - - /** - * Retries the billing service connection with exponential backoff, maxing out at the time - * specified by [RECONNECT_TIMER_MAX_TIME_MILLISECONDS]. - */ - private fun reconnect() { - billingManagerScope.launch(Dispatchers.Main) { - val delay = delayReconnectMills - //next delay - delayReconnectMills = - min(delayReconnectMills * 2, RECONNECT_TIMER_MAX_TIME_MILLISECONDS) - delay(delay) - mBillingClient.startConnection(this@BillingManager) - } - } - - /** - * Launch the billing flow. This will launch an external Activity for a result, so it requires - * an Activity reference. For subscriptions, it supports upgrading from one SKU type to another - * by passing in SKUs to be upgraded. - * - * @param activity active activity to launch our billing flow from - * @param sku SKU (Product ID) to be purchased - * @param upgradeSkusVarargs SKUs that the subscription can be upgraded from - * @return true if launch is successful - */ - fun launchBillingFlow( - host: Activity, - product: String, - vararg upgradeSkusVarargs: String - ): Boolean { - val details = _details.value[product] ?: return false - val params = BillingFlowParams - .newBuilder() - .setProductDetailsParamsList( - listOf( - BillingFlowParams - .ProductDetailsParams - .newBuilder() - .setProductDetails(details) - .build() - ) - ) - .build() - // val upgradeSkus = arrayOf(*upgradeSkusVarargs) - val result = mBillingClient.launchBillingFlow(host, params) - return when (result.responseCode) { - BillingClient.BillingResponseCode.OK -> true - else -> { - Log.e(TAG, "Billing failed: + " + result.debugMessage) - false - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/common/billing/Security.java b/app/src/main/java/com/prime/toolz2/common/billing/Security.java deleted file mode 100644 index c6948d7..0000000 --- a/app/src/main/java/com/prime/toolz2/common/billing/Security.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.prime.toolz2.common.billing; - -/* - * This class is an sample of how you can check to make sure your purchases on the device came from - * Google Play. Putting code like this on your server will provide additional protection. - *

- * One thing that you may also wish to consider doing is caching purchase IDs to make replay attacks - * harder. The reason this code isn't just part of the library is to allow you to customize it (and - * rename it!) to make generic patching exploits more difficult. - */ -import android.text.TextUtils; -import android.util.Base64; -import android.util.Log; - -import com.prime.toolz2.BillingTokens; -import com.prime.toolz2.BuildConfig; - -import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; - -/** - * Security-related methods. For a secure implementation, all of this code should be implemented on - * a server that communicates with the application on the device. - */ -class Security { - static final private String TAG = "IABUtil/Security"; - static final private String KEY_FACTORY_ALGORITHM = "RSA"; - static final private String SIGNATURE_ALGORITHM = "SHA1withRSA"; - - /** - * BASE_64_ENCODED_PUBLIC_KEY should be YOUR APPLICATION PUBLIC KEY. You currently get this - * from the Google Play developer console under the "Monetization Setup" category in the - * Licensing area. This build has been setup so that if you define base64EncodedPublicKey in - * your local.properties, it will be echoed into BuildConfig. - */ - - final private static String BASE_64_ENCODED_PUBLIC_KEY = BillingTokens.PUBLIC_KEY; - - /** - * Verifies that the data was signed with the given signature - * - * @param signedData the signed JSON string (signed, not encrypted) - * @param signature the signature for the data, signed with the private key - */ - static public boolean verifyPurchase(String signedData, String signature) { - if ((TextUtils.isEmpty(signedData) || TextUtils.isEmpty(BASE_64_ENCODED_PUBLIC_KEY) - || TextUtils.isEmpty(signature)) - ) { - Log.w(TAG, "Purchase verification failed: missing data."); - return false; - } - try { - PublicKey key = generatePublicKey(BASE_64_ENCODED_PUBLIC_KEY); - return verify(key, signedData, signature); - } catch (IOException e) { - Log.e(TAG, "Error generating PublicKey from encoded key: " + e.getMessage()); - return false; - } - } - - /** - * Generates a PublicKey instance from a string containing the Base64-encoded public key. - * - * @param encodedPublicKey Base64-encoded public key - * @throws IOException if encoding algorithm is not supported or key specification - * is invalid - */ - static private PublicKey generatePublicKey(String encodedPublicKey) throws IOException { - try { - byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); - return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); - } catch (NoSuchAlgorithmException e) { - // "RSA" is guaranteed to be available. - throw new RuntimeException(e); - } catch (InvalidKeySpecException e) { - String msg = "Invalid key specification: " + e; - Log.w(TAG, msg); - throw new IOException(msg); - } - } - - /** - * Verifies that the signature from the server matches the computed signature on the data. - * Returns true if the data is correctly signed. - * - * @param publicKey public key associated with the developer account - * @param signedData signed data from server - * @param signature server signature - * @return true if the data and signature match - */ - static private Boolean verify(PublicKey publicKey, String signedData, String signature) { - byte[] signatureBytes; - try { - signatureBytes = Base64.decode(signature, Base64.DEFAULT); - } catch (IllegalArgumentException e) { - Log.w(TAG, "Base64 decoding failed."); - return false; - } - try { - Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM); - signatureAlgorithm.initVerify(publicKey); - signatureAlgorithm.update(signedData.getBytes()); - if (!signatureAlgorithm.verify(signatureBytes)) { - Log.w(TAG, "Signature verification failed..."); - return false; - } - return true; - } catch (NoSuchAlgorithmException e) { - // "RSA" is guaranteed to be available. - throw new RuntimeException(e); - } catch (InvalidKeyException e) { - Log.e(TAG, "Invalid key specification."); - } catch (SignatureException e) { - Log.e(TAG, "Signature exception."); - } - return false; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/common/compose/Compose.kt b/app/src/main/java/com/prime/toolz2/common/compose/Compose.kt deleted file mode 100644 index 001ca59..0000000 --- a/app/src/main/java/com/prime/toolz2/common/compose/Compose.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.prime.toolz2.common.compose - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - - diff --git a/app/src/main/java/com/prime/toolz2/common/compose/Snackbar.kt b/app/src/main/java/com/prime/toolz2/common/compose/Snackbar.kt deleted file mode 100644 index 3650cf0..0000000 --- a/app/src/main/java/com/prime/toolz2/common/compose/Snackbar.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.prime.toolz2.common.compose - -import androidx.annotation.StringRes -import androidx.compose.material.SnackbarDuration -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.core.content.res.ResourcesCompat -import com.primex.core.Result -import com.primex.core.Text -import com.primex.core.buildResult -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel - -sealed interface Snack { - - val action: (() -> Unit)? - - val duration: SnackbarDuration - - val label: Text? - - val message: Text - - operator fun component1(): Text? = label - - operator fun component2(): Text = message - - operator fun component3(): SnackbarDuration = duration - - operator fun component4(): (() -> Unit)? = action -} - - -private class SnackImpl( - override val action: (() -> Unit)?, - override val duration: SnackbarDuration, - override val label: Text?, - override val message: Text -) : Snack { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as SnackImpl - - if (duration != other.duration) return false - if (label != other.label) return false - if (message != other.message) return false - - return true - } - - override fun hashCode(): Int { - var result = action?.hashCode() ?: 0 - result = 31 * result + duration.hashCode() - result = 31 * result + label.hashCode() - result = 31 * result + message.hashCode() - return result - } - - override fun toString(): String { - return "SnackImpl(duration=$duration, label=$label, message=$message)" - } -} - -/** - * Construct a Snack from the provided string [label], and [message] - */ -fun Snack( - message: String, - duration: SnackbarDuration = SnackbarDuration.Short, - label: String? = null, - action: (() -> Unit)? = null -): Snack = - SnackImpl( - action = action, - duration = duration, - label = if (label == null) null else Text(label), - message = Text(message) - ) - - -/** - * Construct a Snack from the provided resource [label], and [message] - */ -fun Snack( - @StringRes message: Int, - duration: SnackbarDuration = SnackbarDuration.Short, - @StringRes label: Int? = null, - action: (() -> Unit)? = null, -): Snack = - SnackImpl( - action = action, - duration = duration, - label = if (label == null) null else Text(label), - message = Text(message) - ) - - -typealias SnackDataChannel = Channel - -fun SnackDataChannel( - capacity: Int = Channel.RENDEZVOUS, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, - onUndeliveredElement: ((Snack) -> Unit)? = null -): SnackDataChannel = - Channel( - capacity, - onBufferOverflow, - onUndeliveredElement - ) - -suspend fun SnackDataChannel.send( - message: String, - duration: SnackbarDuration = SnackbarDuration.Short, - label: String? = null, - action: (() -> Unit)? = null -) = - send( - Snack( - label = label, - message = message, - action = action, - duration = duration - ) - ) - -suspend fun SnackDataChannel.send( - @StringRes message: Int, - duration: SnackbarDuration = SnackbarDuration.Short, - @StringRes label: Int? = null, - action: (() -> Unit)? = null, -) = - send( - Snack( - label = label, - message = message, - action = action, - duration = duration - ) - ) - -suspend fun SnackDataChannel.send( - @StringRes message: Int, - vararg formatArgs: Any, - duration: SnackbarDuration = SnackbarDuration.Short, - @StringRes label: Int? = null, - action: (() -> Unit)? = null, -) = - send( - SnackImpl( - action = action, - duration = duration, - label = if (label == null) null else Text(label), - message = Text(message, formatArgs = formatArgs) - ) - ) - - -suspend fun SnackDataChannel.send( - message: Text, - duration: SnackbarDuration = SnackbarDuration.Short, - label: Text? = null, - action: (() -> Unit)? = null, -) = send( - SnackImpl(action = action, duration = duration, label = label, message = message) -) - - -val LocalSnackDataChannel = staticCompositionLocalOf { - error("no local messenger provided!!") -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/common/compose/Util.kt b/app/src/main/java/com/prime/toolz2/common/compose/Util.kt deleted file mode 100644 index d44570d..0000000 --- a/app/src/main/java/com/prime/toolz2/common/compose/Util.kt +++ /dev/null @@ -1,268 +0,0 @@ -package com.prime.toolz2.common.compose - -import android.app.Activity -import android.content.res.Resources -import androidx.compose.animation.core.AnimationConstants -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material.ContentAlpha -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.isUnspecified -import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import androidx.navigation.compose.currentBackStackEntryAsState -import com.google.accompanist.systemuicontroller.SystemUiController -import com.google.android.play.core.appupdate.AppUpdateManager -import com.google.android.play.core.install.model.AppUpdateType -import com.google.android.play.core.install.model.UpdateAvailability -import com.google.android.play.core.ktx.installStatus -import com.google.android.play.core.review.ReviewManager -import com.prime.toolz2.common.billing.BillingManager -import com.primex.core.Text -import com.primex.core.resolve -import com.primex.core.runCatching -import com.primex.core.spannedResource - -/** - * A Utility extension function for managing status bar UI. - * - * @param color: The background color of the statusBar. if [Color.Unspecified] the status bar will - * be painted by primaryVariant. - * @param darkIcons: same as name suggests works in collaboration with color. if it is unspecified; uses - * light icons as we will use primaryVariant as background. - */ -fun Modifier.statusBarsPadding2( - color: Color = Color.Unspecified, - darkIcons: Boolean = false, -) = composed { - val controller = LocalSystemUiController.current - - // invoke but control only icons not color. - SideEffect { - controller.setStatusBarColor( - //INFO we are not going to change the background of the statusBar here. - // Reasons are. - // * It adds a delay and the change becomes ugly. - // * animation to color can't be added. - Color.Transparent, - - // dark icons only when requested by user and color is unSpecified. - // because we are going to paint status bar with primaryVariant if unspecified. - darkIcons && !color.isUnspecified - ) - } - - val paint = color.takeOrElse { MaterialTheme.colors.primaryVariant } - // add padding - - val height = with(LocalDensity.current) { - WindowInsets.statusBars.getTop(this).toFloat() - } - - // add background - Modifier - .drawWithContent { - drawContent() - drawRect(paint, size = size.copy(height = height)) - } - .then(this@composed) - .statusBarsPadding() -} - -val LocalSystemUiController = staticCompositionLocalOf { - error("No ui controller defined!!") -} - -// Nav Host Controller -val LocalNavController = staticCompositionLocalOf { - error("no local nav host controller found") -} - -/** - * The content padding for the screen under current [NavGraph] - */ -val LocalWindowPadding = compositionLocalOf { - PaddingValues(0.dp) -} - -val NavHostController.current - @Composable - get() = currentBackStackEntryAsState().value?.destination?.route - -// Setup animation related default things - -typealias Anim = AnimationConstants - -private const val LONG_DURATION_TIME = 500 - -/** - * 500 Mills - */ -val Anim.LongDurationMills get() = LONG_DURATION_TIME - -private const val MEDIUM_DURATION_TIME = 400 - -/** - * 400 Mills - */ -val Anim.MediumDurationMills get() = MEDIUM_DURATION_TIME - -private const val SHORT_DURATION_TIME = 200 - -/** - * 200 Mills - */ -val Anim.ShortDurationMills get() = SHORT_DURATION_TIME - -private const val ACTIVITY_SHORT_DURATION = 150 - -/** - * 150 Mills - */ -val Anim.ActivityShortDurationMills get() = ACTIVITY_SHORT_DURATION - -private const val ACTIVITY_LONG_DURATION = 220 - -/** - * 220 Mills - */ -val Anim.ActivityLongDurationMills get() = ACTIVITY_LONG_DURATION - -object ContentPadding { - /** - * A small 4 [Dp] Padding - */ - val small: Dp = 4.dp - - /** - * A Medium 8 [Dp] Padding - */ - val medium: Dp = 8.dp - - /** - * Normal 16 [Dp] Padding - */ - val normal: Dp = 16.dp - - /** - * Large 32 [Dp] Padding - */ - val large: Dp = 32.dp -} - -/** - * The Standard Elevation Values. - */ -object ContentElevation { - /** - * Zero Elevation. - */ - val none = 0.dp - - /** - * Elevation of 6 [Dp] - */ - val low = 6.dp - - /** - * Elevation of 12 [Dp] - */ - val medium = 12.dp - - /** - * Elevation of 20 [Dp] - */ - val high = 20.dp - - /** - * Elevation of 30 [Dp] - */ - val xHigh = 30.dp -} - -/** - * The recommended divider Alpha - */ -val ContentAlpha.Divider get() = com.prime.toolz2.common.compose.Divider -private const val Divider = 0.12f - - -/** - * The recommended LocalIndication Alpha - */ -val ContentAlpha.Indication get() = com.prime.toolz2.common.compose.Indication -private const val Indication = 0.1f - -@Composable -@ReadOnlyComposable -@NonRestartableComposable -inline fun stringResource(res: Text) = - spannedResource(value = res) - - -inline fun Resources.stringResource(res: Text) = resolve(res) - -@JvmName("stringResource1") -inline fun Resources.stringResource(res: Text?) = resolve(res) - -val LocalBillingManager = - compositionLocalOf { - error("No Local BillingClient set. ") - } - -private const val TAG = "Util" - -fun AppUpdateManager.check( - activity: Activity, - resultCode: Int, - report: Boolean = false, - emit: (snack: Snack) -> Unit -){ - // obtain the task for each check - val task = appUpdateInfo - task.addOnSuccessListener { info -> - val availability = info.updateAvailability() - val status = info.installStatus - emit(Snack("$status")) - when(availability){ - UpdateAvailability.UNKNOWN -> emit(Snack("Unknown error occurred!. $availability")) - - UpdateAvailability.UPDATE_NOT_AVAILABLE -> - emit(Snack("App is already updated to latest version.")) - - // UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> - // resume or start new - else -> { - val downloaded = info.bytesDownloaded() - - // in case it is 0 to avoid exception. - val total = info.totalBytesToDownload() + 1 - val progress = downloaded / total * 100 - emit(Snack("Downloaded: $progress")) - // update is available - //FixMe - Add way to manually adjust flexibility. - //val isFlexible = (info.clientVersionStalenessDays ?: -1) <= FLEXIBLE_UPDATE_STALENESS_DAYS - val isFlexible = info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) - val options = if (isFlexible) AppUpdateType.FLEXIBLE else AppUpdateType.IMMEDIATE - emit(Snack("$options")) - runCatching(TAG){ - startUpdateFlowForResult(info, options, activity, resultCode) - } - } - - } - } - task.addOnFailureListener { - emit(Snack("Unknown error occured: ${it.message}")) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/common/compose/WindowSize.kt b/app/src/main/java/com/prime/toolz2/common/compose/WindowSize.kt deleted file mode 100644 index a456ae8..0000000 --- a/app/src/main/java/com/prime/toolz2/common/compose/WindowSize.kt +++ /dev/null @@ -1,84 +0,0 @@ -import android.app.Activity -import androidx.compose.runtime.Composable -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.toComposeRect -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp -import androidx.window.layout.WindowMetricsCalculator - - -/** - * Window size classes are a set of opinionated viewport breakpoints for you to design, develop, and - * test resizable application layouts against. They have been chosen specifically to balance layout - * simplicity with the flexibility to optimize your app for unique cases. - * - * ![Window Sizes: Image](https://developer.android.com/images/guide/topics/large-screens/window_size_classes_width.png) - * - * Window size classes partition the raw window size available to your app into more manageable and - * meaningful buckets. There are three buckets: compact, medium, and expanded. The available width - * and height are partitioned individually, so at any point in time, your app has two size classes - * associated with it: a width window size class, and a height window size class. - * - * While window size classes are specified for both width and height, the available width is often - * more important than available height due to the ubiquity of vertical scrolling. Therefore, the - * width window size class will likely be more relevant to your app’s UI. - * @sample - * @see https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes#compose - */ -enum class WindowSize { - - // Small phones - // An window size of 480p width max - COMPACT, - - // medium phones - // An Window Size of 600dp width max - MEDIUM, - - // tablets 7 inch - // An Window size of 800p width max - LARGE, - - // large tablets and beyond. - // Beyond 800p - X_LARGE -} - - -/** - * Remembers the [WindowSize] class for the window corresponding to the current window metrics. - */ -@Composable -fun Activity.rememberWindowSizeClass(): WindowSize { - val configuration = LocalConfiguration.current - val windowMetrics = remember(configuration) { - WindowMetricsCalculator.getOrCreate() - .computeCurrentWindowMetrics(this) - } - val windowDpSize = with(LocalDensity.current) { - windowMetrics.bounds.toComposeRect().size.toDpSize() - } - return when { - windowDpSize.width < 480.dp -> WindowSize.COMPACT - windowDpSize.width < 600.dp -> WindowSize.MEDIUM - windowDpSize.width < 840.dp -> WindowSize.LARGE - else -> WindowSize.X_LARGE - } - - //INFO: This seems not required. - - /* val heightWindowSizeClass = when { - windowDpSize.height < 480.dp -> WindowSize.COMPACT - windowDpSize.height < 900.dp -> WindowSize.MEDIUM - else -> WindowSize.EXPANDED - }*/ - - // Use widthWindowSizeClass and heightWindowSizeClass -} - - -val LocalWindowSizeClass = compositionLocalOf { - error("No Window size available") -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Angle.kt b/app/src/main/java/com/prime/toolz2/core/converter/Angle.kt deleted file mode 100644 index 5c2c2e3..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Angle.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal - -private const val TAG = "Angle" - -@Suppress("FunctionName") -private fun Degree() = Unit( - TAG + "_degree", - R.string.system_international, - R.string.degrees, - R.string.code_degrees, - UnifiedReal(BoundedRational(1, 1)) -) - -@Suppress("FunctionName") -private fun Radian() = Unit( - TAG + "_radian", - R.string.system_international, - R.string.radian, - R.string.code_radian, - UnifiedReal(BoundedRational(572957795130823L, 10000000000000L)) -) - -@Suppress("FunctionName") -private fun Gradian() = Unit( - TAG + "_gradian", - R.string.system_international, - R.string.gradian, - R.string.code_gradian, - UnifiedReal(BoundedRational(9, 10)) -) - -@Suppress("FunctionName") -fun Angle() = Converter( - "converter_$TAG", - R.string.angle, - R.drawable.ic_angle, - arrayOf( - Degree(), - Radian(), - Gradian() - ) -) - diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Area.kt b/app/src/main/java/com/prime/toolz2/core/converter/Area.kt deleted file mode 100644 index 3ef70b5..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Area.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal - -private const val TAG = "Area" - -//Base Unit: Square Metre - -@Suppress("FunctionName") -private fun SquareMillimetre() = Unit( - TAG + "_sq_millimetre", - R.string.system_international, - R.string.sq_millimetres, - R.string.code_sq_millimetres, - UnifiedReal(BoundedRational(1, 1000000)) -) - -@Suppress("FunctionName") -private fun SquareCentimetre() = Unit( - TAG + "_sq_centimetre", - R.string.system_international, - R.string.sq_centimetres, - R.string.code_sq_centimetres, - UnifiedReal(BoundedRational(1, 10000)) -) - - -@Suppress("FunctionName") -private fun SquareMetre() = Unit( - TAG + "_sq_metre", - R.string.system_international, - R.string.sq_metres, - R.string.code_sq_metres, - UnifiedReal(BoundedRational(1, 1)) -) - -@Suppress("FunctionName") -private fun Hectare() = Unit( - TAG + "_sq_hectare", - R.string.system_international, - R.string.hectare, - R.string.code_hectare, - UnifiedReal(BoundedRational(10000, 1)) -) - - -@Suppress("FunctionName") -private fun SqKilometre() = Unit( - TAG + "_sq_kilometre", - R.string.system_international, - R.string.sq_kilometre, - R.string.code_sq_kilometre, - UnifiedReal(BoundedRational(1000000, 1)) -) - -@Suppress("FunctionName") -private fun SqInch() = Unit( - TAG + "_sq_inch", - R.string.imperial_system, - R.string.sq_inche, - R.string.code_sq_inche, - UnifiedReal(BoundedRational(64516, 100000000)) -) - -@Suppress("FunctionName") -private fun SqFoot() = Unit( - TAG + "_sq_foot", - R.string.imperial_system, - R.string.sq_foot, - R.string.code_sq_foot, - UnifiedReal(BoundedRational(92903, 1000000)) -) - -@Suppress("FunctionName") -private fun SqYard() = Unit( - TAG + "_sq_yard", - R.string.imperial_system, - R.string.sq_yard, - R.string.code_sq_yard, - UnifiedReal(BoundedRational(836127, 1000000)) -) - -@Suppress("FunctionName") -private fun Acre() = Unit( - TAG + "_acre", - R.string.imperial_system, - R.string.acre, - R.string.code_acre, - UnifiedReal(BoundedRational(40468564224L, 10000000L)) -) - -@Suppress("FunctionName") -private fun SqMile() = Unit( - TAG + "_sq_mile", - R.string.imperial_system, - R.string.sq_mile, - R.string.code_sq_mile, - UnifiedReal(BoundedRational(2589988110336L, 1000000)) -) - -@Suppress("FunctionName") -fun Area() = Converter( - "converter_$TAG", - R.string.area, - R.drawable.ic_area, - arrayOf( - SquareMillimetre(), - SquareCentimetre(), - SquareMetre(), - Hectare(), - SqKilometre(), - SqInch(), - SqFoot(), - SqYard(), - Acre(), - SqMile() - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Converter.kt b/app/src/main/java/com/prime/toolz2/core/converter/Converter.kt deleted file mode 100644 index 3e26313..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Converter.kt +++ /dev/null @@ -1,227 +0,0 @@ -package com.prime.toolz2.core.converter - - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import com.prime.toolz2.core.math.UnifiedReal -import com.primex.core.Text - - -//TODO: Replace string resources plurals in futuure versions. -interface Unet { - /** - * The unique Id of this unit in the converter. - */ - val uuid: String - - /** - * The title of the Unit. - */ - val title: Text - - /** - * The short name of the Unit - */ - val code: Text - - /** - * The name resource of the group to which this unit belongs; e.g., SI, BTS etc. - */ - val group: Text - - /** - * The optional iconRes [DrawableRes] of the unit - */ - val icon: Int? - get() = null - - /** - * Consumes value in Unit and returns value in [base] unit - * @param value the value in Unit - */ - suspend fun toBase(value: UnifiedReal): UnifiedReal - - /** - * Consumes value in base and returns value in [Unet] - * - *@param value: The value in base. - */ - suspend fun toUnit(value: UnifiedReal): UnifiedReal -} - - - - -interface Converter { - - /** - * The title of the Unit. - */ - val title: Text - - /** - * The resource icon associated with this converter. - */ - val drawableRes: Int - - /** - * The unique Id to identify this converter. - */ - val uuid: String - - /** - * The list of units supported by this converter. - */ - val units: Array - - /** - * The method to convert [value]from -> to - */ - suspend fun convert(from: Unet, to: Unet, value: UnifiedReal): UnifiedReal { - val inBase = from.toBase(value) - return to.toUnit(inBase) - } -} - -@Suppress("FunctionName") -fun Converter( - uuid: String, - @StringRes title: Int, - @DrawableRes drawableRes: Int, - units: Array, -) = object : Converter { - override val title: Text = Text(title) - override val drawableRes: Int = drawableRes - override val uuid: String = uuid - override val units: Array = units -} - -/** - * Constructs the Unit for the Converter - */ -@Suppress("FunctionName") -fun Unit( - uuid: String, - @StringRes group: Int, - @StringRes title: Int, - @StringRes code: Int, - inBase: UnifiedReal, - @DrawableRes icon: Int? = null -) = - object : Unet { - override val uuid: String = uuid - override val title: Text = Text(title) - override val code: Text = Text(code) - override val group: Text = Text(group) - override val icon: Int? = icon - - override suspend fun toBase(value: UnifiedReal): UnifiedReal { - return value.multiply(inBase) - } - - override suspend fun toUnit(value: UnifiedReal): UnifiedReal { - return value.divide(inBase) - } - } - - - -interface UnitConverter { - - var value: UnifiedReal - - var converter: Converter - - var from: Unet - - var to: Unet - - suspend fun convert(): UnifiedReal = converter.convert(from, to, value) - - /** - * @param pct: A value between 0 and 1 - */ - suspend fun mapped(pct: Float): Map { - val map = HashMap() - - val units = converter.units - // the pct must between 0 and one - // throw error if not - require(pct in 0.0..1.0) - - // the unified real form of the limit - val limit = UnifiedReal("$pct") - - units.forEach { unit -> - if (unit != to && unit != from) { - val result = converter.convert(from, unit, value) - - if (result < limit) - return@forEach // continue. - // only add then to map - //if (value.compareTo(value)) - map += unit to result - } - } - //return the computed result - // TODO: Sort in ascending order of the entries. - // this hack currently works but needs some elegent solution. - return map.toSortedMap { o1, o2 -> - val real1 = map[o1]!! - val real2 = map[o2]!! - real1.compareTo(real2) - } - } - - val converters: Array - - /** - * Returns the default suggested units indices of the [converter] - */ - val default: Pair -} - - -private class UnitConverterImpl : UnitConverter { - - override val converters: Array = - arrayOf( - Length(), - Mass(), - Time(), - Temperature(), - // Data(), - Angle(), - Area(), - // Volume(), - Pressure(), - Energy(), - Power(), - Speed() - ) - - override var value: UnifiedReal = UnifiedReal.ZERO - - override var converter: Converter = converters[0] // length - set(value) { - field = value - val units = field.units - // whenever converter changes switch to defaults. - val def = default - // of the unit. - from = units[def.first]; to = units[def.second] - } - - override var from: Unet = converter.units[0] - override var to: Unet = converter.units[1] - - override val default: Pair - get() = when (converter) { - converters[0] -> 4 to 10 //metre to inch - else -> 0 to 1 //TODO: Implement properly this thing. - } - -} - - -fun UnitConverter(): UnitConverter = UnitConverterImpl() \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Data.kt b/app/src/main/java/com/prime/toolz2/core/converter/Data.kt deleted file mode 100644 index 7524fa1..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Data.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R - -private const val TAG = "Data" - -@Suppress("FunctionName") -fun Data() = Converter( - "converter_$TAG", - R.string.data, - R.drawable.ic_sd_card, - arrayOf() -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Energy.kt b/app/src/main/java/com/prime/toolz2/core/converter/Energy.kt deleted file mode 100644 index dff5598..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Energy.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal -import java.math.BigInteger - - -private const val TAG = "Energy" - -//base unit joule - - -@Suppress("FunctionName") -private fun ElectronVolt() = Unit( - TAG + "_electron_volt", - R.string.system_international, - R.string.electron_volt, - R.string.code_electron_volt, - UnifiedReal( - BoundedRational( - BigInteger("1602176565"), BigInteger("10000000000000000000000000000") - ) - ) -) - -@Suppress("FunctionName") -private fun Joule() = Unit( - TAG + "_joule", - R.string.system_international, - R.string.joule, - R.string.code_joule, - UnifiedReal(BoundedRational(1, 1)) -) - -@Suppress("FunctionName") -private fun KiloJoule() = Unit( - TAG + "_kilo_joule", - R.string.system_international, - R.string.kilojoule, - R.string.code_kilojoule, - UnifiedReal(BoundedRational(1000, 1)) -) - -@Suppress("FunctionName") -private fun ThermalCalorie() = Unit( - TAG + "_thermal_calorie", - R.string.system_international, - R.string.thermal_calorie, - R.string.code_thermal_calorie, - UnifiedReal(BoundedRational(4184, 1000)) -) - -@Suppress("FunctionName") -private fun FoodCalorie() = Unit( - TAG + "_food_calorie", - R.string.system_international, - R.string.food_calorie, - R.string.code_food_calorie, - UnifiedReal(BoundedRational(4184, 1)) -) - -@Suppress("FunctionName") -private fun FootPound() = Unit( - TAG + "_foot_pound", - R.string.imperial_system, - R.string.foot_pound, - R.string.code_foot_pound, - UnifiedReal( - BoundedRational(13558179483314003L, 10000000000000000L) - ) -) - - -@Suppress("FunctionName") -fun Energy() = Converter( - "converter_$TAG", - R.string.energy, - R.drawable.ic_energy, - arrayOf( - ElectronVolt(), - Joule(), - KiloJoule(), - ThermalCalorie(), - FoodCalorie(), - FootPound() - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Length.kt b/app/src/main/java/com/prime/toolz2/core/converter/Length.kt deleted file mode 100644 index 07e2876..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Length.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal - -private const val TAG = "Length" - -@Suppress("FunctionName") -private fun Nanometre() = Unit( - TAG + "_nanometre", - R.string.system_international, - R.string.nanometre, - R.string.code_nanometre, - UnifiedReal(BoundedRational(1, 1000000000)) -) - -@Suppress("FunctionName") -private fun Micrometre() = Unit( - TAG + "_micrometre", - R.string.system_international, - R.string.micrometre, - R.string.code_micrometre, - UnifiedReal(BoundedRational(1, 1000000)) -) - -@Suppress("FunctionName") -private fun Millimetre() = Unit( - TAG + "_millimetre", - R.string.system_international, - R.string.millimetre, - R.string.code_millimetre, - UnifiedReal(BoundedRational(1, 1000)) -) - -@Suppress("FunctionName") -private fun Centimetre() = Unit( - TAG + "_centimetre", - R.string.system_international, - R.string.centimetre, - R.string.code_centimetre, - UnifiedReal(BoundedRational(1, 100)) -) - -@Suppress("FunctionName") -private fun Metre() = Unit( - TAG + "_metre", - R.string.system_international, - R.string.metre, - R.string.code_metre, - UnifiedReal(1) -) - -@Suppress("FunctionName") -private fun Kilometre() = Unit( - TAG + "_kilometre", - R.string.system_international, - R.string.kilometre, - R.string.code_kilometre, - UnifiedReal(BoundedRational(1000, 1)) -) - -@Suppress("FunctionName") -private fun Mile() = Unit( - TAG + "_mile", - R.string.imperial_system, - R.string.mile, - R.string.code_mile, - UnifiedReal(BoundedRational(1609344, 1000)) -) - -@Suppress("FunctionName") -private fun NauticalMile() = Unit( - TAG + "_nautical_mile", - R.string.imperial_system, - R.string.nautical_mile, - R.string.code_nautical_mile, - UnifiedReal(BoundedRational(1852, 1)) -) - -@Suppress("FunctionName") -private fun Yard() = Unit( - TAG + "_yard", - R.string.imperial_system, - R.string.yard, - R.string.code_yard, - UnifiedReal(BoundedRational(9144, 10000)) -) - -@Suppress("FunctionName") -private fun Foot() = Unit( - TAG + "_foot", - R.string.imperial_system, - R.string.foot, - R.string.code_foot, - UnifiedReal(BoundedRational(3048, 10000)) -) - -@Suppress("FunctionName") -private fun Inch() = Unit( - TAG + "_inch", - R.string.imperial_system, - R.string.inch, - R.string.code_inch, - UnifiedReal(BoundedRational(254, 10000)) -) - - -@Suppress("FunctionName") -private fun Au() = Unit( - TAG + "_astronomical_unit", - R.string.system_international, - R.string.astronomical_unt, - R.string.code_astronomical_unit, - UnifiedReal(BoundedRational(149597870700L, 1)) -) - - -@Suppress("FunctionName") -private fun LightYear() = Unit( - TAG + "_light_year", - R.string.system_international, - R.string.light_year, - R.string.code_light_year, - UnifiedReal(BoundedRational(9460730472580800L, 1)) -) - -@Suppress("FunctionName") -fun Length() = Converter( - uuid = "converter_$TAG", - title = R.string.length, - drawableRes = R.drawable.ic_length, - units = arrayOf( - Nanometre(), - Micrometre(), - Millimetre(), - Centimetre(), - Metre(), - Kilometre(), - Mile(), - NauticalMile(), - Yard(), - Foot(), - Inch(), - Au(), - LightYear() - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Mass.kt b/app/src/main/java/com/prime/toolz2/core/converter/Mass.kt deleted file mode 100644 index 3e821c5..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Mass.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal - -private const val TAG = "Mass" - -//Base unit - Kilograms - -@Suppress("FunctionName") -private fun Carat() = Unit( - TAG + "_carat", - R.string.system_international, - R.string.carat, - R.string.code_carat, - UnifiedReal(BoundedRational(2, 10000)) -) - -@Suppress("FunctionName") -private fun MilliGram() = Unit( - TAG + "_milli_gram", - R.string.system_international, - R.string.milligram, - R.string.code_milligram, - UnifiedReal(BoundedRational(1, 1000000)) -) - -@Suppress("FunctionName") -private fun Centigram() = Unit( - TAG + "_centi_gram", - R.string.system_international, - R.string.centigram, - R.string.code_centigram, - UnifiedReal(BoundedRational(1, 100000)) -) - -@Suppress("FunctionName") -private fun Decigram() = Unit( - TAG + "_decigram", - R.string.system_international, - R.string.decigram, - R.string.code_decigram, - UnifiedReal(BoundedRational(1, 10000)) -) - - -@Suppress("FunctionName") -private fun Gram() = Unit( - TAG + "_gram", - R.string.system_international, - R.string.gram, - R.string.code_gram, - UnifiedReal(BoundedRational(1, 1000)) -) - -@Suppress("FunctionName") -private fun Decagram() = Unit( - TAG + "_deca_gram", - R.string.system_international, - R.string.decagram, - R.string.code_decagram, - UnifiedReal(BoundedRational(1, 100)) -) - -@Suppress("FunctionName") -private fun Hectogram() = Unit( - TAG + "_hectogram", - R.string.system_international, - R.string.hectogram, - R.string.code_hectogram, - UnifiedReal(BoundedRational(1, 10)) -) - -@Suppress("FunctionName") -private fun Kilogram() = Unit( - TAG + "_kilo_gram", - R.string.system_international, - R.string.kilogram, - R.string.code_kilogram, - UnifiedReal(BoundedRational(1, 1)) -) - -@Suppress("FunctionName") -private fun MetricTonne() = Unit( - TAG + "_metric_tonne", - R.string.system_international, - R.string.metric_ton, - R.string.code_metric_ton, - UnifiedReal(BoundedRational(1000, 1)) -) - -@Suppress("FunctionName") -private fun Ounce() = Unit( - TAG + "_ounce", - R.string.imperial_system, - R.string.ounce, - R.string.code_ounce, - UnifiedReal(BoundedRational(28349523125L, 1000000000000L)) -) - -@Suppress("FunctionName") -private fun Pound() = Unit( - TAG + "_pound", - R.string.imperial_system, - R.string.pound, - R.string.code_pound, - UnifiedReal(BoundedRational(45359237, 100000000)) -) - -@Suppress("FunctionName") -private fun Stone() = Unit( - TAG + "_stone", - R.string.imperial_system, - R.string.stone, - R.string.code_stone, - UnifiedReal(BoundedRational(635029318, 100000000)) -) - -@Suppress("FunctionName") -private fun ShortTonneUS() = Unit( - TAG + "_short_tonne_us", - R.string.imperial_system_us, - R.string.short_ton, - R.string.code_short_ton, - UnifiedReal(BoundedRational(90718474, 100000)) -) - -@Suppress("FunctionName") -private fun LongTonneUK() = Unit( - TAG + "_long_tonne_uk", - R.string.imperial_system, - R.string.long_ton, - R.string.code_long_ton, - UnifiedReal(BoundedRational(10160469088L, 10000000)) -) - -@Suppress("FunctionName") -fun Mass() = Converter( - "converter_$TAG", - R.string.weight_and_mass, - R.drawable.ic_weight_n_mass, - arrayOf( - Carat(), - MilliGram(), - Centigram(), - Decigram(), - Gram(), - Decagram(), - Hectogram(), - Kilogram(), - MetricTonne(), - Ounce(), - Stone(), - ShortTonneUS(), - LongTonneUK() - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Power.kt b/app/src/main/java/com/prime/toolz2/core/converter/Power.kt deleted file mode 100644 index 921f023..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Power.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal - -private const val TAG = "Power" -//basic unit watt - -@Suppress("FunctionName") -private fun Watt() = Unit( - TAG + "_watt", - R.string.system_international, - R.string.watt, - R.string.code_watt, - UnifiedReal(BoundedRational(1, 1)) -) - -@Suppress("FunctionName") -private fun KiloWatt() = Unit( - TAG + "_kilo_watt", - R.string.system_international, - R.string.kilowatt, - R.string.code_kilowatt, - UnifiedReal(BoundedRational(1000, 1)) -) - -@Suppress("FunctionName") -private fun HorsePower() = Unit( - TAG + "_horse_power", - R.string.imperial_system_us, - R.string.horse_power_us, - R.string.code_horse_power_us, - UnifiedReal( - BoundedRational( - 7456998715822702L, - 10000000000000L - ) - ) -) - -@Suppress("FunctionName") -private fun FootPoundsPerMinute() = Unit( - TAG + "_foot_pounds_per_minute", - R.string.imperial_system, - R.string.foot_pounds_per_minute, - R.string.code_foot_pounds_per_minute, - UnifiedReal( - BoundedRational(22596966, 1000000000L) - ) -) - -@Suppress("FunctionName") -private fun BTUPerMinute() = Unit( - TAG + "_btu_per_minute", - R.string.imperial_system, - R.string.british_thermal_units_per_minute, - R.string.code_british_thermal_units_per_minute, - UnifiedReal( - BoundedRational(175842641667L, 10000000000L) - ) -) - -@Suppress("FunctionName") -fun Power() = Converter( - "converter_$TAG", - R.string.power, - R.drawable.ic_power, - arrayOf( - Watt(), - KiloWatt(), - HorsePower(), - FootPoundsPerMinute(), - BTUPerMinute() - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Pressure.kt b/app/src/main/java/com/prime/toolz2/core/converter/Pressure.kt deleted file mode 100644 index 64c92ea..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Pressure.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal - -private const val TAG = "Pressure" - -//Basic Unit Pascal -@Suppress("FunctionName") -private fun Atmosphere() = Unit( - TAG + "_atmosphere", - R.string.system_international, - R.string.atmosphere, - R.string.code_atmosphere, - UnifiedReal( - BoundedRational(101325, 1) - ) -) - -@Suppress("FunctionName") -private fun Bar() = Unit( - TAG + "_bar", - R.string.system_international, - R.string.bar, - R.string.code_bar, - UnifiedReal( - BoundedRational(100000, 1) - ) -) - - -@Suppress("FunctionName") -private fun KiloPascal() = Unit( - TAG + "_kilo_pascal", - R.string.system_international, - R.string.kilopascal, - R.string.code_kilopascal, - UnifiedReal( - BoundedRational(1000, 1) - ) -) - -@Suppress("FunctionName") -private fun PoundsPerInch() = Unit( - TAG + "_pounds_per_inch", - R.string.system_international, - R.string.pounds_per_inch, - R.string.code_pounds_per_inch, - UnifiedReal( - BoundedRational( - 689475729316836L, - 100000000000L - ) - ) -) - -@Suppress("FunctionName") -private fun MMsOfMercury() = Unit( - TAG + "_mms_of_mercury", - R.string.system_international, - R.string.mm_of_mercury, - R.string.code_mm_of_mercury, - UnifiedReal( - BoundedRational(133322387415L, 1000000000) - ) -) - - -@Suppress("FunctionName") -private fun Pascal() = Unit( - TAG + "_pascal", - R.string.system_international, - R.string.pascal, - R.string.code_pascal, - UnifiedReal( - BoundedRational(1, 1) - ) -) - -@Suppress("FunctionName") -fun Pressure() = Converter( - "converter_$TAG", - R.string.pressure, - R.drawable.ic_pressure, - arrayOf( - Atmosphere(), - Bar(), - KiloPascal(), - PoundsPerInch(), - MMsOfMercury(), - Pascal() - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Speed.kt b/app/src/main/java/com/prime/toolz2/core/converter/Speed.kt deleted file mode 100644 index 73e74e5..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Speed.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal -import javax.crypto.Mac - -private const val TAG = "Speed" -//Base Unit kmph - - -@Suppress("FunctionName") -private fun CMsPerSecond() = Unit( - TAG + "_cms_per_second", - R.string.system_international, - R.string.centimetres_per_second, - R.string.code_centimetres_per_second, - UnifiedReal( - BoundedRational(36, 1000) - ) -) - -@Suppress("FunctionName") -private fun MsPerSecond() = Unit( - TAG + "_Ms_per_second", - R.string.system_international, - R.string.metres_per_second, - R.string.code_metres_per_second, - UnifiedReal( - BoundedRational(36, 10) - ) -) - -@Suppress("FunctionName") -private fun KMsPerHour() = Unit( - TAG + "_KMs_per_hour", - R.string.system_international, - R.string.kilometres_per_hour, - R.string.code_kilometres_per_hour, - UnifiedReal( - BoundedRational(1, 1) - ) -) - - -@Suppress("FunctionName") -private fun FeetPerSecond() = Unit( - TAG + "_feet_per_second", - R.string.imperial_system, - R.string.feet_per_second, - R.string.code_feet_per_second, - UnifiedReal( - BoundedRational(109728, 100000) - ) -) - -@Suppress("FunctionName") -private fun MilesPerHour() = Unit( - TAG + "_miles_per_hour", - R.string.imperial_system, - R.string.miles_per_hour, - R.string.code_miles_per_hour, - UnifiedReal( - BoundedRational(16092, 10000) - ) -) - -@Suppress("FunctionName") -private fun Knot() = Unit( - TAG + "_knot", - R.string.imperial_system, - R.string.knot, - R.string.code_knot, - UnifiedReal( - BoundedRational(185184, 100000) - ) -) - -@Suppress("FunctionName") -private fun Mach() = Unit( - TAG + "_mach", - R.string.imperial_system, - R.string.mach, - R.string.code_mach, - UnifiedReal( - BoundedRational(122508, 100) - ) -) - -@Suppress("FunctionName") -fun Speed() = Converter( - "converter_$TAG", - R.string.speed, - R.drawable.ic_motorcycle, - arrayOf( - CMsPerSecond(), - MsPerSecond(), - KMsPerHour(), - FeetPerSecond(), - MilesPerHour(), - Knot(), - Mach() - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Temperature.kt b/app/src/main/java/com/prime/toolz2/core/converter/Temperature.kt deleted file mode 100644 index 24793e0..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Temperature.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal -import com.primex.core.Text - - -private const val TAG = "Temperature" - -@Suppress("FunctionName") -private fun Celsius() = object : Unet { - override val title: Text = Text(R.string.celsius) - override val code: Text = Text(R.string.code_celsius) - override val uuid: String = TAG + "_celsius" - override val group: Text = Text(R.string.system_international) - - override suspend fun toBase(value: UnifiedReal): UnifiedReal = value - - override suspend fun toUnit(value: UnifiedReal): UnifiedReal = value -} - - -@Suppress("FunctionName") -private fun Fahrenheit() = object : Unet { - override val title: Text = Text(R.string.fahrenheit) - override val code: Text = Text(R.string.code_fahrenheit) - override val uuid: String = TAG + "_fahrenheit" - override val group: Text = Text(R.string.united_system_customary_system) - - override suspend fun toBase(value: UnifiedReal): UnifiedReal = - value.subtract(UnifiedReal(32)).multiply(UnifiedReal(BoundedRational(5, 9))) - - override suspend fun toUnit(value: UnifiedReal): UnifiedReal = - value.multiply(UnifiedReal(BoundedRational(9, 5))).add( - UnifiedReal(32) - ) -} - -@Suppress("FunctionName") -private fun Kelvin() = object : Unet { - override val title: Text = Text(R.string.kelvin) - override val code: Text = Text(R.string.code_kelvin) - override val uuid: String = TAG + "_kelvin" - override val group: Text = Text(R.string.system_international) - - override suspend fun toBase(value: UnifiedReal) = - value.subtract(UnifiedReal(BoundedRational(27315, 100))) - - override suspend fun toUnit(value: UnifiedReal) = - value.add(UnifiedReal(BoundedRational(27315, 100))) -} - -@Suppress("FunctionName") -private fun Rankine() = object : Unet { - override val title: Text = Text(R.string.rankine) - override val code: Text = Text(R.string.code_rankine) - override val uuid: String = TAG + "_rankine" - override val group: Text = Text(R.string.imperial_system_us) - - override suspend fun toBase(value: UnifiedReal) = - value.subtract(UnifiedReal(BoundedRational(49167, 100))) - .multiply(UnifiedReal(BoundedRational(5, 9))) - - override suspend fun toUnit(value: UnifiedReal) = - value.add(UnifiedReal(BoundedRational(27315, 100))) - .multiply(UnifiedReal(BoundedRational(9, 5))) -} - - -@Suppress("FunctionName") -private fun Delisle() = object : Unet { - override val title: Text = Text(R.string.delisle) - override val code: Text = Text(R.string.code_delisle) - override val uuid: String = TAG + "_delisle" - override val group: Text = Text(R.string.unknown) - - override suspend fun toBase(value: UnifiedReal) = - UnifiedReal(100).subtract(value.multiply(UnifiedReal(BoundedRational(2, 3)))) - - override suspend fun toUnit(value: UnifiedReal) = UnifiedReal(100).subtract(value) - .multiply(UnifiedReal(BoundedRational(15, 10))) -} - -@Suppress("FunctionName") -private fun Newton() = object : Unet { - override val title: Text = Text(R.string.newton) - override val code: Text = Text(R.string.code_newton) - override val uuid: String = TAG + "_newton" - override val group: Text = Text(R.string.unknown) - - override suspend fun toBase(value: UnifiedReal) = - value.multiply(UnifiedReal(BoundedRational(100, 33))) - - override suspend fun toUnit(value: UnifiedReal) = - value.multiply(UnifiedReal(BoundedRational(33, 100))) -} - - -@Suppress("FunctionName") -private fun Reaumur() = object : Unet { - override val title: Text = Text(R.string.reaumur) - override val code: Text = Text(R.string.code_reaumur) - override val uuid: String = TAG + "_reaumur" - override val group: Text = Text(R.string.unknown) - - override suspend fun toBase(value: UnifiedReal) = - value.multiply(UnifiedReal(BoundedRational(5, 4))) - - override suspend fun toUnit(value: UnifiedReal) = - value.multiply(UnifiedReal(BoundedRational(4, 5))) -} - - -@Suppress("FunctionName") -private fun Romer() = object : Unet { - override val title: Text = Text(R.string.romer) - override val code: Text = Text(R.string.code_romer) - override val uuid: String = TAG + "_romer" - override val group: Text = Text(R.string.unknown) - - override suspend fun toBase(value: UnifiedReal) = - value.subtract(UnifiedReal(BoundedRational(75, 10))) - .multiply(UnifiedReal(BoundedRational(40, 21))) - - override suspend fun toUnit(value: UnifiedReal) = - value.multiply(UnifiedReal(BoundedRational(21, 40))).add( - UnifiedReal( - BoundedRational( - 75, - 10 - ) - ) - ) -} - -@Suppress("FunctionName") -fun Temperature() = Converter( - uuid = "converter_$TAG", - title = R.string.temperature, - drawableRes = R.drawable.ic_temperature, - units = arrayOf(Celsius(), Fahrenheit(), Kelvin(), Rankine(), Newton(), Romer(), Reaumur()) -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Time.kt b/app/src/main/java/com/prime/toolz2/core/converter/Time.kt deleted file mode 100644 index 742b59d..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Time.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R -import com.prime.toolz2.core.math.BoundedRational -import com.prime.toolz2.core.math.UnifiedReal - -//Base unit second -private const val TAG = "Time" - -@Suppress("FunctionName") -private fun Nanosecond() = Unit( - TAG + "_nano_second", - R.string.system_international, - R.string.nanosecond, - R.string.code_nanosecond, - UnifiedReal( - BoundedRational(1, 1000000000) - ) -) - -@Suppress("FunctionName") -private fun Microsecond() = Unit( - TAG + "_micro_second", - R.string.system_international, - R.string.microsecond, - R.string.code_microsecond, - UnifiedReal( - BoundedRational(1, 1000000) - ) -) - - -@Suppress("FunctionName") -private fun Millisecond() = Unit( - TAG + "_milli_second", - R.string.system_international, - R.string.millisecond, - R.string.code_millisecond, - UnifiedReal( - BoundedRational(1, 1000) - ) -) - -@Suppress("FunctionName") -private fun Second() = Unit( - TAG + "_second", - R.string.system_international, - R.string.second, - R.string.code_second, - UnifiedReal( - BoundedRational(1, 1) - ) -) - -@Suppress("FunctionName") -private fun Minute() = Unit( - TAG + "_minute", - R.string.system_international, - R.string.minute, - R.string.code_minute, - UnifiedReal( - BoundedRational(60, 1) - ) -) - - -@Suppress("FunctionName") -private fun Hour() = Unit( - TAG + "_hour", - R.string.system_international, - R.string.hour, - R.string.code_hour, - UnifiedReal( - BoundedRational(3600, 1) - ) -) - - -@Suppress("FunctionName") -private fun Day() = Unit( - TAG + "_day", - R.string.system_international, - R.string.day, - R.string.code_day, - UnifiedReal( - BoundedRational(86400, 1) - ) -) - - -@Suppress("FunctionName") -private fun Week() = Unit( - TAG + "_week", - R.string.system_international, - R.string.week, - R.string.code_week, - UnifiedReal( - BoundedRational(604800, 1) - ) -) - -@Suppress("FunctionName") -private fun Year() = Unit( - TAG + "_year", - R.string.system_international, - R.string.year, - R.string.code_year, - UnifiedReal( - BoundedRational(31557600, 1) - ) -) - -@Suppress("FunctionName") -fun Time() = Converter( - "converter_$TAG", - R.string.time, - R.drawable.ic_time, - arrayOf( - Nanosecond(), - Microsecond(), - Millisecond(), - Second(), - Minute(), - Hour(), - Day(), - Week(), - Year(), - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Volume.kt b/app/src/main/java/com/prime/toolz2/core/converter/Volume.kt deleted file mode 100644 index b812a17..0000000 --- a/app/src/main/java/com/prime/toolz2/core/converter/Volume.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.prime.toolz2.core.converter - -import com.prime.toolz2.R - -private const val TAG = "Volume" - -@Suppress("FunctionName") -fun Volume() = Converter( - "converter_$TAG", - R.string.volume, - R.drawable.ic_volume, - arrayOf() -) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/math/BoundedRational.java b/app/src/main/java/com/prime/toolz2/core/math/BoundedRational.java deleted file mode 100644 index 4ce7606..0000000 --- a/app/src/main/java/com/prime/toolz2/core/math/BoundedRational.java +++ /dev/null @@ -1,564 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.prime.toolz2.core.math; - -import com.prime.toolz2.core.math.creals.CR; - -import java.math.BigInteger; -import java.util.Objects; -import java.util.Random; - -/** - * Rational numbers that may turn to null if they get too big. - * For many operations, if the length of the nuumerator plus the length of the denominator exceeds - * a maximum size, we simply return null, and rely on our caller do something else. - * We currently never return null for a pure integer or for a BoundedRational that has just been - * constructed. - * - * We also implement a number of irrational functions. These return a non-null result only when - * the result is known to be rational. - */ -public class BoundedRational { - // TODO: Consider returning null for integers. With some care, large factorials might become - // much faster. - // TODO: Maybe eventually make this extend Number? - - private static final int MAX_SIZE = 10000; // total, in bits - - private final BigInteger mNum; - private final BigInteger mDen; - - public BoundedRational(BigInteger n, BigInteger d) { - mNum = n; - mDen = d; - } - - public BoundedRational(BigInteger n) { - mNum = n; - mDen = BigInteger.ONE; - } - - public BoundedRational(long n, long d) { - mNum = BigInteger.valueOf(n); - mDen = BigInteger.valueOf(d); - } - - public BoundedRational(long n) { - mNum = BigInteger.valueOf(n); - mDen = BigInteger.valueOf(1); - } - - /** - * Produce BoundedRational equal to the given double. - */ - public static BoundedRational valueOf(double x) { - final long l = Math.round(x); - if ((double) l == x && Math.abs(l) <= 1000) { - return valueOf(l); - } - final long allBits = Double.doubleToRawLongBits(Math.abs(x)); - long mantissa = (allBits & ((1L << 52) - 1)); - final int biased_exp = (int)(allBits >>> 52); - if ((biased_exp & 0x7ff) == 0x7ff) { - throw new ArithmeticException("Infinity or NaN not convertible to BoundedRational"); - } - final long sign = x < 0.0 ? -1 : 1; - int exp = biased_exp - 1075; // 1023 + 52; we treat mantissa as integer. - if (biased_exp == 0) { - exp += 1; // Denormal exponent is 1 greater. - } else { - mantissa += (1L << 52); // Implied leading one. - } - BigInteger num = BigInteger.valueOf(sign * mantissa); - BigInteger den = BigInteger.ONE; - if (exp >= 0) { - num = num.shiftLeft(exp); - } else { - den = den.shiftLeft(-exp); - } - return new BoundedRational(num, den); - } - - /** - * Produce BoundedRational equal to the given long. - */ - public static BoundedRational valueOf(long x) { - if (x >= -2 && x <= 10) { - switch((int) x) { - case -2: - return MINUS_TWO; - case -1: - return MINUS_ONE; - case 0: - return ZERO; - case 1: - return ONE; - case 2: - return TWO; - case 10: - return TEN; - } - } - return new BoundedRational(x); - } - - /** - * Convert to String reflecting raw representation. - * Debug or log messages only, not pretty. - */ - public String toString() { - return mNum.toString() + "/" + mDen.toString(); - } - - /** - * Convert to readable String. - * Intended for output to user. More expensive, less useful for debugging than - * toString(). Not internationalized. - */ - public String toNiceString() { - final BoundedRational nicer = reduce().positiveDen(); - String result = nicer.mNum.toString(); - if (!nicer.mDen.equals(BigInteger.ONE)) { - result += "/" + nicer.mDen; - } - return result; - } - - public static String toString(BoundedRational r) { - if (r == null) { - return "not a small rational"; - } - return r.toString(); - } - - /** - * Returns a truncated (rounded towards 0) representation of the result. - * Includes n digits to the right of the decimal point. - * @param n result precision, >= 0 - */ - public String toStringTruncated(int n) { - String digits = mNum.abs().multiply(BigInteger.TEN.pow(n)).divide(mDen.abs()).toString(); - int len = digits.length(); - if (len < n + 1) { - digits = StringUtils.repeat('0', n + 1 - len) + digits; - len = n + 1; - } - return (signum() < 0 ? "-" : "") + digits.substring(0, len - n) + "." - + digits.substring(len - n); - } - - /** - * Return a double approximation. - * The result is correctly rounded to nearest, with ties rounded away from zero. - * TODO: Should round ties to even. - */ - public double doubleValue() { - final int sign = signum(); - if (sign < 0) { - return -BoundedRational.negate(this).doubleValue(); - } - // We get the mantissa by dividing the numerator by denominator, after - // suitably prescaling them so that the integral part of the result contains - // enough bits. We do the prescaling to avoid any precision loss, so the division result - // is correctly truncated towards zero. - final int apprExp = mNum.bitLength() - mDen.bitLength(); - if (apprExp < -1100 || sign == 0) { - // Bail fast for clearly zero result. - return 0.0; - } - final int neededPrec = apprExp - 80; - final BigInteger dividend = neededPrec < 0 ? mNum.shiftLeft(-neededPrec) : mNum; - final BigInteger divisor = neededPrec > 0 ? mDen.shiftLeft(neededPrec) : mDen; - final BigInteger quotient = dividend.divide(divisor); - final int qLength = quotient.bitLength(); - int extraBits = qLength - 53; - int exponent = neededPrec + qLength; // Exponent assuming leading binary point. - if (exponent >= -1021) { - // Binary point is actually to right of leading bit. - --exponent; - } else { - // We're in the gradual underflow range. Drop more bits. - extraBits += (-1022 - exponent) + 1; - exponent = -1023; - } - final BigInteger bigMantissa = - quotient.add(BigInteger.ONE.shiftLeft(extraBits - 1)).shiftRight(extraBits); - if (exponent > 1024) { - return Double.POSITIVE_INFINITY; - } - if (exponent > -1023 && bigMantissa.bitLength() != 53 - || exponent <= -1023 && bigMantissa.bitLength() >= 53) { - throw new AssertionError("doubleValue internal error"); - } - final long mantissa = bigMantissa.longValue(); - final long bits = (mantissa & ((1l << 52) - 1)) | (((long) exponent + 1023) << 52); - return Double.longBitsToDouble(bits); - } - - public CR crValue() { - return CR.valueOf(mNum).divide(CR.valueOf(mDen)); - } - - public int intValue() { - BoundedRational reduced = reduce(); - if (!reduced.mDen.equals(BigInteger.ONE)) { - throw new ArithmeticException("intValue of non-int"); - } - return reduced.mNum.intValue(); - } - - // Approximate number of bits to left of binary point. - // Negative indicates leading zeroes to the right of binary point. - public int wholeNumberBits() { - if (mNum.signum() == 0) { - return Integer.MIN_VALUE; - } else { - return mNum.bitLength() - mDen.bitLength(); - } - } - - /** - * Is this number too big for us to continue with rational arithmetic? - * We return fals for integers on the assumption that we have no better fallback. - */ - private boolean tooBig() { - if (mDen.equals(BigInteger.ONE)) { - return false; - } - return (mNum.bitLength() + mDen.bitLength() > MAX_SIZE); - } - - /** - * Return an equivalent fraction with a positive denominator. - */ - private BoundedRational positiveDen() { - if (mDen.signum() > 0) { - return this; - } - return new BoundedRational(mNum.negate(), mDen.negate()); - } - - /** - * Return an equivalent fraction in lowest terms. - * Denominator sign may remain negative. - */ - private BoundedRational reduce() { - if (mDen.equals(BigInteger.ONE)) { - return this; // Optimization only - } - final BigInteger divisor = mNum.gcd(mDen); - return new BoundedRational(mNum.divide(divisor), mDen.divide(divisor)); - } - - static Random sReduceRng = new Random(); - - /** - * Return a possibly reduced version of r that's not tooBig(). - * Return null if none exists. - */ - private static BoundedRational maybeReduce(BoundedRational r) { - if (r == null) return null; - // Reduce randomly, with 1/16 probability, or if the result is too big. - if (!r.tooBig() && (sReduceRng.nextInt() & 0xf) != 0) { - return r; - } - BoundedRational result = r.positiveDen(); - result = result.reduce(); - if (!result.tooBig()) { - return result; - } - return null; - } - - public int compareTo(BoundedRational r) { - // Compare by multiplying both sides by denominators, invert result if denominator product - // was negative. - return mNum.multiply(r.mDen).compareTo(r.mNum.multiply(mDen)) * mDen.signum() - * r.mDen.signum(); - } - - public int signum() { - return mNum.signum() * mDen.signum(); - } - - @Override - public int hashCode() { - // Note that this may be too expensive to be useful. - BoundedRational reduced = reduce().positiveDen(); - return Objects.hash(reduced.mNum, reduced.mDen); - } - - @Override - public boolean equals(Object r) { - return r != null && r instanceof BoundedRational && compareTo((BoundedRational) r) == 0; - } - - // We use static methods for arithmetic, so that we can easily handle the null case. We try - // to catch domain errors whenever possible, sometimes even when one of the arguments is null, - // but not relevant. - - /** - * Returns equivalent BigInteger result if it exists, null if not. - */ - public static BigInteger asBigInteger(BoundedRational r) { - if (r == null) { - return null; - } - final BigInteger[] quotAndRem = r.mNum.divideAndRemainder(r.mDen); - if (quotAndRem[1].signum() == 0) { - return quotAndRem[0]; - } else { - return null; - } - } - public static BoundedRational add(BoundedRational r1, BoundedRational r2) { - if (r1 == null || r2 == null) { - return null; - } - final BigInteger den = r1.mDen.multiply(r2.mDen); - final BigInteger num = r1.mNum.multiply(r2.mDen).add(r2.mNum.multiply(r1.mDen)); - return maybeReduce(new BoundedRational(num,den)); - } - - /** - * Return the argument, but with the opposite sign. - * Returns null only for a null argument. - */ - public static BoundedRational negate(BoundedRational r) { - if (r == null) { - return null; - } - return new BoundedRational(r.mNum.negate(), r.mDen); - } - - public static BoundedRational subtract(BoundedRational r1, BoundedRational r2) { - return add(r1, negate(r2)); - } - - /** - * Return product of r1 and r2 without reducing the result. - */ - private static BoundedRational rawMultiply(BoundedRational r1, BoundedRational r2) { - // It's tempting but marginally unsound to reduce 0 * null to 0. The null could represent - // an infinite value, for which we failed to throw an exception because it was too big. - if (r1 == null || r2 == null) { - return null; - } - // Optimize the case of our special ONE constant, since that's cheap and somewhat frequent. - if (r1 == ONE) { - return r2; - } - if (r2 == ONE) { - return r1; - } - final BigInteger num = r1.mNum.multiply(r2.mNum); - final BigInteger den = r1.mDen.multiply(r2.mDen); - return new BoundedRational(num,den); - } - - public static BoundedRational multiply(BoundedRational r1, BoundedRational r2) { - return maybeReduce(rawMultiply(r1, r2)); - } - - public static class ZeroDivisionException extends ArithmeticException { - public ZeroDivisionException() { - super("Division by zero"); - } - } - - /** - * Return the reciprocal of r (or null if the argument was null). - */ - public static BoundedRational inverse(BoundedRational r) { - if (r == null) { - return null; - } - if (r.mNum.signum() == 0) { - throw new ZeroDivisionException(); - } - return new BoundedRational(r.mDen, r.mNum); - } - - public static BoundedRational divide(BoundedRational r1, BoundedRational r2) { - return multiply(r1, inverse(r2)); - } - - public static BoundedRational sqrt(BoundedRational r) { - // Return non-null if numerator and denominator are small perfect squares. - if (r == null) { - return null; - } - r = r.positiveDen().reduce(); - if (r.mNum.signum() < 0) { - throw new ArithmeticException("sqrt(negative)"); - } - final BigInteger num_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(r.mNum.doubleValue()))); - if (!num_sqrt.multiply(num_sqrt).equals(r.mNum)) { - return null; - } - final BigInteger den_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(r.mDen.doubleValue()))); - if (!den_sqrt.multiply(den_sqrt).equals(r.mDen)) { - return null; - } - return new BoundedRational(num_sqrt, den_sqrt); - } - - public final static BoundedRational ZERO = new BoundedRational(0); - public final static BoundedRational HALF = new BoundedRational(1,2); - public final static BoundedRational MINUS_HALF = new BoundedRational(-1,2); - public final static BoundedRational THIRD = new BoundedRational(1,3); - public final static BoundedRational QUARTER = new BoundedRational(1,4); - public final static BoundedRational SIXTH = new BoundedRational(1,6); - public final static BoundedRational ONE = new BoundedRational(1); - public final static BoundedRational MINUS_ONE = new BoundedRational(-1); - public final static BoundedRational TWO = new BoundedRational(2); - public final static BoundedRational MINUS_TWO = new BoundedRational(-2); - public final static BoundedRational TEN = new BoundedRational(10); - public final static BoundedRational TWELVE = new BoundedRational(12); - public final static BoundedRational THIRTY = new BoundedRational(30); - public final static BoundedRational MINUS_THIRTY = new BoundedRational(-30); - public final static BoundedRational FORTY_FIVE = new BoundedRational(45); - public final static BoundedRational MINUS_FORTY_FIVE = new BoundedRational(-45); - public final static BoundedRational NINETY = new BoundedRational(90); - public final static BoundedRational MINUS_NINETY = new BoundedRational(-90); - - private static final BigInteger BIG_TWO = BigInteger.valueOf(2); - private static final BigInteger BIG_MINUS_ONE = BigInteger.valueOf(-1); - - /** - * Compute integral power of this, assuming this has been reduced and exp is >= 0. - */ - private BoundedRational rawPow(BigInteger exp) { - if (exp.equals(BigInteger.ONE)) { - return this; - } - if (exp.and(BigInteger.ONE).intValue() == 1) { - return rawMultiply(rawPow(exp.subtract(BigInteger.ONE)), this); - } - if (exp.signum() == 0) { - return ONE; - } - BoundedRational tmp = rawPow(exp.shiftRight(1)); - if (Thread.interrupted()) { - throw new CR.AbortedException(); - } - BoundedRational result = rawMultiply(tmp, tmp); - if (result == null || result.tooBig()) { - return null; - } - return result; - } - - /** - * Compute an integral power of this. - */ - public BoundedRational pow(BigInteger exp) { - int expSign = exp.signum(); - if (expSign == 0) { - // Questionable if base has undefined or zero value. - // java.lang.Math.pow() returns 1 anyway, so we do the same. - return BoundedRational.ONE; - } - if (exp.equals(BigInteger.ONE)) { - return this; - } - // Reducing once at the beginning means there's no point in reducing later. - BoundedRational reduced = reduce().positiveDen(); - // First handle cases in which huge exponents could give compact results. - if (reduced.mDen.equals(BigInteger.ONE)) { - if (reduced.mNum.equals(BigInteger.ZERO)) { - return ZERO; - } - if (reduced.mNum.equals(BigInteger.ONE)) { - return ONE; - } - if (reduced.mNum.equals(BIG_MINUS_ONE)) { - if (exp.testBit(0)) { - return MINUS_ONE; - } else { - return ONE; - } - } - } - if (exp.bitLength() > 1000) { - // Stack overflow is likely; a useful rational result is not. - return null; - } - if (expSign < 0) { - return inverse(reduced).rawPow(exp.negate()); - } else { - return reduced.rawPow(exp); - } - } - - public static BoundedRational pow(BoundedRational base, BoundedRational exp) { - if (exp == null) { - return null; - } - if (base == null) { - return null; - } - exp = exp.reduce().positiveDen(); - if (!exp.mDen.equals(BigInteger.ONE)) { - return null; - } - return base.pow(exp.mNum); - } - - - private static final BigInteger BIG_FIVE = BigInteger.valueOf(5); - - /** - * Return the number of decimal digits to the right of the decimal point required to represent - * the argument exactly. - * Return Integer.MAX_VALUE if that's not possible. Never returns a value less than zero, even - * if r is a power of ten. - */ - public static int digitsRequired(BoundedRational r) { - if (r == null) { - return Integer.MAX_VALUE; - } - int powersOfTwo = 0; // Max power of 2 that divides denominator - int powersOfFive = 0; // Max power of 5 that divides denominator - // Try the easy case first to speed things up. - if (r.mDen.equals(BigInteger.ONE)) { - return 0; - } - r = r.reduce(); - BigInteger den = r.mDen; - if (den.bitLength() > MAX_SIZE) { - return Integer.MAX_VALUE; - } - while (!den.testBit(0)) { - ++powersOfTwo; - den = den.shiftRight(1); - } - while (den.mod(BIG_FIVE).signum() == 0) { - ++powersOfFive; - den = den.divide(BIG_FIVE); - } - // If the denominator has a factor of other than 2 or 5 (the divisors of 10), the decimal - // expansion does not terminate. Multiplying the fraction by any number of powers of 10 - // will not cancel the demoniator. (Recall the fraction was in lowest terms to start - // with.) Otherwise the powers of 10 we need to cancel the denominator is the larger of - // powersOfTwo and powersOfFive. - if (!den.equals(BigInteger.ONE) && !den.equals(BIG_MINUS_ONE)) { - return Integer.MAX_VALUE; - } - return Math.max(powersOfTwo, powersOfFive); - } -} diff --git a/app/src/main/java/com/prime/toolz2/core/math/StringUtils.java b/app/src/main/java/com/prime/toolz2/core/math/StringUtils.java deleted file mode 100644 index 97f79b5..0000000 --- a/app/src/main/java/com/prime/toolz2/core/math/StringUtils.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.prime.toolz2.core.math; - -/** - * Some helpful methods operating on strings. - */ - -class StringUtils { - - /** - * Return a string with n copies of c. - */ - public static String repeat(char c, int n) { - final StringBuilder result = new StringBuilder(); - for (int i = 0; i < n; ++i) { - result.append(c); - } - return result.toString(); - } - - /** - * Return a copy of the supplied string with commas added every three digits. - * The substring indicated by the supplied range is assumed to contain only - * a whole number, with no decimal point. - * Inserting a digit separator every 3 digits appears to be - * at least somewhat acceptable, though not necessarily preferred, everywhere. - * The grouping separator in the result is NOT localized. - */ - public static String addCommas(String s, int begin, int end) { - // Resist the temptation to use Java's NumberFormat, which converts to long or double - // and hence doesn't handle very large numbers. - StringBuilder result = new StringBuilder(); - int current = begin; - while (current < end && (s.charAt(current) == '-' || s.charAt(current) == ' ')) { - ++current; - } - result.append(s, begin, current); - while (current < end) { - result.append(s.charAt(current)); - ++current; - if ((end - current) % 3 == 0 && end != current) { - result.append(','); - } - } - return result.toString(); - } - - /** - * Ignoring all occurrences of c in both strings, check whether old is a prefix of new. - * If so, return the remaining subsequence of whole. If not, return null. - */ - public static CharSequence getExtensionIgnoring(CharSequence whole, CharSequence prefix, - char c) { - int wIndex = 0; - int pIndex = 0; - final int wLen = whole.length(); - final int pLen = prefix.length(); - while (true) { - while (pIndex < pLen && prefix.charAt(pIndex) == c) { - ++pIndex; - } - while (wIndex < wLen && whole.charAt(wIndex) == c) { - ++wIndex; - } - if (pIndex == pLen) { - break; - } - if (wIndex == wLen || whole.charAt(wIndex) != prefix.charAt(pIndex) ) { - return null; - } - ++pIndex; - ++wIndex; - } - while (wIndex < wLen && whole.charAt(wIndex) == c) { - ++wIndex; - } - return whole.subSequence(wIndex, wLen); - } -} diff --git a/app/src/main/java/com/prime/toolz2/core/math/UnifiedReal.java b/app/src/main/java/com/prime/toolz2/core/math/UnifiedReal.java deleted file mode 100644 index aec21fb..0000000 --- a/app/src/main/java/com/prime/toolz2/core/math/UnifiedReal.java +++ /dev/null @@ -1,1287 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.prime.toolz2.core.math; - -import com.prime.toolz2.core.math.BoundedRational; -import com.prime.toolz2.core.math.creals.CR; -import com.prime.toolz2.core.math.creals.UnaryCRFunction; - -import java.math.BigInteger; - -/** - * Computable real numbers, represented so that we can get exact decidable comparisons - * for a number of interesting special cases, including rational computations. - * - * A real number is represented as the product of two numbers with different representations: - * A) A BoundedRational that can only represent a subset of the rationals, but supports - * exact computable comparisons. - * B) A lazily evaluated "constructive real number" that provides operations to evaluate - * itself to any requested number of digits. - * Whenever possible, we choose (B) to be one of a small set of known constants about which we - * know more. For example, whenever we can, we represent rationals such that (B) is 1. - * This scheme allows us to do some very limited symbolic computation on numbers when both - * have the same (B) value, as well as in some other situations. We try to maximize that - * possibility. - * - * Arithmetic operations and operations that produce finite approximations may throw unchecked - * exceptions produced by the underlying CR and BoundedRational packages, including - * CR.PrecisionOverflowException and CR.AbortedException. - */ -public class UnifiedReal { - - private final BoundedRational mRatFactor; - private final CR mCrFactor; - // TODO: It would be helpful to add flags to indicate whether the result is known - // irrational, etc. This sometimes happens even if mCrFactor is not one of the known ones. - // And exact comparisons between rationals and known irrationals are decidable. - - /** - * Perform some nontrivial consistency checks. - * @hide - */ - public static boolean enableChecks = true; - - private static void check(boolean b) { - if (!b) { - throw new AssertionError(); - } - } - - private UnifiedReal(BoundedRational rat, CR cr) { - if (rat == null) { - throw new ArithmeticException("Building UnifiedReal from null"); - } - // We don't normally traffic in null CRs, and hence don't test explicitly. - mCrFactor = cr; - mRatFactor = rat; - } - - public UnifiedReal(CR cr) { - this(BoundedRational.ONE, cr); - } - - public UnifiedReal(BoundedRational rat) { - this(rat, CR_ONE); - } - - public UnifiedReal(BigInteger n) { - this(new BoundedRational(n)); - } - - public UnifiedReal(long n) { - this(new BoundedRational(n)); - } - - public static UnifiedReal valueOf(double x) { - if (x == 0.0 || x == 1.0) { - return valueOf((long) x); - } - return new UnifiedReal(BoundedRational.valueOf(x)); - } - - public static UnifiedReal valueOf(long x) { - if (x == 0) { - return UnifiedReal.ZERO; - } else if (x == 1) { - return UnifiedReal.ONE; - } else { - return new UnifiedReal(BoundedRational.valueOf(x)); - } - } - - // Various helpful constants - private final static BigInteger BIG_24 = BigInteger.valueOf(24); - private final static int DEFAULT_COMPARE_TOLERANCE = -1000; - - // Well-known CR constants we try to use in the mCrFactor position: - private final static CR CR_ONE = CR.ONE; - private final static CR CR_PI = CR.PI; - private final static CR CR_E = CR.ONE.exp(); - private final static CR CR_SQRT2 = CR.valueOf(2).sqrt(); - private final static CR CR_SQRT3 = CR.valueOf(3).sqrt(); - private final static CR CR_LN2 = CR.valueOf(2).ln(); - private final static CR CR_LN3 = CR.valueOf(3).ln(); - private final static CR CR_LN5 = CR.valueOf(5).ln(); - private final static CR CR_LN6 = CR.valueOf(6).ln(); - private final static CR CR_LN7 = CR.valueOf(7).ln(); - private final static CR CR_LN10 = CR.valueOf(10).ln(); - - // Square roots that we try to recognize. - // We currently recognize only a small fixed collection, since the sqrt() function needs to - // identify numbers of the form *n^2, and we don't otherwise know of a good - // algorithm for that. - private final static CR sSqrts[] = { - null, - CR.ONE, - CR_SQRT2, - CR_SQRT3, - null, - CR.valueOf(5).sqrt(), - CR.valueOf(6).sqrt(), - CR.valueOf(7).sqrt(), - null, - null, - CR.valueOf(10).sqrt() }; - - // Natural logs of small integers that we try to recognize. - private final static CR sLogs[] = { - null, - null, - CR_LN2, - CR_LN3, - null, - CR_LN5, - CR_LN6, - CR_LN7, - null, - null, - CR_LN10 }; - - - // Some convenient UnifiedReal constants. - public static final UnifiedReal PI = new UnifiedReal(CR_PI); - public static final UnifiedReal E = new UnifiedReal(CR_E); - public static final UnifiedReal ZERO = new UnifiedReal(BoundedRational.ZERO); - public static final UnifiedReal ONE = new UnifiedReal(BoundedRational.ONE); - public static final UnifiedReal MINUS_ONE = new UnifiedReal(BoundedRational.MINUS_ONE); - public static final UnifiedReal TWO = new UnifiedReal(BoundedRational.TWO); - public static final UnifiedReal MINUS_TWO = new UnifiedReal(BoundedRational.MINUS_TWO); - public static final UnifiedReal HALF = new UnifiedReal(BoundedRational.HALF); - public static final UnifiedReal MINUS_HALF = new UnifiedReal(BoundedRational.MINUS_HALF); - public static final UnifiedReal TEN = new UnifiedReal(BoundedRational.TEN); - public static final UnifiedReal RADIANS_PER_DEGREE - = new UnifiedReal(new BoundedRational(1, 180), CR_PI); - private static final UnifiedReal SIX = new UnifiedReal(6); - private static final UnifiedReal HALF_SQRT2 = new UnifiedReal(BoundedRational.HALF, CR_SQRT2); - private static final UnifiedReal SQRT3 = new UnifiedReal(CR_SQRT3); - private static final UnifiedReal HALF_SQRT3 = new UnifiedReal(BoundedRational.HALF, CR_SQRT3); - private static final UnifiedReal THIRD_SQRT3 = new UnifiedReal(BoundedRational.THIRD, CR_SQRT3); - private static final UnifiedReal PI_OVER_2 = new UnifiedReal(BoundedRational.HALF, CR_PI); - private static final UnifiedReal PI_OVER_3 = new UnifiedReal(BoundedRational.THIRD, CR_PI); - private static final UnifiedReal PI_OVER_4 = new UnifiedReal(BoundedRational.QUARTER, CR_PI); - private static final UnifiedReal PI_OVER_6 = new UnifiedReal(BoundedRational.SIXTH, CR_PI); - - - /** - * Given a constructive real cr, try to determine whether cr is the square root of - * a small integer. If so, return its square as a BoundedRational. Otherwise return null. - * We make this determination by simple table lookup, so spurious null returns are - * entirely possible, or even likely. - */ - private static BoundedRational getSquare(CR cr) { - for (int i = 0; i < sSqrts.length; ++i) { - if (sSqrts[i] == cr) { - return new BoundedRational(i); - } - } - return null; - } - - /** - * Given a constructive real cr, try to determine whether cr is the logarithm of a small - * integer. If so, return exp(cr) as a BoundedRational. Otherwise return null. - * We make this determination by simple table lookup, so spurious null returns are - * entirely possible, or even likely. - */ - private BoundedRational getExp(CR cr) { - for (int i = 0; i < sLogs.length; ++i) { - if (sLogs[i] == cr) { - return new BoundedRational(i); - } - } - return null; - } - - /** - * If the argument is a well-known constructive real, return its name. - * The name of "CR_ONE" is the empty string. - * No named constructive reals are rational multiples of each other. - * Thus two UnifiedReals with different named mCrFactors can be equal only if both - * mRatFactors are zero or possibly if one is CR_PI and the other is CR_E. - * (The latter is apparently an open problem.) - */ - private static String crName(CR cr) { - if (cr == CR_ONE) { - return ""; - } - if (cr == CR_PI) { - return "\u03C0"; // GREEK SMALL LETTER PI - } - if (cr == CR_E) { - return "e"; - } - for (int i = 0; i < sSqrts.length; ++i) { - if (cr == sSqrts[i]) { - return "\u221A" /* SQUARE ROOT */ + i; - } - } - for (int i = 0; i < sLogs.length; ++i) { - if (cr == sLogs[i]) { - return "ln(" + i + ")"; - } - } - return null; - } - - /** - * Would crName() return non-Null? - */ - private static boolean isNamed(CR cr) { - if (cr == CR_ONE || cr == CR_PI || cr == CR_E) { - return true; - } - for (CR r: sSqrts) { - if (cr == r) { - return true; - } - } - for (CR r: sLogs) { - if (cr == r) { - return true; - } - } - return false; - } - - /** - * Is cr known to be algebraic (as opposed to transcendental)? - * Currently only produces meaningful results for the above known special - * constructive reals. - */ - private static boolean definitelyAlgebraic(CR cr) { - return cr == CR_ONE || getSquare(cr) != null; - } - - /** - * Is this number known to be rational? - */ - public boolean definitelyRational() { - return mCrFactor == CR_ONE || mRatFactor.signum() == 0; - } - - /** - * Is this number known to be irrational? - * TODO: We could track the fact that something is irrational with an explicit flag, which - * could cover many more cases. Whether that matters in practice is TBD. - */ - public boolean definitelyIrrational() { - return !definitelyRational() && isNamed(mCrFactor); - } - - /** - * Is this number known to be algebraic? - */ - public boolean definitelyAlgebraic() { - return definitelyAlgebraic(mCrFactor) || mRatFactor.signum() == 0; - } - - /** - * Is this number known to be transcendental? - */ - public boolean definitelyTranscendental() { - return !definitelyAlgebraic() && isNamed(mCrFactor); - } - - - /** - * Is it known that the two constructive reals differ by something other than a - * a rational factor, i.e. is it known that two UnifiedReals - * with those mCrFactors will compare unequal unless both mRatFactors are zero? - * If this returns true, then a comparison of two UnifiedReals using those two - * mCrFactors cannot diverge, though we don't know of a good runtime bound. - */ - private static boolean definitelyIndependent(CR r1, CR r2) { - // The question here is whether r1 = x*r2, where x is rational, where r1 and r2 - // are in our set of special known CRs, can have a solution. - // This cannot happen if one is CR_ONE and the other is not. - // (Since all others are irrational.) - // This cannot happen for two named square roots, which have no repeated factors. - // (To see this, square both sides of the equation and factor. Each prime - // factor in the numerator and denominator occurs twice.) - // This cannot happen for e or pi on one side, and a square root on the other. - // (One is transcendental, the other is algebraic.) - // This cannot happen for two of our special natural logs. - // (Otherwise ln(m) = (a/b)ln(n) ==> m = n^(a/b) ==> m^b = n^a, which is impossible - // because either m or n includes a prime factor not shared by the other.) - // This cannot happen for a log and a square root. - // (The Lindemann-Weierstrass theorem tells us, among other things, that if - // a is algebraic, then exp(a) is transcendental. Thus if l in our finite - // set of logs where algebraic, expl(l), must be transacendental. - // But exp(l) is an integer. Thus the logs are transcendental. But of course the - // square roots are algebraic. Thus they can't be rational multiples.) - // Unfortunately, we do not know whether e/pi is rational. - if (r1 == r2) { - return false; - } - CR other; - if (r1 == CR_E || r1 == CR_PI) { - return definitelyAlgebraic(r2); - } - if (r2 == CR_E || r2 == CR_PI) { - return definitelyAlgebraic(r1); - } - return isNamed(r1) && isNamed(r2); - } - - /** - * Convert to String reflecting raw representation. - * Debug or log messages only, not pretty. - */ - public String toString() { - return mRatFactor.toString() + "*" + mCrFactor.toString(); - } - - /** - * Convert to readable String. - * Intended for user output. Produces exact expression when possible. - */ - public String toNiceString() { - if (mCrFactor == CR_ONE || mRatFactor.signum() == 0) { - return mRatFactor.toNiceString(); - } - String name = crName(mCrFactor); - if (name != null) { - BigInteger bi = BoundedRational.asBigInteger(mRatFactor); - if (bi != null) { - if (bi.equals(BigInteger.ONE)) { - return name; - } - return mRatFactor.toNiceString() + name; - } - return "(" + mRatFactor.toNiceString() + ")" + name; - } - if (mRatFactor.equals(BoundedRational.ONE)) { - return mCrFactor.toString(); - } - return crValue().toString(); - } - - /** - * Will toNiceString() produce an exact representation? - */ - public boolean exactlyDisplayable() { - return crName(mCrFactor) != null; - } - - // Number of extra bits used in evaluation below to prefer truncation to rounding. - // Must be <= 30. - private final static int EXTRA_PREC = 10; - - /* - * Returns a truncated representation of the result. - * If exactlyTruncatable(), we round correctly towards zero. Otherwise the resulting digit - * string may occasionally be rounded up instead. - * Always includes a decimal point in the result. - * The result includes n digits to the right of the decimal point. - * @param n result precision, >= 0 - */ - public String toStringTruncated(int n) { - if (mCrFactor == CR_ONE || mRatFactor == BoundedRational.ZERO) { - return mRatFactor.toStringTruncated(n); - } - final CR scaled = CR.valueOf(BigInteger.TEN.pow(n)).multiply(crValue()); - boolean negative = false; - BigInteger intScaled; - if (exactlyTruncatable()) { - intScaled = scaled.get_appr(0); - if (intScaled.signum() < 0) { - negative = true; - intScaled = intScaled.negate(); - } - if (CR.valueOf(intScaled).compareTo(scaled.abs()) > 0) { - intScaled = intScaled.subtract(BigInteger.ONE); - } - check(CR.valueOf(intScaled).compareTo(scaled.abs()) < 0); - } else { - // Approximate case. Exact comparisons are impossible. - intScaled = scaled.get_appr(-EXTRA_PREC); - if (intScaled.signum() < 0) { - negative = true; - intScaled = intScaled.negate(); - } - intScaled = intScaled.shiftRight(EXTRA_PREC); - } - String digits = intScaled.toString(); - int len = digits.length(); - if (len < n + 1) { - digits = StringUtils.repeat('0', n + 1 - len) + digits; - len = n + 1; - } - return (negative ? "-" : "") + digits.substring(0, len - n) + "." - + digits.substring(len - n); - } - - /* - * Can we compute correctly truncated approximations of this number? - */ - public boolean exactlyTruncatable() { - // If the value is known rational, we can do exact comparisons. - // If the value is known irrational, then we can safely compare to rational approximations; - // equality is impossible; hence the comparison must converge. - // The only problem cases are the ones in which we don't know. - return mCrFactor == CR_ONE || mRatFactor == BoundedRational.ZERO || definitelyIrrational(); - } - - /** - * Return a double approximation. - * Rational arguments are currently rounded to nearest, with ties away from zero. - * TODO: Improve rounding. - */ - public double doubleValue() { - if (mCrFactor == CR_ONE) { - return mRatFactor.doubleValue(); // Hopefully correctly rounded - } else { - return crValue().doubleValue(); // Approximately correctly rounded - } - } - - public CR crValue() { - return mRatFactor.crValue().multiply(mCrFactor); - } - - /** - * Are this and r exactly comparable? - */ - public boolean isComparable(UnifiedReal u) { - // We check for ONE only to speed up the common case. - // The use of a tolerance here means we can spuriously return false, not true. - return mCrFactor == u.mCrFactor - && (isNamed(mCrFactor) || mCrFactor.signum(DEFAULT_COMPARE_TOLERANCE) != 0) - || mRatFactor.signum() == 0 && u.mRatFactor.signum() == 0 - || definitelyIndependent(mCrFactor, u.mCrFactor) - || crValue().compareTo(u.crValue(), DEFAULT_COMPARE_TOLERANCE) != 0; - } - - /** - * Return +1 if this is greater than r, -1 if this is less than r, or 0 of the two are - * known to be equal. - * May diverge if the two are equal and !isComparable(r). - */ - public int compareTo(UnifiedReal u) { - if (definitelyZero() && u.definitelyZero()) return 0; - if (mCrFactor == u.mCrFactor) { - int signum = mCrFactor.signum(); // Can diverge if mCRFactor == 0. - return signum * mRatFactor.compareTo(u.mRatFactor); - } - return crValue().compareTo(u.crValue()); // Can also diverge. - } - - /** - * Return +1 if this is greater than r, -1 if this is less than r, or possibly 0 of the two are - * within 2^a of each other. - */ - public int compareTo(UnifiedReal u, int a) { - if (isComparable(u)) { - return compareTo(u); - } else { - return crValue().compareTo(u.crValue(), a); - } - } - - /** - * Return compareTo(ZERO, a). - */ - public int signum(int a) { - return compareTo(ZERO, a); - } - - /** - * Return compareTo(ZERO). - * May diverge for ZERO argument if !isComparable(ZERO). - */ - public int signum() { - return compareTo(ZERO); - } - - /** - * Equality comparison. May erroneously return true if values differ by less than 2^a, - * and !isComparable(u). - */ - public boolean approxEquals(UnifiedReal u, int a) { - if (isComparable(u)) { - if (definitelyIndependent(mCrFactor, u.mCrFactor) - && (mRatFactor.signum() != 0 || u.mRatFactor.signum() != 0)) { - // No need to actually evaluate, though we don't know which is larger. - return false; - } else { - return compareTo(u) == 0; - } - } - return crValue().compareTo(u.crValue(), a) == 0; - } - - /** - * Returns true if values are definitely known to be equal, false in all other cases. - * This does not satisfy the contract for Object.equals(). - */ - public boolean definitelyEquals(UnifiedReal u) { - return isComparable(u) && compareTo(u) == 0; - } - - @Override - public int hashCode() { - // Better useless than wrong. Probably. - return 0; - } - - @Override - public boolean equals(Object r) { - if (r == null || !(r instanceof UnifiedReal)) { - return false; - } - // This is almost certainly a programming error. Don't even try. - throw new AssertionError("Can't compare UnifiedReals for exact equality"); - } - - /** - * Returns true if values are definitely known not to be equal, false in all other cases. - * Performs no approximate evaluation. - */ - public boolean definitelyNotEquals(UnifiedReal u) { - boolean isNamed = isNamed(mCrFactor); - boolean uIsNamed = isNamed(u.mCrFactor); - if (isNamed && uIsNamed) { - if (definitelyIndependent(mCrFactor, u.mCrFactor)) { - return mRatFactor.signum() != 0 || u.mRatFactor.signum() != 0; - } else if (mCrFactor == u.mCrFactor) { - return !mRatFactor.equals(u.mRatFactor); - } - return !mRatFactor.equals(u.mRatFactor); - } - if (mRatFactor.signum() == 0) { - return uIsNamed && u.mRatFactor.signum() != 0; - } - if (u.mRatFactor.signum() == 0) { - return isNamed && mRatFactor.signum() != 0; - } - return false; - } - - // And some slightly faster convenience functions for special cases: - - public boolean definitelyZero() { - return mRatFactor.signum() == 0; - } - - /** - * Can this number be determined to be definitely nonzero without performing approximate - * evaluation? - */ - public boolean definitelyNonZero() { - return isNamed(mCrFactor) && mRatFactor.signum() != 0; - } - - public boolean definitelyOne() { - return mCrFactor == CR_ONE && mRatFactor.equals(BoundedRational.ONE); - } - - /** - * Return equivalent BoundedRational, if known to exist, null otherwise - */ - public BoundedRational boundedRationalValue() { - if (mCrFactor == CR_ONE || mRatFactor.signum() == 0) { - return mRatFactor; - } - return null; - } - - /** - * Returns equivalent BigInteger result if it exists, null if not. - */ - public BigInteger bigIntegerValue() { - final BoundedRational r = boundedRationalValue(); - return BoundedRational.asBigInteger(r); - } - - public UnifiedReal add(UnifiedReal u) { - if (mCrFactor == u.mCrFactor) { - BoundedRational nRatFactor = BoundedRational.add(mRatFactor, u.mRatFactor); - if (nRatFactor != null) { - return new UnifiedReal(nRatFactor, mCrFactor); - } - } - if (definitelyZero()) { - // Avoid creating new mCrFactor, even if they don't currently match. - return u; - } - if (u.definitelyZero()) { - return this; - } - return new UnifiedReal(crValue().add(u.crValue())); - } - - public UnifiedReal negate() { - return new UnifiedReal(BoundedRational.negate(mRatFactor), mCrFactor); - } - - public UnifiedReal subtract(UnifiedReal u) { - return add(u.negate()); - } - - public UnifiedReal multiply(UnifiedReal u) { - // Preserve a preexisting mCrFactor when we can. - if (mCrFactor == CR_ONE) { - BoundedRational nRatFactor = BoundedRational.multiply(mRatFactor, u.mRatFactor); - if (nRatFactor != null) { - return new UnifiedReal(nRatFactor, u.mCrFactor); - } - } - if (u.mCrFactor == CR_ONE) { - BoundedRational nRatFactor = BoundedRational.multiply(mRatFactor, u.mRatFactor); - if (nRatFactor != null) { - return new UnifiedReal(nRatFactor, mCrFactor); - } - } - if (definitelyZero() || u.definitelyZero()) { - return ZERO; - } - if (mCrFactor == u.mCrFactor) { - BoundedRational square = getSquare(mCrFactor); - if (square != null) { - BoundedRational nRatFactor = BoundedRational.multiply( - BoundedRational.multiply(square, mRatFactor), u.mRatFactor); - if (nRatFactor != null) { - return new UnifiedReal(nRatFactor); - } - } - } - // Probably a bit cheaper to multiply component-wise. - BoundedRational nRatFactor = BoundedRational.multiply(mRatFactor, u.mRatFactor); - if (nRatFactor != null) { - return new UnifiedReal(nRatFactor, mCrFactor.multiply(u.mCrFactor)); - } - return new UnifiedReal(crValue().multiply(u.crValue())); - } - - public static class ZeroDivisionException extends ArithmeticException { - public ZeroDivisionException() { - super("Division by zero"); - } - } - - /** - * Return the reciprocal. - */ - public UnifiedReal inverse() { - if (definitelyZero()) { - throw new ZeroDivisionException(); - } - BoundedRational square = getSquare(mCrFactor); - if (square != null) { - // 1/sqrt(n) = sqrt(n)/n - BoundedRational nRatFactor = BoundedRational.inverse( - BoundedRational.multiply(mRatFactor, square)); - if (nRatFactor != null) { - return new UnifiedReal(nRatFactor, mCrFactor); - } - } - return new UnifiedReal(BoundedRational.inverse(mRatFactor), mCrFactor.inverse()); - } - - public UnifiedReal divide(UnifiedReal u) { - if (mCrFactor == u.mCrFactor) { - if (u.definitelyZero()) { - throw new ZeroDivisionException(); - } - BoundedRational nRatFactor = BoundedRational.divide(mRatFactor, u.mRatFactor); - if (nRatFactor != null) { - return new UnifiedReal(nRatFactor, CR_ONE); - } - } - return multiply(u.inverse()); - } - - /** - * Return the square root. - * This may fail to return a known rational value, even when the result is rational. - */ - public UnifiedReal sqrt() { - if (definitelyZero()) { - return ZERO; - } - if (mCrFactor == CR_ONE) { - BoundedRational ratSqrt; - // Check for all arguments of the form * small_int, - // where small_int has a known sqrt. This includes the small_int = 1 case. - for (int divisor = 1; divisor < sSqrts.length; ++divisor) { - if (sSqrts[divisor] != null) { - ratSqrt = BoundedRational.sqrt( - BoundedRational.divide(mRatFactor, new BoundedRational(divisor))); - if (ratSqrt != null) { - return new UnifiedReal(ratSqrt, sSqrts[divisor]); - } - } - } - } - return new UnifiedReal(crValue().sqrt()); - } - - /** - * Return (this mod 2pi)/(pi/6) as a BigInteger, or null if that isn't easily possible. - */ - private BigInteger getPiTwelfths() { - if (definitelyZero()) return BigInteger.ZERO; - if (mCrFactor == CR_PI) { - BigInteger quotient = BoundedRational.asBigInteger( - BoundedRational.multiply(mRatFactor, BoundedRational.TWELVE)); - if (quotient == null) { - return null; - } - return quotient.mod(BIG_24); - } - return null; - } - - /** - * Computer the sin() for an integer multiple n of pi/12, if easily representable. - * @param n value between 0 and 23 inclusive. - */ - private static UnifiedReal sinPiTwelfths(int n) { - if (n >= 12) { - UnifiedReal negResult = sinPiTwelfths(n - 12); - return negResult == null ? null : negResult.negate(); - } - switch (n) { - case 0: - return ZERO; - case 2: // 30 degrees - return HALF; - case 3: // 45 degrees - return HALF_SQRT2; - case 4: // 60 degrees - return HALF_SQRT3; - case 6: - return ONE; - case 8: - return HALF_SQRT3; - case 9: - return HALF_SQRT2; - case 10: - return HALF; - default: - return null; - } - } - - public UnifiedReal sin() { - BigInteger piTwelfths = getPiTwelfths(); - if (piTwelfths != null) { - UnifiedReal result = sinPiTwelfths(piTwelfths.intValue()); - if (result != null) { - return result; - } - } - return new UnifiedReal(crValue().sin()); - } - - private static UnifiedReal cosPiTwelfths(int n) { - int sinArg = n + 6; - if (sinArg >= 24) { - sinArg -= 24; - } - return sinPiTwelfths(sinArg); - } - - public UnifiedReal cos() { - BigInteger piTwelfths = getPiTwelfths(); - if (piTwelfths != null) { - UnifiedReal result = cosPiTwelfths(piTwelfths.intValue()); - if (result != null) { - return result; - } - } - return new UnifiedReal(crValue().cos()); - } - - public UnifiedReal tan() { - BigInteger piTwelfths = getPiTwelfths(); - if (piTwelfths != null) { - int i = piTwelfths.intValue(); - if (i == 6 || i == 18) { - throw new ArithmeticException("Tangent undefined"); - } - UnifiedReal top = sinPiTwelfths(i); - UnifiedReal bottom = cosPiTwelfths(i); - if (top != null && bottom != null) { - return top.divide(bottom); - } - } - return sin().divide(cos()); - } - - // Throw an exception if the argument is definitely out of bounds for asin or acos. - private void checkAsinDomain() { - if (isComparable(ONE) && (compareTo(ONE) > 0 || compareTo(MINUS_ONE) < 0)) { - throw new ArithmeticException("inverse trig argument out of range"); - } - } - - /** - * Return asin(n/2). n is between -2 and 2. - */ - public static UnifiedReal asinHalves(int n){ - if (n < 0) { - return (asinHalves(-n).negate()); - } - switch (n) { - case 0: - return ZERO; - case 1: - return new UnifiedReal(BoundedRational.SIXTH, CR.PI); - case 2: - return new UnifiedReal(BoundedRational.HALF, CR.PI); - } - throw new AssertionError("asinHalves: Bad argument"); - } - - /** - * Return asin of this, assuming this is not an integral multiple of a half. - */ - public UnifiedReal asinNonHalves() - { - if (compareTo(ZERO, -10) < 0) { - return negate().asinNonHalves().negate(); - } - if (definitelyEquals(HALF_SQRT2)) { - return new UnifiedReal(BoundedRational.QUARTER, CR_PI); - } - if (definitelyEquals(HALF_SQRT3)) { - return new UnifiedReal(BoundedRational.THIRD, CR_PI); - } - return new UnifiedReal(crValue().asin()); - } - - public UnifiedReal asin() { - checkAsinDomain(); - final BigInteger halves = multiply(TWO).bigIntegerValue(); - if (halves != null) { - return asinHalves(halves.intValue()); - } - if (mCrFactor == CR.ONE || mCrFactor != CR_SQRT2 ||mCrFactor != CR_SQRT3) { - return asinNonHalves(); - } - return new UnifiedReal(crValue().asin()); - } - - public UnifiedReal acos() { - return PI_OVER_2.subtract(asin()); - } - - public UnifiedReal atan() { - if (compareTo(ZERO, -10) < 0) { - return negate().atan().negate(); - } - final BigInteger asBI = bigIntegerValue(); - if (asBI != null && asBI.compareTo(BigInteger.ONE) <= 0) { - final int asInt = asBI.intValue(); - // These seem to be all rational cases: - switch (asInt) { - case 0: - return ZERO; - case 1: - return PI_OVER_4; - default: - throw new AssertionError("Impossible r_int"); - } - } - if (definitelyEquals(THIRD_SQRT3)) { - return PI_OVER_6; - } - if (definitelyEquals(SQRT3)) { - return PI_OVER_3; - } - return new UnifiedReal(UnaryCRFunction.atanFunction.execute(crValue())); - } - - private static final BigInteger BIG_TWO = BigInteger.valueOf(2); - - // The (in abs value) integral exponent for which we attempt to use a recursive - // algorithm for evaluating pow(). The recursive algorithm works independent of the sign of the - // base, and can produce rational results. But it can become slow for very large exponents. - private static final BigInteger RECURSIVE_POW_LIMIT = BigInteger.valueOf(1000); - // The corresponding limit when we're using rational arithmetic. This should fail fast - // anyway, but we avoid ridiculously deep recursion. - private static final BigInteger HARD_RECURSIVE_POW_LIMIT = BigInteger.ONE.shiftLeft(1000); - - /** - * Compute an integral power of a constructive real, using the standard recursive algorithm. - * exp is known to be positive. - */ - private static CR recursivePow(CR base, BigInteger exp) { - if (exp.equals(BigInteger.ONE)) { - return base; - } - if (exp.testBit(0)) { - return base.multiply(recursivePow(base, exp.subtract(BigInteger.ONE))); - } - CR tmp = recursivePow(base, exp.shiftRight(1)); - if (Thread.interrupted()) { - throw new CR.AbortedException(); - } - return tmp.multiply(tmp); - } - - /** - * Compute an integral power of a constructive real, using the exp function when - * we safely can. Use recursivePow when we can't. exp is known to be nozero. - */ - private UnifiedReal expLnPow(BigInteger exp) { - int sign = signum(DEFAULT_COMPARE_TOLERANCE); - if (sign > 0) { - // Safe to take the log. This avoids deep recursion for huge exponents, which - // may actually make sense here. - return new UnifiedReal(crValue().ln().multiply(CR.valueOf(exp)).exp()); - } else if (sign < 0) { - CR result = crValue().negate().ln().multiply(CR.valueOf(exp)).exp(); - if (exp.testBit(0) /* odd exponent */) { - result = result.negate(); - } - return new UnifiedReal(result); - } else { - // Base of unknown sign with integer exponent. Use a recursive computation. - // (Another possible option would be to use the absolute value of the base, and then - // adjust the sign at the end. But that would have to be done in the CR - // implementation.) - if (exp.signum() < 0) { - // This may be very expensive if exp.negate() is large. - return new UnifiedReal(recursivePow(crValue(), exp.negate()).inverse()); - } else { - return new UnifiedReal(recursivePow(crValue(), exp)); - } - } - } - - - /** - * Compute an integral power of this. - * This recurses roughly as deeply as the number of bits in the exponent, and can, in - * ridiculous cases, result in a stack overflow. - */ - private UnifiedReal pow(BigInteger exp) { - if (exp.equals(BigInteger.ONE)) { - return this; - } - if (exp.signum() == 0) { - // Questionable if base has undefined value or is 0. - // Java.lang.Math.pow() returns 1 anyway, so we do the same. - return ONE; - } - BigInteger absExp = exp.abs(); - if (mCrFactor == CR_ONE && absExp.compareTo(HARD_RECURSIVE_POW_LIMIT) <= 0) { - final BoundedRational ratPow = mRatFactor.pow(exp); - // We count on this to fail, e.g. for very large exponents, when it would - // otherwise be too expensive. - if (ratPow != null) { - return new UnifiedReal(ratPow); - } - } - if (absExp.compareTo(RECURSIVE_POW_LIMIT) > 0) { - return expLnPow(exp); - } - BoundedRational square = getSquare(mCrFactor); - if (square != null) { - final BoundedRational nRatFactor = - BoundedRational.multiply(mRatFactor.pow(exp), square.pow(exp.shiftRight(1))); - if (nRatFactor != null) { - if (exp.and(BigInteger.ONE).intValue() == 1) { - // Odd power: Multiply by remaining square root. - return new UnifiedReal(nRatFactor, mCrFactor); - } else { - return new UnifiedReal(nRatFactor); - } - } - } - return expLnPow(exp); - } - - /** - * Return this ^ expon. - * This is really only well-defined for a positive base, particularly since - * 0^x is not continuous at zero. (0^0 = 1 (as is epsilon^0), but 0^epsilon is 0. - * We nonetheless try to do reasonable things at zero, when we recognize that case. - */ - public UnifiedReal pow(UnifiedReal expon) { - if (mCrFactor == CR_E) { - if (mRatFactor.equals(BoundedRational.ONE)) { - return expon.exp(); - } else { - UnifiedReal ratPart = new UnifiedReal(mRatFactor).pow(expon); - return expon.exp().multiply(ratPart); - } - } - final BoundedRational expAsBR = expon.boundedRationalValue(); - if (expAsBR != null) { - BigInteger expAsBI = BoundedRational.asBigInteger(expAsBR); - if (expAsBI != null) { - return pow(expAsBI); - } else { - // Check for exponent that is a multiple of a half. - expAsBI = BoundedRational.asBigInteger( - BoundedRational.multiply(BoundedRational.TWO, expAsBR)); - if (expAsBI != null) { - return pow(expAsBI).sqrt(); - } - } - } - // If the exponent were known zero, we would have handled it above. - if (definitelyZero()) { - return ZERO; - } - int sign = signum(DEFAULT_COMPARE_TOLERANCE); - if (sign < 0) { - throw new ArithmeticException("Negative base for pow() with non-integer exponent"); - } - return new UnifiedReal(crValue().ln().multiply(expon.crValue()).exp()); - } - - /** - * Raise the argument to the 16th power. - */ - private static long pow16(int n) { - if (n > 10) { - throw new AssertionError("Unexpected pow16 argument"); - } - long result = n*n; - result *= result; - result *= result; - result *= result; - return result; - } - - /** - * Return the integral log with respect to the given base if it exists, 0 otherwise. - * n is presumed positive. - */ - private static long getIntLog(BigInteger n, int base) { - double nAsDouble = n.doubleValue(); - double approx = Math.log(nAsDouble)/Math.log(base); - // A relatively quick test first. - // Unfortunately, this doesn't help for values to big to fit in a Double. - if (!Double.isInfinite(nAsDouble) && Math.abs(approx - Math.rint(approx)) > 1.0e-6) { - return 0; - } - long result = 0; - BigInteger remaining = n; - BigInteger bigBase = BigInteger.valueOf(base); - BigInteger base16th = null; // base^16, computed lazily - while (n.mod(bigBase).signum() == 0) { - if (Thread.interrupted()) { - throw new CR.AbortedException(); - } - n = n.divide(bigBase); - ++result; - // And try a slightly faster computation for large n: - if (base16th == null) { - base16th = BigInteger.valueOf(pow16(base)); - } - while (n.mod(base16th).signum() == 0) { - n = n.divide(base16th); - result += 16; - } - } - if (n.equals(BigInteger.ONE)) { - return result; - } - return 0; - } - - public UnifiedReal ln() { - if (mCrFactor == CR_E) { - return new UnifiedReal(mRatFactor, CR_ONE).ln().add(ONE); - } - if (isComparable(ZERO)) { - if (signum() <= 0) { - throw new ArithmeticException("log(non-positive)"); - } - int compare1 = compareTo(ONE, DEFAULT_COMPARE_TOLERANCE); - if (compare1 == 0) { - if (definitelyEquals(ONE)) { - return ZERO; - } - } else if (compare1 < 0) { - return inverse().ln().negate(); - } - final BigInteger bi = BoundedRational.asBigInteger(mRatFactor); - if (bi != null) { - if (mCrFactor == CR_ONE) { - // Check for a power of a small integer. We can use sLogs[] to return - // a more useful answer for those. - for (int i = 0; i < sLogs.length; ++i) { - if (sLogs[i] != null) { - long intLog = getIntLog(bi, i); - if (intLog != 0) { - return new UnifiedReal(new BoundedRational(intLog), sLogs[i]); - } - } - } - } else { - // Check for n^k * sqrt(n), for which we can also return a more useful answer. - BoundedRational square = getSquare(mCrFactor); - if (square != null) { - int intSquare = square.intValue(); - if (sLogs[intSquare] != null) { - long intLog = getIntLog(bi, intSquare); - if (intLog != 0) { - BoundedRational nRatFactor = - BoundedRational.add(new BoundedRational(intLog), - BoundedRational.HALF); - if (nRatFactor != null) { - return new UnifiedReal(nRatFactor, sLogs[intSquare]); - } - } - } - } - } - } - } - return new UnifiedReal(crValue().ln()); - } - - public UnifiedReal exp() { - if (definitelyEquals(ZERO)) { - return ONE; - } - if (definitelyEquals(ONE)) { - // Avoid redundant computations, and ensure we recognize all instances as equal. - return E; - } - final BoundedRational crExp = getExp(mCrFactor); - if (crExp != null) { - boolean needSqrt = false; - BoundedRational ratExponent = mRatFactor; - BigInteger asBI = BoundedRational.asBigInteger(ratExponent); - if (asBI == null) { - // check for multiple of one half. - needSqrt = true; - ratExponent = BoundedRational.multiply(ratExponent, BoundedRational.TWO); - } - BoundedRational nRatFactor = BoundedRational.pow(crExp, ratExponent); - if (nRatFactor != null) { - UnifiedReal result = new UnifiedReal(nRatFactor); - if (needSqrt) { - result = result.sqrt(); - } - return result; - } - } - return new UnifiedReal(crValue().exp()); - } - - - /** - * Generalized factorial. - * Compute n * (n - step) * (n - 2 * step) * etc. This can be used to compute factorial a bit - * faster, especially if BigInteger uses sub-quadratic multiplication. - */ - private static BigInteger genFactorial(long n, long step) { - if (n > 4 * step) { - BigInteger prod1 = genFactorial(n, 2 * step); - if (Thread.interrupted()) { - throw new CR.AbortedException(); - } - BigInteger prod2 = genFactorial(n - step, 2 * step); - if (Thread.interrupted()) { - throw new CR.AbortedException(); - } - return prod1.multiply(prod2); - } else { - if (n == 0) { - return BigInteger.ONE; - } - BigInteger res = BigInteger.valueOf(n); - for (long i = n - step; i > 1; i -= step) { - res = res.multiply(BigInteger.valueOf(i)); - } - return res; - } - } - - - /** - * Factorial function. - * Fails if argument is clearly not an integer. - * May round to nearest integer if value is close. - */ - public UnifiedReal fact() { - BigInteger asBI = bigIntegerValue(); - if (asBI == null) { - asBI = crValue().get_appr(0); // Correct if it was an integer. - if (!approxEquals(new UnifiedReal(asBI), DEFAULT_COMPARE_TOLERANCE)) { - throw new ArithmeticException("Non-integral factorial argument"); - } - } - if (asBI.signum() < 0) { - throw new ArithmeticException("Negative factorial argument"); - } - if (asBI.bitLength() > 20) { - // Will fail. LongValue() may not work. Punt now. - throw new ArithmeticException("Factorial argument too big"); - } - BigInteger biResult = genFactorial(asBI.longValue(), 1); - BoundedRational nRatFactor = new BoundedRational(biResult); - return new UnifiedReal(nRatFactor); - } - - /** - * Return the number of decimal digits to the right of the decimal point required to represent - * the argument exactly. - * Return Integer.MAX_VALUE if that's not possible. Never returns a value less than zero, even - * if r is a power of ten. - */ - public int digitsRequired() { - if (mCrFactor == CR_ONE || mRatFactor.signum() == 0) { - return BoundedRational.digitsRequired(mRatFactor); - } else { - return Integer.MAX_VALUE; - } - } - - /** - * Return an upper bound on the number of leading zero bits. - * These are the number of 0 bits - * to the right of the binary point and to the left of the most significant digit. - * Return Integer.MAX_VALUE if we cannot bound it. - */ - public int leadingBinaryZeroes() { - if (isNamed(mCrFactor)) { - // Only ln(2) is smaller than one, and could possibly add one zero bit. - // Adding 3 gives us a somewhat sloppy upper bound. - final int wholeBits = mRatFactor.wholeNumberBits(); - if (wholeBits == Integer.MIN_VALUE) { - return Integer.MAX_VALUE; - } - if (wholeBits >= 3) { - return 0; - } else { - return -wholeBits + 3; - } - } - return Integer.MAX_VALUE; - } - - /** - * Is the number of bits to the left of the decimal point greater than bound? - * The result is inexact: We roughly approximate the whole number bits. - */ - public boolean approxWholeNumberBitsGreaterThan(int bound) { - if (isNamed(mCrFactor)) { - return mRatFactor.wholeNumberBits() > bound; - } else { - return crValue().get_appr(bound - 2).bitLength() > 2; - } - } -} diff --git a/app/src/main/java/com/prime/toolz2/core/math/Util.java b/app/src/main/java/com/prime/toolz2/core/math/Util.java deleted file mode 100644 index bfc75a8..0000000 --- a/app/src/main/java/com/prime/toolz2/core/math/Util.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (C) 2007-2008 Mihai Preda. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.prime.toolz2.core.math; - -/** - Contains static helper methods for formatting double values. - */ -class Util { - public static final int LEN_UNLIMITED = 100; - public static final int FLOAT_PRECISION = -1; - - /** - Returns an approximation with no more than maxLen chars. - - This method is not public, it is called through doubleToString, - that's why we can make some assumptions about the format of the string, - such as assuming that the exponent 'E' is upper-case. - - @param str the value to truncate (e.g. "-2.898983455E20") - @param maxLen the maximum number of characters in the returned string - @return a truncation no longer then maxLen (e.g. "-2.8E20" for maxLen=7). - */ - static String sizeTruncate(String str, int maxLen) { - if (maxLen == LEN_UNLIMITED) { - return str; - } - int ePos = str.lastIndexOf('E'); - String tail = (ePos != -1) ? str.substring(ePos) : ""; - int tailLen = tail.length(); - int headLen = str.length() - tailLen; - int maxHeadLen = maxLen - tailLen; - int keepLen = Math.min(headLen, maxHeadLen); - if (keepLen < 1 || (keepLen < 2 && str.length() > 0 && str.charAt(0) == '-')) { - return str; // impossible to truncate - } - int dotPos = str.indexOf('.'); - if (dotPos == -1) { - dotPos = headLen; - } - if (dotPos > keepLen) { - int exponent = (ePos != -1) ? Integer.parseInt(str.substring(ePos + 1)) : 0; - int start = str.charAt(0) == '-' ? 1 : 0; - exponent += dotPos - start - 1; - String newStr = str.substring(0, start+1) + '.' + str.substring(start+1, headLen) + 'E' + exponent; - return sizeTruncate(newStr, maxLen); - - } - return str.substring(0, keepLen) + tail; - } - - /** - Rounds by dropping roundingDigits of double precision - (similar to 'hidden precision digits' on calculators), - and formats to String. - @param v the value to be converted to String - @param roundingDigits the number of 'hidden precision' digits (e.g. 2). - @return a String representation of v - */ - public static String doubleToString(final double v, final int roundingDigits) { - final double absv = Math.abs(v); - final String str = roundingDigits == FLOAT_PRECISION ? Float.toString((float) absv) : Double.toString(absv); - StringBuffer buf = new StringBuffer(str); - int roundingStart = (roundingDigits <= 0 || roundingDigits > 13) ? 17 : (16 - roundingDigits); - - int ePos = str.lastIndexOf('E'); - int exp = (ePos != -1) ? Integer.parseInt(str.substring(ePos + 1)) : 0; - if (ePos != -1) { - buf.setLength(ePos); - } - int len = buf.length(); - - //remove dot - int dotPos; - for (dotPos = 0; dotPos < len && buf.charAt(dotPos) != '.';) { - ++dotPos; - } - exp += dotPos; - if (dotPos < len) { - buf.deleteCharAt(dotPos); - --len; - } - - //round - for (int p = 0; p < len && buf.charAt(p) == '0'; ++p) { - ++roundingStart; - } - - if (roundingStart < len) { - if (buf.charAt(roundingStart) >= '5') { - int p; - for (p = roundingStart-1; p >= 0 && buf.charAt(p)=='9'; --p) { - buf.setCharAt(p, '0'); - } - if (p >= 0) { - buf.setCharAt(p, (char)(buf.charAt(p)+1)); - } else { - buf.insert(0, '1'); - ++roundingStart; - ++exp; - } - } - buf.setLength(roundingStart); - } - - //re-insert dot - if ((exp < -5) || (exp > 10)) { - buf.insert(1, '.'); - --exp; - } else { - for (int i = len; i < exp; ++i) { - buf.append('0'); - } - for (int i = exp; i <= 0; ++i) { - buf.insert(0, '0'); - } - buf.insert((exp<=0)? 1 : exp, '.'); - exp = 0; - } - len = buf.length(); - - //remove trailing dot and 0s. - int tail; - for (tail = len-1; tail >= 0 && buf.charAt(tail) == '0'; --tail) { - buf.deleteCharAt(tail); - } - if (tail >= 0 && buf.charAt(tail) == '.') { - buf.deleteCharAt(tail); - } - - if (exp != 0) { - buf.append('E').append(exp); - } - if (v < 0) { - buf.insert(0, '-'); - } - return buf.toString(); - } - - /** - Renders a real number to a String (for user display). - @param maxLen the maximum total length of the resulting string - @param rounding the number of final digits to round - */ - public static String doubleToString(double x, int maxLen, int rounding) { - return sizeTruncate(doubleToString(x, rounding), maxLen); - } - -} diff --git a/app/src/main/java/com/prime/toolz2/core/math/creals/CR.java b/app/src/main/java/com/prime/toolz2/core/math/creals/CR.java deleted file mode 100644 index b0e01cc..0000000 --- a/app/src/main/java/com/prime/toolz2/core/math/creals/CR.java +++ /dev/null @@ -1,1646 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * The above license covers additions and changes by AOSP authors. - * The original code is licensed as follows: - */ - -// -// Copyright (c) 1999, Silicon Graphics, Inc. -- ALL RIGHTS RESERVED -// -// Permission is granted free of charge to copy, modify, use and distribute -// this software provided you include the entirety of this notice in all -// copies made. -// -// THIS SOFTWARE IS PROVIDED ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY -// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, -// WARRANTIES THAT THE SUBJECT SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT -// FOR A PARTICULAR PURPOSE OR NON-INFRINGING. SGI ASSUMES NO RISK AS TO THE -// QUALITY AND PERFORMANCE OF THE SOFTWARE. SHOULD THE SOFTWARE PROVE -// DEFECTIVE IN ANY RESPECT, SGI ASSUMES NO COST OR LIABILITY FOR ANY -// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES -// AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY SUBJECT SOFTWARE IS -// AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. -// -// UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING, -// WITHOUT LIMITATION, NEGLIGENCE OR STRICT LIABILITY), CONTRACT, OR -// OTHERWISE, SHALL SGI BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, -// INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER WITH RESPECT TO THE -// SOFTWARE INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK -// STOPPAGE, LOSS OF DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL -// OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SGI SHALL HAVE BEEN INFORMED OF -// THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT -// APPLY TO LIABILITY RESULTING FROM SGI's NEGLIGENCE TO THE EXTENT APPLICABLE -// LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE -// EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT -// EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. -// -// These license terms shall be governed by and construed in accordance with -// the laws of the United States and the State of California as applied to -// agreements entered into and to be performed entirely within California -// between California residents. Any litigation relating to these license -// terms shall be subject to the exclusive jurisdiction of the Federal Courts -// of the Northern District of California (or, absent subject matter -// jurisdiction in such courts, the courts of the State of California), with -// venue lying exclusively in Santa Clara County, California. - -// Copyright (c) 2001-2004, Hewlett-Packard Development Company, L.P. -// -// Permission is granted free of charge to copy, modify, use and distribute -// this software provided you include the entirety of this notice in all -// copies made. -// -// THIS SOFTWARE IS PROVIDED ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY -// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, -// WARRANTIES THAT THE SUBJECT SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT -// FOR A PARTICULAR PURPOSE OR NON-INFRINGING. HEWLETT-PACKARD ASSUMES -// NO RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE. -// SHOULD THE SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, -// HEWLETT-PACKARD ASSUMES NO COST OR LIABILITY FOR ANY -// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES -// AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY SUBJECT SOFTWARE IS -// AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. -// -// UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING, -// WITHOUT LIMITATION, NEGLIGENCE OR STRICT LIABILITY), CONTRACT, OR -// OTHERWISE, SHALL HEWLETT-PACKARD BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, -// INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER WITH RESPECT TO THE -// SOFTWARE INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK -// STOPPAGE, LOSS OF DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL -// OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF HEWLETT-PACKARD SHALL -// HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. -// THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY RESULTING -// FROM HEWLETT-PACKARD's NEGLIGENCE TO THE EXTENT APPLICABLE -// LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE -// EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT -// EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. -// - -// Added valueOf(string, radix), fixed some documentation comments. -// Hans_Boehm@hp.com 1/12/2001 -// Fixed a serious typo in inv_CR(): For negative arguments it produced -// the wrong sign. This affected the sign of divisions. -// Added byteValue and fixed some comments. Hans.Boehm@hp.com 12/17/2002 -// Added toStringFloatRep. Hans.Boehm@hp.com 4/1/2004 -// Added get_appr() synchronization to allow access from multiple threads -// hboehm@google.com 4/25/2014 -// Changed cos() prescaling to avoid logarithmic depth tree. -// hboehm@google.com 6/30/2014 -// Added explicit asin() implementation. Remove one. Add ZERO and ONE and -// make them public. hboehm@google.com 5/21/2015 -// Added Gauss-Legendre PI implementation. Removed two. -// hboehm@google.com 4/12/2016 -// Fix shift operation in doubleValue. That produced incorrect values for -// large negative exponents. -// Don't negate argument and compute inverse for exp(). That causes severe -// performance problems for (-huge).exp() -// hboehm@google.com 8/21/2017 -// Have comparison check for interruption. hboehm@google.com 10/31/2017 -// Fix precision overflow issue in most general compareTo function. -// Fix a couple of unused variable bugs. Notably selector_sign was -// accidentally locally redeclared. (This turns out to be safe but useless.) -// hboehm@google.com 11/20/2018. -// Fix an exception-safety issue in gl_pi_CR.approximate. -// hboehm@google.com 3/3/2019. - -package com.prime.toolz2.core.math.creals; - -import java.math.BigInteger; -import java.util.ArrayList; - -/** -* Constructive real numbers, also known as recursive, or computable reals. -* Each recursive real number is represented as an object that provides an -* approximation function for the real number. -* The approximation function guarantees that the generated approximation -* is accurate to the specified precision. -* Arithmetic operations on constructive reals produce new such objects; -* they typically do not perform any real computation. -* In this sense, arithmetic computations are exact: They produce -* a description which describes the exact answer, and can be used to -* later approximate it to arbitrary precision. -*

-* When approximations are generated, e.g. for output, they are -* accurate to the requested precision; no cumulative rounding errors -* are visible. -* In order to achieve this precision, the approximation function will often -* need to approximate subexpressions to greater precision than was originally -* demanded. Thus the approximation of a constructive real number -* generated through a complex sequence of operations may eventually require -* evaluation to very high precision. This usually makes such computations -* prohibitively expensive for large numerical problems. -* But it is perfectly appropriate for use in a desk calculator, -* for small numerical problems, for the evaluation of expressions -* computated by a symbolic algebra system, for testing of accuracy claims -* for floating point code on small inputs, or the like. -*

-* We expect that the vast majority of uses will ignore the particular -* implementation, and the member functons approximate -* and get_appr. Such applications will treat CR as -* a conventional numerical type, with an interface modelled on -* java.math.BigInteger. No subclasses of CR -* will be explicitly mentioned by such a program. -*

-* All standard arithmetic operations, as well as a few algebraic -* and transcendal functions are provided. Constructive reals are -* immutable; thus all of these operations return a new constructive real. -*

-* A few uses will require explicit construction of approximation functions. -* The requires the construction of a subclass of CR with -* an overridden approximate function. Note that approximate -* should only be defined, but never called. get_appr -* provides the same functionality, but adds the caching necessary to obtain -* reasonable performance. -*

-* Any operation may throw com.hp.creals.AbortedException if the thread -* in which it is executing is interrupted. (InterruptedException -* cannot be used for this purpose, since CR inherits from Number.) -*

-* Any operation may also throw com.hp.creals.PrecisionOverflowException -* If the precision request generated during any subcalculation overflows -* a 28-bit integer. (This should be extremely unlikely, except as an -* outcome of a division by zero, or other erroneous computation.) -* -*/ -public abstract class CR extends Number { - // CR is the basic representation of a number. - // Abstractly this is a function for computing an approximation - // plus the current best approximation. - // We could do without the latter, but that would - // be atrociously slow. - -/** - * Indicates a constructive real operation was interrupted. - * Most constructive real operations may throw such an exception. - * This is unchecked, since Number methods may not raise checked - * exceptions. -*/ -public static class AbortedException extends RuntimeException { - public AbortedException() { super(); } - public AbortedException(String s) { super(s); } -} - -/** - * Indicates that the number of bits of precision requested by - * a computation on constructive reals required more than 28 bits, - * and was thus in danger of overflowing an int. - * This is likely to be a symptom of a diverging computation, - * e.g. division by zero. -*/ -public static class PrecisionOverflowException extends RuntimeException { - public PrecisionOverflowException() { super(); } - public PrecisionOverflowException(String s) { super(s); } -} - - // First some frequently used constants, so we don't have to - // recompute these all over the place. - static final BigInteger big0 = BigInteger.ZERO; - static final BigInteger big1 = BigInteger.ONE; - static final BigInteger bigm1 = BigInteger.valueOf(-1); - static final BigInteger big2 = BigInteger.valueOf(2); - static final BigInteger bigm2 = BigInteger.valueOf(-2); - static final BigInteger big3 = BigInteger.valueOf(3); - static final BigInteger big6 = BigInteger.valueOf(6); - static final BigInteger big8 = BigInteger.valueOf(8); - static final BigInteger big10 = BigInteger.TEN; - static final BigInteger big750 = BigInteger.valueOf(750); - static final BigInteger bigm750 = BigInteger.valueOf(-750); - -/** -* Setting this to true requests that all computations be aborted by -* throwing AbortedException. Must be rest to false before any further -* computation. Ideally Thread.interrupt() should be used instead, but -* that doesn't appear to be consistently supported by browser VMs. -*/ -public volatile static boolean please_stop = false; - -/** -* Must be defined in subclasses of CR. -* Most users can ignore the existence of this method, and will -* not ever need to define a CR subclass. -* Returns value / 2 ** precision rounded to an integer. -* The error in the result is strictly < 1. -* Informally, approximate(n) gives a scaled approximation -* accurate to 2**n. -* Implementations may safely assume that precision is -* at least a factor of 8 away from overflow. -* Called only with the lock on the CR object -* already held. -*/ - protected abstract BigInteger approximate(int precision); - transient int min_prec; - // The smallest precision value with which the above - // has been called. - transient BigInteger max_appr; - // The scaled approximation corresponding to min_prec. - transient boolean appr_valid = false; - // min_prec and max_val are valid. - - // Helper functions - static int bound_log2(int n) { - int abs_n = Math.abs(n); - return (int)Math.ceil(Math.log((double)(abs_n + 1))/Math.log(2.0)); - } - // Check that a precision is at least a factor of 8 away from - // overflowng the integer used to hold a precision spec. - // We generally perform this check early on, and then convince - // ourselves that none of the operations performed on precisions - // inside a function can generate an overflow. - static void check_prec(int n) { - int high = n >> 28; - // if n is not in danger of overflowing, then the 4 high order - // bits should be identical. Thus high is either 0 or -1. - // The rest of this is to test for either of those in a way - // that should be as cheap as possible. - int high_shifted = n >> 29; - if (0 != (high ^ high_shifted)) { - throw new PrecisionOverflowException(); - } - } - -/** -* The constructive real number corresponding to a -* BigInteger. -*/ - public static CR valueOf(BigInteger n) { - return new int_CR(n); - } - -/** -* The constructive real number corresponding to a -* Java int. -*/ - public static CR valueOf(int n) { - return valueOf(BigInteger.valueOf(n)); - } - -/** -* The constructive real number corresponding to a -* Java long. -*/ - public static CR valueOf(long n) { - return valueOf(BigInteger.valueOf(n)); - } - -/** -* The constructive real number corresponding to a -* Java double. -* The result is undefined if argument is infinite or NaN. -*/ - public static CR valueOf(double n) { - if (Double.isNaN(n)) throw new ArithmeticException("Nan argument"); - if (Double.isInfinite(n)) { - throw new ArithmeticException("Infinite argument"); - } - boolean negative = (n < 0.0); - long bits = Double.doubleToLongBits(Math.abs(n)); - long mantissa = (bits & 0xfffffffffffffL); - int biased_exp = (int)(bits >> 52); - int exp = biased_exp - 1075; - if (biased_exp != 0) { - mantissa += (1L << 52); - } else { - mantissa <<= 1; - } - CR result = valueOf(mantissa).shiftLeft(exp); - if (negative) result = result.negate(); - return result; - } - -/** -* The constructive real number corresponding to a -* Java float. -* The result is undefined if argument is infinite or NaN. -*/ - public static CR valueOf(float n) { - return valueOf((double) n); - } - - public static CR ZERO = valueOf(0); - public static CR ONE = valueOf(1); - - // Multiply k by 2**n. - static BigInteger shift(BigInteger k, int n) { - if (n == 0) return k; - if (n < 0) return k.shiftRight(-n); - return k.shiftLeft(n); - } - - // Multiply by 2**n, rounding result - static BigInteger scale(BigInteger k, int n) { - if (n >= 0) { - return k.shiftLeft(n); - } else { - BigInteger adj_k = shift(k, n+1).add(big1); - return adj_k.shiftRight(1); - } - } - - // Identical to approximate(), but maintain and update cache. -/** -* Returns value / 2 ** prec rounded to an integer. -* The error in the result is strictly < 1. -* Produces the same answer as approximate, but uses and -* maintains a cached approximation. -* Normally not overridden, and called only from approximate -* methods in subclasses. Not needed if the provided operations -* on constructive reals suffice. -*/ - public synchronized BigInteger get_appr(int precision) { - check_prec(precision); - if (appr_valid && precision >= min_prec) { - return scale(max_appr, min_prec - precision); - } else { - BigInteger result = approximate(precision); - min_prec = precision; - max_appr = result; - appr_valid = true; - return result; - } - } - - // Return the position of the msd. - // If x.msd() == n then - // 2**(n-1) < abs(x) < 2**(n+1) - // This initial version assumes that max_appr is valid - // and sufficiently removed from zero - // that the msd is determined. - int known_msd() { - int first_digit; - int length; - if (max_appr.signum() >= 0) { - length = max_appr.bitLength(); - } else { - length = max_appr.negate().bitLength(); - } - first_digit = min_prec + length - 1; - return first_digit; - } - - // This version may return Integer.MIN_VALUE if the correct - // answer is < n. - int msd(int n) { - if (!appr_valid || - max_appr.compareTo(big1) <= 0 - && max_appr.compareTo(bigm1) >= 0) { - get_appr(n - 1); - if (max_appr.abs().compareTo(big1) <= 0) { - // msd could still be arbitrarily far to the right. - return Integer.MIN_VALUE; - } - } - return known_msd(); - } - - - // Functionally equivalent, but iteratively evaluates to higher - // precision. - int iter_msd(int n) - { - int prec = 0; - - for (;prec > n + 30; prec = (prec * 3)/2 - 16) { - int msd = msd(prec); - if (msd != Integer.MIN_VALUE) return msd; - check_prec(prec); - if (Thread.interrupted() || please_stop) { - throw new AbortedException(); - } - } - return msd(n); - } - - // This version returns a correct answer eventually, except - // that it loops forever (or throws an exception when the - // requested precision overflows) if this constructive real is zero. - int msd() { - return iter_msd(Integer.MIN_VALUE); - } - - // A helper function for toString. - // Generate a String containing n zeroes. - private static String zeroes(int n) { - char[] a = new char[n]; - for (int i = 0; i < n; ++i) { - a[i] = '0'; - } - return new String(a); - } - - // Natural log of 2. Needed for some prescaling below. - // ln(2) = 7ln(10/9) - 2ln(25/24) + 3ln(81/80) - CR simple_ln() { - return new prescaled_ln_CR(this.subtract(ONE)); - } - static CR ten_ninths = valueOf(10).divide(valueOf(9)); - static CR twentyfive_twentyfourths = valueOf(25).divide(valueOf(24)); - static CR eightyone_eightyeths = valueOf(81).divide(valueOf(80)); - static CR ln2_1 = valueOf(7).multiply(ten_ninths.simple_ln()); - static CR ln2_2 = - valueOf(2).multiply(twentyfive_twentyfourths.simple_ln()); - static CR ln2_3 = valueOf(3).multiply(eightyone_eightyeths.simple_ln()); - static CR ln2 = ln2_1.subtract(ln2_2).add(ln2_3); - - // Atan of integer reciprocal. Used for atan_PI. Could perhaps be made - // public. - static CR atan_reciprocal(int n) { - return new integral_atan_CR(n); - } - // Other constants used for PI computation. - static CR four = valueOf(4); - - // Public operations. -/** -* Return 0 if x = y to within the indicated tolerance, -* -1 if x < y, and +1 if x > y. If x and y are indeed -* equal, it is guaranteed that 0 will be returned. If -* they differ by less than the tolerance, anything -* may happen. The tolerance allowed is -* the maximum of (abs(this)+abs(x))*(2**r) and 2**a -* @param x The other constructive real -* @param r Relative tolerance in bits -* @param a Absolute tolerance in bits -*/ - public int compareTo(CR x, int r, int a) { - int this_msd = iter_msd(a); - int x_msd = x.iter_msd(this_msd > a? this_msd : a); - int max_msd = (x_msd > this_msd? x_msd : this_msd); - if (max_msd == Integer.MIN_VALUE) { - return 0; - } - check_prec(r); - int rel = max_msd + r; - int abs_prec = (rel > a? rel : a); - return compareTo(x, abs_prec); - } - -/** -* Approximate comparison with only an absolute tolerance. -* Identical to the three argument version, but without a relative -* tolerance. -* Result is 0 if both constructive reals are equal, indeterminate -* if they differ by less than 2**a. -* -* @param x The other constructive real -* @param a Absolute tolerance in bits -*/ - public int compareTo(CR x, int a) { - int needed_prec = a - 1; - BigInteger this_appr = get_appr(needed_prec); - BigInteger x_appr = x.get_appr(needed_prec); - int comp1 = this_appr.compareTo(x_appr.add(big1)); - if (comp1 > 0) return 1; - int comp2 = this_appr.compareTo(x_appr.subtract(big1)); - if (comp2 < 0) return -1; - return 0; - } - -/** -* Return -1 if this < x, or +1 if this > x. -* Should be called only if this != x. -* If this == x, this will not terminate correctly; typically it -* will run until it exhausts memory. -* If the two constructive reals may be equal, the two or 3 argument -* version of compareTo should be used. -*/ - public int compareTo(CR x) { - for (int a = -20; ; a *= 2) { - check_prec(a); - int result = compareTo(x, a); - if (0 != result) return result; - if (Thread.interrupted() || please_stop) { - throw new AbortedException(); - } - } - } - -/** -* Equivalent to compareTo(CR.valueOf(0), a) -*/ - public int signum(int a) { - if (appr_valid) { - int quick_try = max_appr.signum(); - if (0 != quick_try) return quick_try; - } - int needed_prec = a - 1; - BigInteger this_appr = get_appr(needed_prec); - return this_appr.signum(); - } - -/** -* Return -1 if negative, +1 if positive. -* Should be called only if this != 0. -* In the 0 case, this will not terminate correctly; typically it -* will run until it exhausts memory. -* If the two constructive reals may be equal, the one or two argument -* version of signum should be used. -*/ - public int signum() { - for (int a = -20; ; a *= 2) { - check_prec(a); - int result = signum(a); - if (0 != result) return result; - if (Thread.interrupted() || please_stop) { - throw new AbortedException(); - } - } - } - -/** -* Return the constructive real number corresponding to the given -* textual representation and radix. -* -* @param s [-] digit* [. digit*] -* @param radix -*/ - - public static CR valueOf(String s, int radix) - throws NumberFormatException { - int len = s.length(); - int start_pos = 0, point_pos; - String fraction; - while (s.charAt(start_pos) == ' ') ++start_pos; - while (s.charAt(len - 1) == ' ') --len; - point_pos = s.indexOf('.', start_pos); - if (point_pos == -1) { - point_pos = len; - fraction = "0"; - } else { - fraction = s.substring(point_pos + 1, len); - } - String whole = s.substring(start_pos, point_pos); - BigInteger scaled_result = new BigInteger(whole + fraction, radix); - BigInteger divisor = BigInteger.valueOf(radix).pow(fraction.length()); - return CR.valueOf(scaled_result).divide(CR.valueOf(divisor)); - } - -/** -* Return a textual representation accurate to n places -* to the right of the decimal point. n must be nonnegative. -* -* @param n Number of digits (>= 0) included to the right of decimal point -* @param radix Base ( >= 2, <= 16) for the resulting representation. -*/ - public String toString(int n, int radix) { - CR scaled_CR; - if (16 == radix) { - scaled_CR = shiftLeft(4*n); - } else { - BigInteger scale_factor = BigInteger.valueOf(radix).pow(n); - scaled_CR = multiply(new int_CR(scale_factor)); - } - BigInteger scaled_int = scaled_CR.get_appr(0); - String scaled_string = scaled_int.abs().toString(radix); - String result; - if (0 == n) { - result = scaled_string; - } else { - int len = scaled_string.length(); - if (len <= n) { - // Add sufficient leading zeroes - String z = zeroes(n + 1 - len); - scaled_string = z + scaled_string; - len = n + 1; - } - String whole = scaled_string.substring(0, len - n); - String fraction = scaled_string.substring(len - n); - result = whole + "." + fraction; - } - if (scaled_int.signum() < 0) { - result = "-" + result; - } - return result; - } - - -/** -* Equivalent to toString(n,10) -* -* @param n Number of digits included to the right of decimal point -*/ - public String toString(int n) { - return toString(n, 10); - } - -/** -* Equivalent to toString(10, 10) -*/ - public String toString() { - return toString(10); - } - - static double doubleLog2 = Math.log(2.0); -/** -* Return a textual scientific notation representation accurate -* to n places to the right of the decimal point. -* n must be nonnegative. A value smaller than -* radix**-m may be displayed as 0. -* The mantissa component of the result is either "0" -* or exactly n digits long. The sign -* component is zero exactly when the mantissa is "0". -* -* @param n Number of digits (> 0) included to the right of decimal point. -* @param radix Base ( ≥ 2, ≤ 16) for the resulting representation. -* @param m Precision used to distinguish number from zero. -* Expressed as a power of m. -*/ - public StringFloatRep toStringFloatRep(int n, int radix, int m) { - if (n <= 0) throw new ArithmeticException("Bad precision argument"); - double log2_radix = Math.log((double)radix)/doubleLog2; - BigInteger big_radix = BigInteger.valueOf(radix); - long long_msd_prec = (long)(log2_radix * (double)m); - if (long_msd_prec > (long)Integer.MAX_VALUE - || long_msd_prec < (long)Integer.MIN_VALUE) - throw new PrecisionOverflowException(); - int msd_prec = (int)long_msd_prec; - check_prec(msd_prec); - int msd = iter_msd(msd_prec - 2); - if (msd == Integer.MIN_VALUE) - return new StringFloatRep(0, "0", radix, 0); - int exponent = (int)Math.ceil((double)msd / log2_radix); - // Guess for the exponent. Try to get it usually right. - int scale_exp = exponent - n; - CR scale; - if (scale_exp > 0) { - scale = CR.valueOf(big_radix.pow(scale_exp)).inverse(); - } else { - scale = CR.valueOf(big_radix.pow(-scale_exp)); - } - CR scaled_res = multiply(scale); - BigInteger scaled_int = scaled_res.get_appr(0); - int sign = scaled_int.signum(); - String scaled_string = scaled_int.abs().toString(radix); - while (scaled_string.length() < n) { - // exponent was too large. Adjust. - scaled_res = scaled_res.multiply(CR.valueOf(big_radix)); - exponent -= 1; - scaled_int = scaled_res.get_appr(0); - sign = scaled_int.signum(); - scaled_string = scaled_int.abs().toString(radix); - } - if (scaled_string.length() > n) { - // exponent was too small. Adjust by truncating. - exponent += (scaled_string.length() - n); - scaled_string = scaled_string.substring(0, n); - } - return new StringFloatRep(sign, scaled_string, radix, exponent); - } - -/** -* Return a BigInteger which differs by less than one from the -* constructive real. -*/ - public BigInteger BigIntegerValue() { - return get_appr(0); - } - -/** -* Return an int which differs by less than one from the -* constructive real. Behavior on overflow is undefined. -*/ - public int intValue() { - return BigIntegerValue().intValue(); - } - -/** -* Return an int which differs by less than one from the -* constructive real. Behavior on overflow is undefined. -*/ - public byte byteValue() { - return BigIntegerValue().byteValue(); - } - -/** -* Return a long which differs by less than one from the -* constructive real. Behavior on overflow is undefined. -*/ - public long longValue() { - return BigIntegerValue().longValue(); - } - -/** -* Return a double which differs by less than one in the least -* represented bit from the constructive real. -* (We're in fact closer to round-to-nearest than that, but we can't and -* don't promise correct rounding.) -*/ - public double doubleValue() { - int my_msd = iter_msd(-1080 /* slightly > exp. range */); - if (Integer.MIN_VALUE == my_msd) return 0.0; - int needed_prec = my_msd - 60; - double scaled_int = get_appr(needed_prec).doubleValue(); - boolean may_underflow = (needed_prec < -1000); - long scaled_int_rep = Double.doubleToLongBits(scaled_int); - long exp_adj = may_underflow? needed_prec + 96 : needed_prec; - long orig_exp = (scaled_int_rep >> 52) & 0x7ff; - if (((orig_exp + exp_adj) & ~0x7ff) != 0) { - // Original unbiased exponent is > 50. Exp_adj > -1050. - // Thus this can overflow the 11 bit exponent only if the result - // itself overflows. - if (scaled_int < 0.0) { - return Double.NEGATIVE_INFINITY; - } else { - return Double.POSITIVE_INFINITY; - } - } - scaled_int_rep += exp_adj << 52; - double result = Double.longBitsToDouble(scaled_int_rep); - if (may_underflow) { - double two48 = (double)(1L << 48); - return result/two48/two48; - } else { - return result; - } - } - -/** -* Return a float which differs by less than one in the least -* represented bit from the constructive real. -*/ - public float floatValue() { - return (float)doubleValue(); - // Note that double-rounding is not a problem here, since we - // cannot, and do not, guarantee correct rounding. - } - -/** -* Add two constructive reals. -*/ - public CR add(CR x) { - return new add_CR(this, x); - } - -/** -* Multiply a constructive real by 2**n. -* @param n shift count, may be negative -*/ - public CR shiftLeft(int n) { - check_prec(n); - return new shifted_CR(this, n); - } - -/** -* Multiply a constructive real by 2**(-n). -* @param n shift count, may be negative -*/ - public CR shiftRight(int n) { - check_prec(n); - return new shifted_CR(this, -n); - } - -/** -* Produce a constructive real equivalent to the original, assuming -* the original was an integer. Undefined results if the original -* was not an integer. Prevents evaluation of digits to the right -* of the decimal point, and may thus improve performance. -*/ - public CR assumeInt() { - return new assumed_int_CR(this); - } - -/** -* The additive inverse of a constructive real -*/ - public CR negate() { - return new neg_CR(this); - } - -/** -* The difference between two constructive reals -*/ - public CR subtract(CR x) { - return new add_CR(this, x.negate()); - } - -/** -* The product of two constructive reals -*/ - public CR multiply(CR x) { - return new mult_CR(this, x); - } - -/** -* The multiplicative inverse of a constructive real. -* x.inverse() is equivalent to CR.valueOf(1).divide(x). -*/ - public CR inverse() { - return new inv_CR(this); - } - -/** -* The quotient of two constructive reals. -*/ - public CR divide(CR x) { - return new mult_CR(this, x.inverse()); - } - -/** -* The real number x if this < 0, or y otherwise. -* Requires x = y if this = 0. -* Since comparisons may diverge, this is often -* a useful alternative to conditionals. -*/ - public CR select(CR x, CR y) { - return new select_CR(this, x, y); - } - -/** -* The maximum of two constructive reals. -*/ - public CR max(CR x) { - return subtract(x).select(x, this); - } - -/** -* The minimum of two constructive reals. -*/ - public CR min(CR x) { - return subtract(x).select(this, x); - } - -/** -* The absolute value of a constructive reals. -* Note that this cannot be written as a conditional. -*/ - public CR abs() { - return select(negate(), this); - } - -/** -* The exponential function, that is e**this. -*/ - public CR exp() { - final int low_prec = -10; - BigInteger rough_appr = get_appr(low_prec); - // Handle negative arguments directly; negating and computing inverse - // can be very expensive. - if (rough_appr.compareTo(big2) > 0 || rough_appr.compareTo(bigm2) < 0) { - CR square_root = shiftRight(1).exp(); - return square_root.multiply(square_root); - } else { - return new prescaled_exp_CR(this); - } - } - -/** -* The ratio of a circle's circumference to its diameter. -*/ - public static CR PI = new gl_pi_CR(); - - // Our old PI implementation. Keep this around for now to allow checking. - // This implementation may also be faster for BigInteger implementations - // that support only quadratic multiplication, but exhibit high performance - // for small computations. (The standard Android 6 implementation supports - // subquadratic multiplication, but has high constant overhead.) Many other - // atan-based formulas are possible, but based on superficial - // experimentation, this is roughly as good as the more complex formulas. - public static CR atan_PI = four.multiply(four.multiply(atan_reciprocal(5)) - .subtract(atan_reciprocal(239))); - // pi/4 = 4*atan(1/5) - atan(1/239) - static CR half_pi = PI.shiftRight(1); - -/** -* The trigonometric cosine function. -*/ - public CR cos() { - BigInteger halfpi_multiples = divide(PI).get_appr(-1); - BigInteger abs_halfpi_multiples = halfpi_multiples.abs(); - if (abs_halfpi_multiples.compareTo(big2) >= 0) { - // Subtract multiples of PI - BigInteger pi_multiples = scale(halfpi_multiples, -1); - CR adjustment = PI.multiply(CR.valueOf(pi_multiples)); - if (pi_multiples.and(big1).signum() != 0) { - return subtract(adjustment).cos().negate(); - } else { - return subtract(adjustment).cos(); - } - } else if (get_appr(-1).abs().compareTo(big2) >= 0) { - // Scale further with double angle formula - CR cos_half = shiftRight(1).cos(); - return cos_half.multiply(cos_half).shiftLeft(1).subtract(ONE); - } else { - return new prescaled_cos_CR(this); - } - } - -/** -* The trigonometric sine function. -*/ - public CR sin() { - return half_pi.subtract(this).cos(); - } - -/** -* The trignonometric arc (inverse) sine function. -*/ - public CR asin() { - BigInteger rough_appr = get_appr(-10); - if (rough_appr.compareTo(big750) /* 1/sqrt(2) + a bit */ > 0){ - CR new_arg = ONE.subtract(multiply(this)).sqrt(); - return new_arg.acos(); - } else if (rough_appr.compareTo(bigm750) < 0) { - return negate().asin().negate(); - } else { - return new prescaled_asin_CR(this); - } - } - -/** -* The trignonometric arc (inverse) cosine function. -*/ - public CR acos() { - return half_pi.subtract(asin()); - } - - static final BigInteger low_ln_limit = big8; /* sixteenths, i.e. 1/2 */ - static final BigInteger high_ln_limit = - BigInteger.valueOf(16 + 8 /* 1.5 */); - static final BigInteger scaled_4 = - BigInteger.valueOf(4*16); - -/** -* The natural (base e) logarithm. -*/ - public CR ln() { - final int low_prec = -4; - BigInteger rough_appr = get_appr(low_prec); /* In sixteenths */ - if (rough_appr.compareTo(big0) < 0) { - throw new ArithmeticException("ln(negative)"); - } - if (rough_appr.compareTo(low_ln_limit) <= 0) { - return inverse().ln().negate(); - } - if (rough_appr.compareTo(high_ln_limit) >= 0) { - if (rough_appr.compareTo(scaled_4) <= 0) { - CR quarter = sqrt().sqrt().ln(); - return quarter.shiftLeft(2); - } else { - int extra_bits = rough_appr.bitLength() - 3; - CR scaled_result = shiftRight(extra_bits).ln(); - return scaled_result.add(CR.valueOf(extra_bits).multiply(ln2)); - } - } - return simple_ln(); - } - -/** -* The square root of a constructive real. -*/ - public CR sqrt() { - return new sqrt_CR(this); - } - -} // end of CR - - -// -// A specialization of CR for cases in which approximate() calls -// to increase evaluation precision are somewhat expensive. -// If we need to (re)evaluate, we speculatively evaluate to slightly -// higher precision, miminimizing reevaluations. -// Note that this requires any arguments to be evaluated to higher -// precision than absolutely necessary. It can thus potentially -// result in lots of wasted effort, and should be used judiciously. -// This assumes that the order of magnitude of the number is roughly one. -// -abstract class slow_CR extends CR { - static int max_prec = -64; - static int prec_incr = 32; - public synchronized BigInteger get_appr(int precision) { - check_prec(precision); - if (appr_valid && precision >= min_prec) { - return scale(max_appr, min_prec - precision); - } else { - int eval_prec = (precision >= max_prec? max_prec : - (precision - prec_incr + 1) & ~(prec_incr - 1)); - BigInteger result = approximate(eval_prec); - min_prec = eval_prec; - max_appr = result; - appr_valid = true; - return scale(result, eval_prec - precision); - } - } -} - - -// Representation of an integer constant. Private. -class int_CR extends CR { - BigInteger value; - int_CR(BigInteger n) { - value = n; - } - protected BigInteger approximate(int p) { - return scale(value, -p) ; - } -} - -// Representation of a number that may not have been completely -// evaluated, but is assumed to be an integer. Hence we never -// evaluate beyond the decimal point. -class assumed_int_CR extends CR { - CR value; - assumed_int_CR(CR x) { - value = x; - } - protected BigInteger approximate(int p) { - if (p >= 0) { - return value.get_appr(p); - } else { - return scale(value.get_appr(0), -p) ; - } - } -} - -// Representation of the sum of 2 constructive reals. Private. -class add_CR extends CR { - CR op1; - CR op2; - add_CR(CR x, CR y) { - op1 = x; - op2 = y; - } - protected BigInteger approximate(int p) { - // Args need to be evaluated so that each error is < 1/4 ulp. - // Rounding error from the cale call is <= 1/2 ulp, so that - // final error is < 1 ulp. - return scale(op1.get_appr(p-2).add(op2.get_appr(p-2)), -2); - } -} - -// Representation of a CR multiplied by 2**n -class shifted_CR extends CR { - CR op; - int count; - shifted_CR(CR x, int n) { - op = x; - count = n; - } - protected BigInteger approximate(int p) { - return op.get_appr(p - count); - } -} - -// Representation of the negation of a constructive real. Private. -class neg_CR extends CR { - CR op; - neg_CR(CR x) { - op = x; - } - protected BigInteger approximate(int p) { - return op.get_appr(p).negate(); - } -} - -// Representation of: -// op1 if selector < 0 -// op2 if selector >= 0 -// Assumes x = y if s = 0 -class select_CR extends CR { - CR selector; - int selector_sign; - CR op1; - CR op2; - select_CR(CR s, CR x, CR y) { - selector = s; - selector_sign = selector.get_appr(-20).signum(); - op1 = x; - op2 = y; - } - protected BigInteger approximate(int p) { - if (selector_sign < 0) return op1.get_appr(p); - if (selector_sign > 0) return op2.get_appr(p); - BigInteger op1_appr = op1.get_appr(p-1); - BigInteger op2_appr = op2.get_appr(p-1); - BigInteger diff = op1_appr.subtract(op2_appr).abs(); - if (diff.compareTo(big1) <= 0) { - // close enough; use either - return scale(op1_appr, -1); - } - // op1 and op2 are different; selector != 0; - // safe to get sign of selector. - if (selector.signum() < 0) { - selector_sign = -1; - return scale(op1_appr, -1); - } else { - selector_sign = 1; - return scale(op2_appr, -1); - } - } -} - -// Representation of the product of 2 constructive reals. Private. -class mult_CR extends CR { - CR op1; - CR op2; - mult_CR(CR x, CR y) { - op1 = x; - op2 = y; - } - protected BigInteger approximate(int p) { - int half_prec = (p >> 1) - 1; - int msd_op1 = op1.msd(half_prec); - int msd_op2; - - if (msd_op1 == Integer.MIN_VALUE) { - msd_op2 = op2.msd(half_prec); - if (msd_op2 == Integer.MIN_VALUE) { - // Product is small enough that zero will do as an - // approximation. - return big0; - } else { - // Swap them, so the larger operand (in absolute value) - // is first. - CR tmp; - tmp = op1; - op1 = op2; - op2 = tmp; - msd_op1 = msd_op2; - } - } - // msd_op1 is valid at this point. - int prec2 = p - msd_op1 - 3; // Precision needed for op2. - // The appr. error is multiplied by at most - // 2 ** (msd_op1 + 1) - // Thus each approximation contributes 1/4 ulp - // to the rounding error, and the final rounding adds - // another 1/2 ulp. - BigInteger appr2 = op2.get_appr(prec2); - if (appr2.signum() == 0) return big0; - msd_op2 = op2.known_msd(); - int prec1 = p - msd_op2 - 3; // Precision needed for op1. - BigInteger appr1 = op1.get_appr(prec1); - int scale_digits = prec1 + prec2 - p; - return scale(appr1.multiply(appr2), scale_digits); - } -} - -// Representation of the multiplicative inverse of a constructive -// real. Private. Should use Newton iteration to refine estimates. -class inv_CR extends CR { - CR op; - inv_CR(CR x) { op = x; } - protected BigInteger approximate(int p) { - int msd = op.msd(); - int inv_msd = 1 - msd; - int digits_needed = inv_msd - p + 3; - // Number of SIGNIFICANT digits needed for - // argument, excl. msd position, which may - // be fictitious, since msd routine can be - // off by 1. Roughly 1 extra digit is - // needed since the relative error is the - // same in the argument and result, but - // this isn't quite the same as the number - // of significant digits. Another digit - // is needed to compensate for slop in the - // calculation. - // One further bit is required, since the - // final rounding introduces a 0.5 ulp - // error. - int prec_needed = msd - digits_needed; - int log_scale_factor = -p - prec_needed; - if (log_scale_factor < 0) return big0; - BigInteger dividend = big1.shiftLeft(log_scale_factor); - BigInteger scaled_divisor = op.get_appr(prec_needed); - BigInteger abs_scaled_divisor = scaled_divisor.abs(); - BigInteger adj_dividend = dividend.add( - abs_scaled_divisor.shiftRight(1)); - // Adjustment so that final result is rounded. - BigInteger result = adj_dividend.divide(abs_scaled_divisor); - if (scaled_divisor.signum() < 0) { - return result.negate(); - } else { - return result; - } - } -} - - -// Representation of the exponential of a constructive real. Private. -// Uses a Taylor series expansion. Assumes |x| < 1/2. -// Note: this is known to be a bad algorithm for -// floating point. Unfortunately, other alternatives -// appear to require precomputed information. -class prescaled_exp_CR extends CR { - CR op; - prescaled_exp_CR(CR x) { op = x; } - protected BigInteger approximate(int p) { - if (p >= 1) return big0; - int iterations_needed = -p/2 + 2; // conservative estimate > 0. - // Claim: each intermediate term is accurate - // to 2*2^calc_precision. - // Total rounding error in series computation is - // 2*iterations_needed*2^calc_precision, - // exclusive of error in op. - int calc_precision = p - bound_log2(2*iterations_needed) - - 4; // for error in op, truncation. - int op_prec = p - 3; - BigInteger op_appr = op.get_appr(op_prec); - // Error in argument results in error of < 3/8 ulp. - // Sum of term eval. rounding error is < 1/16 ulp. - // Series truncation error < 1/16 ulp. - // Final rounding error is <= 1/2 ulp. - // Thus final error is < 1 ulp. - BigInteger scaled_1 = big1.shiftLeft(-calc_precision); - BigInteger current_term = scaled_1; - BigInteger current_sum = scaled_1; - int n = 0; - BigInteger max_trunc_error = - big1.shiftLeft(p - 4 - calc_precision); - while (current_term.abs().compareTo(max_trunc_error) >= 0) { - if (Thread.interrupted() || please_stop) throw new AbortedException(); - n += 1; - /* current_term = current_term * op / n */ - current_term = scale(current_term.multiply(op_appr), op_prec); - current_term = current_term.divide(BigInteger.valueOf(n)); - current_sum = current_sum.add(current_term); - } - return scale(current_sum, calc_precision - p); - } -} - -// Representation of the cosine of a constructive real. Private. -// Uses a Taylor series expansion. Assumes |x| < 1. -class prescaled_cos_CR extends slow_CR { - CR op; - prescaled_cos_CR(CR x) { - op = x; - } - protected BigInteger approximate(int p) { - if (p >= 1) return big0; - int iterations_needed = -p/2 + 4; // conservative estimate > 0. - // Claim: each intermediate term is accurate - // to 2*2^calc_precision. - // Total rounding error in series computation is - // 2*iterations_needed*2^calc_precision, - // exclusive of error in op. - int calc_precision = p - bound_log2(2*iterations_needed) - - 4; // for error in op, truncation. - int op_prec = p - 2; - BigInteger op_appr = op.get_appr(op_prec); - // Error in argument results in error of < 1/4 ulp. - // Cumulative arithmetic rounding error is < 1/16 ulp. - // Series truncation error < 1/16 ulp. - // Final rounding error is <= 1/2 ulp. - // Thus final error is < 1 ulp. - BigInteger current_term; - int n; - BigInteger max_trunc_error = - big1.shiftLeft(p - 4 - calc_precision); - n = 0; - current_term = big1.shiftLeft(-calc_precision); - BigInteger current_sum = current_term; - while (current_term.abs().compareTo(max_trunc_error) >= 0) { - if (Thread.interrupted() || please_stop) throw new AbortedException(); - n += 2; - /* current_term = - current_term * op * op / n * (n - 1) */ - current_term = scale(current_term.multiply(op_appr), op_prec); - current_term = scale(current_term.multiply(op_appr), op_prec); - BigInteger divisor = BigInteger.valueOf(-n) - .multiply(BigInteger.valueOf(n-1)); - current_term = current_term.divide(divisor); - current_sum = current_sum.add(current_term); - } - return scale(current_sum, calc_precision - p); - } -} - -// The constructive real atan(1/n), where n is a small integer -// > base. -// This gives a simple and moderately fast way to compute PI. -class integral_atan_CR extends slow_CR { - int op; - integral_atan_CR(int x) { op = x; } - protected BigInteger approximate(int p) { - if (p >= 1) return big0; - int iterations_needed = -p/2 + 2; // conservative estimate > 0. - // Claim: each intermediate term is accurate - // to 2*base^calc_precision. - // Total rounding error in series computation is - // 2*iterations_needed*base^calc_precision, - // exclusive of error in op. - int calc_precision = p - bound_log2(2*iterations_needed) - - 2; // for error in op, truncation. - // Error in argument results in error of < 3/8 ulp. - // Cumulative arithmetic rounding error is < 1/4 ulp. - // Series truncation error < 1/4 ulp. - // Final rounding error is <= 1/2 ulp. - // Thus final error is < 1 ulp. - BigInteger scaled_1 = big1.shiftLeft(-calc_precision); - BigInteger big_op = BigInteger.valueOf(op); - BigInteger big_op_squared = BigInteger.valueOf(op*op); - BigInteger op_inverse = scaled_1.divide(big_op); - BigInteger current_power = op_inverse; - BigInteger current_term = op_inverse; - BigInteger current_sum = op_inverse; - int current_sign = 1; - int n = 1; - BigInteger max_trunc_error = - big1.shiftLeft(p - 2 - calc_precision); - while (current_term.abs().compareTo(max_trunc_error) >= 0) { - if (Thread.interrupted() || please_stop) throw new AbortedException(); - n += 2; - current_power = current_power.divide(big_op_squared); - current_sign = -current_sign; - current_term = - current_power.divide(BigInteger.valueOf(current_sign*n)); - current_sum = current_sum.add(current_term); - } - return scale(current_sum, calc_precision - p); - } -} - -// Representation for ln(1 + op) -class prescaled_ln_CR extends slow_CR { - CR op; - prescaled_ln_CR(CR x) { op = x; } - // Compute an approximation of ln(1+x) to precision - // prec. This assumes |x| < 1/2. - // It uses a Taylor series expansion. - // Unfortunately there appears to be no way to take - // advantage of old information. - // Note: this is known to be a bad algorithm for - // floating point. Unfortunately, other alternatives - // appear to require precomputed tabular information. - protected BigInteger approximate(int p) { - if (p >= 0) return big0; - int iterations_needed = -p; // conservative estimate > 0. - // Claim: each intermediate term is accurate - // to 2*2^calc_precision. Total error is - // 2*iterations_needed*2^calc_precision - // exclusive of error in op. - int calc_precision = p - bound_log2(2*iterations_needed) - - 4; // for error in op, truncation. - int op_prec = p - 3; - BigInteger op_appr = op.get_appr(op_prec); - // Error analysis as for exponential. - BigInteger x_nth = scale(op_appr, op_prec - calc_precision); - BigInteger current_term = x_nth; // x**n - BigInteger current_sum = current_term; - int n = 1; - int current_sign = 1; // (-1)^(n-1) - BigInteger max_trunc_error = - big1.shiftLeft(p - 4 - calc_precision); - while (current_term.abs().compareTo(max_trunc_error) >= 0) { - if (Thread.interrupted() || please_stop) throw new AbortedException(); - n += 1; - current_sign = -current_sign; - x_nth = scale(x_nth.multiply(op_appr), op_prec); - current_term = x_nth.divide(BigInteger.valueOf(n * current_sign)); - // x**n / (n * (-1)**(n-1)) - current_sum = current_sum.add(current_term); - } - return scale(current_sum, calc_precision - p); - } -} - -// Representation of the arcsine of a constructive real. Private. -// Uses a Taylor series expansion. Assumes |x| < (1/2)^(1/3). -class prescaled_asin_CR extends slow_CR { - CR op; - prescaled_asin_CR(CR x) { - op = x; - } - protected BigInteger approximate(int p) { - // The Taylor series is the sum of x^(2n+1) * (2n)!/(4^n n!^2 (2n+1)) - // Note that (2n)!/(4^n n!^2) is always less than one. - // (The denominator is effectively 2n*2n*(2n-2)*(2n-2)*...*2*2 - // which is clearly > (2n)!) - // Thus all terms are bounded by x^(2n+1). - // Unfortunately, there's no easy way to prescale the argument - // to less than 1/sqrt(2), and we can only approximate that. - // Thus the worst case iteration count is fairly high. - // But it doesn't make much difference. - if (p >= 2) return big0; // Never bigger than 4. - int iterations_needed = -3 * p / 2 + 4; - // conservative estimate > 0. - // Follows from assumed bound on x and - // the fact that only every other Taylor - // Series term is present. - // Claim: each intermediate term is accurate - // to 2*2^calc_precision. - // Total rounding error in series computation is - // 2*iterations_needed*2^calc_precision, - // exclusive of error in op. - int calc_precision = p - bound_log2(2*iterations_needed) - - 4; // for error in op, truncation. - int op_prec = p - 3; // always <= -2 - BigInteger op_appr = op.get_appr(op_prec); - // Error in argument results in error of < 1/4 ulp. - // (Derivative is bounded by 2 in the specified range and we use - // 3 extra digits.) - // Ignoring the argument error, each term has an error of - // < 3ulps relative to calc_precision, which is more precise than p. - // Cumulative arithmetic rounding error is < 3/16 ulp (relative to p). - // Series truncation error < 2/16 ulp. (Each computed term - // is at most 2/3 of last one, so some of remaining series < - // 3/2 * current term.) - // Final rounding error is <= 1/2 ulp. - // Thus final error is < 1 ulp (relative to p). - BigInteger max_last_term = - big1.shiftLeft(p - 4 - calc_precision); - int exp = 1; // Current exponent, = 2n+1 in above expression - BigInteger current_term = op_appr.shiftLeft(op_prec - calc_precision); - BigInteger current_sum = current_term; - BigInteger current_factor = current_term; - // Current scaled Taylor series term - // before division by the exponent. - // Accurate to 3 ulp at calc_precision. - while (current_term.abs().compareTo(max_last_term) >= 0) { - if (Thread.interrupted() || please_stop) throw new AbortedException(); - exp += 2; - // current_factor = current_factor * op * op * (exp-1) * (exp-2) / - // (exp-1) * (exp-1), with the two exp-1 factors cancelling, - // giving - // current_factor = current_factor * op * op * (exp-2) / (exp-1) - // Thus the error any in the previous term is multiplied by - // op^2, adding an error of < (1/2)^(2/3) < 2/3 the original - // error. - current_factor = current_factor.multiply(BigInteger.valueOf(exp - 2)); - current_factor = scale(current_factor.multiply(op_appr), op_prec + 2); - // Carry 2 extra bits of precision forward; thus - // this effectively introduces 1/8 ulp error. - current_factor = current_factor.multiply(op_appr); - BigInteger divisor = BigInteger.valueOf(exp - 1); - current_factor = current_factor.divide(divisor); - // Another 1/4 ulp error here. - current_factor = scale(current_factor, op_prec - 2); - // Remove extra 2 bits. 1/2 ulp rounding error. - // Current_factor has original 3 ulp rounding error, which we - // reduced by 1, plus < 1 ulp new rounding error. - current_term = current_factor.divide(BigInteger.valueOf(exp)); - // Contributes 1 ulp error to sum plus at most 3 ulp - // from current_factor. - current_sum = current_sum.add(current_term); - } - return scale(current_sum, calc_precision - p); - } - } - - -class sqrt_CR extends CR { - CR op; - sqrt_CR(CR x) { op = x; } - // Explicitly provide an initial approximation. - // Useful for arithmetic geometric mean algorithms, where we've previously - // computed a very similar square root. - sqrt_CR(CR x, int min_p, BigInteger max_a) { - op = x; - min_prec = min_p; - max_appr = max_a; - appr_valid = true; - } - final int fp_prec = 50; // Conservative estimate of number of - // significant bits in double precision - // computation. - final int fp_op_prec = 60; - protected BigInteger approximate(int p) { - int max_op_prec_needed = 2*p - 1; - int msd = op.iter_msd(max_op_prec_needed); - if (msd <= max_op_prec_needed) return big0; - int result_msd = msd/2; // +- 1 - int result_digits = result_msd - p; // +- 2 - if (result_digits > fp_prec) { - // Compute less precise approximation and use a Newton iter. - int appr_digits = result_digits/2 + 6; - // This should be conservative. Is fewer enough? - int appr_prec = result_msd - appr_digits; - int prod_prec = 2*appr_prec; - // First compute the argument to maximal precision, so we don't end up - // reevaluating it incrementally. - BigInteger op_appr = op.get_appr(prod_prec); - BigInteger last_appr = get_appr(appr_prec); - // Compute (last_appr * last_appr + op_appr) / last_appr / 2 - // while adjusting the scaling to make everything work - BigInteger prod_prec_scaled_numerator = - last_appr.multiply(last_appr).add(op_appr); - BigInteger scaled_numerator = - scale(prod_prec_scaled_numerator, appr_prec - p); - BigInteger shifted_result = scaled_numerator.divide(last_appr); - return shifted_result.add(big1).shiftRight(1); - } else { - // Use a double precision floating point approximation. - // Make sure all precisions are even - int op_prec = (msd - fp_op_prec) & ~1; - int working_prec = op_prec - fp_op_prec; - BigInteger scaled_bi_appr = op.get_appr(op_prec) - .shiftLeft(fp_op_prec); - double scaled_appr = scaled_bi_appr.doubleValue(); - if (scaled_appr < 0.0) - throw new ArithmeticException("sqrt(negative)"); - double scaled_fp_sqrt = Math.sqrt(scaled_appr); - BigInteger scaled_sqrt = BigInteger.valueOf((long)scaled_fp_sqrt); - int shift_count = working_prec/2 - p; - return shift(scaled_sqrt, shift_count); - } - } -} - -// The constant PI, computed using the Gauss-Legendre alternating -// arithmetic-geometric mean algorithm: -// a[0] = 1 -// b[0] = 1/sqrt(2) -// t[0] = 1/4 -// p[0] = 1 -// -// a[n+1] = (a[n] + b[n])/2 (arithmetic mean, between 0.8 and 1) -// b[n+1] = sqrt(a[n] * b[n]) (geometric mean, between 0.7 and 1) -// t[n+1] = t[n] - (2^n)(a[n]-a[n+1])^2, (always between 0.2 and 0.25) -// -// pi is then approximated as (a[n+1]+b[n+1])^2 / 4*t[n+1]. -// -class gl_pi_CR extends slow_CR { - // In addition to the best approximation kept by the CR base class, we keep - // the entire sequence b[n], to the extent we've needed it so far. Each - // reevaluation leads to slightly different sqrt arguments, but the - // previous result can be used to avoid repeating low precision Newton - // iterations for the sqrt approximation. - ArrayList b_prec = new ArrayList(); - ArrayList b_val = new ArrayList(); - gl_pi_CR() { - b_prec.add(null); // Zeroth entry unused. - b_val.add(null); - } - private static BigInteger TOLERANCE = BigInteger.valueOf(4); - // sqrt(1/2) - private static CR SQRT_HALF = new sqrt_CR(ONE.shiftRight(1)); - - protected BigInteger approximate(int p) { - // Get us back into a consistent state if the last computation - // was interrupted after pushing onto b_prec. - if (b_prec.size() > b_val.size()) { - b_prec.remove(b_prec.size() - 1); - } - // Rough approximations are easy. - if (p >= 0) return scale(BigInteger.valueOf(3), -p); - // We need roughly log2(p) iterations. Each iteration should - // contribute no more than 2 ulps to the error in the corresponding - // term (a[n], b[n], or t[n]). Thus 2log2(n) bits plus a few for the - // final calulation and rounding suffice. - final int extra_eval_prec = - (int)Math.ceil(Math.log(-p) / Math.log(2)) + 10; - // All our terms are implicitly scaled by eval_prec. - final int eval_prec = p - extra_eval_prec; - BigInteger a = BigInteger.ONE.shiftLeft(-eval_prec); - BigInteger b = SQRT_HALF.get_appr(eval_prec); - BigInteger t = BigInteger.ONE.shiftLeft(-eval_prec - 2); - int n = 0; - while (a.subtract(b).subtract(TOLERANCE).signum() > 0) { - // Current values correspond to n, next_ values to n + 1 - // b_prec.size() == b_val.size() >= n + 1 - final BigInteger next_a = a.add(b).shiftRight(1); - final BigInteger next_b; - final BigInteger a_diff = a.subtract(next_a); - final BigInteger b_prod = a.multiply(b).shiftRight(-eval_prec); - // We compute square root approximations using a nested - // temporary CR computation, to avoid implementing BigInteger - // square roots separately. - final CR b_prod_as_CR = CR.valueOf(b_prod).shiftRight(-eval_prec); - if (b_prec.size() == n + 1) { - // Add an n+1st slot. - // Take care to make this exception-safe; b_prec and b_val - // must remain consistent, even if we are interrupted, or run - // out of memory. It's OK to just push on b_prec in that case. - final CR next_b_as_CR = b_prod_as_CR.sqrt(); - next_b = next_b_as_CR.get_appr(eval_prec); - final BigInteger scaled_next_b = scale(next_b, -extra_eval_prec); - b_prec.add(p); - b_val.add(scaled_next_b); - } else { - // Reuse previous approximation to reduce sqrt iterations, - // hopefully to one. - final CR next_b_as_CR = - new sqrt_CR(b_prod_as_CR, - b_prec.get(n + 1), b_val.get(n + 1)); - next_b = next_b_as_CR.get_appr(eval_prec); - // We assume that set() doesn't throw for any reason. - b_prec.set(n + 1, p); - b_val.set(n + 1, scale(next_b, -extra_eval_prec)); - } - // b_prec.size() == b_val.size() >= n + 2 - final BigInteger next_t = - t.subtract(a_diff.multiply(a_diff) - .shiftLeft(n + eval_prec)); // shift dist. usually neg. - a = next_a; - b = next_b; - t = next_t; - ++n; - } - final BigInteger sum = a.add(b); - final BigInteger result = sum.multiply(sum).divide(t).shiftRight(2); - return scale(result, -extra_eval_prec); - } -} diff --git a/app/src/main/java/com/prime/toolz2/core/math/creals/StringFloatRep.java b/app/src/main/java/com/prime/toolz2/core/math/creals/StringFloatRep.java deleted file mode 100644 index eb582f6..0000000 --- a/app/src/main/java/com/prime/toolz2/core/math/creals/StringFloatRep.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2004, Hewlett-Packard Development Company, L.P. -// -// Permission is granted free of charge to copy, modify, use and distribute -// this software provided you include the entirety of this notice in all -// copies made. -// -// THIS SOFTWARE IS PROVIDED ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY -// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, -// WARRANTIES THAT THE SUBJECT SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT -// FOR A PARTICULAR PURPOSE OR NON-INFRINGING. HEWLETT-PACKARD ASSUMES -// NO RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE. -// SHOULD THE SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, -// HEWLETT-PACKARD ASSUMES NO COST OR LIABILITY FOR ANY -// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES -// AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY SUBJECT SOFTWARE IS -// AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. -// -// UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING, -// WITHOUT LIMITATION, NEGLIGENCE OR STRICT LIABILITY), CONTRACT, OR -// OTHERWISE, SHALL HEWLETT-PACKARD BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, -// INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER WITH RESPECT TO THE -// SOFTWARE INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK -// STOPPAGE, LOSS OF DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL -// OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF HEWLETT-PACKARD SHALL -// HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. -// THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY RESULTING -// FROM HEWLETT-PACKARD's NEGLIGENCE TO THE EXTENT APPLICABLE -// LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE -// EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT -// EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. -// - -package com.prime.toolz2.core.math.creals; - -/** -* A scientific notation representation of an approximation to a constructive -* real. -* Generated by CR.toStringFloatRep. -*/ -public class StringFloatRep { - StringFloatRep(int s, String m, int r, int e) { - sign = s; - mantissa = m; - radix = r; - exponent = e; - } -/** -* The sign associated with this approximation. May be -1, _1, or zero. -*/ - public int sign; -/** -* A string representation of the mantissa. The decimal point is implicitly -* to the left of the string of digits, and is not explicitly represented. -*/ - public String mantissa; -/** -* The radix of the representation. Also the base of the exponent field. -*/ - public int radix; -/** -* The mantissa is scaled by radix**exponent. -*/ - public int exponent; - -/** -* Produce a textual representation including the sign and exponent. -*/ - public String toString() { - return - (sign < 0? "-" : "") + mantissa + "E" + Integer.toString(exponent) - + (radix == 10? "" : "(radix " + radix + ")"); - } -} diff --git a/app/src/main/java/com/prime/toolz2/core/math/creals/UnaryCRFunction.java b/app/src/main/java/com/prime/toolz2/core/math/creals/UnaryCRFunction.java deleted file mode 100644 index a19e570..0000000 --- a/app/src/main/java/com/prime/toolz2/core/math/creals/UnaryCRFunction.java +++ /dev/null @@ -1,667 +0,0 @@ -// Copyright (c) 1999, Silicon Graphics, Inc. -- ALL RIGHTS RESERVED -// -// Permission is granted free of charge to copy, modify, use and distribute -// this software provided you include the entirety of this notice in all -// copies made. -// -// THIS SOFTWARE IS PROVIDED ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY -// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, -// WARRANTIES THAT THE SUBJECT SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT -// FOR A PARTICULAR PURPOSE OR NON-INFRINGING. SGI ASSUMES NO RISK AS TO THE -// QUALITY AND PERFORMANCE OF THE SOFTWARE. SHOULD THE SOFTWARE PROVE -// DEFECTIVE IN ANY RESPECT, SGI ASSUMES NO COST OR LIABILITY FOR ANY -// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES -// AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY SUBJECT SOFTWARE IS -// AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. -// -// UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING, -// WITHOUT LIMITATION, NEGLIGENCE OR STRICT LIABILITY), CONTRACT, OR -// OTHERWISE, SHALL SGI BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, -// INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER WITH RESPECT TO THE -// SOFTWARE INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK -// STOPPAGE, LOSS OF DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL -// OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SGI SHALL HAVE BEEN INFORMED OF -// THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT -// APPLY TO LIABILITY RESULTING FROM SGI's NEGLIGENCE TO THE EXTENT APPLICABLE -// LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE -// EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT -// EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. -// -// These license terms shall be governed by and construed in accordance with -// the laws of the United States and the State of California as applied to -// agreements entered into and to be performed entirely within California -// between California residents. Any litigation relating to these license -// terms shall be subject to the exclusive jurisdiction of the Federal Courts -// of the Northern District of California (or, absent subject matter -// jurisdiction in such courts, the courts of the State of California), with -// venue lying exclusively in Santa Clara County, California. -// -// 5/2014 Added Strings to ArithmeticExceptions. -// 5/2015 Added support for direct asin() implementation in CR. - -package com.prime.toolz2.core.math.creals; -// import android.util.Log; - -import java.math.BigInteger; - -/** -* Unary functions on constructive reals implemented as objects. -* The execute member computes the function result. -* Unary function objects on constructive reals inherit from -* UnaryCRFunction. -*/ -// Naming vaguely follows ObjectSpace JGL convention. -public abstract class UnaryCRFunction { - abstract public CR execute(CR x); - -/** -* The function object corresponding to the identity function. -*/ - public static final UnaryCRFunction identityFunction = - new identity_UnaryCRFunction(); - -/** -* The function object corresponding to the negate method of CR. -*/ - public static final UnaryCRFunction negateFunction = - new negate_UnaryCRFunction(); - -/** -* The function object corresponding to the inverse method of CR. -*/ - public static final UnaryCRFunction inverseFunction = - new inverse_UnaryCRFunction(); - -/** -* The function object corresponding to the abs method of CR. -*/ - public static final UnaryCRFunction absFunction = - new abs_UnaryCRFunction(); - -/** -* The function object corresponding to the exp method of CR. -*/ - public static final UnaryCRFunction expFunction = - new exp_UnaryCRFunction(); - -/** -* The function object corresponding to the cos method of CR. -*/ - public static final UnaryCRFunction cosFunction = - new cos_UnaryCRFunction(); - -/** -* The function object corresponding to the sin method of CR. -*/ - public static final UnaryCRFunction sinFunction = - new sin_UnaryCRFunction(); - -/** -* The function object corresponding to the tangent function. -*/ - public static final UnaryCRFunction tanFunction = - new tan_UnaryCRFunction(); - -/** -* The function object corresponding to the inverse sine (arcsine) function. -* The argument must be between -1 and 1 inclusive. The result is between -* -PI/2 and PI/2. -*/ - public static final UnaryCRFunction asinFunction = - new asin_UnaryCRFunction(); - // The following also works, but is slower: - // CR half_pi = CR.PI.divide(CR.valueOf(2)); - // UnaryCRFunction.sinFunction.inverseMonotone(half_pi.negate(), - // half_pi); - -/** -* The function object corresponding to the inverse cosine (arccosine) function. -* The argument must be between -1 and 1 inclusive. The result is between -* 0 and PI. -*/ - public static final UnaryCRFunction acosFunction = - new acos_UnaryCRFunction(); - -/** -* The function object corresponding to the inverse cosine (arctangent) function. -* The result is between -PI/2 and PI/2. -*/ - public static final UnaryCRFunction atanFunction = - new atan_UnaryCRFunction(); - -/** -* The function object corresponding to the ln method of CR. -*/ - public static final UnaryCRFunction lnFunction = - new ln_UnaryCRFunction(); - -/** -* The function object corresponding to the sqrt method of CR. -*/ - public static final UnaryCRFunction sqrtFunction = - new sqrt_UnaryCRFunction(); - -/** -* Compose this function with f2. -*/ - public UnaryCRFunction compose(UnaryCRFunction f2) { - return new compose_UnaryCRFunction(this, f2); - } - -/** -* Compute the inverse of this function, which must be defined -* and strictly monotone on the interval [low, high]. -* The resulting function is defined only on the image of -* [low, high]. -* The original function may be either increasing or decreasing. -*/ - public UnaryCRFunction inverseMonotone(CR low, CR high) { - return new inverseMonotone_UnaryCRFunction(this, low, high); - } - -/** -* Compute the derivative of a function. -* The function must be defined on the interval [low, high], -* and the derivative must exist, and must be continuous and -* monotone in the open interval [low, high]. -* The result is defined only in the open interval. -*/ - public UnaryCRFunction monotoneDerivative(CR low, CR high) { - return new monotoneDerivative_UnaryCRFunction(this, low, high); - } - -} - -// Subclasses of UnaryCRFunction for various built-in functions. -class sin_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.sin(); - } -} - -class cos_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.cos(); - } -} - -class tan_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.sin().divide(x.cos()); - } -} - -class asin_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.asin(); - } -} - -class acos_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.acos(); - } -} - -// This uses the identity (sin x)^2 = (tan x)^2/(1 + (tan x)^2) -// Since we know the tangent of the result, we can get its sine, -// and then use the asin function. Note that we don't always -// want the positive square root when computing the sine. -class atan_UnaryCRFunction extends UnaryCRFunction { - CR one = CR.valueOf(1); - public CR execute(CR x) { - CR x2 = x.multiply(x); - CR abs_sin_atan = x2.divide(one.add(x2)).sqrt(); - CR sin_atan = x.select(abs_sin_atan.negate(), abs_sin_atan); - return sin_atan.asin(); - } -} - -class exp_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.exp(); - } -} - -class ln_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.ln(); - } -} - -class identity_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x; - } -} - -class negate_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.negate(); - } -} - -class inverse_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.inverse(); - } -} - -class abs_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.abs(); - } -} - -class sqrt_UnaryCRFunction extends UnaryCRFunction { - public CR execute(CR x) { - return x.sqrt(); - } -} - -class compose_UnaryCRFunction extends UnaryCRFunction { - UnaryCRFunction f1; - UnaryCRFunction f2; - compose_UnaryCRFunction(UnaryCRFunction func1, - UnaryCRFunction func2) { - f1 = func1; f2 = func2; - } - public CR execute(CR x) { - return f1.execute(f2.execute(x)); - } -} - -class inverseMonotone_UnaryCRFunction extends UnaryCRFunction { - // The following variables are final, so that they - // can be referenced from the inner class inverseIncreasingCR. - // I couldn't find a way to initialize these such that the - // compiler accepted them as final without turning them into arrays. - final UnaryCRFunction f[] = new UnaryCRFunction[1]; - // Monotone increasing. - // If it was monotone decreasing, we - // negate it. - final boolean f_negated[] = new boolean[1]; - final CR low[] = new CR[1]; - final CR high[] = new CR[1]; - final CR f_low[] = new CR[1]; - final CR f_high[] = new CR[1]; - final int max_msd[] = new int[1]; - // Bound on msd of both f(high) and f(low) - final int max_arg_prec[] = new int[1]; - // base**max_arg_prec is a small fraction - // of low - high. - final int deriv_msd[] = new int[1]; - // Rough approx. of msd of first - // derivative. - final static BigInteger BIG1023 = BigInteger.valueOf(1023); - static final boolean ENABLE_TRACE = false; // Change to generate trace - static void trace(String s) { - if (ENABLE_TRACE) { - System.out.println(s); - // Change to Log.v("UnaryCRFunction", s); for Android use. - } - } - inverseMonotone_UnaryCRFunction(UnaryCRFunction func, CR l, CR h) { - low[0] = l; high[0] = h; - CR tmp_f_low = func.execute(l); - CR tmp_f_high = func.execute(h); - // Since func is monotone and low < high, the following test - // converges. - if (tmp_f_low.compareTo(tmp_f_high) > 0) { - f[0] = UnaryCRFunction.negateFunction.compose(func); - f_negated[0] = true; - f_low[0] = tmp_f_low.negate(); - f_high[0] = tmp_f_high.negate(); - } else { - f[0] = func; - f_negated[0] = false; - f_low[0] = tmp_f_low; - f_high[0] = tmp_f_high; - } - max_msd[0] = low[0].abs().max(high[0].abs()).msd(); - max_arg_prec[0] = high[0].subtract(low[0]).msd() - 4; - deriv_msd[0] = f_high[0].subtract(f_low[0]) - .divide(high[0].subtract(low[0])).msd(); - } - class inverseIncreasingCR extends CR { - final CR arg; - inverseIncreasingCR(CR x) { - arg = f_negated[0]? x.negate() : x; - } - // Comparison with a difference of one treated as equality. - int sloppy_compare(BigInteger x, BigInteger y) { - BigInteger difference = x.subtract(y); - if (difference.compareTo(big1) > 0) { - return 1; - } - if (difference.compareTo(bigm1) < 0) { - return -1; - } - return 0; - } - protected BigInteger approximate(int p) { - final int extra_arg_prec = 4; - final UnaryCRFunction fn = f[0]; - int small_step_deficit = 0; // Number of ineffective steps not - // yet compensated for by a binary - // search step. - int digits_needed = max_msd[0] - p; - if (digits_needed < 0) return big0; - int working_arg_prec = p - extra_arg_prec; - if (working_arg_prec > max_arg_prec[0]) { - working_arg_prec = max_arg_prec[0]; - } - int working_eval_prec = working_arg_prec + deriv_msd[0] - 20; - // initial guess - // We use a combination of binary search and something like - // the secant method. This always converges linearly, - // and should converge quadratically under favorable assumptions. - // F_l and f_h are always the approximate images of l and h. - // At any point, arg is between f_l and f_h, or no more than - // one outside [f_l, f_h]. - // L and h are implicitly scaled by working_arg_prec. - // The scaled values of l and h are strictly between low and high. - // If at_left is true, then l is logically at the left - // end of the interval. We approximate this by setting l to - // a point slightly inside the interval, and letting f_l - // approximate the function value at the endpoint. - // If at_right is true, r and f_r are set correspondingly. - // At the endpoints of the interval, f_l and f_h may correspond - // to the endpoints, even if l and h are slightly inside. - // F_l and f_u are scaled by working_eval_prec. - // Working_eval_prec may need to be adjusted depending - // on the derivative of f. - boolean at_left, at_right; - BigInteger l, f_l; - BigInteger h, f_h; - BigInteger low_appr = low[0].get_appr(working_arg_prec) - .add(big1); - BigInteger high_appr = high[0].get_appr(working_arg_prec) - .subtract(big1); - BigInteger arg_appr = arg.get_appr(working_eval_prec); - boolean have_good_appr = (appr_valid && min_prec < max_msd[0]); - if (digits_needed < 30 && !have_good_appr) { - trace("Setting interval to entire domain"); - h = high_appr; - f_h = f_high[0].get_appr(working_eval_prec); - l = low_appr; - f_l = f_low[0].get_appr(working_eval_prec); - // Check for clear out-of-bounds case. - // Close cases may fail in other ways. - if (f_h.compareTo(arg_appr.subtract(big1)) < 0 - || f_l.compareTo(arg_appr.add(big1)) > 0) { - throw new ArithmeticException("inverse(out-of-bounds)"); - } - at_left = true; - at_right = true; - small_step_deficit = 2; // Start with bin search steps. - } else { - int rough_prec = p + digits_needed/2; - - if (have_good_appr && - (digits_needed < 30 || min_prec < p + 3*digits_needed/4)) { - rough_prec = min_prec; - } - BigInteger rough_appr = get_appr(rough_prec); - trace("Setting interval based on prev. appr"); - trace("prev. prec = " + rough_prec + " appr = " + rough_appr); - h = rough_appr.add(big1) - .shiftLeft(rough_prec - working_arg_prec); - l = rough_appr.subtract(big1) - .shiftLeft(rough_prec - working_arg_prec); - if (h.compareTo(high_appr) > 0) { - h = high_appr; - f_h = f_high[0].get_appr(working_eval_prec); - at_right = true; - } else { - CR h_cr = CR.valueOf(h).shiftLeft(working_arg_prec); - f_h = fn.execute(h_cr).get_appr(working_eval_prec); - at_right = false; - } - if (l.compareTo(low_appr) < 0) { - l = low_appr; - f_l = f_low[0].get_appr(working_eval_prec); - at_left = true; - } else { - CR l_cr = CR.valueOf(l).shiftLeft(working_arg_prec); - f_l = fn.execute(l_cr).get_appr(working_eval_prec); - at_left = false; - } - } - BigInteger difference = h.subtract(l); - for(int i = 0;; ++i) { - if (Thread.interrupted() || please_stop) - throw new AbortedException(); - trace("***Iteration: " + i); - trace("Arg prec = " + working_arg_prec - + " eval prec = " + working_eval_prec - + " arg appr. = " + arg_appr); - trace("l = " + l); trace("h = " + h); - trace("f(l) = " + f_l); trace("f(h) = " + f_h); - if (difference.compareTo(big6) < 0) { - // Answer is less than 1/2 ulp away from h. - return scale(h, -extra_arg_prec); - } - BigInteger f_difference = f_h.subtract(f_l); - // Narrow the interval by dividing at a cleverly - // chosen point (guess) in the middle. - { - BigInteger guess; - boolean binary_step = - (small_step_deficit > 0 || f_difference.signum() == 0); - if (binary_step) { - // Do a binary search step to guarantee linear - // convergence. - trace("binary step"); - guess = l.add(h).shiftRight(1); - --small_step_deficit; - } else { - // interpolate. - // f_difference is nonzero here. - trace("interpolating"); - BigInteger arg_difference = arg_appr.subtract(f_l); - BigInteger t = arg_difference.multiply(difference); - BigInteger adj = t.divide(f_difference); - // tentative adjustment to l to compute guess - // If we are within 1/1024 of either end, back off. - // This greatly improves the odds of bounding - // the answer within the smaller interval. - // Note that interpolation will often get us - // MUCH closer than this. - if (adj.compareTo(difference.shiftRight(10)) < 0) { - adj = adj.shiftLeft(8); - trace("adjusting left"); - } else if (adj.compareTo(difference.multiply(BIG1023) - .shiftRight(10)) > 0){ - adj = difference.subtract(difference.subtract(adj) - .shiftLeft(8)); - trace("adjusting right"); - } - if (adj.signum() <= 0) - adj = big2; - if (adj.compareTo(difference) >= 0) - adj = difference.subtract(big2); - guess = (adj.signum() <= 0? l.add(big2) : l.add(adj)); - } - int outcome; - BigInteger tweak = big2; - BigInteger f_guess; - for(boolean adj_prec = false;; adj_prec = !adj_prec) { - CR guess_cr = CR.valueOf(guess) - .shiftLeft(working_arg_prec); - trace("Evaluating at " + guess_cr - + " with precision " + working_eval_prec); - CR f_guess_cr = fn.execute(guess_cr); - trace("fn value = " + f_guess_cr); - f_guess = f_guess_cr.get_appr(working_eval_prec); - outcome = sloppy_compare(f_guess, arg_appr); - if (outcome != 0) break; - // Alternately increase evaluation precision - // and adjust guess slightly. - // This should be an unlikely case. - if (adj_prec) { - // adjust working_eval_prec to get enough - // resolution. - int adjustment = -f_guess.bitLength()/4; - if (adjustment > -20) adjustment = - 20; - CR l_cr = CR.valueOf(l) - .shiftLeft(working_arg_prec); - CR h_cr = CR.valueOf(h) - .shiftLeft(working_arg_prec); - working_eval_prec += adjustment; - trace("New eval prec = " + working_eval_prec - + (at_left? "(at left)" : "") - + (at_right? "(at right)" : "")); - if (at_left) { - f_l = f_low[0].get_appr(working_eval_prec); - } else { - f_l = fn.execute(l_cr) - .get_appr(working_eval_prec); - } - if (at_right) { - f_h = f_high[0].get_appr(working_eval_prec); - } else { - f_h = fn.execute(h_cr) - .get_appr(working_eval_prec); - } - arg_appr = arg.get_appr(working_eval_prec); - } else { - // guess might be exactly right; tweak it - // slightly. - trace("tweaking guess"); - BigInteger new_guess = guess.add(tweak); - if (new_guess.compareTo(h) >= 0) { - guess = guess.subtract(tweak); - } else { - guess = new_guess; - } - // If we keep hitting the right answer, it's - // important to alternate which side we move it - // to, so that the interval shrinks rapidly. - tweak = tweak.negate(); - } - } - if (outcome > 0) { - h = guess; - f_h = f_guess; - at_right = false; - } else { - l = guess; - f_l = f_guess; - at_left = false; - } - BigInteger new_difference = h.subtract(l); - if (!binary_step) { - if (new_difference.compareTo(difference - .shiftRight(1)) >= 0) { - ++small_step_deficit; - } else { - --small_step_deficit; - } - } - difference = new_difference; - } - } - } - } - public CR execute(CR x) { - return new inverseIncreasingCR(x); - } -} - -class monotoneDerivative_UnaryCRFunction extends UnaryCRFunction { - // The following variables are final, so that they - // can be referenced from the inner class inverseIncreasingCR. - final UnaryCRFunction f[] = new UnaryCRFunction[1]; - // Monotone increasing. - // If it was monotone decreasing, we - // negate it. - final CR low[] = new CR[1]; // endpoints and mispoint of interval - final CR mid[] = new CR[1]; - final CR high[] = new CR[1]; - final CR f_low[] = new CR[1]; // Corresponding function values. - final CR f_mid[] = new CR[1]; - final CR f_high[] = new CR[1]; - final int difference_msd[] = new int[1]; // msd of interval len. - final int deriv2_msd[] = new int[1]; - // Rough approx. of msd of second - // derivative. - // This is increased to be an appr. bound - // on the msd of |(f'(y)-f'(x))/(x-y)| - // for any pair of points x and y - // we have considered. - // It may be better to keep a copy per - // derivative value. - - monotoneDerivative_UnaryCRFunction(UnaryCRFunction func, CR l, CR h) { - f[0] = func; - low[0] = l; high[0] = h; - mid[0] = l.add(h).shiftRight(1); - f_low[0] = func.execute(l); - f_mid[0] = func.execute(mid[0]); - f_high[0] = func.execute(h); - CR difference = h.subtract(l); - // compute approximate msd of - // ((f_high - f_mid) - (f_mid - f_low))/(high - low) - // This should be a very rough appr to the second derivative. - // We add a little slop to err on the high side, since - // a low estimate will cause extra iterations. - CR appr_diff2 = f_high[0].subtract(f_mid[0].shiftLeft(1)).add(f_low[0]); - difference_msd[0] = difference.msd(); - deriv2_msd[0] = appr_diff2.msd() - difference_msd[0] + 4; - } - class monotoneDerivativeCR extends CR { - CR arg; - CR f_arg; - int max_delta_msd; - monotoneDerivativeCR(CR x) { - arg = x; - f_arg = f[0].execute(x); - // The following must converge, since arg must be in the - // open interval. - CR left_diff = arg.subtract(low[0]); - int max_delta_left_msd = left_diff.msd(); - CR right_diff = high[0].subtract(arg); - int max_delta_right_msd = right_diff.msd(); - if (left_diff.signum() < 0 || right_diff.signum() < 0) { - throw new ArithmeticException("fn not monotone"); - } - max_delta_msd = (max_delta_left_msd < max_delta_right_msd? - max_delta_left_msd - : max_delta_right_msd); - } - protected BigInteger approximate(int p) { - final int extra_prec = 4; - int log_delta = p - deriv2_msd[0]; - // Ensure that we stay within the interval. - if (log_delta > max_delta_msd) log_delta = max_delta_msd; - log_delta -= extra_prec; - CR delta = ONE.shiftLeft(log_delta); - - CR left = arg.subtract(delta); - CR right = arg.add(delta); - CR f_left = f[0].execute(left); - CR f_right = f[0].execute(right); - CR left_deriv = f_arg.subtract(f_left).shiftRight(log_delta); - CR right_deriv = f_right.subtract(f_arg).shiftRight(log_delta); - int eval_prec = p - extra_prec; - BigInteger appr_left_deriv = left_deriv.get_appr(eval_prec); - BigInteger appr_right_deriv = right_deriv.get_appr(eval_prec); - BigInteger deriv_difference = - appr_right_deriv.subtract(appr_left_deriv).abs(); - if (deriv_difference.compareTo(big8) < 0) { - return scale(appr_left_deriv, -extra_prec); - } else { - if (Thread.interrupted() || please_stop) - throw new AbortedException(); - deriv2_msd[0] = - eval_prec + deriv_difference.bitLength() + 4/*slop*/; - deriv2_msd[0] -= log_delta; - return approximate(p); - } - } - } - public CR execute(CR x) { - return new monotoneDerivativeCR(x); - } -} diff --git a/app/src/main/java/com/prime/toolz2/core/math/more.kt b/app/src/main/java/com/prime/toolz2/core/math/more.kt deleted file mode 100644 index 3d31d0b..0000000 --- a/app/src/main/java/com/prime/toolz2/core/math/more.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.prime.toolz2.core.math - -import java.math.BigInteger - -object NumUtil { - - /** - * The symbol used to represent exponent part of a number. - */ - const val EXPONENT = 'E' - - /** - * The symbol for the decimal point of a number. - */ - const val DECIMAL = '.' - - /** - * The symbol for the zero digit - */ - const val ZERO = '0' - - /** - * Removes Front zeros from the whole part of a number. - * - * @param whole The Whole Part of a number. - * @return modified number. - */ - fun removeFrontZeroes(whole: String): String { - - //FIXME: Use whole number not just 'whole' part of number. - - val s = StringBuilder(whole) - val lastWasZero = whole.indexOf("0") == 0 && s.length > 1 - var i = 0 - while (i < s.length) { - if (lastWasZero && s[i] == '0' && i + 1 < s.length && s[i + 1] != '.') { - s.deleteCharAt(i) - continue - } - i++ - } - return s.toString() - } - - /** - * Strips Trailing Zeros to from fraction part of a [String] representation of a number. - * - * @param fraction [String] representation of Number - * @return [String] representation of modified number. - */ - fun stripTrailingZeroes(fraction: String): String { - - // FIXME: Incorporate whole number not just fraction part. - - // if the provided number doesn't contain the decimal; then return the - // provided number without any modifications - // if (!fraction.contains(DECIMAL)) return fraction - - // else remove/strip th zeros from the fraction part - val s = StringBuilder(fraction) - var lastWasZero = fraction.lastIndexOf("0") == fraction.length - 1 - for (i in s.length - 1 downTo 0) { - if ((s[i] == '0' || s[i] == '.') && lastWasZero) { - if (s[i] == '.') lastWasZero = false - s.deleteCharAt(i) - } else lastWasZero = false - } - return s.toString() - } - - - /** - * A helper function to split a number into 3 parts; whole, fraction and exponent. - * - * **Note** - * if the component (say fraction) is missing; while the symbol i.e., [DECIMAL] is present - * present empty is returned else *null* at its place in the resulting array. - * - * ***For E.g.*** - * If the given number is 12345.6789E12345 it will return a string array where - * a[0] = 12345 (i.e, Whole Part) - * a[1] = .6789 (i.e., Fractional Part) - * a[2] = 12345 (i.e., Scientific part) - * - * @param number: The number to split e.g., 123.45E789 - * @return [Array] of size 3 with whole, fraction and exponent at respective hole. - */ - fun split(number: String): Array { - - //FIXME: Find out what happens to minus sign. - - val array = arrayOfNulls(3) - - // The index of the components or null - // Why null? - // because it is easy to work with null than -1 - val iFraction = number.indexOf(DECIMAL).let { if (it == -1) null else it } - val iExponent = number.indexOf(EXPONENT).let { if (it == -1) null else it } - - // why not use split method. - // first it never returns array of size 3 every time. - // it returns array list than again we have to check if properly it has worked; adds multiple overheads - - // whole part - array[0] = number - .substring(0, iFraction ?: iExponent ?: number.length) - //.let { it.ifEmpty { null } } - - // fractional part - array[1] = - if (iFraction == null) - null - else - number - .substring(iFraction + 1, iExponent ?: number.length) - // if the fractional decimal present but no number - // return zero in its place. - //.let { it.ifEmpty { null } } - - // exponent - array[2] = - if (iExponent == null) - null - else - number - .substring(iExponent + 1, number.length) - // same logic as stated above. - //.let { it.ifEmpty { null } } - - // return - return array - } - - /** - * Return a copy of the supplied string with [separator] added every three digits. - * Inserting a digit separator every 3 digits appears to be - * at least somewhat acceptable, though not necessarily preferred, everywhere. - - * The grouping separator in the result is NOT localized. - * @param whole: The number as whole in the format 123.456E789, - * @param separator: The provided separator char like , - */ - fun addThousandSeparators(whole: String, separator: Char): String { - - //FIXME: Find out what happens to minus sign. and incorparate whole number not just 'whole' part - - // add separators to the resulting whole part. - return buildString { - val begin = 0 - val end = whole.length - - var current = begin - append(whole, begin, current) - while (current < end) { - append(whole[current]) - ++current - if ((end - current) % 3 == 0 && end != current) { - append(separator) - } - } - } - } - - - /** - * Renders a real number to a String (for user display). - * @param maxLen the maximum total length of the resulting string - * @param rounding the number of final digits to round - */ - @Deprecated("not recommended.", level = DeprecationLevel.WARNING) - fun doubleToString(x: Double, maxLen: Int, rounding: Int): String? { - return Util.sizeTruncate(Util.doubleToString(x, rounding), maxLen) - } -} - -@Deprecated("not recommended.", level = DeprecationLevel.HIDDEN) - /** - * Stringifies the value of this [UnifiedReal]'s double value. - * TODO: Move away from double - */ -fun UnifiedReal.stringfy(len: Int, rounding: Int): String = - Util.doubleToString(doubleValue(), len, rounding) - -/** - * Constructs a unified real from any text. e.g., 1234.4565465 - */ -@Suppress("FunctionName") -fun UnifiedReal(text: String): UnifiedReal { - // split number in respective components. - val (whole, fraction, exponent) = NumUtil.split(text) - // Through error if an string with exponent is supplied. - if (exponent != null) - error("Current Implantation doesn't support exponent!!") - - // construct the numerator from the provided info - val numerator = BigInteger( - // replace with zero if null - (whole ?: "0") + (fraction ?: "") - ) - // construct the denominator from the provided info. - val denominator = BigInteger.TEN.pow( - // 10 power zero i.e., if fractional part empty - fraction?.length ?: 0 - ) - // currently we don't support exponents - return UnifiedReal(BoundedRational(numerator, denominator)) -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/settings/FontFamily.kt b/app/src/main/java/com/prime/toolz2/settings/FontFamily.kt deleted file mode 100644 index 1769ae1..0000000 --- a/app/src/main/java/com/prime/toolz2/settings/FontFamily.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.prime.toolz2.settings - -/** - * TODO: Find appropriate way to support Google fonts. - */ -enum class FontFamily { - // The default typography of app - SYSTEM_DEFAULT, - - // - PROVIDED, - - // - SAN_SERIF, - - // - SARIF, - - // - CURSIVE -} diff --git a/app/src/main/java/com/prime/toolz2/settings/GlobalKeys.kt b/app/src/main/java/com/prime/toolz2/settings/GlobalKeys.kt deleted file mode 100644 index dbddf58..0000000 --- a/app/src/main/java/com/prime/toolz2/settings/GlobalKeys.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.prime.toolz2.settings - -import com.primex.preferences.* - -private const val TAG = "PrefKeys" - -object GlobalKeys { - /** - * Retrieves/Sets The [NightMode] Strategy - */ - val NIGHT_MODE = - stringPreferenceKey( - "${TAG}_night_mode", - NightMode.NO, - object : StringSaver { - override fun save(value: NightMode): String = value.name - - override fun restore(value: String): NightMode = NightMode.valueOf(value) - } - ) - - val FONT_FAMILY = - stringPreferenceKey( - TAG + "_font_family", - FontFamily.PROVIDED, - object : StringSaver { - override fun save(value: FontFamily): String = value.name - - override fun restore(value: String): FontFamily = FontFamily.valueOf(value) - } - ) - - - val FORCE_COLORIZE = - booleanPreferenceKey( - TAG + "_force_colorize", - false - ) - - val COLOR_STATUS_BAR = - booleanPreferenceKey( - TAG + "_color_status_bar", - false - ) - - val HIDE_STATUS_BAR = - booleanPreferenceKey( - TAG + "_hide_status_bar", - false - ) - - val GROUP_SEPARATOR = - stringPreferenceKey( - name = TAG + "_group_separator", - defaultValue = ',', - saver = object : StringSaver { - override fun restore(value: String): Char = value[0] - - override fun save(value: Char): String = "$value" - } - ) - - val DECIMAL_SEPARATOR = - stringPreferenceKey( - name = TAG + "_decimal_separator", - defaultValue = "." - ) - - val NUMBER_OF_DECIMALS = - intPreferenceKey( - name = TAG + "_number_of_decimals", - defaultValue = 5 - ) - - val FONT_SCALE = - floatPreferenceKey( - TAG + "_font_scale", - defaultValue = 1.0f - ) - - /** - * The counter counts the number of times this app was launched. - */ - val KEY_LAUNCH_COUNTER = - intPreferenceKey(TAG + "_launch_counter") - -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/settings/NightMode.kt b/app/src/main/java/com/prime/toolz2/settings/NightMode.kt deleted file mode 100644 index de62664..0000000 --- a/app/src/main/java/com/prime/toolz2/settings/NightMode.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.prime.toolz2.settings - -enum class NightMode { - /** - * Night mode which uses always uses a light mode, enabling {@code notnight} qualified - * resources regardless of the time. - * - * @see #setLocalNightMode(int) - */ - YES, - - /** - * Night mode which uses always uses a dark mode, enabling {@code night} qualified - * resources regardless of the time. - * - * @see #setLocalNightMode(int) - */ - NO, - - /** - * Mode which uses the system's night mode setting to determine if it is night or not. - * - * @see #setLocalNightMode(int) - */ - FOLLOW_SYSTEM, - - /** - * Night mode which uses a dark mode when the system's 'Battery Saver' feature is enabled, - * otherwise it uses a 'light mode'. This mode can help the device to decrease power usage, - * depending on the display technology in the device. - * - * Please note: this mode should only be used when running on devices which do not - * provide a similar device-wide setting. - * - * @see #setLocalNightMode(int) - */ - AUTO_BATTER, - - /** - * Night mode which switches between dark and light mode depending on the time of day - * (dark at night, light in the day). - * - * The calculation used to determine whether it is night or not makes use of the location - * APIs (if this app has the necessary permissions). This allows us to generate accurate - * sunrise and sunset times. If this app does not have permission to access the location APIs - * then we use hardcoded times which will be less accurate. - */ - AUTO_TIME -} diff --git a/app/src/main/java/com/prime/toolz2/settings/Settings.kt b/app/src/main/java/com/prime/toolz2/settings/Settings.kt deleted file mode 100644 index 0a5b0b2..0000000 --- a/app/src/main/java/com/prime/toolz2/settings/Settings.kt +++ /dev/null @@ -1,313 +0,0 @@ -package com.prime.toolz2.settings - -import android.app.Activity -import android.content.Intent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.prime.toolz2.* -import com.prime.toolz2.R -import com.prime.toolz2.common.compose.* -import com.primex.core.activity -import com.primex.core.drawHorizontalDivider -import com.primex.core.stringHtmlResource -import com.primex.ui.* -import cz.levinzonr.saferoute.accompanist.navigation.transitions.AnimatedRouteTransition -import cz.levinzonr.saferoute.core.annotations.Route -import cz.levinzonr.saferoute.core.annotations.RouteNavGraph - - -private val RESERVE_PADDING = 56.dp - -private const val FONT_SCALE_LOWER_BOUND = 0.5f -private const val FONT_SCALE_UPPER_BOUND = 2.0f - -private const val SLIDER_STEPS = 15 - -private const val ZERO_WIDTH_CHAR = '\u200B' - -//TODO: Instead of string use Text -private val familyList = - listOf( - "Lato" to FontFamily.PROVIDED, - "Cursive" to FontFamily.CURSIVE, - "San serif" to FontFamily.SAN_SERIF, - "serif" to FontFamily.SARIF, - "System default" to FontFamily.SYSTEM_DEFAULT - ) - -val GroupSeparatorList = - listOf( - "Space ( ) " to ' ', - "Hyphen (_) " to '_', - "None" to ZERO_WIDTH_CHAR, - "Comma ( , )" to ',' - ) - -@Composable -private inline fun PrefHeader(text: String) { - val primary = MaterialTheme.colors.secondary - val modifier = - Modifier - .padding( - start = RESERVE_PADDING, - top = ContentPadding.normal, - end = ContentPadding.large, - bottom = ContentPadding.medium - ) - .fillMaxWidth() - .drawHorizontalDivider(color = primary) - .padding(bottom = ContentPadding.medium) - Label( - text = text, - modifier = modifier, - fontWeight = FontWeight.SemiBold, - maxLines = 2, - color = primary - ) -} - -context(ColumnScope) @Composable -private inline fun AboutUs() { - PrefHeader(text = "Feedback") - - // val feedbackCollector = LocalFeedbackCollector.current - val onRequestFeedback = { - // TODO: Handle feedback - } - - Preference( - title = stringResource(R.string.feedback), - summery = (stringResource(id = R.string.feedback_dialog_placeholder) + "\nTap to open feedback dialog."), - icon = Icons.Outlined.Feedback, - modifier = Modifier.clickable(onClick = onRequestFeedback) - ) - - val onRequestRateApp = { - // TODO: Handle rate app. - } - Preference( - title = stringResource(R.string.rate_us), - summery = stringResource(id = R.string.review_msg), - icon = Icons.Outlined.Star, - modifier = Modifier.clickable(onClick = onRequestRateApp) - ) - - val onRequestShareApp = { - // TODO: Share app. - } - Preference( - title = stringResource(R.string.spread_the_word), - summery = stringResource(R.string.spread_the_word_summery), - icon = Icons.Outlined.Share, - modifier = Modifier.clickable(onClick = onRequestShareApp) - ) - - PrefHeader(text = stringResource(R.string.about_us)) - Text( - text = stringHtmlResource(R.string.about_us_desc), - style = MaterialTheme.typography.body2, - modifier = Modifier - .padding(start = RESERVE_PADDING, end = ContentPadding.large) - .padding(vertical = ContentPadding.small), - color = LocalContentColor.current.copy(ContentAlpha.medium) - ) - - val context = LocalContext.current - val version = remember { - context.packageManager.getPackageInfo(context.packageName, 0).versionName - } - //val updateNotifier = LocalUpdateNotifier.current - val activity = LocalContext.current.activity!! - val channel = LocalSnackDataChannel.current - val onCheckUpdate: () -> Unit = { - activity.launchUpdateFlow(channel, true) - } - Preference( - title = stringResource(R.string.app_version), - summery = "$version \nClick to check for updates.", - icon = Icons.Outlined.TouchApp, - modifier = Modifier.clickable(onClick = onCheckUpdate) - ) -} - -private fun Activity.restart() { - finish() - startActivity(Intent(this, this.javaClass)) - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) -} - -@Composable -private fun TopAppBar(modifier: Modifier = Modifier) { - val navigator = LocalNavController.current - - NeumorphicTopAppBar( - title = { Label(text = stringResource(R.string.settings)) }, - modifier = modifier.padding(top = ContentPadding.medium), - navigationIcon = { - IconButton( - onClick = { navigator.navigateUp() }, - imageVector = Icons.Outlined.ReplyAll, - contentDescription = null - ) - }, - shape = CircleShape, - elevation = ContentElevation.low, - lightShadowColor = Material.colors.lightShadowColor, - darkShadowColor = Material.colors.darkShadowColor - ) -} - - -@OptIn(ExperimentalAnimationApi::class) -@Route( - transition = AnimatedRouteTransition.Default::class, - navGraph = RouteNavGraph(start = false) -) -@Composable -fun Settings(viewModel: SettingsViewModel) { - with(viewModel) { - Scaffold( - topBar = { - val (colorStatusBar, _, _, _) = colorStatusBar.value - val primaryOrTransparent = - Material.colors.primary(colorStatusBar, Color.Transparent) - TopAppBar( - modifier = Modifier - .statusBarsPadding2( - color = primaryOrTransparent, - darkIcons = !colorStatusBar && Material.colors.isLight - ) - .drawHorizontalDivider(color = Material.colors.onSurface) - .padding(bottom = ContentPadding.medium) - ) - }, - - - content = { - val state = rememberScrollState() - //val color = if (MaterialTheme.colors.isLight) Color.White else Color.Black - Column( - modifier = Modifier - .padding(it) - //FixMe: Creates a issue between Theme changes - // needs to be study properly - // disabling for now - //.fadeEdge(state = state, length = 16.dp, horizontal = false, color = color) - .verticalScroll(state), - ) { - PrefHeader(text = stringResource(R.string.appearence)) - - //dark mode - val activity = LocalContext.current.activity!! - val darkTheme by darkUiMode - SwitchPreference( - checked = darkTheme.value, - title = stringResource(res = darkTheme.title), - summery = darkTheme.summery?.let { stringResource(res = it) }, - icon = darkTheme.vector, - onCheckedChange = { new: Boolean -> - set(GlobalKeys.NIGHT_MODE, if (new) NightMode.YES else NightMode.NO) - activity.launchReviewFlow() - } - ) - - //font - val font by font - DropDownPreference( - title = stringResource(res = font.title), - entries = familyList, - defaultValue = font.value, - icon = font.vector, - onRequestChange = { family: FontFamily -> - viewModel.set(GlobalKeys.FONT_FAMILY, family) - } - ) - - val scale by fontScale - SliderPreference( - defaultValue = scale.value, - title = stringResource(res = scale.title), - summery = scale.summery?.let { stringResource(res = it) }, - valueRange = FONT_SCALE_LOWER_BOUND..FONT_SCALE_UPPER_BOUND, - steps = SLIDER_STEPS, - icon = scale.vector, - iconChange = Icons.Outlined.TextFormat, - onValueChange = { value: Float -> - set(GlobalKeys.FONT_SCALE, value) - } - ) - - //force accent - val forceAccent by forceAccent - SwitchPreference( - checked = forceAccent.value, - title = stringResource(res = forceAccent.title), - summery = forceAccent.summery?.let { stringResource(res = it) }, - onCheckedChange = { should: Boolean -> - set(GlobalKeys.FORCE_COLORIZE, should) - if (should) - set(GlobalKeys.COLOR_STATUS_BAR, true) - } - ) - - //color status bar - val colorStatusBar by colorStatusBar - SwitchPreference( - checked = colorStatusBar.value, - title = stringResource(res = colorStatusBar.title), - summery = colorStatusBar.summery?.let { stringResource(res = it) }, - enabled = !forceAccent.value, - onCheckedChange = { should: Boolean -> - set(GlobalKeys.COLOR_STATUS_BAR, should) - } - ) - - //hide status bar - val hideStatusBar by hideStatusBar - SwitchPreference( - checked = hideStatusBar.value, - title = stringResource(res = hideStatusBar.title), - summery = hideStatusBar.summery?.let { stringResource(res = it) }, - onCheckedChange = { should: Boolean -> - set(GlobalKeys.HIDE_STATUS_BAR, should) - } - ) - - // group separator - val separator by groupSeparator - DropDownPreference( - title = stringResource(separator.title), - entries = GroupSeparatorList, - defaultValue = separator.value, - icon = separator.vector, - onRequestChange = { - viewModel.set(GlobalKeys.GROUP_SEPARATOR, it) - } - ) - - //About us section. - AboutUs() - } - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/settings/SettingsViewModel.kt b/app/src/main/java/com/prime/toolz2/settings/SettingsViewModel.kt deleted file mode 100644 index dd75973..0000000 --- a/app/src/main/java/com/prime/toolz2/settings/SettingsViewModel.kt +++ /dev/null @@ -1,173 +0,0 @@ -@file:Suppress("NOTHING_TO_INLINE") - -package com.prime.toolz2.settings - - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.TextFields -import androidx.compose.material.icons.outlined.HideImage -import androidx.compose.material.icons.outlined.Lightbulb -import androidx.compose.material.icons.outlined.ZoomIn -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.primex.core.Text -import com.primex.preferences.* -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import javax.inject.Inject - -data class Preference( - val value: P, - val title: Text, - val vector: ImageVector? = null, - val summery: Text? = null, -) - -@HiltViewModel -class SettingsViewModel @Inject constructor( - private val preferences: Preferences, -) : ViewModel() { - - val darkUiMode = - with(preferences) { - preferences[GlobalKeys.NIGHT_MODE].map { - Preference( - value = when (it) { - NightMode.YES -> true - else -> false - }, - title = Text("Dark Mode"), - summery = Text("Click to change the app night/light mode."), - vector = Icons.Outlined.Lightbulb - ) - }.collectAsState() - } - - val font = - with(preferences) { - preferences[GlobalKeys.FONT_FAMILY].map { - Preference( - vector = Icons.Default.TextFields, - title = Text("Font"), - summery = Text("Choose font to better reflect your desires."), - value = it - ) - }.collectAsState() - } - - val colorStatusBar = - with(preferences) { - preferences[GlobalKeys.COLOR_STATUS_BAR] - .map { - Preference( - vector = null, - title = Text("Color Status Bar"), - summery = Text("Force color status bar."), - value = it - ) - } - .collectAsState() - } - - val hideStatusBar = - with(preferences) { - preferences[GlobalKeys.HIDE_STATUS_BAR] - .map { - Preference( - value = it, - title = Text("Hide Status Bar"), - summery = Text("hide status bar for immersive view"), - vector = Icons.Outlined.HideImage - ) - } - .collectAsState() - } - - val forceAccent = - with(preferences) { - preferences[GlobalKeys.FORCE_COLORIZE] - .map { - Preference( - value = it, - title = Text("Force Accent Color"), - summery = Text("Normally the app follows the rule of using 10% accent color. But if this setting is toggled it can make it use more than 30%") - ) - } - .collectAsState() - } - - - val fontScale = - with(preferences) { - preferences[GlobalKeys.FONT_SCALE] - .map { - Preference( - value = it, - title = Text("Font Scale"), - summery = Text("Zoom in or out the text shown on the screen."), - vector = Icons.Outlined.ZoomIn - ) - } - .collectAsState() - } - - - val groupSeparator = - with(preferences) { - preferences[GlobalKeys.GROUP_SEPARATOR].map { - Preference( - value = it, - title = Text("Group Separator"), - summery = Text("Set the number group separator.") - ) - } - .collectAsState() - } - - - fun set(key: Key, value: T) { - viewModelScope.launch { - preferences[key] = value - } - } - - fun set(key: Key1, value: T) { - viewModelScope.launch { - preferences[key] = value - } - } - - fun set(key: Key2, value: O) { - viewModelScope.launch { - preferences[key] = value - } - } - - fun set(key: Key3, value: O) { - viewModelScope.launch { - preferences[key] = value - } - } - - -} - -context (Preferences, ViewModel) - private fun Flow.collectAsState(): State { - - val state = mutableStateOf( - obtain() - ) - onEach { - state.value = it - } - .launchIn(viewModelScope) - return state -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/ui/Home.kt b/app/src/main/java/com/prime/toolz2/ui/Home.kt deleted file mode 100644 index 70e6368..0000000 --- a/app/src/main/java/com/prime/toolz2/ui/Home.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.prime.toolz2.ui - -import androidx.compose.animation.* -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavGraphBuilder -import com.google.accompanist.navigation.animation.AnimatedNavHost -import com.google.accompanist.navigation.animation.composable -import com.google.accompanist.navigation.animation.rememberAnimatedNavController -import com.prime.toolz2.common.compose.LocalNavController -import com.prime.toolz2.common.compose.LocalWindowPadding -import com.prime.toolz2.settings.Settings -import com.prime.toolz2.settings.SettingsViewModel -import com.prime.toolz2.ui.converter.MainGraphRoutes -import com.prime.toolz2.ui.converter.UnitConverter -import com.prime.toolz2.ui.converter.UnitConverterViewModel -import cz.levinzonr.saferoute.core.ProvideRouteSpecArgs -import cz.levinzonr.saferoute.core.RouteSpec - - -@OptIn(ExperimentalAnimationApi::class) -private val DefaultEnterTransition = scaleIn( - initialScale = 0.98f, - animationSpec = tween(220, delayMillis = 90) -) + fadeIn(animationSpec = tween(700)) - -private val DefaultExitTransition = fadeOut(tween(700)) - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun Home(){ - // Currently; supports only 1 Part - // add others in future - // including support for more tools, like direction, prime factorization etc. - // also support for navGraph. - val controller = rememberAnimatedNavController() - - // The padding suggested by the wrapper. - val padding = LocalWindowPadding.current - CompositionLocalProvider( - LocalNavController provides controller - ) { - AnimatedNavHost( - navController = LocalNavController.current, - startDestination = MainGraphRoutes.UnitConverter.route, - enterTransition = { DefaultEnterTransition}, - exitTransition = { DefaultExitTransition }, - popEnterTransition = {DefaultEnterTransition}, - popExitTransition = { DefaultExitTransition }, - modifier = Modifier.navigationBarsPadding().padding(padding), - ) { - composable(MainGraphRoutes.UnitConverter) { - val viewModel = hiltViewModel() - UnitConverter(viewModel = viewModel) - } - - composable(MainGraphRoutes.Settings) { - val viewModel = hiltViewModel() - Settings(viewModel = viewModel) - } - } - } -} - - -///missing fun -@OptIn(ExperimentalAnimationApi::class) -private fun NavGraphBuilder.composable( - spec: RouteSpec<*>, - enterTransition: (AnimatedContentScope.() -> EnterTransition?)? = null, - exitTransition: (AnimatedContentScope.() -> ExitTransition?)? = null, - popEnterTransition: ( - AnimatedContentScope.() -> EnterTransition? - )? = enterTransition, - popExitTransition: ( - AnimatedContentScope.() -> ExitTransition? - )? = exitTransition, - content: @Composable (NavBackStackEntry) -> Unit -) = composable( - spec.route, - spec.navArgs, - spec.deepLinks, - enterTransition = enterTransition, - exitTransition = exitTransition, - popEnterTransition = popEnterTransition, - popExitTransition = popExitTransition -) { - ProvideRouteSpecArgs(spec = spec, entry = it) { - content.invoke(it) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverter.kt b/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverter.kt deleted file mode 100644 index 73f3d9c..0000000 --- a/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverter.kt +++ /dev/null @@ -1,706 +0,0 @@ -package com.prime.toolz2.ui.converter - -import androidx.annotation.DrawableRes -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.* -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.selection.LocalTextSelectionColors -import androidx.compose.foundation.text.selection.TextSelectionColors -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.* -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.* -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.rememberLottieComposition -import com.prime.toolz2.* -import com.prime.toolz2.R -import com.prime.toolz2.common.compose.* -import com.prime.toolz2.core.converter.Unet -import com.prime.toolz2.core.math.NumUtil -import com.prime.toolz2.settings.GlobalKeys -import com.prime.toolz2.settings.SettingsRoute -import com.primex.core.* -import com.primex.preferences.LocalPreferenceStore -import com.primex.ui.ColoredOutlineButton -import com.primex.ui.IconButton -import com.primex.ui.Label -import cz.levinzonr.saferoute.core.annotations.Route -import cz.levinzonr.saferoute.core.annotations.RouteNavGraph -import cz.levinzonr.saferoute.core.navigateTo -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking - -private const val TAG = "UnitConverter" - -@Suppress("FunctionName") -private fun NumberFormatterTransformation(separator: Char = ',') = - VisualTransformation { - val text = it.text - val transformed = run { - // split into respective components. - // maybe remove this and replace with already formatted text. - val (w, f, e) = NumUtil.split(text) - val whole = if (!w.isNullOrBlank()) NumUtil.addThousandSeparators(w, separator) else "" - val fraction = if (f != null) ".$f" else "" - val exponent = if (e != null) "E$e" else "" - whole + fraction + exponent - } - - // FIXME: The offsets are not mapped accurately - // use separator in the transformed text. - TransformedText( - offsetMapping = object : OffsetMapping { - override fun originalToTransformed(offset: Int): Int { - return transformed.length - } - - override fun transformedToOriginal(offset: Int): Int { - return text.length - } - }, - text = AnnotatedString(transformed) - ) - } - -private inline val NumberFormatTransformation: State - @Composable - get() { - //TODO: maybe consider using LocalComposition for preferences. - val preferences = LocalPreferenceStore.current - return with(preferences) { - val initial = remember { - val value = preferences[GlobalKeys.GROUP_SEPARATOR].obtain() - NumberFormatterTransformation(value) - } - produceState(initialValue = initial) { - preferences[GlobalKeys.GROUP_SEPARATOR].map { - NumberFormatterTransformation(it) - }.collect { - value = it - } - } - } - } - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun AppBarTop( - modifier: Modifier = Modifier -) { - val prefs = LocalPreferenceStore.current - val forceColorize by with(prefs) { - prefs[GlobalKeys.FORCE_COLORIZE].observeAsState() - } - Surface( - color = if (forceColorize) Material.colors.primary else Material.colors.overlay, - contentColor = if (forceColorize) Material.colors.onPrimary else Material.colors.onBackground, - modifier = modifier - .fillMaxWidth() - .height(100.dp), - shape = RoundedCornerShape(bottomStartPercent = 70), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = ContentPadding.large, end = ContentPadding.normal) - ) { - - // app icon - // TODO: Replace it with buy option. - IconButton( - onClick = { /*TODO*/ }, - painter = painterResource(id = R.drawable.ic_handyman), - contentDescription = null, - modifier = Modifier - .padding(horizontal = ContentPadding.normal) - .requiredSize(24.dp) - ) - - // title - Text( - text = stringHtmlResource(id = R.string.unit_converter_html), - fontWeight = FontWeight.Light, - modifier = Modifier.weight(1f), - style = Material.typography.h5 - ) - - val manager = LocalBillingManager.current - val activity = LocalContext.current.activity!! - val purchased by manager.isPurchased(id = BillingTokens.DISABLE_ASD_IN_APP_PRODUCT) - if (!purchased) - Surface( - onClick = { - manager.launchBillingFlow( - activity, - BillingTokens.DISABLE_ASD_IN_APP_PRODUCT - ) - }, - color = Color.Transparent, - shape = CircleShape, - content = { - LottieAnimation( - composition = rememberLottieComposition( - spec = LottieCompositionSpec.RawRes(R.raw.remove_ads) - ).value, - modifier = Modifier - .padding(6.dp) - .requiredSize(30.dp), - iterations = Int.MAX_VALUE, - ) - } - ) - - // actions - val controller = LocalNavController.current - IconButton( - imageVector = Icons.Outlined.Settings, - contentDescription = null, - onClick = { - val direction = SettingsRoute() - controller.navigateTo(direction) - } - ) - } - } -} - -@Composable -private fun Tab( - title: Text, - @DrawableRes imageRes: Int, - selected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val color = - if (selected) Material.colors.secondary.copy(ContentAlpha.Indication) else Material.colors.overlay - val contentColor = if (selected) Material.colors.secondary else Material.colors.onBackground - // The color of the Ripple should always the selected color, as we want to show the color - // before the item is considered selected, and hence before the new contentColor is - // provided by TabTransition. - val ripple = rememberRipple(bounded = false) - Column( - modifier = modifier - .clip(Material.shapes.small) - .selectable( - selected = selected, - onClick = onClick, - enabled = true, - role = Role.Tab, - interactionSource = remember(::MutableInteractionSource), - indication = null - ) - .padding(ContentPadding.small) - .width(62.dp) - .wrapContentHeight(), - - horizontalAlignment = Alignment.CenterHorizontally - ) { - Surface( - modifier = Modifier.size(56.dp), - contentColor = contentColor, - color = color, - border = BorderStroke(1.dp, if (selected) contentColor else color), - shape = RoundedCornerShape(30), - content = { - Icon( - painter = painterResource(id = imageRes), - contentDescription = null, - modifier = Modifier.requiredSize(24.dp) - ) - } - ) - - Label( - text = stringResource(res = title), - maxLines = 2, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = ContentPadding.medium), - color = contentColor, - fontSize = 9.sp - ) - } -} - -@Composable -private fun UnitConverterViewModel.Converters( - modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp) -) { - val current = converter - val activity = LocalContext.current.activity!! - LazyRow( - contentPadding = contentPadding, - modifier = modifier, - content = { - items(converters) { item -> - val selected = item == current - Tab( - title = item.title, - imageRes = item.drawableRes, - selected = selected, - onClick = { converter = item; activity.launchReviewFlow() } - ) - } - } - ) -} - -@Composable -@OptIn(ExperimentalMaterialApi::class) -private fun UnitConverterViewModel.ExposedDropdownMenuBox( - isFrom: Boolean, - values: Map>, - modifier: Modifier = Modifier, - field: @Composable () -> Unit, - expanded: Boolean = false, - onDismissRequest: () -> Unit, -) { - - ExposedDropdownMenuBox( - modifier = modifier, - expanded = expanded, - onExpandedChange = { /*expanded = it*/ }, - content = { - // The field of that this menu exposes - field() - - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = onDismissRequest, - modifier = Modifier.exposedDropdownSize(true), - content = { - val container = MaterialTheme.colors.secondaryContainer - val secondary = MaterialTheme.colors.secondary - - values.forEach { (title, list) -> - // list header - Label( - text = title, - fontWeight = FontWeight.SemiBold, - maxLines = 2, - color = secondary, - modifier = Modifier - .padding( - start = ContentPadding.normal, - top = ContentPadding.normal, - end = ContentPadding.normal, - bottom = ContentPadding.medium - ) - .fillMaxWidth() - .drawHorizontalDivider(color = secondary) - .padding(bottom = ContentPadding.medium), - ) - - // emit the list of this title - list.forEach { value -> - val selected = (if (isFrom) fromUnit else toUnit) == value - val color = if (selected) secondary else LocalContentColor.current - - //TODO find a way to support selected. - DropdownMenuItem( - modifier = if (selected) Modifier.background(color = container) else Modifier, - //handle click based on the type - //weather it is from or to. - onClick = { - if (isFrom) - fromUnit = value - else - toUnit = value - - onDismissRequest() - }, - - content = { - // code - Text( - text = stringResource(res = value.code), - style = MaterialTheme.typography.body1, - fontStyle = FontStyle.Italic, - fontWeight = FontWeight.Bold, - color = color - ) - - // title - Text( - text = stringResource(res = value.title), - modifier = Modifier.padding(start = ContentPadding.normal), - style = MaterialTheme.typography.caption, - color = color, - ) - }, - ) - // show divider only when checked. - if (selected) - Divider(color = color, thickness = 2.dp) - } - } - } - ) - } - ) -} - -private val FIELD_MIN_HEIGHT = 80.dp - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun UnitConverterViewModel.ValueField( - visualTransformation: VisualTransformation, - values: Map>, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - //Header - Text( - text = "FROM", - style = MaterialTheme.typography.overline, - modifier = Modifier - .rotate(false), - ) - - var expanded by rememberState(initial = false) - ExposedDropdownMenuBox( - isFrom = true, - values = values, - modifier = Modifier.padding(start = ContentPadding.medium), - expanded = expanded, - onDismissRequest = { expanded = false }, - field = { - OutlinedTextField( - value = TextFieldValue(value, TextRange(value.length)), - onValueChange = { value = it.text }, - readOnly = false, - singleLine = true, - enabled = true, - visualTransformation = visualTransformation, - shape = RoundedCornerShape(percent = 10), - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Decimal), - - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors( - textColor = Material.colors.onBackground, - cursorColor = Color.Transparent, - ), - - label = { - Label(text = stringResource(res = fromUnit.title)) - }, - - trailingIcon = { - val rotate by animateFloatAsState(targetValue = if (expanded) 0f else 180f) - IconButton( - onClick = { expanded = !expanded }, - imageVector = Icons.Outlined.KeyboardArrowUp, - contentDescription = null, - modifier = Modifier.rotate(rotate) - ) - }, - - textStyle = MaterialTheme.typography.h4.copy( - fontWeight = FontWeight.SemiBold - ), - - modifier = Modifier - .fillMaxWidth() - .heightIn(min = FIELD_MIN_HEIGHT), - ) - } - ) - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun UnitConverterViewModel.ResultField( - visualTransformation: VisualTransformation, - values: Map>, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - //Header - Text( - text = "EQUALS TO", - style = MaterialTheme.typography.overline, - modifier = Modifier - .rotate(false), - ) - - var expanded by rememberState(initial = false) - ExposedDropdownMenuBox( - isFrom = false, - expanded = expanded, - values = values, - modifier = Modifier.padding(start = ContentPadding.medium), - onDismissRequest = { expanded = false }, - field = { - TextField( - readOnly = true, - value = result, - onValueChange = { }, - singleLine = true, - visualTransformation = visualTransformation, - shape = RoundedCornerShape(topStartPercent = 10, topEndPercent = 10), - colors = ExposedDropdownMenuDefaults.textFieldColors(), - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), - - - modifier = Modifier - .fillMaxWidth() - .heightIn(min = FIELD_MIN_HEIGHT), - - label = { Label(text = stringResource(res = toUnit.title)) }, - - trailingIcon = { - val rotate by animateFloatAsState(targetValue = if (expanded) 0f else 180f) - IconButton( - onClick = { expanded = !expanded }, - imageVector = Icons.Outlined.KeyboardArrowUp, - contentDescription = null, - modifier = Modifier.rotate(rotate) - ) - }, - textStyle = MaterialTheme.typography.h5.copy( - letterSpacing = 0.35.sp, - fontWeight = FontWeight.SemiBold - ), - ) - } - ) - } -} - - -@Composable -private fun OutlineChip( - text: AnnotatedString, - onRequestCopy: () -> Unit, - modifier: Modifier = Modifier -) { - Surface( - content = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(start = ContentPadding.medium) - .scale(0.85f) - ) { - Text( - text = text, - style = Material.typography.body2, - color = LocalContentColor.current, - fontWeight = FontWeight.SemiBold - ) - IconButton( - onClick = onRequestCopy, - imageVector = Icons.Outlined.FileCopy, - contentDescription = null, - modifier = Modifier - .padding(start = ContentPadding.medium), - tint = LocalContentColor.current - ) - } - }, - border = BorderStroke(1.dp, Material.colors.outline), - contentColor = Material.colors.onBackground.copy(ContentAlpha.medium), - color = Color.Transparent, - modifier = modifier - .height(34.dp) - .wrapContentWidth(), - shape = CircleShape - ) -} - -@Composable -private fun UnitConverterViewModel.AboutEquals( - modifier: Modifier = Modifier, -) { - StaggeredGrid(rows = 2, modifier = modifier) { - more.forEach { (unit, value) -> - val code = stringResource(res = unit) - OutlineChip( - text = buildAnnotatedString { - append(value) - append(" ") - withStyle( - style = SpanStyle(fontStyle = FontStyle.Italic), - block = { - append(code) - } - ) - }, - onRequestCopy = { /*TODO*/ }, - modifier = Modifier.padding( - end = ContentPadding.medium, - bottom = ContentPadding.normal - ) - ) - } - } -} - -@Route(navGraph = RouteNavGraph(start = true)) -@Composable -fun UnitConverter(viewModel: UnitConverterViewModel) { - // Dispose off messenger when out of scope. - // this ensures the viewModel has a instance of channel only when - // device is active and working. - val channel = LocalSnackDataChannel.current - DisposableEffect(key1 = Unit) { - viewModel.channel = channel - onDispose { - viewModel.channel = null - } - } - - with(viewModel) { - - Scaffold( - topBar = { - val prefs = LocalPreferenceStore.current - val colorize by with(prefs) { - prefs[GlobalKeys.COLOR_STATUS_BAR].observeAsState() - } - AppBarTop( - modifier = Modifier.statusBarsPadding2( - color = if (colorize) Material.colors.primaryVariant else Material.colors.overlay, - darkIcons = Material.colors.isLight && !colorize - ) - ) - }, - - content = { - Column(Modifier.padding(it)) { - Converters( - modifier = Modifier.padding(top = ContentPadding.normal), - contentPadding = PaddingValues(horizontal = 24.dp) - ) - CompositionLocalProvider( - LocalTextSelectionColors provides TextSelectionColors( - Color.Transparent, - Color.Transparent - ) - ) { - val visualTransformation by NumberFormatTransformation - val resources = LocalContext.current.resources - val values = remember(converter.uuid) { - converter.units.groupBy { resources.stringResource(it.group) } - } - ValueField( - modifier = Modifier.padding( - top = ContentPadding.normal, - start = 24.dp, - end = 24.dp - ), - visualTransformation = visualTransformation, - values = values - ) - - ResultField( - modifier = Modifier.padding( - top = ContentPadding.normal, - start = 24.dp, - end = 24.dp - ), - visualTransformation = visualTransformation, - values = values - ) - } - - // copy - TextButton( - onClick = { runBlocking { channel.send("Coming soon!") } }, - modifier = Modifier - .align(Alignment.End) - .padding(end = 24.dp, top = ContentPadding.medium), - content = { - Icon( - imageVector = Icons.Outlined.FileCopy, - contentDescription = null - ) - - Label( - text = "COPY", - modifier = Modifier.padding( - start = ContentPadding.medium - ) - ) - } - ) - - Text( - text = "About Equals", - style = MaterialTheme.typography.h4, - modifier = Modifier - .padding(start = ContentPadding.large, top = ContentPadding.small) - .align(Alignment.Start), - fontWeight = FontWeight.Light, - ) - - AboutEquals( - modifier = Modifier - .horizontalScroll(state = rememberScrollState()) - .wrapContentHeight(Alignment.Top) - .padding(horizontal = 28.dp, vertical = ContentPadding.medium), - ) - - Spacer(modifier = Modifier.weight(1f)) - - ColoredOutlineButton( - onClick = { swap(); }, - shape = CircleShape, - - modifier = Modifier - // add padding so that - .padding(vertical = ContentPadding.normal) - // at least the height of the TopAppBar - .height(48.dp) - // width around 70% of screen - .fillMaxWidth(0.7f) - // align centre of column - .align(Alignment.CenterHorizontally), - - content = { - Icon( - imageVector = Icons.Outlined.SwapVerticalCircle, - contentDescription = null, - modifier = Modifier.padding(end = ContentPadding.medium) - ) - Label(text = "SWAP") - }, - border = BorderStroke(1.dp, Material.colors.primary) - ) - } - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverterViewModel.kt b/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverterViewModel.kt deleted file mode 100644 index ff7fbb0..0000000 --- a/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverterViewModel.kt +++ /dev/null @@ -1,282 +0,0 @@ -package com.prime.toolz2.ui.converter - -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.prime.toolz2.common.compose.SnackDataChannel -import com.prime.toolz2.common.compose.send -import com.prime.toolz2.core.converter.UnitConverter -import com.prime.toolz2.core.math.NumUtil -import com.prime.toolz2.core.math.UnifiedReal -import com.primex.core.Text -import com.primex.preferences.Preferences -import com.primex.preferences.stringPreferenceKey -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import java.text.DecimalFormat -import javax.inject.Inject - - -private const val DEBOUNCE_TIMEOUT = 15L - -private const val DEFAULT_VALUE = "0" - -private const val TAG = "UnitConverterViewModel" - -private val KEY_CONVERTER = stringPreferenceKey(TAG + "_converter") -private val KEY_UNIT_FROM = stringPreferenceKey(TAG + "_unit_from") -private val KEY_UNIT_TO = stringPreferenceKey(TAG + "_unit_to") -private val KEY_VALUE = stringPreferenceKey(TAG + "_converter_value") - -private const val MAX_ALLOWED_CHARS = 12 - -@OptIn(FlowPreview::class) -@HiltViewModel -class UnitConverterViewModel @Inject constructor( - private val preferences: Preferences, -) : ViewModel() { - - /** - * The version of this [ViewModel]. - * Update to this triggers calculation. - */ - private val version = MutableStateFlow(0) - - /** - * The messenger used to show messages on the UI. - */ - @JvmField - var channel: SnackDataChannel? = null - - /** - * The unit converter. - */ - private val engine = UnitConverter() - - /** - * The all of converters supported by [engine] - */ - val converters = engine.converters - - /** - * The converter. - */ - private val _converter = mutableStateOf( - with(preferences) { - // some required blocking calls - val uuid = get(KEY_CONVERTER).obtain() ?: engine.converter.uuid - val selected = converters.find { it.uuid == uuid }!! - - // set converter to engine - engine.converter = selected - - // return the selected converter. - selected - } - ) - var converter - get() = _converter.value - set(value) { - // update the representation of the converter - _converter.value = value - // update the converter of the engine. - engine.converter = value - //update uuiD saved - val uuid = value.uuid - preferences[KEY_CONVERTER] = uuid - // as converter changed - // obviously unit from and to too changed. - fromUnit = engine.from - toUnit = engine.to - - // update version to trigger calculation - version.value += 1 - } - - /** - * The unit from. - */ - private val _fromUnit = mutableStateOf( - with(preferences) { - val uuid = get(KEY_UNIT_FROM).obtain() ?: engine.from.uuid - val units = _converter.value.units - - // obtain selected unit. - val selected = units.find { it.uuid == uuid }!! - - // set selected from unit to engine - engine.from = selected - - // return selected unit - selected - } - ) - - var fromUnit - get() = _fromUnit.value - set(value) { - viewModelScope.launch { - // units of the converter - engine.from = value - _fromUnit.value = value - preferences[KEY_UNIT_FROM] = value.uuid - // update trigger - version.value += 1 - } - } - - /** - * To Unit - */ - private val _toUnit = mutableStateOf( - with(preferences) { - val uuid = get(KEY_UNIT_TO).obtain() ?: engine.to.uuid - val units = _converter.value.units - - // obtain selected unit. - val selected = units.find { it.uuid == uuid }!! - - // set selected to unit to engine - engine.to = selected - - // return selected unit - selected - } - ) - - var toUnit - get() = _toUnit.value - set(value) { - viewModelScope.launch { - // units of the converter - engine.to = value - _toUnit.value = value - preferences[KEY_UNIT_TO] = value.uuid - // update trigger - version.value += 1 - } - } - - /** - * The value to be converted. - * The Max length = [MAX_ALLOWED_CHARS] - */ - private val _value = mutableStateOf( - with(preferences) { - val text = get(KEY_VALUE).obtain() ?: DEFAULT_VALUE - text - } - ) - var value - get() = _value.value - set(value) { - viewModelScope.launch { - // preserve default value. - val modified = when { - value.isBlank() -> DEFAULT_VALUE - // old is default value - value[0] == DEFAULT_VALUE[0] -> value.drop(1) - else -> value - } - - // check for error - // emit without saving - val msg = - when { - modified.length > MAX_ALLOWED_CHARS -> "Max allowed length reached." - // if it is not a valid double. - modified.toDoubleOrNull() == null -> "Provided input is invalid." - else -> null - } - - // if error - // emit message and return - if (msg != null) { - channel?.send(message = msg) - return@launch - } - - // emit value. - _value.value = modified - // update trigger - version.value += 1 - //safe in prefs. - preferences[KEY_VALUE] = modified - } - } - - /** - * The computed result. - */ - private val _result = mutableStateOf(DEFAULT_VALUE) - var result - get() = _result.value - private set(value) { - _result.value = value - } - - /** - * The value of converter in terms of other units. - */ - private val _more = mutableStateOf>(emptyMap()) - var more - get() = _more.value - private set(value) { - _more.value = value - } - - /** - * A convince method to swap the values of the [fromUnit] and [toUnit] - */ - fun swap() { - // from as to - val from = _toUnit.value - val to = _fromUnit.value - - fromUnit = from - toUnit = to - viewModelScope.launch { - channel?.send(message = "Units Swapped!") - } - } - - fun clear() { - value = DEFAULT_VALUE - viewModelScope.launch { - channel?.send(message = "Input cleared") - } - } - - private val formatter = DecimalFormat("###,###.##") - - init { - version - .debounce(DEBOUNCE_TIMEOUT) - .onEach { _ -> - // set value as unified - // TODO: Find which errors might occur - // catch the errors. - // find solution to report to the user. - val value = _value.value - engine.value = UnifiedReal(value) - - // compute and emit result. - val double = engine.convert().doubleValue() - result = NumUtil.doubleToString(double, 12, 2)!! - - // compute in terms of others. - // this might take time, - // use helper threads if possible. - more = engine.mapped(0.1f) - .mapKeys { it.key.code } - .mapValues { formatter.format(it.value.doubleValue()) } - } - .catch { - // FixMe find suitable method to emit the errors. - channel?.send(message = "Oops!! An Unknown error occurred.") - } - .launchIn(viewModelScope) - } -} \ No newline at end of file From 84682c2874018b655b2402fb4b24c9d855af378c Mon Sep 17 00:00:00 2001 From: Zakir Ahmad Sheikh Date: Wed, 31 Aug 2022 22:33:34 +0530 Subject: [PATCH 2/2] * Partially fixed issue with DropDown and Keyboard. --- .gitignore | 1 + .idea/misc.xml | 1 + app/build.gradle | 10 +- app/src/main/java/com/prime/toolz2/App.kt | 36 + .../java/com/prime/toolz2/MainActivity.kt | 334 ++++ app/src/main/java/com/prime/toolz2/Theme.kt | 252 +++ .../com/prime/toolz2/billing/Advertiser.kt | 4 + .../prime/toolz2/billing/BillingManager.kt | 378 ++++ .../com/prime/toolz2/billing/Security.java | 124 ++ .../main/java/com/prime/toolz2/common/Util.kt | 4 + .../prime/toolz2/common/compose/Compose.kt | 105 ++ .../prime/toolz2/common/compose/Snackbar.kt | 172 ++ .../com/prime/toolz2/common/compose/Util.kt | 291 +++ .../prime/toolz2/common/compose/WindowSize.kt | 153 ++ .../com/prime/toolz2/core/converter/Angle.kt | 47 + .../com/prime/toolz2/core/converter/Area.kt | 120 ++ .../prime/toolz2/core/converter/Converter.kt | 227 +++ .../com/prime/toolz2/core/converter/Data.kt | 13 + .../com/prime/toolz2/core/converter/Energy.kt | 88 + .../com/prime/toolz2/core/converter/Length.kt | 148 ++ .../com/prime/toolz2/core/converter/Mass.kt | 158 ++ .../com/prime/toolz2/core/converter/Power.kt | 76 + .../prime/toolz2/core/converter/Pressure.kt | 94 + .../com/prime/toolz2/core/converter/Speed.kt | 104 ++ .../toolz2/core/converter/Temperature.kt | 143 ++ .../com/prime/toolz2/core/converter/Time.kt | 129 ++ .../com/prime/toolz2/core/converter/Volume.kt | 13 + .../toolz2/core/math/BoundedRational.java | 564 ++++++ .../prime/toolz2/core/math/StringUtils.java | 94 + .../prime/toolz2/core/math/UnifiedReal.java | 1287 +++++++++++++ .../java/com/prime/toolz2/core/math/Util.java | 162 ++ .../com/prime/toolz2/core/math/creals/CR.java | 1646 +++++++++++++++++ .../core/math/creals/StringFloatRep.java | 73 + .../core/math/creals/UnaryCRFunction.java | 667 +++++++ .../java/com/prime/toolz2/core/math/more.kt | 207 +++ .../com/prime/toolz2/settings/FontFamily.kt | 21 + .../com/prime/toolz2/settings/GlobalKeys.kt | 87 + .../com/prime/toolz2/settings/NightMode.kt | 49 + .../com/prime/toolz2/settings/Settings.kt | 311 ++++ .../toolz2/settings/SettingsViewModel.kt | 173 ++ app/src/main/java/com/prime/toolz2/ui/Home.kt | 98 + .../toolz2/ui/converter/UnitConverter.kt | 732 ++++++++ .../ui/converter/UnitConverterViewModel.kt | 294 +++ app/src/main/res/drawable/ic_remove_ads.xml | 9 + build.gradle | 2 +- 45 files changed, 9695 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/prime/toolz2/App.kt create mode 100644 app/src/main/java/com/prime/toolz2/MainActivity.kt create mode 100644 app/src/main/java/com/prime/toolz2/Theme.kt create mode 100644 app/src/main/java/com/prime/toolz2/billing/Advertiser.kt create mode 100644 app/src/main/java/com/prime/toolz2/billing/BillingManager.kt create mode 100644 app/src/main/java/com/prime/toolz2/billing/Security.java create mode 100644 app/src/main/java/com/prime/toolz2/common/Util.kt create mode 100644 app/src/main/java/com/prime/toolz2/common/compose/Compose.kt create mode 100644 app/src/main/java/com/prime/toolz2/common/compose/Snackbar.kt create mode 100644 app/src/main/java/com/prime/toolz2/common/compose/Util.kt create mode 100644 app/src/main/java/com/prime/toolz2/common/compose/WindowSize.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Angle.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Area.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Converter.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Data.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Energy.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Length.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Mass.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Power.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Pressure.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Speed.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Temperature.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Time.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/converter/Volume.kt create mode 100644 app/src/main/java/com/prime/toolz2/core/math/BoundedRational.java create mode 100644 app/src/main/java/com/prime/toolz2/core/math/StringUtils.java create mode 100644 app/src/main/java/com/prime/toolz2/core/math/UnifiedReal.java create mode 100644 app/src/main/java/com/prime/toolz2/core/math/Util.java create mode 100644 app/src/main/java/com/prime/toolz2/core/math/creals/CR.java create mode 100644 app/src/main/java/com/prime/toolz2/core/math/creals/StringFloatRep.java create mode 100644 app/src/main/java/com/prime/toolz2/core/math/creals/UnaryCRFunction.java create mode 100644 app/src/main/java/com/prime/toolz2/core/math/more.kt create mode 100644 app/src/main/java/com/prime/toolz2/settings/FontFamily.kt create mode 100644 app/src/main/java/com/prime/toolz2/settings/GlobalKeys.kt create mode 100644 app/src/main/java/com/prime/toolz2/settings/NightMode.kt create mode 100644 app/src/main/java/com/prime/toolz2/settings/Settings.kt create mode 100644 app/src/main/java/com/prime/toolz2/settings/SettingsViewModel.kt create mode 100644 app/src/main/java/com/prime/toolz2/ui/Home.kt create mode 100644 app/src/main/java/com/prime/toolz2/ui/converter/UnitConverter.kt create mode 100644 app/src/main/java/com/prime/toolz2/ui/converter/UnitConverterViewModel.kt create mode 100644 app/src/main/res/drawable/ic_remove_ads.xml diff --git a/.gitignore b/.gitignore index aa724b7..4912720 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ .externalNativeBuild .cxx local.properties +app/src/main/java/com/prime/toolz2/billing/Private.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index c5f5828..c582d8f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,6 +10,7 @@ + diff --git a/app/build.gradle b/app/build.gradle index 7ec4c98..2af7058 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId "com.prime.toolz2" minSdk 21 targetSdk 32 - versionCode 38 - versionName "1.0.0-alpha05" + versionCode 39 + versionName "1.0.0-alpha06" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -62,15 +62,15 @@ android { freeCompilerArgs = [ "-Xopt-in=kotlin.RequiresOptIn", "-Xcontext-receivers", - // "-Xuse-k2", - // "-Xjdk-release=1.8" + "-Xuse-k2", + "-Xjdk-release=1.8" ] } buildFeatures { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.3.0' + kotlinCompilerExtensionVersion '1.2.0-rc01' } packagingOptions { resources { diff --git a/app/src/main/java/com/prime/toolz2/App.kt b/app/src/main/java/com/prime/toolz2/App.kt new file mode 100644 index 0000000..acd421b --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/App.kt @@ -0,0 +1,36 @@ +package com.prime.toolz2 + +import android.app.Application +import androidx.compose.material.MaterialTheme +import dagger.hilt.android.HiltAndroidApp +import android.content.Context +import androidx.compose.runtime.compositionLocalOf +import com.google.android.gms.tasks.Task +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability +import com.primex.preferences.Preferences +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@HiltAndroidApp +class App : Application() + + + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + /** + * Provides the Singleton Implementation of Preferences DataStore. + */ + @Provides + @Singleton + fun preferences(@ApplicationContext context: Context) = Preferences(context) +} diff --git a/app/src/main/java/com/prime/toolz2/MainActivity.kt b/app/src/main/java/com/prime/toolz2/MainActivity.kt new file mode 100644 index 0000000..7cf29df --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/MainActivity.kt @@ -0,0 +1,334 @@ +package com.prime.toolz2 + +import android.animation.ObjectAnimator +import android.app.Activity +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.animation.AnticipateInterpolator +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import androidx.core.animation.doOnEnd +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat +import androidx.lifecycle.lifecycleScope +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.ktx.AppUpdateResult +import com.google.android.play.core.ktx.requestAppUpdateInfo +import com.google.android.play.core.ktx.requestReview +import com.google.android.play.core.ktx.requestUpdateFlow +import com.google.android.play.core.review.ReviewManagerFactory +import com.google.firebase.analytics.FirebaseAnalytics +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import com.prime.toolz2.billing.BillingManager +import com.prime.toolz2.billing.Product +import com.prime.toolz2.common.compose.* +import com.prime.toolz2.settings.GlobalKeys +import com.prime.toolz2.settings.NightMode +import com.prime.toolz2.ui.Home +import com.primex.core.rememberState +import com.primex.preferences.LocalPreferenceStore +import com.primex.preferences.Preferences +import com.primex.preferences.longPreferenceKey +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +private const val TAG = "MainActivity" + +private const val RESULT_CODE_APP_UPDATE = 1000 + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + lateinit var fAnalytics: FirebaseAnalytics + + @Inject + lateinit var preferences: Preferences + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // The app has started from scratch if savedInstanceState is null. + val isColdStart = savedInstanceState == null //why? + // Obtain the FirebaseAnalytics instance. + fAnalytics = Firebase.analytics + // show splash screen + initSplashScreen( + isColdStart + ) + //init + val channel = SnackDataChannel() + if (isColdStart) { + val counter = + with(preferences) { preferences[GlobalKeys.KEY_LAUNCH_COUNTER].obtain() } ?: 0 + // update launch counter if + // cold start. + preferences[GlobalKeys.KEY_LAUNCH_COUNTER] = counter + 1 + // check for updates on startup + // don't report + // check silently + launchUpdateFlow(channel) + // TODO: Try to reconcile if it is any good to ask for reviews here. + // launchReviewFlow() + } + WindowCompat.setDecorFitsSystemWindows(window, false) + // setup billing manager + + val billingManager = + BillingManager( + context = this, + products = arrayOf( + Product.DISABLE_ASD + ) + ) + lifecycle.addObserver(billingManager) + + setContent { + val sWindow = calculateWindowSizeClass(activity = this) + + // observe the change to density + val density = LocalDensity.current + val fontScale by with(preferences) { get(GlobalKeys.FONT_SCALE).observeAsState() } + val modified = Density(density = density.density, fontScale = fontScale) + // The state of the Snackbar + val snackbar = remember(::SnackbarHostState) + // observe the channel + // emit the updates + val resource = LocalContext.current.resources + LaunchedEffect(key1 = channel) { + channel.receiveAsFlow().collect { (label, message, duration, action) -> + // dismantle the given snack and use the corresponding components + val result = snackbar.showSnackbar( + message = resource.stringResource(message).text, + actionLabel = resource.stringResource(label)?.text + ?: resource.getString(R.string.dismiss), + duration = duration + ) + // action based on + when (result) { + SnackbarResult.ActionPerformed -> action?.invoke() + SnackbarResult.Dismissed -> { /*do nothing*/ + } + } + } + } + var windowPadding by rememberState(initial = PaddingValues(0.dp)) + CompositionLocalProvider( + LocalWindowPadding provides windowPadding, + LocalElevationOverlay provides null, + LocalWindowSizeClass provides sWindow, + LocalPreferenceStore provides preferences, + LocalSystemUiController provides rememberSystemUiController(), + LocalDensity provides modified, + LocalSnackDataChannel provides channel, + LocalBillingManager provides billingManager + ) { + Material(isDark = resolveAppThemeState()) { + val state = rememberScaffoldState(snackbarHostState = snackbar) + // scaffold + // FixMe: Re-design the SnackBar Api. + // Introduce: SideBar and Bottom Bar. + Scaffold(scaffoldState = state) { inner -> + windowPadding = inner + Home() + } + } + } + } + } +} + +/** + * Manages SplashScreen + */ +private fun MainActivity.initSplashScreen(isColdStart: Boolean) { + // Install Splash Screen and Play animation when cold start. + installSplashScreen() + .let { splashScreen -> + // Animate entry of content + // if cold start + if (isColdStart) + splashScreen.setOnExitAnimationListener { splashScreenViewProvider -> + val splashScreenView = splashScreenViewProvider.view + // Create your custom animation. + val alpha = ObjectAnimator.ofFloat( + splashScreenView, + View.ALPHA, + 1f, + 0f + ) + alpha.interpolator = AnticipateInterpolator() + alpha.duration = 700L + + // Call SplashScreenView.remove at the end of your custom animation. + alpha.doOnEnd { splashScreenViewProvider.remove() } + + // Run your animation. + alpha.start() + } + } +} + +@Composable +private fun resolveAppThemeState(): Boolean { + val preferences = LocalPreferenceStore.current + val mode by with(preferences) { + preferences[GlobalKeys.NIGHT_MODE].observeAsState() + } + return when (mode) { + NightMode.YES -> true + else -> false + } +} + +private val KEY_LAST_REVIEW_TIME = + longPreferenceKey( + TAG + "_last_review_time" + ) + +private const val MIN_LAUNCH_COUNT = 20 +private val MAX_DAYS_BEFORE_FIRST_REVIEW = TimeUnit.DAYS.toMillis(7) +private val MAX_DAY_AFTER_FIRST_REVIEW = TimeUnit.DAYS.toMillis(10) + +/** + * A convince method for launching an in-app review. + * The review API is guarded by some conditions which are + * * The first review will be asked when launchCount is > [MIN_LAUNCH_COUNT] and daysPassed >=[MAX_DAYS_BEFORE_FIRST_REVIEW] + * * After asking first review then after each [MAX_DAY_AFTER_FIRST_REVIEW] a review dialog will be showed. + * Note: The review should not be asked after every coldBoot. + */ +fun Activity.launchReviewFlow() { + require(this is MainActivity) + val count = + with(preferences) { preferences[GlobalKeys.KEY_LAUNCH_COUNTER].obtain() } ?: 0 + + // the time when lastly asked for review + val lastAskedTime = + with(preferences) { preferences[KEY_LAST_REVIEW_TIME].obtain() } + + val firstInstallTime = + com.primex.core.runCatching(TAG + "_review") { + packageManager.getPackageInfo(packageName, 0).firstInstallTime + } + + val currentTime = System.currentTimeMillis() + + // Only first time we should not ask immediately + // however other than this whenever we do some thing of appreciation. + // we should ask for review. + val askFirstReview = + lastAskedTime == null && + firstInstallTime != null && + count >= MIN_LAUNCH_COUNT && + currentTime - firstInstallTime >= MAX_DAYS_BEFORE_FIRST_REVIEW + + val askNormalOne = + lastAskedTime != null && + count >= MIN_LAUNCH_COUNT && + currentTime - lastAskedTime >= MAX_DAY_AFTER_FIRST_REVIEW + + // The flow has finished. The API does not indicate whether the user + // reviewed or not, or even whether the review dialog was shown. Thus, no + // matter the result, we continue our app flow. + lifecycleScope.launch { + if (askFirstReview || askNormalOne) { + val reviewManager = ReviewManagerFactory.create(this@launchReviewFlow) + com.primex.core.runCatching(TAG) { + // update the last asking + preferences[KEY_LAST_REVIEW_TIME] = System.currentTimeMillis() + val info = reviewManager.requestReview() + reviewManager.launchReviewFlow(this@launchReviewFlow, info) + //host.fAnalytics. + } + } + } +} + +private const val FLEXIBLE_UPDATE_MAX_STALENESS_DAYS = 2 + +/** + * A utility method to check for updates. + * @param channel [SnackDataChannel] to report errors, inform users about the availability of update. + * @param report simple messages. + */ +fun Activity.launchUpdateFlow( + channel: SnackDataChannel, + report: Boolean = false, +) { + require(this is MainActivity) + lifecycleScope.launch { + val manager = AppUpdateManagerFactory.create(this@launchUpdateFlow) + with(manager) { + val result = + kotlin.runCatching { + requestUpdateFlow() + .collect { result -> + when (result) { + AppUpdateResult.NotAvailable -> if (report) + channel.send("The app is already updated to the latest version.") + is AppUpdateResult.InProgress -> { + //FixMe: Publish progress + val state = result.installState + val progress = + state.bytesDownloaded() / (state.totalBytesToDownload() + 0.1) * 100 + Log.i(TAG, "check: $progress") + // currently don't show any message + // future version find ways to show progress. + } + is AppUpdateResult.Downloaded -> { + val info = requestAppUpdateInfo() + //when update first becomes available + //don't force it. + // make it required when staleness days overcome allowed limit + val isFlexible = + (info.clientVersionStalenessDays() ?: -1) <= + FLEXIBLE_UPDATE_MAX_STALENESS_DAYS + + // forcefully update; if it's flexible + if (!isFlexible) + completeUpdate() + else + // ask gracefully + channel.send( + message = "An update has just been downloaded.", + label = "RESTART", + action = this::completeUpdate, + duration = SnackbarDuration.Indefinite + ) + // no message needs to be shown + } + is AppUpdateResult.Available -> { + // if user choose to skip the update handle that case also. + val isFlexible = + (result.updateInfo.clientVersionStalenessDays() ?: -1) <= + FLEXIBLE_UPDATE_MAX_STALENESS_DAYS + if (isFlexible) + result.startFlexibleUpdate( + activity = this@launchUpdateFlow, + RESULT_CODE_APP_UPDATE + ) + else + result.startImmediateUpdate( + activity = this@launchUpdateFlow, + RESULT_CODE_APP_UPDATE + ) + // no message needs to be shown + } + } + } + } + Log.d(TAG, "launchUpdateFlow() returned: $result") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/Theme.kt b/app/src/main/java/com/prime/toolz2/Theme.kt new file mode 100644 index 0000000..c7b9af3 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/Theme.kt @@ -0,0 +1,252 @@ +package com.prime.toolz2 + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.MaterialTheme.colors +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.* +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prime.toolz2.common.compose.LongDurationMills +import com.prime.toolz2.settings.FontFamily +import com.prime.toolz2.settings.GlobalKeys +import com.primex.core.hsl +import com.primex.preferences.LocalPreferenceStore +import com.primex.ui.* +import kotlinx.coroutines.flow.map +import androidx.compose.ui.text.font.FontFamily as AndroidFontFamily + +private const val TAG = "Theme" + +typealias Material = MaterialTheme + +/** + * An Extra font family. + */ +private val ProvidedFontFamily = + AndroidFontFamily( + //light + Font(R.font.lato_light, FontWeight.Light), + //normal + Font(R.font.lato_regular, FontWeight.Normal), + //bold + Font(R.font.lato_bold, FontWeight.Bold), + ) + +/** + * Constructs the typography with the [fontFamily] provided with support for capitalizing. + */ +private fun Typography(fontFamily: AndroidFontFamily): Typography { + return Typography( + defaultFontFamily = fontFamily, + button = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + letterSpacing = 1.25.sp, + // a workaround for capitalizing + fontFeatureSettings = "c2sc, smcp" + ), + overline = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 10.sp, + letterSpacing = 1.5.sp, + // a workaround for capitalizing + fontFeatureSettings = "c2sc, smcp" + ) + ) +} + +/** + * A variant of caption. + */ +private val caption2 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 10.sp, + letterSpacing = 0.4.sp +) + +/** + * A variant of caption + */ +val Typography.caption2 get() = com.prime.toolz2.caption2 + +/** + * The alpha of the container colors. + */ +val MaterialTheme.CONTAINER_COLOR_ALPHA get() = 0.15f + +/** + * checks If [GlobalKeys.FORCE_COLORIZE] + */ +val MaterialTheme.forceColorize + @Composable inline get() = LocalPreferenceStore.current.run { + get(GlobalKeys.FORCE_COLORIZE).observeAsState().value + } + +private val small2 = RoundedCornerShape(8.dp) + +/** + * A variant of MaterialTheme shape with coroner's 8 dp + */ +val Shapes.small2 get() = com.prime.toolz2.small2 + +/** + * returns [primary] if [requires] is met else [elze]. + * @param requires The condition for primary to return. default value is [requiresAccent] + * @param elze The color to return if [requires] is not met The default value is [surface] + */ +@Composable +fun Colors.primary(requires: Boolean = MaterialTheme.forceColorize, elze: Color = colors.surface) = + if (requires) MaterialTheme.colors.primary else elze + +/** + * returns [onPrimary] if [requires] is met else [otherwise]. + * @param requires The condition for onPrimary to return. default value is [requiresAccent] + * @param otherwise The color to return if [requires] is not met The default value is [onSurface] + */ +@Composable +fun Colors.onPrimary( + requires: Boolean = MaterialTheme.forceColorize, + elze: Color = colors.onSurface +) = + if (requires) MaterialTheme.colors.onPrimary else elze + +/** + * @see primary() + */ +@Composable +fun Colors.secondary( + requires: Boolean = MaterialTheme.forceColorize, + elze: Color = colors.surface +) = + if (requires) colors.secondary else elze + +/** + * @see onPrimary() + */ +@Composable +fun Colors.onSecondary( + requires: Boolean = MaterialTheme.forceColorize, + elze: Color = colors.onSurface +) = + if (requires) MaterialTheme.colors.onSecondary else elze + +val Colors.surfaceVariant + @Composable inline get() = colors.surface.hsl(lightness = if (isLight) 0.94f else 0.06f) + +/** + * Primary container is applied to elements needing less emphasis than primary + */ +val Colors.primaryContainer + @Composable inline get() = colors.primary.copy(MaterialTheme.CONTAINER_COLOR_ALPHA) + +/** + * On-primary container is applied to content (icons, text, etc.) that sits on top of primary container + */ +val Colors.onPrimaryContainer @Composable inline get() = colors.primary + +val Colors.secondaryContainer + @Composable inline get() = colors.secondary.copy(MaterialTheme.CONTAINER_COLOR_ALPHA) + +val Colors.onSecondaryContainer @Composable inline get() = colors.secondary + +val Colors.errorContainer + @Composable inline get() = colors.error.copy(MaterialTheme.CONTAINER_COLOR_ALPHA) + +val Colors.onErrorContainer @Composable inline get() = colors.error + +/** + * Observes the coloring [GlobalKeys.COLOR_STATUS_BAR] of status Bar. + */ +val MaterialTheme.colorStatusBar + @Composable inline get() = LocalPreferenceStore.current.run { + get(GlobalKeys.COLOR_STATUS_BAR).observeAsState().value + } + +inline val Colors.overlay + get() = (if (isLight) Color.Black else Color.White).copy(0.04f) + +inline val Colors.outline + get() = (if (isLight) Color.Black else Color.White).copy(0.12f) + +val Colors.onOverlay + @Composable inline get() = + (colors.onBackground).copy(alpha = ContentAlpha.medium) + +val Colors.lightShadowColor + inline get() = + if (isLight) Color.White else Color.White.copy(0.025f) + +val Colors.darkShadowColor + inline get() = + if (isLight) Color(0xFFAEAEC0).copy(0.7f) else Color.Black.copy(0.6f) + + +private val defaultPrimaryColor = Color(0xFF5600E8) +private val defaultSecondaryColor = Color.MetroGreen + +private val defaultThemeShapes = + Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) + ) + +@Composable +fun Material(isDark: Boolean, content: @Composable () -> Unit) { + + val preferences = LocalPreferenceStore.current + + val background by animateColorAsState( + targetValue = if (isDark) Color(0xFF0E0E0F) else Color(0xFFF5F5FA), + animationSpec = tween(AnimationConstants.LongDurationMills) + ) + + val surface by animateColorAsState( + targetValue = if (isDark) Color.TrafficBlack else Color.White, + animationSpec = tween(AnimationConstants.LongDurationMills) + ) + + val primary = defaultPrimaryColor + val secondary = defaultSecondaryColor + + val colors = Colors( + primary = primary, + secondary = secondary, + background = background, + surface = surface, + primaryVariant = primary.blend(Color.Black, 0.2f), + secondaryVariant = secondary.blend(Color.Black, 0.2f), + onPrimary = Color.SignalWhite, + onSurface = if (isDark) Color.SignalWhite else Color.UmbraGrey, + onBackground = if (isDark) Color.SignalWhite else Color.Black, + error = Color.OrientRed, + onSecondary = Color.SignalWhite, + onError = Color.SignalWhite, + isLight = !isDark + ) + + val fontFamily by with(preferences) { + preferences[GlobalKeys.FONT_FAMILY].map { font -> + when (font) { + FontFamily.SYSTEM_DEFAULT -> AndroidFontFamily.Default + FontFamily.PROVIDED -> ProvidedFontFamily + FontFamily.SAN_SERIF -> AndroidFontFamily.SansSerif + FontFamily.SARIF -> AndroidFontFamily.Serif + FontFamily.CURSIVE -> AndroidFontFamily.Cursive + } + }.observeAsState() + } + + MaterialTheme( + colors = colors, + typography = Typography(fontFamily), + shapes = defaultThemeShapes, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/billing/Advertiser.kt b/app/src/main/java/com/prime/toolz2/billing/Advertiser.kt new file mode 100644 index 0000000..1458e79 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/billing/Advertiser.kt @@ -0,0 +1,4 @@ +package com.prime.toolz2.billing + +interface Advertiser { +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/billing/BillingManager.kt b/app/src/main/java/com/prime/toolz2/billing/BillingManager.kt new file mode 100644 index 0000000..656f76d --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/billing/BillingManager.kt @@ -0,0 +1,378 @@ +package com.prime.toolz2.billing + +import android.app.Activity +import android.content.Context +import android.util.Log +import androidx.compose.runtime.* +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.android.billingclient.api.* +import com.prime.toolz2.BuildConfig +import kotlinx.coroutines.* +import java.lang.Long.min + +private const val TAG = "BillingManager" + + +private const val RECONNECT_TIMER_START_MILLISECONDS = 1L * 1000L +private const val RECONNECT_TIMER_MAX_TIME_MILLISECONDS = 1000L * 60L * 15L // 15 minutes +private const val SKU_DETAILS_REQUERY_TIME = 1000L * 60L * 60L * 4L // 4 hours + +/** + * A one stop solution for monetization and Billing. + * @param products The list of in-app product ids. + * @param subscriptions The list of in-app subscription Ids. + * + * @author Zakir Ahmad Sheikh + * @since 18-08-2022 + */ +class BillingManager( + context: Context, + products: Array? = null, + // TODO: Implement in future version of BillingManger + subscriptions: Array? = null, +) : PurchasesUpdatedListener, Advertiser, BillingClientStateListener, DefaultLifecycleObserver { + + private val mBillingClient = + BillingClient.newBuilder(context) + .setListener(this) + .enablePendingPurchases() + .build() + + // how long before the data source tries to reconnect to Google play + private var delayReconnectMills = + RECONNECT_TIMER_START_MILLISECONDS + + private val products = + products?.map { product -> + QueryProductDetailsParams.Product.newBuilder() + .setProductId(product) + .setProductType(BillingClient.ProductType.INAPP) + .build() + } + + private val subscriptions = + products?.map { product -> + QueryProductDetailsParams.Product.newBuilder() + .setProductId(product) + .setProductType(BillingClient.ProductType.SUBS) + .build() + } + + init { + // requires not everything to be null or empty. + require(!products.isNullOrEmpty() || !subscriptions.isNullOrEmpty()) + mBillingClient.startConnection(this) + + } + + /** + * The list of items that the user have purchased. + */ + private val _purchases = + mutableStateOf>(emptyList()) + + /** + * @see _purchases + */ + val purchases: State> get() = _purchases + + /** + * An item is purchased if its state [Purchase.PurchaseState.PURCHASED] && [Purchase.isAcknowledged] + * @param id the product id. + */ + @Composable + fun isPurchased(id: String) = + derivedStateOf { + val purchase = _purchases.value.find { it.products.contains(id) } + purchase != null && purchase.purchaseState == Purchase.PurchaseState.PURCHASED + && if (BuildConfig.DEBUG) true else purchase.isAcknowledged + } + + /** + * The [ProductDetails] mapped with their product Ids. + */ + private val _details = + mutableStateOf>(emptyMap()) + + /** + * @see _details + */ + val details: State> get() = _details + + /** + * A [CoroutineScope] to handle async tasks. + */ + private val billingManagerScope = + CoroutineScope(Dispatchers.IO) + + override fun onPurchasesUpdated( + result: BillingResult, + purchases: MutableList? + ) { + when (result.responseCode) { + BillingClient.BillingResponseCode.OK -> { + if (purchases.isNullOrEmpty()) { + Log.d(TAG, "Null Purchase List Returned from OK response!") + return + } + // process the purchases. + process(purchases) + } + BillingClient.BillingResponseCode.USER_CANCELED -> + Log.i(TAG, "onPurchasesUpdated: User canceled the purchase") + BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> + Log.i(TAG, "onPurchasesUpdated: The user already owns this item") + BillingClient.BillingResponseCode.DEVELOPER_ERROR -> Log.e( + TAG, + "onPurchasesUpdated: Developer error means that Google Play " + + "does not recognize the configuration. If you are just getting started, " + + "make sure you have configured the application correctly in the " + + "Google Play Console. The SKU product ID must match and the APK you " + + "are using must be signed with release keys." + ) + else -> + Log.d( + TAG, "BillingResult [" + result.responseCode + "]: " + result.debugMessage + ) + } + } + + /** + * This is a pretty unusual occurrence. It happens primarily if the Google Play Store + * self-upgrades or is force closed. + */ + override fun onBillingServiceDisconnected() = reconnect() + + override fun onBillingSetupFinished(result: BillingResult) { + if (result.responseCode != BillingClient.BillingResponseCode.OK) { + Log.i(TAG, result.debugMessage) + reconnect() + return + } + // The billing client is ready. You can query purchases here. + // This doesn't mean that your app is set up correctly in the console -- it just + // means that you have a connection to the Billing service. + billingManagerScope + .launch { + val purchases = async { query(BillingClient.ProductType.INAPP) } + val response = purchases.await() + // process them. + process(response) + _purchases.value = response + + // details + val details = async { query(products) } + _details.value = details.await().associateBy { it.productId } + } + } + + /** + * It's recommended to requery purchases during onResume. + */ + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + Log.d(TAG, "ON_RESUME") + // this just avoids an extra purchase refresh after we finish a billing flow + if (mBillingClient.isReady) { + billingManagerScope.launch { + // refresh. + val purchases = query(BillingClient.ProductType.INAPP) + process(purchases) + _purchases.value = purchases + } + } + } + + override fun onDestroy(owner: LifecycleOwner) { + Log.i(TAG, "Terminating connection") + mBillingClient.endConnection() + billingManagerScope.cancel("destroying BillingManager") + super.onDestroy(owner) + } + + + /** + * Simple Utility function does is process the [purchases] and acknowledges them + */ + private fun process(purchases: List) { + billingManagerScope.launch { + for (purchase in purchases) { + // Global check to make sure all purchases are signed correctly. + // This check is best performed on your server. + val state = purchase.purchaseState + if (state == Purchase.PurchaseState.PURCHASED) { + if (!isSignatureValid(purchase)) { + Log.e(TAG, "Invalid signature. Make sure your public key is correct.") + continue + } + } + if (!purchase.isAcknowledged) { + acknowledge(purchase) + } + } + } + } + + /** + * A simple query method. + * *Note - Handles all error cases.* + * @param : The list of products to query. + * @return empty in case error else the products. + */ + private suspend fun query( + products: List? + ): List { + + //leave early if null or empty + Log.i(TAG, "query: ") + if (products.isNullOrEmpty()) { + Log.i(TAG, "query: Null $products id list passed.") + return emptyList() + } + + // construct params + val prams = + QueryProductDetailsParams + .newBuilder() + .setProductList(products) + .build() + + val (result, list) = mBillingClient.queryProductDetails(prams) + if (result.responseCode != BillingClient.BillingResponseCode.OK) { + Log.i(TAG, "query: ${result}}") + return emptyList() + } + if (list.isNullOrEmpty()) { + Log.e( + TAG, + "onProductDetailsResponse: " + + "Found null or empty ProductDetails. " + + "Check to see if the Products you requested are correctly " + + "published in the Google Play Console." + ) + return emptyList() + } + return list + } + + /** + * A convince query method. It just returns [_purchases] and handles error cases and nothing more. + * @param type the type of purchases to fetch E.g., [BillingClient.ProductType.INAPP]] + * @return empty list in case of error else purchases. + */ + private suspend fun query( + type: String = BillingClient.ProductType.INAPP + ): List { + if (!mBillingClient.isReady) { + Log.e(TAG, "queryPurchases: BillingClient is not ready") + reconnect() + } + + val params = + QueryPurchasesParams + .newBuilder() + .setProductType(type) + .build() + + val response = mBillingClient.queryPurchasesAsync(params = params) + val result = response.billingResult + + when (result.responseCode) { + BillingClient.BillingResponseCode.OK -> { + if (response.purchasesList.isEmpty()) { + Log.e(TAG, "Null|Empty Purchase List Returned from OK response!") + return emptyList() + } + return response.purchasesList + } + else -> Log.d(TAG, "query [" + result.responseCode + "]: " + result.debugMessage) + } + return emptyList() + } + + + /** + * Ideally your implementation will comprise a secure server, rendering this check + * unnecessary. @see [Security] + */ + private fun isSignatureValid(purchase: Purchase): Boolean = + Security.verifyPurchase(purchase.originalJson, purchase.signature) + + /** + * Acknowledges the [purchase] in case it is not. + * @param purchase the purchase to acknowledge. + * @return true if acknowledged else false. Note if [purchase] is already acknowledged it will return false. + */ + private suspend fun acknowledge( + purchase: Purchase + ): Boolean { + // don't acknowledge in debug app. + if (BuildConfig.DEBUG) return true + if (purchase.isAcknowledged) return false + val params = + AcknowledgePurchaseParams + .newBuilder() + .setPurchaseToken(purchase.purchaseToken) + .build() + val result = mBillingClient.acknowledgePurchase(params) + Log.i(TAG, "acknowledge: $result") + return result.responseCode == BillingClient.BillingResponseCode.OK + && purchase.purchaseState == Purchase.PurchaseState.PURCHASED + + } + + /** + * Retries the billing service connection with exponential backoff, maxing out at the time + * specified by [RECONNECT_TIMER_MAX_TIME_MILLISECONDS]. + */ + private fun reconnect() { + billingManagerScope.launch(Dispatchers.Main) { + val delay = delayReconnectMills + //next delay + delayReconnectMills = + min(delayReconnectMills * 2, RECONNECT_TIMER_MAX_TIME_MILLISECONDS) + delay(delay) + mBillingClient.startConnection(this@BillingManager) + } + } + + /** + * Launch the billing flow. This will launch an external Activity for a result, so it requires + * an Activity reference. For subscriptions, it supports upgrading from one SKU type to another + * by passing in SKUs to be upgraded. + * + * @param activity active activity to launch our billing flow from + * @param sku SKU (Product ID) to be purchased + * @param upgradeSkusVarargs SKUs that the subscription can be upgraded from + * @return true if launch is successful + */ + fun launchBillingFlow( + host: Activity, + product: String, + vararg upgradeSkusVarargs: String + ): Boolean { + val details = _details.value[product] ?: return false + val params = BillingFlowParams + .newBuilder() + .setProductDetailsParamsList( + listOf( + BillingFlowParams + .ProductDetailsParams + .newBuilder() + .setProductDetails(details) + .build() + ) + ) + .build() + // val upgradeSkus = arrayOf(*upgradeSkusVarargs) + val result = mBillingClient.launchBillingFlow(host, params) + return when (result.responseCode) { + BillingClient.BillingResponseCode.OK -> true + else -> { + Log.e(TAG, "Billing failed: + " + result.debugMessage) + false + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/billing/Security.java b/app/src/main/java/com/prime/toolz2/billing/Security.java new file mode 100644 index 0000000..71f5dcb --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/billing/Security.java @@ -0,0 +1,124 @@ +package com.prime.toolz2.billing; + +/* + * This class is an sample of how you can check to make sure your purchases on the device came from + * Google Play. Putting code like this on your server will provide additional protection. + *

+ * One thing that you may also wish to consider doing is caching purchase IDs to make replay attacks + * harder. The reason this code isn't just part of the library is to allow you to customize it (and + * rename it!) to make generic patching exploits more difficult. + */ + +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +/** + * Security-related methods. For a secure implementation, all of this code should be implemented on + * a server that communicates with the application on the device. + */ +class Security { + static final private String TAG = "IABUtil/Security"; + static final private String KEY_FACTORY_ALGORITHM = "RSA"; + static final private String SIGNATURE_ALGORITHM = "SHA1withRSA"; + + /** + * BASE_64_ENCODED_PUBLIC_KEY should be YOUR APPLICATION PUBLIC KEY. You currently get this + * from the Google Play developer console under the "Monetization Setup" category in the + * Licensing area. This build has been setup so that if you define base64EncodedPublicKey in + * your local.properties, it will be echoed into BuildConfig. + */ + + final private static String BASE_64_ENCODED_PUBLIC_KEY = Product.PUBLIC_KEY; + + /** + * Verifies that the data was signed with the given signature + * + * @param signedData the signed JSON string (signed, not encrypted) + * @param signature the signature for the data, signed with the private key + */ + static public boolean verifyPurchase(String signedData, String signature) { + if ((TextUtils.isEmpty(signedData) || TextUtils.isEmpty(BASE_64_ENCODED_PUBLIC_KEY) + || TextUtils.isEmpty(signature)) + ) { + Log.w(TAG, "Purchase verification failed: missing data."); + return false; + } + try { + PublicKey key = generatePublicKey(BASE_64_ENCODED_PUBLIC_KEY); + return verify(key, signedData, signature); + } catch (IOException e) { + Log.e(TAG, "Error generating PublicKey from encoded key: " + e.getMessage()); + return false; + } + } + + /** + * Generates a PublicKey instance from a string containing the Base64-encoded public key. + * + * @param encodedPublicKey Base64-encoded public key + * @throws IOException if encoding algorithm is not supported or key specification + * is invalid + */ + static private PublicKey generatePublicKey(String encodedPublicKey) throws IOException { + try { + byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); + } catch (NoSuchAlgorithmException e) { + // "RSA" is guaranteed to be available. + throw new RuntimeException(e); + } catch (InvalidKeySpecException e) { + String msg = "Invalid key specification: " + e; + Log.w(TAG, msg); + throw new IOException(msg); + } + } + + /** + * Verifies that the signature from the server matches the computed signature on the data. + * Returns true if the data is correctly signed. + * + * @param publicKey public key associated with the developer account + * @param signedData signed data from server + * @param signature server signature + * @return true if the data and signature match + */ + static private Boolean verify(PublicKey publicKey, String signedData, String signature) { + byte[] signatureBytes; + try { + signatureBytes = Base64.decode(signature, Base64.DEFAULT); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Base64 decoding failed."); + return false; + } + try { + Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM); + signatureAlgorithm.initVerify(publicKey); + signatureAlgorithm.update(signedData.getBytes()); + if (!signatureAlgorithm.verify(signatureBytes)) { + Log.w(TAG, "Signature verification failed..."); + return false; + } + return true; + } catch (NoSuchAlgorithmException e) { + // "RSA" is guaranteed to be available. + throw new RuntimeException(e); + } catch (InvalidKeyException e) { + Log.e(TAG, "Invalid key specification."); + } catch (SignatureException e) { + Log.e(TAG, "Signature exception."); + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/common/Util.kt b/app/src/main/java/com/prime/toolz2/common/Util.kt new file mode 100644 index 0000000..4c9c02a --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/common/Util.kt @@ -0,0 +1,4 @@ +package com.prime.toolz2.common + +private const val TAG = "Util" + diff --git a/app/src/main/java/com/prime/toolz2/common/compose/Compose.kt b/app/src/main/java/com/prime/toolz2/common/compose/Compose.kt new file mode 100644 index 0000000..0a07a1c --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/common/compose/Compose.kt @@ -0,0 +1,105 @@ +package com.prime.toolz2.common.compose + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.primex.ui.Label + +@NonRestartableComposable +@Composable +fun Dialog( + expanded: Boolean, + onDismissRequest: () -> Unit, + properties: DialogProperties = DialogProperties(), + content: @Composable () -> Unit +) { + if (expanded) + androidx.compose.ui.window.Dialog( + onDismissRequest = onDismissRequest, + properties = properties, + content = content + ) +} + +@NonRestartableComposable +@Composable +fun TextButton( + label: AnnotatedString, + onClick: () -> Unit, + leading: Painter? = null, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + elevation: ButtonElevation? = null, + shape: Shape = MaterialTheme.shapes.small, + border: BorderStroke? = null, + colors: ButtonColors = ButtonDefaults.textButtonColors(), + contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, +) { + TextButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + interactionSource = interactionSource, + elevation = elevation, + shape = shape, + border = border, + colors = colors, + contentPadding = contentPadding, + ) { + if (leading != null) + Icon( + painter = leading, contentDescription = null, modifier = Modifier.padding( + end = ContentPadding.medium + ) + ) + Label(text = label) + } +} + +@NonRestartableComposable +@Composable +fun TextButton( + label: String, + onClick: () -> Unit, + leading: Painter? = null, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + elevation: ButtonElevation? = null, + shape: Shape = MaterialTheme.shapes.small, + border: BorderStroke? = null, + colors: ButtonColors = ButtonDefaults.textButtonColors(), + contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, +){ + TextButton( + label = AnnotatedString(label), + leading = leading, + onClick = onClick, + modifier = modifier, + enabled = enabled, + interactionSource = interactionSource, + elevation = elevation, + shape = shape, + border = border, + colors = colors, + contentPadding = contentPadding, + ) + + +} + diff --git a/app/src/main/java/com/prime/toolz2/common/compose/Snackbar.kt b/app/src/main/java/com/prime/toolz2/common/compose/Snackbar.kt new file mode 100644 index 0000000..3650cf0 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/common/compose/Snackbar.kt @@ -0,0 +1,172 @@ +package com.prime.toolz2.common.compose + +import androidx.annotation.StringRes +import androidx.compose.material.SnackbarDuration +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.core.content.res.ResourcesCompat +import com.primex.core.Result +import com.primex.core.Text +import com.primex.core.buildResult +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel + +sealed interface Snack { + + val action: (() -> Unit)? + + val duration: SnackbarDuration + + val label: Text? + + val message: Text + + operator fun component1(): Text? = label + + operator fun component2(): Text = message + + operator fun component3(): SnackbarDuration = duration + + operator fun component4(): (() -> Unit)? = action +} + + +private class SnackImpl( + override val action: (() -> Unit)?, + override val duration: SnackbarDuration, + override val label: Text?, + override val message: Text +) : Snack { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SnackImpl + + if (duration != other.duration) return false + if (label != other.label) return false + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + var result = action?.hashCode() ?: 0 + result = 31 * result + duration.hashCode() + result = 31 * result + label.hashCode() + result = 31 * result + message.hashCode() + return result + } + + override fun toString(): String { + return "SnackImpl(duration=$duration, label=$label, message=$message)" + } +} + +/** + * Construct a Snack from the provided string [label], and [message] + */ +fun Snack( + message: String, + duration: SnackbarDuration = SnackbarDuration.Short, + label: String? = null, + action: (() -> Unit)? = null +): Snack = + SnackImpl( + action = action, + duration = duration, + label = if (label == null) null else Text(label), + message = Text(message) + ) + + +/** + * Construct a Snack from the provided resource [label], and [message] + */ +fun Snack( + @StringRes message: Int, + duration: SnackbarDuration = SnackbarDuration.Short, + @StringRes label: Int? = null, + action: (() -> Unit)? = null, +): Snack = + SnackImpl( + action = action, + duration = duration, + label = if (label == null) null else Text(label), + message = Text(message) + ) + + +typealias SnackDataChannel = Channel + +fun SnackDataChannel( + capacity: Int = Channel.RENDEZVOUS, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, + onUndeliveredElement: ((Snack) -> Unit)? = null +): SnackDataChannel = + Channel( + capacity, + onBufferOverflow, + onUndeliveredElement + ) + +suspend fun SnackDataChannel.send( + message: String, + duration: SnackbarDuration = SnackbarDuration.Short, + label: String? = null, + action: (() -> Unit)? = null +) = + send( + Snack( + label = label, + message = message, + action = action, + duration = duration + ) + ) + +suspend fun SnackDataChannel.send( + @StringRes message: Int, + duration: SnackbarDuration = SnackbarDuration.Short, + @StringRes label: Int? = null, + action: (() -> Unit)? = null, +) = + send( + Snack( + label = label, + message = message, + action = action, + duration = duration + ) + ) + +suspend fun SnackDataChannel.send( + @StringRes message: Int, + vararg formatArgs: Any, + duration: SnackbarDuration = SnackbarDuration.Short, + @StringRes label: Int? = null, + action: (() -> Unit)? = null, +) = + send( + SnackImpl( + action = action, + duration = duration, + label = if (label == null) null else Text(label), + message = Text(message, formatArgs = formatArgs) + ) + ) + + +suspend fun SnackDataChannel.send( + message: Text, + duration: SnackbarDuration = SnackbarDuration.Short, + label: Text? = null, + action: (() -> Unit)? = null, +) = send( + SnackImpl(action = action, duration = duration, label = label, message = message) +) + + +val LocalSnackDataChannel = staticCompositionLocalOf { + error("no local messenger provided!!") +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/common/compose/Util.kt b/app/src/main/java/com/prime/toolz2/common/compose/Util.kt new file mode 100644 index 0000000..3dbc8a1 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/common/compose/Util.kt @@ -0,0 +1,291 @@ +package com.prime.toolz2.common.compose + +import android.app.Activity +import android.content.res.Resources +import androidx.compose.animation.core.AnimationConstants +import androidx.compose.foundation.layout.* +import androidx.compose.material.ContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.isUnspecified +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import com.google.accompanist.systemuicontroller.SystemUiController +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability +import com.google.android.play.core.ktx.installStatus +import com.google.android.play.core.review.ReviewManager +import com.prime.toolz2.billing.BillingManager +import com.primex.core.Text +import com.primex.core.resolve +import com.primex.core.runCatching +import com.primex.core.spannedResource + +/** + * A Utility extension function for managing status bar UI. + * + * @param color: The background color of the statusBar. if [Color.Unspecified] the status bar will + * be painted by primaryVariant. + * @param darkIcons: same as name suggests works in collaboration with color. if it is unspecified; uses + * light icons as we will use primaryVariant as background. + */ +fun Modifier.statusBarsPadding2( + color: Color = Color.Unspecified, + darkIcons: Boolean = false, +) = composed { + val controller = LocalSystemUiController.current + + // invoke but control only icons not color. + SideEffect { + controller.setStatusBarColor( + //INFO we are not going to change the background of the statusBar here. + // Reasons are. + // * It adds a delay and the change becomes ugly. + // * animation to color can't be added. + Color.Transparent, + + // dark icons only when requested by user and color is unSpecified. + // because we are going to paint status bar with primaryVariant if unspecified. + darkIcons && !color.isUnspecified + ) + } + + val paint = color.takeOrElse { MaterialTheme.colors.primaryVariant } + // add padding + + val height = with(LocalDensity.current) { + WindowInsets.statusBars.getTop(this).toFloat() + } + + // add background + Modifier + .drawWithContent { + drawContent() + drawRect(paint, size = size.copy(height = height)) + } + .then(this@composed) + .statusBarsPadding() +} + +val LocalSystemUiController = staticCompositionLocalOf { + error("No ui controller defined!!") +} + +// Nav Host Controller +val LocalNavController = staticCompositionLocalOf { + error("no local nav host controller found") +} + +/** + * The content padding for the screen under current [NavGraph] + */ +val LocalWindowPadding = compositionLocalOf { + PaddingValues(0.dp) +} + +val NavHostController.current + @Composable + get() = currentBackStackEntryAsState().value?.destination?.route + +// Setup animation related default things + +typealias Anim = AnimationConstants + +private const val LONG_DURATION_TIME = 500 + +/** + * 500 Mills + */ +val Anim.LongDurationMills get() = LONG_DURATION_TIME + +private const val MEDIUM_DURATION_TIME = 400 + +/** + * 400 Mills + */ +val Anim.MediumDurationMills get() = MEDIUM_DURATION_TIME + +private const val SHORT_DURATION_TIME = 200 + +/** + * 200 Mills + */ +val Anim.ShortDurationMills get() = SHORT_DURATION_TIME + +private const val ACTIVITY_SHORT_DURATION = 150 + +/** + * 150 Mills + */ +val Anim.ActivityShortDurationMills get() = ACTIVITY_SHORT_DURATION + +private const val ACTIVITY_LONG_DURATION = 220 + +/** + * 220 Mills + */ +val Anim.ActivityLongDurationMills get() = ACTIVITY_LONG_DURATION + +object ContentPadding { + /** + * A small 4 [Dp] Padding + */ + val small: Dp = 4.dp + + /** + * A Medium 8 [Dp] Padding + */ + val medium: Dp = 8.dp + + /** + * Normal 16 [Dp] Padding + */ + val normal: Dp = 16.dp + + /** + * Large 32 [Dp] Padding + */ + val large: Dp = 32.dp +} + +/** + * The Standard Elevation Values. + */ +object ContentElevation { + /** + * Zero Elevation. + */ + val none = 0.dp + + /** + * Elevation of 6 [Dp] + */ + val low = 6.dp + + /** + * Elevation of 12 [Dp] + */ + val medium = 12.dp + + /** + * Elevation of 20 [Dp] + */ + val high = 20.dp + + /** + * Elevation of 30 [Dp] + */ + val xHigh = 30.dp +} + +/** + * The recommended divider Alpha + */ +val ContentAlpha.Divider get() = com.prime.toolz2.common.compose.Divider +private const val Divider = 0.12f + + +/** + * The recommended LocalIndication Alpha + */ +val ContentAlpha.Indication get() = com.prime.toolz2.common.compose.Indication +private const val Indication = 0.1f + +@Composable +@ReadOnlyComposable +@NonRestartableComposable +inline fun stringResource(res: Text) = + spannedResource(value = res) + + +inline fun Resources.stringResource(res: Text) = resolve(res) + +@JvmName("stringResource1") +inline fun Resources.stringResource(res: Text?) = resolve(res) + +val LocalBillingManager = + compositionLocalOf { + error("No Local BillingClient set. ") + } + +private const val TAG = "Util" + +fun AppUpdateManager.check( + activity: Activity, + resultCode: Int, + report: Boolean = false, + emit: (snack: Snack) -> Unit +) { + // obtain the task for each check + val task = appUpdateInfo + task.addOnSuccessListener { info -> + val availability = info.updateAvailability() + val status = info.installStatus + emit(Snack("$status")) + when (availability) { + UpdateAvailability.UNKNOWN -> emit(Snack("Unknown error occurred!. $availability")) + + UpdateAvailability.UPDATE_NOT_AVAILABLE -> + emit(Snack("App is already updated to latest version.")) + + // UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> + // resume or start new + else -> { + val downloaded = info.bytesDownloaded() + + // in case it is 0 to avoid exception. + val total = info.totalBytesToDownload() + 1 + val progress = downloaded / total * 100 + emit(Snack("Downloaded: $progress")) + // update is available + //FixMe - Add way to manually adjust flexibility. + //val isFlexible = (info.clientVersionStalenessDays ?: -1) <= FLEXIBLE_UPDATE_STALENESS_DAYS + val isFlexible = info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) + val options = if (isFlexible) AppUpdateType.FLEXIBLE else AppUpdateType.IMMEDIATE + emit(Snack("$options")) + runCatching(TAG) { + startUpdateFlowForResult(info, options, activity, resultCode) + } + } + + } + } + task.addOnFailureListener { + emit(Snack("Unknown error occured: ${it.message}")) + } +} + + +fun Modifier.padding( + horizontal: Dp, + top: Dp = 0.dp, + bottom: Dp = 0.dp +) = this.then( + Modifier.padding( + start = horizontal, + end = horizontal, + top = top, + bottom = bottom + ) +) + +/* +fun Modifier.padding( + vertical: Dp, + start: Dp = 0.dp, + end: Dp = 0.dp +) = Modifier.padding( + start = start, + end = end, + top = vertical, + bottom = vertical +)*/ diff --git a/app/src/main/java/com/prime/toolz2/common/compose/WindowSize.kt b/app/src/main/java/com/prime/toolz2/common/compose/WindowSize.kt new file mode 100644 index 0000000..0479973 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/common/compose/WindowSize.kt @@ -0,0 +1,153 @@ +package com.prime.toolz2.common.compose + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.toComposeRect +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.window.layout.WindowMetricsCalculator + +/** + * Height/Width-based window size class. + * + * A window size class represents a breakpoint that can be used to build responsive layouts. Each + * window size class breakpoint represents a majority case for typical device scenarios so your + * layouts will work well on most devices and configurations. + * + * For more details see Window size classes documentation. + */ +enum class WindowSize { + /** + * **Width** - Represents the majority of phones in portrait. + * + * **Height** - Represents the majority of phones in landscape + */ + COMPACT, + + /** + * **Width** - Represents the majority of tablets in portrait and large unfolded inner displays in portrait. + * **Height** - Represents the majority of tablets in landscape and majority of phones in portrait + */ + MEDIUM, + + /** + * **Width** - Represents the majority of tablets in landscape and large unfolded inner displays in landscape. + * **Height** - Represents the majority of tablets in portrait + */ + EXPANDED; +} + + + +/** Calculates the [WindowWidthSizeClass] for a given [width] */ +private fun fromWidth(width: Dp): WindowSize { + require(width >= 0.dp) { "Width must not be negative" } + return when { + width < 600.dp -> WindowSize.COMPACT + width < 840.dp -> WindowSize.MEDIUM + else -> WindowSize.EXPANDED + } +} + +/** Calculates the [WindowHeightSizeClass] for a given [height] */ +private fun fromHeight(height: Dp): WindowSize { + require(height >= 0.dp) { "Height must not be negative" } + return when { + height < 480.dp -> WindowSize.COMPACT + height < 900.dp -> WindowSize.MEDIUM + else -> WindowSize.EXPANDED + } +} + +/** + * Window size classes are a set of opinionated viewport breakpoints to design, develop, and test + * responsive application layouts against. + * For more details check Support different screen sizes documentation. + * + * WindowSizeClass contains a [WindowWidthSizeClass] and [WindowHeightSizeClass], representing the + * window size classes for this window's width and height respectively. + * + * See [calculateWindowSizeClass] to calculate the WindowSizeClass for an Activity's current window + * + * @property widthSizeClass width-based window size class ([WindowWidthSizeClass]) + * @property heightSizeClass height-based window size class ([WindowHeightSizeClass]) + */ +@Immutable +class WindowSizeClass private constructor( + val widthSizeClass: WindowSize, + val heightSizeClass: WindowSize +) { + + companion object { + /** + * Calculates [WindowSizeClass] for a given [size]. Should be used for testing purposes only + * - to calculate a [WindowSizeClass] for the Activity's current window see + * [calculateWindowSizeClass]. + * + * @param size of the window + * @return [WindowSizeClass] corresponding to the given width and height + */ + fun calculateFromSize(size: DpSize): WindowSizeClass { + val windowWidthSizeClass = fromWidth(size.width) + val windowHeightSizeClass = fromHeight(size.height) + return WindowSizeClass(windowWidthSizeClass, windowHeightSizeClass) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as WindowSizeClass + + if (widthSizeClass != other.widthSizeClass) return false + if (heightSizeClass != other.heightSizeClass) return false + + return true + } + + override fun hashCode(): Int { + var result = widthSizeClass.hashCode() + result = 31 * result + heightSizeClass.hashCode() + return result + } + + operator fun component1(): WindowSize = widthSizeClass + + operator fun component2(): WindowSize = heightSizeClass + + override fun toString() = "WindowSizeClass($widthSizeClass, $heightSizeClass)" +} + +/** + * Calculates the window's [WindowSizeClass] for the provided [activity]. + * + * A new [WindowSizeClass] will be returned whenever a configuration change causes the width or + * height of the window to cross a breakpoint, such as when the device is rotated or the window + * is resized. + * + * @sample androidx.compose.material3.windowsizeclass.samples.AndroidWindowSizeClassSample + */ +@Composable +fun calculateWindowSizeClass(activity: Activity): WindowSizeClass { + // Observe view configuration changes and recalculate the size class on each change. We can't + // use Activity#onConfigurationChanged as this will sometimes fail to be called on different + // API levels, hence why this function needs to be @Composable so we can observe the + // ComposeView's configuration changes. + LocalConfiguration.current + val density = LocalDensity.current + val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity) + val size = with(density) { metrics.bounds.toComposeRect().size.toDpSize() } + return WindowSizeClass.calculateFromSize(size) +} + + +val LocalWindowSizeClass = + compositionLocalOf { + error("No Window size available") + } \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Angle.kt b/app/src/main/java/com/prime/toolz2/core/converter/Angle.kt new file mode 100644 index 0000000..5c2c2e3 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Angle.kt @@ -0,0 +1,47 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal + +private const val TAG = "Angle" + +@Suppress("FunctionName") +private fun Degree() = Unit( + TAG + "_degree", + R.string.system_international, + R.string.degrees, + R.string.code_degrees, + UnifiedReal(BoundedRational(1, 1)) +) + +@Suppress("FunctionName") +private fun Radian() = Unit( + TAG + "_radian", + R.string.system_international, + R.string.radian, + R.string.code_radian, + UnifiedReal(BoundedRational(572957795130823L, 10000000000000L)) +) + +@Suppress("FunctionName") +private fun Gradian() = Unit( + TAG + "_gradian", + R.string.system_international, + R.string.gradian, + R.string.code_gradian, + UnifiedReal(BoundedRational(9, 10)) +) + +@Suppress("FunctionName") +fun Angle() = Converter( + "converter_$TAG", + R.string.angle, + R.drawable.ic_angle, + arrayOf( + Degree(), + Radian(), + Gradian() + ) +) + diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Area.kt b/app/src/main/java/com/prime/toolz2/core/converter/Area.kt new file mode 100644 index 0000000..3ef70b5 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Area.kt @@ -0,0 +1,120 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal + +private const val TAG = "Area" + +//Base Unit: Square Metre + +@Suppress("FunctionName") +private fun SquareMillimetre() = Unit( + TAG + "_sq_millimetre", + R.string.system_international, + R.string.sq_millimetres, + R.string.code_sq_millimetres, + UnifiedReal(BoundedRational(1, 1000000)) +) + +@Suppress("FunctionName") +private fun SquareCentimetre() = Unit( + TAG + "_sq_centimetre", + R.string.system_international, + R.string.sq_centimetres, + R.string.code_sq_centimetres, + UnifiedReal(BoundedRational(1, 10000)) +) + + +@Suppress("FunctionName") +private fun SquareMetre() = Unit( + TAG + "_sq_metre", + R.string.system_international, + R.string.sq_metres, + R.string.code_sq_metres, + UnifiedReal(BoundedRational(1, 1)) +) + +@Suppress("FunctionName") +private fun Hectare() = Unit( + TAG + "_sq_hectare", + R.string.system_international, + R.string.hectare, + R.string.code_hectare, + UnifiedReal(BoundedRational(10000, 1)) +) + + +@Suppress("FunctionName") +private fun SqKilometre() = Unit( + TAG + "_sq_kilometre", + R.string.system_international, + R.string.sq_kilometre, + R.string.code_sq_kilometre, + UnifiedReal(BoundedRational(1000000, 1)) +) + +@Suppress("FunctionName") +private fun SqInch() = Unit( + TAG + "_sq_inch", + R.string.imperial_system, + R.string.sq_inche, + R.string.code_sq_inche, + UnifiedReal(BoundedRational(64516, 100000000)) +) + +@Suppress("FunctionName") +private fun SqFoot() = Unit( + TAG + "_sq_foot", + R.string.imperial_system, + R.string.sq_foot, + R.string.code_sq_foot, + UnifiedReal(BoundedRational(92903, 1000000)) +) + +@Suppress("FunctionName") +private fun SqYard() = Unit( + TAG + "_sq_yard", + R.string.imperial_system, + R.string.sq_yard, + R.string.code_sq_yard, + UnifiedReal(BoundedRational(836127, 1000000)) +) + +@Suppress("FunctionName") +private fun Acre() = Unit( + TAG + "_acre", + R.string.imperial_system, + R.string.acre, + R.string.code_acre, + UnifiedReal(BoundedRational(40468564224L, 10000000L)) +) + +@Suppress("FunctionName") +private fun SqMile() = Unit( + TAG + "_sq_mile", + R.string.imperial_system, + R.string.sq_mile, + R.string.code_sq_mile, + UnifiedReal(BoundedRational(2589988110336L, 1000000)) +) + +@Suppress("FunctionName") +fun Area() = Converter( + "converter_$TAG", + R.string.area, + R.drawable.ic_area, + arrayOf( + SquareMillimetre(), + SquareCentimetre(), + SquareMetre(), + Hectare(), + SqKilometre(), + SqInch(), + SqFoot(), + SqYard(), + Acre(), + SqMile() + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Converter.kt b/app/src/main/java/com/prime/toolz2/core/converter/Converter.kt new file mode 100644 index 0000000..3e26313 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Converter.kt @@ -0,0 +1,227 @@ +package com.prime.toolz2.core.converter + + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.prime.toolz2.core.math.UnifiedReal +import com.primex.core.Text + + +//TODO: Replace string resources plurals in futuure versions. +interface Unet { + /** + * The unique Id of this unit in the converter. + */ + val uuid: String + + /** + * The title of the Unit. + */ + val title: Text + + /** + * The short name of the Unit + */ + val code: Text + + /** + * The name resource of the group to which this unit belongs; e.g., SI, BTS etc. + */ + val group: Text + + /** + * The optional iconRes [DrawableRes] of the unit + */ + val icon: Int? + get() = null + + /** + * Consumes value in Unit and returns value in [base] unit + * @param value the value in Unit + */ + suspend fun toBase(value: UnifiedReal): UnifiedReal + + /** + * Consumes value in base and returns value in [Unet] + * + *@param value: The value in base. + */ + suspend fun toUnit(value: UnifiedReal): UnifiedReal +} + + + + +interface Converter { + + /** + * The title of the Unit. + */ + val title: Text + + /** + * The resource icon associated with this converter. + */ + val drawableRes: Int + + /** + * The unique Id to identify this converter. + */ + val uuid: String + + /** + * The list of units supported by this converter. + */ + val units: Array + + /** + * The method to convert [value]from -> to + */ + suspend fun convert(from: Unet, to: Unet, value: UnifiedReal): UnifiedReal { + val inBase = from.toBase(value) + return to.toUnit(inBase) + } +} + +@Suppress("FunctionName") +fun Converter( + uuid: String, + @StringRes title: Int, + @DrawableRes drawableRes: Int, + units: Array, +) = object : Converter { + override val title: Text = Text(title) + override val drawableRes: Int = drawableRes + override val uuid: String = uuid + override val units: Array = units +} + +/** + * Constructs the Unit for the Converter + */ +@Suppress("FunctionName") +fun Unit( + uuid: String, + @StringRes group: Int, + @StringRes title: Int, + @StringRes code: Int, + inBase: UnifiedReal, + @DrawableRes icon: Int? = null +) = + object : Unet { + override val uuid: String = uuid + override val title: Text = Text(title) + override val code: Text = Text(code) + override val group: Text = Text(group) + override val icon: Int? = icon + + override suspend fun toBase(value: UnifiedReal): UnifiedReal { + return value.multiply(inBase) + } + + override suspend fun toUnit(value: UnifiedReal): UnifiedReal { + return value.divide(inBase) + } + } + + + +interface UnitConverter { + + var value: UnifiedReal + + var converter: Converter + + var from: Unet + + var to: Unet + + suspend fun convert(): UnifiedReal = converter.convert(from, to, value) + + /** + * @param pct: A value between 0 and 1 + */ + suspend fun mapped(pct: Float): Map { + val map = HashMap() + + val units = converter.units + // the pct must between 0 and one + // throw error if not + require(pct in 0.0..1.0) + + // the unified real form of the limit + val limit = UnifiedReal("$pct") + + units.forEach { unit -> + if (unit != to && unit != from) { + val result = converter.convert(from, unit, value) + + if (result < limit) + return@forEach // continue. + // only add then to map + //if (value.compareTo(value)) + map += unit to result + } + } + //return the computed result + // TODO: Sort in ascending order of the entries. + // this hack currently works but needs some elegent solution. + return map.toSortedMap { o1, o2 -> + val real1 = map[o1]!! + val real2 = map[o2]!! + real1.compareTo(real2) + } + } + + val converters: Array + + /** + * Returns the default suggested units indices of the [converter] + */ + val default: Pair +} + + +private class UnitConverterImpl : UnitConverter { + + override val converters: Array = + arrayOf( + Length(), + Mass(), + Time(), + Temperature(), + // Data(), + Angle(), + Area(), + // Volume(), + Pressure(), + Energy(), + Power(), + Speed() + ) + + override var value: UnifiedReal = UnifiedReal.ZERO + + override var converter: Converter = converters[0] // length + set(value) { + field = value + val units = field.units + // whenever converter changes switch to defaults. + val def = default + // of the unit. + from = units[def.first]; to = units[def.second] + } + + override var from: Unet = converter.units[0] + override var to: Unet = converter.units[1] + + override val default: Pair + get() = when (converter) { + converters[0] -> 4 to 10 //metre to inch + else -> 0 to 1 //TODO: Implement properly this thing. + } + +} + + +fun UnitConverter(): UnitConverter = UnitConverterImpl() \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Data.kt b/app/src/main/java/com/prime/toolz2/core/converter/Data.kt new file mode 100644 index 0000000..7524fa1 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Data.kt @@ -0,0 +1,13 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R + +private const val TAG = "Data" + +@Suppress("FunctionName") +fun Data() = Converter( + "converter_$TAG", + R.string.data, + R.drawable.ic_sd_card, + arrayOf() +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Energy.kt b/app/src/main/java/com/prime/toolz2/core/converter/Energy.kt new file mode 100644 index 0000000..dff5598 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Energy.kt @@ -0,0 +1,88 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal +import java.math.BigInteger + + +private const val TAG = "Energy" + +//base unit joule + + +@Suppress("FunctionName") +private fun ElectronVolt() = Unit( + TAG + "_electron_volt", + R.string.system_international, + R.string.electron_volt, + R.string.code_electron_volt, + UnifiedReal( + BoundedRational( + BigInteger("1602176565"), BigInteger("10000000000000000000000000000") + ) + ) +) + +@Suppress("FunctionName") +private fun Joule() = Unit( + TAG + "_joule", + R.string.system_international, + R.string.joule, + R.string.code_joule, + UnifiedReal(BoundedRational(1, 1)) +) + +@Suppress("FunctionName") +private fun KiloJoule() = Unit( + TAG + "_kilo_joule", + R.string.system_international, + R.string.kilojoule, + R.string.code_kilojoule, + UnifiedReal(BoundedRational(1000, 1)) +) + +@Suppress("FunctionName") +private fun ThermalCalorie() = Unit( + TAG + "_thermal_calorie", + R.string.system_international, + R.string.thermal_calorie, + R.string.code_thermal_calorie, + UnifiedReal(BoundedRational(4184, 1000)) +) + +@Suppress("FunctionName") +private fun FoodCalorie() = Unit( + TAG + "_food_calorie", + R.string.system_international, + R.string.food_calorie, + R.string.code_food_calorie, + UnifiedReal(BoundedRational(4184, 1)) +) + +@Suppress("FunctionName") +private fun FootPound() = Unit( + TAG + "_foot_pound", + R.string.imperial_system, + R.string.foot_pound, + R.string.code_foot_pound, + UnifiedReal( + BoundedRational(13558179483314003L, 10000000000000000L) + ) +) + + +@Suppress("FunctionName") +fun Energy() = Converter( + "converter_$TAG", + R.string.energy, + R.drawable.ic_energy, + arrayOf( + ElectronVolt(), + Joule(), + KiloJoule(), + ThermalCalorie(), + FoodCalorie(), + FootPound() + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Length.kt b/app/src/main/java/com/prime/toolz2/core/converter/Length.kt new file mode 100644 index 0000000..07e2876 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Length.kt @@ -0,0 +1,148 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal + +private const val TAG = "Length" + +@Suppress("FunctionName") +private fun Nanometre() = Unit( + TAG + "_nanometre", + R.string.system_international, + R.string.nanometre, + R.string.code_nanometre, + UnifiedReal(BoundedRational(1, 1000000000)) +) + +@Suppress("FunctionName") +private fun Micrometre() = Unit( + TAG + "_micrometre", + R.string.system_international, + R.string.micrometre, + R.string.code_micrometre, + UnifiedReal(BoundedRational(1, 1000000)) +) + +@Suppress("FunctionName") +private fun Millimetre() = Unit( + TAG + "_millimetre", + R.string.system_international, + R.string.millimetre, + R.string.code_millimetre, + UnifiedReal(BoundedRational(1, 1000)) +) + +@Suppress("FunctionName") +private fun Centimetre() = Unit( + TAG + "_centimetre", + R.string.system_international, + R.string.centimetre, + R.string.code_centimetre, + UnifiedReal(BoundedRational(1, 100)) +) + +@Suppress("FunctionName") +private fun Metre() = Unit( + TAG + "_metre", + R.string.system_international, + R.string.metre, + R.string.code_metre, + UnifiedReal(1) +) + +@Suppress("FunctionName") +private fun Kilometre() = Unit( + TAG + "_kilometre", + R.string.system_international, + R.string.kilometre, + R.string.code_kilometre, + UnifiedReal(BoundedRational(1000, 1)) +) + +@Suppress("FunctionName") +private fun Mile() = Unit( + TAG + "_mile", + R.string.imperial_system, + R.string.mile, + R.string.code_mile, + UnifiedReal(BoundedRational(1609344, 1000)) +) + +@Suppress("FunctionName") +private fun NauticalMile() = Unit( + TAG + "_nautical_mile", + R.string.imperial_system, + R.string.nautical_mile, + R.string.code_nautical_mile, + UnifiedReal(BoundedRational(1852, 1)) +) + +@Suppress("FunctionName") +private fun Yard() = Unit( + TAG + "_yard", + R.string.imperial_system, + R.string.yard, + R.string.code_yard, + UnifiedReal(BoundedRational(9144, 10000)) +) + +@Suppress("FunctionName") +private fun Foot() = Unit( + TAG + "_foot", + R.string.imperial_system, + R.string.foot, + R.string.code_foot, + UnifiedReal(BoundedRational(3048, 10000)) +) + +@Suppress("FunctionName") +private fun Inch() = Unit( + TAG + "_inch", + R.string.imperial_system, + R.string.inch, + R.string.code_inch, + UnifiedReal(BoundedRational(254, 10000)) +) + + +@Suppress("FunctionName") +private fun Au() = Unit( + TAG + "_astronomical_unit", + R.string.system_international, + R.string.astronomical_unt, + R.string.code_astronomical_unit, + UnifiedReal(BoundedRational(149597870700L, 1)) +) + + +@Suppress("FunctionName") +private fun LightYear() = Unit( + TAG + "_light_year", + R.string.system_international, + R.string.light_year, + R.string.code_light_year, + UnifiedReal(BoundedRational(9460730472580800L, 1)) +) + +@Suppress("FunctionName") +fun Length() = Converter( + uuid = "converter_$TAG", + title = R.string.length, + drawableRes = R.drawable.ic_length, + units = arrayOf( + Nanometre(), + Micrometre(), + Millimetre(), + Centimetre(), + Metre(), + Kilometre(), + Mile(), + NauticalMile(), + Yard(), + Foot(), + Inch(), + Au(), + LightYear() + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Mass.kt b/app/src/main/java/com/prime/toolz2/core/converter/Mass.kt new file mode 100644 index 0000000..3e821c5 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Mass.kt @@ -0,0 +1,158 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal + +private const val TAG = "Mass" + +//Base unit - Kilograms + +@Suppress("FunctionName") +private fun Carat() = Unit( + TAG + "_carat", + R.string.system_international, + R.string.carat, + R.string.code_carat, + UnifiedReal(BoundedRational(2, 10000)) +) + +@Suppress("FunctionName") +private fun MilliGram() = Unit( + TAG + "_milli_gram", + R.string.system_international, + R.string.milligram, + R.string.code_milligram, + UnifiedReal(BoundedRational(1, 1000000)) +) + +@Suppress("FunctionName") +private fun Centigram() = Unit( + TAG + "_centi_gram", + R.string.system_international, + R.string.centigram, + R.string.code_centigram, + UnifiedReal(BoundedRational(1, 100000)) +) + +@Suppress("FunctionName") +private fun Decigram() = Unit( + TAG + "_decigram", + R.string.system_international, + R.string.decigram, + R.string.code_decigram, + UnifiedReal(BoundedRational(1, 10000)) +) + + +@Suppress("FunctionName") +private fun Gram() = Unit( + TAG + "_gram", + R.string.system_international, + R.string.gram, + R.string.code_gram, + UnifiedReal(BoundedRational(1, 1000)) +) + +@Suppress("FunctionName") +private fun Decagram() = Unit( + TAG + "_deca_gram", + R.string.system_international, + R.string.decagram, + R.string.code_decagram, + UnifiedReal(BoundedRational(1, 100)) +) + +@Suppress("FunctionName") +private fun Hectogram() = Unit( + TAG + "_hectogram", + R.string.system_international, + R.string.hectogram, + R.string.code_hectogram, + UnifiedReal(BoundedRational(1, 10)) +) + +@Suppress("FunctionName") +private fun Kilogram() = Unit( + TAG + "_kilo_gram", + R.string.system_international, + R.string.kilogram, + R.string.code_kilogram, + UnifiedReal(BoundedRational(1, 1)) +) + +@Suppress("FunctionName") +private fun MetricTonne() = Unit( + TAG + "_metric_tonne", + R.string.system_international, + R.string.metric_ton, + R.string.code_metric_ton, + UnifiedReal(BoundedRational(1000, 1)) +) + +@Suppress("FunctionName") +private fun Ounce() = Unit( + TAG + "_ounce", + R.string.imperial_system, + R.string.ounce, + R.string.code_ounce, + UnifiedReal(BoundedRational(28349523125L, 1000000000000L)) +) + +@Suppress("FunctionName") +private fun Pound() = Unit( + TAG + "_pound", + R.string.imperial_system, + R.string.pound, + R.string.code_pound, + UnifiedReal(BoundedRational(45359237, 100000000)) +) + +@Suppress("FunctionName") +private fun Stone() = Unit( + TAG + "_stone", + R.string.imperial_system, + R.string.stone, + R.string.code_stone, + UnifiedReal(BoundedRational(635029318, 100000000)) +) + +@Suppress("FunctionName") +private fun ShortTonneUS() = Unit( + TAG + "_short_tonne_us", + R.string.imperial_system_us, + R.string.short_ton, + R.string.code_short_ton, + UnifiedReal(BoundedRational(90718474, 100000)) +) + +@Suppress("FunctionName") +private fun LongTonneUK() = Unit( + TAG + "_long_tonne_uk", + R.string.imperial_system, + R.string.long_ton, + R.string.code_long_ton, + UnifiedReal(BoundedRational(10160469088L, 10000000)) +) + +@Suppress("FunctionName") +fun Mass() = Converter( + "converter_$TAG", + R.string.weight_and_mass, + R.drawable.ic_weight_n_mass, + arrayOf( + Carat(), + MilliGram(), + Centigram(), + Decigram(), + Gram(), + Decagram(), + Hectogram(), + Kilogram(), + MetricTonne(), + Ounce(), + Stone(), + ShortTonneUS(), + LongTonneUK() + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Power.kt b/app/src/main/java/com/prime/toolz2/core/converter/Power.kt new file mode 100644 index 0000000..921f023 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Power.kt @@ -0,0 +1,76 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal + +private const val TAG = "Power" +//basic unit watt + +@Suppress("FunctionName") +private fun Watt() = Unit( + TAG + "_watt", + R.string.system_international, + R.string.watt, + R.string.code_watt, + UnifiedReal(BoundedRational(1, 1)) +) + +@Suppress("FunctionName") +private fun KiloWatt() = Unit( + TAG + "_kilo_watt", + R.string.system_international, + R.string.kilowatt, + R.string.code_kilowatt, + UnifiedReal(BoundedRational(1000, 1)) +) + +@Suppress("FunctionName") +private fun HorsePower() = Unit( + TAG + "_horse_power", + R.string.imperial_system_us, + R.string.horse_power_us, + R.string.code_horse_power_us, + UnifiedReal( + BoundedRational( + 7456998715822702L, + 10000000000000L + ) + ) +) + +@Suppress("FunctionName") +private fun FootPoundsPerMinute() = Unit( + TAG + "_foot_pounds_per_minute", + R.string.imperial_system, + R.string.foot_pounds_per_minute, + R.string.code_foot_pounds_per_minute, + UnifiedReal( + BoundedRational(22596966, 1000000000L) + ) +) + +@Suppress("FunctionName") +private fun BTUPerMinute() = Unit( + TAG + "_btu_per_minute", + R.string.imperial_system, + R.string.british_thermal_units_per_minute, + R.string.code_british_thermal_units_per_minute, + UnifiedReal( + BoundedRational(175842641667L, 10000000000L) + ) +) + +@Suppress("FunctionName") +fun Power() = Converter( + "converter_$TAG", + R.string.power, + R.drawable.ic_power, + arrayOf( + Watt(), + KiloWatt(), + HorsePower(), + FootPoundsPerMinute(), + BTUPerMinute() + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Pressure.kt b/app/src/main/java/com/prime/toolz2/core/converter/Pressure.kt new file mode 100644 index 0000000..64c92ea --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Pressure.kt @@ -0,0 +1,94 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal + +private const val TAG = "Pressure" + +//Basic Unit Pascal +@Suppress("FunctionName") +private fun Atmosphere() = Unit( + TAG + "_atmosphere", + R.string.system_international, + R.string.atmosphere, + R.string.code_atmosphere, + UnifiedReal( + BoundedRational(101325, 1) + ) +) + +@Suppress("FunctionName") +private fun Bar() = Unit( + TAG + "_bar", + R.string.system_international, + R.string.bar, + R.string.code_bar, + UnifiedReal( + BoundedRational(100000, 1) + ) +) + + +@Suppress("FunctionName") +private fun KiloPascal() = Unit( + TAG + "_kilo_pascal", + R.string.system_international, + R.string.kilopascal, + R.string.code_kilopascal, + UnifiedReal( + BoundedRational(1000, 1) + ) +) + +@Suppress("FunctionName") +private fun PoundsPerInch() = Unit( + TAG + "_pounds_per_inch", + R.string.system_international, + R.string.pounds_per_inch, + R.string.code_pounds_per_inch, + UnifiedReal( + BoundedRational( + 689475729316836L, + 100000000000L + ) + ) +) + +@Suppress("FunctionName") +private fun MMsOfMercury() = Unit( + TAG + "_mms_of_mercury", + R.string.system_international, + R.string.mm_of_mercury, + R.string.code_mm_of_mercury, + UnifiedReal( + BoundedRational(133322387415L, 1000000000) + ) +) + + +@Suppress("FunctionName") +private fun Pascal() = Unit( + TAG + "_pascal", + R.string.system_international, + R.string.pascal, + R.string.code_pascal, + UnifiedReal( + BoundedRational(1, 1) + ) +) + +@Suppress("FunctionName") +fun Pressure() = Converter( + "converter_$TAG", + R.string.pressure, + R.drawable.ic_pressure, + arrayOf( + Atmosphere(), + Bar(), + KiloPascal(), + PoundsPerInch(), + MMsOfMercury(), + Pascal() + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Speed.kt b/app/src/main/java/com/prime/toolz2/core/converter/Speed.kt new file mode 100644 index 0000000..73e74e5 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Speed.kt @@ -0,0 +1,104 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal +import javax.crypto.Mac + +private const val TAG = "Speed" +//Base Unit kmph + + +@Suppress("FunctionName") +private fun CMsPerSecond() = Unit( + TAG + "_cms_per_second", + R.string.system_international, + R.string.centimetres_per_second, + R.string.code_centimetres_per_second, + UnifiedReal( + BoundedRational(36, 1000) + ) +) + +@Suppress("FunctionName") +private fun MsPerSecond() = Unit( + TAG + "_Ms_per_second", + R.string.system_international, + R.string.metres_per_second, + R.string.code_metres_per_second, + UnifiedReal( + BoundedRational(36, 10) + ) +) + +@Suppress("FunctionName") +private fun KMsPerHour() = Unit( + TAG + "_KMs_per_hour", + R.string.system_international, + R.string.kilometres_per_hour, + R.string.code_kilometres_per_hour, + UnifiedReal( + BoundedRational(1, 1) + ) +) + + +@Suppress("FunctionName") +private fun FeetPerSecond() = Unit( + TAG + "_feet_per_second", + R.string.imperial_system, + R.string.feet_per_second, + R.string.code_feet_per_second, + UnifiedReal( + BoundedRational(109728, 100000) + ) +) + +@Suppress("FunctionName") +private fun MilesPerHour() = Unit( + TAG + "_miles_per_hour", + R.string.imperial_system, + R.string.miles_per_hour, + R.string.code_miles_per_hour, + UnifiedReal( + BoundedRational(16092, 10000) + ) +) + +@Suppress("FunctionName") +private fun Knot() = Unit( + TAG + "_knot", + R.string.imperial_system, + R.string.knot, + R.string.code_knot, + UnifiedReal( + BoundedRational(185184, 100000) + ) +) + +@Suppress("FunctionName") +private fun Mach() = Unit( + TAG + "_mach", + R.string.imperial_system, + R.string.mach, + R.string.code_mach, + UnifiedReal( + BoundedRational(122508, 100) + ) +) + +@Suppress("FunctionName") +fun Speed() = Converter( + "converter_$TAG", + R.string.speed, + R.drawable.ic_motorcycle, + arrayOf( + CMsPerSecond(), + MsPerSecond(), + KMsPerHour(), + FeetPerSecond(), + MilesPerHour(), + Knot(), + Mach() + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Temperature.kt b/app/src/main/java/com/prime/toolz2/core/converter/Temperature.kt new file mode 100644 index 0000000..24793e0 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Temperature.kt @@ -0,0 +1,143 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal +import com.primex.core.Text + + +private const val TAG = "Temperature" + +@Suppress("FunctionName") +private fun Celsius() = object : Unet { + override val title: Text = Text(R.string.celsius) + override val code: Text = Text(R.string.code_celsius) + override val uuid: String = TAG + "_celsius" + override val group: Text = Text(R.string.system_international) + + override suspend fun toBase(value: UnifiedReal): UnifiedReal = value + + override suspend fun toUnit(value: UnifiedReal): UnifiedReal = value +} + + +@Suppress("FunctionName") +private fun Fahrenheit() = object : Unet { + override val title: Text = Text(R.string.fahrenheit) + override val code: Text = Text(R.string.code_fahrenheit) + override val uuid: String = TAG + "_fahrenheit" + override val group: Text = Text(R.string.united_system_customary_system) + + override suspend fun toBase(value: UnifiedReal): UnifiedReal = + value.subtract(UnifiedReal(32)).multiply(UnifiedReal(BoundedRational(5, 9))) + + override suspend fun toUnit(value: UnifiedReal): UnifiedReal = + value.multiply(UnifiedReal(BoundedRational(9, 5))).add( + UnifiedReal(32) + ) +} + +@Suppress("FunctionName") +private fun Kelvin() = object : Unet { + override val title: Text = Text(R.string.kelvin) + override val code: Text = Text(R.string.code_kelvin) + override val uuid: String = TAG + "_kelvin" + override val group: Text = Text(R.string.system_international) + + override suspend fun toBase(value: UnifiedReal) = + value.subtract(UnifiedReal(BoundedRational(27315, 100))) + + override suspend fun toUnit(value: UnifiedReal) = + value.add(UnifiedReal(BoundedRational(27315, 100))) +} + +@Suppress("FunctionName") +private fun Rankine() = object : Unet { + override val title: Text = Text(R.string.rankine) + override val code: Text = Text(R.string.code_rankine) + override val uuid: String = TAG + "_rankine" + override val group: Text = Text(R.string.imperial_system_us) + + override suspend fun toBase(value: UnifiedReal) = + value.subtract(UnifiedReal(BoundedRational(49167, 100))) + .multiply(UnifiedReal(BoundedRational(5, 9))) + + override suspend fun toUnit(value: UnifiedReal) = + value.add(UnifiedReal(BoundedRational(27315, 100))) + .multiply(UnifiedReal(BoundedRational(9, 5))) +} + + +@Suppress("FunctionName") +private fun Delisle() = object : Unet { + override val title: Text = Text(R.string.delisle) + override val code: Text = Text(R.string.code_delisle) + override val uuid: String = TAG + "_delisle" + override val group: Text = Text(R.string.unknown) + + override suspend fun toBase(value: UnifiedReal) = + UnifiedReal(100).subtract(value.multiply(UnifiedReal(BoundedRational(2, 3)))) + + override suspend fun toUnit(value: UnifiedReal) = UnifiedReal(100).subtract(value) + .multiply(UnifiedReal(BoundedRational(15, 10))) +} + +@Suppress("FunctionName") +private fun Newton() = object : Unet { + override val title: Text = Text(R.string.newton) + override val code: Text = Text(R.string.code_newton) + override val uuid: String = TAG + "_newton" + override val group: Text = Text(R.string.unknown) + + override suspend fun toBase(value: UnifiedReal) = + value.multiply(UnifiedReal(BoundedRational(100, 33))) + + override suspend fun toUnit(value: UnifiedReal) = + value.multiply(UnifiedReal(BoundedRational(33, 100))) +} + + +@Suppress("FunctionName") +private fun Reaumur() = object : Unet { + override val title: Text = Text(R.string.reaumur) + override val code: Text = Text(R.string.code_reaumur) + override val uuid: String = TAG + "_reaumur" + override val group: Text = Text(R.string.unknown) + + override suspend fun toBase(value: UnifiedReal) = + value.multiply(UnifiedReal(BoundedRational(5, 4))) + + override suspend fun toUnit(value: UnifiedReal) = + value.multiply(UnifiedReal(BoundedRational(4, 5))) +} + + +@Suppress("FunctionName") +private fun Romer() = object : Unet { + override val title: Text = Text(R.string.romer) + override val code: Text = Text(R.string.code_romer) + override val uuid: String = TAG + "_romer" + override val group: Text = Text(R.string.unknown) + + override suspend fun toBase(value: UnifiedReal) = + value.subtract(UnifiedReal(BoundedRational(75, 10))) + .multiply(UnifiedReal(BoundedRational(40, 21))) + + override suspend fun toUnit(value: UnifiedReal) = + value.multiply(UnifiedReal(BoundedRational(21, 40))).add( + UnifiedReal( + BoundedRational( + 75, + 10 + ) + ) + ) +} + +@Suppress("FunctionName") +fun Temperature() = Converter( + uuid = "converter_$TAG", + title = R.string.temperature, + drawableRes = R.drawable.ic_temperature, + units = arrayOf(Celsius(), Fahrenheit(), Kelvin(), Rankine(), Newton(), Romer(), Reaumur()) +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Time.kt b/app/src/main/java/com/prime/toolz2/core/converter/Time.kt new file mode 100644 index 0000000..742b59d --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Time.kt @@ -0,0 +1,129 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R +import com.prime.toolz2.core.math.BoundedRational +import com.prime.toolz2.core.math.UnifiedReal + +//Base unit second +private const val TAG = "Time" + +@Suppress("FunctionName") +private fun Nanosecond() = Unit( + TAG + "_nano_second", + R.string.system_international, + R.string.nanosecond, + R.string.code_nanosecond, + UnifiedReal( + BoundedRational(1, 1000000000) + ) +) + +@Suppress("FunctionName") +private fun Microsecond() = Unit( + TAG + "_micro_second", + R.string.system_international, + R.string.microsecond, + R.string.code_microsecond, + UnifiedReal( + BoundedRational(1, 1000000) + ) +) + + +@Suppress("FunctionName") +private fun Millisecond() = Unit( + TAG + "_milli_second", + R.string.system_international, + R.string.millisecond, + R.string.code_millisecond, + UnifiedReal( + BoundedRational(1, 1000) + ) +) + +@Suppress("FunctionName") +private fun Second() = Unit( + TAG + "_second", + R.string.system_international, + R.string.second, + R.string.code_second, + UnifiedReal( + BoundedRational(1, 1) + ) +) + +@Suppress("FunctionName") +private fun Minute() = Unit( + TAG + "_minute", + R.string.system_international, + R.string.minute, + R.string.code_minute, + UnifiedReal( + BoundedRational(60, 1) + ) +) + + +@Suppress("FunctionName") +private fun Hour() = Unit( + TAG + "_hour", + R.string.system_international, + R.string.hour, + R.string.code_hour, + UnifiedReal( + BoundedRational(3600, 1) + ) +) + + +@Suppress("FunctionName") +private fun Day() = Unit( + TAG + "_day", + R.string.system_international, + R.string.day, + R.string.code_day, + UnifiedReal( + BoundedRational(86400, 1) + ) +) + + +@Suppress("FunctionName") +private fun Week() = Unit( + TAG + "_week", + R.string.system_international, + R.string.week, + R.string.code_week, + UnifiedReal( + BoundedRational(604800, 1) + ) +) + +@Suppress("FunctionName") +private fun Year() = Unit( + TAG + "_year", + R.string.system_international, + R.string.year, + R.string.code_year, + UnifiedReal( + BoundedRational(31557600, 1) + ) +) + +@Suppress("FunctionName") +fun Time() = Converter( + "converter_$TAG", + R.string.time, + R.drawable.ic_time, + arrayOf( + Nanosecond(), + Microsecond(), + Millisecond(), + Second(), + Minute(), + Hour(), + Day(), + Week(), + Year(), + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/converter/Volume.kt b/app/src/main/java/com/prime/toolz2/core/converter/Volume.kt new file mode 100644 index 0000000..b812a17 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/converter/Volume.kt @@ -0,0 +1,13 @@ +package com.prime.toolz2.core.converter + +import com.prime.toolz2.R + +private const val TAG = "Volume" + +@Suppress("FunctionName") +fun Volume() = Converter( + "converter_$TAG", + R.string.volume, + R.drawable.ic_volume, + arrayOf() +) \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/core/math/BoundedRational.java b/app/src/main/java/com/prime/toolz2/core/math/BoundedRational.java new file mode 100644 index 0000000..4ce7606 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/math/BoundedRational.java @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.prime.toolz2.core.math; + +import com.prime.toolz2.core.math.creals.CR; + +import java.math.BigInteger; +import java.util.Objects; +import java.util.Random; + +/** + * Rational numbers that may turn to null if they get too big. + * For many operations, if the length of the nuumerator plus the length of the denominator exceeds + * a maximum size, we simply return null, and rely on our caller do something else. + * We currently never return null for a pure integer or for a BoundedRational that has just been + * constructed. + * + * We also implement a number of irrational functions. These return a non-null result only when + * the result is known to be rational. + */ +public class BoundedRational { + // TODO: Consider returning null for integers. With some care, large factorials might become + // much faster. + // TODO: Maybe eventually make this extend Number? + + private static final int MAX_SIZE = 10000; // total, in bits + + private final BigInteger mNum; + private final BigInteger mDen; + + public BoundedRational(BigInteger n, BigInteger d) { + mNum = n; + mDen = d; + } + + public BoundedRational(BigInteger n) { + mNum = n; + mDen = BigInteger.ONE; + } + + public BoundedRational(long n, long d) { + mNum = BigInteger.valueOf(n); + mDen = BigInteger.valueOf(d); + } + + public BoundedRational(long n) { + mNum = BigInteger.valueOf(n); + mDen = BigInteger.valueOf(1); + } + + /** + * Produce BoundedRational equal to the given double. + */ + public static BoundedRational valueOf(double x) { + final long l = Math.round(x); + if ((double) l == x && Math.abs(l) <= 1000) { + return valueOf(l); + } + final long allBits = Double.doubleToRawLongBits(Math.abs(x)); + long mantissa = (allBits & ((1L << 52) - 1)); + final int biased_exp = (int)(allBits >>> 52); + if ((biased_exp & 0x7ff) == 0x7ff) { + throw new ArithmeticException("Infinity or NaN not convertible to BoundedRational"); + } + final long sign = x < 0.0 ? -1 : 1; + int exp = biased_exp - 1075; // 1023 + 52; we treat mantissa as integer. + if (biased_exp == 0) { + exp += 1; // Denormal exponent is 1 greater. + } else { + mantissa += (1L << 52); // Implied leading one. + } + BigInteger num = BigInteger.valueOf(sign * mantissa); + BigInteger den = BigInteger.ONE; + if (exp >= 0) { + num = num.shiftLeft(exp); + } else { + den = den.shiftLeft(-exp); + } + return new BoundedRational(num, den); + } + + /** + * Produce BoundedRational equal to the given long. + */ + public static BoundedRational valueOf(long x) { + if (x >= -2 && x <= 10) { + switch((int) x) { + case -2: + return MINUS_TWO; + case -1: + return MINUS_ONE; + case 0: + return ZERO; + case 1: + return ONE; + case 2: + return TWO; + case 10: + return TEN; + } + } + return new BoundedRational(x); + } + + /** + * Convert to String reflecting raw representation. + * Debug or log messages only, not pretty. + */ + public String toString() { + return mNum.toString() + "/" + mDen.toString(); + } + + /** + * Convert to readable String. + * Intended for output to user. More expensive, less useful for debugging than + * toString(). Not internationalized. + */ + public String toNiceString() { + final BoundedRational nicer = reduce().positiveDen(); + String result = nicer.mNum.toString(); + if (!nicer.mDen.equals(BigInteger.ONE)) { + result += "/" + nicer.mDen; + } + return result; + } + + public static String toString(BoundedRational r) { + if (r == null) { + return "not a small rational"; + } + return r.toString(); + } + + /** + * Returns a truncated (rounded towards 0) representation of the result. + * Includes n digits to the right of the decimal point. + * @param n result precision, >= 0 + */ + public String toStringTruncated(int n) { + String digits = mNum.abs().multiply(BigInteger.TEN.pow(n)).divide(mDen.abs()).toString(); + int len = digits.length(); + if (len < n + 1) { + digits = StringUtils.repeat('0', n + 1 - len) + digits; + len = n + 1; + } + return (signum() < 0 ? "-" : "") + digits.substring(0, len - n) + "." + + digits.substring(len - n); + } + + /** + * Return a double approximation. + * The result is correctly rounded to nearest, with ties rounded away from zero. + * TODO: Should round ties to even. + */ + public double doubleValue() { + final int sign = signum(); + if (sign < 0) { + return -BoundedRational.negate(this).doubleValue(); + } + // We get the mantissa by dividing the numerator by denominator, after + // suitably prescaling them so that the integral part of the result contains + // enough bits. We do the prescaling to avoid any precision loss, so the division result + // is correctly truncated towards zero. + final int apprExp = mNum.bitLength() - mDen.bitLength(); + if (apprExp < -1100 || sign == 0) { + // Bail fast for clearly zero result. + return 0.0; + } + final int neededPrec = apprExp - 80; + final BigInteger dividend = neededPrec < 0 ? mNum.shiftLeft(-neededPrec) : mNum; + final BigInteger divisor = neededPrec > 0 ? mDen.shiftLeft(neededPrec) : mDen; + final BigInteger quotient = dividend.divide(divisor); + final int qLength = quotient.bitLength(); + int extraBits = qLength - 53; + int exponent = neededPrec + qLength; // Exponent assuming leading binary point. + if (exponent >= -1021) { + // Binary point is actually to right of leading bit. + --exponent; + } else { + // We're in the gradual underflow range. Drop more bits. + extraBits += (-1022 - exponent) + 1; + exponent = -1023; + } + final BigInteger bigMantissa = + quotient.add(BigInteger.ONE.shiftLeft(extraBits - 1)).shiftRight(extraBits); + if (exponent > 1024) { + return Double.POSITIVE_INFINITY; + } + if (exponent > -1023 && bigMantissa.bitLength() != 53 + || exponent <= -1023 && bigMantissa.bitLength() >= 53) { + throw new AssertionError("doubleValue internal error"); + } + final long mantissa = bigMantissa.longValue(); + final long bits = (mantissa & ((1l << 52) - 1)) | (((long) exponent + 1023) << 52); + return Double.longBitsToDouble(bits); + } + + public CR crValue() { + return CR.valueOf(mNum).divide(CR.valueOf(mDen)); + } + + public int intValue() { + BoundedRational reduced = reduce(); + if (!reduced.mDen.equals(BigInteger.ONE)) { + throw new ArithmeticException("intValue of non-int"); + } + return reduced.mNum.intValue(); + } + + // Approximate number of bits to left of binary point. + // Negative indicates leading zeroes to the right of binary point. + public int wholeNumberBits() { + if (mNum.signum() == 0) { + return Integer.MIN_VALUE; + } else { + return mNum.bitLength() - mDen.bitLength(); + } + } + + /** + * Is this number too big for us to continue with rational arithmetic? + * We return fals for integers on the assumption that we have no better fallback. + */ + private boolean tooBig() { + if (mDen.equals(BigInteger.ONE)) { + return false; + } + return (mNum.bitLength() + mDen.bitLength() > MAX_SIZE); + } + + /** + * Return an equivalent fraction with a positive denominator. + */ + private BoundedRational positiveDen() { + if (mDen.signum() > 0) { + return this; + } + return new BoundedRational(mNum.negate(), mDen.negate()); + } + + /** + * Return an equivalent fraction in lowest terms. + * Denominator sign may remain negative. + */ + private BoundedRational reduce() { + if (mDen.equals(BigInteger.ONE)) { + return this; // Optimization only + } + final BigInteger divisor = mNum.gcd(mDen); + return new BoundedRational(mNum.divide(divisor), mDen.divide(divisor)); + } + + static Random sReduceRng = new Random(); + + /** + * Return a possibly reduced version of r that's not tooBig(). + * Return null if none exists. + */ + private static BoundedRational maybeReduce(BoundedRational r) { + if (r == null) return null; + // Reduce randomly, with 1/16 probability, or if the result is too big. + if (!r.tooBig() && (sReduceRng.nextInt() & 0xf) != 0) { + return r; + } + BoundedRational result = r.positiveDen(); + result = result.reduce(); + if (!result.tooBig()) { + return result; + } + return null; + } + + public int compareTo(BoundedRational r) { + // Compare by multiplying both sides by denominators, invert result if denominator product + // was negative. + return mNum.multiply(r.mDen).compareTo(r.mNum.multiply(mDen)) * mDen.signum() + * r.mDen.signum(); + } + + public int signum() { + return mNum.signum() * mDen.signum(); + } + + @Override + public int hashCode() { + // Note that this may be too expensive to be useful. + BoundedRational reduced = reduce().positiveDen(); + return Objects.hash(reduced.mNum, reduced.mDen); + } + + @Override + public boolean equals(Object r) { + return r != null && r instanceof BoundedRational && compareTo((BoundedRational) r) == 0; + } + + // We use static methods for arithmetic, so that we can easily handle the null case. We try + // to catch domain errors whenever possible, sometimes even when one of the arguments is null, + // but not relevant. + + /** + * Returns equivalent BigInteger result if it exists, null if not. + */ + public static BigInteger asBigInteger(BoundedRational r) { + if (r == null) { + return null; + } + final BigInteger[] quotAndRem = r.mNum.divideAndRemainder(r.mDen); + if (quotAndRem[1].signum() == 0) { + return quotAndRem[0]; + } else { + return null; + } + } + public static BoundedRational add(BoundedRational r1, BoundedRational r2) { + if (r1 == null || r2 == null) { + return null; + } + final BigInteger den = r1.mDen.multiply(r2.mDen); + final BigInteger num = r1.mNum.multiply(r2.mDen).add(r2.mNum.multiply(r1.mDen)); + return maybeReduce(new BoundedRational(num,den)); + } + + /** + * Return the argument, but with the opposite sign. + * Returns null only for a null argument. + */ + public static BoundedRational negate(BoundedRational r) { + if (r == null) { + return null; + } + return new BoundedRational(r.mNum.negate(), r.mDen); + } + + public static BoundedRational subtract(BoundedRational r1, BoundedRational r2) { + return add(r1, negate(r2)); + } + + /** + * Return product of r1 and r2 without reducing the result. + */ + private static BoundedRational rawMultiply(BoundedRational r1, BoundedRational r2) { + // It's tempting but marginally unsound to reduce 0 * null to 0. The null could represent + // an infinite value, for which we failed to throw an exception because it was too big. + if (r1 == null || r2 == null) { + return null; + } + // Optimize the case of our special ONE constant, since that's cheap and somewhat frequent. + if (r1 == ONE) { + return r2; + } + if (r2 == ONE) { + return r1; + } + final BigInteger num = r1.mNum.multiply(r2.mNum); + final BigInteger den = r1.mDen.multiply(r2.mDen); + return new BoundedRational(num,den); + } + + public static BoundedRational multiply(BoundedRational r1, BoundedRational r2) { + return maybeReduce(rawMultiply(r1, r2)); + } + + public static class ZeroDivisionException extends ArithmeticException { + public ZeroDivisionException() { + super("Division by zero"); + } + } + + /** + * Return the reciprocal of r (or null if the argument was null). + */ + public static BoundedRational inverse(BoundedRational r) { + if (r == null) { + return null; + } + if (r.mNum.signum() == 0) { + throw new ZeroDivisionException(); + } + return new BoundedRational(r.mDen, r.mNum); + } + + public static BoundedRational divide(BoundedRational r1, BoundedRational r2) { + return multiply(r1, inverse(r2)); + } + + public static BoundedRational sqrt(BoundedRational r) { + // Return non-null if numerator and denominator are small perfect squares. + if (r == null) { + return null; + } + r = r.positiveDen().reduce(); + if (r.mNum.signum() < 0) { + throw new ArithmeticException("sqrt(negative)"); + } + final BigInteger num_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(r.mNum.doubleValue()))); + if (!num_sqrt.multiply(num_sqrt).equals(r.mNum)) { + return null; + } + final BigInteger den_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(r.mDen.doubleValue()))); + if (!den_sqrt.multiply(den_sqrt).equals(r.mDen)) { + return null; + } + return new BoundedRational(num_sqrt, den_sqrt); + } + + public final static BoundedRational ZERO = new BoundedRational(0); + public final static BoundedRational HALF = new BoundedRational(1,2); + public final static BoundedRational MINUS_HALF = new BoundedRational(-1,2); + public final static BoundedRational THIRD = new BoundedRational(1,3); + public final static BoundedRational QUARTER = new BoundedRational(1,4); + public final static BoundedRational SIXTH = new BoundedRational(1,6); + public final static BoundedRational ONE = new BoundedRational(1); + public final static BoundedRational MINUS_ONE = new BoundedRational(-1); + public final static BoundedRational TWO = new BoundedRational(2); + public final static BoundedRational MINUS_TWO = new BoundedRational(-2); + public final static BoundedRational TEN = new BoundedRational(10); + public final static BoundedRational TWELVE = new BoundedRational(12); + public final static BoundedRational THIRTY = new BoundedRational(30); + public final static BoundedRational MINUS_THIRTY = new BoundedRational(-30); + public final static BoundedRational FORTY_FIVE = new BoundedRational(45); + public final static BoundedRational MINUS_FORTY_FIVE = new BoundedRational(-45); + public final static BoundedRational NINETY = new BoundedRational(90); + public final static BoundedRational MINUS_NINETY = new BoundedRational(-90); + + private static final BigInteger BIG_TWO = BigInteger.valueOf(2); + private static final BigInteger BIG_MINUS_ONE = BigInteger.valueOf(-1); + + /** + * Compute integral power of this, assuming this has been reduced and exp is >= 0. + */ + private BoundedRational rawPow(BigInteger exp) { + if (exp.equals(BigInteger.ONE)) { + return this; + } + if (exp.and(BigInteger.ONE).intValue() == 1) { + return rawMultiply(rawPow(exp.subtract(BigInteger.ONE)), this); + } + if (exp.signum() == 0) { + return ONE; + } + BoundedRational tmp = rawPow(exp.shiftRight(1)); + if (Thread.interrupted()) { + throw new CR.AbortedException(); + } + BoundedRational result = rawMultiply(tmp, tmp); + if (result == null || result.tooBig()) { + return null; + } + return result; + } + + /** + * Compute an integral power of this. + */ + public BoundedRational pow(BigInteger exp) { + int expSign = exp.signum(); + if (expSign == 0) { + // Questionable if base has undefined or zero value. + // java.lang.Math.pow() returns 1 anyway, so we do the same. + return BoundedRational.ONE; + } + if (exp.equals(BigInteger.ONE)) { + return this; + } + // Reducing once at the beginning means there's no point in reducing later. + BoundedRational reduced = reduce().positiveDen(); + // First handle cases in which huge exponents could give compact results. + if (reduced.mDen.equals(BigInteger.ONE)) { + if (reduced.mNum.equals(BigInteger.ZERO)) { + return ZERO; + } + if (reduced.mNum.equals(BigInteger.ONE)) { + return ONE; + } + if (reduced.mNum.equals(BIG_MINUS_ONE)) { + if (exp.testBit(0)) { + return MINUS_ONE; + } else { + return ONE; + } + } + } + if (exp.bitLength() > 1000) { + // Stack overflow is likely; a useful rational result is not. + return null; + } + if (expSign < 0) { + return inverse(reduced).rawPow(exp.negate()); + } else { + return reduced.rawPow(exp); + } + } + + public static BoundedRational pow(BoundedRational base, BoundedRational exp) { + if (exp == null) { + return null; + } + if (base == null) { + return null; + } + exp = exp.reduce().positiveDen(); + if (!exp.mDen.equals(BigInteger.ONE)) { + return null; + } + return base.pow(exp.mNum); + } + + + private static final BigInteger BIG_FIVE = BigInteger.valueOf(5); + + /** + * Return the number of decimal digits to the right of the decimal point required to represent + * the argument exactly. + * Return Integer.MAX_VALUE if that's not possible. Never returns a value less than zero, even + * if r is a power of ten. + */ + public static int digitsRequired(BoundedRational r) { + if (r == null) { + return Integer.MAX_VALUE; + } + int powersOfTwo = 0; // Max power of 2 that divides denominator + int powersOfFive = 0; // Max power of 5 that divides denominator + // Try the easy case first to speed things up. + if (r.mDen.equals(BigInteger.ONE)) { + return 0; + } + r = r.reduce(); + BigInteger den = r.mDen; + if (den.bitLength() > MAX_SIZE) { + return Integer.MAX_VALUE; + } + while (!den.testBit(0)) { + ++powersOfTwo; + den = den.shiftRight(1); + } + while (den.mod(BIG_FIVE).signum() == 0) { + ++powersOfFive; + den = den.divide(BIG_FIVE); + } + // If the denominator has a factor of other than 2 or 5 (the divisors of 10), the decimal + // expansion does not terminate. Multiplying the fraction by any number of powers of 10 + // will not cancel the demoniator. (Recall the fraction was in lowest terms to start + // with.) Otherwise the powers of 10 we need to cancel the denominator is the larger of + // powersOfTwo and powersOfFive. + if (!den.equals(BigInteger.ONE) && !den.equals(BIG_MINUS_ONE)) { + return Integer.MAX_VALUE; + } + return Math.max(powersOfTwo, powersOfFive); + } +} diff --git a/app/src/main/java/com/prime/toolz2/core/math/StringUtils.java b/app/src/main/java/com/prime/toolz2/core/math/StringUtils.java new file mode 100644 index 0000000..97f79b5 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/math/StringUtils.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.prime.toolz2.core.math; + +/** + * Some helpful methods operating on strings. + */ + +class StringUtils { + + /** + * Return a string with n copies of c. + */ + public static String repeat(char c, int n) { + final StringBuilder result = new StringBuilder(); + for (int i = 0; i < n; ++i) { + result.append(c); + } + return result.toString(); + } + + /** + * Return a copy of the supplied string with commas added every three digits. + * The substring indicated by the supplied range is assumed to contain only + * a whole number, with no decimal point. + * Inserting a digit separator every 3 digits appears to be + * at least somewhat acceptable, though not necessarily preferred, everywhere. + * The grouping separator in the result is NOT localized. + */ + public static String addCommas(String s, int begin, int end) { + // Resist the temptation to use Java's NumberFormat, which converts to long or double + // and hence doesn't handle very large numbers. + StringBuilder result = new StringBuilder(); + int current = begin; + while (current < end && (s.charAt(current) == '-' || s.charAt(current) == ' ')) { + ++current; + } + result.append(s, begin, current); + while (current < end) { + result.append(s.charAt(current)); + ++current; + if ((end - current) % 3 == 0 && end != current) { + result.append(','); + } + } + return result.toString(); + } + + /** + * Ignoring all occurrences of c in both strings, check whether old is a prefix of new. + * If so, return the remaining subsequence of whole. If not, return null. + */ + public static CharSequence getExtensionIgnoring(CharSequence whole, CharSequence prefix, + char c) { + int wIndex = 0; + int pIndex = 0; + final int wLen = whole.length(); + final int pLen = prefix.length(); + while (true) { + while (pIndex < pLen && prefix.charAt(pIndex) == c) { + ++pIndex; + } + while (wIndex < wLen && whole.charAt(wIndex) == c) { + ++wIndex; + } + if (pIndex == pLen) { + break; + } + if (wIndex == wLen || whole.charAt(wIndex) != prefix.charAt(pIndex) ) { + return null; + } + ++pIndex; + ++wIndex; + } + while (wIndex < wLen && whole.charAt(wIndex) == c) { + ++wIndex; + } + return whole.subSequence(wIndex, wLen); + } +} diff --git a/app/src/main/java/com/prime/toolz2/core/math/UnifiedReal.java b/app/src/main/java/com/prime/toolz2/core/math/UnifiedReal.java new file mode 100644 index 0000000..aec21fb --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/math/UnifiedReal.java @@ -0,0 +1,1287 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.prime.toolz2.core.math; + +import com.prime.toolz2.core.math.BoundedRational; +import com.prime.toolz2.core.math.creals.CR; +import com.prime.toolz2.core.math.creals.UnaryCRFunction; + +import java.math.BigInteger; + +/** + * Computable real numbers, represented so that we can get exact decidable comparisons + * for a number of interesting special cases, including rational computations. + * + * A real number is represented as the product of two numbers with different representations: + * A) A BoundedRational that can only represent a subset of the rationals, but supports + * exact computable comparisons. + * B) A lazily evaluated "constructive real number" that provides operations to evaluate + * itself to any requested number of digits. + * Whenever possible, we choose (B) to be one of a small set of known constants about which we + * know more. For example, whenever we can, we represent rationals such that (B) is 1. + * This scheme allows us to do some very limited symbolic computation on numbers when both + * have the same (B) value, as well as in some other situations. We try to maximize that + * possibility. + * + * Arithmetic operations and operations that produce finite approximations may throw unchecked + * exceptions produced by the underlying CR and BoundedRational packages, including + * CR.PrecisionOverflowException and CR.AbortedException. + */ +public class UnifiedReal { + + private final BoundedRational mRatFactor; + private final CR mCrFactor; + // TODO: It would be helpful to add flags to indicate whether the result is known + // irrational, etc. This sometimes happens even if mCrFactor is not one of the known ones. + // And exact comparisons between rationals and known irrationals are decidable. + + /** + * Perform some nontrivial consistency checks. + * @hide + */ + public static boolean enableChecks = true; + + private static void check(boolean b) { + if (!b) { + throw new AssertionError(); + } + } + + private UnifiedReal(BoundedRational rat, CR cr) { + if (rat == null) { + throw new ArithmeticException("Building UnifiedReal from null"); + } + // We don't normally traffic in null CRs, and hence don't test explicitly. + mCrFactor = cr; + mRatFactor = rat; + } + + public UnifiedReal(CR cr) { + this(BoundedRational.ONE, cr); + } + + public UnifiedReal(BoundedRational rat) { + this(rat, CR_ONE); + } + + public UnifiedReal(BigInteger n) { + this(new BoundedRational(n)); + } + + public UnifiedReal(long n) { + this(new BoundedRational(n)); + } + + public static UnifiedReal valueOf(double x) { + if (x == 0.0 || x == 1.0) { + return valueOf((long) x); + } + return new UnifiedReal(BoundedRational.valueOf(x)); + } + + public static UnifiedReal valueOf(long x) { + if (x == 0) { + return UnifiedReal.ZERO; + } else if (x == 1) { + return UnifiedReal.ONE; + } else { + return new UnifiedReal(BoundedRational.valueOf(x)); + } + } + + // Various helpful constants + private final static BigInteger BIG_24 = BigInteger.valueOf(24); + private final static int DEFAULT_COMPARE_TOLERANCE = -1000; + + // Well-known CR constants we try to use in the mCrFactor position: + private final static CR CR_ONE = CR.ONE; + private final static CR CR_PI = CR.PI; + private final static CR CR_E = CR.ONE.exp(); + private final static CR CR_SQRT2 = CR.valueOf(2).sqrt(); + private final static CR CR_SQRT3 = CR.valueOf(3).sqrt(); + private final static CR CR_LN2 = CR.valueOf(2).ln(); + private final static CR CR_LN3 = CR.valueOf(3).ln(); + private final static CR CR_LN5 = CR.valueOf(5).ln(); + private final static CR CR_LN6 = CR.valueOf(6).ln(); + private final static CR CR_LN7 = CR.valueOf(7).ln(); + private final static CR CR_LN10 = CR.valueOf(10).ln(); + + // Square roots that we try to recognize. + // We currently recognize only a small fixed collection, since the sqrt() function needs to + // identify numbers of the form *n^2, and we don't otherwise know of a good + // algorithm for that. + private final static CR sSqrts[] = { + null, + CR.ONE, + CR_SQRT2, + CR_SQRT3, + null, + CR.valueOf(5).sqrt(), + CR.valueOf(6).sqrt(), + CR.valueOf(7).sqrt(), + null, + null, + CR.valueOf(10).sqrt() }; + + // Natural logs of small integers that we try to recognize. + private final static CR sLogs[] = { + null, + null, + CR_LN2, + CR_LN3, + null, + CR_LN5, + CR_LN6, + CR_LN7, + null, + null, + CR_LN10 }; + + + // Some convenient UnifiedReal constants. + public static final UnifiedReal PI = new UnifiedReal(CR_PI); + public static final UnifiedReal E = new UnifiedReal(CR_E); + public static final UnifiedReal ZERO = new UnifiedReal(BoundedRational.ZERO); + public static final UnifiedReal ONE = new UnifiedReal(BoundedRational.ONE); + public static final UnifiedReal MINUS_ONE = new UnifiedReal(BoundedRational.MINUS_ONE); + public static final UnifiedReal TWO = new UnifiedReal(BoundedRational.TWO); + public static final UnifiedReal MINUS_TWO = new UnifiedReal(BoundedRational.MINUS_TWO); + public static final UnifiedReal HALF = new UnifiedReal(BoundedRational.HALF); + public static final UnifiedReal MINUS_HALF = new UnifiedReal(BoundedRational.MINUS_HALF); + public static final UnifiedReal TEN = new UnifiedReal(BoundedRational.TEN); + public static final UnifiedReal RADIANS_PER_DEGREE + = new UnifiedReal(new BoundedRational(1, 180), CR_PI); + private static final UnifiedReal SIX = new UnifiedReal(6); + private static final UnifiedReal HALF_SQRT2 = new UnifiedReal(BoundedRational.HALF, CR_SQRT2); + private static final UnifiedReal SQRT3 = new UnifiedReal(CR_SQRT3); + private static final UnifiedReal HALF_SQRT3 = new UnifiedReal(BoundedRational.HALF, CR_SQRT3); + private static final UnifiedReal THIRD_SQRT3 = new UnifiedReal(BoundedRational.THIRD, CR_SQRT3); + private static final UnifiedReal PI_OVER_2 = new UnifiedReal(BoundedRational.HALF, CR_PI); + private static final UnifiedReal PI_OVER_3 = new UnifiedReal(BoundedRational.THIRD, CR_PI); + private static final UnifiedReal PI_OVER_4 = new UnifiedReal(BoundedRational.QUARTER, CR_PI); + private static final UnifiedReal PI_OVER_6 = new UnifiedReal(BoundedRational.SIXTH, CR_PI); + + + /** + * Given a constructive real cr, try to determine whether cr is the square root of + * a small integer. If so, return its square as a BoundedRational. Otherwise return null. + * We make this determination by simple table lookup, so spurious null returns are + * entirely possible, or even likely. + */ + private static BoundedRational getSquare(CR cr) { + for (int i = 0; i < sSqrts.length; ++i) { + if (sSqrts[i] == cr) { + return new BoundedRational(i); + } + } + return null; + } + + /** + * Given a constructive real cr, try to determine whether cr is the logarithm of a small + * integer. If so, return exp(cr) as a BoundedRational. Otherwise return null. + * We make this determination by simple table lookup, so spurious null returns are + * entirely possible, or even likely. + */ + private BoundedRational getExp(CR cr) { + for (int i = 0; i < sLogs.length; ++i) { + if (sLogs[i] == cr) { + return new BoundedRational(i); + } + } + return null; + } + + /** + * If the argument is a well-known constructive real, return its name. + * The name of "CR_ONE" is the empty string. + * No named constructive reals are rational multiples of each other. + * Thus two UnifiedReals with different named mCrFactors can be equal only if both + * mRatFactors are zero or possibly if one is CR_PI and the other is CR_E. + * (The latter is apparently an open problem.) + */ + private static String crName(CR cr) { + if (cr == CR_ONE) { + return ""; + } + if (cr == CR_PI) { + return "\u03C0"; // GREEK SMALL LETTER PI + } + if (cr == CR_E) { + return "e"; + } + for (int i = 0; i < sSqrts.length; ++i) { + if (cr == sSqrts[i]) { + return "\u221A" /* SQUARE ROOT */ + i; + } + } + for (int i = 0; i < sLogs.length; ++i) { + if (cr == sLogs[i]) { + return "ln(" + i + ")"; + } + } + return null; + } + + /** + * Would crName() return non-Null? + */ + private static boolean isNamed(CR cr) { + if (cr == CR_ONE || cr == CR_PI || cr == CR_E) { + return true; + } + for (CR r: sSqrts) { + if (cr == r) { + return true; + } + } + for (CR r: sLogs) { + if (cr == r) { + return true; + } + } + return false; + } + + /** + * Is cr known to be algebraic (as opposed to transcendental)? + * Currently only produces meaningful results for the above known special + * constructive reals. + */ + private static boolean definitelyAlgebraic(CR cr) { + return cr == CR_ONE || getSquare(cr) != null; + } + + /** + * Is this number known to be rational? + */ + public boolean definitelyRational() { + return mCrFactor == CR_ONE || mRatFactor.signum() == 0; + } + + /** + * Is this number known to be irrational? + * TODO: We could track the fact that something is irrational with an explicit flag, which + * could cover many more cases. Whether that matters in practice is TBD. + */ + public boolean definitelyIrrational() { + return !definitelyRational() && isNamed(mCrFactor); + } + + /** + * Is this number known to be algebraic? + */ + public boolean definitelyAlgebraic() { + return definitelyAlgebraic(mCrFactor) || mRatFactor.signum() == 0; + } + + /** + * Is this number known to be transcendental? + */ + public boolean definitelyTranscendental() { + return !definitelyAlgebraic() && isNamed(mCrFactor); + } + + + /** + * Is it known that the two constructive reals differ by something other than a + * a rational factor, i.e. is it known that two UnifiedReals + * with those mCrFactors will compare unequal unless both mRatFactors are zero? + * If this returns true, then a comparison of two UnifiedReals using those two + * mCrFactors cannot diverge, though we don't know of a good runtime bound. + */ + private static boolean definitelyIndependent(CR r1, CR r2) { + // The question here is whether r1 = x*r2, where x is rational, where r1 and r2 + // are in our set of special known CRs, can have a solution. + // This cannot happen if one is CR_ONE and the other is not. + // (Since all others are irrational.) + // This cannot happen for two named square roots, which have no repeated factors. + // (To see this, square both sides of the equation and factor. Each prime + // factor in the numerator and denominator occurs twice.) + // This cannot happen for e or pi on one side, and a square root on the other. + // (One is transcendental, the other is algebraic.) + // This cannot happen for two of our special natural logs. + // (Otherwise ln(m) = (a/b)ln(n) ==> m = n^(a/b) ==> m^b = n^a, which is impossible + // because either m or n includes a prime factor not shared by the other.) + // This cannot happen for a log and a square root. + // (The Lindemann-Weierstrass theorem tells us, among other things, that if + // a is algebraic, then exp(a) is transcendental. Thus if l in our finite + // set of logs where algebraic, expl(l), must be transacendental. + // But exp(l) is an integer. Thus the logs are transcendental. But of course the + // square roots are algebraic. Thus they can't be rational multiples.) + // Unfortunately, we do not know whether e/pi is rational. + if (r1 == r2) { + return false; + } + CR other; + if (r1 == CR_E || r1 == CR_PI) { + return definitelyAlgebraic(r2); + } + if (r2 == CR_E || r2 == CR_PI) { + return definitelyAlgebraic(r1); + } + return isNamed(r1) && isNamed(r2); + } + + /** + * Convert to String reflecting raw representation. + * Debug or log messages only, not pretty. + */ + public String toString() { + return mRatFactor.toString() + "*" + mCrFactor.toString(); + } + + /** + * Convert to readable String. + * Intended for user output. Produces exact expression when possible. + */ + public String toNiceString() { + if (mCrFactor == CR_ONE || mRatFactor.signum() == 0) { + return mRatFactor.toNiceString(); + } + String name = crName(mCrFactor); + if (name != null) { + BigInteger bi = BoundedRational.asBigInteger(mRatFactor); + if (bi != null) { + if (bi.equals(BigInteger.ONE)) { + return name; + } + return mRatFactor.toNiceString() + name; + } + return "(" + mRatFactor.toNiceString() + ")" + name; + } + if (mRatFactor.equals(BoundedRational.ONE)) { + return mCrFactor.toString(); + } + return crValue().toString(); + } + + /** + * Will toNiceString() produce an exact representation? + */ + public boolean exactlyDisplayable() { + return crName(mCrFactor) != null; + } + + // Number of extra bits used in evaluation below to prefer truncation to rounding. + // Must be <= 30. + private final static int EXTRA_PREC = 10; + + /* + * Returns a truncated representation of the result. + * If exactlyTruncatable(), we round correctly towards zero. Otherwise the resulting digit + * string may occasionally be rounded up instead. + * Always includes a decimal point in the result. + * The result includes n digits to the right of the decimal point. + * @param n result precision, >= 0 + */ + public String toStringTruncated(int n) { + if (mCrFactor == CR_ONE || mRatFactor == BoundedRational.ZERO) { + return mRatFactor.toStringTruncated(n); + } + final CR scaled = CR.valueOf(BigInteger.TEN.pow(n)).multiply(crValue()); + boolean negative = false; + BigInteger intScaled; + if (exactlyTruncatable()) { + intScaled = scaled.get_appr(0); + if (intScaled.signum() < 0) { + negative = true; + intScaled = intScaled.negate(); + } + if (CR.valueOf(intScaled).compareTo(scaled.abs()) > 0) { + intScaled = intScaled.subtract(BigInteger.ONE); + } + check(CR.valueOf(intScaled).compareTo(scaled.abs()) < 0); + } else { + // Approximate case. Exact comparisons are impossible. + intScaled = scaled.get_appr(-EXTRA_PREC); + if (intScaled.signum() < 0) { + negative = true; + intScaled = intScaled.negate(); + } + intScaled = intScaled.shiftRight(EXTRA_PREC); + } + String digits = intScaled.toString(); + int len = digits.length(); + if (len < n + 1) { + digits = StringUtils.repeat('0', n + 1 - len) + digits; + len = n + 1; + } + return (negative ? "-" : "") + digits.substring(0, len - n) + "." + + digits.substring(len - n); + } + + /* + * Can we compute correctly truncated approximations of this number? + */ + public boolean exactlyTruncatable() { + // If the value is known rational, we can do exact comparisons. + // If the value is known irrational, then we can safely compare to rational approximations; + // equality is impossible; hence the comparison must converge. + // The only problem cases are the ones in which we don't know. + return mCrFactor == CR_ONE || mRatFactor == BoundedRational.ZERO || definitelyIrrational(); + } + + /** + * Return a double approximation. + * Rational arguments are currently rounded to nearest, with ties away from zero. + * TODO: Improve rounding. + */ + public double doubleValue() { + if (mCrFactor == CR_ONE) { + return mRatFactor.doubleValue(); // Hopefully correctly rounded + } else { + return crValue().doubleValue(); // Approximately correctly rounded + } + } + + public CR crValue() { + return mRatFactor.crValue().multiply(mCrFactor); + } + + /** + * Are this and r exactly comparable? + */ + public boolean isComparable(UnifiedReal u) { + // We check for ONE only to speed up the common case. + // The use of a tolerance here means we can spuriously return false, not true. + return mCrFactor == u.mCrFactor + && (isNamed(mCrFactor) || mCrFactor.signum(DEFAULT_COMPARE_TOLERANCE) != 0) + || mRatFactor.signum() == 0 && u.mRatFactor.signum() == 0 + || definitelyIndependent(mCrFactor, u.mCrFactor) + || crValue().compareTo(u.crValue(), DEFAULT_COMPARE_TOLERANCE) != 0; + } + + /** + * Return +1 if this is greater than r, -1 if this is less than r, or 0 of the two are + * known to be equal. + * May diverge if the two are equal and !isComparable(r). + */ + public int compareTo(UnifiedReal u) { + if (definitelyZero() && u.definitelyZero()) return 0; + if (mCrFactor == u.mCrFactor) { + int signum = mCrFactor.signum(); // Can diverge if mCRFactor == 0. + return signum * mRatFactor.compareTo(u.mRatFactor); + } + return crValue().compareTo(u.crValue()); // Can also diverge. + } + + /** + * Return +1 if this is greater than r, -1 if this is less than r, or possibly 0 of the two are + * within 2^a of each other. + */ + public int compareTo(UnifiedReal u, int a) { + if (isComparable(u)) { + return compareTo(u); + } else { + return crValue().compareTo(u.crValue(), a); + } + } + + /** + * Return compareTo(ZERO, a). + */ + public int signum(int a) { + return compareTo(ZERO, a); + } + + /** + * Return compareTo(ZERO). + * May diverge for ZERO argument if !isComparable(ZERO). + */ + public int signum() { + return compareTo(ZERO); + } + + /** + * Equality comparison. May erroneously return true if values differ by less than 2^a, + * and !isComparable(u). + */ + public boolean approxEquals(UnifiedReal u, int a) { + if (isComparable(u)) { + if (definitelyIndependent(mCrFactor, u.mCrFactor) + && (mRatFactor.signum() != 0 || u.mRatFactor.signum() != 0)) { + // No need to actually evaluate, though we don't know which is larger. + return false; + } else { + return compareTo(u) == 0; + } + } + return crValue().compareTo(u.crValue(), a) == 0; + } + + /** + * Returns true if values are definitely known to be equal, false in all other cases. + * This does not satisfy the contract for Object.equals(). + */ + public boolean definitelyEquals(UnifiedReal u) { + return isComparable(u) && compareTo(u) == 0; + } + + @Override + public int hashCode() { + // Better useless than wrong. Probably. + return 0; + } + + @Override + public boolean equals(Object r) { + if (r == null || !(r instanceof UnifiedReal)) { + return false; + } + // This is almost certainly a programming error. Don't even try. + throw new AssertionError("Can't compare UnifiedReals for exact equality"); + } + + /** + * Returns true if values are definitely known not to be equal, false in all other cases. + * Performs no approximate evaluation. + */ + public boolean definitelyNotEquals(UnifiedReal u) { + boolean isNamed = isNamed(mCrFactor); + boolean uIsNamed = isNamed(u.mCrFactor); + if (isNamed && uIsNamed) { + if (definitelyIndependent(mCrFactor, u.mCrFactor)) { + return mRatFactor.signum() != 0 || u.mRatFactor.signum() != 0; + } else if (mCrFactor == u.mCrFactor) { + return !mRatFactor.equals(u.mRatFactor); + } + return !mRatFactor.equals(u.mRatFactor); + } + if (mRatFactor.signum() == 0) { + return uIsNamed && u.mRatFactor.signum() != 0; + } + if (u.mRatFactor.signum() == 0) { + return isNamed && mRatFactor.signum() != 0; + } + return false; + } + + // And some slightly faster convenience functions for special cases: + + public boolean definitelyZero() { + return mRatFactor.signum() == 0; + } + + /** + * Can this number be determined to be definitely nonzero without performing approximate + * evaluation? + */ + public boolean definitelyNonZero() { + return isNamed(mCrFactor) && mRatFactor.signum() != 0; + } + + public boolean definitelyOne() { + return mCrFactor == CR_ONE && mRatFactor.equals(BoundedRational.ONE); + } + + /** + * Return equivalent BoundedRational, if known to exist, null otherwise + */ + public BoundedRational boundedRationalValue() { + if (mCrFactor == CR_ONE || mRatFactor.signum() == 0) { + return mRatFactor; + } + return null; + } + + /** + * Returns equivalent BigInteger result if it exists, null if not. + */ + public BigInteger bigIntegerValue() { + final BoundedRational r = boundedRationalValue(); + return BoundedRational.asBigInteger(r); + } + + public UnifiedReal add(UnifiedReal u) { + if (mCrFactor == u.mCrFactor) { + BoundedRational nRatFactor = BoundedRational.add(mRatFactor, u.mRatFactor); + if (nRatFactor != null) { + return new UnifiedReal(nRatFactor, mCrFactor); + } + } + if (definitelyZero()) { + // Avoid creating new mCrFactor, even if they don't currently match. + return u; + } + if (u.definitelyZero()) { + return this; + } + return new UnifiedReal(crValue().add(u.crValue())); + } + + public UnifiedReal negate() { + return new UnifiedReal(BoundedRational.negate(mRatFactor), mCrFactor); + } + + public UnifiedReal subtract(UnifiedReal u) { + return add(u.negate()); + } + + public UnifiedReal multiply(UnifiedReal u) { + // Preserve a preexisting mCrFactor when we can. + if (mCrFactor == CR_ONE) { + BoundedRational nRatFactor = BoundedRational.multiply(mRatFactor, u.mRatFactor); + if (nRatFactor != null) { + return new UnifiedReal(nRatFactor, u.mCrFactor); + } + } + if (u.mCrFactor == CR_ONE) { + BoundedRational nRatFactor = BoundedRational.multiply(mRatFactor, u.mRatFactor); + if (nRatFactor != null) { + return new UnifiedReal(nRatFactor, mCrFactor); + } + } + if (definitelyZero() || u.definitelyZero()) { + return ZERO; + } + if (mCrFactor == u.mCrFactor) { + BoundedRational square = getSquare(mCrFactor); + if (square != null) { + BoundedRational nRatFactor = BoundedRational.multiply( + BoundedRational.multiply(square, mRatFactor), u.mRatFactor); + if (nRatFactor != null) { + return new UnifiedReal(nRatFactor); + } + } + } + // Probably a bit cheaper to multiply component-wise. + BoundedRational nRatFactor = BoundedRational.multiply(mRatFactor, u.mRatFactor); + if (nRatFactor != null) { + return new UnifiedReal(nRatFactor, mCrFactor.multiply(u.mCrFactor)); + } + return new UnifiedReal(crValue().multiply(u.crValue())); + } + + public static class ZeroDivisionException extends ArithmeticException { + public ZeroDivisionException() { + super("Division by zero"); + } + } + + /** + * Return the reciprocal. + */ + public UnifiedReal inverse() { + if (definitelyZero()) { + throw new ZeroDivisionException(); + } + BoundedRational square = getSquare(mCrFactor); + if (square != null) { + // 1/sqrt(n) = sqrt(n)/n + BoundedRational nRatFactor = BoundedRational.inverse( + BoundedRational.multiply(mRatFactor, square)); + if (nRatFactor != null) { + return new UnifiedReal(nRatFactor, mCrFactor); + } + } + return new UnifiedReal(BoundedRational.inverse(mRatFactor), mCrFactor.inverse()); + } + + public UnifiedReal divide(UnifiedReal u) { + if (mCrFactor == u.mCrFactor) { + if (u.definitelyZero()) { + throw new ZeroDivisionException(); + } + BoundedRational nRatFactor = BoundedRational.divide(mRatFactor, u.mRatFactor); + if (nRatFactor != null) { + return new UnifiedReal(nRatFactor, CR_ONE); + } + } + return multiply(u.inverse()); + } + + /** + * Return the square root. + * This may fail to return a known rational value, even when the result is rational. + */ + public UnifiedReal sqrt() { + if (definitelyZero()) { + return ZERO; + } + if (mCrFactor == CR_ONE) { + BoundedRational ratSqrt; + // Check for all arguments of the form * small_int, + // where small_int has a known sqrt. This includes the small_int = 1 case. + for (int divisor = 1; divisor < sSqrts.length; ++divisor) { + if (sSqrts[divisor] != null) { + ratSqrt = BoundedRational.sqrt( + BoundedRational.divide(mRatFactor, new BoundedRational(divisor))); + if (ratSqrt != null) { + return new UnifiedReal(ratSqrt, sSqrts[divisor]); + } + } + } + } + return new UnifiedReal(crValue().sqrt()); + } + + /** + * Return (this mod 2pi)/(pi/6) as a BigInteger, or null if that isn't easily possible. + */ + private BigInteger getPiTwelfths() { + if (definitelyZero()) return BigInteger.ZERO; + if (mCrFactor == CR_PI) { + BigInteger quotient = BoundedRational.asBigInteger( + BoundedRational.multiply(mRatFactor, BoundedRational.TWELVE)); + if (quotient == null) { + return null; + } + return quotient.mod(BIG_24); + } + return null; + } + + /** + * Computer the sin() for an integer multiple n of pi/12, if easily representable. + * @param n value between 0 and 23 inclusive. + */ + private static UnifiedReal sinPiTwelfths(int n) { + if (n >= 12) { + UnifiedReal negResult = sinPiTwelfths(n - 12); + return negResult == null ? null : negResult.negate(); + } + switch (n) { + case 0: + return ZERO; + case 2: // 30 degrees + return HALF; + case 3: // 45 degrees + return HALF_SQRT2; + case 4: // 60 degrees + return HALF_SQRT3; + case 6: + return ONE; + case 8: + return HALF_SQRT3; + case 9: + return HALF_SQRT2; + case 10: + return HALF; + default: + return null; + } + } + + public UnifiedReal sin() { + BigInteger piTwelfths = getPiTwelfths(); + if (piTwelfths != null) { + UnifiedReal result = sinPiTwelfths(piTwelfths.intValue()); + if (result != null) { + return result; + } + } + return new UnifiedReal(crValue().sin()); + } + + private static UnifiedReal cosPiTwelfths(int n) { + int sinArg = n + 6; + if (sinArg >= 24) { + sinArg -= 24; + } + return sinPiTwelfths(sinArg); + } + + public UnifiedReal cos() { + BigInteger piTwelfths = getPiTwelfths(); + if (piTwelfths != null) { + UnifiedReal result = cosPiTwelfths(piTwelfths.intValue()); + if (result != null) { + return result; + } + } + return new UnifiedReal(crValue().cos()); + } + + public UnifiedReal tan() { + BigInteger piTwelfths = getPiTwelfths(); + if (piTwelfths != null) { + int i = piTwelfths.intValue(); + if (i == 6 || i == 18) { + throw new ArithmeticException("Tangent undefined"); + } + UnifiedReal top = sinPiTwelfths(i); + UnifiedReal bottom = cosPiTwelfths(i); + if (top != null && bottom != null) { + return top.divide(bottom); + } + } + return sin().divide(cos()); + } + + // Throw an exception if the argument is definitely out of bounds for asin or acos. + private void checkAsinDomain() { + if (isComparable(ONE) && (compareTo(ONE) > 0 || compareTo(MINUS_ONE) < 0)) { + throw new ArithmeticException("inverse trig argument out of range"); + } + } + + /** + * Return asin(n/2). n is between -2 and 2. + */ + public static UnifiedReal asinHalves(int n){ + if (n < 0) { + return (asinHalves(-n).negate()); + } + switch (n) { + case 0: + return ZERO; + case 1: + return new UnifiedReal(BoundedRational.SIXTH, CR.PI); + case 2: + return new UnifiedReal(BoundedRational.HALF, CR.PI); + } + throw new AssertionError("asinHalves: Bad argument"); + } + + /** + * Return asin of this, assuming this is not an integral multiple of a half. + */ + public UnifiedReal asinNonHalves() + { + if (compareTo(ZERO, -10) < 0) { + return negate().asinNonHalves().negate(); + } + if (definitelyEquals(HALF_SQRT2)) { + return new UnifiedReal(BoundedRational.QUARTER, CR_PI); + } + if (definitelyEquals(HALF_SQRT3)) { + return new UnifiedReal(BoundedRational.THIRD, CR_PI); + } + return new UnifiedReal(crValue().asin()); + } + + public UnifiedReal asin() { + checkAsinDomain(); + final BigInteger halves = multiply(TWO).bigIntegerValue(); + if (halves != null) { + return asinHalves(halves.intValue()); + } + if (mCrFactor == CR.ONE || mCrFactor != CR_SQRT2 ||mCrFactor != CR_SQRT3) { + return asinNonHalves(); + } + return new UnifiedReal(crValue().asin()); + } + + public UnifiedReal acos() { + return PI_OVER_2.subtract(asin()); + } + + public UnifiedReal atan() { + if (compareTo(ZERO, -10) < 0) { + return negate().atan().negate(); + } + final BigInteger asBI = bigIntegerValue(); + if (asBI != null && asBI.compareTo(BigInteger.ONE) <= 0) { + final int asInt = asBI.intValue(); + // These seem to be all rational cases: + switch (asInt) { + case 0: + return ZERO; + case 1: + return PI_OVER_4; + default: + throw new AssertionError("Impossible r_int"); + } + } + if (definitelyEquals(THIRD_SQRT3)) { + return PI_OVER_6; + } + if (definitelyEquals(SQRT3)) { + return PI_OVER_3; + } + return new UnifiedReal(UnaryCRFunction.atanFunction.execute(crValue())); + } + + private static final BigInteger BIG_TWO = BigInteger.valueOf(2); + + // The (in abs value) integral exponent for which we attempt to use a recursive + // algorithm for evaluating pow(). The recursive algorithm works independent of the sign of the + // base, and can produce rational results. But it can become slow for very large exponents. + private static final BigInteger RECURSIVE_POW_LIMIT = BigInteger.valueOf(1000); + // The corresponding limit when we're using rational arithmetic. This should fail fast + // anyway, but we avoid ridiculously deep recursion. + private static final BigInteger HARD_RECURSIVE_POW_LIMIT = BigInteger.ONE.shiftLeft(1000); + + /** + * Compute an integral power of a constructive real, using the standard recursive algorithm. + * exp is known to be positive. + */ + private static CR recursivePow(CR base, BigInteger exp) { + if (exp.equals(BigInteger.ONE)) { + return base; + } + if (exp.testBit(0)) { + return base.multiply(recursivePow(base, exp.subtract(BigInteger.ONE))); + } + CR tmp = recursivePow(base, exp.shiftRight(1)); + if (Thread.interrupted()) { + throw new CR.AbortedException(); + } + return tmp.multiply(tmp); + } + + /** + * Compute an integral power of a constructive real, using the exp function when + * we safely can. Use recursivePow when we can't. exp is known to be nozero. + */ + private UnifiedReal expLnPow(BigInteger exp) { + int sign = signum(DEFAULT_COMPARE_TOLERANCE); + if (sign > 0) { + // Safe to take the log. This avoids deep recursion for huge exponents, which + // may actually make sense here. + return new UnifiedReal(crValue().ln().multiply(CR.valueOf(exp)).exp()); + } else if (sign < 0) { + CR result = crValue().negate().ln().multiply(CR.valueOf(exp)).exp(); + if (exp.testBit(0) /* odd exponent */) { + result = result.negate(); + } + return new UnifiedReal(result); + } else { + // Base of unknown sign with integer exponent. Use a recursive computation. + // (Another possible option would be to use the absolute value of the base, and then + // adjust the sign at the end. But that would have to be done in the CR + // implementation.) + if (exp.signum() < 0) { + // This may be very expensive if exp.negate() is large. + return new UnifiedReal(recursivePow(crValue(), exp.negate()).inverse()); + } else { + return new UnifiedReal(recursivePow(crValue(), exp)); + } + } + } + + + /** + * Compute an integral power of this. + * This recurses roughly as deeply as the number of bits in the exponent, and can, in + * ridiculous cases, result in a stack overflow. + */ + private UnifiedReal pow(BigInteger exp) { + if (exp.equals(BigInteger.ONE)) { + return this; + } + if (exp.signum() == 0) { + // Questionable if base has undefined value or is 0. + // Java.lang.Math.pow() returns 1 anyway, so we do the same. + return ONE; + } + BigInteger absExp = exp.abs(); + if (mCrFactor == CR_ONE && absExp.compareTo(HARD_RECURSIVE_POW_LIMIT) <= 0) { + final BoundedRational ratPow = mRatFactor.pow(exp); + // We count on this to fail, e.g. for very large exponents, when it would + // otherwise be too expensive. + if (ratPow != null) { + return new UnifiedReal(ratPow); + } + } + if (absExp.compareTo(RECURSIVE_POW_LIMIT) > 0) { + return expLnPow(exp); + } + BoundedRational square = getSquare(mCrFactor); + if (square != null) { + final BoundedRational nRatFactor = + BoundedRational.multiply(mRatFactor.pow(exp), square.pow(exp.shiftRight(1))); + if (nRatFactor != null) { + if (exp.and(BigInteger.ONE).intValue() == 1) { + // Odd power: Multiply by remaining square root. + return new UnifiedReal(nRatFactor, mCrFactor); + } else { + return new UnifiedReal(nRatFactor); + } + } + } + return expLnPow(exp); + } + + /** + * Return this ^ expon. + * This is really only well-defined for a positive base, particularly since + * 0^x is not continuous at zero. (0^0 = 1 (as is epsilon^0), but 0^epsilon is 0. + * We nonetheless try to do reasonable things at zero, when we recognize that case. + */ + public UnifiedReal pow(UnifiedReal expon) { + if (mCrFactor == CR_E) { + if (mRatFactor.equals(BoundedRational.ONE)) { + return expon.exp(); + } else { + UnifiedReal ratPart = new UnifiedReal(mRatFactor).pow(expon); + return expon.exp().multiply(ratPart); + } + } + final BoundedRational expAsBR = expon.boundedRationalValue(); + if (expAsBR != null) { + BigInteger expAsBI = BoundedRational.asBigInteger(expAsBR); + if (expAsBI != null) { + return pow(expAsBI); + } else { + // Check for exponent that is a multiple of a half. + expAsBI = BoundedRational.asBigInteger( + BoundedRational.multiply(BoundedRational.TWO, expAsBR)); + if (expAsBI != null) { + return pow(expAsBI).sqrt(); + } + } + } + // If the exponent were known zero, we would have handled it above. + if (definitelyZero()) { + return ZERO; + } + int sign = signum(DEFAULT_COMPARE_TOLERANCE); + if (sign < 0) { + throw new ArithmeticException("Negative base for pow() with non-integer exponent"); + } + return new UnifiedReal(crValue().ln().multiply(expon.crValue()).exp()); + } + + /** + * Raise the argument to the 16th power. + */ + private static long pow16(int n) { + if (n > 10) { + throw new AssertionError("Unexpected pow16 argument"); + } + long result = n*n; + result *= result; + result *= result; + result *= result; + return result; + } + + /** + * Return the integral log with respect to the given base if it exists, 0 otherwise. + * n is presumed positive. + */ + private static long getIntLog(BigInteger n, int base) { + double nAsDouble = n.doubleValue(); + double approx = Math.log(nAsDouble)/Math.log(base); + // A relatively quick test first. + // Unfortunately, this doesn't help for values to big to fit in a Double. + if (!Double.isInfinite(nAsDouble) && Math.abs(approx - Math.rint(approx)) > 1.0e-6) { + return 0; + } + long result = 0; + BigInteger remaining = n; + BigInteger bigBase = BigInteger.valueOf(base); + BigInteger base16th = null; // base^16, computed lazily + while (n.mod(bigBase).signum() == 0) { + if (Thread.interrupted()) { + throw new CR.AbortedException(); + } + n = n.divide(bigBase); + ++result; + // And try a slightly faster computation for large n: + if (base16th == null) { + base16th = BigInteger.valueOf(pow16(base)); + } + while (n.mod(base16th).signum() == 0) { + n = n.divide(base16th); + result += 16; + } + } + if (n.equals(BigInteger.ONE)) { + return result; + } + return 0; + } + + public UnifiedReal ln() { + if (mCrFactor == CR_E) { + return new UnifiedReal(mRatFactor, CR_ONE).ln().add(ONE); + } + if (isComparable(ZERO)) { + if (signum() <= 0) { + throw new ArithmeticException("log(non-positive)"); + } + int compare1 = compareTo(ONE, DEFAULT_COMPARE_TOLERANCE); + if (compare1 == 0) { + if (definitelyEquals(ONE)) { + return ZERO; + } + } else if (compare1 < 0) { + return inverse().ln().negate(); + } + final BigInteger bi = BoundedRational.asBigInteger(mRatFactor); + if (bi != null) { + if (mCrFactor == CR_ONE) { + // Check for a power of a small integer. We can use sLogs[] to return + // a more useful answer for those. + for (int i = 0; i < sLogs.length; ++i) { + if (sLogs[i] != null) { + long intLog = getIntLog(bi, i); + if (intLog != 0) { + return new UnifiedReal(new BoundedRational(intLog), sLogs[i]); + } + } + } + } else { + // Check for n^k * sqrt(n), for which we can also return a more useful answer. + BoundedRational square = getSquare(mCrFactor); + if (square != null) { + int intSquare = square.intValue(); + if (sLogs[intSquare] != null) { + long intLog = getIntLog(bi, intSquare); + if (intLog != 0) { + BoundedRational nRatFactor = + BoundedRational.add(new BoundedRational(intLog), + BoundedRational.HALF); + if (nRatFactor != null) { + return new UnifiedReal(nRatFactor, sLogs[intSquare]); + } + } + } + } + } + } + } + return new UnifiedReal(crValue().ln()); + } + + public UnifiedReal exp() { + if (definitelyEquals(ZERO)) { + return ONE; + } + if (definitelyEquals(ONE)) { + // Avoid redundant computations, and ensure we recognize all instances as equal. + return E; + } + final BoundedRational crExp = getExp(mCrFactor); + if (crExp != null) { + boolean needSqrt = false; + BoundedRational ratExponent = mRatFactor; + BigInteger asBI = BoundedRational.asBigInteger(ratExponent); + if (asBI == null) { + // check for multiple of one half. + needSqrt = true; + ratExponent = BoundedRational.multiply(ratExponent, BoundedRational.TWO); + } + BoundedRational nRatFactor = BoundedRational.pow(crExp, ratExponent); + if (nRatFactor != null) { + UnifiedReal result = new UnifiedReal(nRatFactor); + if (needSqrt) { + result = result.sqrt(); + } + return result; + } + } + return new UnifiedReal(crValue().exp()); + } + + + /** + * Generalized factorial. + * Compute n * (n - step) * (n - 2 * step) * etc. This can be used to compute factorial a bit + * faster, especially if BigInteger uses sub-quadratic multiplication. + */ + private static BigInteger genFactorial(long n, long step) { + if (n > 4 * step) { + BigInteger prod1 = genFactorial(n, 2 * step); + if (Thread.interrupted()) { + throw new CR.AbortedException(); + } + BigInteger prod2 = genFactorial(n - step, 2 * step); + if (Thread.interrupted()) { + throw new CR.AbortedException(); + } + return prod1.multiply(prod2); + } else { + if (n == 0) { + return BigInteger.ONE; + } + BigInteger res = BigInteger.valueOf(n); + for (long i = n - step; i > 1; i -= step) { + res = res.multiply(BigInteger.valueOf(i)); + } + return res; + } + } + + + /** + * Factorial function. + * Fails if argument is clearly not an integer. + * May round to nearest integer if value is close. + */ + public UnifiedReal fact() { + BigInteger asBI = bigIntegerValue(); + if (asBI == null) { + asBI = crValue().get_appr(0); // Correct if it was an integer. + if (!approxEquals(new UnifiedReal(asBI), DEFAULT_COMPARE_TOLERANCE)) { + throw new ArithmeticException("Non-integral factorial argument"); + } + } + if (asBI.signum() < 0) { + throw new ArithmeticException("Negative factorial argument"); + } + if (asBI.bitLength() > 20) { + // Will fail. LongValue() may not work. Punt now. + throw new ArithmeticException("Factorial argument too big"); + } + BigInteger biResult = genFactorial(asBI.longValue(), 1); + BoundedRational nRatFactor = new BoundedRational(biResult); + return new UnifiedReal(nRatFactor); + } + + /** + * Return the number of decimal digits to the right of the decimal point required to represent + * the argument exactly. + * Return Integer.MAX_VALUE if that's not possible. Never returns a value less than zero, even + * if r is a power of ten. + */ + public int digitsRequired() { + if (mCrFactor == CR_ONE || mRatFactor.signum() == 0) { + return BoundedRational.digitsRequired(mRatFactor); + } else { + return Integer.MAX_VALUE; + } + } + + /** + * Return an upper bound on the number of leading zero bits. + * These are the number of 0 bits + * to the right of the binary point and to the left of the most significant digit. + * Return Integer.MAX_VALUE if we cannot bound it. + */ + public int leadingBinaryZeroes() { + if (isNamed(mCrFactor)) { + // Only ln(2) is smaller than one, and could possibly add one zero bit. + // Adding 3 gives us a somewhat sloppy upper bound. + final int wholeBits = mRatFactor.wholeNumberBits(); + if (wholeBits == Integer.MIN_VALUE) { + return Integer.MAX_VALUE; + } + if (wholeBits >= 3) { + return 0; + } else { + return -wholeBits + 3; + } + } + return Integer.MAX_VALUE; + } + + /** + * Is the number of bits to the left of the decimal point greater than bound? + * The result is inexact: We roughly approximate the whole number bits. + */ + public boolean approxWholeNumberBitsGreaterThan(int bound) { + if (isNamed(mCrFactor)) { + return mRatFactor.wholeNumberBits() > bound; + } else { + return crValue().get_appr(bound - 2).bitLength() > 2; + } + } +} diff --git a/app/src/main/java/com/prime/toolz2/core/math/Util.java b/app/src/main/java/com/prime/toolz2/core/math/Util.java new file mode 100644 index 0000000..bfc75a8 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/math/Util.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2007-2008 Mihai Preda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.prime.toolz2.core.math; + +/** + Contains static helper methods for formatting double values. + */ +class Util { + public static final int LEN_UNLIMITED = 100; + public static final int FLOAT_PRECISION = -1; + + /** + Returns an approximation with no more than maxLen chars. + + This method is not public, it is called through doubleToString, + that's why we can make some assumptions about the format of the string, + such as assuming that the exponent 'E' is upper-case. + + @param str the value to truncate (e.g. "-2.898983455E20") + @param maxLen the maximum number of characters in the returned string + @return a truncation no longer then maxLen (e.g. "-2.8E20" for maxLen=7). + */ + static String sizeTruncate(String str, int maxLen) { + if (maxLen == LEN_UNLIMITED) { + return str; + } + int ePos = str.lastIndexOf('E'); + String tail = (ePos != -1) ? str.substring(ePos) : ""; + int tailLen = tail.length(); + int headLen = str.length() - tailLen; + int maxHeadLen = maxLen - tailLen; + int keepLen = Math.min(headLen, maxHeadLen); + if (keepLen < 1 || (keepLen < 2 && str.length() > 0 && str.charAt(0) == '-')) { + return str; // impossible to truncate + } + int dotPos = str.indexOf('.'); + if (dotPos == -1) { + dotPos = headLen; + } + if (dotPos > keepLen) { + int exponent = (ePos != -1) ? Integer.parseInt(str.substring(ePos + 1)) : 0; + int start = str.charAt(0) == '-' ? 1 : 0; + exponent += dotPos - start - 1; + String newStr = str.substring(0, start+1) + '.' + str.substring(start+1, headLen) + 'E' + exponent; + return sizeTruncate(newStr, maxLen); + + } + return str.substring(0, keepLen) + tail; + } + + /** + Rounds by dropping roundingDigits of double precision + (similar to 'hidden precision digits' on calculators), + and formats to String. + @param v the value to be converted to String + @param roundingDigits the number of 'hidden precision' digits (e.g. 2). + @return a String representation of v + */ + public static String doubleToString(final double v, final int roundingDigits) { + final double absv = Math.abs(v); + final String str = roundingDigits == FLOAT_PRECISION ? Float.toString((float) absv) : Double.toString(absv); + StringBuffer buf = new StringBuffer(str); + int roundingStart = (roundingDigits <= 0 || roundingDigits > 13) ? 17 : (16 - roundingDigits); + + int ePos = str.lastIndexOf('E'); + int exp = (ePos != -1) ? Integer.parseInt(str.substring(ePos + 1)) : 0; + if (ePos != -1) { + buf.setLength(ePos); + } + int len = buf.length(); + + //remove dot + int dotPos; + for (dotPos = 0; dotPos < len && buf.charAt(dotPos) != '.';) { + ++dotPos; + } + exp += dotPos; + if (dotPos < len) { + buf.deleteCharAt(dotPos); + --len; + } + + //round + for (int p = 0; p < len && buf.charAt(p) == '0'; ++p) { + ++roundingStart; + } + + if (roundingStart < len) { + if (buf.charAt(roundingStart) >= '5') { + int p; + for (p = roundingStart-1; p >= 0 && buf.charAt(p)=='9'; --p) { + buf.setCharAt(p, '0'); + } + if (p >= 0) { + buf.setCharAt(p, (char)(buf.charAt(p)+1)); + } else { + buf.insert(0, '1'); + ++roundingStart; + ++exp; + } + } + buf.setLength(roundingStart); + } + + //re-insert dot + if ((exp < -5) || (exp > 10)) { + buf.insert(1, '.'); + --exp; + } else { + for (int i = len; i < exp; ++i) { + buf.append('0'); + } + for (int i = exp; i <= 0; ++i) { + buf.insert(0, '0'); + } + buf.insert((exp<=0)? 1 : exp, '.'); + exp = 0; + } + len = buf.length(); + + //remove trailing dot and 0s. + int tail; + for (tail = len-1; tail >= 0 && buf.charAt(tail) == '0'; --tail) { + buf.deleteCharAt(tail); + } + if (tail >= 0 && buf.charAt(tail) == '.') { + buf.deleteCharAt(tail); + } + + if (exp != 0) { + buf.append('E').append(exp); + } + if (v < 0) { + buf.insert(0, '-'); + } + return buf.toString(); + } + + /** + Renders a real number to a String (for user display). + @param maxLen the maximum total length of the resulting string + @param rounding the number of final digits to round + */ + public static String doubleToString(double x, int maxLen, int rounding) { + return sizeTruncate(doubleToString(x, rounding), maxLen); + } + +} diff --git a/app/src/main/java/com/prime/toolz2/core/math/creals/CR.java b/app/src/main/java/com/prime/toolz2/core/math/creals/CR.java new file mode 100644 index 0000000..b0e01cc --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/math/creals/CR.java @@ -0,0 +1,1646 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * The above license covers additions and changes by AOSP authors. + * The original code is licensed as follows: + */ + +// +// Copyright (c) 1999, Silicon Graphics, Inc. -- ALL RIGHTS RESERVED +// +// Permission is granted free of charge to copy, modify, use and distribute +// this software provided you include the entirety of this notice in all +// copies made. +// +// THIS SOFTWARE IS PROVIDED ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY +// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, +// WARRANTIES THAT THE SUBJECT SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT +// FOR A PARTICULAR PURPOSE OR NON-INFRINGING. SGI ASSUMES NO RISK AS TO THE +// QUALITY AND PERFORMANCE OF THE SOFTWARE. SHOULD THE SOFTWARE PROVE +// DEFECTIVE IN ANY RESPECT, SGI ASSUMES NO COST OR LIABILITY FOR ANY +// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES +// AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY SUBJECT SOFTWARE IS +// AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. +// +// UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING, +// WITHOUT LIMITATION, NEGLIGENCE OR STRICT LIABILITY), CONTRACT, OR +// OTHERWISE, SHALL SGI BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, +// INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER WITH RESPECT TO THE +// SOFTWARE INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK +// STOPPAGE, LOSS OF DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL +// OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SGI SHALL HAVE BEEN INFORMED OF +// THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT +// APPLY TO LIABILITY RESULTING FROM SGI's NEGLIGENCE TO THE EXTENT APPLICABLE +// LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE +// EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT +// EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. +// +// These license terms shall be governed by and construed in accordance with +// the laws of the United States and the State of California as applied to +// agreements entered into and to be performed entirely within California +// between California residents. Any litigation relating to these license +// terms shall be subject to the exclusive jurisdiction of the Federal Courts +// of the Northern District of California (or, absent subject matter +// jurisdiction in such courts, the courts of the State of California), with +// venue lying exclusively in Santa Clara County, California. + +// Copyright (c) 2001-2004, Hewlett-Packard Development Company, L.P. +// +// Permission is granted free of charge to copy, modify, use and distribute +// this software provided you include the entirety of this notice in all +// copies made. +// +// THIS SOFTWARE IS PROVIDED ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY +// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, +// WARRANTIES THAT THE SUBJECT SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT +// FOR A PARTICULAR PURPOSE OR NON-INFRINGING. HEWLETT-PACKARD ASSUMES +// NO RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE. +// SHOULD THE SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, +// HEWLETT-PACKARD ASSUMES NO COST OR LIABILITY FOR ANY +// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES +// AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY SUBJECT SOFTWARE IS +// AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. +// +// UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING, +// WITHOUT LIMITATION, NEGLIGENCE OR STRICT LIABILITY), CONTRACT, OR +// OTHERWISE, SHALL HEWLETT-PACKARD BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, +// INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER WITH RESPECT TO THE +// SOFTWARE INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK +// STOPPAGE, LOSS OF DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL +// OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF HEWLETT-PACKARD SHALL +// HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. +// THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY RESULTING +// FROM HEWLETT-PACKARD's NEGLIGENCE TO THE EXTENT APPLICABLE +// LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE +// EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT +// EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. +// + +// Added valueOf(string, radix), fixed some documentation comments. +// Hans_Boehm@hp.com 1/12/2001 +// Fixed a serious typo in inv_CR(): For negative arguments it produced +// the wrong sign. This affected the sign of divisions. +// Added byteValue and fixed some comments. Hans.Boehm@hp.com 12/17/2002 +// Added toStringFloatRep. Hans.Boehm@hp.com 4/1/2004 +// Added get_appr() synchronization to allow access from multiple threads +// hboehm@google.com 4/25/2014 +// Changed cos() prescaling to avoid logarithmic depth tree. +// hboehm@google.com 6/30/2014 +// Added explicit asin() implementation. Remove one. Add ZERO and ONE and +// make them public. hboehm@google.com 5/21/2015 +// Added Gauss-Legendre PI implementation. Removed two. +// hboehm@google.com 4/12/2016 +// Fix shift operation in doubleValue. That produced incorrect values for +// large negative exponents. +// Don't negate argument and compute inverse for exp(). That causes severe +// performance problems for (-huge).exp() +// hboehm@google.com 8/21/2017 +// Have comparison check for interruption. hboehm@google.com 10/31/2017 +// Fix precision overflow issue in most general compareTo function. +// Fix a couple of unused variable bugs. Notably selector_sign was +// accidentally locally redeclared. (This turns out to be safe but useless.) +// hboehm@google.com 11/20/2018. +// Fix an exception-safety issue in gl_pi_CR.approximate. +// hboehm@google.com 3/3/2019. + +package com.prime.toolz2.core.math.creals; + +import java.math.BigInteger; +import java.util.ArrayList; + +/** +* Constructive real numbers, also known as recursive, or computable reals. +* Each recursive real number is represented as an object that provides an +* approximation function for the real number. +* The approximation function guarantees that the generated approximation +* is accurate to the specified precision. +* Arithmetic operations on constructive reals produce new such objects; +* they typically do not perform any real computation. +* In this sense, arithmetic computations are exact: They produce +* a description which describes the exact answer, and can be used to +* later approximate it to arbitrary precision. +*

+* When approximations are generated, e.g. for output, they are +* accurate to the requested precision; no cumulative rounding errors +* are visible. +* In order to achieve this precision, the approximation function will often +* need to approximate subexpressions to greater precision than was originally +* demanded. Thus the approximation of a constructive real number +* generated through a complex sequence of operations may eventually require +* evaluation to very high precision. This usually makes such computations +* prohibitively expensive for large numerical problems. +* But it is perfectly appropriate for use in a desk calculator, +* for small numerical problems, for the evaluation of expressions +* computated by a symbolic algebra system, for testing of accuracy claims +* for floating point code on small inputs, or the like. +*

+* We expect that the vast majority of uses will ignore the particular +* implementation, and the member functons approximate +* and get_appr. Such applications will treat CR as +* a conventional numerical type, with an interface modelled on +* java.math.BigInteger. No subclasses of CR +* will be explicitly mentioned by such a program. +*

+* All standard arithmetic operations, as well as a few algebraic +* and transcendal functions are provided. Constructive reals are +* immutable; thus all of these operations return a new constructive real. +*

+* A few uses will require explicit construction of approximation functions. +* The requires the construction of a subclass of CR with +* an overridden approximate function. Note that approximate +* should only be defined, but never called. get_appr +* provides the same functionality, but adds the caching necessary to obtain +* reasonable performance. +*

+* Any operation may throw com.hp.creals.AbortedException if the thread +* in which it is executing is interrupted. (InterruptedException +* cannot be used for this purpose, since CR inherits from Number.) +*

+* Any operation may also throw com.hp.creals.PrecisionOverflowException +* If the precision request generated during any subcalculation overflows +* a 28-bit integer. (This should be extremely unlikely, except as an +* outcome of a division by zero, or other erroneous computation.) +* +*/ +public abstract class CR extends Number { + // CR is the basic representation of a number. + // Abstractly this is a function for computing an approximation + // plus the current best approximation. + // We could do without the latter, but that would + // be atrociously slow. + +/** + * Indicates a constructive real operation was interrupted. + * Most constructive real operations may throw such an exception. + * This is unchecked, since Number methods may not raise checked + * exceptions. +*/ +public static class AbortedException extends RuntimeException { + public AbortedException() { super(); } + public AbortedException(String s) { super(s); } +} + +/** + * Indicates that the number of bits of precision requested by + * a computation on constructive reals required more than 28 bits, + * and was thus in danger of overflowing an int. + * This is likely to be a symptom of a diverging computation, + * e.g. division by zero. +*/ +public static class PrecisionOverflowException extends RuntimeException { + public PrecisionOverflowException() { super(); } + public PrecisionOverflowException(String s) { super(s); } +} + + // First some frequently used constants, so we don't have to + // recompute these all over the place. + static final BigInteger big0 = BigInteger.ZERO; + static final BigInteger big1 = BigInteger.ONE; + static final BigInteger bigm1 = BigInteger.valueOf(-1); + static final BigInteger big2 = BigInteger.valueOf(2); + static final BigInteger bigm2 = BigInteger.valueOf(-2); + static final BigInteger big3 = BigInteger.valueOf(3); + static final BigInteger big6 = BigInteger.valueOf(6); + static final BigInteger big8 = BigInteger.valueOf(8); + static final BigInteger big10 = BigInteger.TEN; + static final BigInteger big750 = BigInteger.valueOf(750); + static final BigInteger bigm750 = BigInteger.valueOf(-750); + +/** +* Setting this to true requests that all computations be aborted by +* throwing AbortedException. Must be rest to false before any further +* computation. Ideally Thread.interrupt() should be used instead, but +* that doesn't appear to be consistently supported by browser VMs. +*/ +public volatile static boolean please_stop = false; + +/** +* Must be defined in subclasses of CR. +* Most users can ignore the existence of this method, and will +* not ever need to define a CR subclass. +* Returns value / 2 ** precision rounded to an integer. +* The error in the result is strictly < 1. +* Informally, approximate(n) gives a scaled approximation +* accurate to 2**n. +* Implementations may safely assume that precision is +* at least a factor of 8 away from overflow. +* Called only with the lock on the CR object +* already held. +*/ + protected abstract BigInteger approximate(int precision); + transient int min_prec; + // The smallest precision value with which the above + // has been called. + transient BigInteger max_appr; + // The scaled approximation corresponding to min_prec. + transient boolean appr_valid = false; + // min_prec and max_val are valid. + + // Helper functions + static int bound_log2(int n) { + int abs_n = Math.abs(n); + return (int)Math.ceil(Math.log((double)(abs_n + 1))/Math.log(2.0)); + } + // Check that a precision is at least a factor of 8 away from + // overflowng the integer used to hold a precision spec. + // We generally perform this check early on, and then convince + // ourselves that none of the operations performed on precisions + // inside a function can generate an overflow. + static void check_prec(int n) { + int high = n >> 28; + // if n is not in danger of overflowing, then the 4 high order + // bits should be identical. Thus high is either 0 or -1. + // The rest of this is to test for either of those in a way + // that should be as cheap as possible. + int high_shifted = n >> 29; + if (0 != (high ^ high_shifted)) { + throw new PrecisionOverflowException(); + } + } + +/** +* The constructive real number corresponding to a +* BigInteger. +*/ + public static CR valueOf(BigInteger n) { + return new int_CR(n); + } + +/** +* The constructive real number corresponding to a +* Java int. +*/ + public static CR valueOf(int n) { + return valueOf(BigInteger.valueOf(n)); + } + +/** +* The constructive real number corresponding to a +* Java long. +*/ + public static CR valueOf(long n) { + return valueOf(BigInteger.valueOf(n)); + } + +/** +* The constructive real number corresponding to a +* Java double. +* The result is undefined if argument is infinite or NaN. +*/ + public static CR valueOf(double n) { + if (Double.isNaN(n)) throw new ArithmeticException("Nan argument"); + if (Double.isInfinite(n)) { + throw new ArithmeticException("Infinite argument"); + } + boolean negative = (n < 0.0); + long bits = Double.doubleToLongBits(Math.abs(n)); + long mantissa = (bits & 0xfffffffffffffL); + int biased_exp = (int)(bits >> 52); + int exp = biased_exp - 1075; + if (biased_exp != 0) { + mantissa += (1L << 52); + } else { + mantissa <<= 1; + } + CR result = valueOf(mantissa).shiftLeft(exp); + if (negative) result = result.negate(); + return result; + } + +/** +* The constructive real number corresponding to a +* Java float. +* The result is undefined if argument is infinite or NaN. +*/ + public static CR valueOf(float n) { + return valueOf((double) n); + } + + public static CR ZERO = valueOf(0); + public static CR ONE = valueOf(1); + + // Multiply k by 2**n. + static BigInteger shift(BigInteger k, int n) { + if (n == 0) return k; + if (n < 0) return k.shiftRight(-n); + return k.shiftLeft(n); + } + + // Multiply by 2**n, rounding result + static BigInteger scale(BigInteger k, int n) { + if (n >= 0) { + return k.shiftLeft(n); + } else { + BigInteger adj_k = shift(k, n+1).add(big1); + return adj_k.shiftRight(1); + } + } + + // Identical to approximate(), but maintain and update cache. +/** +* Returns value / 2 ** prec rounded to an integer. +* The error in the result is strictly < 1. +* Produces the same answer as approximate, but uses and +* maintains a cached approximation. +* Normally not overridden, and called only from approximate +* methods in subclasses. Not needed if the provided operations +* on constructive reals suffice. +*/ + public synchronized BigInteger get_appr(int precision) { + check_prec(precision); + if (appr_valid && precision >= min_prec) { + return scale(max_appr, min_prec - precision); + } else { + BigInteger result = approximate(precision); + min_prec = precision; + max_appr = result; + appr_valid = true; + return result; + } + } + + // Return the position of the msd. + // If x.msd() == n then + // 2**(n-1) < abs(x) < 2**(n+1) + // This initial version assumes that max_appr is valid + // and sufficiently removed from zero + // that the msd is determined. + int known_msd() { + int first_digit; + int length; + if (max_appr.signum() >= 0) { + length = max_appr.bitLength(); + } else { + length = max_appr.negate().bitLength(); + } + first_digit = min_prec + length - 1; + return first_digit; + } + + // This version may return Integer.MIN_VALUE if the correct + // answer is < n. + int msd(int n) { + if (!appr_valid || + max_appr.compareTo(big1) <= 0 + && max_appr.compareTo(bigm1) >= 0) { + get_appr(n - 1); + if (max_appr.abs().compareTo(big1) <= 0) { + // msd could still be arbitrarily far to the right. + return Integer.MIN_VALUE; + } + } + return known_msd(); + } + + + // Functionally equivalent, but iteratively evaluates to higher + // precision. + int iter_msd(int n) + { + int prec = 0; + + for (;prec > n + 30; prec = (prec * 3)/2 - 16) { + int msd = msd(prec); + if (msd != Integer.MIN_VALUE) return msd; + check_prec(prec); + if (Thread.interrupted() || please_stop) { + throw new AbortedException(); + } + } + return msd(n); + } + + // This version returns a correct answer eventually, except + // that it loops forever (or throws an exception when the + // requested precision overflows) if this constructive real is zero. + int msd() { + return iter_msd(Integer.MIN_VALUE); + } + + // A helper function for toString. + // Generate a String containing n zeroes. + private static String zeroes(int n) { + char[] a = new char[n]; + for (int i = 0; i < n; ++i) { + a[i] = '0'; + } + return new String(a); + } + + // Natural log of 2. Needed for some prescaling below. + // ln(2) = 7ln(10/9) - 2ln(25/24) + 3ln(81/80) + CR simple_ln() { + return new prescaled_ln_CR(this.subtract(ONE)); + } + static CR ten_ninths = valueOf(10).divide(valueOf(9)); + static CR twentyfive_twentyfourths = valueOf(25).divide(valueOf(24)); + static CR eightyone_eightyeths = valueOf(81).divide(valueOf(80)); + static CR ln2_1 = valueOf(7).multiply(ten_ninths.simple_ln()); + static CR ln2_2 = + valueOf(2).multiply(twentyfive_twentyfourths.simple_ln()); + static CR ln2_3 = valueOf(3).multiply(eightyone_eightyeths.simple_ln()); + static CR ln2 = ln2_1.subtract(ln2_2).add(ln2_3); + + // Atan of integer reciprocal. Used for atan_PI. Could perhaps be made + // public. + static CR atan_reciprocal(int n) { + return new integral_atan_CR(n); + } + // Other constants used for PI computation. + static CR four = valueOf(4); + + // Public operations. +/** +* Return 0 if x = y to within the indicated tolerance, +* -1 if x < y, and +1 if x > y. If x and y are indeed +* equal, it is guaranteed that 0 will be returned. If +* they differ by less than the tolerance, anything +* may happen. The tolerance allowed is +* the maximum of (abs(this)+abs(x))*(2**r) and 2**a +* @param x The other constructive real +* @param r Relative tolerance in bits +* @param a Absolute tolerance in bits +*/ + public int compareTo(CR x, int r, int a) { + int this_msd = iter_msd(a); + int x_msd = x.iter_msd(this_msd > a? this_msd : a); + int max_msd = (x_msd > this_msd? x_msd : this_msd); + if (max_msd == Integer.MIN_VALUE) { + return 0; + } + check_prec(r); + int rel = max_msd + r; + int abs_prec = (rel > a? rel : a); + return compareTo(x, abs_prec); + } + +/** +* Approximate comparison with only an absolute tolerance. +* Identical to the three argument version, but without a relative +* tolerance. +* Result is 0 if both constructive reals are equal, indeterminate +* if they differ by less than 2**a. +* +* @param x The other constructive real +* @param a Absolute tolerance in bits +*/ + public int compareTo(CR x, int a) { + int needed_prec = a - 1; + BigInteger this_appr = get_appr(needed_prec); + BigInteger x_appr = x.get_appr(needed_prec); + int comp1 = this_appr.compareTo(x_appr.add(big1)); + if (comp1 > 0) return 1; + int comp2 = this_appr.compareTo(x_appr.subtract(big1)); + if (comp2 < 0) return -1; + return 0; + } + +/** +* Return -1 if this < x, or +1 if this > x. +* Should be called only if this != x. +* If this == x, this will not terminate correctly; typically it +* will run until it exhausts memory. +* If the two constructive reals may be equal, the two or 3 argument +* version of compareTo should be used. +*/ + public int compareTo(CR x) { + for (int a = -20; ; a *= 2) { + check_prec(a); + int result = compareTo(x, a); + if (0 != result) return result; + if (Thread.interrupted() || please_stop) { + throw new AbortedException(); + } + } + } + +/** +* Equivalent to compareTo(CR.valueOf(0), a) +*/ + public int signum(int a) { + if (appr_valid) { + int quick_try = max_appr.signum(); + if (0 != quick_try) return quick_try; + } + int needed_prec = a - 1; + BigInteger this_appr = get_appr(needed_prec); + return this_appr.signum(); + } + +/** +* Return -1 if negative, +1 if positive. +* Should be called only if this != 0. +* In the 0 case, this will not terminate correctly; typically it +* will run until it exhausts memory. +* If the two constructive reals may be equal, the one or two argument +* version of signum should be used. +*/ + public int signum() { + for (int a = -20; ; a *= 2) { + check_prec(a); + int result = signum(a); + if (0 != result) return result; + if (Thread.interrupted() || please_stop) { + throw new AbortedException(); + } + } + } + +/** +* Return the constructive real number corresponding to the given +* textual representation and radix. +* +* @param s [-] digit* [. digit*] +* @param radix +*/ + + public static CR valueOf(String s, int radix) + throws NumberFormatException { + int len = s.length(); + int start_pos = 0, point_pos; + String fraction; + while (s.charAt(start_pos) == ' ') ++start_pos; + while (s.charAt(len - 1) == ' ') --len; + point_pos = s.indexOf('.', start_pos); + if (point_pos == -1) { + point_pos = len; + fraction = "0"; + } else { + fraction = s.substring(point_pos + 1, len); + } + String whole = s.substring(start_pos, point_pos); + BigInteger scaled_result = new BigInteger(whole + fraction, radix); + BigInteger divisor = BigInteger.valueOf(radix).pow(fraction.length()); + return CR.valueOf(scaled_result).divide(CR.valueOf(divisor)); + } + +/** +* Return a textual representation accurate to n places +* to the right of the decimal point. n must be nonnegative. +* +* @param n Number of digits (>= 0) included to the right of decimal point +* @param radix Base ( >= 2, <= 16) for the resulting representation. +*/ + public String toString(int n, int radix) { + CR scaled_CR; + if (16 == radix) { + scaled_CR = shiftLeft(4*n); + } else { + BigInteger scale_factor = BigInteger.valueOf(radix).pow(n); + scaled_CR = multiply(new int_CR(scale_factor)); + } + BigInteger scaled_int = scaled_CR.get_appr(0); + String scaled_string = scaled_int.abs().toString(radix); + String result; + if (0 == n) { + result = scaled_string; + } else { + int len = scaled_string.length(); + if (len <= n) { + // Add sufficient leading zeroes + String z = zeroes(n + 1 - len); + scaled_string = z + scaled_string; + len = n + 1; + } + String whole = scaled_string.substring(0, len - n); + String fraction = scaled_string.substring(len - n); + result = whole + "." + fraction; + } + if (scaled_int.signum() < 0) { + result = "-" + result; + } + return result; + } + + +/** +* Equivalent to toString(n,10) +* +* @param n Number of digits included to the right of decimal point +*/ + public String toString(int n) { + return toString(n, 10); + } + +/** +* Equivalent to toString(10, 10) +*/ + public String toString() { + return toString(10); + } + + static double doubleLog2 = Math.log(2.0); +/** +* Return a textual scientific notation representation accurate +* to n places to the right of the decimal point. +* n must be nonnegative. A value smaller than +* radix**-m may be displayed as 0. +* The mantissa component of the result is either "0" +* or exactly n digits long. The sign +* component is zero exactly when the mantissa is "0". +* +* @param n Number of digits (> 0) included to the right of decimal point. +* @param radix Base ( ≥ 2, ≤ 16) for the resulting representation. +* @param m Precision used to distinguish number from zero. +* Expressed as a power of m. +*/ + public StringFloatRep toStringFloatRep(int n, int radix, int m) { + if (n <= 0) throw new ArithmeticException("Bad precision argument"); + double log2_radix = Math.log((double)radix)/doubleLog2; + BigInteger big_radix = BigInteger.valueOf(radix); + long long_msd_prec = (long)(log2_radix * (double)m); + if (long_msd_prec > (long)Integer.MAX_VALUE + || long_msd_prec < (long)Integer.MIN_VALUE) + throw new PrecisionOverflowException(); + int msd_prec = (int)long_msd_prec; + check_prec(msd_prec); + int msd = iter_msd(msd_prec - 2); + if (msd == Integer.MIN_VALUE) + return new StringFloatRep(0, "0", radix, 0); + int exponent = (int)Math.ceil((double)msd / log2_radix); + // Guess for the exponent. Try to get it usually right. + int scale_exp = exponent - n; + CR scale; + if (scale_exp > 0) { + scale = CR.valueOf(big_radix.pow(scale_exp)).inverse(); + } else { + scale = CR.valueOf(big_radix.pow(-scale_exp)); + } + CR scaled_res = multiply(scale); + BigInteger scaled_int = scaled_res.get_appr(0); + int sign = scaled_int.signum(); + String scaled_string = scaled_int.abs().toString(radix); + while (scaled_string.length() < n) { + // exponent was too large. Adjust. + scaled_res = scaled_res.multiply(CR.valueOf(big_radix)); + exponent -= 1; + scaled_int = scaled_res.get_appr(0); + sign = scaled_int.signum(); + scaled_string = scaled_int.abs().toString(radix); + } + if (scaled_string.length() > n) { + // exponent was too small. Adjust by truncating. + exponent += (scaled_string.length() - n); + scaled_string = scaled_string.substring(0, n); + } + return new StringFloatRep(sign, scaled_string, radix, exponent); + } + +/** +* Return a BigInteger which differs by less than one from the +* constructive real. +*/ + public BigInteger BigIntegerValue() { + return get_appr(0); + } + +/** +* Return an int which differs by less than one from the +* constructive real. Behavior on overflow is undefined. +*/ + public int intValue() { + return BigIntegerValue().intValue(); + } + +/** +* Return an int which differs by less than one from the +* constructive real. Behavior on overflow is undefined. +*/ + public byte byteValue() { + return BigIntegerValue().byteValue(); + } + +/** +* Return a long which differs by less than one from the +* constructive real. Behavior on overflow is undefined. +*/ + public long longValue() { + return BigIntegerValue().longValue(); + } + +/** +* Return a double which differs by less than one in the least +* represented bit from the constructive real. +* (We're in fact closer to round-to-nearest than that, but we can't and +* don't promise correct rounding.) +*/ + public double doubleValue() { + int my_msd = iter_msd(-1080 /* slightly > exp. range */); + if (Integer.MIN_VALUE == my_msd) return 0.0; + int needed_prec = my_msd - 60; + double scaled_int = get_appr(needed_prec).doubleValue(); + boolean may_underflow = (needed_prec < -1000); + long scaled_int_rep = Double.doubleToLongBits(scaled_int); + long exp_adj = may_underflow? needed_prec + 96 : needed_prec; + long orig_exp = (scaled_int_rep >> 52) & 0x7ff; + if (((orig_exp + exp_adj) & ~0x7ff) != 0) { + // Original unbiased exponent is > 50. Exp_adj > -1050. + // Thus this can overflow the 11 bit exponent only if the result + // itself overflows. + if (scaled_int < 0.0) { + return Double.NEGATIVE_INFINITY; + } else { + return Double.POSITIVE_INFINITY; + } + } + scaled_int_rep += exp_adj << 52; + double result = Double.longBitsToDouble(scaled_int_rep); + if (may_underflow) { + double two48 = (double)(1L << 48); + return result/two48/two48; + } else { + return result; + } + } + +/** +* Return a float which differs by less than one in the least +* represented bit from the constructive real. +*/ + public float floatValue() { + return (float)doubleValue(); + // Note that double-rounding is not a problem here, since we + // cannot, and do not, guarantee correct rounding. + } + +/** +* Add two constructive reals. +*/ + public CR add(CR x) { + return new add_CR(this, x); + } + +/** +* Multiply a constructive real by 2**n. +* @param n shift count, may be negative +*/ + public CR shiftLeft(int n) { + check_prec(n); + return new shifted_CR(this, n); + } + +/** +* Multiply a constructive real by 2**(-n). +* @param n shift count, may be negative +*/ + public CR shiftRight(int n) { + check_prec(n); + return new shifted_CR(this, -n); + } + +/** +* Produce a constructive real equivalent to the original, assuming +* the original was an integer. Undefined results if the original +* was not an integer. Prevents evaluation of digits to the right +* of the decimal point, and may thus improve performance. +*/ + public CR assumeInt() { + return new assumed_int_CR(this); + } + +/** +* The additive inverse of a constructive real +*/ + public CR negate() { + return new neg_CR(this); + } + +/** +* The difference between two constructive reals +*/ + public CR subtract(CR x) { + return new add_CR(this, x.negate()); + } + +/** +* The product of two constructive reals +*/ + public CR multiply(CR x) { + return new mult_CR(this, x); + } + +/** +* The multiplicative inverse of a constructive real. +* x.inverse() is equivalent to CR.valueOf(1).divide(x). +*/ + public CR inverse() { + return new inv_CR(this); + } + +/** +* The quotient of two constructive reals. +*/ + public CR divide(CR x) { + return new mult_CR(this, x.inverse()); + } + +/** +* The real number x if this < 0, or y otherwise. +* Requires x = y if this = 0. +* Since comparisons may diverge, this is often +* a useful alternative to conditionals. +*/ + public CR select(CR x, CR y) { + return new select_CR(this, x, y); + } + +/** +* The maximum of two constructive reals. +*/ + public CR max(CR x) { + return subtract(x).select(x, this); + } + +/** +* The minimum of two constructive reals. +*/ + public CR min(CR x) { + return subtract(x).select(this, x); + } + +/** +* The absolute value of a constructive reals. +* Note that this cannot be written as a conditional. +*/ + public CR abs() { + return select(negate(), this); + } + +/** +* The exponential function, that is e**this. +*/ + public CR exp() { + final int low_prec = -10; + BigInteger rough_appr = get_appr(low_prec); + // Handle negative arguments directly; negating and computing inverse + // can be very expensive. + if (rough_appr.compareTo(big2) > 0 || rough_appr.compareTo(bigm2) < 0) { + CR square_root = shiftRight(1).exp(); + return square_root.multiply(square_root); + } else { + return new prescaled_exp_CR(this); + } + } + +/** +* The ratio of a circle's circumference to its diameter. +*/ + public static CR PI = new gl_pi_CR(); + + // Our old PI implementation. Keep this around for now to allow checking. + // This implementation may also be faster for BigInteger implementations + // that support only quadratic multiplication, but exhibit high performance + // for small computations. (The standard Android 6 implementation supports + // subquadratic multiplication, but has high constant overhead.) Many other + // atan-based formulas are possible, but based on superficial + // experimentation, this is roughly as good as the more complex formulas. + public static CR atan_PI = four.multiply(four.multiply(atan_reciprocal(5)) + .subtract(atan_reciprocal(239))); + // pi/4 = 4*atan(1/5) - atan(1/239) + static CR half_pi = PI.shiftRight(1); + +/** +* The trigonometric cosine function. +*/ + public CR cos() { + BigInteger halfpi_multiples = divide(PI).get_appr(-1); + BigInteger abs_halfpi_multiples = halfpi_multiples.abs(); + if (abs_halfpi_multiples.compareTo(big2) >= 0) { + // Subtract multiples of PI + BigInteger pi_multiples = scale(halfpi_multiples, -1); + CR adjustment = PI.multiply(CR.valueOf(pi_multiples)); + if (pi_multiples.and(big1).signum() != 0) { + return subtract(adjustment).cos().negate(); + } else { + return subtract(adjustment).cos(); + } + } else if (get_appr(-1).abs().compareTo(big2) >= 0) { + // Scale further with double angle formula + CR cos_half = shiftRight(1).cos(); + return cos_half.multiply(cos_half).shiftLeft(1).subtract(ONE); + } else { + return new prescaled_cos_CR(this); + } + } + +/** +* The trigonometric sine function. +*/ + public CR sin() { + return half_pi.subtract(this).cos(); + } + +/** +* The trignonometric arc (inverse) sine function. +*/ + public CR asin() { + BigInteger rough_appr = get_appr(-10); + if (rough_appr.compareTo(big750) /* 1/sqrt(2) + a bit */ > 0){ + CR new_arg = ONE.subtract(multiply(this)).sqrt(); + return new_arg.acos(); + } else if (rough_appr.compareTo(bigm750) < 0) { + return negate().asin().negate(); + } else { + return new prescaled_asin_CR(this); + } + } + +/** +* The trignonometric arc (inverse) cosine function. +*/ + public CR acos() { + return half_pi.subtract(asin()); + } + + static final BigInteger low_ln_limit = big8; /* sixteenths, i.e. 1/2 */ + static final BigInteger high_ln_limit = + BigInteger.valueOf(16 + 8 /* 1.5 */); + static final BigInteger scaled_4 = + BigInteger.valueOf(4*16); + +/** +* The natural (base e) logarithm. +*/ + public CR ln() { + final int low_prec = -4; + BigInteger rough_appr = get_appr(low_prec); /* In sixteenths */ + if (rough_appr.compareTo(big0) < 0) { + throw new ArithmeticException("ln(negative)"); + } + if (rough_appr.compareTo(low_ln_limit) <= 0) { + return inverse().ln().negate(); + } + if (rough_appr.compareTo(high_ln_limit) >= 0) { + if (rough_appr.compareTo(scaled_4) <= 0) { + CR quarter = sqrt().sqrt().ln(); + return quarter.shiftLeft(2); + } else { + int extra_bits = rough_appr.bitLength() - 3; + CR scaled_result = shiftRight(extra_bits).ln(); + return scaled_result.add(CR.valueOf(extra_bits).multiply(ln2)); + } + } + return simple_ln(); + } + +/** +* The square root of a constructive real. +*/ + public CR sqrt() { + return new sqrt_CR(this); + } + +} // end of CR + + +// +// A specialization of CR for cases in which approximate() calls +// to increase evaluation precision are somewhat expensive. +// If we need to (re)evaluate, we speculatively evaluate to slightly +// higher precision, miminimizing reevaluations. +// Note that this requires any arguments to be evaluated to higher +// precision than absolutely necessary. It can thus potentially +// result in lots of wasted effort, and should be used judiciously. +// This assumes that the order of magnitude of the number is roughly one. +// +abstract class slow_CR extends CR { + static int max_prec = -64; + static int prec_incr = 32; + public synchronized BigInteger get_appr(int precision) { + check_prec(precision); + if (appr_valid && precision >= min_prec) { + return scale(max_appr, min_prec - precision); + } else { + int eval_prec = (precision >= max_prec? max_prec : + (precision - prec_incr + 1) & ~(prec_incr - 1)); + BigInteger result = approximate(eval_prec); + min_prec = eval_prec; + max_appr = result; + appr_valid = true; + return scale(result, eval_prec - precision); + } + } +} + + +// Representation of an integer constant. Private. +class int_CR extends CR { + BigInteger value; + int_CR(BigInteger n) { + value = n; + } + protected BigInteger approximate(int p) { + return scale(value, -p) ; + } +} + +// Representation of a number that may not have been completely +// evaluated, but is assumed to be an integer. Hence we never +// evaluate beyond the decimal point. +class assumed_int_CR extends CR { + CR value; + assumed_int_CR(CR x) { + value = x; + } + protected BigInteger approximate(int p) { + if (p >= 0) { + return value.get_appr(p); + } else { + return scale(value.get_appr(0), -p) ; + } + } +} + +// Representation of the sum of 2 constructive reals. Private. +class add_CR extends CR { + CR op1; + CR op2; + add_CR(CR x, CR y) { + op1 = x; + op2 = y; + } + protected BigInteger approximate(int p) { + // Args need to be evaluated so that each error is < 1/4 ulp. + // Rounding error from the cale call is <= 1/2 ulp, so that + // final error is < 1 ulp. + return scale(op1.get_appr(p-2).add(op2.get_appr(p-2)), -2); + } +} + +// Representation of a CR multiplied by 2**n +class shifted_CR extends CR { + CR op; + int count; + shifted_CR(CR x, int n) { + op = x; + count = n; + } + protected BigInteger approximate(int p) { + return op.get_appr(p - count); + } +} + +// Representation of the negation of a constructive real. Private. +class neg_CR extends CR { + CR op; + neg_CR(CR x) { + op = x; + } + protected BigInteger approximate(int p) { + return op.get_appr(p).negate(); + } +} + +// Representation of: +// op1 if selector < 0 +// op2 if selector >= 0 +// Assumes x = y if s = 0 +class select_CR extends CR { + CR selector; + int selector_sign; + CR op1; + CR op2; + select_CR(CR s, CR x, CR y) { + selector = s; + selector_sign = selector.get_appr(-20).signum(); + op1 = x; + op2 = y; + } + protected BigInteger approximate(int p) { + if (selector_sign < 0) return op1.get_appr(p); + if (selector_sign > 0) return op2.get_appr(p); + BigInteger op1_appr = op1.get_appr(p-1); + BigInteger op2_appr = op2.get_appr(p-1); + BigInteger diff = op1_appr.subtract(op2_appr).abs(); + if (diff.compareTo(big1) <= 0) { + // close enough; use either + return scale(op1_appr, -1); + } + // op1 and op2 are different; selector != 0; + // safe to get sign of selector. + if (selector.signum() < 0) { + selector_sign = -1; + return scale(op1_appr, -1); + } else { + selector_sign = 1; + return scale(op2_appr, -1); + } + } +} + +// Representation of the product of 2 constructive reals. Private. +class mult_CR extends CR { + CR op1; + CR op2; + mult_CR(CR x, CR y) { + op1 = x; + op2 = y; + } + protected BigInteger approximate(int p) { + int half_prec = (p >> 1) - 1; + int msd_op1 = op1.msd(half_prec); + int msd_op2; + + if (msd_op1 == Integer.MIN_VALUE) { + msd_op2 = op2.msd(half_prec); + if (msd_op2 == Integer.MIN_VALUE) { + // Product is small enough that zero will do as an + // approximation. + return big0; + } else { + // Swap them, so the larger operand (in absolute value) + // is first. + CR tmp; + tmp = op1; + op1 = op2; + op2 = tmp; + msd_op1 = msd_op2; + } + } + // msd_op1 is valid at this point. + int prec2 = p - msd_op1 - 3; // Precision needed for op2. + // The appr. error is multiplied by at most + // 2 ** (msd_op1 + 1) + // Thus each approximation contributes 1/4 ulp + // to the rounding error, and the final rounding adds + // another 1/2 ulp. + BigInteger appr2 = op2.get_appr(prec2); + if (appr2.signum() == 0) return big0; + msd_op2 = op2.known_msd(); + int prec1 = p - msd_op2 - 3; // Precision needed for op1. + BigInteger appr1 = op1.get_appr(prec1); + int scale_digits = prec1 + prec2 - p; + return scale(appr1.multiply(appr2), scale_digits); + } +} + +// Representation of the multiplicative inverse of a constructive +// real. Private. Should use Newton iteration to refine estimates. +class inv_CR extends CR { + CR op; + inv_CR(CR x) { op = x; } + protected BigInteger approximate(int p) { + int msd = op.msd(); + int inv_msd = 1 - msd; + int digits_needed = inv_msd - p + 3; + // Number of SIGNIFICANT digits needed for + // argument, excl. msd position, which may + // be fictitious, since msd routine can be + // off by 1. Roughly 1 extra digit is + // needed since the relative error is the + // same in the argument and result, but + // this isn't quite the same as the number + // of significant digits. Another digit + // is needed to compensate for slop in the + // calculation. + // One further bit is required, since the + // final rounding introduces a 0.5 ulp + // error. + int prec_needed = msd - digits_needed; + int log_scale_factor = -p - prec_needed; + if (log_scale_factor < 0) return big0; + BigInteger dividend = big1.shiftLeft(log_scale_factor); + BigInteger scaled_divisor = op.get_appr(prec_needed); + BigInteger abs_scaled_divisor = scaled_divisor.abs(); + BigInteger adj_dividend = dividend.add( + abs_scaled_divisor.shiftRight(1)); + // Adjustment so that final result is rounded. + BigInteger result = adj_dividend.divide(abs_scaled_divisor); + if (scaled_divisor.signum() < 0) { + return result.negate(); + } else { + return result; + } + } +} + + +// Representation of the exponential of a constructive real. Private. +// Uses a Taylor series expansion. Assumes |x| < 1/2. +// Note: this is known to be a bad algorithm for +// floating point. Unfortunately, other alternatives +// appear to require precomputed information. +class prescaled_exp_CR extends CR { + CR op; + prescaled_exp_CR(CR x) { op = x; } + protected BigInteger approximate(int p) { + if (p >= 1) return big0; + int iterations_needed = -p/2 + 2; // conservative estimate > 0. + // Claim: each intermediate term is accurate + // to 2*2^calc_precision. + // Total rounding error in series computation is + // 2*iterations_needed*2^calc_precision, + // exclusive of error in op. + int calc_precision = p - bound_log2(2*iterations_needed) + - 4; // for error in op, truncation. + int op_prec = p - 3; + BigInteger op_appr = op.get_appr(op_prec); + // Error in argument results in error of < 3/8 ulp. + // Sum of term eval. rounding error is < 1/16 ulp. + // Series truncation error < 1/16 ulp. + // Final rounding error is <= 1/2 ulp. + // Thus final error is < 1 ulp. + BigInteger scaled_1 = big1.shiftLeft(-calc_precision); + BigInteger current_term = scaled_1; + BigInteger current_sum = scaled_1; + int n = 0; + BigInteger max_trunc_error = + big1.shiftLeft(p - 4 - calc_precision); + while (current_term.abs().compareTo(max_trunc_error) >= 0) { + if (Thread.interrupted() || please_stop) throw new AbortedException(); + n += 1; + /* current_term = current_term * op / n */ + current_term = scale(current_term.multiply(op_appr), op_prec); + current_term = current_term.divide(BigInteger.valueOf(n)); + current_sum = current_sum.add(current_term); + } + return scale(current_sum, calc_precision - p); + } +} + +// Representation of the cosine of a constructive real. Private. +// Uses a Taylor series expansion. Assumes |x| < 1. +class prescaled_cos_CR extends slow_CR { + CR op; + prescaled_cos_CR(CR x) { + op = x; + } + protected BigInteger approximate(int p) { + if (p >= 1) return big0; + int iterations_needed = -p/2 + 4; // conservative estimate > 0. + // Claim: each intermediate term is accurate + // to 2*2^calc_precision. + // Total rounding error in series computation is + // 2*iterations_needed*2^calc_precision, + // exclusive of error in op. + int calc_precision = p - bound_log2(2*iterations_needed) + - 4; // for error in op, truncation. + int op_prec = p - 2; + BigInteger op_appr = op.get_appr(op_prec); + // Error in argument results in error of < 1/4 ulp. + // Cumulative arithmetic rounding error is < 1/16 ulp. + // Series truncation error < 1/16 ulp. + // Final rounding error is <= 1/2 ulp. + // Thus final error is < 1 ulp. + BigInteger current_term; + int n; + BigInteger max_trunc_error = + big1.shiftLeft(p - 4 - calc_precision); + n = 0; + current_term = big1.shiftLeft(-calc_precision); + BigInteger current_sum = current_term; + while (current_term.abs().compareTo(max_trunc_error) >= 0) { + if (Thread.interrupted() || please_stop) throw new AbortedException(); + n += 2; + /* current_term = - current_term * op * op / n * (n - 1) */ + current_term = scale(current_term.multiply(op_appr), op_prec); + current_term = scale(current_term.multiply(op_appr), op_prec); + BigInteger divisor = BigInteger.valueOf(-n) + .multiply(BigInteger.valueOf(n-1)); + current_term = current_term.divide(divisor); + current_sum = current_sum.add(current_term); + } + return scale(current_sum, calc_precision - p); + } +} + +// The constructive real atan(1/n), where n is a small integer +// > base. +// This gives a simple and moderately fast way to compute PI. +class integral_atan_CR extends slow_CR { + int op; + integral_atan_CR(int x) { op = x; } + protected BigInteger approximate(int p) { + if (p >= 1) return big0; + int iterations_needed = -p/2 + 2; // conservative estimate > 0. + // Claim: each intermediate term is accurate + // to 2*base^calc_precision. + // Total rounding error in series computation is + // 2*iterations_needed*base^calc_precision, + // exclusive of error in op. + int calc_precision = p - bound_log2(2*iterations_needed) + - 2; // for error in op, truncation. + // Error in argument results in error of < 3/8 ulp. + // Cumulative arithmetic rounding error is < 1/4 ulp. + // Series truncation error < 1/4 ulp. + // Final rounding error is <= 1/2 ulp. + // Thus final error is < 1 ulp. + BigInteger scaled_1 = big1.shiftLeft(-calc_precision); + BigInteger big_op = BigInteger.valueOf(op); + BigInteger big_op_squared = BigInteger.valueOf(op*op); + BigInteger op_inverse = scaled_1.divide(big_op); + BigInteger current_power = op_inverse; + BigInteger current_term = op_inverse; + BigInteger current_sum = op_inverse; + int current_sign = 1; + int n = 1; + BigInteger max_trunc_error = + big1.shiftLeft(p - 2 - calc_precision); + while (current_term.abs().compareTo(max_trunc_error) >= 0) { + if (Thread.interrupted() || please_stop) throw new AbortedException(); + n += 2; + current_power = current_power.divide(big_op_squared); + current_sign = -current_sign; + current_term = + current_power.divide(BigInteger.valueOf(current_sign*n)); + current_sum = current_sum.add(current_term); + } + return scale(current_sum, calc_precision - p); + } +} + +// Representation for ln(1 + op) +class prescaled_ln_CR extends slow_CR { + CR op; + prescaled_ln_CR(CR x) { op = x; } + // Compute an approximation of ln(1+x) to precision + // prec. This assumes |x| < 1/2. + // It uses a Taylor series expansion. + // Unfortunately there appears to be no way to take + // advantage of old information. + // Note: this is known to be a bad algorithm for + // floating point. Unfortunately, other alternatives + // appear to require precomputed tabular information. + protected BigInteger approximate(int p) { + if (p >= 0) return big0; + int iterations_needed = -p; // conservative estimate > 0. + // Claim: each intermediate term is accurate + // to 2*2^calc_precision. Total error is + // 2*iterations_needed*2^calc_precision + // exclusive of error in op. + int calc_precision = p - bound_log2(2*iterations_needed) + - 4; // for error in op, truncation. + int op_prec = p - 3; + BigInteger op_appr = op.get_appr(op_prec); + // Error analysis as for exponential. + BigInteger x_nth = scale(op_appr, op_prec - calc_precision); + BigInteger current_term = x_nth; // x**n + BigInteger current_sum = current_term; + int n = 1; + int current_sign = 1; // (-1)^(n-1) + BigInteger max_trunc_error = + big1.shiftLeft(p - 4 - calc_precision); + while (current_term.abs().compareTo(max_trunc_error) >= 0) { + if (Thread.interrupted() || please_stop) throw new AbortedException(); + n += 1; + current_sign = -current_sign; + x_nth = scale(x_nth.multiply(op_appr), op_prec); + current_term = x_nth.divide(BigInteger.valueOf(n * current_sign)); + // x**n / (n * (-1)**(n-1)) + current_sum = current_sum.add(current_term); + } + return scale(current_sum, calc_precision - p); + } +} + +// Representation of the arcsine of a constructive real. Private. +// Uses a Taylor series expansion. Assumes |x| < (1/2)^(1/3). +class prescaled_asin_CR extends slow_CR { + CR op; + prescaled_asin_CR(CR x) { + op = x; + } + protected BigInteger approximate(int p) { + // The Taylor series is the sum of x^(2n+1) * (2n)!/(4^n n!^2 (2n+1)) + // Note that (2n)!/(4^n n!^2) is always less than one. + // (The denominator is effectively 2n*2n*(2n-2)*(2n-2)*...*2*2 + // which is clearly > (2n)!) + // Thus all terms are bounded by x^(2n+1). + // Unfortunately, there's no easy way to prescale the argument + // to less than 1/sqrt(2), and we can only approximate that. + // Thus the worst case iteration count is fairly high. + // But it doesn't make much difference. + if (p >= 2) return big0; // Never bigger than 4. + int iterations_needed = -3 * p / 2 + 4; + // conservative estimate > 0. + // Follows from assumed bound on x and + // the fact that only every other Taylor + // Series term is present. + // Claim: each intermediate term is accurate + // to 2*2^calc_precision. + // Total rounding error in series computation is + // 2*iterations_needed*2^calc_precision, + // exclusive of error in op. + int calc_precision = p - bound_log2(2*iterations_needed) + - 4; // for error in op, truncation. + int op_prec = p - 3; // always <= -2 + BigInteger op_appr = op.get_appr(op_prec); + // Error in argument results in error of < 1/4 ulp. + // (Derivative is bounded by 2 in the specified range and we use + // 3 extra digits.) + // Ignoring the argument error, each term has an error of + // < 3ulps relative to calc_precision, which is more precise than p. + // Cumulative arithmetic rounding error is < 3/16 ulp (relative to p). + // Series truncation error < 2/16 ulp. (Each computed term + // is at most 2/3 of last one, so some of remaining series < + // 3/2 * current term.) + // Final rounding error is <= 1/2 ulp. + // Thus final error is < 1 ulp (relative to p). + BigInteger max_last_term = + big1.shiftLeft(p - 4 - calc_precision); + int exp = 1; // Current exponent, = 2n+1 in above expression + BigInteger current_term = op_appr.shiftLeft(op_prec - calc_precision); + BigInteger current_sum = current_term; + BigInteger current_factor = current_term; + // Current scaled Taylor series term + // before division by the exponent. + // Accurate to 3 ulp at calc_precision. + while (current_term.abs().compareTo(max_last_term) >= 0) { + if (Thread.interrupted() || please_stop) throw new AbortedException(); + exp += 2; + // current_factor = current_factor * op * op * (exp-1) * (exp-2) / + // (exp-1) * (exp-1), with the two exp-1 factors cancelling, + // giving + // current_factor = current_factor * op * op * (exp-2) / (exp-1) + // Thus the error any in the previous term is multiplied by + // op^2, adding an error of < (1/2)^(2/3) < 2/3 the original + // error. + current_factor = current_factor.multiply(BigInteger.valueOf(exp - 2)); + current_factor = scale(current_factor.multiply(op_appr), op_prec + 2); + // Carry 2 extra bits of precision forward; thus + // this effectively introduces 1/8 ulp error. + current_factor = current_factor.multiply(op_appr); + BigInteger divisor = BigInteger.valueOf(exp - 1); + current_factor = current_factor.divide(divisor); + // Another 1/4 ulp error here. + current_factor = scale(current_factor, op_prec - 2); + // Remove extra 2 bits. 1/2 ulp rounding error. + // Current_factor has original 3 ulp rounding error, which we + // reduced by 1, plus < 1 ulp new rounding error. + current_term = current_factor.divide(BigInteger.valueOf(exp)); + // Contributes 1 ulp error to sum plus at most 3 ulp + // from current_factor. + current_sum = current_sum.add(current_term); + } + return scale(current_sum, calc_precision - p); + } + } + + +class sqrt_CR extends CR { + CR op; + sqrt_CR(CR x) { op = x; } + // Explicitly provide an initial approximation. + // Useful for arithmetic geometric mean algorithms, where we've previously + // computed a very similar square root. + sqrt_CR(CR x, int min_p, BigInteger max_a) { + op = x; + min_prec = min_p; + max_appr = max_a; + appr_valid = true; + } + final int fp_prec = 50; // Conservative estimate of number of + // significant bits in double precision + // computation. + final int fp_op_prec = 60; + protected BigInteger approximate(int p) { + int max_op_prec_needed = 2*p - 1; + int msd = op.iter_msd(max_op_prec_needed); + if (msd <= max_op_prec_needed) return big0; + int result_msd = msd/2; // +- 1 + int result_digits = result_msd - p; // +- 2 + if (result_digits > fp_prec) { + // Compute less precise approximation and use a Newton iter. + int appr_digits = result_digits/2 + 6; + // This should be conservative. Is fewer enough? + int appr_prec = result_msd - appr_digits; + int prod_prec = 2*appr_prec; + // First compute the argument to maximal precision, so we don't end up + // reevaluating it incrementally. + BigInteger op_appr = op.get_appr(prod_prec); + BigInteger last_appr = get_appr(appr_prec); + // Compute (last_appr * last_appr + op_appr) / last_appr / 2 + // while adjusting the scaling to make everything work + BigInteger prod_prec_scaled_numerator = + last_appr.multiply(last_appr).add(op_appr); + BigInteger scaled_numerator = + scale(prod_prec_scaled_numerator, appr_prec - p); + BigInteger shifted_result = scaled_numerator.divide(last_appr); + return shifted_result.add(big1).shiftRight(1); + } else { + // Use a double precision floating point approximation. + // Make sure all precisions are even + int op_prec = (msd - fp_op_prec) & ~1; + int working_prec = op_prec - fp_op_prec; + BigInteger scaled_bi_appr = op.get_appr(op_prec) + .shiftLeft(fp_op_prec); + double scaled_appr = scaled_bi_appr.doubleValue(); + if (scaled_appr < 0.0) + throw new ArithmeticException("sqrt(negative)"); + double scaled_fp_sqrt = Math.sqrt(scaled_appr); + BigInteger scaled_sqrt = BigInteger.valueOf((long)scaled_fp_sqrt); + int shift_count = working_prec/2 - p; + return shift(scaled_sqrt, shift_count); + } + } +} + +// The constant PI, computed using the Gauss-Legendre alternating +// arithmetic-geometric mean algorithm: +// a[0] = 1 +// b[0] = 1/sqrt(2) +// t[0] = 1/4 +// p[0] = 1 +// +// a[n+1] = (a[n] + b[n])/2 (arithmetic mean, between 0.8 and 1) +// b[n+1] = sqrt(a[n] * b[n]) (geometric mean, between 0.7 and 1) +// t[n+1] = t[n] - (2^n)(a[n]-a[n+1])^2, (always between 0.2 and 0.25) +// +// pi is then approximated as (a[n+1]+b[n+1])^2 / 4*t[n+1]. +// +class gl_pi_CR extends slow_CR { + // In addition to the best approximation kept by the CR base class, we keep + // the entire sequence b[n], to the extent we've needed it so far. Each + // reevaluation leads to slightly different sqrt arguments, but the + // previous result can be used to avoid repeating low precision Newton + // iterations for the sqrt approximation. + ArrayList b_prec = new ArrayList(); + ArrayList b_val = new ArrayList(); + gl_pi_CR() { + b_prec.add(null); // Zeroth entry unused. + b_val.add(null); + } + private static BigInteger TOLERANCE = BigInteger.valueOf(4); + // sqrt(1/2) + private static CR SQRT_HALF = new sqrt_CR(ONE.shiftRight(1)); + + protected BigInteger approximate(int p) { + // Get us back into a consistent state if the last computation + // was interrupted after pushing onto b_prec. + if (b_prec.size() > b_val.size()) { + b_prec.remove(b_prec.size() - 1); + } + // Rough approximations are easy. + if (p >= 0) return scale(BigInteger.valueOf(3), -p); + // We need roughly log2(p) iterations. Each iteration should + // contribute no more than 2 ulps to the error in the corresponding + // term (a[n], b[n], or t[n]). Thus 2log2(n) bits plus a few for the + // final calulation and rounding suffice. + final int extra_eval_prec = + (int)Math.ceil(Math.log(-p) / Math.log(2)) + 10; + // All our terms are implicitly scaled by eval_prec. + final int eval_prec = p - extra_eval_prec; + BigInteger a = BigInteger.ONE.shiftLeft(-eval_prec); + BigInteger b = SQRT_HALF.get_appr(eval_prec); + BigInteger t = BigInteger.ONE.shiftLeft(-eval_prec - 2); + int n = 0; + while (a.subtract(b).subtract(TOLERANCE).signum() > 0) { + // Current values correspond to n, next_ values to n + 1 + // b_prec.size() == b_val.size() >= n + 1 + final BigInteger next_a = a.add(b).shiftRight(1); + final BigInteger next_b; + final BigInteger a_diff = a.subtract(next_a); + final BigInteger b_prod = a.multiply(b).shiftRight(-eval_prec); + // We compute square root approximations using a nested + // temporary CR computation, to avoid implementing BigInteger + // square roots separately. + final CR b_prod_as_CR = CR.valueOf(b_prod).shiftRight(-eval_prec); + if (b_prec.size() == n + 1) { + // Add an n+1st slot. + // Take care to make this exception-safe; b_prec and b_val + // must remain consistent, even if we are interrupted, or run + // out of memory. It's OK to just push on b_prec in that case. + final CR next_b_as_CR = b_prod_as_CR.sqrt(); + next_b = next_b_as_CR.get_appr(eval_prec); + final BigInteger scaled_next_b = scale(next_b, -extra_eval_prec); + b_prec.add(p); + b_val.add(scaled_next_b); + } else { + // Reuse previous approximation to reduce sqrt iterations, + // hopefully to one. + final CR next_b_as_CR = + new sqrt_CR(b_prod_as_CR, + b_prec.get(n + 1), b_val.get(n + 1)); + next_b = next_b_as_CR.get_appr(eval_prec); + // We assume that set() doesn't throw for any reason. + b_prec.set(n + 1, p); + b_val.set(n + 1, scale(next_b, -extra_eval_prec)); + } + // b_prec.size() == b_val.size() >= n + 2 + final BigInteger next_t = + t.subtract(a_diff.multiply(a_diff) + .shiftLeft(n + eval_prec)); // shift dist. usually neg. + a = next_a; + b = next_b; + t = next_t; + ++n; + } + final BigInteger sum = a.add(b); + final BigInteger result = sum.multiply(sum).divide(t).shiftRight(2); + return scale(result, -extra_eval_prec); + } +} diff --git a/app/src/main/java/com/prime/toolz2/core/math/creals/StringFloatRep.java b/app/src/main/java/com/prime/toolz2/core/math/creals/StringFloatRep.java new file mode 100644 index 0000000..eb582f6 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/math/creals/StringFloatRep.java @@ -0,0 +1,73 @@ +// Copyright (c) 2004, Hewlett-Packard Development Company, L.P. +// +// Permission is granted free of charge to copy, modify, use and distribute +// this software provided you include the entirety of this notice in all +// copies made. +// +// THIS SOFTWARE IS PROVIDED ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY +// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, +// WARRANTIES THAT THE SUBJECT SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT +// FOR A PARTICULAR PURPOSE OR NON-INFRINGING. HEWLETT-PACKARD ASSUMES +// NO RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE. +// SHOULD THE SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, +// HEWLETT-PACKARD ASSUMES NO COST OR LIABILITY FOR ANY +// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES +// AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY SUBJECT SOFTWARE IS +// AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. +// +// UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING, +// WITHOUT LIMITATION, NEGLIGENCE OR STRICT LIABILITY), CONTRACT, OR +// OTHERWISE, SHALL HEWLETT-PACKARD BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, +// INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER WITH RESPECT TO THE +// SOFTWARE INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK +// STOPPAGE, LOSS OF DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL +// OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF HEWLETT-PACKARD SHALL +// HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. +// THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY RESULTING +// FROM HEWLETT-PACKARD's NEGLIGENCE TO THE EXTENT APPLICABLE +// LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE +// EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT +// EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. +// + +package com.prime.toolz2.core.math.creals; + +/** +* A scientific notation representation of an approximation to a constructive +* real. +* Generated by CR.toStringFloatRep. +*/ +public class StringFloatRep { + StringFloatRep(int s, String m, int r, int e) { + sign = s; + mantissa = m; + radix = r; + exponent = e; + } +/** +* The sign associated with this approximation. May be -1, _1, or zero. +*/ + public int sign; +/** +* A string representation of the mantissa. The decimal point is implicitly +* to the left of the string of digits, and is not explicitly represented. +*/ + public String mantissa; +/** +* The radix of the representation. Also the base of the exponent field. +*/ + public int radix; +/** +* The mantissa is scaled by radix**exponent. +*/ + public int exponent; + +/** +* Produce a textual representation including the sign and exponent. +*/ + public String toString() { + return + (sign < 0? "-" : "") + mantissa + "E" + Integer.toString(exponent) + + (radix == 10? "" : "(radix " + radix + ")"); + } +} diff --git a/app/src/main/java/com/prime/toolz2/core/math/creals/UnaryCRFunction.java b/app/src/main/java/com/prime/toolz2/core/math/creals/UnaryCRFunction.java new file mode 100644 index 0000000..a19e570 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/math/creals/UnaryCRFunction.java @@ -0,0 +1,667 @@ +// Copyright (c) 1999, Silicon Graphics, Inc. -- ALL RIGHTS RESERVED +// +// Permission is granted free of charge to copy, modify, use and distribute +// this software provided you include the entirety of this notice in all +// copies made. +// +// THIS SOFTWARE IS PROVIDED ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY +// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, +// WARRANTIES THAT THE SUBJECT SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT +// FOR A PARTICULAR PURPOSE OR NON-INFRINGING. SGI ASSUMES NO RISK AS TO THE +// QUALITY AND PERFORMANCE OF THE SOFTWARE. SHOULD THE SOFTWARE PROVE +// DEFECTIVE IN ANY RESPECT, SGI ASSUMES NO COST OR LIABILITY FOR ANY +// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES +// AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY SUBJECT SOFTWARE IS +// AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. +// +// UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING, +// WITHOUT LIMITATION, NEGLIGENCE OR STRICT LIABILITY), CONTRACT, OR +// OTHERWISE, SHALL SGI BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, +// INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER WITH RESPECT TO THE +// SOFTWARE INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK +// STOPPAGE, LOSS OF DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL +// OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SGI SHALL HAVE BEEN INFORMED OF +// THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT +// APPLY TO LIABILITY RESULTING FROM SGI's NEGLIGENCE TO THE EXTENT APPLICABLE +// LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE +// EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT +// EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. +// +// These license terms shall be governed by and construed in accordance with +// the laws of the United States and the State of California as applied to +// agreements entered into and to be performed entirely within California +// between California residents. Any litigation relating to these license +// terms shall be subject to the exclusive jurisdiction of the Federal Courts +// of the Northern District of California (or, absent subject matter +// jurisdiction in such courts, the courts of the State of California), with +// venue lying exclusively in Santa Clara County, California. +// +// 5/2014 Added Strings to ArithmeticExceptions. +// 5/2015 Added support for direct asin() implementation in CR. + +package com.prime.toolz2.core.math.creals; +// import android.util.Log; + +import java.math.BigInteger; + +/** +* Unary functions on constructive reals implemented as objects. +* The execute member computes the function result. +* Unary function objects on constructive reals inherit from +* UnaryCRFunction. +*/ +// Naming vaguely follows ObjectSpace JGL convention. +public abstract class UnaryCRFunction { + abstract public CR execute(CR x); + +/** +* The function object corresponding to the identity function. +*/ + public static final UnaryCRFunction identityFunction = + new identity_UnaryCRFunction(); + +/** +* The function object corresponding to the negate method of CR. +*/ + public static final UnaryCRFunction negateFunction = + new negate_UnaryCRFunction(); + +/** +* The function object corresponding to the inverse method of CR. +*/ + public static final UnaryCRFunction inverseFunction = + new inverse_UnaryCRFunction(); + +/** +* The function object corresponding to the abs method of CR. +*/ + public static final UnaryCRFunction absFunction = + new abs_UnaryCRFunction(); + +/** +* The function object corresponding to the exp method of CR. +*/ + public static final UnaryCRFunction expFunction = + new exp_UnaryCRFunction(); + +/** +* The function object corresponding to the cos method of CR. +*/ + public static final UnaryCRFunction cosFunction = + new cos_UnaryCRFunction(); + +/** +* The function object corresponding to the sin method of CR. +*/ + public static final UnaryCRFunction sinFunction = + new sin_UnaryCRFunction(); + +/** +* The function object corresponding to the tangent function. +*/ + public static final UnaryCRFunction tanFunction = + new tan_UnaryCRFunction(); + +/** +* The function object corresponding to the inverse sine (arcsine) function. +* The argument must be between -1 and 1 inclusive. The result is between +* -PI/2 and PI/2. +*/ + public static final UnaryCRFunction asinFunction = + new asin_UnaryCRFunction(); + // The following also works, but is slower: + // CR half_pi = CR.PI.divide(CR.valueOf(2)); + // UnaryCRFunction.sinFunction.inverseMonotone(half_pi.negate(), + // half_pi); + +/** +* The function object corresponding to the inverse cosine (arccosine) function. +* The argument must be between -1 and 1 inclusive. The result is between +* 0 and PI. +*/ + public static final UnaryCRFunction acosFunction = + new acos_UnaryCRFunction(); + +/** +* The function object corresponding to the inverse cosine (arctangent) function. +* The result is between -PI/2 and PI/2. +*/ + public static final UnaryCRFunction atanFunction = + new atan_UnaryCRFunction(); + +/** +* The function object corresponding to the ln method of CR. +*/ + public static final UnaryCRFunction lnFunction = + new ln_UnaryCRFunction(); + +/** +* The function object corresponding to the sqrt method of CR. +*/ + public static final UnaryCRFunction sqrtFunction = + new sqrt_UnaryCRFunction(); + +/** +* Compose this function with f2. +*/ + public UnaryCRFunction compose(UnaryCRFunction f2) { + return new compose_UnaryCRFunction(this, f2); + } + +/** +* Compute the inverse of this function, which must be defined +* and strictly monotone on the interval [low, high]. +* The resulting function is defined only on the image of +* [low, high]. +* The original function may be either increasing or decreasing. +*/ + public UnaryCRFunction inverseMonotone(CR low, CR high) { + return new inverseMonotone_UnaryCRFunction(this, low, high); + } + +/** +* Compute the derivative of a function. +* The function must be defined on the interval [low, high], +* and the derivative must exist, and must be continuous and +* monotone in the open interval [low, high]. +* The result is defined only in the open interval. +*/ + public UnaryCRFunction monotoneDerivative(CR low, CR high) { + return new monotoneDerivative_UnaryCRFunction(this, low, high); + } + +} + +// Subclasses of UnaryCRFunction for various built-in functions. +class sin_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.sin(); + } +} + +class cos_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.cos(); + } +} + +class tan_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.sin().divide(x.cos()); + } +} + +class asin_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.asin(); + } +} + +class acos_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.acos(); + } +} + +// This uses the identity (sin x)^2 = (tan x)^2/(1 + (tan x)^2) +// Since we know the tangent of the result, we can get its sine, +// and then use the asin function. Note that we don't always +// want the positive square root when computing the sine. +class atan_UnaryCRFunction extends UnaryCRFunction { + CR one = CR.valueOf(1); + public CR execute(CR x) { + CR x2 = x.multiply(x); + CR abs_sin_atan = x2.divide(one.add(x2)).sqrt(); + CR sin_atan = x.select(abs_sin_atan.negate(), abs_sin_atan); + return sin_atan.asin(); + } +} + +class exp_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.exp(); + } +} + +class ln_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.ln(); + } +} + +class identity_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x; + } +} + +class negate_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.negate(); + } +} + +class inverse_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.inverse(); + } +} + +class abs_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.abs(); + } +} + +class sqrt_UnaryCRFunction extends UnaryCRFunction { + public CR execute(CR x) { + return x.sqrt(); + } +} + +class compose_UnaryCRFunction extends UnaryCRFunction { + UnaryCRFunction f1; + UnaryCRFunction f2; + compose_UnaryCRFunction(UnaryCRFunction func1, + UnaryCRFunction func2) { + f1 = func1; f2 = func2; + } + public CR execute(CR x) { + return f1.execute(f2.execute(x)); + } +} + +class inverseMonotone_UnaryCRFunction extends UnaryCRFunction { + // The following variables are final, so that they + // can be referenced from the inner class inverseIncreasingCR. + // I couldn't find a way to initialize these such that the + // compiler accepted them as final without turning them into arrays. + final UnaryCRFunction f[] = new UnaryCRFunction[1]; + // Monotone increasing. + // If it was monotone decreasing, we + // negate it. + final boolean f_negated[] = new boolean[1]; + final CR low[] = new CR[1]; + final CR high[] = new CR[1]; + final CR f_low[] = new CR[1]; + final CR f_high[] = new CR[1]; + final int max_msd[] = new int[1]; + // Bound on msd of both f(high) and f(low) + final int max_arg_prec[] = new int[1]; + // base**max_arg_prec is a small fraction + // of low - high. + final int deriv_msd[] = new int[1]; + // Rough approx. of msd of first + // derivative. + final static BigInteger BIG1023 = BigInteger.valueOf(1023); + static final boolean ENABLE_TRACE = false; // Change to generate trace + static void trace(String s) { + if (ENABLE_TRACE) { + System.out.println(s); + // Change to Log.v("UnaryCRFunction", s); for Android use. + } + } + inverseMonotone_UnaryCRFunction(UnaryCRFunction func, CR l, CR h) { + low[0] = l; high[0] = h; + CR tmp_f_low = func.execute(l); + CR tmp_f_high = func.execute(h); + // Since func is monotone and low < high, the following test + // converges. + if (tmp_f_low.compareTo(tmp_f_high) > 0) { + f[0] = UnaryCRFunction.negateFunction.compose(func); + f_negated[0] = true; + f_low[0] = tmp_f_low.negate(); + f_high[0] = tmp_f_high.negate(); + } else { + f[0] = func; + f_negated[0] = false; + f_low[0] = tmp_f_low; + f_high[0] = tmp_f_high; + } + max_msd[0] = low[0].abs().max(high[0].abs()).msd(); + max_arg_prec[0] = high[0].subtract(low[0]).msd() - 4; + deriv_msd[0] = f_high[0].subtract(f_low[0]) + .divide(high[0].subtract(low[0])).msd(); + } + class inverseIncreasingCR extends CR { + final CR arg; + inverseIncreasingCR(CR x) { + arg = f_negated[0]? x.negate() : x; + } + // Comparison with a difference of one treated as equality. + int sloppy_compare(BigInteger x, BigInteger y) { + BigInteger difference = x.subtract(y); + if (difference.compareTo(big1) > 0) { + return 1; + } + if (difference.compareTo(bigm1) < 0) { + return -1; + } + return 0; + } + protected BigInteger approximate(int p) { + final int extra_arg_prec = 4; + final UnaryCRFunction fn = f[0]; + int small_step_deficit = 0; // Number of ineffective steps not + // yet compensated for by a binary + // search step. + int digits_needed = max_msd[0] - p; + if (digits_needed < 0) return big0; + int working_arg_prec = p - extra_arg_prec; + if (working_arg_prec > max_arg_prec[0]) { + working_arg_prec = max_arg_prec[0]; + } + int working_eval_prec = working_arg_prec + deriv_msd[0] - 20; + // initial guess + // We use a combination of binary search and something like + // the secant method. This always converges linearly, + // and should converge quadratically under favorable assumptions. + // F_l and f_h are always the approximate images of l and h. + // At any point, arg is between f_l and f_h, or no more than + // one outside [f_l, f_h]. + // L and h are implicitly scaled by working_arg_prec. + // The scaled values of l and h are strictly between low and high. + // If at_left is true, then l is logically at the left + // end of the interval. We approximate this by setting l to + // a point slightly inside the interval, and letting f_l + // approximate the function value at the endpoint. + // If at_right is true, r and f_r are set correspondingly. + // At the endpoints of the interval, f_l and f_h may correspond + // to the endpoints, even if l and h are slightly inside. + // F_l and f_u are scaled by working_eval_prec. + // Working_eval_prec may need to be adjusted depending + // on the derivative of f. + boolean at_left, at_right; + BigInteger l, f_l; + BigInteger h, f_h; + BigInteger low_appr = low[0].get_appr(working_arg_prec) + .add(big1); + BigInteger high_appr = high[0].get_appr(working_arg_prec) + .subtract(big1); + BigInteger arg_appr = arg.get_appr(working_eval_prec); + boolean have_good_appr = (appr_valid && min_prec < max_msd[0]); + if (digits_needed < 30 && !have_good_appr) { + trace("Setting interval to entire domain"); + h = high_appr; + f_h = f_high[0].get_appr(working_eval_prec); + l = low_appr; + f_l = f_low[0].get_appr(working_eval_prec); + // Check for clear out-of-bounds case. + // Close cases may fail in other ways. + if (f_h.compareTo(arg_appr.subtract(big1)) < 0 + || f_l.compareTo(arg_appr.add(big1)) > 0) { + throw new ArithmeticException("inverse(out-of-bounds)"); + } + at_left = true; + at_right = true; + small_step_deficit = 2; // Start with bin search steps. + } else { + int rough_prec = p + digits_needed/2; + + if (have_good_appr && + (digits_needed < 30 || min_prec < p + 3*digits_needed/4)) { + rough_prec = min_prec; + } + BigInteger rough_appr = get_appr(rough_prec); + trace("Setting interval based on prev. appr"); + trace("prev. prec = " + rough_prec + " appr = " + rough_appr); + h = rough_appr.add(big1) + .shiftLeft(rough_prec - working_arg_prec); + l = rough_appr.subtract(big1) + .shiftLeft(rough_prec - working_arg_prec); + if (h.compareTo(high_appr) > 0) { + h = high_appr; + f_h = f_high[0].get_appr(working_eval_prec); + at_right = true; + } else { + CR h_cr = CR.valueOf(h).shiftLeft(working_arg_prec); + f_h = fn.execute(h_cr).get_appr(working_eval_prec); + at_right = false; + } + if (l.compareTo(low_appr) < 0) { + l = low_appr; + f_l = f_low[0].get_appr(working_eval_prec); + at_left = true; + } else { + CR l_cr = CR.valueOf(l).shiftLeft(working_arg_prec); + f_l = fn.execute(l_cr).get_appr(working_eval_prec); + at_left = false; + } + } + BigInteger difference = h.subtract(l); + for(int i = 0;; ++i) { + if (Thread.interrupted() || please_stop) + throw new AbortedException(); + trace("***Iteration: " + i); + trace("Arg prec = " + working_arg_prec + + " eval prec = " + working_eval_prec + + " arg appr. = " + arg_appr); + trace("l = " + l); trace("h = " + h); + trace("f(l) = " + f_l); trace("f(h) = " + f_h); + if (difference.compareTo(big6) < 0) { + // Answer is less than 1/2 ulp away from h. + return scale(h, -extra_arg_prec); + } + BigInteger f_difference = f_h.subtract(f_l); + // Narrow the interval by dividing at a cleverly + // chosen point (guess) in the middle. + { + BigInteger guess; + boolean binary_step = + (small_step_deficit > 0 || f_difference.signum() == 0); + if (binary_step) { + // Do a binary search step to guarantee linear + // convergence. + trace("binary step"); + guess = l.add(h).shiftRight(1); + --small_step_deficit; + } else { + // interpolate. + // f_difference is nonzero here. + trace("interpolating"); + BigInteger arg_difference = arg_appr.subtract(f_l); + BigInteger t = arg_difference.multiply(difference); + BigInteger adj = t.divide(f_difference); + // tentative adjustment to l to compute guess + // If we are within 1/1024 of either end, back off. + // This greatly improves the odds of bounding + // the answer within the smaller interval. + // Note that interpolation will often get us + // MUCH closer than this. + if (adj.compareTo(difference.shiftRight(10)) < 0) { + adj = adj.shiftLeft(8); + trace("adjusting left"); + } else if (adj.compareTo(difference.multiply(BIG1023) + .shiftRight(10)) > 0){ + adj = difference.subtract(difference.subtract(adj) + .shiftLeft(8)); + trace("adjusting right"); + } + if (adj.signum() <= 0) + adj = big2; + if (adj.compareTo(difference) >= 0) + adj = difference.subtract(big2); + guess = (adj.signum() <= 0? l.add(big2) : l.add(adj)); + } + int outcome; + BigInteger tweak = big2; + BigInteger f_guess; + for(boolean adj_prec = false;; adj_prec = !adj_prec) { + CR guess_cr = CR.valueOf(guess) + .shiftLeft(working_arg_prec); + trace("Evaluating at " + guess_cr + + " with precision " + working_eval_prec); + CR f_guess_cr = fn.execute(guess_cr); + trace("fn value = " + f_guess_cr); + f_guess = f_guess_cr.get_appr(working_eval_prec); + outcome = sloppy_compare(f_guess, arg_appr); + if (outcome != 0) break; + // Alternately increase evaluation precision + // and adjust guess slightly. + // This should be an unlikely case. + if (adj_prec) { + // adjust working_eval_prec to get enough + // resolution. + int adjustment = -f_guess.bitLength()/4; + if (adjustment > -20) adjustment = - 20; + CR l_cr = CR.valueOf(l) + .shiftLeft(working_arg_prec); + CR h_cr = CR.valueOf(h) + .shiftLeft(working_arg_prec); + working_eval_prec += adjustment; + trace("New eval prec = " + working_eval_prec + + (at_left? "(at left)" : "") + + (at_right? "(at right)" : "")); + if (at_left) { + f_l = f_low[0].get_appr(working_eval_prec); + } else { + f_l = fn.execute(l_cr) + .get_appr(working_eval_prec); + } + if (at_right) { + f_h = f_high[0].get_appr(working_eval_prec); + } else { + f_h = fn.execute(h_cr) + .get_appr(working_eval_prec); + } + arg_appr = arg.get_appr(working_eval_prec); + } else { + // guess might be exactly right; tweak it + // slightly. + trace("tweaking guess"); + BigInteger new_guess = guess.add(tweak); + if (new_guess.compareTo(h) >= 0) { + guess = guess.subtract(tweak); + } else { + guess = new_guess; + } + // If we keep hitting the right answer, it's + // important to alternate which side we move it + // to, so that the interval shrinks rapidly. + tweak = tweak.negate(); + } + } + if (outcome > 0) { + h = guess; + f_h = f_guess; + at_right = false; + } else { + l = guess; + f_l = f_guess; + at_left = false; + } + BigInteger new_difference = h.subtract(l); + if (!binary_step) { + if (new_difference.compareTo(difference + .shiftRight(1)) >= 0) { + ++small_step_deficit; + } else { + --small_step_deficit; + } + } + difference = new_difference; + } + } + } + } + public CR execute(CR x) { + return new inverseIncreasingCR(x); + } +} + +class monotoneDerivative_UnaryCRFunction extends UnaryCRFunction { + // The following variables are final, so that they + // can be referenced from the inner class inverseIncreasingCR. + final UnaryCRFunction f[] = new UnaryCRFunction[1]; + // Monotone increasing. + // If it was monotone decreasing, we + // negate it. + final CR low[] = new CR[1]; // endpoints and mispoint of interval + final CR mid[] = new CR[1]; + final CR high[] = new CR[1]; + final CR f_low[] = new CR[1]; // Corresponding function values. + final CR f_mid[] = new CR[1]; + final CR f_high[] = new CR[1]; + final int difference_msd[] = new int[1]; // msd of interval len. + final int deriv2_msd[] = new int[1]; + // Rough approx. of msd of second + // derivative. + // This is increased to be an appr. bound + // on the msd of |(f'(y)-f'(x))/(x-y)| + // for any pair of points x and y + // we have considered. + // It may be better to keep a copy per + // derivative value. + + monotoneDerivative_UnaryCRFunction(UnaryCRFunction func, CR l, CR h) { + f[0] = func; + low[0] = l; high[0] = h; + mid[0] = l.add(h).shiftRight(1); + f_low[0] = func.execute(l); + f_mid[0] = func.execute(mid[0]); + f_high[0] = func.execute(h); + CR difference = h.subtract(l); + // compute approximate msd of + // ((f_high - f_mid) - (f_mid - f_low))/(high - low) + // This should be a very rough appr to the second derivative. + // We add a little slop to err on the high side, since + // a low estimate will cause extra iterations. + CR appr_diff2 = f_high[0].subtract(f_mid[0].shiftLeft(1)).add(f_low[0]); + difference_msd[0] = difference.msd(); + deriv2_msd[0] = appr_diff2.msd() - difference_msd[0] + 4; + } + class monotoneDerivativeCR extends CR { + CR arg; + CR f_arg; + int max_delta_msd; + monotoneDerivativeCR(CR x) { + arg = x; + f_arg = f[0].execute(x); + // The following must converge, since arg must be in the + // open interval. + CR left_diff = arg.subtract(low[0]); + int max_delta_left_msd = left_diff.msd(); + CR right_diff = high[0].subtract(arg); + int max_delta_right_msd = right_diff.msd(); + if (left_diff.signum() < 0 || right_diff.signum() < 0) { + throw new ArithmeticException("fn not monotone"); + } + max_delta_msd = (max_delta_left_msd < max_delta_right_msd? + max_delta_left_msd + : max_delta_right_msd); + } + protected BigInteger approximate(int p) { + final int extra_prec = 4; + int log_delta = p - deriv2_msd[0]; + // Ensure that we stay within the interval. + if (log_delta > max_delta_msd) log_delta = max_delta_msd; + log_delta -= extra_prec; + CR delta = ONE.shiftLeft(log_delta); + + CR left = arg.subtract(delta); + CR right = arg.add(delta); + CR f_left = f[0].execute(left); + CR f_right = f[0].execute(right); + CR left_deriv = f_arg.subtract(f_left).shiftRight(log_delta); + CR right_deriv = f_right.subtract(f_arg).shiftRight(log_delta); + int eval_prec = p - extra_prec; + BigInteger appr_left_deriv = left_deriv.get_appr(eval_prec); + BigInteger appr_right_deriv = right_deriv.get_appr(eval_prec); + BigInteger deriv_difference = + appr_right_deriv.subtract(appr_left_deriv).abs(); + if (deriv_difference.compareTo(big8) < 0) { + return scale(appr_left_deriv, -extra_prec); + } else { + if (Thread.interrupted() || please_stop) + throw new AbortedException(); + deriv2_msd[0] = + eval_prec + deriv_difference.bitLength() + 4/*slop*/; + deriv2_msd[0] -= log_delta; + return approximate(p); + } + } + } + public CR execute(CR x) { + return new monotoneDerivativeCR(x); + } +} diff --git a/app/src/main/java/com/prime/toolz2/core/math/more.kt b/app/src/main/java/com/prime/toolz2/core/math/more.kt new file mode 100644 index 0000000..3d31d0b --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/core/math/more.kt @@ -0,0 +1,207 @@ +package com.prime.toolz2.core.math + +import java.math.BigInteger + +object NumUtil { + + /** + * The symbol used to represent exponent part of a number. + */ + const val EXPONENT = 'E' + + /** + * The symbol for the decimal point of a number. + */ + const val DECIMAL = '.' + + /** + * The symbol for the zero digit + */ + const val ZERO = '0' + + /** + * Removes Front zeros from the whole part of a number. + * + * @param whole The Whole Part of a number. + * @return modified number. + */ + fun removeFrontZeroes(whole: String): String { + + //FIXME: Use whole number not just 'whole' part of number. + + val s = StringBuilder(whole) + val lastWasZero = whole.indexOf("0") == 0 && s.length > 1 + var i = 0 + while (i < s.length) { + if (lastWasZero && s[i] == '0' && i + 1 < s.length && s[i + 1] != '.') { + s.deleteCharAt(i) + continue + } + i++ + } + return s.toString() + } + + /** + * Strips Trailing Zeros to from fraction part of a [String] representation of a number. + * + * @param fraction [String] representation of Number + * @return [String] representation of modified number. + */ + fun stripTrailingZeroes(fraction: String): String { + + // FIXME: Incorporate whole number not just fraction part. + + // if the provided number doesn't contain the decimal; then return the + // provided number without any modifications + // if (!fraction.contains(DECIMAL)) return fraction + + // else remove/strip th zeros from the fraction part + val s = StringBuilder(fraction) + var lastWasZero = fraction.lastIndexOf("0") == fraction.length - 1 + for (i in s.length - 1 downTo 0) { + if ((s[i] == '0' || s[i] == '.') && lastWasZero) { + if (s[i] == '.') lastWasZero = false + s.deleteCharAt(i) + } else lastWasZero = false + } + return s.toString() + } + + + /** + * A helper function to split a number into 3 parts; whole, fraction and exponent. + * + * **Note** + * if the component (say fraction) is missing; while the symbol i.e., [DECIMAL] is present + * present empty is returned else *null* at its place in the resulting array. + * + * ***For E.g.*** + * If the given number is 12345.6789E12345 it will return a string array where + * a[0] = 12345 (i.e, Whole Part) + * a[1] = .6789 (i.e., Fractional Part) + * a[2] = 12345 (i.e., Scientific part) + * + * @param number: The number to split e.g., 123.45E789 + * @return [Array] of size 3 with whole, fraction and exponent at respective hole. + */ + fun split(number: String): Array { + + //FIXME: Find out what happens to minus sign. + + val array = arrayOfNulls(3) + + // The index of the components or null + // Why null? + // because it is easy to work with null than -1 + val iFraction = number.indexOf(DECIMAL).let { if (it == -1) null else it } + val iExponent = number.indexOf(EXPONENT).let { if (it == -1) null else it } + + // why not use split method. + // first it never returns array of size 3 every time. + // it returns array list than again we have to check if properly it has worked; adds multiple overheads + + // whole part + array[0] = number + .substring(0, iFraction ?: iExponent ?: number.length) + //.let { it.ifEmpty { null } } + + // fractional part + array[1] = + if (iFraction == null) + null + else + number + .substring(iFraction + 1, iExponent ?: number.length) + // if the fractional decimal present but no number + // return zero in its place. + //.let { it.ifEmpty { null } } + + // exponent + array[2] = + if (iExponent == null) + null + else + number + .substring(iExponent + 1, number.length) + // same logic as stated above. + //.let { it.ifEmpty { null } } + + // return + return array + } + + /** + * Return a copy of the supplied string with [separator] added every three digits. + * Inserting a digit separator every 3 digits appears to be + * at least somewhat acceptable, though not necessarily preferred, everywhere. + + * The grouping separator in the result is NOT localized. + * @param whole: The number as whole in the format 123.456E789, + * @param separator: The provided separator char like , + */ + fun addThousandSeparators(whole: String, separator: Char): String { + + //FIXME: Find out what happens to minus sign. and incorparate whole number not just 'whole' part + + // add separators to the resulting whole part. + return buildString { + val begin = 0 + val end = whole.length + + var current = begin + append(whole, begin, current) + while (current < end) { + append(whole[current]) + ++current + if ((end - current) % 3 == 0 && end != current) { + append(separator) + } + } + } + } + + + /** + * Renders a real number to a String (for user display). + * @param maxLen the maximum total length of the resulting string + * @param rounding the number of final digits to round + */ + @Deprecated("not recommended.", level = DeprecationLevel.WARNING) + fun doubleToString(x: Double, maxLen: Int, rounding: Int): String? { + return Util.sizeTruncate(Util.doubleToString(x, rounding), maxLen) + } +} + +@Deprecated("not recommended.", level = DeprecationLevel.HIDDEN) + /** + * Stringifies the value of this [UnifiedReal]'s double value. + * TODO: Move away from double + */ +fun UnifiedReal.stringfy(len: Int, rounding: Int): String = + Util.doubleToString(doubleValue(), len, rounding) + +/** + * Constructs a unified real from any text. e.g., 1234.4565465 + */ +@Suppress("FunctionName") +fun UnifiedReal(text: String): UnifiedReal { + // split number in respective components. + val (whole, fraction, exponent) = NumUtil.split(text) + // Through error if an string with exponent is supplied. + if (exponent != null) + error("Current Implantation doesn't support exponent!!") + + // construct the numerator from the provided info + val numerator = BigInteger( + // replace with zero if null + (whole ?: "0") + (fraction ?: "") + ) + // construct the denominator from the provided info. + val denominator = BigInteger.TEN.pow( + // 10 power zero i.e., if fractional part empty + fraction?.length ?: 0 + ) + // currently we don't support exponents + return UnifiedReal(BoundedRational(numerator, denominator)) +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/settings/FontFamily.kt b/app/src/main/java/com/prime/toolz2/settings/FontFamily.kt new file mode 100644 index 0000000..1769ae1 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/settings/FontFamily.kt @@ -0,0 +1,21 @@ +package com.prime.toolz2.settings + +/** + * TODO: Find appropriate way to support Google fonts. + */ +enum class FontFamily { + // The default typography of app + SYSTEM_DEFAULT, + + // + PROVIDED, + + // + SAN_SERIF, + + // + SARIF, + + // + CURSIVE +} diff --git a/app/src/main/java/com/prime/toolz2/settings/GlobalKeys.kt b/app/src/main/java/com/prime/toolz2/settings/GlobalKeys.kt new file mode 100644 index 0000000..dbddf58 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/settings/GlobalKeys.kt @@ -0,0 +1,87 @@ +package com.prime.toolz2.settings + +import com.primex.preferences.* + +private const val TAG = "PrefKeys" + +object GlobalKeys { + /** + * Retrieves/Sets The [NightMode] Strategy + */ + val NIGHT_MODE = + stringPreferenceKey( + "${TAG}_night_mode", + NightMode.NO, + object : StringSaver { + override fun save(value: NightMode): String = value.name + + override fun restore(value: String): NightMode = NightMode.valueOf(value) + } + ) + + val FONT_FAMILY = + stringPreferenceKey( + TAG + "_font_family", + FontFamily.PROVIDED, + object : StringSaver { + override fun save(value: FontFamily): String = value.name + + override fun restore(value: String): FontFamily = FontFamily.valueOf(value) + } + ) + + + val FORCE_COLORIZE = + booleanPreferenceKey( + TAG + "_force_colorize", + false + ) + + val COLOR_STATUS_BAR = + booleanPreferenceKey( + TAG + "_color_status_bar", + false + ) + + val HIDE_STATUS_BAR = + booleanPreferenceKey( + TAG + "_hide_status_bar", + false + ) + + val GROUP_SEPARATOR = + stringPreferenceKey( + name = TAG + "_group_separator", + defaultValue = ',', + saver = object : StringSaver { + override fun restore(value: String): Char = value[0] + + override fun save(value: Char): String = "$value" + } + ) + + val DECIMAL_SEPARATOR = + stringPreferenceKey( + name = TAG + "_decimal_separator", + defaultValue = "." + ) + + val NUMBER_OF_DECIMALS = + intPreferenceKey( + name = TAG + "_number_of_decimals", + defaultValue = 5 + ) + + val FONT_SCALE = + floatPreferenceKey( + TAG + "_font_scale", + defaultValue = 1.0f + ) + + /** + * The counter counts the number of times this app was launched. + */ + val KEY_LAUNCH_COUNTER = + intPreferenceKey(TAG + "_launch_counter") + +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/settings/NightMode.kt b/app/src/main/java/com/prime/toolz2/settings/NightMode.kt new file mode 100644 index 0000000..de62664 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/settings/NightMode.kt @@ -0,0 +1,49 @@ +package com.prime.toolz2.settings + +enum class NightMode { + /** + * Night mode which uses always uses a light mode, enabling {@code notnight} qualified + * resources regardless of the time. + * + * @see #setLocalNightMode(int) + */ + YES, + + /** + * Night mode which uses always uses a dark mode, enabling {@code night} qualified + * resources regardless of the time. + * + * @see #setLocalNightMode(int) + */ + NO, + + /** + * Mode which uses the system's night mode setting to determine if it is night or not. + * + * @see #setLocalNightMode(int) + */ + FOLLOW_SYSTEM, + + /** + * Night mode which uses a dark mode when the system's 'Battery Saver' feature is enabled, + * otherwise it uses a 'light mode'. This mode can help the device to decrease power usage, + * depending on the display technology in the device. + * + * Please note: this mode should only be used when running on devices which do not + * provide a similar device-wide setting. + * + * @see #setLocalNightMode(int) + */ + AUTO_BATTER, + + /** + * Night mode which switches between dark and light mode depending on the time of day + * (dark at night, light in the day). + * + * The calculation used to determine whether it is night or not makes use of the location + * APIs (if this app has the necessary permissions). This allows us to generate accurate + * sunrise and sunset times. If this app does not have permission to access the location APIs + * then we use hardcoded times which will be less accurate. + */ + AUTO_TIME +} diff --git a/app/src/main/java/com/prime/toolz2/settings/Settings.kt b/app/src/main/java/com/prime/toolz2/settings/Settings.kt new file mode 100644 index 0000000..b69898a --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/settings/Settings.kt @@ -0,0 +1,311 @@ +package com.prime.toolz2.settings + +import android.app.Activity +import android.content.Intent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.prime.toolz2.* +import com.prime.toolz2.R +import com.prime.toolz2.common.compose.* +import com.primex.core.activity +import com.primex.core.drawHorizontalDivider +import com.primex.core.stringHtmlResource +import com.primex.ui.* +import cz.levinzonr.saferoute.accompanist.navigation.transitions.AnimatedRouteTransition +import cz.levinzonr.saferoute.core.annotations.Route +import cz.levinzonr.saferoute.core.annotations.RouteNavGraph + + +private val RESERVE_PADDING = 56.dp + +private const val FONT_SCALE_LOWER_BOUND = 0.5f +private const val FONT_SCALE_UPPER_BOUND = 2.0f + +private const val SLIDER_STEPS = 15 + +private const val ZERO_WIDTH_CHAR = '\u200B' + +//TODO: Instead of string use Text +private val familyList = + listOf( + "Lato" to FontFamily.PROVIDED, + "Cursive" to FontFamily.CURSIVE, + "San serif" to FontFamily.SAN_SERIF, + "serif" to FontFamily.SARIF, + "System default" to FontFamily.SYSTEM_DEFAULT + ) + +val GroupSeparatorList = + listOf( + "Space ( ) " to ' ', + "Hyphen (_) " to '_', + "None" to ZERO_WIDTH_CHAR, + "Comma ( , )" to ',' + ) + +@Composable +private inline fun PrefHeader(text: String) { + val primary = MaterialTheme.colors.secondary + val modifier = + Modifier + .padding( + start = RESERVE_PADDING, + top = ContentPadding.normal, + end = ContentPadding.large, + bottom = ContentPadding.medium + ) + .fillMaxWidth() + .drawHorizontalDivider(color = primary) + .padding(bottom = ContentPadding.medium) + Label( + text = text, + modifier = modifier, + fontWeight = FontWeight.SemiBold, + maxLines = 2, + color = primary + ) +} + +context(ColumnScope) @Composable +private inline fun AboutUs() { + PrefHeader(text = "Feedback") + + // val feedbackCollector = LocalFeedbackCollector.current + val onRequestFeedback = { + // TODO: Handle feedback + } + + Preference( + title = stringResource(R.string.feedback), + summery = (stringResource(id = R.string.feedback_dialog_placeholder) + "\nTap to open feedback dialog."), + icon = Icons.Outlined.Feedback, + modifier = Modifier.clickable(onClick = onRequestFeedback) + ) + + val onRequestRateApp = { + // TODO: Handle rate app. + } + Preference( + title = stringResource(R.string.rate_us), + summery = stringResource(id = R.string.review_msg), + icon = Icons.Outlined.Star, + modifier = Modifier.clickable(onClick = onRequestRateApp) + ) + + val onRequestShareApp = { + // TODO: Share app. + } + Preference( + title = stringResource(R.string.spread_the_word), + summery = stringResource(R.string.spread_the_word_summery), + icon = Icons.Outlined.Share, + modifier = Modifier.clickable(onClick = onRequestShareApp) + ) + + PrefHeader(text = stringResource(R.string.about_us)) + Text( + text = stringHtmlResource(R.string.about_us_desc), + style = MaterialTheme.typography.body2, + modifier = Modifier + .padding(start = RESERVE_PADDING, end = ContentPadding.large) + .padding(vertical = ContentPadding.small), + color = LocalContentColor.current.copy(ContentAlpha.medium) + ) + + val context = LocalContext.current + val version = remember { + context.packageManager.getPackageInfo(context.packageName, 0).versionName + } + //val updateNotifier = LocalUpdateNotifier.current + val activity = LocalContext.current.activity!! + val channel = LocalSnackDataChannel.current + val onCheckUpdate: () -> Unit = { + activity.launchUpdateFlow(channel, true) + } + Preference( + title = stringResource(R.string.app_version), + summery = "$version \nClick to check for updates.", + icon = Icons.Outlined.TouchApp, + modifier = Modifier.clickable(onClick = onCheckUpdate) + ) +} + +private fun Activity.restart() { + finish() + startActivity(Intent(this, this.javaClass)) + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) +} + +@Composable +private fun TopAppBar(modifier: Modifier = Modifier) { + val navigator = LocalNavController.current + + NeumorphicTopAppBar( + title = { Label(text = stringResource(R.string.settings)) }, + modifier = modifier.padding(top = ContentPadding.medium), + navigationIcon = { + IconButton( + onClick = { navigator.navigateUp() }, + imageVector = Icons.Outlined.ReplyAll, + contentDescription = null + ) + }, + shape = CircleShape, + elevation = ContentElevation.low, + lightShadowColor = Material.colors.lightShadowColor, + darkShadowColor = Material.colors.darkShadowColor + ) +} + + +@OptIn(ExperimentalAnimationApi::class) +@Route( + transition = AnimatedRouteTransition.Default::class, + navGraph = RouteNavGraph(start = false) +) +@Composable +fun Settings(viewModel: SettingsViewModel) { + with(viewModel) { + Scaffold( + topBar = { + val (colorStatusBar, _, _, _) = colorStatusBar.value + val primaryOrTransparent = + Material.colors.primary(colorStatusBar, Color.Transparent) + TopAppBar( + modifier = Modifier + .statusBarsPadding2( + color = primaryOrTransparent, + darkIcons = !colorStatusBar && Material.colors.isLight + ) + .drawHorizontalDivider(color = Material.colors.onSurface) + .padding(bottom = ContentPadding.medium) + ) + }, + + + content = { + val state = rememberScrollState() + //val color = if (MaterialTheme.colors.isLight) Color.White else Color.Black + Column( + modifier = Modifier + .padding(it) + //FixMe: Creates a issue between Theme changes + // needs to be study properly + // disabling for now + //.fadeEdge(state = state, length = 16.dp, horizontal = false, color = color) + .verticalScroll(state), + ) { + PrefHeader(text = stringResource(R.string.appearence)) + + //dark mode + val darkTheme by darkUiMode + SwitchPreference( + checked = darkTheme.value, + title = stringResource(res = darkTheme.title), + summery = darkTheme.summery?.let { stringResource(res = it) }, + icon = darkTheme.vector, + onCheckedChange = { new: Boolean -> + set(GlobalKeys.NIGHT_MODE, if (new) NightMode.YES else NightMode.NO) + } + ) + + //font + val font by font + DropDownPreference( + title = stringResource(res = font.title), + entries = familyList, + defaultValue = font.value, + icon = font.vector, + onRequestChange = { family: FontFamily -> + viewModel.set(GlobalKeys.FONT_FAMILY, family) + } + ) + + val scale by fontScale + SliderPreference( + defaultValue = scale.value, + title = stringResource(res = scale.title), + summery = scale.summery?.let { stringResource(res = it) }, + valueRange = FONT_SCALE_LOWER_BOUND..FONT_SCALE_UPPER_BOUND, + steps = SLIDER_STEPS, + icon = scale.vector, + iconChange = Icons.Outlined.TextFormat, + onValueChange = { value: Float -> + set(GlobalKeys.FONT_SCALE, value) + } + ) + + //force accent + val forceAccent by forceAccent + SwitchPreference( + checked = forceAccent.value, + title = stringResource(res = forceAccent.title), + summery = forceAccent.summery?.let { stringResource(res = it) }, + onCheckedChange = { should: Boolean -> + set(GlobalKeys.FORCE_COLORIZE, should) + if (should) + set(GlobalKeys.COLOR_STATUS_BAR, true) + } + ) + + //color status bar + val colorStatusBar by colorStatusBar + SwitchPreference( + checked = colorStatusBar.value, + title = stringResource(res = colorStatusBar.title), + summery = colorStatusBar.summery?.let { stringResource(res = it) }, + enabled = !forceAccent.value, + onCheckedChange = { should: Boolean -> + set(GlobalKeys.COLOR_STATUS_BAR, should) + } + ) + + //hide status bar + val hideStatusBar by hideStatusBar + SwitchPreference( + checked = hideStatusBar.value, + title = stringResource(res = hideStatusBar.title), + summery = hideStatusBar.summery?.let { stringResource(res = it) }, + onCheckedChange = { should: Boolean -> + set(GlobalKeys.HIDE_STATUS_BAR, should) + } + ) + + // group separator + val separator by groupSeparator + DropDownPreference( + title = stringResource(separator.title), + entries = GroupSeparatorList, + defaultValue = separator.value, + icon = separator.vector, + onRequestChange = { + viewModel.set(GlobalKeys.GROUP_SEPARATOR, it) + } + ) + + //About us section. + AboutUs() + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/settings/SettingsViewModel.kt b/app/src/main/java/com/prime/toolz2/settings/SettingsViewModel.kt new file mode 100644 index 0000000..dd75973 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/settings/SettingsViewModel.kt @@ -0,0 +1,173 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package com.prime.toolz2.settings + + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.TextFields +import androidx.compose.material.icons.outlined.HideImage +import androidx.compose.material.icons.outlined.Lightbulb +import androidx.compose.material.icons.outlined.ZoomIn +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.primex.core.Text +import com.primex.preferences.* +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class Preference( + val value: P, + val title: Text, + val vector: ImageVector? = null, + val summery: Text? = null, +) + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val preferences: Preferences, +) : ViewModel() { + + val darkUiMode = + with(preferences) { + preferences[GlobalKeys.NIGHT_MODE].map { + Preference( + value = when (it) { + NightMode.YES -> true + else -> false + }, + title = Text("Dark Mode"), + summery = Text("Click to change the app night/light mode."), + vector = Icons.Outlined.Lightbulb + ) + }.collectAsState() + } + + val font = + with(preferences) { + preferences[GlobalKeys.FONT_FAMILY].map { + Preference( + vector = Icons.Default.TextFields, + title = Text("Font"), + summery = Text("Choose font to better reflect your desires."), + value = it + ) + }.collectAsState() + } + + val colorStatusBar = + with(preferences) { + preferences[GlobalKeys.COLOR_STATUS_BAR] + .map { + Preference( + vector = null, + title = Text("Color Status Bar"), + summery = Text("Force color status bar."), + value = it + ) + } + .collectAsState() + } + + val hideStatusBar = + with(preferences) { + preferences[GlobalKeys.HIDE_STATUS_BAR] + .map { + Preference( + value = it, + title = Text("Hide Status Bar"), + summery = Text("hide status bar for immersive view"), + vector = Icons.Outlined.HideImage + ) + } + .collectAsState() + } + + val forceAccent = + with(preferences) { + preferences[GlobalKeys.FORCE_COLORIZE] + .map { + Preference( + value = it, + title = Text("Force Accent Color"), + summery = Text("Normally the app follows the rule of using 10% accent color. But if this setting is toggled it can make it use more than 30%") + ) + } + .collectAsState() + } + + + val fontScale = + with(preferences) { + preferences[GlobalKeys.FONT_SCALE] + .map { + Preference( + value = it, + title = Text("Font Scale"), + summery = Text("Zoom in or out the text shown on the screen."), + vector = Icons.Outlined.ZoomIn + ) + } + .collectAsState() + } + + + val groupSeparator = + with(preferences) { + preferences[GlobalKeys.GROUP_SEPARATOR].map { + Preference( + value = it, + title = Text("Group Separator"), + summery = Text("Set the number group separator.") + ) + } + .collectAsState() + } + + + fun set(key: Key, value: T) { + viewModelScope.launch { + preferences[key] = value + } + } + + fun set(key: Key1, value: T) { + viewModelScope.launch { + preferences[key] = value + } + } + + fun set(key: Key2, value: O) { + viewModelScope.launch { + preferences[key] = value + } + } + + fun set(key: Key3, value: O) { + viewModelScope.launch { + preferences[key] = value + } + } + + +} + +context (Preferences, ViewModel) + private fun Flow.collectAsState(): State { + + val state = mutableStateOf( + obtain() + ) + onEach { + state.value = it + } + .launchIn(viewModelScope) + return state +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/ui/Home.kt b/app/src/main/java/com/prime/toolz2/ui/Home.kt new file mode 100644 index 0000000..c9dd20c --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/ui/Home.kt @@ -0,0 +1,98 @@ +package com.prime.toolz2.ui + + +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavGraphBuilder +import com.google.accompanist.navigation.animation.AnimatedNavHost +import com.google.accompanist.navigation.animation.composable +import com.google.accompanist.navigation.animation.rememberAnimatedNavController +import com.prime.toolz2.common.compose.LocalNavController +import com.prime.toolz2.common.compose.LocalWindowPadding +import com.prime.toolz2.settings.Settings +import com.prime.toolz2.settings.SettingsViewModel +import com.prime.toolz2.ui.converter.MainGraphRoutes +import com.prime.toolz2.ui.converter.UnitConverter +import com.prime.toolz2.ui.converter.UnitConverterViewModel +import cz.levinzonr.saferoute.core.ProvideRouteSpecArgs +import cz.levinzonr.saferoute.core.RouteSpec + + +@OptIn(ExperimentalAnimationApi::class) +private val DefaultEnterTransition = scaleIn( + initialScale = 0.98f, + animationSpec = tween(220, delayMillis = 90) +) + fadeIn(animationSpec = tween(700)) + +private val DefaultExitTransition = fadeOut(tween(700)) + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun Home(){ + // Currently; supports only 1 Part + // add others in future + // including support for more tools, like direction, prime factorization etc. + // also support for navGraph. + val controller = rememberAnimatedNavController() + + // The padding suggested by the wrapper. + val padding = LocalWindowPadding.current + CompositionLocalProvider( + LocalNavController provides controller + ) { + AnimatedNavHost( + navController = LocalNavController.current, + startDestination = MainGraphRoutes.UnitConverter.route, + enterTransition = { DefaultEnterTransition}, + exitTransition = { DefaultExitTransition }, + popEnterTransition = {DefaultEnterTransition}, + popExitTransition = { DefaultExitTransition }, + modifier = Modifier.navigationBarsPadding().padding(padding), + ) { + composable(MainGraphRoutes.UnitConverter) { + val viewModel = hiltViewModel() + UnitConverter(viewModel = viewModel) + } + + composable(MainGraphRoutes.Settings) { + val viewModel = hiltViewModel() + Settings(viewModel = viewModel) + } + } + } +} + + +///missing fun +@OptIn(ExperimentalAnimationApi::class) +private fun NavGraphBuilder.composable( + spec: RouteSpec<*>, + enterTransition: (AnimatedContentScope.() -> EnterTransition?)? = null, + exitTransition: (AnimatedContentScope.() -> ExitTransition?)? = null, + popEnterTransition: ( + AnimatedContentScope.() -> EnterTransition? + )? = enterTransition, + popExitTransition: ( + AnimatedContentScope.() -> ExitTransition? + )? = exitTransition, + content: @Composable (NavBackStackEntry) -> Unit +) = composable( + spec.route, + spec.navArgs, + spec.deepLinks, + enterTransition = enterTransition, + exitTransition = exitTransition, + popEnterTransition = popEnterTransition, + popExitTransition = popExitTransition +) { + ProvideRouteSpecArgs(spec = spec, entry = it) { + content.invoke(it) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverter.kt b/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverter.kt new file mode 100644 index 0000000..fef1e2d --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverter.kt @@ -0,0 +1,732 @@ +package com.prime.toolz2.ui.converter + +import android.util.Log +import androidx.annotation.DrawableRes +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.outlined.* +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.* +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.prime.toolz2.* +import com.prime.toolz2.R +import com.prime.toolz2.billing.Product +import com.prime.toolz2.common.compose.* +import com.prime.toolz2.core.converter.Unet +import com.prime.toolz2.core.math.NumUtil +import com.prime.toolz2.settings.GlobalKeys +import com.prime.toolz2.settings.SettingsRoute +import com.primex.core.* +import com.primex.preferences.LocalPreferenceStore +import com.primex.ui.* +import com.primex.ui.dialog.BottomSheetDialog +import cz.levinzonr.saferoute.core.annotations.Route +import cz.levinzonr.saferoute.core.annotations.RouteNavGraph +import cz.levinzonr.saferoute.core.navigateTo +import kotlinx.coroutines.flow.map + + +private const val TAG = "UnitConverter" + +@Suppress("FunctionName") +private fun NumberFormatterTransformation(separator: Char = ',') = + VisualTransformation { + val text = it.text + val transformed = run { + // split into respective components. + // maybe remove this and replace with already formatted text. + val (w, f, e) = NumUtil.split(text) + val whole = if (!w.isNullOrBlank()) NumUtil.addThousandSeparators(w, separator) else "" + val fraction = if (f != null) ".$f" else "" + val exponent = if (e != null) "E$e" else "" + whole + fraction + exponent + } + + // FIXME: The offsets are not mapped accurately + // use separator in the transformed text. + TransformedText( + offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return transformed.length + } + + override fun transformedToOriginal(offset: Int): Int { + return text.length + } + }, + text = AnnotatedString(transformed) + ) + } + +private inline val NumberFormatTransformation: State + @Composable + get() { + //TODO: maybe consider using LocalComposition for preferences. + val preferences = LocalPreferenceStore.current + return with(preferences) { + val initial = remember { + val value = preferences[GlobalKeys.GROUP_SEPARATOR].obtain() + NumberFormatterTransformation(value) + } + produceState(initialValue = initial) { + preferences[GlobalKeys.GROUP_SEPARATOR].map { + NumberFormatterTransformation(it) + }.collect { + value = it + } + } + } + } + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun AppBarTop( + modifier: Modifier = Modifier +) { + val prefs = LocalPreferenceStore.current + val forceColorize by with(prefs) { + prefs[GlobalKeys.FORCE_COLORIZE].observeAsState() + } + Surface( + color = if (forceColorize) Material.colors.primary else Material.colors.overlay, + contentColor = if (forceColorize) Material.colors.onPrimary else Material.colors.onBackground, + modifier = modifier + .fillMaxWidth() + .height(100.dp), + shape = RoundedCornerShape(bottomStartPercent = 70), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = ContentPadding.large, end = ContentPadding.normal) + ) { + + // app icon + // TODO: Replace it with buy option. + IconButton( + onClick = { /*TODO*/ }, + painter = painterResource(id = R.drawable.ic_handyman), + contentDescription = null, + modifier = Modifier + .padding(horizontal = ContentPadding.normal) + .requiredSize(24.dp) + ) + + // title + Text( + text = stringHtmlResource(id = R.string.unit_converter_html), + fontWeight = FontWeight.Light, + modifier = Modifier.weight(1f), + style = Material.typography.h5 + ) + + val manager = LocalBillingManager.current + val activity = LocalContext.current.activity!! + val purchased by manager.isPurchased(id = Product.DISABLE_ASD) + if (!purchased) + IconButton( + painter = painterResource(id = R.drawable.ic_remove_ads), + contentDescription = null, + onClick = { + manager.launchBillingFlow(activity, Product.DISABLE_ASD) + } + ) + + // actions + val controller = LocalNavController.current + IconButton( + imageVector = Icons.Outlined.Settings, + contentDescription = null, + onClick = { + val direction = SettingsRoute() + controller.navigateTo(direction) + } + ) + } + } +} + + +@Composable +private fun Tab( + title: Text, + @DrawableRes imageRes: Int, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val color = + if (selected) Material.colors.secondary.copy(ContentAlpha.Indication) else Material.colors.overlay + val contentColor = if (selected) Material.colors.secondary else Material.colors.onBackground + // The color of the Ripple should always the selected color, as we want to show the color + // before the item is considered selected, and hence before the new contentColor is + // provided by TabTransition. + val ripple = rememberRipple(bounded = false) + Column( + modifier = modifier + .clip(Material.shapes.small) + .selectable( + selected = selected, + onClick = onClick, + enabled = true, + role = Role.Tab, + interactionSource = remember(::MutableInteractionSource), + indication = null + ) + .padding(ContentPadding.small) + .width(62.dp) + .wrapContentHeight(), + + horizontalAlignment = Alignment.CenterHorizontally + ) { + Surface( + modifier = Modifier.size(56.dp), + contentColor = contentColor, + color = color, + border = BorderStroke(1.dp, if (selected) contentColor else color), + shape = RoundedCornerShape(30), + content = { + Icon( + painter = painterResource(id = imageRes), + contentDescription = null, + modifier = Modifier.requiredSize(24.dp) + ) + } + ) + + Label( + text = stringResource(res = title), + maxLines = 2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = ContentPadding.medium), + color = contentColor, + fontSize = 9.sp + ) + } +} + +@Composable +private fun UnitConverterViewModel.Converters( + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp) +) { + val current = converter + val activity = LocalContext.current.activity!! + LazyRow( + contentPadding = contentPadding, + modifier = modifier, + content = { + items(converters) { item -> + val selected = item == current + Tab( + title = item.title, + imageRes = item.drawableRes, + selected = selected, + onClick = { converter = item; activity.launchReviewFlow() } + ) + } + } + ) +} + +@Composable +fun Unit( + value: Unet, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val container = if (selected) Material.colors.primaryContainer else Color.Transparent + val color = if (selected) Material.colors.primary else LocalContentColor.current + val bg = + if (selected) + Modifier.drawBehind { + drawRect(brush = Brush.horizontalGradient(listOf(container, Color.Transparent))) + drawRect(color = color, size = size.copy(width = 4.dp.toPx())) + } + else + Modifier + DropdownMenuItem( + modifier = modifier.then(bg), + onClick = onClick, + enabled = !selected, + content = { + // code + Text( + text = stringResource(res = value.code), + style = MaterialTheme.typography.body1, + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.Bold, + color = color + ) + + // title + Text( + text = stringResource(res = value.title), + modifier = Modifier.padding(start = ContentPadding.normal), + style = MaterialTheme.typography.caption, + color = color, + ) + } + ) +} + + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun UnitConverterViewModel.ExposedDropdownMenuBox( + expanded: Boolean, + onDismissRequest: () -> Unit, + isFrom: Boolean, + values: Map>, + modifier: Modifier = Modifier, + field: @Composable () -> Unit, +) { + ExposedDropdownMenuBox( + modifier = modifier, + expanded = expanded, + onExpandedChange = { /*expanded = it*/ }, + content = { + // The field of that this menu exposes + field() + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = Modifier.exposedDropdownSize(true), + content = { + val primary = Material.colors.primary + val selected = if (isFrom) fromUnit else toUnit + + values.forEach { (title, list) -> + + // list header + Label( + text = title, + fontWeight = FontWeight.Light, + maxLines = 2, + color = primary, + modifier = Modifier + .padding( + start = ContentPadding.normal, + top = ContentPadding.normal, + end = ContentPadding.normal, + bottom = ContentPadding.medium + ) + .fillMaxWidth() + .drawHorizontalDivider(color = primary) + .padding(bottom = ContentPadding.medium), + style = Material.typography.h4 + ) + + // emit the list of this title + val activity = LocalContext.current.activity!! + list.forEach { value -> + Unit( + value = value, + selected = selected == value, + onClick = { + if (isFrom) fromUnit = value + else toUnit = value + // only place where it should be called. + activity.launchReviewFlow() + onDismissRequest() + } + ) + } + } + } + ) + } + ) +} + + +private val FIELD_MIN_HEIGHT = 80.dp + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun UnitConverterViewModel.ValueField( + visualTransformation: VisualTransformation, + values: Map>, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + //Header + Text( + text = "FROM", + style = MaterialTheme.typography.overline, + modifier = Modifier + .rotate(false), + ) + var expanded by rememberState(initial = false) + ExposedDropdownMenuBox( + isFrom = true, + values = values, + modifier = Modifier.padding(start = ContentPadding.medium), + expanded = expanded, + onDismissRequest = { expanded = false }, + field = { + OutlinedTextField( + value = TextFieldValue(value, TextRange(value.length)), + onValueChange = { value = it.text }, + readOnly = false, + singleLine = true, + + // a workaround for keyboard issue. + // using readOnly introduces another issue. + enabled = !expanded, + visualTransformation = visualTransformation, + shape = RoundedCornerShape(percent = 10), + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Decimal), + + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors( + textColor = Material.colors.onBackground, + cursorColor = Color.Transparent, + ), + + label = { + Label(text = stringResource(res = fromUnit.title)) + }, + + trailingIcon = { + val rotate by animateFloatAsState(targetValue = if (expanded) 0f else 180f) + IconButton( + onClick = { expanded = !expanded }, + imageVector = Icons.Outlined.KeyboardArrowUp, + contentDescription = null, + modifier = Modifier.rotate(rotate) + ) + }, + + textStyle = MaterialTheme.typography.h4.copy( + fontWeight = FontWeight.SemiBold + ), + + modifier = Modifier + .fillMaxWidth() + .heightIn(min = FIELD_MIN_HEIGHT), + ) + } + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun UnitConverterViewModel.ResultField( + visualTransformation: VisualTransformation, + values: Map>, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + //Header + Text( + text = "EQUALS TO", + style = MaterialTheme.typography.overline, + modifier = Modifier + .rotate(false), + ) + + var expanded by rememberState(initial = false) + ExposedDropdownMenuBox( + isFrom = false, + expanded = expanded, + values = values, + modifier = Modifier.padding(start = ContentPadding.medium), + onDismissRequest = { expanded = false }, + field = { + TextField( + readOnly = true, + value = result, + onValueChange = { }, + singleLine = true, + visualTransformation = visualTransformation, + shape = RoundedCornerShape(topStartPercent = 10, topEndPercent = 10), + colors = ExposedDropdownMenuDefaults.textFieldColors(), + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), + + + modifier = Modifier + .fillMaxWidth() + .heightIn(min = FIELD_MIN_HEIGHT), + + label = { Label(text = stringResource(res = toUnit.title)) }, + + trailingIcon = { + val rotate by animateFloatAsState(targetValue = if (expanded) 0f else 180f) + IconButton( + onClick = { expanded = !expanded }, + imageVector = Icons.Outlined.KeyboardArrowUp, + contentDescription = null, + modifier = Modifier.rotate(rotate) + ) + }, + textStyle = MaterialTheme.typography.h5.copy( + letterSpacing = 0.35.sp, + fontWeight = FontWeight.SemiBold + ), + ) + } + ) + } +} + +@Composable +private fun OutlineChip( + text: AnnotatedString, + onRequestCopy: () -> Unit, + modifier: Modifier = Modifier +) { + Surface( + content = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(start = ContentPadding.medium) + .scale(0.85f) + ) { + Text( + text = text, + style = Material.typography.body2, + color = LocalContentColor.current, + fontWeight = FontWeight.SemiBold + ) + IconButton( + onClick = onRequestCopy, + imageVector = Icons.Outlined.FileCopy, + contentDescription = null, + modifier = Modifier + .padding(start = ContentPadding.medium), + tint = LocalContentColor.current + ) + } + }, + border = BorderStroke(1.dp, Material.colors.outline), + contentColor = Material.colors.onBackground.copy(ContentAlpha.medium), + color = Color.Transparent, + modifier = modifier + .height(34.dp) + .wrapContentWidth(), + shape = CircleShape + ) +} + + +@Composable +private fun UnitConverterViewModel.AboutEquals( + modifier: Modifier = Modifier, +) { + val manager = LocalClipboardManager.current + StaggeredGrid( + rows = 2, + modifier = Modifier + .horizontalScroll(state = rememberScrollState()) + .then(modifier) + ) { + more.forEach { (unit, value) -> + val code = stringResource(res = unit) + OutlineChip( + text = buildAnnotatedString { + append(value) + append(" ") + withStyle( + style = SpanStyle(fontStyle = FontStyle.Italic), + block = { + append(code) + } + ) + }, + onRequestCopy = { manager.copy() }, + modifier = Modifier.padding( + end = ContentPadding.medium, + bottom = ContentPadding.normal + ) + ) + } + } +} + +@OptIn(ExperimentalComposeApi::class) +@Route(navGraph = RouteNavGraph(start = true)) +@Composable +fun UnitConverter( + viewModel: UnitConverterViewModel +) { + // Dispose off messenger when out of scope. + // this ensures the viewModel has a instance of channel only when + // device is active and working. + val channel = LocalSnackDataChannel.current + DisposableEffect(key1 = Unit) { + viewModel.channel = channel + onDispose { + viewModel.channel = null + } + } + + with(viewModel) { + + Scaffold( + topBar = { + val prefs = LocalPreferenceStore.current + val colorize by with(prefs) { + prefs[GlobalKeys.COLOR_STATUS_BAR].observeAsState() + } + AppBarTop( + modifier = Modifier.statusBarsPadding2( + color = if (colorize) Material.colors.primaryVariant else Material.colors.overlay, + darkIcons = Material.colors.isLight && !colorize + ) + ) + }, + + floatingActionButton = { + FloatingActionButton(onClick = { swap(); }, shape = RoundedCornerShape(30)) { + Icon( + imageVector = Icons.Outlined.SwapVert, + contentDescription = null + ) + } + }, + + content = { + Column( + modifier = Modifier.padding(it), + content = { + + Converters( + modifier = Modifier + .sizeIn(minHeight = 110.dp) + .padding(top = ContentPadding.normal), + contentPadding = PaddingValues(horizontal = 24.dp) + ) + + CompositionLocalProvider( + LocalTextSelectionColors provides TextSelectionColors( + Color.Transparent, + Color.Transparent + ) + ) { + val visualTransformation by NumberFormatTransformation + val resources = LocalContext.current.resources + val values = remember(converter.uuid) { + converter.units.groupBy { resources.stringResource(it.group) } + } + ValueField( + modifier = Modifier.padding( + top = ContentPadding.normal, + start = 24.dp, + end = 24.dp + ), + visualTransformation = visualTransformation, + values = values + ) + + ResultField( + modifier = Modifier.padding( + top = ContentPadding.normal, + start = 24.dp, + end = 24.dp + ), + visualTransformation = visualTransformation, + values = values + ) + } + + // actions + Row( + modifier = Modifier + .align(Alignment.End) + .padding(end = 24.dp, top = ContentPadding.medium), + ) { + + var expanded by rememberState(initial = false) + BottomSheetDialog(expanded = expanded, onDismissRequest = { expanded = false}) { + Surface() { + Column { + Text( + text = "About Equals", + style = MaterialTheme.typography.h4, + modifier = Modifier + .padding( + start = ContentPadding.large, + top = ContentPadding.small + ) + .align(Alignment.Start), + fontWeight = FontWeight.Light, + ) + + Divider( + modifier = Modifier.padding( + vertical = ContentPadding.medium, + horizontal = ContentPadding.normal + ) + ) + + AboutEquals( + modifier = Modifier + .padding(horizontal = 28.dp, vertical = ContentPadding.medium), + ) + } + } + } + + IconButton( + onClick = { expanded = true }, + imageVector = Icons.Filled.Info, + contentDescription = "More", + tint = Material.colors.primary, + ) + + val clipboard = LocalClipboardManager.current + TextButton( + label = "COPY", + leading = rememberVectorPainter(image = Icons.Outlined.FileCopy), + onClick = { clipboard.copy() } + ) + } + } + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverterViewModel.kt b/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverterViewModel.kt new file mode 100644 index 0000000..2740586 --- /dev/null +++ b/app/src/main/java/com/prime/toolz2/ui/converter/UnitConverterViewModel.kt @@ -0,0 +1,294 @@ +package com.prime.toolz2.ui.converter + +import android.content.ClipboardManager +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.prime.toolz2.common.compose.SnackDataChannel +import com.prime.toolz2.common.compose.send +import com.prime.toolz2.core.converter.Unet +import com.prime.toolz2.core.converter.UnitConverter +import com.prime.toolz2.core.math.NumUtil +import com.prime.toolz2.core.math.UnifiedReal +import com.primex.core.Text +import com.primex.preferences.Preferences +import com.primex.preferences.stringPreferenceKey +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.text.DecimalFormat +import javax.inject.Inject + + +private const val DEBOUNCE_TIMEOUT = 15L + +private const val DEFAULT_VALUE = "0" + +private const val TAG = "UnitConverterViewModel" + +private val KEY_CONVERTER = stringPreferenceKey(TAG + "_converter") +private val KEY_UNIT_FROM = stringPreferenceKey(TAG + "_unit_from") +private val KEY_UNIT_TO = stringPreferenceKey(TAG + "_unit_to") +private val KEY_VALUE = stringPreferenceKey(TAG + "_converter_value") + +private const val MAX_ALLOWED_CHARS = 12 + +@OptIn(FlowPreview::class) +@HiltViewModel +class UnitConverterViewModel @Inject constructor( + private val preferences: Preferences, +) : ViewModel() { + + /** + * The version of this [ViewModel]. + * Update to this triggers calculation. + */ + private val version = MutableStateFlow(0) + + /** + * The messenger used to show messages on the UI. + */ + @JvmField + var channel: SnackDataChannel? = null + + /** + * The unit converter. + */ + private val engine = UnitConverter() + + /** + * The all of converters supported by [engine] + */ + val converters = engine.converters + + /** + * The converter. + */ + private val _converter = mutableStateOf( + with(preferences) { + // some required blocking calls + val uuid = get(KEY_CONVERTER).obtain() ?: engine.converter.uuid + val selected = converters.find { it.uuid == uuid }!! + + // set converter to engine + engine.converter = selected + + // return the selected converter. + selected + } + ) + var converter + get() = _converter.value + set(value) { + // update the representation of the converter + _converter.value = value + // update the converter of the engine. + engine.converter = value + //update uuiD saved + val uuid = value.uuid + preferences[KEY_CONVERTER] = uuid + // as converter changed + // obviously unit from and to too changed. + fromUnit = engine.from + toUnit = engine.to + + // update version to trigger calculation + version.value += 1 + } + + /** + * The unit from. + */ + private val _fromUnit = mutableStateOf( + with(preferences) { + val uuid = get(KEY_UNIT_FROM).obtain() ?: engine.from.uuid + val units = _converter.value.units + + // obtain selected unit. + val selected = units.find { it.uuid == uuid }!! + + // set selected from unit to engine + engine.from = selected + + // return selected unit + selected + } + ) + + var fromUnit + get() = _fromUnit.value + set(value) { + viewModelScope.launch { + // units of the converter + engine.from = value + _fromUnit.value = value + preferences[KEY_UNIT_FROM] = value.uuid + // update trigger + version.value += 1 + } + } + + /** + * To Unit + */ + private val _toUnit = mutableStateOf( + with(preferences) { + val uuid = get(KEY_UNIT_TO).obtain() ?: engine.to.uuid + val units = _converter.value.units + + // obtain selected unit. + val selected = units.find { it.uuid == uuid }!! + + // set selected to unit to engine + engine.to = selected + + // return selected unit + selected + } + ) + + var toUnit + get() = _toUnit.value + set(value) { + viewModelScope.launch { + // units of the converter + engine.to = value + _toUnit.value = value + preferences[KEY_UNIT_TO] = value.uuid + // update trigger + version.value += 1 + } + } + + /** + * The value to be converted. + * The Max length = [MAX_ALLOWED_CHARS] + */ + private val _value = mutableStateOf( + with(preferences) { + val text = get(KEY_VALUE).obtain() ?: DEFAULT_VALUE + text + } + ) + var value + get() = _value.value + set(value) { + viewModelScope.launch { + // preserve default value. + val modified = when { + value.isBlank() -> DEFAULT_VALUE + // old is default value + value[0] == DEFAULT_VALUE[0] -> value.drop(1) + else -> value + } + + // check for error + // emit without saving + val msg = + when { + modified.length > MAX_ALLOWED_CHARS -> "Max allowed length reached." + // if it is not a valid double. + modified.toDoubleOrNull() == null -> "Provided input is invalid." + else -> null + } + + // if error + // emit message and return + if (msg != null) { + channel?.send(message = msg) + return@launch + } + + // emit value. + _value.value = modified + // update trigger + version.value += 1 + //safe in prefs. + preferences[KEY_VALUE] = modified + } + } + + /** + * The computed result. + */ + private val _result = mutableStateOf(DEFAULT_VALUE) + var result + get() = _result.value + private set(value) { + _result.value = value + } + + /** + * The value of converter in terms of other units. + */ + private val _more = mutableStateOf>(emptyMap()) + var more + get() = _more.value + private set(value) { + _more.value = value + } + + /** + * A convince method to swap the values of the [fromUnit] and [toUnit] + */ + fun swap() { + // from as to + val from = _toUnit.value + val to = _fromUnit.value + + fromUnit = from + toUnit = to + viewModelScope.launch { + channel?.send(message = "Units Swapped!") + } + } + + fun clear() { + value = DEFAULT_VALUE + viewModelScope.launch { + channel?.send(message = "Input cleared") + } + } + + /** + * A util function to copy the converted value into [ClipboardManager] + * @param to The [toUnit] might be one from about equals. + */ + fun androidx.compose.ui.platform.ClipboardManager.copy(to: Unet? = null){ + viewModelScope.launch { + channel?.send(message = "Not Implemented yet!. Coming Soon!") + } + } + + private val formatter = DecimalFormat("###,###.##") + + init { + version + .debounce(DEBOUNCE_TIMEOUT) + .onEach { _ -> + // set value as unified + // TODO: Find which errors might occur + // catch the errors. + // find solution to report to the user. + val value = _value.value + engine.value = UnifiedReal(value) + + // compute and emit result. + val double = engine.convert().doubleValue() + result = NumUtil.doubleToString(double, 12, 2)!! + + // compute in terms of others. + // this might take time, + // use helper threads if possible. + more = engine.mapped(0.1f) + .mapKeys { it.key.code } + .mapValues { formatter.format(it.value.doubleValue()) } + } + .catch { + // FixMe find suitable method to emit the errors. + channel?.send(message = "Oops!! An Unknown error occurred.") + } + .launchIn(viewModelScope) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_remove_ads.xml b/app/src/main/res/drawable/ic_remove_ads.xml new file mode 100644 index 0000000..61e309e --- /dev/null +++ b/app/src/main/res/drawable/ic_remove_ads.xml @@ -0,0 +1,9 @@ + + + diff --git a/build.gradle b/build.gradle index 5ad9af0..fdcf28d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.7.10' + kotlin_version = '1.6.21' hilt_version = '2.43.2' }