diff --git a/app/BUILD.bazel b/app/BUILD.bazel index d39fbe0ab9f..c085dd160f7 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -189,7 +189,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/onboarding/OnboardingViewModel.kt", "src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt", "src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt", - "src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt", + "src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt", "src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt", "src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragDropInteractionContentViewModel.kt", @@ -658,6 +658,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_event_logger", "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_module", + "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", # TODO(#59): Remove 'debug_util_module' once we completely migrate to Bazel from Gradle as # we can then directly exclude debug files from the build and thus won't be requiring this module. "//utility/src/main/java/org/oppia/android/util/networking:debug_util_module", diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt index 6209a6c269e..49972bda253 100644 --- a/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt @@ -20,6 +20,8 @@ import org.oppia.android.app.utility.KeyboardHelper.Companion.showSoftKeyboard // background="@drawable/edit_text_background" // maxLength="200". +// TODO(#4135): Add a dedicated test suite for this class. + /** The custom EditText class for fraction input interaction view. */ class FractionInputInteractionView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt new file mode 100644 index 00000000000..731d26e0590 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt @@ -0,0 +1,45 @@ +package org.oppia.android.app.parser + +import androidx.annotation.StringRes +import org.oppia.android.R +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.math.FractionParser.FractionParsingError + +/** Enum to store the errors of [FractionInputInteractionView]. */ +enum class FractionParsingUiError(@StringRes private var error: Int?) { + /** Corresponds to [FractionParsingError.VALID]. */ + VALID(error = null), + + /** Corresponds to [FractionParsingError.INVALID_CHARS]. */ + INVALID_CHARS(error = R.string.fraction_error_invalid_chars), + + /** Corresponds to [FractionParsingError.INVALID_FORMAT]. */ + INVALID_FORMAT(error = R.string.fraction_error_invalid_format), + + /** Corresponds to [FractionParsingError.DIVISION_BY_ZERO]. */ + DIVISION_BY_ZERO(error = R.string.fraction_error_divide_by_zero), + + /** Corresponds to [FractionParsingError.NUMBER_TOO_LONG]. */ + NUMBER_TOO_LONG(error = R.string.fraction_error_larger_than_seven_digits); + + /** + * Returns the string corresponding to this error's string resources, or null if there is none. + */ + fun getErrorMessageFromStringRes(resourceHandler: AppLanguageResourceHandler): String? = + error?.let(resourceHandler::getStringInLocale) + + companion object { + /** + * Returns the [FractionParsingUiError] corresponding to the specified [FractionParsingError]. + */ + fun createFromParsingError(parsingError: FractionParsingError): FractionParsingUiError { + return when (parsingError) { + FractionParsingError.VALID -> VALID + FractionParsingError.INVALID_CHARS -> INVALID_CHARS + FractionParsingError.INVALID_FORMAT -> INVALID_FORMAT + FractionParsingError.DIVISION_BY_ZERO -> DIVISION_BY_ZERO + FractionParsingError.NUMBER_TOO_LONG -> NUMBER_TOO_LONG + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt b/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt index 3670e2fd16c..5b625623ad2 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt +++ b/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt @@ -3,7 +3,7 @@ package org.oppia.android.app.parser import androidx.annotation.StringRes import org.oppia.android.R import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace /** * This class contains methods that help to parse string to number, check realtime and submit time diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt b/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt index 1d2562fa73a..31895402263 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt +++ b/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt @@ -4,8 +4,8 @@ import androidx.annotation.StringRes import org.oppia.android.R import org.oppia.android.app.model.RatioExpression import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.domain.util.normalizeWhitespace -import org.oppia.android.domain.util.removeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace +import org.oppia.android.util.extensions.removeWhitespace /** * Utility for parsing [RatioExpression]s from strings and validating strings can be parsed diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt index b2e2329cefd..8ce11c53e71 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt @@ -9,12 +9,13 @@ import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.WrittenTranslationContext -import org.oppia.android.app.parser.StringToFractionParser +import org.oppia.android.app.parser.FractionParsingUiError import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.util.math.FractionParser /** [StateItemViewModel] for the fraction input interaction. */ class FractionInteractionViewModel( @@ -32,7 +33,7 @@ class FractionInteractionViewModel( var errorMessage = ObservableField("") val hintText: CharSequence = deriveHintText(interaction) - private val stringToFractionParser: StringToFractionParser = StringToFractionParser() + private val fractionParser = FractionParser() init { val callback: Observable.OnPropertyChangedCallback = @@ -52,7 +53,7 @@ class FractionInteractionViewModel( if (answerText.isNotEmpty()) { val answerTextString = answerText.toString() answer = InteractionObject.newBuilder().apply { - fraction = stringToFractionParser.parseFractionFromString(answerTextString) + fraction = fractionParser.parseFractionFromString(answerTextString) }.build() plainAnswer = answerTextString this.writtenTranslationContext = this@FractionInteractionViewModel.writtenTranslationContext @@ -63,14 +64,18 @@ class FractionInteractionViewModel( override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { if (answerText.isNotEmpty()) { when (category) { - AnswerErrorCategory.REAL_TIME -> + AnswerErrorCategory.REAL_TIME -> { pendingAnswerError = - stringToFractionParser.getRealTimeAnswerError(answerText.toString()) - .getErrorMessageFromStringRes(resourceHandler) - AnswerErrorCategory.SUBMIT_TIME -> + FractionParsingUiError.createFromParsingError( + fractionParser.getRealTimeAnswerError(answerText.toString()) + ).getErrorMessageFromStringRes(resourceHandler) + } + AnswerErrorCategory.SUBMIT_TIME -> { pendingAnswerError = - stringToFractionParser.getSubmitTimeError(answerText.toString()) - .getErrorMessageFromStringRes(resourceHandler) + FractionParsingUiError.createFromParsingError( + fractionParser.getSubmitTimeError(answerText.toString()) + ).getErrorMessageFromStringRes(resourceHandler) + } } errorMessage.set(pendingAnswerError) } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt index 064b7fc3f60..6f916b0f4d0 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt @@ -16,7 +16,7 @@ import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandle import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.utility.toAccessibleAnswerString import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.toAnswerString +import org.oppia.android.util.math.toAnswerString /** [StateItemViewModel] for the ratio expression input interaction. */ class RatioExpressionInputInteractionViewModel( diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt index 420cef8111a..c4c349b7e2d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt @@ -127,6 +127,8 @@ class InputInteractionViewTestActivityTest { ApplicationProvider.getApplicationContext().inject(this) } + // TODO(#4135): Move fraction input tests to a dedicated test suite. + @Test fun testFractionInput_withNoInput_hasCorrectPendingAnswerType() { val activityScenario = ActivityScenario.launch( diff --git a/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt new file mode 100644 index 00000000000..df39f6a196e --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt @@ -0,0 +1,282 @@ +package org.oppia.android.app.parser + +import android.app.Application +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.Component +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.testing.activity.TestActivity +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.math.FractionParser +import org.oppia.android.util.math.FractionParser.FractionParsingError +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +import org.oppia.android.util.parser.image.GlideImageLoaderModule +import org.oppia.android.util.parser.image.ImageParsingModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Singleton + +/** Tests for [FractionParsingUiError]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = FractionParsingUiErrorTest.TestApplication::class, qualifiers = "port-xxhdpi") +class FractionParsingUiErrorTest { + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + var activityRule = + ActivityScenarioRule( + TestActivity.createIntent(ApplicationProvider.getApplicationContext()) + ) + + private lateinit var fractionParser: FractionParser + + @Before + fun setUp() { + setUpTestApplicationComponent() + fractionParser = FractionParser() + } + + @Test + fun testSubmitTimeError_validMixedNumber_noErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("11 22/33") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage).isNull() + } + } + + @Test + fun testSubmitTimeError_tenDigitNumber_numberTooLong_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("0123456789") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("None of the numbers in the fraction should have more than 7 digits.") + } + } + + @Test + fun testSubmitTimeError_nonDigits_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("jdhfc") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testSubmitTimeError_divisionByZero_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("123/0") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage).isEqualTo("Please do not put 0 in the denominator") + } + } + + @Test + fun testSubmitTimeError_ambiguousSpacing_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("1 2 3/4") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testSubmitTimeError_emptyString_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testSubmitTimeError_noDenominator_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("3/") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testRealTimeError_validRegularFraction_noErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("2/3") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage).isNull() + } + } + + @Test + fun testRealTimeError_nonDigits_invalidChars_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("abc") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please only use numerical digits, spaces or forward slashes (/)") + } + } + + @Test + fun testRealTimeError_noNumerator_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("/3") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testRealTimeError_severalSlashes_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("1/3/8") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testRealTimeError_severalDashes_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("-1/-3") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + private companion object { + private fun FractionParsingError.toUiError(): FractionParsingUiError = + FractionParsingUiError.createFromParsingError(this) + } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + @Singleton + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, RobolectricModule::class, + PlatformParameterModule::class, PlatformParameterSingletonModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, + NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(fractionParsingUiErrorTest: FractionParsingUiErrorTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerFractionParsingUiErrorTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(fractionParsingUiErrorTest: FractionParsingUiErrorTest) { + component.inject(fractionParsingUiErrorTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } +} diff --git a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt b/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt deleted file mode 100644 index 4e3f9141027..00000000000 --- a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt +++ /dev/null @@ -1,525 +0,0 @@ -package org.oppia.android.app.parser - -import android.app.Application -import androidx.appcompat.app.AppCompatActivity -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.rules.ActivityScenarioRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import dagger.Component -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.oppia.android.app.activity.ActivityComponent -import org.oppia.android.app.activity.ActivityComponentFactory -import org.oppia.android.app.application.ApplicationComponent -import org.oppia.android.app.application.ApplicationInjector -import org.oppia.android.app.application.ApplicationInjectorProvider -import org.oppia.android.app.application.ApplicationModule -import org.oppia.android.app.application.ApplicationStartupListenerModule -import org.oppia.android.app.devoptions.DeveloperOptionsModule -import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule -import org.oppia.android.app.model.Fraction -import org.oppia.android.app.shim.ViewBindingShimModule -import org.oppia.android.app.testing.activity.TestActivity -import org.oppia.android.app.topic.PracticeTabModule -import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule -import org.oppia.android.data.backends.gae.NetworkConfigProdModule -import org.oppia.android.data.backends.gae.NetworkModule -import org.oppia.android.domain.classify.InteractionsModule -import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule -import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule -import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule -import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule -import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule -import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule -import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule -import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule -import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule -import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule -import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule -import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule -import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule -import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule -import org.oppia.android.domain.oppialogger.LogStorageModule -import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerModule -import org.oppia.android.domain.platformparameter.PlatformParameterModule -import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule -import org.oppia.android.domain.question.QuestionModule -import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule -import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule -import org.oppia.android.testing.TestLogReportingModule -import org.oppia.android.testing.assertThrows -import org.oppia.android.testing.junit.InitializeDefaultLocaleRule -import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.accessibility.AccessibilityTestModule -import org.oppia.android.util.caching.AssetModule -import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.gcsresource.GcsResourceModule -import org.oppia.android.util.locale.LocaleProdModule -import org.oppia.android.util.logging.LoggerModule -import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule -import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule -import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule -import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule -import org.oppia.android.util.parser.image.GlideImageLoaderModule -import org.oppia.android.util.parser.image.ImageParsingModule -import org.robolectric.annotation.Config -import org.robolectric.annotation.LooperMode -import javax.inject.Singleton - -/** Tests for [StringToFractionParser]. */ -@RunWith(AndroidJUnit4::class) -@LooperMode(LooperMode.Mode.PAUSED) -@Config(application = StringToFractionParserTest.TestApplication::class, qualifiers = "port-xxhdpi") -class StringToFractionParserTest { - @get:Rule - val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() - - @get:Rule - var activityRule = - ActivityScenarioRule( - TestActivity.createIntent(ApplicationProvider.getApplicationContext()) - ) - - private lateinit var stringToFractionParser: StringToFractionParser - - @Before - fun setUp() { - setUpTestApplicationComponent() - stringToFractionParser = StringToFractionParser() - } - - @Test - fun testSubmitTimeError_regularFraction_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("1/2") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_regularNegativeFractionWithExtraSpaces_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError(" -1 / 2 ") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_atLengthLimit_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("1234567/1234567") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_wholeNumber_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("888") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_wholeNegativeNumber_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("-777") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_mixedNumber_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("11 22/33") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_validMixedNumber_noErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("11 22/33") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage).isNull() - } - } - - @Test - fun testSubmitTimeError_tenDigitNumber_returnsNumberTooLong() { - val error = stringToFractionParser.getSubmitTimeError("0123456789") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.NUMBER_TOO_LONG) - } - - @Test - fun testSubmitTimeError_tenDigitNumber_numberTooLong_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("0123456789") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("None of the numbers in the fraction should have more than 7 digits.") - } - } - - @Test - fun testSubmitTimeError_nonDigits_returnsInvalidFormat() { - val error = stringToFractionParser.getSubmitTimeError("jdhfc") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testSubmitTimeError_nonDigits_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("jdhfc") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testSubmitTimeError_divisionByZero_returnsDivisionByZero() { - val error = stringToFractionParser.getSubmitTimeError("123/0") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.DIVISION_BY_ZERO) - } - - @Test - fun testSubmitTimeError_divisionByZero_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("123/0") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage).isEqualTo("Please do not put 0 in the denominator") - } - } - - @Test - fun testSubmitTimeError_ambiguousSpacing_returnsInvalidFormat() { - val error = stringToFractionParser.getSubmitTimeError("1 2 3/4") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testSubmitTimeError_ambiguousSpacing_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("1 2 3/4") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testSubmitTimeError_emptyString_returnsInvalidFormat() { - val error = stringToFractionParser.getSubmitTimeError("") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testSubmitTimeError_emptyString_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testSubmitTimeError_noDenominator_returnsInvalidFormat() { - val error = stringToFractionParser.getSubmitTimeError("3/") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testSubmitTimeError_noDenominator_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("3/") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testRealTimeError_regularFraction_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("2/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_regularNegativeFraction_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("-2/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_wholeNumber_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("4") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_wholeNegativeNumber_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("-4") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_mixedNumber_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("5 2/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_mixedNegativeNumber_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("-5 2/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_validRegularFraction_noErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("2/3") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage).isNull() - } - } - - @Test - fun testRealTimeError_nonDigits_returnsInvalidChars() { - val error = stringToFractionParser.getRealTimeAnswerError("abc") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_CHARS) - } - - @Test - fun testRealTimeError_nonDigits_invalidChars_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("abc") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please only use numerical digits, spaces or forward slashes (/)") - } - } - - @Test - fun testRealTimeError_noNumerator_returnsInvalidFormat() { - val error = stringToFractionParser.getRealTimeAnswerError("/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testRealTimeError_noNumerator_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("/3") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testRealTimeError_severalSlashes_invalidFormat_returnsInvalidFormat() { - val error = stringToFractionParser.getRealTimeAnswerError("1/3/8") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testRealTimeError_severalSlashes_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("1/3/8") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testRealTimeError_severalDashes_returnsInvalidFormat() { - val error = stringToFractionParser.getRealTimeAnswerError("-1/-3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testRealTimeError_severalDashes_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("-1/-3") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testParseFraction_divisionByZero_returnsFraction() { - val parseFraction = stringToFractionParser.parseFraction("8/0") - val parseFractionFromString = stringToFractionParser.parseFractionFromString("8/0") - val expectedFraction = Fraction.newBuilder().apply { - numerator = 8 - denominator = 0 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_multipleFractions_failsWithError() { - val parseFraction = stringToFractionParser.parseFraction("7 1/2 4/5") - assertThat(parseFraction).isEqualTo(null) - - val exception = assertThrows(IllegalArgumentException::class) { - stringToFractionParser.parseFractionFromString("7 1/2 4/5") - } - assertThat(exception).hasMessageThat().contains("Incorrectly formatted fraction: 7 1/2 4/5") - } - - @Test - fun testParseFraction_nonDigits_failsWithError() { - val parseFraction = stringToFractionParser.parseFraction("abc") - assertThat(parseFraction).isEqualTo(null) - - val exception = assertThrows(IllegalArgumentException::class) { - stringToFractionParser.parseFractionFromString("abc") - } - assertThat(exception).hasMessageThat().contains("Incorrectly formatted fraction: abc") - } - - @Test - fun testParseFraction_regularFraction_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("1/2") - val parseFraction = stringToFractionParser.parseFraction("1/2") - val expectedFraction = Fraction.newBuilder().apply { - numerator = 1 - denominator = 2 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_regularNegativeFraction_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("-8/4") - val parseFraction = stringToFractionParser.parseFraction("-8/4") - val expectedFraction = Fraction.newBuilder().apply { - isNegative = true - numerator = 8 - denominator = 4 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_wholeNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("7") - val parseFraction = stringToFractionParser.parseFraction("7") - val expectedFraction = Fraction.newBuilder().apply { - wholeNumber = 7 - numerator = 0 - denominator = 1 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_wholeNegativeNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("-7") - val parseFraction = stringToFractionParser.parseFraction("-7") - val expectedFraction = Fraction.newBuilder().apply { - isNegative = true - wholeNumber = 7 - numerator = 0 - denominator = 1 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_mixedNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("1 3/4") - val parseFraction = stringToFractionParser.parseFraction("1 3/4") - val expectedFraction = Fraction.newBuilder().apply { - wholeNumber = 1 - numerator = 3 - denominator = 4 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_negativeMixedNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("-123 456/7") - val parseFraction = stringToFractionParser.parseFraction("-123 456/7") - val expectedFraction = Fraction.newBuilder().apply { - isNegative = true - wholeNumber = 123 - numerator = 456 - denominator = 7 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_longMixedNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser - .parseFractionFromString("1234567 1234567/1234567") - val parseFraction = stringToFractionParser - .parseFraction("1234567 1234567/1234567") - val expectedFraction = Fraction.newBuilder().apply { - wholeNumber = 1234567 - numerator = 1234567 - denominator = 1234567 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - private fun setUpTestApplicationComponent() { - ApplicationProvider.getApplicationContext().inject(this) - } - - // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. - @Singleton - @Component( - modules = [ - TestDispatcherModule::class, ApplicationModule::class, RobolectricModule::class, - PlatformParameterModule::class, PlatformParameterSingletonModule::class, - LoggerModule::class, ContinueModule::class, FractionInputModule::class, - ItemSelectionInputModule::class, MultipleChoiceInputModule::class, - NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, - DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, - GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, - HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, - AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, - PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, - ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class, - ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, - HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, - FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class, - DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, - ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, - NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class - ] - ) - interface TestApplicationComponent : ApplicationComponent { - @Component.Builder - interface Builder : ApplicationComponent.Builder - - fun inject(stringToFractionParserTest: StringToFractionParserTest) - } - - class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { - private val component: TestApplicationComponent by lazy { - DaggerStringToFractionParserTest_TestApplicationComponent.builder() - .setApplication(this) - .build() as TestApplicationComponent - } - - fun inject(stringToFractionParserTest: StringToFractionParserTest) { - component.inject(stringToFractionParserTest) - } - - override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { - return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() - } - - override fun getApplicationInjector(): ApplicationInjector = component - } -} diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 5ec5e4363c9..c834c089e16 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -124,8 +124,10 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/data:data_providers", "//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions", "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", + "//utility/src/main/java/org/oppia/android/util/extensions:string_extensions", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", + "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_util", "//utility/src/main/java/org/oppia/android/util/parser/html:exploration_html_parser_entity_type", "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_annonations", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index 76ed9cb0f72..f9498f7d965 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -6,9 +6,9 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat -import org.oppia.android.domain.util.toSimplestForm +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toDouble +import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject /** @@ -36,6 +36,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat().approximatelyEquals(input.toFloat()) && answer == input.toSimplestForm() + return answer.toDouble().approximatelyEquals(input.toDouble()) && + answer == input.toSimplestForm() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index d7f8c597461..e2c42f7ec67 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -6,8 +6,8 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -34,6 +34,6 @@ class FractionInputIsEquivalentToRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat().approximatelyEquals(input.toFloat()) + return answer.toDouble().approximatelyEquals(input.toDouble()) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt index 740ab078eee..89d83f1e3d6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -33,6 +33,6 @@ class FractionInputIsGreaterThanRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat() > input.toFloat() + return answer.toDouble() > input.toDouble() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt index 3fbf98e8fac..02d4b9766c9 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -33,6 +33,6 @@ class FractionInputIsLessThanRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat() < input.toFloat() + return answer.toDouble() < input.toDouble() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt index e8375af7173..9a225cc41ca 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals +import org.oppia.android.util.math.approximatelyEquals import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index daab9473b4e..e94fc9191e7 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -6,8 +6,8 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -47,7 +47,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( private fun extractRealValue(number: NumberWithUnits): Double { return when (number.numberTypeCase) { NumberWithUnits.NumberTypeCase.REAL -> number.real - NumberWithUnits.NumberTypeCase.FRACTION -> number.fraction.toFloat().toDouble() + NumberWithUnits.NumberTypeCase.FRACTION -> number.fraction.toDouble() else -> throw IllegalArgumentException("Invalid number type: ${number.numberTypeCase.name}") } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt index f5c84525281..2c7a6dc5212 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt @@ -5,7 +5,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals +import org.oppia.android.util.math.approximatelyEquals import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt index 57aba25a07c..f9b9b2e9df8 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toSimplestForm +import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel index 0701b29ff46..a28d873bc5a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel @@ -24,6 +24,7 @@ kt_android_library( "//model/src/main/proto:interaction_object_java_proto_lite", "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", + "//utility/src/main/java/org/oppia/android/util/extensions:string_extensions", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt index 76b65756b5b..7c663c136c0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt index a4b705f05b8..a9086ed0e87 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt index 3c69d469dec..ac17f971a71 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt index a216eabce4c..5862ce68045 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel index 26dc7fb7027..de927b14eaf 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel @@ -21,18 +21,15 @@ kt_android_library( kt_android_library( name = "extensions", srcs = [ - "FloatExtensions.kt", - "FractionExtensions.kt", "InteractionObjectExtensions.kt", "JsonExtensions.kt", - "RatioExtensions.kt", - "StringExtensions.kt", "WorkDataExtensions.kt", ], visibility = ["//domain:__subpackages__"], deps = [ "//model/src/main/proto:question_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", + "//utility/src/main/java/org/oppia/android/util/math:extensions", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt index a4d813c6ec8..2e37e34e0f7 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt @@ -29,6 +29,7 @@ import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds import org.oppia.android.app.model.StringList import org.oppia.android.app.model.TranslatableHtmlContentId import org.oppia.android.app.model.TranslatableSetOfNormalizedString +import org.oppia.android.util.math.toAnswerString /** * Returns a parsable string representation of a user-submitted answer version of this diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt index 1c7f3c2eb96..f7485b13545 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt @@ -11,8 +11,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder -import org.oppia.android.domain.util.FLOAT_EQUALITY_INTERVAL import org.oppia.android.testing.assertThrows +import org.oppia.android.util.math.FLOAT_EQUALITY_INTERVAL import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject diff --git a/domain/src/test/java/org/oppia/android/domain/util/StringExtensionsTest.kt b/domain/src/test/java/org/oppia/android/domain/util/StringExtensionsTest.kt index f345fd1d134..4e42340ba6b 100644 --- a/domain/src/test/java/org/oppia/android/domain/util/StringExtensionsTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/util/StringExtensionsTest.kt @@ -4,6 +4,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.util.extensions.normalizeWhitespace +import org.oppia.android.util.extensions.removeWhitespace import org.robolectric.annotation.LooperMode /** Tests for [StringExtensions]. */ diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel index b38796c5237..6a6f5c3ad47 100644 --- a/model/src/main/proto/BUILD.bazel +++ b/model/src/main/proto/BUILD.bazel @@ -62,6 +62,7 @@ oppia_proto_library( name = "interaction_object_proto", srcs = ["interaction_object.proto"], visibility = ["//:oppia_api_visibility"], + deps = [":math_proto"], ) java_lite_proto_library( @@ -82,6 +83,17 @@ java_lite_proto_library( deps = [":languages_proto"], ) +oppia_proto_library( + name = "math_proto", + srcs = ["math.proto"], +) + +java_lite_proto_library( + name = "math_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":math_proto"], +) + oppia_proto_library( name = "onboarding_proto", srcs = ["onboarding.proto"], diff --git a/model/src/main/proto/interaction_object.proto b/model/src/main/proto/interaction_object.proto index c444f316431..bb6154f9255 100644 --- a/model/src/main/proto/interaction_object.proto +++ b/model/src/main/proto/interaction_object.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package model; +import "math.proto"; + option java_package = "org.oppia.android.app.model"; option java_multiple_files = true; @@ -35,13 +37,6 @@ message StringList { repeated string html = 1; } -// Structure containing a ratio object for eg - [1,2,3] for 1:2:3. -message RatioExpression { - // List of components in a ratio. It's expected that list should have more than - // 1 element. - repeated uint32 ratio_component = 1; -} - // Structure for a number with units object. message NumberWithUnits { oneof number_type { @@ -57,14 +52,6 @@ message NumberUnit { int32 exponent = 2; } -// Structure for a fraction object. -message Fraction { - bool is_negative = 1; - int32 whole_number = 2; - int32 numerator = 3; - int32 denominator = 4; -} - // Structure for a ListOfString object. message ListOfSetsOfHtmlStrings { repeated StringList set_of_html_strings = 1; diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto new file mode 100644 index 00000000000..0288db3148b --- /dev/null +++ b/model/src/main/proto/math.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package model; + +option java_package = "org.oppia.android.app.model"; +option java_multiple_files = true; + +// Structure for a fraction object. +message Fraction { + bool is_negative = 1; + int32 whole_number = 2; + int32 numerator = 3; + int32 denominator = 4; +} + +// Structure containing a ratio object for eg - [1,2,3] for 1:2:3. +message RatioExpression { + // List of components in a ratio. It's expected that list should have more than + // 1 element. + repeated uint32 ratio_component = 1; +} diff --git a/scripts/assets/kdoc_validity_exemptions.textproto b/scripts/assets/kdoc_validity_exemptions.textproto index e3c33bbfc1c..adb016b4ac0 100644 --- a/scripts/assets/kdoc_validity_exemptions.textproto +++ b/scripts/assets/kdoc_validity_exemptions.textproto @@ -161,7 +161,6 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/options/RouteToAudi exempted_file_path: "app/src/main/java/org/oppia/android/app/options/RouteToReadingTextSizeListener.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/TextSizeRadioButtonListener.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 79af5b5aada..9ac351e7d69 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -613,8 +613,6 @@ exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/PrimeTo exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/PrimeTopicAssetsControllerImpl.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/PrimeTopicAssetsControllerModule.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/RevisionCardRetriever.kt" -exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt" -exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/JsonAssetRetriever.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/JsonExtensions.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/WorkDataExtensions.kt" @@ -711,6 +709,8 @@ exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/fireba exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploaderModule.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/LogReportingModule.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/networking/ConnectionStatus.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/networking/NetworkConnectionDebugUtil.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/networking/NetworkConnectionDebugUtilModule.kt" diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index bf0b47e2a26..ea0aac5c42a 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -19,6 +19,7 @@ MIGRATED_PROD_FILES = glob([ "src/main/java/org/oppia/android/util/extensions/*.kt", "src/main/java/org/oppia/android/util/gcsresource/*.kt", "src/main/java/org/oppia/android/util/logging/*.kt", + "src/main/java/org/oppia/android/util/math/**/*.kt", "src/main/java/org/oppia/android/util/networking/*.kt", "src/main/java/org/oppia/android/util/profile/*.kt", "src/main/java/org/oppia/android/util/statusbar/*.kt", diff --git a/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel index 525f5160210..cfc2be6bb6a 100644 --- a/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel @@ -25,3 +25,11 @@ kt_android_library( "//third_party:com_google_protobuf_protobuf-javalite", ], ) + +kt_android_library( + name = "string_extensions", + srcs = [ + "StringExtensions.kt", + ], + visibility = ["//:oppia_api_visibility"], +) diff --git a/domain/src/main/java/org/oppia/android/domain/util/StringExtensions.kt b/utility/src/main/java/org/oppia/android/util/extensions/StringExtensions.kt similarity index 92% rename from domain/src/main/java/org/oppia/android/domain/util/StringExtensions.kt rename to utility/src/main/java/org/oppia/android/util/extensions/StringExtensions.kt index 0dbdc30fc86..5e48805739d 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/StringExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/extensions/StringExtensions.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.extensions /** * Normalizes whitespace in the specified string in a way consistent with Oppia web: diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel new file mode 100644 index 00000000000..cae9779bc08 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,32 @@ +""" +General-purpose mathematics utilities, especially for supporting math-based interactions. +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "extensions", + srcs = [ + "FloatExtensions.kt", + "FractionExtensions.kt", + "RatioExtensions.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "fraction_parser", + srcs = ["FractionParser.kt"], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//utility/src/main/java/org/oppia/android/util/extensions:string_extensions", + ], +) diff --git a/domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt similarity index 86% rename from domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 5ce8d4b0c10..62504046c78 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -1,9 +1,9 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import kotlin.math.abs /** The error margin used for float equality by [Float.approximatelyEquals]. */ -public const val FLOAT_EQUALITY_INTERVAL = 1e-5 +const val FLOAT_EQUALITY_INTERVAL = 1e-5 /** Returns whether this float approximately equals another based on a consistent epsilon value. */ fun Float.approximatelyEquals(other: Float): Boolean { diff --git a/domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt similarity index 55% rename from domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 878576da012..d4a57faf9be 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,16 +1,16 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import org.oppia.android.app.model.Fraction /** - * Returns a float version of this fraction. + * Returns a [Double] version of this fraction. * * See: https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L73. */ -fun Fraction.toFloat(): Float { - val totalParts = ((wholeNumber * denominator) + numerator).toFloat() - val floatVal = totalParts / denominator.toFloat() - return if (isNegative) -floatVal else floatVal +fun Fraction.toDouble(): Double { + val totalParts = ((wholeNumber.toDouble() * denominator.toDouble()) + numerator.toDouble()) + val doubleVal = totalParts / denominator.toDouble() + return if (isNegative) -doubleVal else doubleVal } /** @@ -20,8 +20,10 @@ fun Fraction.toFloat(): Float { */ fun Fraction.toSimplestForm(): Fraction { val commonDenominator = gcd(numerator, denominator) - return toBuilder().setNumerator(numerator / commonDenominator) - .setDenominator(denominator / commonDenominator).build() + return toBuilder().apply { + numerator = this@toSimplestForm.numerator / commonDenominator + denominator = this@toSimplestForm.denominator / commonDenominator + }.build() } /** Returns the greatest common divisor between two integers. */ diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt b/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt similarity index 81% rename from app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt rename to utility/src/main/java/org/oppia/android/util/math/FractionParser.kt index 8a11120cacb..7e2288cda95 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt @@ -1,13 +1,10 @@ -package org.oppia.android.app.parser +package org.oppia.android.util.math -import androidx.annotation.StringRes -import org.oppia.android.R import org.oppia.android.app.model.Fraction -import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace -/** This class contains method that helps to parse string to fraction. */ -class StringToFractionParser { +/** String parser for [Fraction]s. */ +class FractionParser { private val wholeNumberOnlyRegex = """^-? ?(\d+)$""".toRegex() private val fractionOnlyRegex = @@ -112,18 +109,27 @@ class StringToFractionParser { private fun isInputNegative(inputText: String): Boolean = inputText.startsWith("-") - /** Enum to store the errors of [FractionInputInteractionView]. */ - enum class FractionParsingError(@StringRes private var error: Int?) { - VALID(error = null), - INVALID_CHARS(error = R.string.fraction_error_invalid_chars), - INVALID_FORMAT(error = R.string.fraction_error_invalid_format), - DIVISION_BY_ZERO(error = R.string.fraction_error_divide_by_zero), - NUMBER_TOO_LONG(error = R.string.fraction_error_larger_than_seven_digits); + /** Represents errors that can occur when parsing a fraction from a string. */ + enum class FractionParsingError { + /** Indicates that the considered string is a valid fraction. */ + VALID, + + /** Indicates that the string contains characters not found in fractions. */ + INVALID_CHARS, + + /** Indicates that the string does not resemble a fraction. */ + INVALID_FORMAT, + + /** + * Indicates that the string includes a zero denominator which would result in a division by + * zero. + */ + DIVISION_BY_ZERO, /** - * Returns the string corresponding to this error's string resources, or null if there is none. + * Indicates that at least one of the numbers present in the string is too long to be + * precisely represented in a fraction. */ - fun getErrorMessageFromStringRes(resourceHandler: AppLanguageResourceHandler): String? = - error?.let(resourceHandler::getStringInLocale) + NUMBER_TOO_LONG } } diff --git a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt similarity index 94% rename from domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 821fa274e31..83d85e9098c 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import org.oppia.android.app.model.RatioExpression @@ -13,6 +13,7 @@ fun RatioExpression.toSimplestForm(): List { this.ratioComponentList.map { x -> x / gcdComponentResult } } } + /** * Returns this Ratio in string format. * E.g. [1, 2, 3] will yield to 1:2:3 diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel new file mode 100644 index 00000000000..6fb6fb73482 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,40 @@ +""" +Tests for general-purpose mathematics utilities. +""" + +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "FractionParserTest", + srcs = ["FractionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.FractionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", + ], +) + +oppia_android_test( + name = "RatioExtensionsTest", + srcs = ["RatioExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.RatioExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) diff --git a/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt new file mode 100644 index 00000000000..864a429f753 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt @@ -0,0 +1,286 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.Fraction +import org.oppia.android.testing.assertThrows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [FractionParser]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config +class FractionParserTest { + private lateinit var fractionParser: FractionParser + + @Before + fun setUp() { + fractionParser = FractionParser() + } + + @Test + fun testSubmitTimeError_regularFraction_returnsValid() { + val error = fractionParser.getSubmitTimeError("1/2") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_regularNegativeFractionWithExtraSpaces_returnsValid() { + val error = fractionParser.getSubmitTimeError(" -1 / 2 ") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_atLengthLimit_returnsValid() { + val error = fractionParser.getSubmitTimeError("1234567/1234567") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_wholeNumber_returnsValid() { + val error = fractionParser.getSubmitTimeError("888") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_wholeNegativeNumber_returnsValid() { + val error = fractionParser.getSubmitTimeError("-777") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_mixedNumber_returnsValid() { + val error = fractionParser.getSubmitTimeError("11 22/33") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_tenDigitNumber_returnsNumberTooLong() { + val error = fractionParser.getSubmitTimeError("0123456789") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.NUMBER_TOO_LONG) + } + + @Test + fun testSubmitTimeError_nonDigits_returnsInvalidFormat() { + val error = fractionParser.getSubmitTimeError("jdhfc") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testSubmitTimeError_divisionByZero_returnsDivisionByZero() { + val error = fractionParser.getSubmitTimeError("123/0") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.DIVISION_BY_ZERO) + } + + @Test + fun testSubmitTimeError_ambiguousSpacing_returnsInvalidFormat() { + val error = fractionParser.getSubmitTimeError("1 2 3/4") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testSubmitTimeError_emptyString_returnsInvalidFormat() { + val error = fractionParser.getSubmitTimeError("") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testSubmitTimeError_noDenominator_returnsInvalidFormat() { + val error = fractionParser.getSubmitTimeError("3/") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testRealTimeError_regularFraction_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("2/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_regularNegativeFraction_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("-2/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_wholeNumber_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("4") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_wholeNegativeNumber_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("-4") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_mixedNumber_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("5 2/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_mixedNegativeNumber_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("-5 2/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_nonDigits_returnsInvalidChars() { + val error = fractionParser.getRealTimeAnswerError("abc") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_CHARS) + } + + @Test + fun testRealTimeError_noNumerator_returnsInvalidFormat() { + val error = fractionParser.getRealTimeAnswerError("/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testRealTimeError_severalSlashes_invalidFormat_returnsInvalidFormat() { + val error = fractionParser.getRealTimeAnswerError("1/3/8") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testRealTimeError_severalDashes_returnsInvalidFormat() { + val error = fractionParser.getRealTimeAnswerError("-1/-3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testParseFraction_divisionByZero_returnsFraction() { + val parseFraction = fractionParser.parseFraction("8/0") + val parseFractionFromString = fractionParser.parseFractionFromString("8/0") + val expectedFraction = Fraction.newBuilder().apply { + numerator = 8 + denominator = 0 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_multipleFractions_failsWithError() { + val parseFraction = fractionParser.parseFraction("7 1/2 4/5") + assertThat(parseFraction).isEqualTo(null) + + val exception = assertThrows(IllegalArgumentException::class) { + fractionParser.parseFractionFromString("7 1/2 4/5") + } + assertThat(exception).hasMessageThat().contains("Incorrectly formatted fraction: 7 1/2 4/5") + } + + @Test + fun testParseFraction_nonDigits_failsWithError() { + val parseFraction = fractionParser.parseFraction("abc") + assertThat(parseFraction).isEqualTo(null) + + val exception = assertThrows(IllegalArgumentException::class) { + fractionParser.parseFractionFromString("abc") + } + assertThat(exception).hasMessageThat().contains("Incorrectly formatted fraction: abc") + } + + @Test + fun testParseFraction_regularFraction_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("1/2") + val parseFraction = fractionParser.parseFraction("1/2") + val expectedFraction = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_regularNegativeFraction_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("-8/4") + val parseFraction = fractionParser.parseFraction("-8/4") + val expectedFraction = Fraction.newBuilder().apply { + isNegative = true + numerator = 8 + denominator = 4 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_wholeNumber_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("7") + val parseFraction = fractionParser.parseFraction("7") + val expectedFraction = Fraction.newBuilder().apply { + wholeNumber = 7 + numerator = 0 + denominator = 1 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_wholeNegativeNumber_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("-7") + val parseFraction = fractionParser.parseFraction("-7") + val expectedFraction = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 7 + numerator = 0 + denominator = 1 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_mixedNumber_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("1 3/4") + val parseFraction = fractionParser.parseFraction("1 3/4") + val expectedFraction = Fraction.newBuilder().apply { + wholeNumber = 1 + numerator = 3 + denominator = 4 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_negativeMixedNumber_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("-123 456/7") + val parseFraction = fractionParser.parseFraction("-123 456/7") + val expectedFraction = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 123 + numerator = 456 + denominator = 7 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_longMixedNumber_returnsFraction() { + val parseFractionFromString = fractionParser + .parseFractionFromString("1234567 1234567/1234567") + val parseFraction = fractionParser + .parseFraction("1234567 1234567/1234567") + val expectedFraction = Fraction.newBuilder().apply { + wholeNumber = 1234567 + numerator = 1234567 + denominator = 1234567 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/util/RatioExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt similarity index 97% rename from domain/src/test/java/org/oppia/android/domain/util/RatioExtensionsTest.kt rename to utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt index dffb8e65b2f..ca380220b0b 100644 --- a/domain/src/test/java/org/oppia/android/domain/util/RatioExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat